import {Directionality} from '@angular/cdk/bidi'; import { DOWN_ARROW, END, ENTER, HOME, LEFT_ARROW, RIGHT_ARROW, SPACE, TAB, UP_ARROW, A, } from '@angular/cdk/keycodes'; import {OverlayContainer} from '@angular/cdk/overlay'; import {Platform} from '@angular/cdk/platform'; import {ScrollDispatcher, ViewportRuler} from '@angular/cdk/scrolling'; import {wrappedErrorMessage} from '@angular/cdk/private/testing'; import { createKeyboardEvent, dispatchEvent, dispatchFakeEvent, dispatchKeyboardEvent, } from '@angular/cdk/testing'; import { ChangeDetectionStrategy, Component, DebugElement, OnInit, QueryList, ViewChild, ViewChildren, } from '@angular/core'; import { async, ComponentFixture, fakeAsync, flush, inject, TestBed, tick, } from '@angular/core/testing'; import { ControlValueAccessor, FormControl, FormGroup, FormGroupDirective, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, Validators, } from '@angular/forms'; import { ErrorStateMatcher, FloatLabelType, MAT_LABEL_GLOBAL_OPTIONS, MatOption, MatOptionSelectionChange, } from '@angular/material/core'; import {MatFormFieldModule} from '@angular/material/form-field'; import {By} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {LiveAnnouncer} from '@angular/cdk/a11y'; import {Subject, Subscription, EMPTY, Observable} from 'rxjs'; import {map} from 'rxjs/operators'; import {MatSelectModule} from './index'; import {MatSelect} from './select'; import { getMatSelectDynamicMultipleError, getMatSelectNonArrayValueError, getMatSelectNonFunctionValueError, } from './select-errors'; /** Default debounce interval when typing letters to select an option. */ const DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL = 200; describe('MatSelect', () => { let overlayContainer: OverlayContainer; let overlayContainerElement: HTMLElement; let dir: {value: 'ltr'|'rtl', change: Observable}; let scrolledSubject = new Subject(); let viewportRuler: ViewportRuler; let platform: Platform; /** * Configures the test module for MatSelect with the given declarations. This is broken out so * that we're only compiling the necessary test components for each test in order to speed up * overall test time. * @param declarations Components to declare for this block */ function configureMatSelectTestingModule(declarations: any[]) { TestBed.configureTestingModule({ imports: [ MatFormFieldModule, MatSelectModule, ReactiveFormsModule, FormsModule, NoopAnimationsModule, ], declarations: declarations, providers: [ {provide: Directionality, useFactory: () => dir = {value: 'ltr', change: EMPTY}}, { provide: ScrollDispatcher, useFactory: () => ({ scrolled: () => scrolledSubject.asObservable(), }), }, ], }).compileComponents(); inject([OverlayContainer, Platform], (oc: OverlayContainer, p: Platform) => { overlayContainer = oc; overlayContainerElement = oc.getContainerElement(); platform = p; })(); } afterEach(() => { overlayContainer.ngOnDestroy(); }); describe('core', () => { beforeEach(async(() => { configureMatSelectTestingModule([ BasicSelect, MultiSelect, SelectWithGroups, SelectWithGroupsAndNgContainer, SelectWithFormFieldLabel, SelectWithChangeEvent, ]); })); describe('accessibility', () => { describe('for select', () => { let fixture: ComponentFixture; let select: HTMLElement; beforeEach(fakeAsync(() => { fixture = TestBed.createComponent(BasicSelect); fixture.detectChanges(); select = fixture.debugElement.query(By.css('mat-select'))!.nativeElement; })); it('should set the role of the select to listbox', fakeAsync(() => { expect(select.getAttribute('role')).toEqual('listbox'); })); it('should set the aria label of the select to the placeholder', fakeAsync(() => { expect(select.getAttribute('aria-label')).toEqual('Food'); })); it('should support setting a custom aria-label', fakeAsync(() => { fixture.componentInstance.ariaLabel = 'Custom Label'; fixture.detectChanges(); expect(select.getAttribute('aria-label')).toEqual('Custom Label'); })); it('should not set an aria-label if aria-labelledby is specified', fakeAsync(() => { fixture.componentInstance.ariaLabelledby = 'myLabelId'; fixture.detectChanges(); expect(select.getAttribute('aria-label')).toBeFalsy('Expected no aria-label to be set.'); expect(select.getAttribute('aria-labelledby')).toBe('myLabelId'); })); it('should not have aria-labelledby in the DOM if it`s not specified', fakeAsync(() => { fixture.detectChanges(); expect(select.hasAttribute('aria-labelledby')).toBeFalsy(); })); it('should set the tabindex of the select to 0 by default', fakeAsync(() => { expect(select.getAttribute('tabindex')).toEqual('0'); })); it('should be able to override the tabindex', fakeAsync(() => { fixture.componentInstance.tabIndexOverride = 3; fixture.detectChanges(); expect(select.getAttribute('tabindex')).toBe('3'); })); it('should set aria-required for required selects', fakeAsync(() => { expect(select.getAttribute('aria-required')) .toEqual('false', `Expected aria-required attr to be false for normal selects.`); fixture.componentInstance.isRequired = true; fixture.detectChanges(); expect(select.getAttribute('aria-required')) .toEqual('true', `Expected aria-required attr to be true for required selects.`); })); it('should set the mat-select-required class for required selects', fakeAsync(() => { expect(select.classList).not.toContain( 'mat-select-required', `Expected the mat-select-required class not to be set.`); fixture.componentInstance.isRequired = true; fixture.detectChanges(); expect(select.classList).toContain( 'mat-select-required', `Expected the mat-select-required class to be set.`); })); it('should set aria-invalid for selects that are invalid and touched', fakeAsync(() => { expect(select.getAttribute('aria-invalid')) .toEqual('false', `Expected aria-invalid attr to be false for valid selects.`); fixture.componentInstance.isRequired = true; fixture.componentInstance.control.markAsTouched(); fixture.detectChanges(); expect(select.getAttribute('aria-invalid')) .toEqual('true', `Expected aria-invalid attr to be true for invalid selects.`); })); it('should set aria-disabled for disabled selects', fakeAsync(() => { expect(select.getAttribute('aria-disabled')).toEqual('false'); fixture.componentInstance.control.disable(); fixture.detectChanges(); expect(select.getAttribute('aria-disabled')).toEqual('true'); })); it('should set the tabindex of the select to -1 if disabled', fakeAsync(() => { fixture.componentInstance.control.disable(); fixture.detectChanges(); expect(select.getAttribute('tabindex')).toEqual('-1'); fixture.componentInstance.control.enable(); fixture.detectChanges(); expect(select.getAttribute('tabindex')).toEqual('0'); })); it('should set `aria-labelledby` to form field label if there is no placeholder', () => { fixture.destroy(); const labelFixture = TestBed.createComponent(SelectWithFormFieldLabel); labelFixture.detectChanges(); select = labelFixture.debugElement.query(By.css('mat-select'))!.nativeElement; expect(select.getAttribute('aria-labelledby')).toBeTruthy(); expect(select.getAttribute('aria-labelledby')) .toBe(labelFixture.nativeElement.querySelector('label').getAttribute('id')); }); it('should not set `aria-labelledby` if there is a placeholder', () => { fixture.destroy(); const labelFixture = TestBed.createComponent(SelectWithFormFieldLabel); labelFixture.componentInstance.placeholder = 'Thing selector'; labelFixture.detectChanges(); select = labelFixture.debugElement.query(By.css('mat-select'))!.nativeElement; expect(select.getAttribute('aria-labelledby')).toBeFalsy(); }); it('should not set `aria-labelledby` if there is no form field label', () => { fixture.destroy(); const labelFixture = TestBed.createComponent(SelectWithChangeEvent); labelFixture.detectChanges(); select = labelFixture.debugElement.query(By.css('mat-select'))!.nativeElement; expect(select.getAttribute('aria-labelledby')).toBeFalsy(); }); it('should select options via the UP/DOWN arrow keys on a closed select', fakeAsync(() => { const formControl = fixture.componentInstance.control; const options = fixture.componentInstance.options.toArray(); expect(formControl.value).toBeFalsy('Expected no initial value.'); dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); expect(options[0].selected).toBe(true, 'Expected first option to be selected.'); expect(formControl.value).toBe(options[0].value, 'Expected value from first option to have been set on the model.'); dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); // Note that the third option is skipped, because it is disabled. expect(options[3].selected).toBe(true, 'Expected fourth option to be selected.'); expect(formControl.value).toBe(options[3].value, 'Expected value from fourth option to have been set on the model.'); dispatchKeyboardEvent(select, 'keydown', UP_ARROW); expect(options[1].selected).toBe(true, 'Expected second option to be selected.'); expect(formControl.value).toBe(options[1].value, 'Expected value from second option to have been set on the model.'); flush(); })); it('should select first/last options via the HOME/END keys on a closed select', fakeAsync(() => { const formControl = fixture.componentInstance.control; const firstOption = fixture.componentInstance.options.first; const lastOption = fixture.componentInstance.options.last; expect(formControl.value).toBeFalsy('Expected no initial value.'); const endEvent = dispatchKeyboardEvent(select, 'keydown', END); expect(endEvent.defaultPrevented).toBe(true); expect(lastOption.selected).toBe(true, 'Expected last option to be selected.'); expect(formControl.value).toBe(lastOption.value, 'Expected value from last option to have been set on the model.'); const homeEvent = dispatchKeyboardEvent(select, 'keydown', HOME); expect(homeEvent.defaultPrevented).toBe(true); expect(firstOption.selected).toBe(true, 'Expected first option to be selected.'); expect(formControl.value).toBe(firstOption.value, 'Expected value from first option to have been set on the model.'); flush(); })); it('should resume focus from selected item after selecting via click', fakeAsync(() => { const formControl = fixture.componentInstance.control; const options = fixture.componentInstance.options.toArray(); expect(formControl.value).toBeFalsy('Expected no initial value.'); fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); (overlayContainerElement.querySelectorAll('mat-option')[3] as HTMLElement).click(); fixture.detectChanges(); flush(); expect(formControl.value).toBe(options[3].value); dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); fixture.detectChanges(); expect(formControl.value).toBe(options[4].value); flush(); })); it('should select options via LEFT/RIGHT arrow keys on a closed select', fakeAsync(() => { const formControl = fixture.componentInstance.control; const options = fixture.componentInstance.options.toArray(); expect(formControl.value).toBeFalsy('Expected no initial value.'); dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW); expect(options[0].selected).toBe(true, 'Expected first option to be selected.'); expect(formControl.value).toBe(options[0].value, 'Expected value from first option to have been set on the model.'); dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW); dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW); // Note that the third option is skipped, because it is disabled. expect(options[3].selected).toBe(true, 'Expected fourth option to be selected.'); expect(formControl.value).toBe(options[3].value, 'Expected value from fourth option to have been set on the model.'); dispatchKeyboardEvent(select, 'keydown', LEFT_ARROW); expect(options[1].selected).toBe(true, 'Expected second option to be selected.'); expect(formControl.value).toBe(options[1].value, 'Expected value from second option to have been set on the model.'); flush(); })); it('should announce changes via the keyboard on a closed select', fakeAsync(inject([LiveAnnouncer], (liveAnnouncer: LiveAnnouncer) => { spyOn(liveAnnouncer, 'announce'); dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW); expect(liveAnnouncer.announce).toHaveBeenCalledWith('Steak', jasmine.any(Number)); flush(); }))); it('should not throw when reaching a reset option using the arrow keys on a closed select', fakeAsync(() => { fixture.componentInstance.foods = [{value: 'steak-0', viewValue: 'Steak'}, {value: null, viewValue: 'None'}]; fixture.detectChanges(); fixture.componentInstance.control.setValue('steak-0'); expect(() => { dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); fixture.detectChanges(); }).not.toThrow(); flush(); })); it('should open a single-selection select using ALT + DOWN_ARROW', fakeAsync(() => { const {control: formControl, select: selectInstance} = fixture.componentInstance; expect(selectInstance.panelOpen).toBe(false, 'Expected select to be closed.'); expect(formControl.value).toBeFalsy('Expected no initial value.'); const event = createKeyboardEvent('keydown', DOWN_ARROW); Object.defineProperty(event, 'altKey', {get: () => true}); dispatchEvent(select, event); expect(selectInstance.panelOpen).toBe(true, 'Expected select to be open.'); expect(formControl.value).toBeFalsy('Expected value not to have changed.'); })); it('should open a single-selection select using ALT + UP_ARROW', fakeAsync(() => { const {control: formControl, select: selectInstance} = fixture.componentInstance; expect(selectInstance.panelOpen).toBe(false, 'Expected select to be closed.'); expect(formControl.value).toBeFalsy('Expected no initial value.'); const event = createKeyboardEvent('keydown', UP_ARROW); Object.defineProperty(event, 'altKey', {get: () => true}); dispatchEvent(select, event); expect(selectInstance.panelOpen).toBe(true, 'Expected select to be open.'); expect(formControl.value).toBeFalsy('Expected value not to have changed.'); })); it('should close when pressing ALT + DOWN_ARROW', fakeAsync(() => { const {select: selectInstance} = fixture.componentInstance; selectInstance.open(); fixture.detectChanges(); expect(selectInstance.panelOpen).toBe(true, 'Expected select to be open.'); const event = createKeyboardEvent('keydown', DOWN_ARROW); Object.defineProperty(event, 'altKey', {get: () => true}); dispatchEvent(select, event); expect(selectInstance.panelOpen).toBe(false, 'Expected select to be closed.'); expect(event.defaultPrevented).toBe(true, 'Expected default action to be prevented.'); })); it('should close when pressing ALT + UP_ARROW', fakeAsync(() => { const {select: selectInstance} = fixture.componentInstance; selectInstance.open(); fixture.detectChanges(); expect(selectInstance.panelOpen).toBe(true, 'Expected select to be open.'); const event = createKeyboardEvent('keydown', UP_ARROW); Object.defineProperty(event, 'altKey', {get: () => true}); dispatchEvent(select, event); expect(selectInstance.panelOpen).toBe(false, 'Expected select to be closed.'); expect(event.defaultPrevented).toBe(true, 'Expected default action to be prevented.'); })); it('should be able to select options by typing on a closed select', fakeAsync(() => { const formControl = fixture.componentInstance.control; const options = fixture.componentInstance.options.toArray(); expect(formControl.value).toBeFalsy('Expected no initial value.'); dispatchEvent(select, createKeyboardEvent('keydown', 80, 'p')); tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL); expect(options[1].selected).toBe(true, 'Expected second option to be selected.'); expect(formControl.value).toBe(options[1].value, 'Expected value from second option to have been set on the model.'); dispatchEvent(select, createKeyboardEvent('keydown', 69, 'e')); tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL); expect(options[5].selected).toBe(true, 'Expected sixth option to be selected.'); expect(formControl.value).toBe(options[5].value, 'Expected value from sixth option to have been set on the model.'); })); it('should be able to customize the typeahead debounce interval', fakeAsync(() => { const formControl = fixture.componentInstance.control; const options = fixture.componentInstance.options.toArray(); fixture.componentInstance.typeaheadDebounceInterval = 1337; fixture.detectChanges(); expect(formControl.value).toBeFalsy('Expected no initial value.'); dispatchEvent(select, createKeyboardEvent('keydown', 80, 'p')); tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL); expect(formControl.value).toBeFalsy('Expected no value after a bit of time has passed.'); tick(1337); expect(options[1].selected) .toBe(true, 'Expected second option to be selected after all the time has passed.'); expect(formControl.value).toBe(options[1].value, 'Expected value from second option to have been set on the model.'); })); it('should open the panel when pressing a vertical arrow key on a closed multiple select', fakeAsync(() => { fixture.destroy(); const multiFixture = TestBed.createComponent(MultiSelect); const instance = multiFixture.componentInstance; multiFixture.detectChanges(); select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement; const initialValue = instance.control.value; expect(instance.select.panelOpen).toBe(false, 'Expected panel to be closed.'); const event = dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); expect(instance.select.panelOpen).toBe(true, 'Expected panel to be open.'); expect(instance.control.value).toBe(initialValue, 'Expected value to stay the same.'); expect(event.defaultPrevented).toBe(true, 'Expected default to be prevented.'); })); it('should open the panel when pressing a horizontal arrow key on closed multiple select', fakeAsync(() => { fixture.destroy(); const multiFixture = TestBed.createComponent(MultiSelect); const instance = multiFixture.componentInstance; multiFixture.detectChanges(); select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement; const initialValue = instance.control.value; expect(instance.select.panelOpen).toBe(false, 'Expected panel to be closed.'); const event = dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW); expect(instance.select.panelOpen).toBe(true, 'Expected panel to be open.'); expect(instance.control.value).toBe(initialValue, 'Expected value to stay the same.'); expect(event.defaultPrevented).toBe(true, 'Expected default to be prevented.'); })); it('should do nothing when typing on a closed multi-select', fakeAsync(() => { fixture.destroy(); const multiFixture = TestBed.createComponent(MultiSelect); const instance = multiFixture.componentInstance; multiFixture.detectChanges(); select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement; const initialValue = instance.control.value; expect(instance.select.panelOpen).toBe(false, 'Expected panel to be closed.'); dispatchEvent(select, createKeyboardEvent('keydown', 80, 'p')); expect(instance.select.panelOpen).toBe(false, 'Expected panel to stay closed.'); expect(instance.control.value).toBe(initialValue, 'Expected value to stay the same.'); })); it('should do nothing if the key manager did not change the active item', fakeAsync(() => { const formControl = fixture.componentInstance.control; expect(formControl.value).toBeNull('Expected form control value to be empty.'); expect(formControl.pristine).toBe(true, 'Expected form control to be clean.'); dispatchKeyboardEvent(select, 'keydown', 16); // Press a random key. expect(formControl.value).toBeNull('Expected form control value to stay empty.'); expect(formControl.pristine).toBe(true, 'Expected form control to stay clean.'); })); it('should continue from the selected option when the value is set programmatically', fakeAsync(() => { const formControl = fixture.componentInstance.control; formControl.setValue('eggs-5'); fixture.detectChanges(); dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); expect(formControl.value).toBe('pasta-6'); expect(fixture.componentInstance.options.toArray()[6].selected).toBe(true); flush(); })); it('should not shift focus when the selected options are updated programmatically ' + 'in a multi select', fakeAsync(() => { fixture.destroy(); const multiFixture = TestBed.createComponent(MultiSelect); multiFixture.detectChanges(); select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement; multiFixture.componentInstance.select.open(); multiFixture.detectChanges(); const options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[3].focus(); expect(document.activeElement).toBe(options[3], 'Expected fourth option to be focused.'); multiFixture.componentInstance.control.setValue(['steak-0', 'sushi-7']); multiFixture.detectChanges(); expect(document.activeElement) .toBe(options[3], 'Expected fourth option to remain focused.'); })); it('should not cycle through the options if the control is disabled', fakeAsync(() => { const formControl = fixture.componentInstance.control; formControl.setValue('eggs-5'); formControl.disable(); dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); expect(formControl.value).toBe('eggs-5', 'Expected value to remain unchaged.'); })); it('should not wrap selection after reaching the end of the options', fakeAsync(() => { const lastOption = fixture.componentInstance.options.last; fixture.componentInstance.options.forEach(() => { dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); }); expect(lastOption.selected).toBe(true, 'Expected last option to be selected.'); dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); expect(lastOption.selected).toBe(true, 'Expected last option to stay selected.'); flush(); })); it('should not open a multiple select when tabbing through', fakeAsync(() => { fixture.destroy(); const multiFixture = TestBed.createComponent(MultiSelect); multiFixture.detectChanges(); select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement; expect(multiFixture.componentInstance.select.panelOpen) .toBe(false, 'Expected panel to be closed initially.'); dispatchKeyboardEvent(select, 'keydown', TAB); expect(multiFixture.componentInstance.select.panelOpen) .toBe(false, 'Expected panel to stay closed.'); })); it('should toggle the next option when pressing shift + DOWN_ARROW on a multi-select', fakeAsync(() => { fixture.destroy(); const multiFixture = TestBed.createComponent(MultiSelect); const event = createKeyboardEvent('keydown', DOWN_ARROW); Object.defineProperty(event, 'shiftKey', {get: () => true}); multiFixture.detectChanges(); select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement; multiFixture.componentInstance.select.open(); multiFixture.detectChanges(); flush(); expect(multiFixture.componentInstance.select.value).toBeFalsy(); dispatchEvent(select, event); multiFixture.detectChanges(); expect(multiFixture.componentInstance.select.value).toEqual(['pizza-1']); dispatchEvent(select, event); multiFixture.detectChanges(); expect(multiFixture.componentInstance.select.value).toEqual(['pizza-1', 'tacos-2']); })); it('should toggle the previous option when pressing shift + UP_ARROW on a multi-select', fakeAsync(() => { fixture.destroy(); const multiFixture = TestBed.createComponent(MultiSelect); const event = createKeyboardEvent('keydown', UP_ARROW); Object.defineProperty(event, 'shiftKey', {get: () => true}); multiFixture.detectChanges(); select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement; multiFixture.componentInstance.select.open(); multiFixture.detectChanges(); flush(); // Move focus down first. for (let i = 0; i < 5; i++) { dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); multiFixture.detectChanges(); } expect(multiFixture.componentInstance.select.value).toBeFalsy(); dispatchEvent(select, event); multiFixture.detectChanges(); expect(multiFixture.componentInstance.select.value).toEqual(['chips-4']); dispatchEvent(select, event); multiFixture.detectChanges(); expect(multiFixture.componentInstance.select.value).toEqual(['sandwich-3', 'chips-4']); })); it('should prevent the default action when pressing space', fakeAsync(() => { const event = dispatchKeyboardEvent(select, 'keydown', SPACE); expect(event.defaultPrevented).toBe(true); })); it('should prevent the default action when pressing enter', fakeAsync(() => { const event = dispatchKeyboardEvent(select, 'keydown', ENTER); expect(event.defaultPrevented).toBe(true); })); it('should not prevent the default actions on selection keys when pressing a modifier', fakeAsync(() => { [ENTER, SPACE].forEach(key => { const event = createKeyboardEvent('keydown', key); Object.defineProperty(event, 'shiftKey', {get: () => true}); expect(event.defaultPrevented).toBe(false); }); })); it('should consider the selection a result of a user action when closed', fakeAsync(() => { const option = fixture.componentInstance.options.first; const spy = jasmine.createSpy('option selection spy'); const subscription = option.onSelectionChange.pipe(map(e => e.isUserInput)).subscribe(spy); dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); expect(spy).toHaveBeenCalledWith(true); subscription.unsubscribe(); flush(); })); it('should be able to focus the select trigger', fakeAsync(() => { document.body.focus(); // ensure that focus isn't on the trigger already fixture.componentInstance.select.focus(); expect(document.activeElement).toBe(select, 'Expected select element to be focused.'); })); // Having `aria-hidden` on the trigger avoids issues where // screen readers read out the wrong amount of options. it('should set aria-hidden on the trigger element', fakeAsync(() => { const trigger = fixture.debugElement.query(By.css('.mat-select-trigger'))!.nativeElement; expect(trigger.getAttribute('aria-hidden')) .toBe('true', 'Expected aria-hidden to be true when the select is open.'); })); it('should set `aria-multiselectable` to true on multi-select instances', fakeAsync(() => { fixture.destroy(); const multiFixture = TestBed.createComponent(MultiSelect); multiFixture.detectChanges(); select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement; expect(select.getAttribute('aria-multiselectable')).toBe('true'); })); it('should set aria-multiselectable false on single-selection instances', fakeAsync(() => { expect(select.getAttribute('aria-multiselectable')).toBe('false'); })); it('should set aria-activedescendant only while the panel is open', fakeAsync(() => { fixture.componentInstance.control.setValue('chips-4'); fixture.detectChanges(); const host = fixture.debugElement.query(By.css('mat-select'))!.nativeElement; expect(host.hasAttribute('aria-activedescendant')) .toBe(false, 'Expected no aria-activedescendant on init.'); fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); const options = overlayContainerElement.querySelectorAll('mat-option'); expect(host.getAttribute('aria-activedescendant')) .toBe(options[4].id, 'Expected aria-activedescendant to match the active option.'); fixture.componentInstance.select.close(); fixture.detectChanges(); flush(); expect(host.hasAttribute('aria-activedescendant')) .toBe(false, 'Expected no aria-activedescendant when closed.'); })); it('should set aria-activedescendant based on the focused option', fakeAsync(() => { const host = fixture.debugElement.query(By.css('mat-select'))!.nativeElement; fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); const options = overlayContainerElement.querySelectorAll('mat-option'); expect(host.getAttribute('aria-activedescendant')).toBe(options[0].id); [1, 2, 3].forEach(() => { dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW); fixture.detectChanges(); }); expect(host.getAttribute('aria-activedescendant')).toBe(options[4].id); dispatchKeyboardEvent(host, 'keydown', UP_ARROW); fixture.detectChanges(); expect(host.getAttribute('aria-activedescendant')).toBe(options[3].id); })); it('should not change the aria-activedescendant using the horizontal arrow keys', fakeAsync(() => { const host = fixture.debugElement.query(By.css('mat-select'))!.nativeElement; fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); const options = overlayContainerElement.querySelectorAll('mat-option'); expect(host.getAttribute('aria-activedescendant')).toBe(options[0].id); [1, 2, 3].forEach(() => { dispatchKeyboardEvent(host, 'keydown', RIGHT_ARROW); fixture.detectChanges(); }); expect(host.getAttribute('aria-activedescendant')).toBe(options[0].id); })); it('should restore focus to the trigger after selecting an option in multi-select mode', fakeAsync(() => { fixture.destroy(); const multiFixture = TestBed.createComponent(MultiSelect); const instance = multiFixture.componentInstance; multiFixture.detectChanges(); select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement; instance.select.open(); multiFixture.detectChanges(); // Ensure that the select isn't focused to begin with. select.blur(); expect(document.activeElement).not.toBe(select, 'Expected trigger not to be focused.'); const option = overlayContainerElement.querySelector('mat-option')! as HTMLElement; option.click(); multiFixture.detectChanges(); expect(document.activeElement).toBe(select, 'Expected trigger to be focused.'); })); }); describe('for options', () => { let fixture: ComponentFixture; let trigger: HTMLElement; let options: Array; beforeEach(fakeAsync(() => { fixture = TestBed.createComponent(BasicSelect); fixture.detectChanges(); trigger = fixture.debugElement.query(By.css('.mat-select-trigger'))!.nativeElement; trigger.click(); fixture.detectChanges(); options = Array.from(overlayContainerElement.querySelectorAll('mat-option')); })); it('should set the role of mat-option to option', fakeAsync(() => { expect(options[0].getAttribute('role')).toEqual('option'); expect(options[1].getAttribute('role')).toEqual('option'); expect(options[2].getAttribute('role')).toEqual('option'); })); it('should set aria-selected on each option for single select', fakeAsync(() => { expect(options.every(option => !option.hasAttribute('aria-selected'))).toBe(true, 'Expected all unselected single-select options not to have aria-selected set.'); options[1].click(); fixture.detectChanges(); trigger.click(); fixture.detectChanges(); flush(); expect(options[1].getAttribute('aria-selected')).toEqual('true', 'Expected selected single-select option to have aria-selected="true".'); options.splice(1, 1); expect(options.every(option => !option.hasAttribute('aria-selected'))).toBe(true, 'Expected all unselected single-select options not to have aria-selected set.'); })); it('should set aria-selected on each option for multi-select', fakeAsync(() => { fixture.destroy(); const multiFixture = TestBed.createComponent(MultiSelect); multiFixture.detectChanges(); trigger = multiFixture.debugElement.query(By.css('.mat-select-trigger'))!.nativeElement; trigger.click(); multiFixture.detectChanges(); options = Array.from(overlayContainerElement.querySelectorAll('mat-option')); expect(options.every(option => option.hasAttribute('aria-selected') && option.getAttribute('aria-selected') === 'false')).toBe(true, 'Expected all unselected multi-select options to have aria-selected="false".'); options[1].click(); multiFixture.detectChanges(); trigger.click(); multiFixture.detectChanges(); flush(); expect(options[1].getAttribute('aria-selected')).toEqual('true', 'Expected selected multi-select option to have aria-selected="true".'); options.splice(1, 1); expect(options.every(option => option.hasAttribute('aria-selected') && option.getAttribute('aria-selected') === 'false')).toBe(true, 'Expected all unselected multi-select options to have aria-selected="false".'); })); it('should set the tabindex of each option according to disabled state', fakeAsync(() => { expect(options[0].getAttribute('tabindex')).toEqual('0'); expect(options[1].getAttribute('tabindex')).toEqual('0'); expect(options[2].getAttribute('tabindex')).toEqual('-1'); })); it('should set aria-disabled for disabled options', fakeAsync(() => { expect(options[0].getAttribute('aria-disabled')).toEqual('false'); expect(options[1].getAttribute('aria-disabled')).toEqual('false'); expect(options[2].getAttribute('aria-disabled')).toEqual('true'); fixture.componentInstance.foods[2]['disabled'] = false; fixture.detectChanges(); expect(options[0].getAttribute('aria-disabled')).toEqual('false'); expect(options[1].getAttribute('aria-disabled')).toEqual('false'); expect(options[2].getAttribute('aria-disabled')).toEqual('false'); })); }); describe('for option groups', () => { let fixture: ComponentFixture; let trigger: HTMLElement; let groups: NodeListOf; beforeEach(fakeAsync(() => { fixture = TestBed.createComponent(SelectWithGroups); fixture.detectChanges(); trigger = fixture.debugElement.query(By.css('.mat-select-trigger'))!.nativeElement; trigger.click(); fixture.detectChanges(); groups = overlayContainerElement.querySelectorAll('mat-optgroup') as NodeListOf; })); it('should set the appropriate role', fakeAsync(() => { expect(groups[0].getAttribute('role')).toBe('group'); })); it('should set the `aria-labelledby` attribute', fakeAsync(() => { let group = groups[0]; let label = group.querySelector('label')!; expect(label.getAttribute('id')).toBeTruthy('Expected label to have an id.'); expect(group.getAttribute('aria-labelledby')) .toBe(label.getAttribute('id'), 'Expected `aria-labelledby` to match the label id.'); })); it('should set the `aria-disabled` attribute if the group is disabled', fakeAsync(() => { expect(groups[1].getAttribute('aria-disabled')).toBe('true'); })); }); }); describe('overlay panel', () => { let fixture: ComponentFixture; let trigger: HTMLElement; beforeEach(fakeAsync(() => { fixture = TestBed.createComponent(BasicSelect); fixture.detectChanges(); trigger = fixture.debugElement.query(By.css('.mat-select-trigger'))!.nativeElement; })); it('should not throw when attempting to open too early', () => { // Create component and then immediately open without running change detection fixture = TestBed.createComponent(BasicSelect); expect(() => fixture.componentInstance.select.open()).not.toThrow(); }); it('should open the panel when trigger is clicked', fakeAsync(() => { trigger.click(); fixture.detectChanges(); flush(); expect(fixture.componentInstance.select.panelOpen).toBe(true); expect(overlayContainerElement.textContent).toContain('Steak'); expect(overlayContainerElement.textContent).toContain('Pizza'); expect(overlayContainerElement.textContent).toContain('Tacos'); })); it('should close the panel when an item is clicked', fakeAsync(() => { trigger.click(); fixture.detectChanges(); flush(); const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; option.click(); fixture.detectChanges(); flush(); expect(overlayContainerElement.textContent).toEqual(''); expect(fixture.componentInstance.select.panelOpen).toBe(false); })); it('should close the panel when a click occurs outside the panel', fakeAsync(() => { trigger.click(); fixture.detectChanges(); flush(); const backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement; backdrop.click(); fixture.detectChanges(); flush(); expect(overlayContainerElement.textContent).toEqual(''); expect(fixture.componentInstance.select.panelOpen).toBe(false); })); it('should set the width of the overlay based on the trigger', fakeAsync(() => { trigger.style.width = '200px'; trigger.click(); fixture.detectChanges(); flush(); const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; expect(pane.style.minWidth).toBe('200px'); const scrollContainer = document.querySelector('.cdk-overlay-pane .mat-select-panel'); const scrollContainerWidth = scrollContainer!.getBoundingClientRect().width; expect(scrollContainerWidth).toBeCloseTo(200 + 32, 0, 'Expected select panel width to be 100% + 32px of the select field trigger'); })); it('should set the width of the overlay based on a larger trigger width', fakeAsync(() => { // the trigger width exceeds the minimum width of the mat-select-panel trigger.style.width = '400px'; trigger.click(); fixture.detectChanges(); flush(); const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; expect(pane.style.minWidth).toBe('400px'); const scrollContainer = document.querySelector('.cdk-overlay-pane .mat-select-panel'); const scrollContainerWidth = scrollContainer!.getBoundingClientRect().width; expect(scrollContainerWidth).toBeCloseTo(400 + 32, 0, 'Expected select panel width to be 100% + 32px of the select field trigger'); })); it('should update the width of the panel on resize', fakeAsync(() => { trigger.style.width = '300px'; trigger.click(); fixture.detectChanges(); flush(); const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; const initialWidth = parseInt(pane.style.minWidth || '0'); expect(initialWidth).toBeGreaterThan(0); trigger.style.width = '400px'; dispatchFakeEvent(window, 'resize'); fixture.detectChanges(); tick(1000); fixture.detectChanges(); expect(parseInt(pane.style.minWidth || '0')).toBeGreaterThan(initialWidth); })); it('should not attempt to open a select that does not have any options', fakeAsync(() => { fixture.componentInstance.foods = []; fixture.detectChanges(); trigger.click(); fixture.detectChanges(); expect(fixture.componentInstance.select.panelOpen).toBe(false); })); it('should close the panel when tabbing out', fakeAsync(() => { trigger.click(); fixture.detectChanges(); flush(); expect(fixture.componentInstance.select.panelOpen).toBe(true); dispatchKeyboardEvent(trigger, 'keydown', TAB); fixture.detectChanges(); flush(); expect(fixture.componentInstance.select.panelOpen).toBe(false); })); it('should restore focus to the host before tabbing away', fakeAsync(() => { const select = fixture.nativeElement.querySelector('.mat-select'); trigger.click(); fixture.detectChanges(); flush(); expect(fixture.componentInstance.select.panelOpen).toBe(true); // Use a spy since focus can be flaky in unit tests. spyOn(select, 'focus').and.callThrough(); dispatchKeyboardEvent(trigger, 'keydown', TAB); fixture.detectChanges(); flush(); expect(select.focus).toHaveBeenCalled(); })); it('should close when tabbing out from inside the panel', fakeAsync(() => { trigger.click(); fixture.detectChanges(); flush(); expect(fixture.componentInstance.select.panelOpen).toBe(true); const panel = overlayContainerElement.querySelector('.mat-select-panel')!; dispatchKeyboardEvent(panel, 'keydown', TAB); fixture.detectChanges(); flush(); expect(fixture.componentInstance.select.panelOpen).toBe(false); })); it('should focus the first option when pressing HOME', fakeAsync(() => { fixture.componentInstance.control.setValue('pizza-1'); fixture.detectChanges(); trigger.click(); fixture.detectChanges(); flush(); const event = dispatchKeyboardEvent(trigger, 'keydown', HOME); fixture.detectChanges(); expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0); expect(event.defaultPrevented).toBe(true); })); it('should focus the last option when pressing END', fakeAsync(() => { fixture.componentInstance.control.setValue('pizza-1'); fixture.detectChanges(); trigger.click(); fixture.detectChanges(); flush(); const event = dispatchKeyboardEvent(trigger, 'keydown', END); fixture.detectChanges(); expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(7); expect(event.defaultPrevented).toBe(true); })); it('should be able to set extra classes on the panel', fakeAsync(() => { trigger.click(); fixture.detectChanges(); const panel = overlayContainerElement.querySelector('.mat-select-panel') as HTMLElement; expect(panel.classList).toContain('custom-one'); expect(panel.classList).toContain('custom-two'); })); it('should update disableRipple properly on each option', fakeAsync(() => { const options = fixture.componentInstance.options.toArray(); expect(options.every(option => option.disableRipple === false)) .toBeTruthy('Expected all options to have disableRipple set to false initially.'); fixture.componentInstance.disableRipple = true; fixture.detectChanges(); expect(options.every(option => option.disableRipple === true)) .toBeTruthy('Expected all options to have disableRipple set to true.'); })); it('should not show ripples if they were disabled', fakeAsync(() => { fixture.componentInstance.disableRipple = true; fixture.detectChanges(); trigger.click(); fixture.detectChanges(); flush(); const option = overlayContainerElement.querySelector('mat-option')!; dispatchFakeEvent(option, 'mousedown'); dispatchFakeEvent(option, 'mouseup'); expect(option.querySelectorAll('.mat-ripple-element').length).toBe(0); })); it('should be able to render options inside groups with an ng-container', fakeAsync(() => { fixture.destroy(); const groupFixture = TestBed.createComponent(SelectWithGroupsAndNgContainer); groupFixture.detectChanges(); trigger = groupFixture.debugElement.query(By.css('.mat-select-trigger'))!.nativeElement; trigger.click(); groupFixture.detectChanges(); expect(document.querySelectorAll('.cdk-overlay-container mat-option').length) .toBeGreaterThan(0, 'Expected at least one option to be rendered.'); })); it('should not consider itself as blurred if the trigger loses focus while the ' + 'panel is still open', fakeAsync(() => { const selectElement = fixture.nativeElement.querySelector('.mat-select'); const selectInstance = fixture.componentInstance.select; dispatchFakeEvent(selectElement, 'focus'); fixture.detectChanges(); expect(selectInstance.focused).toBe(true, 'Expected select to be focused.'); selectInstance.open(); fixture.detectChanges(); flush(); dispatchFakeEvent(selectElement, 'blur'); fixture.detectChanges(); expect(selectInstance.focused).toBe(true, 'Expected select element to remain focused.'); })); }); describe('selection logic', () => { let fixture: ComponentFixture; let trigger: HTMLElement; let formField: HTMLElement; beforeEach(fakeAsync(() => { fixture = TestBed.createComponent(BasicSelect); fixture.detectChanges(); trigger = fixture.debugElement.query(By.css('.mat-select-trigger'))!.nativeElement; formField = fixture.debugElement.query(By.css('.mat-form-field'))!.nativeElement; })); it('should not float label if no option is selected', fakeAsync(() => { expect(formField.classList.contains('mat-form-field-should-float')) .toBe(false, 'Label should not be floating'); })); it('should focus the first option if no option is selected', fakeAsync(() => { trigger.click(); fixture.detectChanges(); flush(); expect(fixture.componentInstance.select._keyManager.activeItemIndex).toEqual(0); })); it('should select an option when it is clicked', fakeAsync(() => { trigger.click(); fixture.detectChanges(); flush(); let option = overlayContainerElement.querySelector('mat-option') as HTMLElement; option.click(); fixture.detectChanges(); flush(); trigger.click(); fixture.detectChanges(); flush(); option = overlayContainerElement.querySelector('mat-option') as HTMLElement; expect(option.classList).toContain('mat-selected'); expect(fixture.componentInstance.options.first.selected).toBe(true); expect(fixture.componentInstance.select.selected) .toBe(fixture.componentInstance.options.first); })); it('should be able to select an option using the MatOption API', fakeAsync(() => { trigger.click(); fixture.detectChanges(); flush(); const optionInstances = fixture.componentInstance.options.toArray(); const optionNodes: NodeListOf = overlayContainerElement.querySelectorAll('mat-option'); optionInstances[1].select(); fixture.detectChanges(); expect(optionNodes[1].classList).toContain('mat-selected'); expect(optionInstances[1].selected).toBe(true); expect(fixture.componentInstance.select.selected).toBe(optionInstances[1]); })); it('should deselect other options when one is selected', fakeAsync(() => { trigger.click(); fixture.detectChanges(); flush(); let options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[0].click(); fixture.detectChanges(); flush(); trigger.click(); fixture.detectChanges(); flush(); options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; expect(options[1].classList).not.toContain('mat-selected'); expect(options[2].classList).not.toContain('mat-selected'); const optionInstances = fixture.componentInstance.options.toArray(); expect(optionInstances[1].selected).toBe(false); expect(optionInstances[2].selected).toBe(false); })); it('should deselect other options when one is programmatically selected', fakeAsync(() => { let control = fixture.componentInstance.control; let foods = fixture.componentInstance.foods; trigger.click(); fixture.detectChanges(); flush(); let options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[0].click(); fixture.detectChanges(); flush(); control.setValue(foods[1].value); fixture.detectChanges(); trigger.click(); fixture.detectChanges(); flush(); options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; expect(options[0].classList) .not.toContain('mat-selected', 'Expected first option to no longer be selected'); expect(options[1].classList) .toContain('mat-selected', 'Expected second option to be selected'); const optionInstances = fixture.componentInstance.options.toArray(); expect(optionInstances[0].selected) .toBe(false, 'Expected first option to no longer be selected'); expect(optionInstances[1].selected) .toBe(true, 'Expected second option to be selected'); })); it('should remove selection if option has been removed', fakeAsync(() => { let select = fixture.componentInstance.select; trigger.click(); fixture.detectChanges(); flush(); let firstOption = overlayContainerElement.querySelectorAll('mat-option')[0] as HTMLElement; firstOption.click(); fixture.detectChanges(); expect(select.selected).toBe(select.options.first, 'Expected first option to be selected.'); fixture.componentInstance.foods = []; fixture.detectChanges(); flush(); expect(select.selected) .toBeUndefined('Expected selection to be removed when option no longer exists.'); })); it('should display the selected option in the trigger', fakeAsync(() => { trigger.click(); fixture.detectChanges(); flush(); const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; option.click(); fixture.detectChanges(); flush(); const value = fixture.debugElement.query(By.css('.mat-select-value'))!.nativeElement; expect(formField.classList.contains('mat-form-field-should-float')) .toBe(true, 'Label should be floating'); expect(value.textContent).toContain('Steak'); })); it('should focus the selected option if an option is selected', fakeAsync(() => { // must wait for initial writeValue promise to finish flush(); fixture.componentInstance.control.setValue('pizza-1'); fixture.detectChanges(); trigger.click(); fixture.detectChanges(); flush(); // must wait for animation to finish fixture.detectChanges(); expect(fixture.componentInstance.select._keyManager.activeItemIndex).toEqual(1); })); it('should select an option that was added after initialization', fakeAsync(() => { fixture.componentInstance.foods.push({viewValue: 'Potatoes', value: 'potatoes-8'}); trigger.click(); fixture.detectChanges(); flush(); const options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[8].click(); fixture.detectChanges(); flush(); expect(trigger.textContent).toContain('Potatoes'); expect(fixture.componentInstance.select.selected) .toBe(fixture.componentInstance.options.last); })); it('should update the trigger when the selected option label is changed', fakeAsync(() => { fixture.componentInstance.control.setValue('pizza-1'); fixture.detectChanges(); expect(trigger.textContent!.trim()).toBe('Pizza'); fixture.componentInstance.foods[1].viewValue = 'Calzone'; fixture.detectChanges(); expect(trigger.textContent!.trim()).toBe('Calzone'); })); it('should not select disabled options', fakeAsync(() => { trigger.click(); fixture.detectChanges(); const options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; options[2].click(); fixture.detectChanges(); expect(fixture.componentInstance.select.panelOpen).toBe(true); expect(options[2].classList).not.toContain('mat-selected'); expect(fixture.componentInstance.select.selected).toBeUndefined(); })); it('should not select options inside a disabled group', fakeAsync(() => { fixture.destroy(); const groupFixture = TestBed.createComponent(SelectWithGroups); groupFixture.detectChanges(); groupFixture.debugElement.query(By.css('.mat-select-trigger'))!.nativeElement.click(); groupFixture.detectChanges(); const disabledGroup = overlayContainerElement.querySelectorAll('mat-optgroup')[1]; const options = disabledGroup.querySelectorAll('mat-option'); (options[0] as HTMLElement).click(); groupFixture.detectChanges(); expect(groupFixture.componentInstance.select.panelOpen).toBe(true); expect(options[0].classList).not.toContain('mat-selected'); expect(groupFixture.componentInstance.select.selected).toBeUndefined(); })); it('should not throw if triggerValue accessed with no selected value', fakeAsync(() => { expect(() => fixture.componentInstance.select.triggerValue).not.toThrow(); })); it('should emit to `optionSelectionChanges` when an option is selected', fakeAsync(() => { trigger.click(); fixture.detectChanges(); flush(); const spy = jasmine.createSpy('option selection spy'); const subscription = fixture.componentInstance.select.optionSelectionChanges.subscribe(spy); const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; option.click(); fixture.detectChanges(); flush(); expect(spy).toHaveBeenCalledWith(jasmine.any(MatOptionSelectionChange)); subscription.unsubscribe(); })); it('should handle accessing `optionSelectionChanges` before the options are initialized', fakeAsync(() => { fixture.destroy(); fixture = TestBed.createComponent(BasicSelect); let spy = jasmine.createSpy('option selection spy'); let subscription: Subscription; expect(fixture.componentInstance.select.options).toBeFalsy(); expect(() => { subscription = fixture.componentInstance.select.optionSelectionChanges.subscribe(spy); }).not.toThrow(); fixture.detectChanges(); trigger = fixture.debugElement.query(By.css('.mat-select-trigger'))!.nativeElement; trigger.click(); fixture.detectChanges(); flush(); const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; option.click(); fixture.detectChanges(); flush(); expect(spy).toHaveBeenCalledWith(jasmine.any(MatOptionSelectionChange)); subscription!.unsubscribe(); })); it('should emit to `optionSelectionChanges` after the list of options has changed', fakeAsync(() => { let spy = jasmine.createSpy('option selection spy'); let subscription = fixture.componentInstance.select.optionSelectionChanges.subscribe(spy); let selectFirstOption = () => { trigger.click(); fixture.detectChanges(); flush(); const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; option.click(); fixture.detectChanges(); flush(); }; fixture.componentInstance.foods = [{value: 'salad-8', viewValue: 'Salad'}]; fixture.detectChanges(); selectFirstOption(); expect(spy).toHaveBeenCalledTimes(1); fixture.componentInstance.foods = [{value: 'fruit-9', viewValue: 'Fruit'}]; fixture.detectChanges(); selectFirstOption(); expect(spy).toHaveBeenCalledTimes(2); subscription!.unsubscribe(); })); }); describe('forms integration', () => { let fixture: ComponentFixture; let trigger: HTMLElement; beforeEach(fakeAsync(() => { fixture = TestBed.createComponent(BasicSelect); fixture.detectChanges(); trigger = fixture.debugElement.query(By.css('.mat-select-trigger'))!.nativeElement; })); it('should take an initial view value with reactive forms', fakeAsync(() => { fixture.componentInstance.control = new FormControl('pizza-1'); fixture.detectChanges(); const value = fixture.debugElement.query(By.css('.mat-select-value'))!; expect(value.nativeElement.textContent) .toContain('Pizza', `Expected trigger to be populated by the control's initial value.`); trigger = fixture.debugElement.query(By.css('.mat-select-trigger'))!.nativeElement; trigger.click(); fixture.detectChanges(); flush(); const options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; expect(options[1].classList) .toContain('mat-selected', `Expected option with the control's initial value to be selected.`); })); it('should set the view value from the form', fakeAsync(() => { let value = fixture.debugElement.query(By.css('.mat-select-value'))!; expect(value.nativeElement.textContent.trim()).toBe('Food'); fixture.componentInstance.control.setValue('pizza-1'); fixture.detectChanges(); value = fixture.debugElement.query(By.css('.mat-select-value'))!; expect(value.nativeElement.textContent) .toContain('Pizza', `Expected trigger to be populated by the control's new value.`); trigger.click(); fixture.detectChanges(); flush(); const options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; expect(options[1].classList).toContain( 'mat-selected', `Expected option with the control's new value to be selected.`); })); it('should update the form value when the view changes', fakeAsync(() => { expect(fixture.componentInstance.control.value) .toEqual(null, `Expected the control's value to be empty initially.`); trigger.click(); fixture.detectChanges(); flush(); const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; option.click(); fixture.detectChanges(); flush(); expect(fixture.componentInstance.control.value) .toEqual('steak-0', `Expected control's value to be set to the new option.`); })); it('should clear the selection when a nonexistent option value is selected', fakeAsync(() => { fixture.componentInstance.control.setValue('pizza-1'); fixture.detectChanges(); fixture.componentInstance.control.setValue('gibberish'); fixture.detectChanges(); const value = fixture.debugElement.query(By.css('.mat-select-value'))!; expect(value.nativeElement.textContent.trim()) .toBe('Food', `Expected trigger to show the placeholder.`); expect(trigger.textContent) .not.toContain('Pizza', `Expected trigger is cleared when option value is not found.`); trigger.click(); fixture.detectChanges(); flush(); const options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; expect(options[1].classList) .not.toContain('mat-selected', `Expected option w/ the old value not to be selected.`); })); it('should clear the selection when the control is reset', fakeAsync(() => { fixture.componentInstance.control.setValue('pizza-1'); fixture.detectChanges(); fixture.componentInstance.control.reset(); fixture.detectChanges(); const value = fixture.debugElement.query(By.css('.mat-select-value'))!; expect(value.nativeElement.textContent.trim()) .toBe('Food', `Expected trigger to show the placeholder.`); expect(trigger.textContent) .not.toContain('Pizza', `Expected trigger is cleared when option value is not found.`); trigger.click(); fixture.detectChanges(); flush(); const options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf; expect(options[1].classList) .not.toContain('mat-selected', `Expected option w/ the old value not to be selected.`); })); it('should set the control to touched when the select is blurred', fakeAsync(() => { expect(fixture.componentInstance.control.touched) .toEqual(false, `Expected the control to start off as untouched.`); trigger.click(); dispatchFakeEvent(trigger, 'blur'); fixture.detectChanges(); flush(); expect(fixture.componentInstance.control.touched) .toEqual(false, `Expected the control to stay untouched when menu opened.`); const backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement; backdrop.click(); dispatchFakeEvent(trigger, 'blur'); fixture.detectChanges(); flush(); expect(fixture.componentInstance.control.touched) .toEqual(true, `Expected the control to be touched as soon as focus left the select.`); })); it('should set the control to touched when the panel is closed', fakeAsync(() => { expect(fixture.componentInstance.control.touched) .toBe(false, 'Expected the control to start off as untouched.'); trigger.click(); dispatchFakeEvent(trigger, 'blur'); fixture.detectChanges(); flush(); expect(fixture.componentInstance.control.touched) .toBe(false, 'Expected the control to stay untouched when menu opened.'); fixture.componentInstance.select.close(); fixture.detectChanges(); flush(); expect(fixture.componentInstance.control.touched) .toBe(true, 'Expected the control to be touched when the panel was closed.'); })); it('should not set touched when a disabled select is touched', fakeAsync(() => { expect(fixture.componentInstance.control.touched) .toBe(false, 'Expected the control to start off as untouched.'); fixture.componentInstance.control.disable(); dispatchFakeEvent(trigger, 'blur'); expect(fixture.componentInstance.control.touched) .toBe(false, 'Expected the control to stay untouched.'); })); it('should set the control to dirty when the select value changes in DOM', fakeAsync(() => { expect(fixture.componentInstance.control.dirty) .toEqual(false, `Expected control to start out pristine.`); trigger.click(); fixture.detectChanges(); flush(); const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; option.click(); fixture.detectChanges(); flush(); expect(fixture.componentInstance.control.dirty) .toEqual(true, `Expected control to be dirty after value was changed by user.`); })); it('should not set the control to dirty when the value changes programmatically', fakeAsync(() => { expect(fixture.componentInstance.control.dirty) .toEqual(false, `Expected control to start out pristine.`); fixture.componentInstance.control.setValue('pizza-1'); expect(fixture.componentInstance.control.dirty) .toEqual(false, `Expected control to stay pristine after programmatic change.`); })); it('should set an asterisk after the label if control is required', fakeAsync(() => { let requiredMarker = fixture.debugElement.query(By.css('.mat-form-field-required-marker')); expect(requiredMarker) .toBeNull(`Expected label not to have an asterisk, as control was not required.`); fixture.componentInstance.isRequired = true; fixture.detectChanges(); requiredMarker = fixture.debugElement.query(By.css('.mat-form-field-required-marker')); expect(requiredMarker) .not.toBeNull(`Expected label to have an asterisk, as control was required.`); })); }); describe('disabled behavior', () => { it('should disable itself when control is disabled programmatically', fakeAsync(() => { const fixture = TestBed.createComponent(BasicSelect); fixture.detectChanges(); fixture.componentInstance.control.disable(); fixture.detectChanges(); let trigger = fixture.debugElement.query(By.css('.mat-select-trigger'))!.nativeElement; expect(getComputedStyle(trigger).getPropertyValue('cursor')) .toEqual('default', `Expected cursor to be default arrow on disabled control.`); trigger.click(); fixture.detectChanges(); expect(overlayContainerElement.textContent) .toEqual('', `Expected select panel to stay closed.`); expect(fixture.componentInstance.select.panelOpen) .toBe(false, `Expected select panelOpen property to stay false.`); fixture.componentInstance.control.enable(); fixture.detectChanges(); expect(getComputedStyle(trigger).getPropertyValue('cursor')) .toEqual('pointer', `Expected cursor to be a pointer on enabled control.`); trigger.click(); fixture.detectChanges(); expect(overlayContainerElement.textContent) .toContain('Steak', `Expected select panel to open normally on re-enabled control`); expect(fixture.componentInstance.select.panelOpen) .toBe(true, `Expected select panelOpen property to become true.`); })); }); describe('animations', () => { let fixture: ComponentFixture; let formField: HTMLElement; beforeEach(fakeAsync(() => { fixture = TestBed.createComponent(BasicSelect); fixture.detectChanges(); formField = fixture.debugElement.query(By.css('.mat-form-field'))!.nativeElement; })); it('should float the label when the panel is open and unselected', fakeAsync(() => { expect(formField.classList.contains('mat-form-field-should-float')) .toBe(false, 'Expected label to initially have a normal position.'); fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); expect(formField.classList).toContain('mat-form-field-should-float', 'Expected label to animate up to floating position.'); fixture.componentInstance.select.close(); fixture.detectChanges(); flush(); expect(formField.classList).not.toContain('mat-form-field-should-float', 'Expected placeholder to animate back down to normal position.'); })); }); describe('keyboard scrolling', () => { let fixture: ComponentFixture; let host: HTMLElement; let panel: HTMLElement; beforeEach(fakeAsync(() => { fixture = TestBed.createComponent(BasicSelect); fixture.componentInstance.foods = []; for (let i = 0; i < 30; i++) { fixture.componentInstance.foods.push({value: `value-${i}`, viewValue: `Option ${i}`}); } fixture.detectChanges(); fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); fixture.detectChanges(); host = fixture.debugElement.query(By.css('mat-select'))!.nativeElement; panel = overlayContainerElement.querySelector('.mat-select-panel')! as HTMLElement; })); it('should not scroll to options that are completely in the view', fakeAsync(() => { const initialScrollPosition = panel.scrollTop; [1, 2, 3].forEach(() => { dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW); }); expect(panel.scrollTop) .toBe(initialScrollPosition, 'Expected scroll position not to change'); })); it('should scroll down to the active option', fakeAsync(() => { for (let i = 0; i < 15; i++) { dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW); } //