I am implementing a wizard component in Angular 2 RC4, and now I am trying to write som unit tests. Unit testing in Angular 2 is starting to get well documented, but I simply cannot find out how to mock the result of a content query in the component.
The app has 2 components (in addition to the app component), WizardComponent and WizardStepComponent. The app component (app.ts) defines the wizard and the steps in its template:
<div>
<fa-wizard>
<fa-wizard-step stepTitle="First step">step 1 content</fa-wizard-step>
<fa-wizard-step stepTitle="Second step">step 2 content</fa-wizard-step>
<fa-wizard-step stepTitle="Third step">step 3 content</fa-wizard-step>
</fa-wizard>
</div>
The WizardComponent (wizard-component.ts) gets a reference to the steps by using a ContentChildren query.
@Component({
selector: 'fa-wizard',
template: `<div *ngFor="let step of steps">
<ng-content></ng-content>
</div>
<div><button (click)="cycleSteps()">Cycle steps</button></div>`
})
export class WizardComponent implements AfterContentInit {
@ContentChildren(WizardStepComponent) steps: QueryList<WizardStepComponent>;
....
}
The problem is how to mock the steps variable in the unit test:
describe('Wizard component', () => {
it('should set first step active on init', async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb
.createAsync(WizardComponent)
.then( (fixture) =>{
let nativeElement = fixture.nativeElement;
let testComponent: WizardComponent = fixture.componentInstance;
//how to initialize testComponent.steps with mock data?
fixture.detectChanges();
expect(fixture.componentInstance.steps[0].active).toBe(true);
});
})));
});
I have created a plunker implementing a very simple wizard demonstrating the problem. The wizard-component.spec.ts file contains the unit test.
If anyone can point me in the right direction, I would greatly appreciate it.
Thanks to drewmoore's answer in this question, I have been able to get this working.
The key is to create a wrapper component for testing, which specifies the wizard and the wizard steps in it's template. Angular will then do the content query for you and populate the variable.
Edit: Implementation is for Angular 6.0.0-beta.3
My full test implementation looks like this:
//We need to wrap the WizardComponent in this component when testing, to have the wizard steps initialized
@Component({
selector: 'test-cmp',
template: `<fa-wizard>
<fa-wizard-step stepTitle="step1"></fa-wizard-step>
<fa-wizard-step stepTitle="step2"></fa-wizard-step>
</fa-wizard>`,
})
class TestWrapperComponent { }
describe('Wizard component', () => {
let component: WizardComponent;
let fixture: ComponentFixture<TestWrapperComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
schemas: [ NO_ERRORS_SCHEMA ],
declarations: [
TestWrapperComponent,
WizardComponent,
WizardStepComponent
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TestWrapperComponent);
component = fixture.debugElement.children[0].componentInstance;
});
it('should set first step active on init', () => {
expect(component.steps[0].active).toBe(true);
expect(component.steps.length).toBe(3);
});
});
If you have better/other solutions, you are very welcome to add you answer as well. I'll leave the question open for some time.
For anybody coming to this question recently, things have changed slightly and there is a different way to do this, which I find a bit easier. It is different because it uses a template reference and @ViewChild
to access the component under test rather than fixture.debugElement.children[0].componentInstance
. Also, the syntax has changed.
Let's say we have a select component that requires an option template to be passed in. And we want to test that our ngAfterContentInit
method throws an error if that option template is not provided.
Here is a minimal version of that component:
@Component({
selector: 'my-select',
template: `
<div>
<ng-template
*ngFor="let option of options"
[ngTemplateOutlet]="optionTemplate"
[ngOutletContext]="{$implicit: option}">
</ng-template>
</div>
`
})
export class MySelectComponent<T> implements AfterContentInit {
@Input() options: T[];
@ContentChild('option') optionTemplate: TemplateRef<any>;
ngAfterContentInit() {
if (!this.optionTemplate) {
throw new Error('Missing option template!');
}
}
}
First, create a WrapperComponent
which contains the component under test, like so:
@Component({
template: `
<my-select [options]="[1, 2, 3]">
<ng-template #option let-number>
<p>{{ number }}</p>
</ng-template>
</my-select>
`
})
class WrapperComponent {
@ViewChild(MySelectComponent) mySelect: MySelectComponent<number>;
}
Note the use of the @ViewChild
decorator in the test component. That gives access to MySelectComponent
by name as a property on the TestComponent
class. Then in the test setup, declare both the TestComponent
and the MySelectComponent
.
describe('MySelectComponent', () => {
let component: MySelectComponent<number>;
let fixture: ComponentFixture<WrapperComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
/*
Declare both the TestComponent and the component you want to
test.
*/
declarations: [
TestComponent,
MySelectComponent
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(WrapperComponent);
/*
Access the component you really want to test via the
ElementRef property on the WrapperComponent.
*/
component = fixture.componentInstance.mySelect;
});
/*
Then test the component as normal.
*/
describe('ngAfterContentInit', () => {
component.optionTemplate = undefined;
expect(() => component.ngAfterContentInit())
.toThrowError('Missing option template!');
});
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With