Skip to content

Commit e00b16f

Browse files
committed
refactor(checkbox): switch to fakeAsync tests
Moves all of the checkbox tests away from the `async` zone for improved reliability.
1 parent 6e865b7 commit e00b16f

File tree

1 file changed

+73
-56
lines changed

1 file changed

+73
-56
lines changed

src/lib/checkbox/checkbox.spec.ts

Lines changed: 73 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,17 @@
1-
import {
2-
async,
3-
ComponentFixture,
4-
fakeAsync,
5-
flushMicrotasks,
6-
TestBed,
7-
tick,
8-
} from '@angular/core/testing';
1+
import {ComponentFixture, fakeAsync, TestBed, tick, flush} from '@angular/core/testing';
92
import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms';
103
import {Component, DebugElement} from '@angular/core';
114
import {By} from '@angular/platform-browser';
125
import {dispatchFakeEvent} from '@angular/cdk/testing';
136
import {MatCheckbox, MatCheckboxChange, MatCheckboxModule} from './index';
147
import {RIPPLE_FADE_IN_DURATION, RIPPLE_FADE_OUT_DURATION} from '@angular/material/core';
8+
import {MutationObserverFactory} from '@angular/cdk/observers';
159

1610

1711
describe('MatCheckbox', () => {
1812
let fixture: ComponentFixture<any>;
1913

20-
beforeEach(async(() => {
14+
beforeEach(fakeAsync(() => {
2115
TestBed.configureTestingModule({
2216
imports: [MatCheckboxModule, FormsModule, ReactiveFormsModule],
2317
declarations: [
@@ -115,7 +109,7 @@ describe('MatCheckbox', () => {
115109
fixture.detectChanges();
116110

117111
// Flush the microtasks because the forms module updates the model state asynchronously.
118-
flushMicrotasks();
112+
flush();
119113

120114
// The checked property has been updated from the model and now the view needs
121115
// to reflect the state change.
@@ -140,7 +134,7 @@ describe('MatCheckbox', () => {
140134
fixture.detectChanges();
141135

142136
// Flush the microtasks because the forms module updates the model state asynchronously.
143-
flushMicrotasks();
137+
flush();
144138

145139
// The checked property has been updated from the model and now the view needs
146140
// to reflect the state change.
@@ -152,7 +146,7 @@ describe('MatCheckbox', () => {
152146
expect(testComponent.isIndeterminate).toBe(false);
153147
}));
154148

155-
it('should not set indeterminate to false when checked is set programmatically', async(() => {
149+
it('should not set indeterminate to false when checked is set programmatically', () => {
156150
testComponent.isIndeterminate = true;
157151
fixture.detectChanges();
158152

@@ -175,7 +169,7 @@ describe('MatCheckbox', () => {
175169
expect(inputElement.indeterminate).toBe(true);
176170
expect(inputElement.checked).toBe(false);
177171
expect(testComponent.isIndeterminate).toBe(true);
178-
}));
172+
});
179173

180174
it('should change native element checked when check programmatically', () => {
181175
expect(inputElement.checked).toBe(false);
@@ -211,7 +205,7 @@ describe('MatCheckbox', () => {
211205
checkboxInstance._onInputClick(<Event>{stopPropagation: () => {}});
212206

213207
// Flush the microtasks because the indeterminate state will be updated in the next tick.
214-
flushMicrotasks();
208+
flush();
215209

216210
expect(checkboxInstance.checked).toBe(true);
217211
expect(checkboxInstance.indeterminate).toBe(false);
@@ -261,7 +255,7 @@ describe('MatCheckbox', () => {
261255
fixture.detectChanges();
262256

263257
// Flush the microtasks because the indeterminate state will be updated in the next tick.
264-
flushMicrotasks();
258+
flush();
265259

266260
expect(checkboxInstance.checked).toBe(true);
267261
expect(checkboxInstance.indeterminate).toBe(false);
@@ -316,7 +310,7 @@ describe('MatCheckbox', () => {
316310
expect(testComponent.onCheckboxClick).toHaveBeenCalledTimes(1);
317311
});
318312

319-
it('should trigger a change event when the native input does', async(() => {
313+
it('should trigger a change event when the native input does', fakeAsync(() => {
320314
spyOn(testComponent, 'onCheckboxChange');
321315

322316
expect(inputElement.checked).toBe(false);
@@ -328,16 +322,15 @@ describe('MatCheckbox', () => {
328322
expect(inputElement.checked).toBe(true);
329323
expect(checkboxNativeElement.classList).toContain('mat-checkbox-checked');
330324

331-
// Wait for the fixture to become stable, because the EventEmitter for the change event,
332-
// will only fire after the zone async change detection has finished.
333-
fixture.whenStable().then(() => {
334-
// The change event shouldn't fire, because the value change was not caused
335-
// by any interaction.
336-
expect(testComponent.onCheckboxChange).toHaveBeenCalledTimes(1);
337-
});
325+
fixture.detectChanges();
326+
flush();
327+
328+
// The change event shouldn't fire, because the value change was not caused
329+
// by any interaction.
330+
expect(testComponent.onCheckboxChange).toHaveBeenCalledTimes(1);
338331
}));
339332

340-
it('should not trigger the change event by changing the native value', async(() => {
333+
it('should not trigger the change event by changing the native value', fakeAsync(() => {
341334
spyOn(testComponent, 'onCheckboxChange');
342335

343336
expect(inputElement.checked).toBe(false);
@@ -349,14 +342,12 @@ describe('MatCheckbox', () => {
349342
expect(inputElement.checked).toBe(true);
350343
expect(checkboxNativeElement.classList).toContain('mat-checkbox-checked');
351344

352-
// Wait for the fixture to become stable, because the EventEmitter for the change event,
353-
// will only fire after the zone async change detection has finished.
354-
fixture.whenStable().then(() => {
355-
// The change event shouldn't fire, because the value change was not caused
356-
// by any interaction.
357-
expect(testComponent.onCheckboxChange).not.toHaveBeenCalled();
358-
});
345+
fixture.detectChanges();
346+
flush();
359347

348+
// The change event shouldn't fire, because the value change was not caused
349+
// by any interaction.
350+
expect(testComponent.onCheckboxChange).not.toHaveBeenCalled();
360351
}));
361352

362353
it('should forward the required attribute', () => {
@@ -582,22 +573,20 @@ describe('MatCheckbox', () => {
582573
expect(changeSpy).toHaveBeenCalledTimes(1);
583574
});
584575

585-
it('should not emit a DOM event to the change output', async(() => {
576+
it('should not emit a DOM event to the change output', fakeAsync(() => {
586577
fixture.detectChanges();
587578
expect(testComponent.lastEvent).toBeUndefined();
588579

589580
// Trigger the click on the inputElement, because the input will probably
590581
// emit a DOM event to the change output.
591582
inputElement.click();
592583
fixture.detectChanges();
584+
flush();
593585

594-
fixture.whenStable().then(() => {
595-
// We're checking the arguments type / emitted value to be a boolean, because sometimes the
596-
// emitted value can be a DOM Event, which is not valid.
597-
// See angular/angular#4059
598-
expect(testComponent.lastEvent.checked).toBe(true);
599-
});
600-
586+
// We're checking the arguments type / emitted value to be a boolean, because sometimes the
587+
// emitted value can be a DOM Event, which is not valid.
588+
// See angular/angular#4059
589+
expect(testComponent.lastEvent.checked).toBe(true);
601590
}));
602591
});
603592

@@ -682,7 +671,7 @@ describe('MatCheckbox', () => {
682671

683672
describe('with native tabindex attribute', () => {
684673

685-
it('should properly detect native tabindex attribute', async(() => {
674+
it('should properly detect native tabindex attribute', fakeAsync(() => {
686675
fixture = TestBed.createComponent(CheckboxWithTabindexAttr);
687676
fixture.detectChanges();
688677

@@ -728,7 +717,7 @@ describe('MatCheckbox', () => {
728717
});
729718

730719
it('should be in pristine, untouched, and valid states initially', fakeAsync(() => {
731-
flushMicrotasks();
720+
flush();
732721

733722
let checkboxElement = fixture.debugElement.query(By.directive(MatCheckbox));
734723
let ngModel = checkboxElement.injector.get<NgModel>(NgModel);
@@ -861,35 +850,63 @@ describe('MatCheckbox', () => {
861850
.toContain('mat-checkbox-inner-container-no-side-margin');
862851
});
863852

864-
it('should not remove margin if initial label is set through binding', async(() => {
853+
it('should not remove margin if initial label is set through binding', () => {
865854
testComponent.label = 'Some content';
866855
fixture.detectChanges();
867856

868857
expect(checkboxInnerContainer.classList)
869858
.not.toContain('mat-checkbox-inner-container-no-side-margin');
870-
}));
859+
});
860+
861+
it('should re-add margin if label is added asynchronously', () => {
862+
fixture.destroy();
863+
864+
const mutationCallbacks: Function[] = [];
865+
866+
TestBed
867+
.resetTestingModule()
868+
.configureTestingModule({
869+
imports: [MatCheckboxModule, FormsModule, ReactiveFormsModule],
870+
declarations: [CheckboxWithoutLabel],
871+
providers: [{
872+
provide: MutationObserverFactory,
873+
useValue: {
874+
// Stub out the factory that creates mutation observers for the underlying directive
875+
// to allows us to flush out the callbacks asynchronously.
876+
create: (callback: Function) => {
877+
mutationCallbacks.push(callback);
878+
879+
return {
880+
observe: () => {},
881+
disconnect: () => {}
882+
};
883+
}
884+
}
885+
}]
886+
})
887+
.compileComponents();
888+
889+
fixture = TestBed.createComponent(CheckboxWithoutLabel);
890+
checkboxInnerContainer = fixture.debugElement
891+
.query(By.css('.mat-checkbox-inner-container')).nativeElement;
871892

872-
it('should re-add margin if label is added asynchronously', async(() => {
873893
fixture.detectChanges();
874894

875895
expect(checkboxInnerContainer.classList)
876896
.toContain('mat-checkbox-inner-container-no-side-margin');
877897

878-
testComponent.label = 'Some content';
898+
fixture.componentInstance.label = 'Some content';
879899
fixture.detectChanges();
900+
mutationCallbacks.forEach(callback => callback());
880901

881-
// Wait for the MutationObserver to detect the content change and for the cdkObserveContent
882-
// to emit the change event to the checkbox.
883-
setTimeout(() => {
884-
// The MutationObserver from the cdkObserveContent directive detected the content change
885-
// and notified the checkbox component. The checkbox then marks the component as dirty
886-
// by calling `markForCheck()`. This needs to be reflected by the component template then.
887-
fixture.detectChanges();
902+
// The MutationObserver from the cdkObserveContent directive detected the content change
903+
// and notified the checkbox component. The checkbox then marks the component as dirty
904+
// by calling `markForCheck()`. This needs to be reflected by the component template then.
905+
fixture.detectChanges();
888906

889-
expect(checkboxInnerContainer.classList)
890-
.not.toContain('mat-checkbox-inner-container-no-side-margin');
891-
}, 1);
892-
}));
907+
expect(checkboxInnerContainer.classList)
908+
.not.toContain('mat-checkbox-inner-container-no-side-margin');
909+
});
893910

894911
it('should not add the "name" attribute if it is not passed in', () => {
895912
fixture.detectChanges();

0 commit comments

Comments
 (0)