From 5dabf81bdfc01e8d0ae7edc326b693f86c53dfc7 Mon Sep 17 00:00:00 2001 From: Jaime Burgos <73321943+SkyZeroZx@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:24:37 -0500 Subject: [PATCH] test(forms): migrate integration tests to zoneless scheduling Use whenStable instead of manual change detection so integration tests exercise scheduled rendering. Await asynchronous callbacks and remove redundant timer waits to keep assertions within the test lifecycle. (cherry picked from commit 8afd85c6d752207f975718dbc6306c5ae079f2ca) --- .../forms/test/reactive_integration_spec.ts | 16 +- .../forms/test/template_integration_spec.ts | 829 ++++++++---------- .../test/value_accessor_integration_spec.ts | 420 ++++----- 3 files changed, 543 insertions(+), 722 deletions(-) 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');