import {Directionality} from '@angular/cdk/bidi'; import {DOWN_ARROW, ENTER, ESCAPE, SPACE, TAB, UP_ARROW} from '@angular/cdk/keycodes'; import {Overlay, OverlayContainer} from '@angular/cdk/overlay'; import {_supportsShadowDom} from '@angular/cdk/platform'; import {ScrollDispatcher} from '@angular/cdk/scrolling'; import {MockNgZone} from '@angular/cdk/private/testing'; import { clearElement, createKeyboardEvent, dispatchEvent, dispatchFakeEvent, dispatchKeyboardEvent, typeInElement, } from '@angular/cdk/testing'; import { ChangeDetectionStrategy, Component, NgZone, OnDestroy, OnInit, Provider, QueryList, Type, ViewChild, ViewChildren, ViewEncapsulation, } from '@angular/core'; import { async, ComponentFixture, fakeAsync, flush, inject, TestBed, tick, } 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 {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 {MatInputModule} from '../input/index'; import { getMatAutocompleteMissingPanelError, MAT_AUTOCOMPLETE_DEFAULT_OPTIONS, MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocomplete, MatAutocompleteModule, MatAutocompleteOrigin, MatAutocompleteSelectedEvent, MatAutocompleteTrigger, } from './index'; describe('MatAutocomplete', () => { let overlayContainer: OverlayContainer; let overlayContainerElement: HTMLElement; let zone: MockNgZone; // Creates a test component fixture. function createComponent(component: Type, providers: Provider[] = []) { TestBed.configureTestingModule({ imports: [ MatAutocompleteModule, MatFormFieldModule, MatInputModule, FormsModule, ReactiveFormsModule, NoopAnimationsModule ], declarations: [component], providers: [ {provide: NgZone, useFactory: () => zone = new MockNgZone()}, ...providers ] }); TestBed.compileComponents(); inject([OverlayContainer], (oc: OverlayContainer) => { overlayContainer = oc; overlayContainerElement = oc.getContainerElement(); })(); return TestBed.createComponent(component); } afterEach(inject([OverlayContainer], (currentOverlayContainer: OverlayContainer) => { // Since we're resetting the testing module in some of the tests, // we can potentially have multiple overlay containers. currentOverlayContainer.ngOnDestroy(); overlayContainer.ngOnDestroy(); })); 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) .toBe(false, `Expected panel state to start out closed.`); dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) .toBe(true, `Expected panel state to read open when input is focused.`); expect(overlayContainerElement.textContent) .toContain('Alabama', `Expected panel to display when input is focused.`); expect(overlayContainerElement.textContent) .toContain('California', `Expected panel to display when input is focused.`); }); 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).toBe(false, 'Expected panel state to start out closed.'); dispatchFakeEvent(input, 'focusin'); flush(); fixture.detectChanges(); expect(trigger.panelOpen).toBe(false, 'Expected panel to stay closed.'); })); 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).toBe(false, 'Expected panel state to start out closed.'); dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW); flush(); fixture.detectChanges(); expect(trigger.panelOpen).toBe(false, 'Expected panel to stay closed.'); })); it('should open the panel programmatically', () => { expect(fixture.componentInstance.trigger.panelOpen) .toBe(false, `Expected panel state to start out closed.`); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) .toBe(true, `Expected panel state to read open when opened programmatically.`); expect(overlayContainerElement.textContent) .toContain('Alabama', `Expected panel to display when opened programmatically.`); expect(overlayContainerElement.textContent) .toContain('California', `Expected panel to display when opened programmatically.`); }); it('should show the panel when the first open is after the initial zone stabilization', async(() => { // Note that we're running outside the Angular zone, in order to be able // to test properly without the subscription from `_subscribeToClosingActions` // giving us a false positive. fixture.ngZone!.runOutsideAngular(() => { fixture.componentInstance.trigger.openPanel(); Promise.resolve().then(() => { expect(fixture.componentInstance.panel.showPanel) .toBe(true, `Expected panel to be visible.`); }); }); })); it('should close the panel when the user clicks away', fakeAsync(() => { dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); zone.simulateZoneExit(); dispatchFakeEvent(document, 'click'); expect(fixture.componentInstance.trigger.panelOpen) .toBe(false, `Expected clicking outside the panel to set its state to closed.`); expect(overlayContainerElement.textContent) .toEqual('', `Expected clicking outside the panel to close the panel.`); })); 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) .toBe(false, `Expected tapping outside the panel to set its state to closed.`); expect(overlayContainerElement.textContent) .toEqual('', `Expected tapping outside the panel to close the panel.`); })); it('should close the panel when an option is clicked', fakeAsync(() => { dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); zone.simulateZoneExit(); const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; option.click(); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) .toBe(false, `Expected clicking an option to set the panel state to closed.`); expect(overlayContainerElement.textContent) .toEqual('', `Expected clicking an option to close the panel.`); })); it('should close the panel when a newly created option is clicked', fakeAsync(() => { dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); zone.simulateZoneExit(); // Filter down the option list to a subset of original options ('Alabama', 'California') typeInElement(input, 'al'); fixture.detectChanges(); tick(); 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(); tick(); options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[1].click(); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) .toBe(false, `Expected clicking a new option to set the panel state to closed.`); expect(overlayContainerElement.textContent) .toEqual('', `Expected clicking a new option to close the panel.`); })); it('should close the panel programmatically', () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); fixture.componentInstance.trigger.closePanel(); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) .toBe(false, `Expected closing programmatically to set the panel state to closed.`); expect(overlayContainerElement.textContent) .toEqual('', `Expected closing programmatically to close the panel.`); }); 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-autocomplete-panel') as HTMLElement; expect(panel.classList) .toContain('mat-autocomplete-visible', `Expected panel to start out visible.`); // Filter down the option list such that no options match the value typeInElement(input, 'af'); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(panel.classList) .toContain('mat-autocomplete-hidden', `Expected panel to hide itself when empty.`); })); it('should keep the label floating until the panel closes', fakeAsync(() => { fixture.componentInstance.trigger.openPanel(); expect(fixture.componentInstance.formField.floatLabel) .toEqual('always', 'Expected label to float as soon as panel opens.'); zone.simulateZoneExit(); fixture.detectChanges(); const options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[1].click(); fixture.detectChanges(); expect(fixture.componentInstance.formField.floatLabel) .toEqual('auto', 'Expected label to return to auto state after panel closes.'); })); it('should not open the panel when the `input` event is invoked on a non-focused input', () => { expect(fixture.componentInstance.trigger.panelOpen) .toBe(false, `Expected panel state to start out closed.`); input.value = 'Alabama'; dispatchFakeEvent(input, 'input'); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) .toBe(false, `Expected panel state to stay closed.`); }); it('should not mess with label placement if set to never', fakeAsync(() => { fixture.componentInstance.floatLabel = 'never'; fixture.detectChanges(); fixture.componentInstance.trigger.openPanel(); expect(fixture.componentInstance.formField.floatLabel) .toEqual('never', 'Expected label to stay static.'); flush(); fixture.detectChanges(); const options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[1].click(); fixture.detectChanges(); expect(fixture.componentInstance.formField.floatLabel) .toEqual('never', 'Expected label to stay in static state after close.'); })); it('should not mess with label placement if set to always', fakeAsync(() => { fixture.componentInstance.floatLabel = 'always'; fixture.detectChanges(); fixture.componentInstance.trigger.openPanel(); expect(fixture.componentInstance.formField.floatLabel) .toEqual('always', 'Expected label to stay elevated on open.'); flush(); fixture.detectChanges(); const options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[1].click(); fixture.detectChanges(); expect(fixture.componentInstance.formField.floatLabel) .toEqual('always', 'Expected label to stay elevated after close.'); })); it('should toggle the visibility when typing and closing the panel', fakeAsync(() => { fixture.componentInstance.trigger.openPanel(); tick(); fixture.detectChanges(); expect(overlayContainerElement.querySelector('.mat-autocomplete-panel')!.classList) .toContain('mat-autocomplete-visible', 'Expected panel to be visible.'); typeInElement(input, 'x'); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(overlayContainerElement.querySelector('.mat-autocomplete-panel')!.classList) .toContain('mat-autocomplete-hidden', 'Expected panel to be 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-autocomplete-panel')!.classList) .toContain('mat-autocomplete-visible', 'Expected panel to be 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.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.detectChanges(); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); expect(fixture.componentInstance.openedSpy).not.toHaveBeenCalled(); fixture.componentInstance.filteredStates = fixture.componentInstance.states = [{name: 'California', code: 'CA'}]; 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.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) .toBe(false, `Expected panel state to start out closed.`); fixture.componentInstance.autocompleteDisabled = true; fixture.detectChanges(); dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) .toBe(false, `Expected panel to remain closed.`); }); it('should continue to update the model if the autocomplete is disabled', () => { fixture.componentInstance.autocompleteDisabled = true; 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('true'); fixture.componentInstance.autocompleteDisabled = true; fixture.detectChanges(); expect(input.getAttribute('aria-haspopup')).toBe('false'); }); }); it('should not close the panel when clicking on the input', fakeAsync(() => { const fixture = createComponent(SimpleAutocomplete); fixture.detectChanges(); const input = fixture.debugElement.query(By.css('input'))!.nativeElement; dispatchFakeEvent(input, 'focusin'); fixture.detectChanges(); zone.simulateZoneExit(); expect(fixture.componentInstance.trigger.panelOpen) .toBe(true, 'Expected panel to be opened on focus.'); input.click(); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) .toBe(true, 'Expected panel to remain opened after clicking on the input.'); })); it('should not close the panel when clicking on the input inside shadow DOM', fakeAsync(() => { // 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(); zone.simulateZoneExit(); expect(fixture.componentInstance.trigger.panelOpen) .toBe(true, 'Expected panel to be opened on focus.'); input.click(); fixture.detectChanges(); expect(fixture.componentInstance.trigger.panelOpen) .toBe(true, 'Expected panel to remain opened after clicking on the input.'); })); 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(); }); 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', () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); zone.simulateZoneExit(); typeInElement(input, 'a'); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.value) .toEqual('a', 'Expected control value to be updated as user types.'); typeInElement(input, 'l'); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.value) .toEqual('al', 'Expected control value to be updated as user types.'); }); 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) .toBe('Alabama', 'Expected value to be propagated to the form control.'); }); it('should update control value when option is selected with option value', fakeAsync(() => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); zone.simulateZoneExit(); const options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[1].click(); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.value) .toEqual({code: 'CA', name: 'California'}, 'Expected control value to equal the selected option value.'); })); it('should update the control back to a string if user types after an option is selected', fakeAsync(() => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); zone.simulateZoneExit(); const options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[1].click(); fixture.detectChanges(); clearElement(input); typeInElement(input, 'Californi'); fixture.detectChanges(); tick(); expect(fixture.componentInstance.stateCtrl.value) .toEqual('Californi', 'Expected control value to revert back to string.'); })); it('should fill the text field with display value when an option is selected', fakeAsync(() => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); zone.simulateZoneExit(); const options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[1].click(); fixture.detectChanges(); expect(input.value) .toContain('California', `Expected text field to fill with selected value.`); })); it('should fill the text field with value if displayWith is not set', fakeAsync(() => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); zone.simulateZoneExit(); fixture.componentInstance.panel.displayWith = null; fixture.componentInstance.options.toArray()[1].value = 'test value'; fixture.detectChanges(); const options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[1].click(); fixture.detectChanges(); expect(input.value) .toContain('test value', `Expected input to fall back to selected option's 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) .toContain('Alabama', `Expected input to fill with matching option's viewValue.`); })); 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).toEqual('', `Expected input value to be empty after reset.`); })); it('should disable input in view when disabled programmatically', () => { const formFieldElement = fixture.debugElement.query(By.css('.mat-form-field'))!.nativeElement; expect(input.disabled) .toBe(false, `Expected input to start out enabled in view.`); expect(formFieldElement.classList.contains('mat-form-field-disabled')) .toBe(false, `Expected input underline to start out with normal styles.`); fixture.componentInstance.stateCtrl.disable(); fixture.detectChanges(); expect(input.disabled) .toBe(true, `Expected input to be disabled in view when disabled programmatically.`); expect(formFieldElement.classList.contains('mat-form-field-disabled')) .toBe(true, `Expected input underline to display disabled styles.`); }); it('should mark the autocomplete control as dirty as user types', () => { expect(fixture.componentInstance.stateCtrl.dirty) .toBe(false, `Expected control to start out pristine.`); typeInElement(input, 'a'); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.dirty) .toBe(true, `Expected control to become dirty when the user types into the input.`); }); it('should mark the autocomplete control as dirty when an option is selected', fakeAsync(() => { expect(fixture.componentInstance.stateCtrl.dirty) .toBe(false, `Expected control to start out pristine.`); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); zone.simulateZoneExit(); const options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[1].click(); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.dirty) .toBe(true, `Expected control to become dirty when an option was selected.`); })); it('should not mark the control dirty when the value is set programmatically', () => { expect(fixture.componentInstance.stateCtrl.dirty) .toBe(false, `Expected control to start out pristine.`); fixture.componentInstance.stateCtrl.setValue('AL'); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.dirty) .toBe(false, `Expected control to stay pristine if value is set programmatically.`); }); it('should mark the autocomplete control as touched on blur', () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.touched) .toBe(false, `Expected control to start out untouched.`); dispatchFakeEvent(input, 'blur'); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.touched) .toBe(true, `Expected control to become touched on blur.`); }); it('should disable the input when used with a value accessor and without `matInput`', () => { overlayContainer.ngOnDestroy(); 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('keyboard events', () => { let fixture: ComponentFixture; let input: HTMLInputElement; let DOWN_ARROW_EVENT: KeyboardEvent; let UP_ARROW_EVENT: KeyboardEvent; let ENTER_EVENT: KeyboardEvent; beforeEach(fakeAsync(() => { 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(); zone.simulateZoneExit(); })); 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) .toBe(true, `Expected panel state to stay open when DOWN key is pressed.`); expect(overlayContainerElement.textContent) .toContain('Alabama', `Expected panel to keep displaying when DOWN key is pressed.`); expect(overlayContainerElement.textContent) .toContain('California', `Expected panel to keep displaying when DOWN key is pressed.`); }); 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) .toBe(true, 'Expected first down press to open the pane.'); componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.detectChanges(); expect(componentInstance.trigger.activeOption === componentInstance.options.first) .toBe(true, 'Expected first option to be active.'); expect(optionEls[0].classList).toContain('mat-active'); expect(optionEls[1].classList).not.toContain('mat-active'); componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.detectChanges(); expect(componentInstance.trigger.activeOption === componentInstance.options.toArray()[1]) .toBe(true, 'Expected second option to be active.'); expect(optionEls[0].classList).not.toContain('mat-active'); expect(optionEls[1].classList).toContain('mat-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) .toBe(true, 'Expected first up press to open the pane.'); componentInstance.trigger._handleKeydown(UP_ARROW_EVENT); fixture.detectChanges(); expect(componentInstance.trigger.activeOption === componentInstance.options.last) .toBe(true, 'Expected last option to be active.'); expect(optionEls[10].classList).toContain('mat-active'); expect(optionEls[0].classList).not.toContain('mat-active'); componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.detectChanges(); expect(componentInstance.trigger.activeOption === componentInstance.options.first) .toBe(true, 'Expected first option to be active.'); expect(optionEls[0].classList).toContain('mat-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) .toBe(true, 'Expected first option to be active.'); expect(optionEls[0].classList).toContain('mat-active'); expect(optionEls[1].classList).not.toContain('mat-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(); expect(input.value) .toContain('Alabama', `Expected text field to fill with selected value on ENTER.`); })); it('should prevent the default enter key action', fakeAsync(() => { fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); flush(); fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT); expect(ENTER_EVENT.defaultPrevented) .toBe(true, 'Expected the default action to have been prevented.'); })); 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).toBe(false, 'Default action should not be prevented.'); }); 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.toContain('New York', `Expected option not to be selected on SPACE.`); }); it('should mark the control dirty when selecting an option from the keyboard', fakeAsync(() => { expect(fixture.componentInstance.stateCtrl.dirty) .toBe(false, `Expected control to start out pristine.`); fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); flush(); fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT); fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.dirty) .toBe(true, `Expected control to become dirty when option was selected by ENTER.`); })); 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(); expect(fixture.componentInstance.trigger.panelOpen) .toBe(false, `Expected panel state to read closed after ENTER key.`); expect(overlayContainerElement.textContent) .toEqual('', `Expected panel to close after ENTER key.`); dispatchFakeEvent(input, 'focusin'); clearElement(input); typeInElement(input, 'Alabama'); fixture.detectChanges(); tick(); expect(fixture.componentInstance.trigger.panelOpen) .toBe(true, `Expected panel state to read open when typing in input.`); expect(overlayContainerElement.textContent) .toContain('Alabama', `Expected panel to display when typing in input.`); })); 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).toBe(true, 'Expected panel to be open.'); trigger.closePanel(); fixture.detectChanges(); expect(trigger.panelOpen).toBe(false, 'Expected panel to be closed.'); // 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).toBe(false, 'Expected panel to stay closed.'); })); it('should scroll to active options below the fold', () => { const trigger = fixture.componentInstance.trigger; const scrollContainer = document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel')!; trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.detectChanges(); expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`); // 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 (288 - 256 = 32) expect(scrollContainer.scrollTop) .toEqual(32, `Expected panel to reveal the sixth option.`); }); it('should scroll to active options on UP arrow', () => { const scrollContainer = document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel')!; fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT); fixture.detectChanges(); // Expect option bottom minus the panel height (528 - 256 = 272) expect(scrollContainer.scrollTop).toEqual(272, `Expected panel to reveal last option.`); }); 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-autocomplete-panel')!; trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.detectChanges(); expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`); // 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 (288 - 256 = 32) expect(scrollContainer.scrollTop) .toEqual(32, `Expected panel to reveal the sixth option.`); // 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(scrollContainer.scrollTop) .toEqual(32, `Expected panel not to scroll up since sixth option still fully visible.`); }); it('should scroll to active options that are above the panel', () => { const trigger = fixture.componentInstance.trigger; const scrollContainer = document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel')!; trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.detectChanges(); expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`); // 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(scrollContainer.scrollTop) .toEqual(48, `Expected panel to scroll up when option is above panel.`); }); it('should close the panel when pressing escape', 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.'); dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); fixture.detectChanges(); expect(document.activeElement).toBe(input, 'Expected input to continue to be focused.'); expect(trigger.panelOpen).toBe(false, 'Expected panel to be closed.'); })); it('should prevent the default action when pressing escape', fakeAsync(() => { const escapeEvent = dispatchKeyboardEvent(input, 'keydown', ESCAPE); fixture.detectChanges(); expect(escapeEvent.defaultPrevented).toBe(true); })); it('should close the panel when pressing ALT + UP_ARROW', fakeAsync(() => { const trigger = fixture.componentInstance.trigger; const upArrowEvent = createKeyboardEvent('keydown', UP_ARROW); Object.defineProperty(upArrowEvent, 'altKey', {get: () => true}); spyOn(upArrowEvent, 'stopPropagation').and.callThrough(); 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.'); dispatchEvent(document.body, upArrowEvent); fixture.detectChanges(); expect(document.activeElement).toBe(input, 'Expected input to continue to be focused.'); expect(trigger.panelOpen).toBe(false, 'Expected panel to be closed.'); 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.detectChanges(); input.focus(); flush(); expect(overlayContainerElement.querySelector('.mat-autocomplete-panel')) .toBeTruthy('Expected panel to be rendered.'); dispatchKeyboardEvent(input, 'keydown', TAB); fixture.detectChanges(); expect(overlayContainerElement.querySelector('.mat-autocomplete-panel')) .toBeFalsy('Expected panel to be removed.'); })); 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).toBe(true, 'Expected panel to be open.'); expect(!!trigger.activeOption).toBe(false, 'Expected no active option.'); // 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. expect(!!trigger.activeOption).toBe(true, 'Expected to find an active option.'); dispatchKeyboardEvent(document.body, 'keydown', ESCAPE); tick(); expect(!!trigger.activeOption).toBe(false, 'Expected no active options.'); })); 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).toBe(true, 'Expected panel to be open.'); expect(!!trigger.activeOption).toBe(false, 'Expected no active option.'); // 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. expect(!!trigger.activeOption).toBe(true, 'Expected to find an active option.'); trigger._handleKeydown(ENTER_EVENT); tick(); expect(!!trigger.activeOption).toBe(false, 'Expected no active options.'); })); }); describe('option groups', () => { let fixture: ComponentFixture; let DOWN_ARROW_EVENT: KeyboardEvent; let UP_ARROW_EVENT: KeyboardEvent; let container: HTMLElement; beforeEach(fakeAsync(() => { fixture = createComponent(AutocompleteWithGroups); fixture.detectChanges(); DOWN_ARROW_EVENT = createKeyboardEvent('keydown', DOWN_ARROW); UP_ARROW_EVENT = createKeyboardEvent('keydown', UP_ARROW); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); tick(); fixture.detectChanges(); container = document.querySelector('.mat-autocomplete-panel') as HTMLElement; })); it('should scroll to active options below the fold', fakeAsync(() => { fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); tick(); fixture.detectChanges(); expect(container.scrollTop).toBe(0, 'Expected the panel not to scroll.'); // Press the down arrow five times. [1, 2, 3, 4, 5].forEach(() => { fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); tick(); }); //