From f3e8aaa8aef046731eedf8b86291ac711cc1dc57 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 13 Mar 2024 17:08:55 +0000 Subject: [PATCH] test: fix tests --- .../autocomplete/autocomplete-trigger.ts | 60 ++- .../autocomplete/autocomplete.spec.ts | 468 ++++++++++-------- 2 files changed, 289 insertions(+), 239 deletions(-) diff --git a/src/material/autocomplete/autocomplete-trigger.ts b/src/material/autocomplete/autocomplete-trigger.ts index 3852150c44c3..d88d7aa89a43 100644 --- a/src/material/autocomplete/autocomplete-trigger.ts +++ b/src/material/autocomplete/autocomplete-trigger.ts @@ -310,7 +310,12 @@ export class MatAutocompleteTrigger if (this.panelOpen) { // Only emit if the panel was visible. - this.autocomplete.closed.emit(); + // `afterNextRender` always runs outside of the Angular zone, so all the subscriptions from + // `_subscribeToClosingActions()` are also outside of the Angular zone. + // We should manually run in Angular zone to update UI after panel closing. + this._zone.run(() => { + this.autocomplete.closed.emit(); + }); } this.autocomplete._isOpen = this._overlayAttached = false; @@ -614,33 +619,38 @@ export class MatAutocompleteTrigger .pipe( // create a new stream of panelClosingActions, replacing any previous streams // that were created, and flatten it so our stream only emits closing events... - switchMap(() => { - const wasOpen = this.panelOpen; - this._resetActiveItem(); - this._updatePanelState(); - this._changeDetectorRef.detectChanges(); - - if (this.panelOpen) { - this._overlayRef!.updatePosition(); - } - - if (wasOpen !== this.panelOpen) { - // If the `panelOpen` state changed, we need to make sure to emit the `opened` or - // `closed` event, because we may not have emitted it. This can happen - // - if the users opens the panel and there are no options, but the - // options come in slightly later or as a result of the value changing, - // - if the panel is closed after the user entered a string that did not match any - // of the available options, - // - if a valid string is entered after an invalid one. + switchMap(() => + this._zone.run(() => { + // `afterNextRender` always runs outside of the Angular zone, thus we have to re-enter + // the Angular zone. This will lead to change detection being called outside of the Angular + // zone and the `autocomplete.opened` will also emit outside of the Angular. + const wasOpen = this.panelOpen; + this._resetActiveItem(); + this._updatePanelState(); + this._changeDetectorRef.detectChanges(); + if (this.panelOpen) { - this._emitOpened(); - } else { - this.autocomplete.closed.emit(); + this._overlayRef!.updatePosition(); + } + + if (wasOpen !== this.panelOpen) { + // If the `panelOpen` state changed, we need to make sure to emit the `opened` or + // `closed` event, because we may not have emitted it. This can happen + // - if the users opens the panel and there are no options, but the + // options come in slightly later or as a result of the value changing, + // - if the panel is closed after the user entered a string that did not match any + // of the available options, + // - if a valid string is entered after an invalid one. + if (this.panelOpen) { + this._emitOpened(); + } else { + this.autocomplete.closed.emit(); + } } - } - return this.panelClosingActions; - }), + return this.panelClosingActions; + }), + ), // when the first closing event occurs... take(1), ) diff --git a/src/material/autocomplete/autocomplete.spec.ts b/src/material/autocomplete/autocomplete.spec.ts index 9632f858a588..0c4022033648 100644 --- a/src/material/autocomplete/autocomplete.spec.ts +++ b/src/material/autocomplete/autocomplete.spec.ts @@ -10,7 +10,6 @@ import { dispatchFakeEvent, dispatchKeyboardEvent, dispatchMouseEvent, - MockNgZone, typeInElement, } from '@angular/cdk/testing/private'; import { @@ -58,7 +57,6 @@ import { describe('MDC-based MatAutocomplete', () => { let overlayContainerElement: HTMLElement; - let zone: MockNgZone; // Creates a test component fixture. function createComponent(component: Type, providers: Provider[] = []) { @@ -72,7 +70,7 @@ describe('MDC-based MatAutocomplete', () => { NoopAnimationsModule, OverlayModule, ], - providers: [{provide: NgZone, useFactory: () => (zone = new MockNgZone())}, ...providers], + providers, declarations: [component], }); @@ -178,12 +176,12 @@ describe('MDC-based MatAutocomplete', () => { }); })); - it('should close the panel when the user clicks away', fakeAsync(() => { + it('should close the panel when the user clicks away', waitForAsync(async () => { dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); dispatchFakeEvent(document, 'click'); - tick(); + await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected clicking outside the panel to set its state to closed.`) @@ -193,12 +191,12 @@ describe('MDC-based MatAutocomplete', () => { .toEqual(''); })); - it('should close the panel when the user clicks away via auxilliary button', fakeAsync(() => { + it('should close the panel when the user clicks away via auxilliary button', waitForAsync(async () => { dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); dispatchFakeEvent(document, 'auxclick'); - tick(); + await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected clicking outside the panel to set its state to closed.`) @@ -222,15 +220,15 @@ describe('MDC-based MatAutocomplete', () => { .toEqual(''); })); - it('should close the panel when an option is clicked', fakeAsync(() => { + it('should close the panel when an option is clicked', waitForAsync(async () => { dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; option.click(); fixture.detectChanges(); - tick(); + await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected clicking an option to set the panel state to closed.`) @@ -240,15 +238,15 @@ describe('MDC-based MatAutocomplete', () => { .toEqual(''); })); - it('should close the panel when a newly created option is clicked', fakeAsync(() => { + it('should close the panel when a newly created option is clicked', waitForAsync(async () => { dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); // Filter down the option list to a subset of original options ('Alabama', 'California') typeInElement(input, 'al'); fixture.detectChanges(); - tick(); + await new Promise(r => setTimeout(r)); let options = overlayContainerElement.querySelectorAll( 'mat-option', @@ -261,12 +259,12 @@ describe('MDC-based MatAutocomplete', () => { clearElement(input); typeInElement(input, 'al'); fixture.detectChanges(); - tick(); + await new Promise(r => setTimeout(r)); options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[1].click(); fixture.detectChanges(); - tick(); + await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected clicking a new option to set the panel state to closed.`) @@ -325,13 +323,13 @@ describe('MDC-based MatAutocomplete', () => { .toContain('mat-mdc-autocomplete-hidden'); })); - it('should keep the label floating until the panel closes', fakeAsync(() => { + it('should keep the label floating until the panel closes', waitForAsync(async () => { fixture.componentInstance.trigger.openPanel(); expect(fixture.componentInstance.formField.floatLabel) .withContext('Expected label to float as soon as panel opens.') .toEqual('always'); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); fixture.detectChanges(); const options = overlayContainerElement.querySelectorAll( @@ -596,14 +594,14 @@ describe('MDC-based MatAutocomplete', () => { })); }); - it('should not close the panel when clicking on the input', fakeAsync(() => { + it('should not close the panel when clicking on the input', waitForAsync(async () => { const fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); const input = fixture.debugElement.query(By.css('input'))!.nativeElement; dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.trigger.panelOpen) .withContext('Expected panel to be opened on focus.') @@ -617,7 +615,7 @@ describe('MDC-based MatAutocomplete', () => { .toBe(true); })); - it('should not close the panel when clicking on the input inside shadow DOM', fakeAsync(() => { + it('should not close the panel when clicking on the input inside shadow DOM', waitForAsync(async () => { // This test is only relevant for Shadow DOM-capable browsers. if (!_supportsShadowDom()) { return; @@ -629,7 +627,7 @@ describe('MDC-based MatAutocomplete', () => { dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.trigger.panelOpen) .withContext('Expected panel to be opened on focus.') @@ -705,23 +703,23 @@ describe('MDC-based MatAutocomplete', () => { }).not.toThrow(); }); - it('should clear the selected option if it no longer matches the input text while typing', fakeAsync(() => { + it('should clear the selected option if it no longer matches the input text while typing', waitForAsync(async () => { const fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); - tick(); + await new Promise(r => setTimeout(r)); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); // Select an option and reopen the panel. (overlayContainerElement.querySelector('mat-option') as HTMLElement).click(); fixture.detectChanges(); - tick(); + await new Promise(r => setTimeout(r)); fixture.detectChanges(); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.options.first.selected).toBe(true); @@ -729,29 +727,29 @@ describe('MDC-based MatAutocomplete', () => { input.value = ''; typeInElement(input, 'Ala'); fixture.detectChanges(); - tick(); + await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.options.first.selected).toBe(false); })); - it('should not clear the selected option if it no longer matches the input text while typing with requireSelection', fakeAsync(() => { + it('should not clear the selected option if it no longer matches the input text while typing with requireSelection', waitForAsync(async () => { const fixture = createComponent(SimpleAutocomplete); fixture.componentInstance.requireSelection = true; fixture.detectChanges(); - tick(); + await new Promise(r => setTimeout(r)); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); // Select an option and reopen the panel. (overlayContainerElement.querySelector('mat-option') as HTMLElement).click(); fixture.detectChanges(); - tick(); + await new Promise(r => setTimeout(r)); fixture.detectChanges(); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.options.first.selected).toBe(true); @@ -759,7 +757,7 @@ describe('MDC-based MatAutocomplete', () => { input.value = ''; typeInElement(input, 'Ala'); fixture.detectChanges(); - tick(); + await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.options.first.selected).toBe(true); })); @@ -775,10 +773,10 @@ describe('MDC-based MatAutocomplete', () => { input = fixture.debugElement.query(By.css('input'))!.nativeElement; }); - it('should update control value as user types with input value', () => { + it('should update control value as user types with input value', waitForAsync(async () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); typeInElement(input, 'a'); fixture.detectChanges(); @@ -793,7 +791,7 @@ describe('MDC-based MatAutocomplete', () => { expect(fixture.componentInstance.stateCtrl.value) .withContext('Expected control value to be updated as user types.') .toEqual('al'); - }); + })); it('should update control value when autofilling', () => { // Simulate the browser autofilling the input by setting a value and @@ -808,10 +806,10 @@ describe('MDC-based MatAutocomplete', () => { .toBe('Alabama'); }); - it('should update control value when option is selected with option value', fakeAsync(() => { + it('should update control value when option is selected with option value', waitForAsync(async () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); const options = overlayContainerElement.querySelectorAll( 'mat-option', @@ -824,10 +822,10 @@ describe('MDC-based MatAutocomplete', () => { .toEqual({code: 'CA', name: 'California'}); })); - it('should update the control back to a string if user types after an option is selected', fakeAsync(() => { + it('should update the control back to a string if user types after an option is selected', waitForAsync(async () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); const options = overlayContainerElement.querySelectorAll( 'mat-option', @@ -838,17 +836,17 @@ describe('MDC-based MatAutocomplete', () => { clearElement(input); typeInElement(input, 'Californi'); fixture.detectChanges(); - tick(); + await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.stateCtrl.value) .withContext('Expected control value to revert back to string.') .toEqual('Californi'); })); - it('should fill the text field with display value when an option is selected', fakeAsync(() => { + it('should fill the text field with display value when an option is selected', waitForAsync(async () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); const options = overlayContainerElement.querySelectorAll( 'mat-option', @@ -861,10 +859,10 @@ describe('MDC-based MatAutocomplete', () => { .toContain('California'); })); - it('should fill the text field with value if displayWith is not set', fakeAsync(() => { + it('should fill the text field with value if displayWith is not set', waitForAsync(async () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); fixture.componentInstance.panel.displayWith = null; fixture.componentInstance.options.toArray()[1].value = 'test value'; @@ -906,10 +904,10 @@ describe('MDC-based MatAutocomplete', () => { expect(input.value).withContext(`Expected input value to be empty after reset.`).toEqual(''); })); - it('should clear the previous selection when reactive form field is reset programmatically', fakeAsync(() => { + it('should clear the previous selection when reactive form field is reset programmatically', waitForAsync(async () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); const options = overlayContainerElement.querySelectorAll( 'mat-option', @@ -924,10 +922,10 @@ describe('MDC-based MatAutocomplete', () => { expect(option.selected).toBe(true); fixture.componentInstance.stateCtrl.reset(); - tick(); + await new Promise(r => setTimeout(r)); fixture.detectChanges(); - tick(); + await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.stateCtrl.value).toEqual(null); expect(option.selected).toBe(false); @@ -969,14 +967,14 @@ describe('MDC-based MatAutocomplete', () => { .toBe(true); }); - it('should mark the autocomplete control as dirty when an option is selected', fakeAsync(() => { + it('should mark the autocomplete control as dirty when an option is selected', waitForAsync(async () => { expect(fixture.componentInstance.stateCtrl.dirty) .withContext(`Expected control to start out pristine.`) .toBe(false); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); const options = overlayContainerElement.querySelectorAll( 'mat-option', @@ -1063,7 +1061,7 @@ describe('MDC-based MatAutocomplete', () => { let UP_ARROW_EVENT: KeyboardEvent; let ENTER_EVENT: KeyboardEvent; - beforeEach(fakeAsync(() => { + beforeEach(waitForAsync(async () => { fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); @@ -1074,7 +1072,7 @@ describe('MDC-based MatAutocomplete', () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); })); it('should not focus the option when DOWN key is pressed', () => { @@ -1628,26 +1626,26 @@ describe('MDC-based MatAutocomplete', () => { UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW); }); - it('should scroll to active options below the fold', fakeAsync(() => { + it('should scroll to active options below the fold', waitForAsync(async () => { const fixture = createComponent(AutocompleteWithGroups); fixture.detectChanges(); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - zone.simulateZoneExit(); + await new Promise(r => setTimeout(r)); fixture.detectChanges(); const container = document.querySelector('.mat-mdc-autocomplete-panel') as HTMLElement; fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); - tick(); + await new Promise(r => setTimeout(r)); fixture.detectChanges(); expect(container.scrollTop).withContext('Expected the panel not to scroll.').toBe(0); // Press the down arrow five times. - [1, 2, 3, 4, 5].forEach(() => { + for (const _unused of [1, 2, 3, 4, 5]) { fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); - tick(); - }); + await new Promise(r => setTimeout(r)); + } //