I'm having trouble testing an Angular component that utilizes the two way [(ngModel)]
binding on checkbox inputs inside an ngFor
. It works just fine in the actual app. This is just a problem with the test.
Here's an example test that fails:
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { Component, EventEmitter, Output } from '@angular/core';
import { FormsModule } from '@angular/forms';
describe('Example Test', () => {
@Component({
template: `
<input *ngFor="let value of values"
type="checkbox"
class="checkbox-1"
[(ngModel)]="value.isSelected"
(change)="output.emit(values)">
`,
styles: [``]
})
class TestHostComponent {
@Output() output: EventEmitter<any> = new EventEmitter();
values = [
{ isSelected: true },
{ isSelected: true },
{ isSelected: true },
];
}
let testHost: TestHostComponent;
let fixture: ComponentFixture<TestHostComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [TestHostComponent],
providers: []
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
fixture.detectChanges();
});
it('should change isSelected', fakeAsync(() => {
const spy = spyOn(testHost.output, 'emit');
fixture.nativeElement.querySelectorAll('.checkbox-1')[0].click();
fixture.detectChanges();
tick();
expect(spy).toHaveBeenCalledWith([
{ isSelected: false }, // it fails because this is still true
{ isSelected: true },
{ isSelected: true },
]);
}));
});
Using [(ngModel)]
with a single input that's not in a loop works fine in a similar test. I've even logged the emitted value from (ngModelChange)
and when the checkbox is clicked $event
is true
when it should really be false
.
Any ideas?
It seems like that method of performing a click isn't triggering change detection. Dispatching a change event on the checkbox instead gave the expected result:
it('should change isSelected', fakeAsync(() => {
const spy = spyOn(testHost.output, 'emit');
const checkbox = fixture.nativeElement.querySelectorAll('.checkbox-1')[0];
checkbox.dispatchEvent(new Event('change'));
fixture.detectChanges();
tick();
expect(spy).toHaveBeenCalledWith([
{ isSelected: false }, // This is now false
{ isSelected: true },
{ isSelected: true },
]);
}));
Solution inspired by this post.
UPDATE:
It looks like there's a need to wait for some controls to be initialized or registered on the CheckboxControlValueAccessor. If you modify the second beforeEach()
to wait for one cycle after creating the component, the original test code works:
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
fixture.detectChanges();
tick();
}));
See this Github issue for the full answer/explanation.
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