diff --git a/packages/forms/test/reactive_integration_spec.ts b/packages/forms/test/reactive_integration_spec.ts index 0253dfc0c5e..c465a37c596 100644 --- a/packages/forms/test/reactive_integration_spec.ts +++ b/packages/forms/test/reactive_integration_spec.ts @@ -1758,21 +1758,27 @@ describe('reactive forms integration tests', () => { it('should work with single fields and async validators', async () => { const fixture = initTest(FormControlComp); - const control = new FormControl('', null!, uniqLoginAsyncValidator('good')); + // Delay validation so the scheduled renders can observe the pending state. + const control = new FormControl('', null!, uniqLoginAsyncValidator('good', 100)); fixture.debugElement.componentInstance.control = control; - fixture.detectChanges(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-untouched']); dispatchEvent(input, 'blur'); - fixture.detectChanges(); + await fixture.whenStable(); expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-touched']); input.value = 'good'; dispatchEvent(input, 'input'); - await timeout(); - fixture.detectChanges(); + await fixture.whenStable(); + expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-pending', 'ng-touched']); + + // Wait for the validator's 100 ms delay, which keeps it pending during rendering. + // whenStable() doesn't wait for this timer; useAutoTick advances the mock clock. + await timeout(100); + await fixture.whenStable(); expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); }); diff --git a/packages/forms/test/template_integration_spec.ts b/packages/forms/test/template_integration_spec.ts index 79827048530..eca7653c495 100644 --- a/packages/forms/test/template_integration_spec.ts +++ b/packages/forms/test/template_integration_spec.ts @@ -59,8 +59,7 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(StandaloneNgModel); fixture.componentInstance.name = 'oldValue'; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); // model -> view const input = fixture.debugElement.query(By.css('input')).nativeElement; @@ -68,7 +67,7 @@ describe('template-driven forms integration tests', () => { input.value = 'updatedValue'; dispatchEvent(input, 'input'); - await timeout(); + await fixture.whenStable(); // view -> model expect(fixture.componentInstance.name).toEqual('updatedValue'); @@ -78,8 +77,7 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = 'Nancy'; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.value).toEqual({name: 'Nancy'}); @@ -101,8 +99,8 @@ describe('template-driven forms integration tests', () => { } TestBed.configureTestingModule({imports: [FormsModule], declarations: [AppComponent]}); const fixture = TestBed.createComponent(AppComponent); - // We need the Await as `ngModel` writes data asynchronously into the DOM - await fixture.detectChanges(); + // NgModel writes data asynchronously into the DOM. + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')); expect(input.properties['checked']).toBe(true); expect(input.nativeElement.checked).toBe(true); @@ -111,8 +109,7 @@ describe('template-driven forms integration tests', () => { it('should add novalidate by default to form element', async () => { const fixture = initTest(NgModelForm); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.query(By.css('form')); expect(form.nativeElement.getAttribute('novalidate')).toEqual(''); @@ -121,8 +118,7 @@ describe('template-driven forms integration tests', () => { it('should be possible to use native validation and angular forms', async () => { const fixture = initTest(NgModelNativeValidateForm); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.query(By.css('form')); expect(form.nativeElement.hasAttribute('novalidate')).toEqual(false); @@ -134,8 +130,7 @@ describe('template-driven forms integration tests', () => { fixture.componentInstance.last = 'Drew'; fixture.componentInstance.email = 'some email'; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); // model -> view const inputs = fixture.debugElement.queryAll(By.css('input')); @@ -144,7 +139,7 @@ describe('template-driven forms integration tests', () => { inputs[0].nativeElement.value = 'Carson'; dispatchEvent(inputs[0].nativeElement, 'input'); - await timeout(); + await fixture.whenStable(); // view -> model const form = fixture.debugElement.children[0].injector.get(NgForm); @@ -157,8 +152,7 @@ describe('template-driven forms integration tests', () => { fixture.componentInstance.last = 'Drew'; fixture.componentInstance.email = 'some email'; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.control.get('name')!.value).toEqual({first: 'Nancy', last: 'Drew'}); @@ -172,8 +166,7 @@ describe('template-driven forms integration tests', () => { fixture.componentInstance.first = 'Nancy'; fixture.componentInstance.email = 'some email'; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.control.get('email')!.value).toEqual('some email'); @@ -182,8 +175,7 @@ describe('template-driven forms integration tests', () => { // should remove individual control successfully fixture.componentInstance.emailShowing = false; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(form.control.get('email')).toBe(null); expect(form.value).toEqual({name: {first: 'Nancy'}}); @@ -194,8 +186,7 @@ describe('template-driven forms integration tests', () => { // should remove form group successfully fixture.componentInstance.groupShowing = false; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(form.control.get('name')).toBe(null); expect(form.control.get('name.first')).toBe(null); @@ -205,59 +196,61 @@ describe('template-driven forms integration tests', () => { it('should set status classes with ngModel', async () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = 'aa'; - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); + await fixture.whenStable(); - const input = fixture.debugElement.query(By.css('input')).nativeElement; - expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']); + const input = fixture.debugElement.query(By.css('input')).nativeElement; + expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']); - dispatchEvent(input, 'blur'); - fixture.detectChanges(); + dispatchEvent(input, 'blur'); + await fixture.whenStable(); - expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-touched']); + expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-touched']); - input.value = 'updatedValue'; - dispatchEvent(input, 'input'); - fixture.detectChanges(); - expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); + input.value = 'updatedValue'; + dispatchEvent(input, 'input'); + await fixture.whenStable(); + expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); - const formEl = fixture.debugElement.query(By.css('form')).nativeElement; - dispatchEvent(formEl, 'submit'); - fixture.detectChanges(); + const formEl = fixture.debugElement.query(By.css('form')).nativeElement; + dispatchEvent(formEl, 'submit'); + await fixture.whenStable(); - expect(sortedClassList(formEl)).toEqual([ - 'ng-dirty', - 'ng-submitted', - 'ng-touched', - 'ng-valid', - ]); - expect(sortedClassList(input)).not.toContain('ng-submitted'); + expect(sortedClassList(formEl)).toEqual([ + 'ng-dirty', + 'ng-submitted', + 'ng-touched', + 'ng-valid', + ]); + expect(sortedClassList(input)).not.toContain('ng-submitted'); - dispatchEvent(formEl, 'reset'); - fixture.detectChanges(); + dispatchEvent(formEl, 'reset'); + await fixture.whenStable(); - expect(sortedClassList(formEl)).toEqual(['ng-pristine', 'ng-untouched', 'ng-valid']); - expect(sortedClassList(input)).not.toContain('ng-submitted'); - }); + expect(sortedClassList(formEl)).toEqual(['ng-pristine', 'ng-untouched', 'ng-valid']); + expect(sortedClassList(input)).not.toContain('ng-submitted'); }); it('should set status classes with ngModel and async validators', async () => { const fixture = initTest(NgModelAsyncValidation, NgAsyncValidator); - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-untouched']); dispatchEvent(input, 'blur'); - fixture.detectChanges(); + await fixture.whenStable(); expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-touched']); input.value = 'updatedValue'; dispatchEvent(input, 'input'); await fixture.whenStable(); + expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-pending', 'ng-touched']); + + // Wait for the validator's 100 ms delay, which keeps it pending during rendering. + // whenStable() doesn't wait for this timer; useAutoTick advances the mock clock. + await timeout(100); + await fixture.whenStable(); expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); }); @@ -265,64 +258,58 @@ describe('template-driven forms integration tests', () => { it('should set status classes with ngModelGroup and ngForm', async () => { const fixture = initTest(NgModelGroupForm); fixture.componentInstance.first = ''; - fixture.detectChanges(); + await fixture.whenStable(); const form = fixture.debugElement.query(By.css('form')).nativeElement; const modelGroup = fixture.debugElement.query(By.css('[ngModelGroup]')).nativeElement; const input = fixture.debugElement.query(By.css('input')).nativeElement; - // ngModelGroup creates its control asynchronously - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(sortedClassList(modelGroup)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']); + expect(sortedClassList(modelGroup)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']); - expect(sortedClassList(form)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']); + expect(sortedClassList(form)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']); - dispatchEvent(input, 'blur'); - fixture.detectChanges(); + dispatchEvent(input, 'blur'); + await fixture.whenStable(); - expect(sortedClassList(modelGroup)).toEqual(['ng-invalid', 'ng-pristine', 'ng-touched']); - expect(sortedClassList(form)).toEqual(['ng-invalid', 'ng-pristine', 'ng-touched']); + expect(sortedClassList(modelGroup)).toEqual(['ng-invalid', 'ng-pristine', 'ng-touched']); + expect(sortedClassList(form)).toEqual(['ng-invalid', 'ng-pristine', 'ng-touched']); - input.value = 'updatedValue'; - dispatchEvent(input, 'input'); - fixture.detectChanges(); + input.value = 'updatedValue'; + dispatchEvent(input, 'input'); + await fixture.whenStable(); - expect(sortedClassList(modelGroup)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); - expect(sortedClassList(form)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); + expect(sortedClassList(modelGroup)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); + expect(sortedClassList(form)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); - const formEl = fixture.debugElement.query(By.css('form')).nativeElement; - dispatchEvent(formEl, 'submit'); - fixture.detectChanges(); + const formEl = fixture.debugElement.query(By.css('form')).nativeElement; + dispatchEvent(formEl, 'submit'); + await fixture.whenStable(); - expect(sortedClassList(formEl)).toEqual([ - 'ng-dirty', - 'ng-submitted', - 'ng-touched', - 'ng-valid', - ]); - }); + expect(sortedClassList(formEl)).toEqual([ + 'ng-dirty', + 'ng-submitted', + 'ng-touched', + 'ng-valid', + ]); }); it('should set status classes involving nested FormGroups', async () => { const fixture = initTest(NgModelNestedForm); fixture.componentInstance.first = ''; fixture.componentInstance.other = ''; - fixture.detectChanges(); + await fixture.whenStable(); const form = fixture.debugElement.query(By.css('form')).nativeElement; const modelGroup = fixture.debugElement.query(By.css('[ngModelGroup]')).nativeElement; const input = fixture.debugElement.query(By.css('input')).nativeElement; - await fixture.whenStable(); - fixture.detectChanges(); expect(sortedClassList(modelGroup)).toEqual(['ng-pristine', 'ng-untouched', 'ng-valid']); expect(sortedClassList(form)).toEqual(['ng-pristine', 'ng-untouched', 'ng-valid']); const formEl = fixture.debugElement.query(By.css('form')).nativeElement; dispatchEvent(formEl, 'submit'); - fixture.detectChanges(); + await fixture.whenStable(); expect(sortedClassList(modelGroup)).toEqual(['ng-pristine', 'ng-untouched', 'ng-valid']); expect(sortedClassList(form)).toEqual([ @@ -334,22 +321,22 @@ describe('template-driven forms integration tests', () => { expect(sortedClassList(input)).not.toContain('ng-submitted'); dispatchEvent(formEl, 'reset'); - fixture.detectChanges(); + await fixture.whenStable(); expect(sortedClassList(modelGroup)).toEqual(['ng-pristine', 'ng-untouched', 'ng-valid']); expect(sortedClassList(form)).toEqual(['ng-pristine', 'ng-untouched', 'ng-valid']); expect(sortedClassList(input)).not.toContain('ng-submitted'); }); - it('should not create a template-driven form when ngNoForm is used', () => { + it('should not create a template-driven form when ngNoForm is used', async () => { const fixture = initTest(NgNoFormComp); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.debugElement.children[0].providerTokens!.length).toEqual(0); }); - it('should not add novalidate when ngNoForm is used', () => { + it('should not add novalidate when ngNoForm is used', async () => { const fixture = initTest(NgNoFormComp); - fixture.detectChanges(); + await fixture.whenStable(); const form = fixture.debugElement.query(By.css('form')); expect(form.nativeElement.hasAttribute('novalidate')).toEqual(false); }); @@ -386,32 +373,27 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(App); fixture.componentInstance.add(3); fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(getValues()).toEqual(['0', '1', '2']); fixture.componentInstance.remove(1); fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(getValues()).toEqual(['0', '2']); fixture.componentInstance.add(1); fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(getValues()).toEqual(['0', '2', '3']); fixture.componentInstance.items[1].value = '1'; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(getValues()).toEqual(['0', '1', '3']); fixture.componentInstance.items[2].value = '2'; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(getValues()).toEqual(['0', '1', '2']); }); @@ -450,57 +432,52 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(App); fixture.componentInstance.add(3); fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(getValues()).toEqual(['0', '1', '2']); fixture.componentInstance.remove(1); fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(getValues()).toEqual(['0', '2']); fixture.componentInstance.add(1); fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(getValues()).toEqual(['0', '2', '3']); fixture.componentInstance.items[1].value = '1'; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(getValues()).toEqual(['0', '1', '3']); fixture.componentInstance.items[2].value = '2'; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(getValues()).toEqual(['0', '1', '2']); }); }); describe('name and ngModelOptions', () => { - it('should throw if ngModel has a parent form but no name attr or standalone label', () => { + it('should throw if ngModel has a parent form but no name attr or standalone label', async () => { const fixture = initTest(InvalidNgModelNoName); - expect(() => fixture.detectChanges()).toThrowError(new RegExp(`name attribute must be set`)); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( + new RegExp(`name attribute must be set`), + ); }); - it('should not throw if ngModel has a parent form, no name attr, and a standalone label', () => { + it('should not throw if ngModel has a parent form, no name attr, and a standalone label', async () => { const fixture = initTest(NgModelOptionsStandalone); - expect(() => fixture.detectChanges()).not.toThrow(); + await expectAsync(fixture.whenStable()).toBeResolved(); }); it('should not register standalone ngModels with parent form', async () => { const fixture = initTest(NgModelOptionsStandalone); fixture.componentInstance.one = 'some data'; fixture.componentInstance.two = 'should not show'; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); const inputs = fixture.debugElement.queryAll(By.css('input')); - await timeout(); expect(form.value).toEqual({one: 'some data'}); expect(inputs[1].nativeElement.value).toEqual('should not show'); @@ -510,8 +487,7 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.options = {name: 'override'}; fixture.componentInstance.name = 'some data'; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.value).toEqual({override: 'some data'}); @@ -524,8 +500,7 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = ''; fixture.componentInstance.options = {}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); const name = form.control.get('name') as FormControl; @@ -537,8 +512,7 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = ''; fixture.componentInstance.options = {updateOn: 'blur'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); const name = form.control.get('name') as FormControl; @@ -550,8 +524,7 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = 'Nancy Drew'; fixture.componentInstance.options = {updateOn: 'blur'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); @@ -569,13 +542,11 @@ describe('template-driven forms integration tests', () => { fixture.componentInstance.name = 'Nancy Drew'; fixture.componentInstance.options = {updateOn: 'blur'}; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); fixture.componentInstance.name = 'Carson'; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); @@ -595,14 +566,12 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = 'Carson'; fixture.componentInstance.options = {updateOn: 'blur'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; input.value = 'Nancy Drew'; dispatchEvent(input, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(fixture.componentInstance.name) @@ -611,7 +580,7 @@ describe('template-driven forms integration tests', () => { expect(form.valid).withContext('Expected validation not to run on input.').toBe(false); dispatchEvent(input, 'blur'); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.name) .withContext('Expected value to update on blur.') @@ -623,21 +592,19 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = 'Carson'; fixture.componentInstance.options = {updateOn: 'blur'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; input.value = 'Nancy Drew'; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); dispatchEvent(input, 'blur'); - fixture.detectChanges(); + await fixture.whenStable(); input.value = 'Carson'; dispatchEvent(input, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(fixture.componentInstance.name) @@ -648,7 +615,7 @@ describe('template-driven forms integration tests', () => { .toBe(true); dispatchEvent(input, 'blur'); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.name) .withContext('Expected value to update on second blur.') @@ -660,20 +627,18 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = ''; fixture.componentInstance.options = {updateOn: 'blur'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; input.value = 'Nancy Drew'; dispatchEvent(input, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.dirty).withContext('Expected dirtiness not to update on input.').toBe(false); dispatchEvent(input, 'blur'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.dirty).withContext('Expected dirtiness to update on blur.').toBe(true); }); @@ -682,20 +647,18 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = ''; fixture.componentInstance.options = {updateOn: 'blur'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; input.value = 'Nancy Drew'; dispatchEvent(input, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.touched).withContext('Expected touched not to update on input.').toBe(false); dispatchEvent(input, 'blur'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.touched).withContext('Expected touched to update on blur.').toBe(true); }); @@ -704,8 +667,7 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = ''; fixture.componentInstance.options = {updateOn: 'blur'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const values: any[] = []; const form = fixture.debugElement.children[0].injector.get(NgForm); @@ -717,15 +679,14 @@ describe('template-driven forms integration tests', () => { const input = fixture.debugElement.query(By.css('input')).nativeElement; input.value = 'Nancy Drew'; dispatchEvent(input, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(values) .withContext('Expected no valueChanges or statusChanges on input.') .toEqual([]); dispatchEvent(input, 'blur'); - fixture.detectChanges(); + await fixture.whenStable(); expect(values).toEqual( [{name: 'Nancy Drew'}, 'VALID'], @@ -739,8 +700,7 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelChangesForm); fixture.componentInstance.name = 'Carson'; fixture.componentInstance.options = {updateOn: 'blur'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(fixture.componentInstance.events) .withContext('Expected ngModelChanges not to fire.') @@ -748,7 +708,7 @@ describe('template-driven forms integration tests', () => { const input = fixture.debugElement.query(By.css('input')).nativeElement; dispatchEvent(input, 'blur'); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.events) .withContext('Expected ngModelChanges not to fire if value unchanged.') @@ -756,22 +716,21 @@ describe('template-driven forms integration tests', () => { input.value = 'Carson'; dispatchEvent(input, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(fixture.componentInstance.events) .withContext('Expected ngModelChanges not to fire on input.') .toEqual([]); dispatchEvent(input, 'blur'); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.events) .withContext('Expected ngModelChanges to fire once blurred if value changed.') .toEqual(['fired']); dispatchEvent(input, 'blur'); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.events).toEqual( ['fired'], @@ -780,15 +739,14 @@ describe('template-driven forms integration tests', () => { input.value = 'Bess'; dispatchEvent(input, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(fixture.componentInstance.events) .withContext('Expected ngModelChanges not to fire on input after blur.') .toEqual(['fired']); dispatchEvent(input, 'blur'); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.events).toEqual( ['fired', 'fired'], @@ -802,8 +760,7 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = ''; fixture.componentInstance.options = {updateOn: 'submit'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); const name = form.control.get('name') as FormControl; @@ -815,8 +772,7 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = 'Nancy Drew'; fixture.componentInstance.options = {updateOn: 'submit'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); @@ -834,13 +790,11 @@ describe('template-driven forms integration tests', () => { fixture.componentInstance.name = 'Nancy Drew'; fixture.componentInstance.options = {updateOn: 'submit'}; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); fixture.componentInstance.name = 'Carson'; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); @@ -859,14 +813,12 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = 'Carson'; fixture.componentInstance.options = {updateOn: 'submit'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; input.value = 'Nancy Drew'; dispatchEvent(input, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(fixture.componentInstance.name) @@ -875,8 +827,7 @@ describe('template-driven forms integration tests', () => { expect(form.valid).withContext('Expected validation not to run on input.').toBe(false); dispatchEvent(input, 'blur'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(fixture.componentInstance.name) .withContext('Expected value not to update on blur.') @@ -885,7 +836,7 @@ describe('template-driven forms integration tests', () => { const formEl = fixture.debugElement.query(By.css('form')).nativeElement; dispatchEvent(formEl, 'submit'); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.name) .withContext('Expected value to update on submit.') @@ -897,24 +848,20 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = 'Carson'; fixture.componentInstance.options = {updateOn: 'submit'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; input.value = 'Nancy Drew'; dispatchEvent(input, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const formEl = fixture.debugElement.query(By.css('form')).nativeElement; dispatchEvent(formEl, 'submit'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); input.value = 'Carson'; dispatchEvent(input, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(fixture.componentInstance.name) @@ -925,8 +872,7 @@ describe('template-driven forms integration tests', () => { .toBe(true); dispatchEvent(formEl, 'submit'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(fixture.componentInstance.name) .withContext('Expected value to update on second submit.') @@ -940,8 +886,7 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelGroupForm); fixture.componentInstance.options = {updateOn: 'submit'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); form.control.get('name')!.setValidators(groupValidatorSpy); @@ -949,7 +894,7 @@ describe('template-driven forms integration tests', () => { const formEl = fixture.debugElement.query(By.css('form')).nativeElement; dispatchEvent(formEl, 'submit'); - fixture.detectChanges(); + await fixture.whenStable(); expect(validatorSpy).not.toHaveBeenCalled(); expect(groupValidatorSpy).not.toHaveBeenCalled(); @@ -959,27 +904,24 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = ''; fixture.componentInstance.options = {updateOn: 'submit'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; input.value = 'Nancy Drew'; dispatchEvent(input, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.dirty).withContext('Expected dirtiness not to update on input.').toBe(false); dispatchEvent(input, 'blur'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(form.dirty).withContext('Expected dirtiness not to update on blur.').toBe(false); const formEl = fixture.debugElement.query(By.css('form')).nativeElement; dispatchEvent(formEl, 'submit'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.dirty).withContext('Expected dirtiness to update on submit.').toBe(true); }); @@ -988,25 +930,22 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = ''; fixture.componentInstance.options = {updateOn: 'submit'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; input.value = 'Nancy Drew'; dispatchEvent(input, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); dispatchEvent(input, 'blur'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.touched).withContext('Expected touched not to update on blur.').toBe(false); const formEl = fixture.debugElement.query(By.css('form')).nativeElement; dispatchEvent(formEl, 'submit'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.touched).withContext('Expected touched to update on submit.').toBe(true); }); @@ -1015,21 +954,19 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = 'Nancy' as string | null; fixture.componentInstance.options = {updateOn: 'submit'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; input.value = 'Nancy Drew'; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); dispatchEvent(input, 'blur'); - fixture.detectChanges(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); form.resetForm(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(input.value).withContext('Expected view value to reset.').toEqual(''); expect(form.value).withContext('Expected form value to reset.').toEqual({name: null}); @@ -1041,7 +978,7 @@ describe('template-driven forms integration tests', () => { const formEl = fixture.debugElement.query(By.css('form')).nativeElement; dispatchEvent(formEl, 'submit'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.value).withContext('Expected form value to stay empty on submit').toEqual({ name: null, @@ -1057,8 +994,7 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = ''; fixture.componentInstance.options = {updateOn: 'submit'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const values: any[] = []; const form = fixture.debugElement.children[0].injector.get(NgForm); @@ -1070,16 +1006,14 @@ describe('template-driven forms integration tests', () => { const input = fixture.debugElement.query(By.css('input')).nativeElement; input.value = 'Nancy Drew'; dispatchEvent(input, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(values) .withContext('Expected no valueChanges or statusChanges on input.') .toEqual([]); dispatchEvent(input, 'blur'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(values) .withContext('Expected no valueChanges or statusChanges on blur.') @@ -1087,7 +1021,7 @@ describe('template-driven forms integration tests', () => { const formEl = fixture.debugElement.query(By.css('form')).nativeElement; dispatchEvent(formEl, 'submit'); - fixture.detectChanges(); + await fixture.whenStable(); expect(values).toEqual( [{name: 'Nancy Drew'}, 'VALID'], @@ -1100,12 +1034,11 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelChangesForm); fixture.componentInstance.name = 'Carson'; fixture.componentInstance.options = {updateOn: 'submit'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const formEl = fixture.debugElement.query(By.css('form')).nativeElement; dispatchEvent(formEl, 'submit'); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.events) .withContext('Expected ngModelChanges not to fire if value unchanged.') @@ -1114,22 +1047,21 @@ describe('template-driven forms integration tests', () => { const input = fixture.debugElement.query(By.css('input')).nativeElement; input.value = 'Carson'; dispatchEvent(input, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(fixture.componentInstance.events) .withContext('Expected ngModelChanges not to fire on input.') .toEqual([]); dispatchEvent(formEl, 'submit'); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.events) .withContext('Expected ngModelChanges to fire once submitted if value changed.') .toEqual(['fired']); dispatchEvent(formEl, 'submit'); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.events).toEqual( ['fired'], @@ -1138,15 +1070,14 @@ describe('template-driven forms integration tests', () => { input.value = 'Bess'; dispatchEvent(input, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(fixture.componentInstance.events) .withContext('Expected ngModelChanges not to fire on input after submit.') .toEqual(['fired']); dispatchEvent(formEl, 'submit'); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.events).toEqual( ['fired', 'fired'], @@ -1160,10 +1091,9 @@ describe('template-driven forms integration tests', () => { } const fixture = initTest(NativeDialogForm); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const event = dispatchEvent(fixture.componentInstance.form.nativeElement, 'submit'); - fixture.detectChanges(); + await fixture.whenStable(); expect(event.defaultPrevented).toBe(false); }); @@ -1174,8 +1104,7 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelOptionsStandalone); fixture.componentInstance.options = {name: 'two'}; fixture.componentInstance.formOptions = {updateOn: 'blur'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); const controlOne = form.control.get('one')! as FormControl; @@ -1195,14 +1124,12 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelOptionsStandalone); fixture.componentInstance.one = ''; fixture.componentInstance.formOptions = {updateOn: 'blur'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; input.value = 'Nancy Drew'; dispatchEvent(input, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.value).withContext('Expected value not to update on input.').toEqual({ @@ -1210,7 +1137,7 @@ describe('template-driven forms integration tests', () => { }); dispatchEvent(input, 'blur'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.value).withContext('Expected value to update on blur.').toEqual({ one: 'Nancy Drew', @@ -1221,8 +1148,7 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelOptionsStandalone); fixture.componentInstance.options = {updateOn: 'blur', name: 'two'}; fixture.componentInstance.formOptions = {updateOn: 'change'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); const controlOne = form.control.get('one')! as FormControl; @@ -1246,13 +1172,12 @@ describe('template-driven forms integration tests', () => { fixture.componentInstance.two = ''; fixture.componentInstance.options = {updateOn: 'blur', name: 'two'}; fixture.componentInstance.formOptions = {updateOn: 'change'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const [inputOne, inputTwo] = fixture.debugElement.queryAll(By.css('input')); inputOne.nativeElement.value = 'Nancy Drew'; dispatchEvent(inputOne.nativeElement, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.value).withContext('Expected first value to update on input.').toEqual({ @@ -1262,8 +1187,7 @@ describe('template-driven forms integration tests', () => { inputTwo.nativeElement.value = 'Carson Drew'; dispatchEvent(inputTwo.nativeElement, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(form.value).withContext('Expected second value not to update on input.').toEqual({ one: 'Nancy Drew', @@ -1271,7 +1195,7 @@ describe('template-driven forms integration tests', () => { }); dispatchEvent(inputTwo.nativeElement, 'blur'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.value).toEqual( {one: 'Nancy Drew', two: 'Carson Drew'}, @@ -1284,13 +1208,12 @@ describe('template-driven forms integration tests', () => { fixture.componentInstance.two = ''; fixture.componentInstance.options = {standalone: true}; fixture.componentInstance.formOptions = {updateOn: 'blur'}; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const inputTwo = fixture.debugElement.queryAll(By.css('input'))[1].nativeElement; inputTwo.value = 'Nancy Drew'; dispatchEvent(inputTwo, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.two) .withContext('Expected standalone ngModel not to inherit blur update.') @@ -1303,10 +1226,11 @@ describe('template-driven forms integration tests', () => { it('should emit ngSubmit event with the original submit event on submit', async () => { const fixture = initTest(NgModelForm); fixture.componentInstance.event = null!; + await fixture.whenStable(); const form = fixture.debugElement.query(By.css('form')); dispatchEvent(form.nativeElement, 'submit'); - await timeout(); + await fixture.whenStable(); expect(fixture.componentInstance.event.type).toEqual('submit'); }); @@ -1314,13 +1238,13 @@ describe('template-driven forms integration tests', () => { it('should mark NgForm as submitted on submit event', async () => { const fixture = initTest(NgModelForm); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.submitted).toBe(false); const formEl = fixture.debugElement.query(By.css('form')).nativeElement; dispatchEvent(formEl, 'submit'); - await timeout(); + await fixture.whenStable(); expect(form.submitted).toBe(true); }); @@ -1328,8 +1252,7 @@ describe('template-driven forms integration tests', () => { it('should reset the form to empty when reset event is fired', async () => { const fixture = initTest(NgModelForm); fixture.componentInstance.name = 'should be cleared' as string | null; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); const formEl = fixture.debugElement.query(By.css('form')); @@ -1340,8 +1263,7 @@ describe('template-driven forms integration tests', () => { expect(form.value.name).toEqual('should be cleared'); // control value dispatchEvent(formEl.nativeElement, 'reset'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(input.nativeElement.value).toBe(''); // view value expect(fixture.componentInstance.name).toBe(null); // ngModel value @@ -1350,17 +1272,16 @@ describe('template-driven forms integration tests', () => { it('should reset the form submit state when reset button is clicked', async () => { const fixture = initTest(NgModelForm); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); const formEl = fixture.debugElement.query(By.css('form')); dispatchEvent(formEl.nativeElement, 'submit'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(form.submitted).toBe(true); dispatchEvent(formEl.nativeElement, 'reset'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(form.submitted).toBe(false); }); }); @@ -1370,7 +1291,6 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); const form = fixture.debugElement.children[0].injector.get(NgForm); fixture.componentInstance.name = 'aa'; - fixture.detectChanges(); expect(form.valid).toEqual(true); expect(form.value).toEqual({}); @@ -1381,7 +1301,7 @@ describe('template-driven forms integration tests', () => { form.statusChanges!.subscribe((status: string) => (formValidity = status)); form.valueChanges!.subscribe((value: string) => (formValue = value)); - await timeout(); + await fixture.whenStable(); expect(formValidity).toEqual('INVALID'); expect(formValue).toEqual({name: 'aa'}); @@ -1391,8 +1311,7 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelForm); const form = fixture.debugElement.children[0].injector.get(NgForm).form; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); form.get('name')!.valueChanges.subscribe(() => { expect(form.get('name')!.dirty).toBe(true); @@ -1406,8 +1325,7 @@ describe('template-driven forms integration tests', () => { it('should mark controls pristine before emitting the value change event when resetting ', async () => { const fixture = initTest(NgModelForm); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm).form; const formEl = fixture.debugElement.query(By.css('form')).nativeElement; @@ -1434,8 +1352,7 @@ describe('template-driven forms integration tests', () => { fixture.componentInstance.last = 'Drew'; fixture.componentInstance.email = 'some email'; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.value).toEqual({name: {first: '', last: 'Drew'}, email: 'some email'}); @@ -1444,8 +1361,7 @@ describe('template-driven forms integration tests', () => { fixture.componentInstance.isDisabled = true; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(form.value).toEqual({name: {last: 'Drew'}, email: 'some email'}); expect(form.valid).toBe(true); @@ -1456,13 +1372,11 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelGroupForm); fixture.componentInstance.isDisabled = false; fixture.componentInstance.first = 'Nancy'; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); form.control.get('name.first')!.disable(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css(`[name="first"]`)); expect(input.nativeElement.disabled).toBe(true); @@ -1472,9 +1386,6 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelCustomWrapper, NgModelCustomComp); fixture.componentInstance.name = 'Nancy'; fixture.componentInstance.isDisabled = true; - fixture.detectChanges(); - await fixture.whenStable(); - fixture.detectChanges(); await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); @@ -1495,8 +1406,7 @@ describe('template-driven forms integration tests', () => { }, }); const fixture = initTest(NgModelForm); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.control.get('name')!.disabled).toBe(true); @@ -1504,8 +1414,7 @@ describe('template-driven forms integration tests', () => { expect(input.nativeElement.disabled).toEqual(true); form.control.enable(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(input.nativeElement.disabled).toEqual(false); }); }); @@ -1513,8 +1422,7 @@ describe('template-driven forms integration tests', () => { describe('validation directives', () => { it('required validator should validate checkbox', async () => { const fixture = initTest(NgModelCheckboxRequiredValidator); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const control = fixture.debugElement.children[0].injector .get(NgForm) @@ -1526,32 +1434,28 @@ describe('template-driven forms integration tests', () => { fixture.componentInstance.required = true; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(input.nativeElement.checked).toBe(false); expect(control.hasError('required')).toBe(true); input.nativeElement.checked = true; dispatchEvent(input.nativeElement, 'change'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(input.nativeElement.checked).toBe(true); expect(control.hasError('required')).toBe(false); input.nativeElement.checked = false; dispatchEvent(input.nativeElement, 'change'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(input.nativeElement.checked).toBe(false); expect(control.hasError('required')).toBe(true); fixture.componentInstance.required = false; dispatchEvent(input.nativeElement, 'change'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(input.nativeElement.checked).toBe(false); expect(control.hasError('required')).toBe(false); @@ -1559,8 +1463,7 @@ describe('template-driven forms integration tests', () => { it('should validate email', async () => { const fixture = initTest(NgModelEmailValidator); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const control = fixture.debugElement.children[0].injector.get(NgForm).control.get('email')!; @@ -1569,31 +1472,28 @@ describe('template-driven forms integration tests', () => { fixture.componentInstance.validatorEnabled = true; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(input.nativeElement.value).toEqual(''); expect(control.hasError('email')).toBe(false); input.nativeElement.value = '@'; dispatchEvent(input.nativeElement, 'input'); - await timeout(); + await fixture.whenStable(); expect(input.nativeElement.value).toEqual('@'); expect(control.hasError('email')).toBe(true); input.nativeElement.value = 'test@gmail.com'; dispatchEvent(input.nativeElement, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(input.nativeElement.value).toEqual('test@gmail.com'); expect(control.hasError('email')).toBe(false); input.nativeElement.value = 'text'; dispatchEvent(input.nativeElement, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(input.nativeElement.value).toEqual('text'); expect(control.hasError('email')).toBe(true); @@ -1605,8 +1505,7 @@ describe('template-driven forms integration tests', () => { fixture.componentInstance.minLen = 3; fixture.componentInstance.maxLen = 3; fixture.componentInstance.pattern = '.{3,}'; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const required = fixture.debugElement.query(By.css('[name=required]')); const minLength = fixture.debugElement.query(By.css('[name=minlength]')); @@ -1622,7 +1521,7 @@ describe('template-driven forms integration tests', () => { dispatchEvent(minLength.nativeElement, 'input'); dispatchEvent(maxLength.nativeElement, 'input'); dispatchEvent(pattern.nativeElement, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); expect(form.control.hasError('required', ['required'])).toEqual(true); @@ -1647,20 +1546,19 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelMultipleValidators); fixture.componentInstance.required = false; fixture.componentInstance.pattern = '[a-z]+'; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); const input = fixture.debugElement.query(By.css('input')); input.nativeElement.value = ''; dispatchEvent(input.nativeElement, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toBeTruthy(); input.nativeElement.value = '1'; dispatchEvent(input.nativeElement, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toBeFalsy(); expect(form.control.hasError('pattern', ['tovalidate'])).toBeTruthy(); }); @@ -1669,20 +1567,19 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelMultipleValidators); fixture.componentInstance.required = false; fixture.componentInstance.pattern = /^[a-z]+$/; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); const input = fixture.debugElement.query(By.css('input')); input.nativeElement.value = ''; dispatchEvent(input.nativeElement, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toBeTruthy(); input.nativeElement.value = '1'; dispatchEvent(input.nativeElement, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toBeFalsy(); expect(form.control.hasError('pattern', ['tovalidate'])).toBeTruthy(); }); @@ -1691,28 +1588,26 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelMultipleValidators); fixture.componentInstance.required = false; fixture.componentInstance.minLen = 2; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); const input = fixture.debugElement.query(By.css('input')); input.nativeElement.value = ''; dispatchEvent(input.nativeElement, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toBeTruthy(); input.nativeElement.value = '1'; dispatchEvent(input.nativeElement, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toBeFalsy(); expect(form.control.hasError('minlength', ['tovalidate'])).toBeTruthy(); }); it('changes on bound properties should change the validation state of the form', async () => { const fixture = initTest(NgModelValidationBindings); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const required = fixture.debugElement.query(By.css('[name=required]')); const minLength = fixture.debugElement.query(By.css('[name=minlength]')); @@ -1740,7 +1635,8 @@ describe('template-driven forms integration tests', () => { fixture.componentInstance.minLen = 3; fixture.componentInstance.maxLen = 3; fixture.componentInstance.pattern = '.{3,}'; - fixture.detectChanges(); + fixture.changeDetectorRef.markForCheck(); + await fixture.whenStable(); dispatchEvent(required.nativeElement, 'input'); dispatchEvent(minLength.nativeElement, 'input'); @@ -1768,7 +1664,8 @@ describe('template-driven forms integration tests', () => { fixture.componentInstance.minLen = null!; fixture.componentInstance.maxLen = null!; fixture.componentInstance.pattern = null!; - fixture.detectChanges(); + fixture.changeDetectorRef.markForCheck(); + await fixture.whenStable(); expect(form.control.hasError('required', ['required'])).toEqual(false); expect(form.control.hasError('minlength', ['minlength'])).toEqual(false); @@ -1788,8 +1685,7 @@ describe('template-driven forms integration tests', () => { const inputNativeEl = inputEl.nativeElement; const onNgModelChange = jasmine.createSpy('onNgModelChange'); fixture.componentInstance.onNgModelChange = onNgModelChange; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(onNgModelChange).not.toHaveBeenCalled(); @@ -1800,7 +1696,7 @@ describe('template-driven forms integration tests', () => { }); dispatchEvent(inputNativeEl, 'input'); expect(onNgModelChange).toHaveBeenCalled(); - await timeout(); + await fixture.whenStable(); inputNativeEl.value = '333'; onNgModelChange.and.callFake((ngModel: NgModel) => { @@ -1809,51 +1705,49 @@ describe('template-driven forms integration tests', () => { }); dispatchEvent(inputNativeEl, 'input'); expect(onNgModelChange).toHaveBeenCalledTimes(2); - await timeout(); + await fixture.whenStable(); }); it('should validate max', async () => { const fixture = initTest(NgModelMaxValidator); fixture.componentInstance.max = 10; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); input.value = ''; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(input.getAttribute('max')).toEqual('10'); expect(form.valid).toEqual(true); expect(form.controls['max'].errors).toBeNull(); input.value = 11; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(false); expect(form.controls['max'].errors).toEqual({max: {max: 10, actual: 11}}); input.value = 9; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['max'].errors).toBeNull(); fixture.componentInstance.max = 0; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(input.getAttribute('max')).toEqual('0'); expect(form.valid).toEqual(false); expect(form.controls['max'].errors).toEqual({max: {max: 0, actual: 9}}); input.value = 0; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['max'].errors).toBeNull(); }); @@ -1861,34 +1755,33 @@ describe('template-driven forms integration tests', () => { it('should validate max for float number', async () => { const fixture = initTest(NgModelMaxValidator); fixture.componentInstance.max = 10.25; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); input.value = ''; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(input.getAttribute('max')).toEqual('10.25'); expect(form.valid).toEqual(true); expect(form.controls['max'].errors).toBeNull(); input.value = 10.25; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['max'].errors).toBeNull(); input.value = 10.15; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['max'].errors).toBeNull(); input.value = 10.35; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(false); expect(form.controls['max'].errors).toEqual({max: {max: 10.25, actual: 10.35}}); }); @@ -1896,22 +1789,21 @@ describe('template-driven forms integration tests', () => { it('should apply max validation when control value is defined as a string', async () => { const fixture = initTest(NgModelMaxValidator); fixture.componentInstance.max = 10; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); input.value = '11'; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(input.getAttribute('max')).toEqual('10'); expect(form.valid).toEqual(false); expect(form.controls['max'].errors).toEqual({max: {max: 10, actual: 11}}); input.value = '9'; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['max'].errors).toBeNull(); }); @@ -1920,27 +1812,26 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelMaxValidator); fixture.componentInstance.max = 10; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); input.value = 11; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(false); expect(form.controls['max'].errors).toEqual({max: {max: 10, actual: 11}}); input.value = 9; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['max'].errors).toBeNull(); fixture.componentInstance.max = 5; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(false); expect(form.controls['max'].errors).toEqual({max: {max: 5, actual: 9}}); }); @@ -1949,45 +1840,43 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelMinValidator); fixture.componentInstance.min = 10; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); input.value = ''; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(input.getAttribute('min')).toEqual('10'); expect(form.valid).toEqual(true); expect(form.controls['min'].errors).toBeNull(); input.value = 11; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['min'].errors).toBeNull(); input.value = 9; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(false); expect(form.controls['min'].errors).toEqual({min: {min: 10, actual: 9}}); fixture.componentInstance.min = 0; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); input.value = -5; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(input.getAttribute('min')).toEqual('0'); expect(form.valid).toEqual(false); expect(form.controls['min'].errors).toEqual({min: {min: 0, actual: -5}}); input.value = 0; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['min'].errors).toBeNull(); }); @@ -1995,56 +1884,54 @@ describe('template-driven forms integration tests', () => { it('should validate min for float number', async () => { const fixture = initTest(NgModelMinValidator); fixture.componentInstance.min = 10.25; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); input.value = ''; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(input.getAttribute('min')).toEqual('10.25'); expect(form.valid).toEqual(true); expect(form.controls['min'].errors).toBeNull(); input.value = 10.35; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['min'].errors).toBeNull(); input.value = 10.25; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['min'].errors).toBeNull(); input.value = 10.15; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(false); expect(form.controls['min'].errors).toEqual({min: {min: 10.25, actual: 10.15}}); }); it('should apply min validation when control value is defined as a string', async () => { const fixture = initTest(NgModelMinValidator); fixture.componentInstance.min = 10; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); input.value = '11'; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(input.getAttribute('min')).toEqual('10'); expect(form.valid).toEqual(true); expect(form.controls['min'].errors).toBeNull(); input.value = '9'; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(false); expect(form.controls['min'].errors).toEqual({min: {min: 10, actual: 9}}); }); @@ -2053,27 +1940,26 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelMinValidator); fixture.componentInstance.min = 10; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); input.value = 11; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['min'].errors).toBeNull(); input.value = 9; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(false); expect(form.controls['min'].errors).toEqual({min: {min: 10, actual: 9}}); fixture.componentInstance.min = 9; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['min'].errors).toBeNull(); }); @@ -2084,8 +1970,7 @@ describe('template-driven forms integration tests', () => { fixture.componentInstance.min = 10; fixture.componentInstance.max = 20; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const min = fixture.debugElement.query(By.directive(MinValidator)); expect(min).toBeNull(); @@ -2134,8 +2019,7 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(AppComponent, MyCustomComponentDirective); const validateFnSpy = spyOn(MaxValidator.prototype, 'validate'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const mv = fixture.debugElement.query(By.directive(MaxValidator)); expect(mv).toBeNull(); @@ -2158,8 +2042,7 @@ describe('template-driven forms integration tests', () => { const maxValidateFnSpy = spyOn(MaxValidator.prototype, 'validate'); const minValidateFnSpy = spyOn(MinValidator.prototype, 'validate'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const maxValidator = fixture.debugElement.query(By.directive(MaxValidator)); expect(maxValidator).toBeNull(); @@ -2186,8 +2069,7 @@ describe('template-driven forms integration tests', () => { } const fixture = initTest(MinLengthMaxLengthComponent); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); @@ -2205,20 +2087,20 @@ describe('template-driven forms integration tests', () => { failedValidator?: string; } - const setInputValue = (value: number) => { + const setInputValue = async (value: number) => { input.value = value; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); }; const verifyValidatorAttrValues = (values: {minlength: any; maxlength: any}) => { expect(input.getAttribute('minlength')).toBe(values.minlength); expect(input.getAttribute('maxlength')).toBe(values.maxlength); }; - const setValidatorValues = (values: minmax) => { + const setValidatorValues = async (values: minmax) => { fixture.componentInstance.minlen = values.minlength; fixture.componentInstance.maxlen = values.maxlength; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + await fixture.whenStable(); }; const verifyFormState = (state: state) => { expect(form.valid).toBe(state.isValid); @@ -2235,29 +2117,29 @@ describe('template-driven forms integration tests', () => { // 2. Verify that setting validator inputs (to a value different from `null`) activate // validators. - setInputValue(12345); - setValidatorValues({minlength: 2, maxlength: 4}); + await setInputValue(12345); + await setValidatorValues({minlength: 2, maxlength: 4}); verifyValidatorAttrValues({minlength: '2', maxlength: '4'}); verifyFormState({isValid: false, failedValidator: 'maxlength'}); // 3. Changing value to the valid range should make the form valid. - setInputValue(123); + await setInputValue(123); verifyFormState({isValid: true}); // 4. Changing value to trigger `minlength` validator. - setInputValue(1); + await setInputValue(1); verifyFormState({isValid: false, failedValidator: 'minlength'}); // 5. Changing validator inputs to verify that attribute values are updated (and the // form is now valid). - setInputValue(1); - setValidatorValues({minlength: 1, maxlength: 5}); + await setInputValue(1); + await setValidatorValues({minlength: 1, maxlength: 5}); verifyValidatorAttrValues({minlength: '1', maxlength: '5'}); verifyFormState({isValid: true}); // 6. Reset validator inputs back to `null` should deactivate validators. - setInputValue(123); - setValidatorValues({minlength: null, maxlength: null}); + await setInputValue(123); + await setValidatorValues({minlength: null, maxlength: null}); verifyValidatorAttrValues({minlength: null, maxlength: null}); verifyFormState({isValid: true}); }); @@ -2276,8 +2158,7 @@ describe('template-driven forms integration tests', () => { } const fixture = initTest(MinLengthMaxLengthComponent); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); @@ -2295,20 +2176,20 @@ describe('template-driven forms integration tests', () => { failedValidator?: string; } - const setInputValue = (value: number) => { + const setInputValue = async (value: number) => { input.value = value; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); }; const verifyValidatorAttrValues = (values: {min: any; max: any}) => { expect(input.getAttribute('min')).toBe(values.min); expect(input.getAttribute('max')).toBe(values.max); }; - const setValidatorValues = (values: minmax) => { + const setValidatorValues = async (values: minmax) => { fixture.componentInstance.minlen = values.min; fixture.componentInstance.maxlen = values.max; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + await fixture.whenStable(); }; const verifyFormState = (state: state) => { expect(form.valid).toBe(state.isValid); @@ -2325,29 +2206,29 @@ describe('template-driven forms integration tests', () => { // 2. Verify that setting validator inputs (to a value different from `null`) activate // validators. - setInputValue(12345); - setValidatorValues({min: 2, max: 4}); + await setInputValue(12345); + await setValidatorValues({min: 2, max: 4}); verifyValidatorAttrValues({min: '2', max: '4'}); verifyFormState({isValid: false, failedValidator: 'max'}); // 3. Changing value to the valid range should make the form valid. - setInputValue(3); + await setInputValue(3); verifyFormState({isValid: true}); // 4. Changing value to trigger `minlength` validator. - setInputValue(1); + await setInputValue(1); verifyFormState({isValid: false, failedValidator: 'min'}); // 5. Changing validator inputs to verify that attribute values are updated (and the // form is now valid). - setInputValue(1); - setValidatorValues({min: 1, max: 5}); + await setInputValue(1); + await setValidatorValues({min: 1, max: 5}); verifyValidatorAttrValues({min: '1', max: '5'}); verifyFormState({isValid: true}); // 6. Reset validator inputs back to `null` should deactivate validators. - setInputValue(123); - setValidatorValues({min: null, max: null}); + await setInputValue(123); + await setValidatorValues({min: null, max: null}); verifyValidatorAttrValues({min: null, max: null}); verifyFormState({isValid: true}); }); @@ -2360,33 +2241,32 @@ describe('template-driven forms integration tests', () => { fixture.componentInstance.min = inputType === 'string' ? '5' : 5; fixture.componentInstance.max = inputType === 'string' ? '10' : 10; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); input.value = ''; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['min_max'].errors).toBeNull(); input.value = 11; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(false); expect(form.controls['min_max'].errors).toEqual({max: {max: 10, actual: 11}}); input.value = 4; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(false); expect(form.controls['min_max'].errors).toEqual({min: {min: 5, actual: 4}}); input.value = 9; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['min_max'].errors).toBeNull(); }); @@ -2395,33 +2275,32 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelMinMaxValidator); fixture.componentInstance.min = 5; fixture.componentInstance.max = 10; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); input.value = ''; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['min_max'].errors).toBeNull(); input.value = 11; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(false); expect(form.controls['min_max'].errors).toEqual({max: {max: 10, actual: 11}}); input.value = 4; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(false); expect(form.controls['min_max'].errors).toEqual({min: {min: 5, actual: 4}}); input.value = 9; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['min_max'].errors).toBeNull(); }); @@ -2430,33 +2309,32 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelMinMaxValidator); fixture.componentInstance.min = 5; fixture.componentInstance.max = 10; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); input.value = ''; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['min_max'].errors).toBeNull(); input.value = '11'; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(false); expect(form.controls['min_max'].errors).toEqual({max: {max: 10, actual: 11}}); input.value = '4'; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(false); expect(form.controls['min_max'].errors).toEqual({min: {min: 5, actual: 4}}); input.value = '9'; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['min_max'].errors).toBeNull(); }); @@ -2466,45 +2344,44 @@ describe('template-driven forms integration tests', () => { fixture.componentInstance.min = 5; fixture.componentInstance.max = 10; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); input.value = 10; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['min_max'].errors).toBeNull(); input.value = 12; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(false); expect(form.controls['min_max'].errors).toEqual({max: {max: 10, actual: 12}}); fixture.componentInstance.max = 12; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['min_max'].errors).toBeNull(); input.value = 5; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['min_max'].errors).toBeNull(); input.value = 0; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(false); expect(form.controls['min_max'].errors).toEqual({min: {min: 5, actual: 0}}); fixture.componentInstance.min = 0; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['min_max'].errors).toBeNull(); }); @@ -2513,8 +2390,7 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelMinMaxValidator); fixture.componentInstance.min = 5; fixture.componentInstance.max = 10; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); @@ -2524,7 +2400,7 @@ describe('template-driven forms integration tests', () => { input.value = ''; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toEqual(true); expect(form.controls['min_max'].errors).toBeNull(); @@ -2536,33 +2412,32 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(NgModelMinMaxValidator); fixture.componentInstance.min = -20; fixture.componentInstance.max = -10; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; const form = fixture.debugElement.children[0].injector.get(NgForm); input.value = '-30'; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toBeFalse(); expect(form.controls['min_max'].errors).toEqual({min: {min: -20, actual: -30}}); input.value = -15; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toBeTruthy(); expect(form.controls['min_max'].errors).toBeNull(); input.value = -5; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toBeFalse(); expect(form.controls['min_max'].errors).toEqual({max: {max: -10, actual: -5}}); input.value = 0; dispatchEvent(input, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.valid).toBeFalse(); expect(form.controls['min_max'].errors).toEqual({max: {max: -10, actual: 0}}); }); @@ -2636,15 +2511,14 @@ describe('template-driven forms integration tests', () => { } const fixture = initTest(NgModelNoOpValidation, NoOpValidator, NoOpAsyncValidator); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(registerOnValidatorChangeFired).toBe(1); expect(registerOnAsyncValidatorChangeFired).toBe(1); fixture.componentInstance.validatorInput = 'bar'; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + await fixture.whenStable(); // Changing validator inputs should not cause `registerOnValidatorChange` to be invoked, // since it's invoked just once during the setup phase. @@ -2659,15 +2533,14 @@ describe('template-driven forms integration tests', () => { const inputEl = fixture.debugElement.query(By.css('input')); const inputNativeEl = inputEl.nativeElement; fixture.componentInstance.name = 'oldValue'; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(inputNativeEl.value).toEqual('oldValue'); inputEl.triggerEventHandler('compositionstart'); inputNativeEl.value = 'updatedValue'; dispatchEvent(inputNativeEl, 'input'); - await timeout(); + await fixture.whenStable(); const isAndroid = /android (\d+)/.test(getDOM().getUserAgent().toLowerCase()); if (isAndroid) { @@ -2679,8 +2552,7 @@ describe('template-driven forms integration tests', () => { inputEl.triggerEventHandler('compositionend', {target: {value: 'updatedValue'}}); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(fixture.componentInstance.name).toEqual('updatedValue'); } @@ -2694,23 +2566,21 @@ describe('template-driven forms integration tests', () => { const inputEl = fixture.debugElement.query(By.css('input')); const inputNativeEl = inputEl.nativeElement; fixture.componentInstance.name = 'oldValue'; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(inputNativeEl.value).toEqual('oldValue'); inputEl.triggerEventHandler('compositionstart'); inputNativeEl.value = 'updatedValue'; dispatchEvent(inputNativeEl, 'input'); - await timeout(); + await fixture.whenStable(); // ngModel should not update when compositionstart expect(fixture.componentInstance.name).toEqual('oldValue'); inputEl.triggerEventHandler('compositionend', {target: {value: 'updatedValue'}}); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); // ngModel should update when compositionend expect(fixture.componentInstance.name).toEqual('updatedValue'); @@ -2725,15 +2595,14 @@ describe('template-driven forms integration tests', () => { const inputEl = fixture.debugElement.query(By.css('input')); const inputNativeEl = inputEl.nativeElement; fixture.componentInstance.name = 'oldValue'; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(inputNativeEl.value).toEqual('oldValue'); inputEl.triggerEventHandler('compositionstart'); inputNativeEl.value = 'updatedValue'; dispatchEvent(inputNativeEl, 'input'); - await timeout(); + await fixture.whenStable(); // ngModel should update normally expect(fixture.componentInstance.name).toEqual('updatedValue'); @@ -2745,64 +2614,59 @@ describe('template-driven forms integration tests', () => { const fixture = initTest(StandaloneNgModel); fixture.componentInstance.name = ''; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')).nativeElement; input.value = 'aa'; input.selectionStart = 1; dispatchEvent(input, 'input'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(fixture.componentInstance.name).toEqual('aa'); // Programmatically update the input value to be "bb". fixture.componentInstance.name = 'bb'; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(input.value).toEqual('bb'); // Programatically set it back to "aa". fixture.componentInstance.name = 'aa'; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(input.value).toEqual('aa'); }); it('should not crash when validity is checked from a binding', async () => { const fixture = initTest(NgModelValidBinding); - await timeout(); - expect(() => fixture.detectChanges()).not.toThrowError(); + await expectAsync(fixture.whenStable()).toBeResolved(); }); describe('cross-component boundary warning', () => { - it('should warn when ngModel in a child component cannot reach parent NgForm via @Host()', () => { + it('should warn when ngModel in a child component cannot reach parent NgForm via @Host()', async () => { const warnSpy = spyOn(console, 'warn'); const fixture = initTest(NgModelCrossComponentParent, NgModelCrossComponentChild); - fixture.detectChanges(); + await fixture.whenStable(); expect(warnSpy).toHaveBeenCalledWith(jasmine.stringContaining('NgForm')); expect(warnSpy).toHaveBeenCalledWith(jasmine.stringContaining('viewProviders')); }); - it('should warn with FormGroupDirective name when ngModel cannot reach parent FormGroupDirective via @Host()', () => { + it('should warn with FormGroupDirective name when ngModel cannot reach parent FormGroupDirective via @Host()', async () => { const warnSpy = spyOn(console, 'warn'); TestBed.configureTestingModule({ declarations: [NgModelCrossComponentFormGroupParent, NgModelCrossComponentFormGroupChild], imports: [FormsModule, ReactiveFormsModule, CommonModule], }); const fixture = TestBed.createComponent(NgModelCrossComponentFormGroupParent); - fixture.detectChanges(); + await fixture.whenStable(); expect(warnSpy).toHaveBeenCalledWith(jasmine.stringContaining('FormGroupDirective')); expect(warnSpy).toHaveBeenCalledWith(jasmine.stringContaining('viewProviders')); }); - it('should warn with NgModelGroup name when ngModel cannot reach parent NgModelGroup via @Host()', () => { + it('should warn with NgModelGroup name when ngModel cannot reach parent NgModelGroup via @Host()', async () => { const warnSpy = spyOn(console, 'warn'); const fixture = initTest(NgModelCrossComponentGroupParent, NgModelCrossComponentGroupChild); - fixture.detectChanges(); + await fixture.whenStable(); expect(warnSpy).toHaveBeenCalledWith(jasmine.stringContaining('NgModelGroup')); expect(warnSpy).not.toHaveBeenCalledWith( jasmine.stringContaining('useExisting: ControlContainer'), @@ -2812,30 +2676,28 @@ describe('template-driven forms integration tests', () => { it('should not warn when ngModel is in the same component as NgForm', async () => { const warnSpy = spyOn(console, 'warn'); const fixture = initTest(NgModelForm); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(warnSpy).not.toHaveBeenCalledWith(jasmine.stringContaining('viewProviders')); }); - it('should not warn when child component uses viewProviders to bridge ControlContainer', () => { + it('should not warn when child component uses viewProviders to bridge ControlContainer', async () => { const warnSpy = spyOn(console, 'warn'); const fixture = initTest( NgModelCrossComponentParentWithViewProviders, NgModelCrossComponentChildWithViewProviders, ); - fixture.detectChanges(); + await fixture.whenStable(); expect(warnSpy).not.toHaveBeenCalledWith(jasmine.stringContaining('viewProviders')); }); it('should not warn when ngModel is standalone with no parent form', async () => { const warnSpy = spyOn(console, 'warn'); const fixture = initTest(StandaloneNgModel); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(warnSpy).not.toHaveBeenCalledWith(jasmine.stringContaining('viewProviders')); }); - it('should not warn when ngModel in a child component uses [ngModelOptions]="{standalone: true}"', () => { + it('should not warn when ngModel in a child component uses [ngModelOptions]="{standalone: true}"', async () => { const warnSpy = spyOn(console, 'warn'); TestBed.configureTestingModule({ declarations: [ @@ -2845,18 +2707,18 @@ describe('template-driven forms integration tests', () => { imports: [FormsModule], }); const fixture = TestBed.createComponent(NgModelCrossComponentParentStandaloneOpt); - fixture.detectChanges(); + await fixture.whenStable(); expect(warnSpy).not.toHaveBeenCalledWith(jasmine.stringContaining('viewProviders')); }); - it('should not warn when ngModel inside a ControlValueAccessor uses [ngModelOptions]="{standalone: true}"', () => { + it('should not warn when ngModel inside a ControlValueAccessor uses [ngModelOptions]="{standalone: true}"', async () => { const warnSpy = spyOn(console, 'warn'); TestBed.configureTestingModule({ declarations: [NgModelCvaHostParent, NgModelCvaWithInternalNgModel], imports: [FormsModule], }); const fixture = TestBed.createComponent(NgModelCvaHostParent); - fixture.detectChanges(); + await fixture.whenStable(); expect(warnSpy).not.toHaveBeenCalledWith(jasmine.stringContaining('viewProviders')); }); }); @@ -3094,7 +2956,8 @@ class NgModelEmailValidator { }) class NgAsyncValidator implements AsyncValidator { validate(c: AbstractControl) { - return Promise.resolve(null); + // Delay validation so the scheduled renders can observe the pending state. + return timeout(100).then(() => null); } } diff --git a/packages/forms/test/value_accessor_integration_spec.ts b/packages/forms/test/value_accessor_integration_spec.ts index 4f474db62d9..fce40422527 100644 --- a/packages/forms/test/value_accessor_integration_spec.ts +++ b/packages/forms/test/value_accessor_integration_spec.ts @@ -54,14 +54,14 @@ describe('value accessors', () => { return TestBed.createComponent(component); } - it('should support without type', () => { + it('should support without type', async () => { TestBed.overrideComponent(FormControlComp, { set: {template: ``}, }); const fixture = initTest(FormControlComp); const control = new FormControl('old'); fixture.componentInstance.control = control; - fixture.detectChanges(); + await fixture.whenStable(); // model -> view const input = fixture.debugElement.query(By.css('input')); @@ -74,11 +74,11 @@ describe('value accessors', () => { expect(control.value).toEqual('new'); }); - it('should support ', () => { + it('should support ', async () => { const fixture = initTest(FormGroupComp); const form = new FormGroup({'login': new FormControl('old')}); fixture.componentInstance.form = form; - fixture.detectChanges(); + await fixture.whenStable(); // model -> view const input = fixture.debugElement.query(By.css('input')); @@ -91,11 +91,11 @@ describe('value accessors', () => { expect(form.value).toEqual({'login': 'new'}); }); - it('should ignore the change event for ', () => { + it('should ignore the change event for ', async () => { const fixture = initTest(FormGroupComp); const form = new FormGroup({'login': new FormControl('oldValue')}); fixture.componentInstance.form = form; - fixture.detectChanges(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')); form.valueChanges.subscribe({ @@ -108,14 +108,14 @@ describe('value accessors', () => { dispatchEvent(input.nativeElement, 'change'); }); - it('should support `}, }); const fixture = initTest(FormControlComp); const control = new FormControl('old'); fixture.componentInstance.control = control; - fixture.detectChanges(); + await fixture.whenStable(); // model -> view const textarea = fixture.debugElement.query(By.css('textarea')); @@ -128,14 +128,14 @@ describe('value accessors', () => { expect(control.value).toEqual('new'); }); - it('should support ', () => { + it('should support ', async () => { TestBed.overrideComponent(FormControlComp, { set: {template: ``}, }); const fixture = initTest(FormControlComp); const control = new FormControl(true); fixture.componentInstance.control = control; - fixture.detectChanges(); + await fixture.whenStable(); // model -> view const input = fixture.debugElement.query(By.css('input')); @@ -149,11 +149,11 @@ describe('value accessors', () => { }); describe('should support ', () => { - it('with basic use case', () => { + it('with basic use case', async () => { const fixture = initTest(FormControlNumberInput); const control = new FormControl(10); fixture.componentInstance.control = control; - fixture.detectChanges(); + await fixture.whenStable(); // model -> view const input = fixture.debugElement.query(By.css('input')); @@ -166,11 +166,11 @@ describe('value accessors', () => { expect(control.value).toEqual(20); }); - it('when value is cleared in the UI', () => { + it('when value is cleared in the UI', async () => { const fixture = initTest(FormControlNumberInput); const control = new FormControl(10, Validators.required); fixture.componentInstance.control = control; - fixture.detectChanges(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')); input.nativeElement.value = ''; @@ -186,11 +186,11 @@ describe('value accessors', () => { expect(control.value).toEqual(0); }); - it('should ignore the change event', () => { + it('should ignore the change event', async () => { const fixture = initTest(FormControlNumberInput); const control = new FormControl(); fixture.componentInstance.control = control; - fixture.detectChanges(); + await fixture.whenStable(); control.valueChanges.subscribe({ next: (value) => { @@ -203,11 +203,11 @@ describe('value accessors', () => { dispatchEvent(input.nativeElement, 'change'); }); - it('when value is cleared programmatically', () => { + it('when value is cleared programmatically', async () => { const fixture = initTest(FormControlNumberInput); const control = new FormControl(10); fixture.componentInstance.control = control; - fixture.detectChanges(); + await fixture.whenStable(); control.setValue(null); @@ -218,10 +218,10 @@ describe('value accessors', () => { describe('select controls', () => { describe('in reactive forms', () => { - it(`should support primitive values`, () => { + it(`should support primitive values`, async () => { if (isNode) return; const fixture = initTest(FormControlNameSelect); - fixture.detectChanges(); + await fixture.whenStable(); // model -> view const select = fixture.debugElement.query(By.css('select')); @@ -231,17 +231,17 @@ describe('value accessors', () => { select.nativeElement.value = 'NY'; dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); + await fixture.whenStable(); // view -> model expect(sfOption.nativeElement.selected).toBe(false); expect(fixture.componentInstance.form.value).toEqual({'city': 'NY'}); }); - it(`should support objects`, () => { + it(`should support objects`, async () => { if (isNode) return; const fixture = initTest(FormControlSelectNgValue); - fixture.detectChanges(); + await fixture.whenStable(); // model -> view const select = fixture.debugElement.query(By.css('select')); @@ -250,18 +250,18 @@ describe('value accessors', () => { expect(sfOption.nativeElement.selected).toBe(true); }); - it('should throw an error if compareWith is not a function', () => { + it('should throw an error if compareWith is not a function', async () => { const fixture = initTest(FormControlSelectWithCompareFn); fixture.componentInstance.compareFn = null!; - expect(() => fixture.detectChanges()).toThrowError( + await expectAsync(fixture.whenStable()).toBeRejectedWithError( /compareWith must be a function, but received null/, ); }); - it('should compare options using provided compareWith function', () => { + it('should compare options using provided compareWith function', async () => { if (isNode) return; const fixture = initTest(FormControlSelectWithCompareFn); - fixture.detectChanges(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); const sfOption = fixture.debugElement.query(By.css('option')); @@ -269,17 +269,17 @@ describe('value accessors', () => { expect(sfOption.nativeElement.selected).toBe(true); }); - it('should support re-assigning the options array with compareWith', () => { + it('should support re-assigning the options array with compareWith', async () => { if (isNode) return; const fixture = initTest(FormControlSelectWithCompareFn); - fixture.detectChanges(); + await fixture.whenStable(); // Option IDs start out as 0 and 1, so setting the select value to "1: Object" // will select the second option (NY). const select = fixture.debugElement.query(By.css('select')); select.nativeElement.value = '1: Object'; dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.form.value).toEqual({city: {id: 2, name: 'NY'}}); @@ -288,7 +288,7 @@ describe('value accessors', () => { {id: 2, name: 'NY'}, ]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + await fixture.whenStable(); // Now that the options array has been re-assigned, new option instances will // be created by ngFor. These instances will have different option IDs, subsequent @@ -307,8 +307,7 @@ describe('value accessors', () => { const comp = fixture.componentInstance; comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}]; comp.selectedCity = comp.cities[1]; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); const nycOption = fixture.debugElement.queryAll(By.css('option'))[1]; @@ -319,8 +318,7 @@ describe('value accessors', () => { select.nativeElement.value = '2: Object'; dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); // view -> model expect(comp.selectedCity['name']).toEqual('Buffalo'); @@ -333,14 +331,12 @@ describe('value accessors', () => { comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; comp.selectedCity = comp.cities[1]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); comp.cities.push({'name': 'Buffalo'}); comp.selectedCity = comp.cities[2]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); const buffalo = fixture.debugElement.queryAll(By.css('option'))[2]; @@ -354,16 +350,14 @@ describe('value accessors', () => { comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; comp.selectedCity = comp.cities[1]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); expect(select.nativeElement.value).toEqual('1: Object'); comp.cities.pop(); fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(select.nativeElement.value).not.toEqual('1: Object'); }); @@ -375,12 +369,11 @@ describe('value accessors', () => { comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'NYC'}]; comp.selectedCity = comp.cities[0]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + await fixture.whenStable(); comp.selectedCity = comp.cities[2]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); const secondNYC = fixture.debugElement.queryAll(By.css('option'))[2]; @@ -400,21 +393,19 @@ describe('value accessors', () => { select.nativeElement.value = '2: Object'; dispatchEvent(select.nativeElement, 'change'); await fixture.whenStable(); - await timeout(); expect(comp.selectedCity!['name']).toEqual('NYC'); select.nativeElement.value = '0: null'; dispatchEvent(select.nativeElement, 'change'); await fixture.whenStable(); - await timeout(); expect(comp.selectedCity).toEqual(null); }); - it('should throw an error when compareWith is not a function', () => { + it('should throw an error when compareWith is not a function', async () => { const fixture = initTest(NgModelSelectWithCustomCompareFnForm); const comp = fixture.componentInstance; comp.compareFn = null!; - expect(() => fixture.detectChanges()).toThrowError( + await expectAsync(fixture.whenStable()).toBeRejectedWithError( /compareWith must be a function, but received null/, ); }); @@ -428,8 +419,7 @@ describe('value accessors', () => { {id: 1, name: 'SF'}, {id: 2, name: 'LA'}, ]; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); const sfOption = fixture.debugElement.query(By.css('option')); @@ -446,15 +436,14 @@ describe('value accessors', () => { {id: 2, name: 'NY'}, ]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); // Option IDs start out as 0 and 1, so setting the select value to "1: Object" // will select the second option (NY). const select = fixture.debugElement.query(By.css('select')); select.nativeElement.value = '1: Object'; dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); + await fixture.whenStable(); const model = fixture.debugElement.children[0].injector.get(NgModel); expect(model.value).toEqual({id: 2, name: 'NY'}); @@ -464,8 +453,7 @@ describe('value accessors', () => { {id: 2, name: 'NY'}, ]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); // Now that the options array has been re-assigned, new option instances will // be created by ngFor. These instances will have different option IDs, subsequent @@ -480,10 +468,10 @@ describe('value accessors', () => { describe('select multiple controls', () => { describe('in reactive forms', () => { - it('should support primitive values', () => { + it('should support primitive values', async () => { if (isNode) return; const fixture = initTest(FormControlSelectMultiple); - fixture.detectChanges(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); const sfOption = fixture.debugElement.query(By.css('option')); @@ -491,10 +479,10 @@ describe('value accessors', () => { expect(sfOption.nativeElement.selected).toBe(true); }); - it('should support objects', () => { + it('should support objects', async () => { if (isNode) return; const fixture = initTest(FormControlSelectMultipleNgValue); - fixture.detectChanges(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); const sfOption = fixture.debugElement.query(By.css('option')); @@ -502,10 +490,10 @@ describe('value accessors', () => { expect(sfOption.nativeElement.selected).toBe(true); }); - it('should throw an error when compareWith is not a function', () => { + it('should throw an error when compareWith is not a function', async () => { const fixture = initTest(FormControlSelectMultipleWithCompareFn); fixture.componentInstance.compareFn = null!; - expect(() => fixture.detectChanges()).toThrowError( + await expectAsync(fixture.whenStable()).toBeRejectedWithError( /compareWith must be a function, but received null/, ); }); @@ -513,8 +501,7 @@ describe('value accessors', () => { it('should compare options using provided compareWith function', async () => { if (isNode) return; const fixture = initTest(FormControlSelectMultipleWithCompareFn); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); const sfOption = fixture.debugElement.query(By.css('option')); @@ -533,22 +520,17 @@ describe('value accessors', () => { comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}]; }); - const detectChangesAndTick = async (): Promise => { - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); - }; - const setSelectedCities = async (selectedCities: any): Promise => { comp.selectedCities = selectedCities; - await detectChangesAndTick(); + fixture.changeDetectorRef.markForCheck(); + await fixture.whenStable(); }; - const selectOptionViaUI = (valueString: string): void => { + const selectOptionViaUI = async (valueString: string): Promise => { const select = fixture.debugElement.query(By.css('select')); select.nativeElement.value = valueString; dispatchEvent(select.nativeElement, 'change'); - detectChangesAndTick(); + await fixture.whenStable(); }; const assertOptionElementSelectedState = (selectedStates: boolean[]): void => { @@ -570,7 +552,7 @@ describe('value accessors', () => { ).and.callThrough(); await setSelectedCities([]); - selectOptionViaUI('1: Object'); + await selectOptionViaUI('1: Object'); assertOptionElementSelectedState([false, true, false]); expect(spy).toHaveBeenCalled(); }); @@ -579,11 +561,12 @@ describe('value accessors', () => { if (isNode) return; await setSelectedCities([]); - selectOptionViaUI('1: Object'); + await selectOptionViaUI('1: Object'); assertOptionElementSelectedState([false, true, false]); comp.cities.push({'name': 'Chicago'}); - await detectChangesAndTick(); + fixture.changeDetectorRef.markForCheck(); + await fixture.whenStable(); assertOptionElementSelectedState([false, true, false, false]); }); @@ -592,21 +575,22 @@ describe('value accessors', () => { if (isNode) return; await setSelectedCities([]); - selectOptionViaUI('1: Object'); + await selectOptionViaUI('1: Object'); assertOptionElementSelectedState([false, true, false]); comp.cities.pop(); - await detectChangesAndTick(); + fixture.changeDetectorRef.markForCheck(); + await fixture.whenStable(); assertOptionElementSelectedState([false, true]); }); }); - it('should throw an error when compareWith is not a function', () => { + it('should throw an error when compareWith is not a function', async () => { const fixture = initTest(NgModelSelectMultipleWithCustomCompareFnForm); const comp = fixture.componentInstance; comp.compareFn = null!; - expect(() => fixture.detectChanges()).toThrowError( + await expectAsync(fixture.whenStable()).toBeRejectedWithError( /compareWith must be a function, but received null/, ); }); @@ -620,8 +604,7 @@ describe('value accessors', () => { {id: 2, name: 'LA'}, ]; comp.selectedCities = [comp.cities[0]]; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); const sfOption = fixture.debugElement.query(By.css('option')); @@ -632,14 +615,14 @@ describe('value accessors', () => { describe('should support ', () => { describe('in reactive forms', () => { - it('should support basic functionality', () => { + it('should support basic functionality', async () => { const fixture = initTest(FormControlRadioButtons); const form = new FormGroup({ 'food': new FormControl('fish'), 'drink': new FormControl('sprite'), }); fixture.componentInstance.form = form; - fixture.detectChanges(); + await fixture.whenStable(); // model -> view const inputs = fixture.debugElement.queryAll(By.css('input')); @@ -647,14 +630,14 @@ describe('value accessors', () => { expect(inputs[1].nativeElement.checked).toEqual(true); dispatchEvent(inputs[0].nativeElement, 'change'); - fixture.detectChanges(); + await fixture.whenStable(); // view -> model expect(form.get('food')!.value).toEqual('chicken'); expect(inputs[1].nativeElement.checked).toEqual(false); form.get('food')!.setValue('fish'); - fixture.detectChanges(); + await fixture.whenStable(); // programmatic change -> view expect(inputs[0].nativeElement.checked).toEqual(false); @@ -677,63 +660,63 @@ describe('value accessors', () => { expect(getRadioCheckedStates(fixture)).toEqual([false, true]); }); - it('should support an initial undefined value', () => { + it('should support an initial undefined value', async () => { const fixture = initTest(FormControlRadioButtons); const form = new FormGroup({'food': new FormControl(), 'drink': new FormControl()}); fixture.componentInstance.form = form; - fixture.detectChanges(); + await fixture.whenStable(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.checked).toEqual(false); expect(inputs[1].nativeElement.checked).toEqual(false); }); - it('should reset properly', () => { + it('should reset properly', async () => { const fixture = initTest(FormControlRadioButtons); const form = new FormGroup({ 'food': new FormControl('fish'), 'drink': new FormControl('sprite'), }); fixture.componentInstance.form = form; - fixture.detectChanges(); + await fixture.whenStable(); form.reset(); - fixture.detectChanges(); + await fixture.whenStable(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.checked).toEqual(false); expect(inputs[1].nativeElement.checked).toEqual(false); }); - it('should properly set value to null and undefined', () => { + it('should properly set value to null and undefined', async () => { const fixture = initTest(FormControlRadioButtons); const form: FormGroup = new FormGroup({ 'food': new FormControl('chicken'), 'drink': new FormControl('sprite'), }); fixture.componentInstance.form = form; - fixture.detectChanges(); + await fixture.whenStable(); form.get('food')!.setValue(null); - fixture.detectChanges(); + await fixture.whenStable(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.checked).toEqual(false); form.get('food')!.setValue('chicken'); - fixture.detectChanges(); + await fixture.whenStable(); form.get('food')!.setValue(undefined); - fixture.detectChanges(); + await fixture.whenStable(); expect(inputs[0].nativeElement.checked).toEqual(false); }); - it('should use formControlName to group radio buttons when name is absent', () => { + it('should use formControlName to group radio buttons when name is absent', async () => { const fixture = initTest(FormControlRadioButtons); const foodCtrl = new FormControl('fish'); const drinkCtrl = new FormControl('sprite'); fixture.componentInstance.form = new FormGroup({'food': foodCtrl, 'drink': drinkCtrl}); - fixture.detectChanges(); + await fixture.whenStable(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.checked).toEqual(false); @@ -743,7 +726,7 @@ describe('value accessors', () => { dispatchEvent(inputs[0].nativeElement, 'change'); inputs[0].nativeElement.checked = true; - fixture.detectChanges(); + await fixture.whenStable(); const value = fixture.componentInstance.form.value; expect(value.food).toEqual('chicken'); @@ -752,7 +735,7 @@ describe('value accessors', () => { expect(inputs[3].nativeElement.checked).toEqual(true); drinkCtrl.setValue('cola'); - fixture.detectChanges(); + await fixture.whenStable(); expect(inputs[0].nativeElement.checked).toEqual(true); expect(inputs[1].nativeElement.checked).toEqual(false); @@ -760,7 +743,7 @@ describe('value accessors', () => { expect(inputs[3].nativeElement.checked).toEqual(false); }); - it('should support removing controls from ', () => { + it('should support removing controls from ', async () => { const fixture = initTest(FormControlRadioButtons); const showRadio = new FormControl('yes'); const form: FormGroup = new FormGroup({ @@ -774,16 +757,16 @@ describe('value accessors', () => { ? form.addControl('food', new FormControl('fish')) : form.removeControl('food'); }); - fixture.detectChanges(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('[value="no"]')); dispatchEvent(input.nativeElement, 'change'); - fixture.detectChanges(); + await fixture.whenStable(); expect(form.value).toEqual({drink: 'sprite'}); }); - it('should differentiate controls on different levels with the same name', () => { + it('should differentiate controls on different levels with the same name', async () => { TestBed.overrideComponent(FormControlRadioButtons, { set: { template: ` @@ -804,7 +787,7 @@ describe('value accessors', () => { nested: new FormGroup({food: new FormControl('fish')}), }); fixture.componentInstance.form = form; - fixture.detectChanges(); + await fixture.whenStable(); // model -> view const inputs = fixture.debugElement.queryAll(By.css('input')); @@ -814,7 +797,7 @@ describe('value accessors', () => { expect(inputs[3].nativeElement.checked).toEqual(true); dispatchEvent(inputs[0].nativeElement, 'change'); - fixture.detectChanges(); + await fixture.whenStable(); // view -> model expect(form.get('food')!.value).toEqual('chicken'); @@ -825,11 +808,11 @@ describe('value accessors', () => { expect(inputs[3].nativeElement.checked).toEqual(true); }); - it('should disable all radio buttons when disable() is called', () => { + it('should disable all radio buttons when disable() is called', async () => { const fixture = initTest(FormControlRadioButtons); const form = new FormGroup({food: new FormControl('fish'), drink: new FormControl('cola')}); fixture.componentInstance.form = form; - fixture.detectChanges(); + await fixture.whenStable(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.disabled).toEqual(false); @@ -856,14 +839,14 @@ describe('value accessors', () => { expect(inputs[3].nativeElement.disabled).toEqual(false); }); - it('should disable all radio buttons when initially disabled', () => { + it('should disable all radio buttons when initially disabled', async () => { const fixture = initTest(FormControlRadioButtons); const form = new FormGroup({ food: new FormControl({value: 'fish', disabled: true}), drink: new FormControl('cola'), }); fixture.componentInstance.form = form; - fixture.detectChanges(); + await fixture.whenStable(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.disabled).toEqual(true); @@ -872,21 +855,22 @@ describe('value accessors', () => { expect(inputs[3].nativeElement.disabled).toEqual(false); }); - it('should work with reusing controls', () => { + it('should work with reusing controls', async () => { const fixture = initTest(FormControlRadioButtons); const food = new FormControl('chicken'); fixture.componentInstance.form = new FormGroup({ 'food': food, 'drink': new FormControl(''), }); - fixture.detectChanges(); + await fixture.whenStable(); const newForm = new FormGroup({'food': food, 'drink': new FormControl('')}); fixture.componentInstance.form = newForm; - fixture.detectChanges(); + fixture.changeDetectorRef.markForCheck(); + await fixture.whenStable(); newForm.setValue({food: 'fish', drink: ''}); - fixture.detectChanges(); + await fixture.whenStable(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.checked).toBe(false); expect(inputs[1].nativeElement.checked).toBe(true); @@ -897,8 +881,7 @@ describe('value accessors', () => { it('should support basic functionality', async () => { const fixture = initTest(NgModelRadioForm); fixture.componentInstance.food = 'fish'; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); // model -> view const inputs = fixture.debugElement.queryAll(By.css('input')); @@ -906,7 +889,7 @@ describe('value accessors', () => { expect(inputs[1].nativeElement.checked).toEqual(true); dispatchEvent(inputs[0].nativeElement, 'change'); - await timeout(); + await fixture.whenStable(); // view -> model expect(fixture.componentInstance.food).toEqual('chicken'); @@ -917,8 +900,7 @@ describe('value accessors', () => { const fixture = initTest(NgModelRadioForm); fixture.componentInstance.food = 'fish'; fixture.componentInstance.drink = 'sprite'; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.checked).toEqual(false); @@ -927,7 +909,7 @@ describe('value accessors', () => { expect(inputs[3].nativeElement.checked).toEqual(true); dispatchEvent(inputs[0].nativeElement, 'change'); - await timeout(); + await fixture.whenStable(); expect(fixture.componentInstance.food).toEqual('chicken'); expect(fixture.componentInstance.drink).toEqual('sprite'); @@ -938,8 +920,7 @@ describe('value accessors', () => { it('should support initial undefined value', async () => { const fixture = initTest(NgModelRadioForm); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.checked).toEqual(false); @@ -951,13 +932,11 @@ describe('value accessors', () => { it('should support resetting properly', async () => { const fixture = initTest(NgModelRadioForm); fixture.componentInstance.food = 'chicken'; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.query(By.css('form')); dispatchEvent(form.nativeElement, 'reset'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.checked).toEqual(false); @@ -968,13 +947,11 @@ describe('value accessors', () => { const fixture = initTest(NgModelRadioForm); fixture.componentInstance.food = 'chicken'; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); fixture.componentInstance.food = null!; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.checked).toEqual(false); @@ -982,13 +959,11 @@ describe('value accessors', () => { fixture.componentInstance.food = 'chicken'; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); fixture.componentInstance.food = undefined!; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(inputs[0].nativeElement.checked).toEqual(false); expect(inputs[1].nativeElement.checked).toEqual(false); }); @@ -996,12 +971,11 @@ describe('value accessors', () => { it('should disable radio controls properly with programmatic call', async () => { const fixture = initTest(NgModelRadioForm); fixture.componentInstance.food = 'fish'; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const form = fixture.debugElement.children[0].injector.get(NgForm); form.control.get('food')!.disable(); - await timeout(); + await fixture.whenStable(); const inputs = fixture.debugElement.queryAll(By.css('input')); expect(inputs[0].nativeElement.disabled).toBe(true); @@ -1010,7 +984,7 @@ describe('value accessors', () => { expect(inputs[3].nativeElement.disabled).toBe(false); form.control.disable(); - await timeout(); + await fixture.whenStable(); expect(inputs[0].nativeElement.disabled).toBe(true); expect(inputs[1].nativeElement.disabled).toBe(true); @@ -1018,7 +992,7 @@ describe('value accessors', () => { expect(inputs[3].nativeElement.disabled).toBe(true); form.control.enable(); - await timeout(); + await fixture.whenStable(); expect(inputs[0].nativeElement.disabled).toBe(false); expect(inputs[1].nativeElement.disabled).toBe(false); @@ -1030,11 +1004,11 @@ describe('value accessors', () => { describe('should support ', () => { describe('in reactive forms', () => { - it('with basic use case', () => { + it('with basic use case', async () => { const fixture = initTest(FormControlRangeInput); const control = new FormControl(10); fixture.componentInstance.control = control; - fixture.detectChanges(); + await fixture.whenStable(); // model -> view const input = fixture.debugElement.query(By.css('input')); @@ -1047,11 +1021,11 @@ describe('value accessors', () => { expect(control.value).toEqual(20); }); - it('when value is cleared in the UI', () => { + it('when value is cleared in the UI', async () => { const fixture = initTest(FormControlNumberInput); const control = new FormControl(10, Validators.required); fixture.componentInstance.control = control; - fixture.detectChanges(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input')); input.nativeElement.value = ''; @@ -1067,11 +1041,11 @@ describe('value accessors', () => { expect(control.value).toEqual(0); }); - it('when value is cleared programmatically', () => { + it('when value is cleared programmatically', async () => { const fixture = initTest(FormControlNumberInput); const control = new FormControl(10); fixture.componentInstance.control = control; - fixture.detectChanges(); + await fixture.whenStable(); control.setValue(null); @@ -1082,10 +1056,10 @@ describe('value accessors', () => { describe('select controls', () => { describe('in reactive forms', () => { - it(`should support primitive values`, () => { + it(`should support primitive values`, async () => { if (isNode) return; const fixture = initTest(FormControlNameSelect); - fixture.detectChanges(); + await fixture.whenStable(); // model -> view const select = fixture.debugElement.query(By.css('select')); @@ -1095,17 +1069,17 @@ describe('value accessors', () => { select.nativeElement.value = 'NY'; dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); + await fixture.whenStable(); // view -> model expect(sfOption.nativeElement.selected).toBe(false); expect(fixture.componentInstance.form.value).toEqual({'city': 'NY'}); }); - it(`should support objects`, () => { + it(`should support objects`, async () => { if (isNode) return; const fixture = initTest(FormControlSelectNgValue); - fixture.detectChanges(); + await fixture.whenStable(); // model -> view const select = fixture.debugElement.query(By.css('select')); @@ -1114,18 +1088,18 @@ describe('value accessors', () => { expect(sfOption.nativeElement.selected).toBe(true); }); - it('should throw an error if compareWith is not a function', () => { + it('should throw an error if compareWith is not a function', async () => { const fixture = initTest(FormControlSelectWithCompareFn); fixture.componentInstance.compareFn = null!; - expect(() => fixture.detectChanges()).toThrowError( + await expectAsync(fixture.whenStable()).toBeRejectedWithError( /compareWith must be a function, but received null/, ); }); - it('should compare options using provided compareWith function', () => { + it('should compare options using provided compareWith function', async () => { if (isNode) return; const fixture = initTest(FormControlSelectWithCompareFn); - fixture.detectChanges(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); const sfOption = fixture.debugElement.query(By.css('option')); @@ -1133,10 +1107,10 @@ describe('value accessors', () => { expect(sfOption.nativeElement.selected).toBe(true); }); - it('should support re-assigning the options array with compareWith', () => { + it('should support re-assigning the options array with compareWith', async () => { if (isNode) return; const fixture = initTest(FormControlSelectWithCompareFn); - fixture.detectChanges(); + await fixture.whenStable(); // Option IDs start out as 0 and 1, so setting the select value to "1: Object" // will select the second option (NY). @@ -1145,7 +1119,7 @@ describe('value accessors', () => { select.nativeElement.value = '1: Object'; dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.form.value).toEqual({city: {id: 2, name: 'NY'}}); @@ -1154,7 +1128,7 @@ describe('value accessors', () => { {id: 2, name: 'NY'}, ]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + await fixture.whenStable(); // Now that the options array has been re-assigned, new option instances will // be created by ngFor. These instances will have different option IDs, subsequent @@ -1165,10 +1139,10 @@ describe('value accessors', () => { expect(nyOption.nativeElement.selected).toBe(true); }); - it('should support re-assigning the options array with compareWith and trackBy', () => { + it('should support re-assigning the options array with compareWith and trackBy', async () => { if (isNode) return; const fixture = initTest(FormControlSelectWithCompareTrackByFn); - fixture.detectChanges(); + await fixture.whenStable(); // Option IDs start out as 0 and 1, so setting the select value to "1: Object" // will select the second option (NY). @@ -1177,7 +1151,7 @@ describe('value accessors', () => { select.nativeElement.value = '1: Object'; dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.form.value).toEqual({city: {id: 2, name: 'NY'}}); @@ -1186,7 +1160,7 @@ describe('value accessors', () => { {id: 4, name: 'BXL'}, ]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + await fixture.whenStable(); // using trackBy, instances can be re-used, the option IDs stays the same but their // (ng)value can change @@ -1202,10 +1176,10 @@ describe('value accessors', () => { expect(fixture.componentInstance.form.value).toEqual({city: {id: 2, name: 'NY'}}); }); - it('should keep current value when selected option is removed/replaced', () => { + it('should keep current value when selected option is removed/replaced', async () => { if (isNode) return; const fixture = initTest(FormControlSelectWithCompareFn); - fixture.detectChanges(); + await fixture.whenStable(); // Option IDs start out as 0 and 1, so setting the select value to "1: Object" // will select the second option (NY). @@ -1214,7 +1188,7 @@ describe('value accessors', () => { select.nativeElement.value = '1: Object'; dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.form.value).toEqual({city: {id: 2, name: 'NY'}}); @@ -1223,7 +1197,7 @@ describe('value accessors', () => { {id: 3, name: 'LA'}, ]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + await fixture.whenStable(); // Now that the options array has been re-assigned, new option instances will // be created by ngFor. These instances will have different option IDs, subsequent @@ -1244,24 +1218,24 @@ describe('value accessors', () => { ); }); - it('should call compareWith once for each added option until a match is found', () => { + it('should call compareWith once for each added option until a match is found', async () => { // see issue #41330 if (isNode) return; const fixture = initTest(FormControlSelectWithComparePerfFn); - fixture.detectChanges(); + await fixture.whenStable(); // compareWith should only be called once since first city is selected expect(fixture.componentInstance.compareFnCalls).toEqual(1); }); - it('should not call compareWith for removed options', () => { + it('should not call compareWith for removed options', async () => { if (isNode) return; const fixture = initTest(FormControlSelectWithComparePerfFn); - fixture.detectChanges(); + await fixture.whenStable(); fixture.componentInstance.compareFnCalls = 0; fixture.componentInstance.cities.splice(2, 2); fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + await fixture.whenStable(); // compareWith should only be called once since first city is still selected expect(fixture.componentInstance.compareFnCalls).toBe(1); @@ -1275,8 +1249,7 @@ describe('value accessors', () => { const comp = fixture.componentInstance; comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}]; comp.selectedCity = comp.cities[1]; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); const nycOption = fixture.debugElement.queryAll(By.css('option'))[1]; @@ -1287,8 +1260,7 @@ describe('value accessors', () => { select.nativeElement.value = '2: Object'; dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); // view -> model expect(comp.selectedCity['name']).toEqual('Buffalo'); @@ -1301,14 +1273,12 @@ describe('value accessors', () => { comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; comp.selectedCity = comp.cities[1]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); comp.cities.push({'name': 'Buffalo'}); comp.selectedCity = comp.cities[2]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); const buffalo = fixture.debugElement.queryAll(By.css('option'))[2]; @@ -1321,13 +1291,11 @@ describe('value accessors', () => { if (isNode) return; const fixture = initTest(NgModelSelectForm); const comp = fixture.componentInstance; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); comp.cities.push({name: 'Minneapolis'}); fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); expect(select.nativeElement.selectedIndex).toEqual(-1); @@ -1340,8 +1308,7 @@ describe('value accessors', () => { const fixture = initTest(NgModelSelectWithPlaceholderForm); const comp = fixture.componentInstance; comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const placeholder = fixture.debugElement.queryAll(By.css('option'))[0]; expect(placeholder.nativeElement.selected).toBe(true); @@ -1354,16 +1321,14 @@ describe('value accessors', () => { comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; comp.selectedCity = comp.cities[1]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); expect(select.nativeElement.value).toEqual('1: Object'); comp.cities.pop(); fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); // no option should be selected, since ngModel doesn't match anything in cities array expect(select.nativeElement.value).not.toEqual('1: Object'); @@ -1390,7 +1355,6 @@ describe('value accessors', () => { const comp = fixture.componentInstance; comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; comp.selectedCity = comp.cities[1]; - fixture.autoDetectChanges(); await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); @@ -1412,12 +1376,11 @@ describe('value accessors', () => { comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'NYC'}]; comp.selectedCity = comp.cities[0]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + await fixture.whenStable(); comp.selectedCity = comp.cities[2]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); const secondNYC = fixture.debugElement.queryAll(By.css('option'))[2]; @@ -1431,30 +1394,28 @@ describe('value accessors', () => { comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; comp.selectedCity = null; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); select.nativeElement.value = '2: Object'; dispatchEvent(select.nativeElement, 'change'); fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(comp.selectedCity!['name']).toEqual('NYC'); select.nativeElement.value = '0: null'; dispatchEvent(select.nativeElement, 'change'); fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); expect(comp.selectedCity).toEqual(null); }); - it('should throw an error when compareWith is not a function', () => { + it('should throw an error when compareWith is not a function', async () => { const fixture = initTest(NgModelSelectWithCustomCompareFnForm); const comp = fixture.componentInstance; comp.compareFn = null!; - expect(() => fixture.detectChanges()).toThrowError( + await expectAsync(fixture.whenStable()).toBeRejectedWithError( /compareWith must be a function, but received null/, ); }); @@ -1468,8 +1429,7 @@ describe('value accessors', () => { {id: 1, name: 'SF'}, {id: 2, name: 'LA'}, ]; - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); const select = fixture.debugElement.query(By.css('select')); const sfOption = fixture.debugElement.query(By.css('option')); @@ -1486,15 +1446,14 @@ describe('value accessors', () => { {id: 2, name: 'NY'}, ]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); // Option IDs start out as 0 and 1, so setting the select value to "1: Object" // will select the second option (NY). const select = fixture.debugElement.query(By.css('select')); select.nativeElement.value = '1: Object'; dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); + await fixture.whenStable(); const model = fixture.debugElement.children[0].injector.get(NgModel); expect(model.value).toEqual({id: 2, name: 'NY'}); @@ -1504,8 +1463,7 @@ describe('value accessors', () => { {id: 2, name: 'NY'}, ]; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - await timeout(); + await fixture.whenStable(); // Now that the options array has been re-assigned, new option instances will // be created by ngFor. These instances will have different option IDs, subsequent @@ -1521,11 +1479,11 @@ describe('value accessors', () => { describe('custom value accessors', () => { describe('in reactive forms', () => { - it('should support basic functionality', () => { + it('should support basic functionality', async () => { const fixture = initTest(WrappedValueForm, WrappedValue); const form = new FormGroup({'login': new FormControl('aa')}); fixture.componentInstance.form = form; - fixture.detectChanges(); + await fixture.whenStable(); // model -> view const input = fixture.debugElement.query(By.css('input')); @@ -1543,10 +1501,10 @@ describe('value accessors', () => { expect(form.get('login')!.errors).toEqual(null); }); - it("should support non builtin input elements that fire a change event without a 'target' property", () => { + it("should support non builtin input elements that fire a change event without a 'target' property", async () => { const fixture = initTest(MyInputForm, MyInput); fixture.componentInstance.form = new FormGroup({'login': new FormControl('aa')}); - fixture.detectChanges(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('my-input')); expect(input.componentInstance.value).toEqual('!aa!'); @@ -1558,23 +1516,23 @@ describe('value accessors', () => { input.componentInstance.dispatchChangeEvent(); }); - it('should support custom accessors without setDisabledState - formControlName', () => { + it('should support custom accessors without setDisabledState - formControlName', async () => { const fixture = initTest(WrappedValueForm, WrappedValue); fixture.componentInstance.form = new FormGroup({ 'login': new FormControl({value: 'aa', disabled: true}), }); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.form.status).toEqual('DISABLED'); expect(fixture.componentInstance.form.get('login')!.status).toEqual('DISABLED'); }); - it('should support custom accessors without setDisabledState - formControlDirective', () => { + it('should support custom accessors without setDisabledState - formControlDirective', async () => { TestBed.overrideComponent(FormControlComp, { set: {template: ``}, }); const fixture = initTest(FormControlComp); fixture.componentInstance.control = new FormControl({value: 'aa', disabled: true}); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.control.status).toEqual('DISABLED'); }); @@ -1585,11 +1543,11 @@ describe('value accessors', () => { fixture = initTest(CvaWithDisabledStateForm, CvaWithDisabledState); }); - it('sets the disabled state when the control is initially disabled', () => { + it('sets the disabled state when the control is initially disabled', async () => { fixture.componentInstance.form = new FormGroup({ 'login': new FormControl({value: 'aa', disabled: true}), }); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.form.status).toEqual('DISABLED'); expect(fixture.componentInstance.form.get('login')!.status).toEqual('DISABLED'); @@ -1599,11 +1557,11 @@ describe('value accessors', () => { ).toContain('DISABLED'); }); - it('sets the enabled state when the control is initially enabled', () => { + it('sets the enabled state when the control is initially enabled', async () => { fixture.componentInstance.form = new FormGroup({ 'login': new FormControl({value: 'aa', disabled: false}), }); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.form.status).toEqual('VALID'); expect(fixture.componentInstance.form.get('login')!.status).toEqual('VALID'); @@ -1614,10 +1572,10 @@ describe('value accessors', () => { }); }); - it('should populate control in ngOnInit when injecting NgControl', () => { + it('should populate control in ngOnInit when injecting NgControl', async () => { const fixture = initTest(MyInputForm, MyInput); fixture.componentInstance.form = new FormGroup({'login': new FormControl('aa')}); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.myInput!.control).toBeDefined(); expect(fixture.componentInstance.myInput!.control).toEqual( @@ -1630,9 +1588,6 @@ describe('value accessors', () => { it('should support standard writing to view and model', async () => { const fixture = initTest(NgModelCustomWrapper, NgModelCustomComp); fixture.componentInstance.name = 'Nancy'; - fixture.detectChanges(); - await fixture.whenStable(); - fixture.detectChanges(); await fixture.whenStable(); // model -> view @@ -1641,7 +1596,7 @@ describe('value accessors', () => { customInput.nativeElement.value = 'Carson'; dispatchEvent(customInput.nativeElement, 'input'); - fixture.detectChanges(); + await fixture.whenStable(); // view -> model expect(fixture.componentInstance.name).toEqual('Carson'); @@ -1663,7 +1618,7 @@ describe('value accessors', () => { setTimeoutAndChangeValue(): void { setTimeout(() => { this.value = 'Carson'; - this.ref.detectChanges(); + this.ref.markForCheck(); }, 50); } } @@ -1688,10 +1643,6 @@ describe('value accessors', () => { const fixture = initTest(Parent, Child); fixture.componentInstance.value = 'Nancy'; - fixture.detectChanges(); - - await fixture.whenStable(); - fixture.detectChanges(); await fixture.whenStable(); const child = fixture.debugElement.query(By.css('child')); @@ -1703,7 +1654,6 @@ describe('value accessors', () => { await timeout(50); - fixture.detectChanges(); await fixture.whenStable(); expect(child.nativeElement.innerHTML).toEqual('Value: Carson'); @@ -1713,6 +1663,8 @@ describe('value accessors', () => { }); describe('value accessors in reactive forms with custom options', () => { + useAutoTick(); + function initTest(component: Type, ...directives: Type[]): ComponentFixture { TestBed.configureTestingModule({ declarations: [component, ...directives], @@ -1730,11 +1682,11 @@ describe('value accessors in reactive forms with custom options', () => { fixture = initTest(CvaWithDisabledStateForm, CvaWithDisabledState); }); - it('does not set the enabled state when the control is initially enabled', () => { + it('does not set the enabled state when the control is initially enabled', async () => { fixture.componentInstance.form = new FormGroup({ 'login': new FormControl({value: 'aa', disabled: false}), }); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.form.status).toEqual('VALID'); expect(fixture.componentInstance.form.get('login')!.status).toEqual('VALID');