0

So, I'm trying to write a unit test for my component. My component is a dynamically created list, and when a user clicks certain items, I'd like my test to validate that the component generates outputs correctly.

To test this, I wrote a demo component that creates my component within its template, and then attempted to get TestComponentBuilder to create this demo component and click items on the lists. Unfortunately, my unit test cases can't find my component's html elements to click them. I can find the demo component's elements, but it seems like the test is having trouble accessing my nested component.

My question is: Can I get this approach to work? Or is there a better way to unit test my component's inputs and outputs?

Any help would be appreciated. Here's what I'm doing:

<== UPDATED per Gunter's Answer ==>

CustomListComponent.ts (My component):

import {Component, EventEmitter} from "angular2/core";
import {CustomListItem} from "./CustomListItem";

@Component({
    selector: 'custom-list-component',
    inputs: ['itemClass', 'items'],
    outputs: ['onChange'],
    template: `
    <ul class="list-unstyled">
      <li *ngFor="#item of items" [hidden]="item.hidden">
        <div [class]="itemClass" (click)="onItemClicked(item)">
            {{item.text}}
        </div>
      </li>
    </ul>
    `
})
export class CustomListComponent {
    itemClass: String;
    items: CustomListItem[];
    onChange: EventEmitter<CustomListItem>;

    constructor() {
        this.items = new Array<CustomListItem>();
        this.onChange = new EventEmitter();
    }

    onItemClicked(item: CustomListItem): void {
        let clone = new CustomListItem(item.text, item.hidden);
        this.onChange.emit(clone);
    }
}

CustomListItem.ts:

export class CustomListItem {
    text: String;
    hidden: boolean;

    constructor(text: String, hidden: boolean) {
        this.text = text;
        this.hidden = hidden;
    }
}

CustomListSpec.ts (Here's where I'm struggling):

import {CustomListComponent} from './CustomListComponent';
import {ComponentFixture, describe, expect, fakeAsync, inject, injectAsync, it, TestComponentBuilder} from 'angular2/testing'
import {Component} from "angular2/core";
import {CustomListItem} from "./CustomListItem";
import {By} from "angular2/src/platform/dom/debug/by";

describe('CustomListComponent', () => {
    var el;
    var dropdownToggleBtn, dropdownListEl;
    var regularListEl;

    it('Event output properly when list item clicked', injectAsync([TestComponentBuilder], (tcb) => {
        return createComponent(tcb).then((fixture) => {
            console.log("el (" + el + ")"); // => [object HTMLDivElement]
            console.log("dropdownToggleBtn (" + dropdownToggleBtn + ")"); // => [object HTMLButtonElement]
            console.log("dropdownListContainer(" + dropdownListContainer + ")"); // => [object HTMLDivElement]
            console.log("dropdownListEl(" + dropdownListEl + ")"); // => [object HTMLUListElement]
            //console.log("regularListEl (" + regularListEl+ ")");

            //...further testing...
        });
    }));

    function createComponent(tcb: TestComponentBuilder): Promise<ComponentFixture> {
        return tcb.createAsync(CustomListComponentDemo).then((fixture) => {
            fixture.detectChanges();

            el = fixture.debugElement.nativeElement;
            dropdownToggleBtn = fixture.debugElement.query(By.css(".btn")).nativeElement;
            dropdownListContainer = fixture.debugElement.query(By.css(".dropdown-menu")).nativeElement;
            dropdownListEl = dropdownListContainer.children[0].children[0];
            //regularListEl = fixture.debugElement.query(By.css(".list-unstyled")); //TIMES OUT unfortunately. Maybe because there are 2.

            return fixture;
        })
    }
});

@Component({
    directives: [CustomListComponent],
    template: `
        <div class="btn-group" dropdown>
            <button id="dropdown-toggle-id" type="button" class="btn btn-light-gray" dropdownToggle>
                <i class="glyphicon icon-recent_activity dark-green"></i> Dropdown <span class="caret"></span>
            </button>
            <div class="dropdown-menu" role="menu" aria-labelledby="dropdown-toggle-id">
            <custom-list-component id="dropdown-list-id"
                 [items]="dropdownListItems" [itemClass]="'dropdown-item'"
                 (onChange)="onDropdownListChange($event)">
            </custom-list-component>
        </div>
        <span class="divider">&nbsp;</span>
    </div>
    <custom-list-component id="regular-list-id"
        [items]="regularListItems"
        (onChange)="onRegularListChange($event)">
    </custom-list-component>
    `
})
class CustomListComponentDemo {
    dropdownListItems: CustomListItem[];
    regularListItems: CustomListItem[];

    constructor() {
        //intialize the item lists
    }

    onDropdownListChange(item: CustomListItem): void {
        //print something to the console logs
    }

    onRegularListChange(item: CustomListItem): void {
        //print something to the console logs
    }
}
1
  • Updated my example per Gunter's answer - moved fixture.detectChanges() to be before my queries. That worked! (After I remembered to add my CustomListComponent to the directives of the Demo comonent). Unfortunately I'm still having trouble querying for the CustomListComponent's elements. Commented Mar 22, 2016 at 19:03

1 Answer 1

1

Call detectChanges() before you query dynamically created elements. They are not created before change detection has been run.

function createComponent(tcb: TestComponentBuilder): Promise<ComponentFixture> {
    return tcb.createAsync(CustomListComponentDemo).then((fixture) => {
        fixture.detectChanges();
        el = fixture.debugElement.nativeElement;
        regularListEl = fixture.debugElement.query(By.css(".list-unstyled"));
        dropdownListEl = fixture.debugElement.query(By.css(".dropdown-menu")).nativeElement;
        dropdownToggleBtn = fixture.debugElement.query(By.css(".btn")).nativeElement;

        return fixture;
    })
}
Sign up to request clarification or add additional context in comments.

7 Comments

Sorry for the delayed response (I've been out for a long weekend), and thank you, but that didn't work for me either. Should I be calling some other async function like tick() to give everything time to load?
The .list-unstyled doesn't depend on data. I assumed it's an item generated by ngFor. The problem is that the elements you could successfully query are in the template of the test component, but .list-unstyled is in the template of a child component. I'll have to check in a concrete running example locally...
Ooh, maybe it's because I'm not injecting my component? Perhaps I need to add CustomListComponent to the injectAsync list?
You somehow need to navigate to a child debugElement. debugElement.children.... I have don't it in my own tests. I tried to build a simple project with your code to be able to provide concrete instructions but I run into a weird problem and wasn't yet able to make it work locally.
I don't have a TypeScript environment running because I only use Dart. This part works a bit different currently in Dart therefore I can't give exact instructions. You should get it working by querying in two steps, first the custom-list-component, then again query on the debugElement you get for custom-list-component.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.