import {Directionality} from '@angular/cdk/bidi'; import {DOWN_ARROW, ENTER, ESCAPE, SPACE, TAB, UP_ARROW} from '@angular/cdk/keycodes'; import {Overlay, OverlayContainer, OverlayModule} from '@angular/cdk/overlay'; import {_supportsShadowDom} from '@angular/cdk/platform'; import {ScrollDispatcher} from '@angular/cdk/scrolling'; import { clearElement, createKeyboardEvent, dispatchEvent, dispatchFakeEvent, dispatchKeyboardEvent, dispatchMouseEvent, typeInElement, } from '@angular/cdk/testing/private'; import { ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, Provider, QueryList, Type, ViewChild, ViewChildren, ViewEncapsulation, } from '@angular/core'; import { ComponentFixture, TestBed, fakeAsync, flush, inject, tick, waitForAsync, } from '@angular/core/testing'; import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {MatOption, MatOptionSelectionChange} from '@angular/material/core'; import {MatFormField, MatFormFieldModule} from '@angular/material/form-field'; import {MatInputModule} from '@angular/material/input'; import {By} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {EMPTY, Observable, Subject, Subscription} from 'rxjs'; import {map, startWith} from 'rxjs/operators'; import { MAT_AUTOCOMPLETE_DEFAULT_OPTIONS, MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocomplete, MatAutocompleteDefaultOptions, MatAutocompleteModule, MatAutocompleteOrigin, MatAutocompleteSelectedEvent, MatAutocompleteTrigger, getMatAutocompleteMissingPanelError, } from './index'; describe('MatAutocomplete', () => { let overlayContainerElement: HTMLElement; // Creates a test component fixture. function createComponent(component: Type, providers: Provider[] = []) { TestBed.configureTestingModule({ imports: [ MatAutocompleteModule, MatFormFieldModule, MatInputModule, FormsModule, ReactiveFormsModule, NoopAnimationsModule, OverlayModule, ], providers, declarations: [component], }); inject([OverlayContainer], (oc: OverlayContainer) => { overlayContainerElement = oc.getContainerElement(); })(); return TestBed.createComponent(component); } describe('panel toggling', () => { let fixture: ComponentFixture; let input: HTMLInputElement; beforeEach(() => { fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); input = fixture.debugElement.query(By.css('input'))!.nativeElement; }); it('should open the panel when the input is focused', () => { expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected panel state to start out closed.`) .toBe(false); dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected panel state to read open when input is focused.`) .toBe(true); expect(overlayContainerElement.textContent) .withContext(`Expected panel to display when input is focused.`) .toContain('Alabama'); expect(overlayContainerElement.textContent) .withContext(`Expected panel to display when input is focused.`) .toContain('California'); }); it('should not open the panel on focus if the input is readonly', fakeAsync(() => { const trigger = fixture.componentInstance.trigger; input.readOnly = true; fixture.detectChanges(); expect(trigger.panelOpen) .withContext('Expected panel state to start out closed.') .toBe(false); dispatchFakeEvent(input, 'focusin'); flush(); fixture.detectChanges(); expect(trigger.panelOpen).withContext('Expected panel to stay closed.').toBe(false); })); it('should not open using the arrow keys when the input is readonly', fakeAsync(() => { const trigger = fixture.componentInstance.trigger; input.readOnly = true; fixture.detectChanges(); expect(trigger.panelOpen) .withContext('Expected panel state to start out closed.') .toBe(false); dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW); flush(); fixture.detectChanges(); expect(trigger.panelOpen).withContext('Expected panel to stay closed.').toBe(false); })); it('should open the panel programmatically', () => { expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected panel state to start out closed.`) .toBe(false); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected panel state to read open when opened programmatically.`) .toBe(true); expect(overlayContainerElement.textContent) .withContext(`Expected panel to display when opened programmatically.`) .toContain('Alabama'); expect(overlayContainerElement.textContent) .withContext(`Expected panel to display when opened programmatically.`) .toContain('California'); }); it('should close the panel when the user clicks away', waitForAsync(async () => { dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); await new Promise(r => setTimeout(r)); dispatchFakeEvent(document, 'click'); await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected clicking outside the panel to set its state to closed.`) .toBe(false); expect(overlayContainerElement.textContent) .withContext(`Expected clicking outside the panel to close the panel.`) .toEqual(''); })); it('should close the panel when the user clicks away via auxiliary button', waitForAsync(async () => { dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); await new Promise(r => setTimeout(r)); dispatchFakeEvent(document, 'auxclick'); await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected clicking outside the panel to set its state to closed.`) .toBe(false); expect(overlayContainerElement.textContent) .withContext(`Expected clicking outside the panel to close the panel.`) .toEqual(''); })); it('should close the panel when the user taps away on a touch device', fakeAsync(() => { dispatchFakeEvent(input, 'focus'); fixture.detectChanges(); flush(); dispatchFakeEvent(document, 'touchend'); expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected tapping outside the panel to set its state to closed.`) .toBe(false); expect(overlayContainerElement.textContent) .withContext(`Expected tapping outside the panel to close the panel.`) .toEqual(''); })); it('should close the panel when an option is clicked', waitForAsync(async () => { dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); await new Promise(r => setTimeout(r)); const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; option.click(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected clicking an option to set the panel state to closed.`) .toBe(false); expect(overlayContainerElement.textContent) .withContext(`Expected clicking an option to close the panel.`) .toEqual(''); })); it('should close the panel when a newly created option is clicked', waitForAsync(async () => { dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); await new Promise(r => setTimeout(r)); // Filter down the option list to a subset of original options ('Alabama', 'California') typeInElement(input, 'al'); fixture.detectChanges(); await new Promise(r => setTimeout(r)); let options = overlayContainerElement.querySelectorAll( 'mat-option', ) as NodeListOf; options[0].click(); // Changing value from 'Alabama' to 'al' to re-populate the option list, // ensuring that 'California' is created new. dispatchFakeEvent(input, 'focusin'); clearElement(input); typeInElement(input, 'al'); fixture.detectChanges(); await new Promise(r => setTimeout(r)); options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[1].click(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected clicking a new option to set the panel state to closed.`) .toBe(false); expect(overlayContainerElement.textContent) .withContext(`Expected clicking a new option to close the panel.`) .toEqual(''); })); it('should close the panel programmatically', fakeAsync(() => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); fixture.componentInstance.trigger.closePanel(); fixture.detectChanges(); tick(); expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected closing programmatically to set the panel state to closed.`) .toBe(false); expect(overlayContainerElement.textContent) .withContext(`Expected closing programmatically to close the panel.`) .toEqual(''); })); it('should not throw when attempting to close the panel of a destroyed autocomplete', () => { const trigger = fixture.componentInstance.trigger; trigger.openPanel(); fixture.detectChanges(); fixture.destroy(); expect(() => trigger.closePanel()).not.toThrow(); }); it('should hide the panel when the options list is empty', fakeAsync(() => { dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); const panel = overlayContainerElement.querySelector( '.mat-mdc-autocomplete-panel', ) as HTMLElement; expect(panel.classList) .withContext(`Expected panel to start out visible.`) .toContain('mat-mdc-autocomplete-visible'); // Filter down the option list such that no options match the value typeInElement(input, 'af'); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(panel.classList) .withContext(`Expected panel to hide itself when empty.`) .toContain('mat-mdc-autocomplete-hidden'); })); 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'); await new Promise(r => setTimeout(r)); fixture.detectChanges(); const options = overlayContainerElement.querySelectorAll( 'mat-option', ) as NodeListOf; options[1].click(); fixture.detectChanges(); expect(fixture.componentInstance.formField.floatLabel) .withContext('Expected label to return to auto state after panel closes.') .toEqual('auto'); })); it('should not open the panel when the `input` event is invoked on a non-focused input', () => { expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected panel state to start out closed.`) .toBe(false); input.value = 'Alabama'; dispatchFakeEvent(input, 'input'); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected panel state to stay closed.`) .toBe(false); }); it('should not mess with label placement if set to never', fakeAsync(() => { fixture.componentInstance.floatLabel = 'never'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.componentInstance.trigger.openPanel(); expect(fixture.componentInstance.formField.floatLabel) .withContext('Expected label to stay static.') .toEqual('never'); flush(); fixture.detectChanges(); const options = overlayContainerElement.querySelectorAll( 'mat-option', ) as NodeListOf; options[1].click(); fixture.detectChanges(); flush(); expect(fixture.componentInstance.formField.floatLabel) .withContext('Expected label to stay in static state after close.') .toEqual('never'); })); it('should not mess with label placement if set to always', fakeAsync(() => { fixture.componentInstance.floatLabel = 'always'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.componentInstance.trigger.openPanel(); expect(fixture.componentInstance.formField.floatLabel) .withContext('Expected label to stay elevated on open.') .toEqual('always'); flush(); fixture.detectChanges(); const options = overlayContainerElement.querySelectorAll( 'mat-option', ) as NodeListOf; options[1].click(); fixture.detectChanges(); flush(); expect(fixture.componentInstance.formField.floatLabel) .withContext('Expected label to stay elevated after close.') .toEqual('always'); })); it('should toggle the visibility when typing and closing the panel', fakeAsync(() => { fixture.componentInstance.trigger.openPanel(); tick(); fixture.detectChanges(); expect(overlayContainerElement.querySelector('.mat-mdc-autocomplete-panel')!.classList) .withContext('Expected panel to be visible.') .toContain('mat-mdc-autocomplete-visible'); typeInElement(input, 'x'); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(overlayContainerElement.querySelector('.mat-mdc-autocomplete-panel')!.classList) .withContext('Expected panel to be hidden.') .toContain('mat-mdc-autocomplete-hidden'); fixture.componentInstance.trigger.closePanel(); fixture.detectChanges(); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); clearElement(input); typeInElement(input, 'al'); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(overlayContainerElement.querySelector('.mat-mdc-autocomplete-panel')!.classList) .withContext('Expected panel to be visible.') .toContain('mat-mdc-autocomplete-visible'); })); it('should animate the label when the input is focused', () => { const inputContainer = fixture.componentInstance.formField; spyOn(inputContainer, '_animateAndLockLabel'); expect(inputContainer._animateAndLockLabel).not.toHaveBeenCalled(); dispatchFakeEvent(fixture.debugElement.query(By.css('input'))!.nativeElement, 'focusin'); expect(inputContainer._animateAndLockLabel).toHaveBeenCalled(); }); it('should provide the open state of the panel', fakeAsync(() => { expect(fixture.componentInstance.panel.isOpen).toBeFalsy( `Expected the panel to be unopened initially.`, ); dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); flush(); expect(fixture.componentInstance.panel.isOpen).toBeTruthy( `Expected the panel to be opened on focus.`, ); })); it('should emit an event when the panel is opened', () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); expect(fixture.componentInstance.openedSpy).toHaveBeenCalled(); }); it('should not emit the `opened` event when no options are being shown', () => { fixture.componentInstance.filteredStates = fixture.componentInstance.states = []; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); expect(fixture.componentInstance.openedSpy).not.toHaveBeenCalled(); }); it('should emit the `opened` event if the options come in after the panel is shown', fakeAsync(() => { fixture.componentInstance.filteredStates = fixture.componentInstance.states = []; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); expect(fixture.componentInstance.openedSpy).not.toHaveBeenCalled(); fixture.componentInstance.filteredStates = fixture.componentInstance.states = [ {name: 'California', code: 'CA'}, ]; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(fixture.componentInstance.openedSpy).toHaveBeenCalled(); })); it('should not emit the opened event multiple times while typing', fakeAsync(() => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); expect(fixture.componentInstance.openedSpy).toHaveBeenCalledTimes(1); typeInElement(input, 'Alabam'); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(fixture.componentInstance.openedSpy).toHaveBeenCalledTimes(1); })); it('should emit an event when the panel is closed', () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); fixture.componentInstance.trigger.closePanel(); fixture.detectChanges(); expect(fixture.componentInstance.closedSpy).toHaveBeenCalled(); }); it('should not emit the `closed` event when no options were shown', () => { fixture.componentInstance.filteredStates = fixture.componentInstance.states = []; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); fixture.componentInstance.trigger.closePanel(); fixture.detectChanges(); expect(fixture.componentInstance.closedSpy).not.toHaveBeenCalled(); }); it('should not be able to open the panel if the autocomplete is disabled', () => { expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected panel state to start out closed.`) .toBe(false); fixture.componentInstance.autocompleteDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected panel to remain closed.`) .toBe(false); }); it('should continue to update the model if the autocomplete is disabled', () => { fixture.componentInstance.autocompleteDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); typeInElement(input, 'hello'); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.value).toBe('hello'); }); it('should set aria-haspopup depending on whether the autocomplete is disabled', () => { expect(input.getAttribute('aria-haspopup')).toBe('listbox'); fixture.componentInstance.autocompleteDisabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(input.hasAttribute('aria-haspopup')).toBe(false); }); it('should reopen the panel when clicking on the input', fakeAsync(() => { const trigger = fixture.componentInstance.trigger; input.focus(); flush(); fixture.detectChanges(); expect(document.activeElement).withContext('Expected input to be focused.').toBe(input); expect(trigger.panelOpen).withContext('Expected panel to be open.').toBe(true); trigger.closePanel(); fixture.detectChanges(); expect(document.activeElement) .withContext('Expected input to continue to be focused.') .toBe(input); expect(trigger.panelOpen).withContext('Expected panel to be closed.').toBe(false); input.click(); flush(); fixture.detectChanges(); expect(trigger.panelOpen).withContext('Expected panel to reopen on click.').toBe(true); })); }); 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(); await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.trigger.panelOpen) .withContext('Expected panel to be opened on focus.') .toBe(true); input.click(); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) .withContext('Expected panel to remain opened after clicking on the input.') .toBe(true); })); 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; } const fixture = createComponent(SimpleAutocompleteShadowDom); fixture.detectChanges(); const input = fixture.debugElement.query(By.css('input'))!.nativeElement; dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.trigger.panelOpen) .withContext('Expected panel to be opened on focus.') .toBe(true); input.click(); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) .withContext('Expected panel to remain opened after clicking on the input.') .toBe(true); })); it('should have the correct text direction in RTL', () => { const rtlFixture = createComponent(SimpleAutocomplete, [ {provide: Directionality, useFactory: () => ({value: 'rtl', change: EMPTY})}, ]); rtlFixture.detectChanges(); rtlFixture.componentInstance.trigger.openPanel(); rtlFixture.detectChanges(); const boundingBox = overlayContainerElement.querySelector( '.cdk-overlay-connected-position-bounding-box', )!; expect(boundingBox.getAttribute('dir')).toEqual('rtl'); }); it('should update the panel direction if it changes for the trigger', () => { const dirProvider = {value: 'rtl', change: EMPTY}; const rtlFixture = createComponent(SimpleAutocomplete, [ {provide: Directionality, useFactory: () => dirProvider}, ]); rtlFixture.detectChanges(); rtlFixture.componentInstance.trigger.openPanel(); rtlFixture.detectChanges(); let boundingBox = overlayContainerElement.querySelector( '.cdk-overlay-connected-position-bounding-box', )!; expect(boundingBox.getAttribute('dir')).toEqual('rtl'); rtlFixture.componentInstance.trigger.closePanel(); rtlFixture.detectChanges(); dirProvider.value = 'ltr'; rtlFixture.componentInstance.trigger.openPanel(); rtlFixture.detectChanges(); boundingBox = overlayContainerElement.querySelector( '.cdk-overlay-connected-position-bounding-box', )!; expect(boundingBox.getAttribute('dir')).toEqual('ltr'); }); it('should be able to set a custom value for the `autocomplete` attribute', () => { const fixture = createComponent(AutocompleteWithNativeAutocompleteAttribute); const input = fixture.nativeElement.querySelector('input'); fixture.detectChanges(); expect(input.getAttribute('autocomplete')).toBe('changed'); }); it('should not throw when typing in an element with a null and disabled autocomplete', () => { const fixture = createComponent(InputWithoutAutocompleteAndDisabled); fixture.detectChanges(); expect(() => { dispatchKeyboardEvent(fixture.nativeElement.querySelector('input'), 'keydown', SPACE); fixture.detectChanges(); }).not.toThrow(); }); it('should clear the selected option if it no longer matches the input text while typing', waitForAsync(async () => { const fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); await new Promise(r => setTimeout(r)); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); // Select an option and reopen the panel. (overlayContainerElement.querySelector('mat-option') as HTMLElement).click(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); fixture.detectChanges(); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.options.first.selected).toBe(true); const input = fixture.debugElement.query(By.css('input'))!.nativeElement; input.value = ''; typeInElement(input, 'Ala'); fixture.detectChanges(); 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', waitForAsync(async () => { const fixture = createComponent(SimpleAutocomplete); fixture.componentInstance.requireSelection = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); // Select an option and reopen the panel. (overlayContainerElement.querySelector('mat-option') as HTMLElement).click(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); fixture.detectChanges(); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.options.first.selected).toBe(true); const input = fixture.debugElement.query(By.css('input'))!.nativeElement; input.value = ''; typeInElement(input, 'Ala'); fixture.detectChanges(); await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.options.first.selected).toBe(true); })); describe('forms integration', () => { let fixture: ComponentFixture; let input: HTMLInputElement; beforeEach(() => { fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); input = fixture.debugElement.query(By.css('input'))!.nativeElement; }); it('should update control value as user types with input value', waitForAsync(async () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); typeInElement(input, 'a'); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.value) .withContext('Expected control value to be updated as user types.') .toEqual('a'); typeInElement(input, 'l'); fixture.detectChanges(); 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 // dispatching an `input` event while the input is out of focus. expect(document.activeElement).not.toBe(input, 'Expected input not to have focus.'); input.value = 'Alabama'; dispatchFakeEvent(input, 'input'); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.value) .withContext('Expected value to be propagated to the form control.') .toBe('Alabama'); }); it('should update control value when option is selected with option value', waitForAsync(async () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); const options = overlayContainerElement.querySelectorAll( 'mat-option', ) as NodeListOf; options[1].click(); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.value) .withContext('Expected control value to equal the selected option value.') .toEqual({code: 'CA', name: 'California'}); })); 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(); await new Promise(r => setTimeout(r)); const options = overlayContainerElement.querySelectorAll( 'mat-option', ) as NodeListOf; options[1].click(); fixture.detectChanges(); clearElement(input); typeInElement(input, 'Californi'); fixture.detectChanges(); 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', waitForAsync(async () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); const options = overlayContainerElement.querySelectorAll( 'mat-option', ) as NodeListOf; options[1].click(); fixture.detectChanges(); expect(input.value) .withContext(`Expected text field to fill with selected value.`) .toContain('California'); })); it('should fill the text field with value if displayWith is not set', waitForAsync(async () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); fixture.componentInstance.panel.displayWith = null; fixture.componentInstance.options.toArray()[1].value = 'test value'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const options = overlayContainerElement.querySelectorAll( 'mat-option', ) as NodeListOf; options[1].click(); fixture.detectChanges(); expect(input.value) .withContext(`Expected input to fall back to selected option's value.`) .toContain('test value'); })); it('should fill the text field correctly if value is set to obj programmatically', fakeAsync(() => { fixture.componentInstance.stateCtrl.setValue({code: 'AL', name: 'Alabama'}); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(input.value) .withContext(`Expected input to fill with matching option's viewValue.`) .toContain('Alabama'); })); it('should clear the text field if value is reset programmatically', fakeAsync(() => { typeInElement(input, 'Alabama'); fixture.detectChanges(); tick(); fixture.componentInstance.stateCtrl.reset(); tick(); fixture.detectChanges(); tick(); 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', waitForAsync(async () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); const options = overlayContainerElement.querySelectorAll( 'mat-option', ) as NodeListOf; const clickedOption = options[0]; const option = fixture.componentInstance.options.first; clickedOption.click(); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'}); expect(option.selected).toBe(true); fixture.componentInstance.stateCtrl.reset(); await new Promise(r => setTimeout(r)); fixture.detectChanges(); await new Promise(r => setTimeout(r)); expect(fixture.componentInstance.stateCtrl.value).toEqual(null); expect(option.selected).toBe(false); })); it('should disable input in view when disabled programmatically', () => { const formFieldElement = fixture.debugElement.query( By.css('.mat-mdc-form-field'), )!.nativeElement; expect(input.disabled) .withContext(`Expected input to start out enabled in view.`) .toBe(false); expect(formFieldElement.classList.contains('mat-form-field-disabled')) .withContext(`Expected input underline to start out with normal styles.`) .toBe(false); fixture.componentInstance.stateCtrl.disable(); fixture.detectChanges(); expect(input.disabled) .withContext(`Expected input to be disabled in view when disabled programmatically.`) .toBe(true); expect(formFieldElement.classList.contains('mat-form-field-disabled')) .withContext(`Expected input underline to display disabled styles.`) .toBe(true); }); it('should mark the autocomplete control as dirty as user types', () => { expect(fixture.componentInstance.stateCtrl.dirty) .withContext(`Expected control to start out pristine.`) .toBe(false); typeInElement(input, 'a'); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.dirty) .withContext(`Expected control to become dirty when the user types into the input.`) .toBe(true); }); 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(); await new Promise(r => setTimeout(r)); const options = overlayContainerElement.querySelectorAll( 'mat-option', ) as NodeListOf; options[1].click(); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.dirty) .withContext(`Expected control to become dirty when an option was selected.`) .toBe(true); })); it('should not mark the control dirty when the value is set programmatically', () => { expect(fixture.componentInstance.stateCtrl.dirty) .withContext(`Expected control to start out pristine.`) .toBe(false); fixture.componentInstance.stateCtrl.setValue('AL'); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.dirty) .withContext(`Expected control to stay pristine if value is set programmatically.`) .toBe(false); }); it('should mark the autocomplete control as touched on blur', () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.touched) .withContext(`Expected control to start out untouched.`) .toBe(false); dispatchFakeEvent(input, 'blur'); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.touched) .withContext(`Expected control to become touched on blur.`) .toBe(true); }); it('should disable the input when used with a value accessor and without `matInput`', () => { fixture.destroy(); TestBed.resetTestingModule(); const plainFixture = createComponent(PlainAutocompleteInputWithFormControl); plainFixture.detectChanges(); input = plainFixture.nativeElement.querySelector('input'); expect(input.disabled).toBe(false); plainFixture.componentInstance.formControl.disable(); plainFixture.detectChanges(); expect(input.disabled).toBe(true); }); }); describe('with theming', () => { let fixture: ComponentFixture; beforeEach(() => { fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); }); it('should transfer the theme to the autocomplete panel', () => { fixture.componentInstance.theme = 'warn'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); const panel = overlayContainerElement.querySelector( '.mat-mdc-autocomplete-panel', )! as HTMLElement; expect(panel.classList).toContain('mat-warn'); }); }); describe('keyboard events', () => { let fixture: ComponentFixture; let input: HTMLInputElement; let DOWN_ARROW_EVENT: KeyboardEvent; let UP_ARROW_EVENT: KeyboardEvent; let ENTER_EVENT: KeyboardEvent; beforeEach(waitForAsync(async () => { fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); input = fixture.debugElement.query(By.css('input'))!.nativeElement; DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW); UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW); ENTER_EVENT = createKeyboardEvent('keydown', ENTER); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); await new Promise(r => setTimeout(r)); })); it('should not focus the option when DOWN key is pressed', () => { spyOn(fixture.componentInstance.options.first, 'focus'); fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); expect(fixture.componentInstance.options.first.focus).not.toHaveBeenCalled(); }); it('should not close the panel when DOWN key is pressed', () => { fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected panel state to stay open when DOWN key is pressed.`) .toBe(true); expect(overlayContainerElement.textContent) .withContext(`Expected panel to keep displaying when DOWN key is pressed.`) .toContain('Alabama'); expect(overlayContainerElement.textContent) .withContext(`Expected panel to keep displaying when DOWN key is pressed.`) .toContain('California'); }); it('should set the active item to the first option when DOWN key is pressed', () => { const componentInstance = fixture.componentInstance; const optionEls = overlayContainerElement.querySelectorAll( 'mat-option', ) as NodeListOf; expect(componentInstance.trigger.panelOpen) .withContext('Expected first down press to open the pane.') .toBe(true); componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.detectChanges(); expect(componentInstance.trigger.activeOption === componentInstance.options.first) .withContext('Expected first option to be active.') .toBe(true); expect(optionEls[0].classList).toContain('mat-mdc-option-active'); expect(optionEls[1].classList).not.toContain('mat-mdc-option-active'); componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.detectChanges(); expect(componentInstance.trigger.activeOption === componentInstance.options.toArray()[1]) .withContext('Expected second option to be active.') .toBe(true); expect(optionEls[0].classList).not.toContain('mat-mdc-option-active'); expect(optionEls[1].classList).toContain('mat-mdc-option-active'); }); it('should set the active item to the last option when UP key is pressed', () => { const componentInstance = fixture.componentInstance; const optionEls = overlayContainerElement.querySelectorAll( 'mat-option', ) as NodeListOf; expect(componentInstance.trigger.panelOpen) .withContext('Expected first up press to open the pane.') .toBe(true); componentInstance.trigger._handleKeydown(UP_ARROW_EVENT); fixture.detectChanges(); expect(componentInstance.trigger.activeOption === componentInstance.options.last) .withContext('Expected last option to be active.') .toBe(true); expect(optionEls[10].classList).toContain('mat-mdc-option-active'); expect(optionEls[0].classList).not.toContain('mat-mdc-option-active'); componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.detectChanges(); expect(componentInstance.trigger.activeOption === componentInstance.options.first) .withContext('Expected first option to be active.') .toBe(true); expect(optionEls[0].classList).toContain('mat-mdc-option-active'); }); it('should set the active item properly after filtering', fakeAsync(() => { const componentInstance = fixture.componentInstance; componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); tick(); fixture.detectChanges(); })); it('should set the active item properly after filtering', () => { const componentInstance = fixture.componentInstance; typeInElement(input, 'o'); fixture.detectChanges(); componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.detectChanges(); const optionEls = overlayContainerElement.querySelectorAll( 'mat-option', ) as NodeListOf; expect(componentInstance.trigger.activeOption === componentInstance.options.first) .withContext('Expected first option to be active.') .toBe(true); expect(optionEls[0].classList).toContain('mat-mdc-option-active'); expect(optionEls[1].classList).not.toContain('mat-mdc-option-active'); }); it('should fill the text field when an option is selected with ENTER', fakeAsync(() => { fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); flush(); fixture.detectChanges(); fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT); fixture.detectChanges(); flush(); expect(input.value) .withContext(`Expected text field to fill with selected value on ENTER.`) .toContain('Alabama'); })); it('should prevent the default enter key action', fakeAsync(() => { fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); flush(); fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT); flush(); expect(ENTER_EVENT.defaultPrevented) .withContext('Expected the default action to have been prevented.') .toBe(true); })); it('should not prevent the default enter action for a closed panel after a user action', () => { fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT); fixture.detectChanges(); fixture.componentInstance.trigger.closePanel(); fixture.detectChanges(); fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT); expect(ENTER_EVENT.defaultPrevented) .withContext('Default action should not be prevented.') .toBe(false); }); it('should not interfere with the ENTER key when pressing a modifier', fakeAsync(() => { const trigger = fixture.componentInstance.trigger; expect(input.value).withContext('Expected input to start off blank.').toBeFalsy(); expect(trigger.panelOpen).withContext('Expected panel to start off open.').toBe(true); fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); flush(); fixture.detectChanges(); Object.defineProperty(ENTER_EVENT, 'altKey', {get: () => true}); fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT); fixture.detectChanges(); expect(trigger.panelOpen).withContext('Expected panel to remain open.').toBe(true); expect(input.value).withContext('Expected input to remain blank.').toBeFalsy(); expect(ENTER_EVENT.defaultPrevented) .withContext('Expected the default ENTER action not to have been prevented.') .toBe(false); })); it('should fill the text field, not select an option, when SPACE is entered', () => { typeInElement(input, 'New'); fixture.detectChanges(); const SPACE_EVENT = createKeyboardEvent('keydown', SPACE); fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.detectChanges(); fixture.componentInstance.trigger._handleKeydown(SPACE_EVENT); fixture.detectChanges(); expect(input.value) .not.withContext(`Expected option not to be selected on SPACE.`) .toContain('New York'); }); it('should mark the control dirty when selecting an option from the keyboard', fakeAsync(() => { expect(fixture.componentInstance.stateCtrl.dirty) .withContext(`Expected control to start out pristine.`) .toBe(false); fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); flush(); fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT); fixture.detectChanges(); flush(); expect(fixture.componentInstance.stateCtrl.dirty) .withContext(`Expected control to become dirty when option was selected by ENTER.`) .toBe(true); })); it('should open the panel again when typing after making a selection', fakeAsync(() => { fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); flush(); fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT); fixture.detectChanges(); tick(); expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected panel state to read closed after ENTER key.`) .toBe(false); expect(overlayContainerElement.textContent) .withContext(`Expected panel to close after ENTER key.`) .toEqual(''); dispatchFakeEvent(input, 'focusin'); clearElement(input); typeInElement(input, 'Alabama'); fixture.detectChanges(); tick(); expect(fixture.componentInstance.trigger.panelOpen) .withContext(`Expected panel state to read open when typing in input.`) .toBe(true); expect(overlayContainerElement.textContent) .withContext(`Expected panel to display when typing in input.`) .toContain('Alabama'); })); it('should not open the panel if the `input` event was dispatched with changing the value', fakeAsync(() => { const trigger = fixture.componentInstance.trigger; dispatchFakeEvent(input, 'focusin'); typeInElement(input, 'A'); fixture.detectChanges(); tick(); expect(trigger.panelOpen).withContext('Expected panel to be open.').toBe(true); trigger.closePanel(); fixture.detectChanges(); expect(trigger.panelOpen).withContext('Expected panel to be closed.').toBe(false); // Dispatch the event without actually changing the value // to simulate what happen in some cases on IE. dispatchFakeEvent(input, 'input'); fixture.detectChanges(); tick(); expect(trigger.panelOpen).withContext('Expected panel to stay closed.').toBe(false); })); it('should scroll to active options below the fold', () => { const trigger = fixture.componentInstance.trigger; const scrollContainer = document.querySelector( '.cdk-overlay-pane .mat-mdc-autocomplete-panel', )!; trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.detectChanges(); expect(scrollContainer.scrollTop).withContext(`Expected panel not to scroll.`).toEqual(0); // These down arrows will set the 6th option active, below the fold. [1, 2, 3, 4, 5].forEach(() => trigger._handleKeydown(DOWN_ARROW_EVENT)); // Expect option bottom minus the panel height plus padding (288 - 256 + 8 = 40) // Expect option bottom minus the panel height plus padding (288 - 256 + 8 = 40) expect(scrollContainer.scrollTop) .withContext(`Expected panel to reveal the sixth option.`) .toEqual(40); }); it('should scroll to active options below if the option height is variable', () => { // Make every other option a bit taller than the base of 48. fixture.componentInstance.states.forEach((state, index) => { if (index % 2 === 0) { state.height = 64; } }); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const trigger = fixture.componentInstance.trigger; const scrollContainer = document.querySelector( '.cdk-overlay-pane .mat-mdc-autocomplete-panel', )!; trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.detectChanges(); expect(scrollContainer.scrollTop).withContext(`Expected panel not to scroll.`).toEqual(0); // These down arrows will set the 6th option active, below the fold. [1, 2, 3, 4, 5].forEach(() => trigger._handleKeydown(DOWN_ARROW_EVENT)); // Expect option bottom minus the panel height (336 - 256 + 8 = 88) // Expect option bottom minus the panel height (336 - 256 + 8 = 88) expect(scrollContainer.scrollTop) .withContext(`Expected panel to reveal the sixth option.`) .toEqual(88); }); it('should scroll to active options on UP arrow', () => { const scrollContainer = document.querySelector( '.cdk-overlay-pane .mat-mdc-autocomplete-panel', )!; const initialScrollTop = scrollContainer.scrollTop; fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT); fixture.detectChanges(); expect(scrollContainer.scrollTop) .withContext(`Expected panel to reveal last option.`) .toBeGreaterThan(initialScrollTop); }); it('should not scroll to active options that are fully in the panel', () => { const trigger = fixture.componentInstance.trigger; const scrollContainer = document.querySelector( '.cdk-overlay-pane .mat-mdc-autocomplete-panel', )!; trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.detectChanges(); expect(scrollContainer.scrollTop).withContext(`Expected panel not to scroll.`).toEqual(0); // These down arrows will set the 6th option active, below the fold. [1, 2, 3, 4, 5].forEach(() => trigger._handleKeydown(DOWN_ARROW_EVENT)); // Expect option bottom minus the panel height plus the padding (288 - 256 + 8 = 40) // Expect option bottom minus the panel height plus the padding (288 - 256 + 8 = 40) expect(scrollContainer.scrollTop) .withContext(`Expected panel to reveal the sixth option.`) .toEqual(40); // These up arrows will set the 2nd option active [4, 3, 2, 1].forEach(() => trigger._handleKeydown(UP_ARROW_EVENT)); // Expect no scrolling to have occurred. Still showing bottom of 6th option. // Expect no scrolling to have occurred. Still showing bottom of 6th option. expect(scrollContainer.scrollTop) .withContext(`Expected panel not to scroll up since sixth option still fully visible.`) .toEqual(40); }); it('should scroll to active options that are above the panel', () => { const trigger = fixture.componentInstance.trigger; const scrollContainer = document.querySelector( '.cdk-overlay-pane .mat-mdc-autocomplete-panel', )!; trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.detectChanges(); expect(scrollContainer.scrollTop).withContext(`Expected panel not to scroll.`).toEqual(0); // These down arrows will set the 7th option active, below the fold. [1, 2, 3, 4, 5, 6].forEach(() => trigger._handleKeydown(DOWN_ARROW_EVENT)); // These up arrows will set the 2nd option active [5, 4, 3, 2, 1].forEach(() => trigger._handleKeydown(UP_ARROW_EVENT)); // Expect to show the top of the 2nd option at the top of the panel // Expect to show the top of the 2nd option at the top of the panel expect(scrollContainer.scrollTop) .withContext(`Expected panel to scroll up when option is above panel.`) .toEqual(56); }); it('should close the panel when pressing escape', fakeAsync(() => { const trigger = fixture.componentInstance.trigger; input.focus(); flush(); fixture.detectChanges(); expect(document.activeElement).withContext('Expected input to be focused.').toBe(input); expect(trigger.panelOpen).withContext('Expected panel to be open.').toBe(true); dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); fixture.detectChanges(); flush(); expect(document.activeElement) .withContext('Expected input to continue to be focused.') .toBe(input); expect(trigger.panelOpen).withContext('Expected panel to be closed.').toBe(false); })); it('should prevent the default action when pressing escape', () => { const escapeEvent = dispatchKeyboardEvent(input, 'keydown', ESCAPE); fixture.detectChanges(); expect(escapeEvent.defaultPrevented).toBe(true); }); it('should not close the panel when pressing escape with a modifier', fakeAsync(() => { const trigger = fixture.componentInstance.trigger; input.focus(); flush(); fixture.detectChanges(); expect(document.activeElement).withContext('Expected input to be focused.').toBe(input); expect(trigger.panelOpen).withContext('Expected panel to be open.').toBe(true); const event = dispatchKeyboardEvent(document.body, 'keydown', ESCAPE, undefined, {alt: true}); fixture.detectChanges(); expect(document.activeElement) .withContext('Expected input to continue to be focused.') .toBe(input); expect(trigger.panelOpen).withContext('Expected panel to stay open.').toBe(true); expect(event.defaultPrevented) .withContext('Expected default action not to be prevented.') .toBe(false); })); it('should close the panel when pressing ALT + UP_ARROW', fakeAsync(() => { const trigger = fixture.componentInstance.trigger; const upArrowEvent = createKeyboardEvent('keydown', UP_ARROW, undefined, {alt: true}); spyOn(upArrowEvent, 'stopPropagation').and.callThrough(); input.focus(); flush(); fixture.detectChanges(); expect(document.activeElement).withContext('Expected input to be focused.').toBe(input); expect(trigger.panelOpen).withContext('Expected panel to be open.').toBe(true); dispatchEvent(document.body, upArrowEvent); fixture.detectChanges(); flush(); expect(document.activeElement) .withContext('Expected input to continue to be focused.') .toBe(input); expect(trigger.panelOpen).withContext('Expected panel to be closed.').toBe(false); expect(upArrowEvent.stopPropagation).toHaveBeenCalled(); })); it('should close the panel when tabbing away from a trigger without results', fakeAsync(() => { fixture.componentInstance.states = []; fixture.componentInstance.filteredStates = []; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); input.focus(); flush(); expect(overlayContainerElement.querySelector('.mat-mdc-autocomplete-panel')) .withContext('Expected panel to be rendered.') .toBeTruthy(); dispatchKeyboardEvent(input, 'keydown', TAB); fixture.detectChanges(); tick(); expect(overlayContainerElement.querySelector('.mat-mdc-autocomplete-panel')) .withContext('Expected panel to be removed.') .toBeFalsy(); })); it('should not close when a click event occurs on the outside while the panel has focus', fakeAsync(() => { const trigger = fixture.componentInstance.trigger; input.focus(); flush(); fixture.detectChanges(); expect(document.activeElement).toBe(input, 'Expected input to be focused.'); expect(trigger.panelOpen).toBe(true, 'Expected panel to be open.'); dispatchMouseEvent(document.body, 'click'); fixture.detectChanges(); expect(document.activeElement).toBe(input, 'Expected input to continue to be focused.'); expect(trigger.panelOpen).toBe(true, 'Expected panel to stay open.'); })); it('should reset the active option when closing with the escape key', fakeAsync(() => { const trigger = fixture.componentInstance.trigger; trigger.openPanel(); fixture.detectChanges(); tick(); expect(trigger.panelOpen).withContext('Expected panel to be open.').toBe(true); expect(!!trigger.activeOption).withContext('Expected no active option.').toBe(false); // Press the down arrow a few times. [1, 2, 3].forEach(() => { trigger._handleKeydown(DOWN_ARROW_EVENT); tick(); fixture.detectChanges(); }); // Note that this casts to a boolean, in order to prevent Jasmine // from crashing when trying to stringify the option if the test fails. // Note that this casts to a boolean, in order to prevent Jasmine // from crashing when trying to stringify the option if the test fails. expect(!!trigger.activeOption).withContext('Expected to find an active option.').toBe(true); dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); tick(); expect(!!trigger.activeOption).withContext('Expected no active options.').toBe(false); })); it('should reset the active option when closing by selecting with enter', fakeAsync(() => { const trigger = fixture.componentInstance.trigger; trigger.openPanel(); fixture.detectChanges(); tick(); expect(trigger.panelOpen).withContext('Expected panel to be open.').toBe(true); expect(!!trigger.activeOption).withContext('Expected no active option.').toBe(false); // Press the down arrow a few times. [1, 2, 3].forEach(() => { trigger._handleKeydown(DOWN_ARROW_EVENT); tick(); fixture.detectChanges(); }); // Note that this casts to a boolean, in order to prevent Jasmine // from crashing when trying to stringify the option if the test fails. // Note that this casts to a boolean, in order to prevent Jasmine // from crashing when trying to stringify the option if the test fails. expect(!!trigger.activeOption).withContext('Expected to find an active option.').toBe(true); trigger._handleKeydown(ENTER_EVENT); tick(); expect(!!trigger.activeOption).withContext('Expected no active options.').toBe(false); })); it('should not prevent the default action when a modifier key is pressed', () => { ['metaKey', 'ctrlKey', 'altKey', 'shiftKey'].forEach(name => { const event = createKeyboardEvent('keydown', DOWN_ARROW); Object.defineProperty(event, name, {get: () => true}); fixture.componentInstance.trigger._handleKeydown(event); fixture.detectChanges(); expect(event.defaultPrevented) .withContext(`Expected autocomplete not to block ${name} key`) .toBe(false); }); }); }); describe('option groups', () => { let DOWN_ARROW_EVENT: KeyboardEvent; let UP_ARROW_EVENT: KeyboardEvent; beforeEach(() => { DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW); UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW); }); it('should scroll to active options below the fold', waitForAsync(async () => { const fixture = createComponent(AutocompleteWithGroups); fixture.detectChanges(); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); 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); 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. for (const _unused of [1, 2, 3, 4, 5]) { fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); await new Promise(r => setTimeout(r)); } //