import {LiveAnnouncer} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import { A, DOWN_ARROW, END, ENTER, ESCAPE, HOME, LEFT_ARROW, PAGE_DOWN, PAGE_UP, RIGHT_ARROW, SPACE, TAB, UP_ARROW, } from '@angular/cdk/keycodes'; import {OverlayContainer, OverlayModule} from '@angular/cdk/overlay'; import {ScrollDispatcher} from '@angular/cdk/scrolling'; import { createKeyboardEvent, dispatchEvent, dispatchFakeEvent, dispatchKeyboardEvent, wrappedErrorMessage, } from '@angular/cdk/testing/private'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DebugElement, ElementRef, OnInit, Provider, QueryList, ViewChild, ViewChildren, inject, } from '@angular/core'; import { ComponentFixture, TestBed, fakeAsync, flush, tick, waitForAsync, } from '@angular/core/testing'; import { ControlValueAccessor, FormBuilder, FormControl, FormGroup, FormGroupDirective, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, Validators, } from '@angular/forms'; import {ErrorStateMatcher, MatOption, MatOptionSelectionChange} from '@angular/material/core'; import { FloatLabelType, MAT_FORM_FIELD_DEFAULT_OPTIONS, MatFormFieldModule, } from '@angular/material/form-field'; import {MAT_SELECT_CONFIG, MatSelectConfig} from '@angular/material/select'; import {By} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {EMPTY, Observable, Subject, Subscription} 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 overlayContainerElement: HTMLElement; let dir: {value: 'ltr' | 'rtl'; change: Observable}; let scrolledSubject = new Subject(); /** * 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 * @param providers Additional providers for this block */ function configureMatSelectTestingModule(declarations: any[], providers: Provider[] = []) { TestBed.configureTestingModule({ imports: [ MatFormFieldModule, MatSelectModule, ReactiveFormsModule, FormsModule, NoopAnimationsModule, OverlayModule, ], providers: [ {provide: Directionality, useFactory: () => (dir = {value: 'ltr', change: EMPTY})}, { provide: ScrollDispatcher, useFactory: () => ({ scrolled: () => scrolledSubject, }), }, ...providers, ], declarations: declarations, }); overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement(); } describe('core', () => { beforeEach(waitForAsync(() => { configureMatSelectTestingModule([ BasicSelect, SelectInsideAModal, MultiSelect, SelectWithGroups, SelectWithGroupsAndNgContainer, SelectWithFormFieldLabel, SelectWithChangeEvent, SelectInsideDynamicFormGroup, ]); })); describe('accessibility', () => { describe('for select', () => { let fixture: ComponentFixture; let select: HTMLElement; beforeEach(() => { fixture = TestBed.createComponent(BasicSelect); fixture.detectChanges(); select = fixture.debugElement.query(By.css('mat-select'))!.nativeElement; }); it('should set the role of the select to combobox', () => { expect(select.getAttribute('role')).toEqual('combobox'); expect(select.getAttribute('aria-haspopup')).toBe('listbox'); }); it('should point the aria-controls attribute to the listbox', fakeAsync(() => { expect(select.hasAttribute('aria-controls')).toBe(false); fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); const ariaControls = select.getAttribute('aria-controls'); expect(ariaControls).toBeTruthy(); expect(ariaControls).toBe(document.querySelector('.mat-mdc-select-panel')!.id); })); it('should set aria-expanded based on the select open state', fakeAsync(() => { expect(select.getAttribute('aria-expanded')).toBe('false'); fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); expect(select.getAttribute('aria-expanded')).toBe('true'); })); it('should support setting a custom aria-label', () => { fixture.componentInstance.ariaLabel = 'Custom Label'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(select.getAttribute('aria-label')).toEqual('Custom Label'); expect(select.hasAttribute('aria-labelledby')).toBeFalsy(); }); it('should be able to add an extra aria-labelledby on top of the default', () => { fixture.componentInstance.ariaLabelledby = 'myLabelId'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const labelId = fixture.nativeElement.querySelector('label').id; const valueId = fixture.nativeElement.querySelector('.mat-mdc-select-value').id; expect(select.getAttribute('aria-labelledby')).toBe(`${labelId} ${valueId} myLabelId`); }); it('should set aria-labelledby to the value and label IDs', () => { fixture.detectChanges(); const labelId = fixture.nativeElement.querySelector('label').id; const valueId = fixture.nativeElement.querySelector('.mat-mdc-select-value').id; expect(select.getAttribute('aria-labelledby')).toBe(`${labelId} ${valueId}`); }); it('should trim the trigger aria-labelledby when there is no label', fakeAsync(() => { fixture.componentInstance.hasLabel = false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); fixture.detectChanges(); // Note that we assert that there are no spaces around the value. const valueId = fixture.nativeElement.querySelector('.mat-mdc-select-value').id; expect(select.getAttribute('aria-labelledby')).toBe(`${valueId}`); })); it('should set the tabindex of the select to 0 by default', () => { expect(select.getAttribute('tabindex')).toEqual('0'); }); it('should set `aria-describedby` to the id of the mat-hint', () => { expect(select.getAttribute('aria-describedby')).toBeNull(); fixture.componentInstance.hint = 'test'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const hint = fixture.debugElement.query(By.css('mat-hint')).nativeElement; expect(select.getAttribute('aria-describedby')).toBe(hint.getAttribute('id')); expect(select.getAttribute('aria-describedby')).toMatch(/^mat-mdc-hint-\w+\d+$/); }); it('should support user binding to `aria-describedby`', () => { fixture.componentInstance.ariaDescribedBy = 'test'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(select.getAttribute('aria-describedby')).toBe('test'); }); it('should be able to override the tabindex', () => { fixture.componentInstance.tabIndexOverride = 3; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(select.getAttribute('tabindex')).toBe('3'); }); it('should set aria-required for required selects', () => { expect(select.getAttribute('aria-required')) .withContext(`Expected aria-required attr to be false for normal selects.`) .toEqual('false'); fixture.componentInstance.isRequired = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(select.getAttribute('aria-required')) .withContext(`Expected aria-required attr to be true for required selects.`) .toEqual('true'); }); it('should set the mat-select-required class for required selects', () => { expect(select.classList).not.toContain( 'mat-mdc-select-required', `Expected the mat-mdc-select-required class not to be set.`, ); fixture.componentInstance.isRequired = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(select.classList) .withContext(`Expected the mat-mdc-select-required class to be set.`) .toContain('mat-mdc-select-required'); }); it('should set aria-invalid for selects that are invalid and touched', () => { expect(select.getAttribute('aria-invalid')) .withContext(`Expected aria-invalid attr to be false for valid selects.`) .toEqual('false'); fixture.componentInstance.isRequired = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.componentInstance.control.markAsTouched(); fixture.detectChanges(); expect(select.getAttribute('aria-invalid')) .withContext(`Expected aria-invalid attr to be true for invalid selects.`) .toEqual('true'); }); it('should set aria-disabled for disabled selects', () => { 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', () => { 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 the value ID if there is no form field', () => { fixture.destroy(); const labelFixture = TestBed.createComponent(SelectWithChangeEvent); labelFixture.detectChanges(); select = labelFixture.debugElement.query(By.css('mat-select'))!.nativeElement; const valueId = labelFixture.nativeElement.querySelector('.mat-mdc-select-value').id; expect(select.getAttribute('aria-labelledby')?.trim()).toBe(valueId); }); 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).withContext('Expected no initial value.').toBeFalsy(); dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); expect(options[0].selected) .withContext('Expected first option to be selected.') .toBe(true); expect(formControl.value) .withContext('Expected value from first option to have been set on the model.') .toBe(options[0].value); dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); // Note that the third option is skipped, because it is disabled. // Note that the third option is skipped, because it is disabled. expect(options[3].selected) .withContext('Expected fourth option to be selected.') .toBe(true); expect(formControl.value) .withContext('Expected value from fourth option to have been set on the model.') .toBe(options[3].value); dispatchKeyboardEvent(select, 'keydown', UP_ARROW); expect(options[1].selected) .withContext('Expected second option to be selected.') .toBe(true); expect(formControl.value) .withContext('Expected value from second option to have been set on the model.') .toBe(options[1].value); flush(); })); it('should go back to first option if value is reset after interacting using the arrow keys on a closed select', fakeAsync(() => { const formControl = fixture.componentInstance.control; const options = fixture.componentInstance.options.toArray(); expect(formControl.value).withContext('Expected no initial value.').toBeFalsy(); dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); flush(); expect(options[0].selected) .withContext('Expected first option to be selected.') .toBe(true); expect(formControl.value) .withContext('Expected value from first option to have been set on the model.') .toBe(options[0].value); formControl.reset(); fixture.detectChanges(); expect(options[0].selected) .withContext('Expected first option to be deselected.') .toBe(false); expect(formControl.value).withContext('Expected value to be reset.').toBeFalsy(); dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); flush(); expect(options[0].selected) .withContext('Expected first option to be selected again.') .toBe(true); expect(formControl.value) .withContext('Expected value from first option to have been set on the model again.') .toBe(options[0].value); })); 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).withContext('Expected no initial value.').toBeFalsy(); const endEvent = dispatchKeyboardEvent(select, 'keydown', END); expect(endEvent.defaultPrevented).toBe(true); expect(lastOption.selected) .withContext('Expected last option to be selected.') .toBe(true); expect(formControl.value) .withContext('Expected value from last option to have been set on the model.') .toBe(lastOption.value); const homeEvent = dispatchKeyboardEvent(select, 'keydown', HOME); expect(homeEvent.defaultPrevented).toBe(true); expect(firstOption.selected) .withContext('Expected first option to be selected.') .toBe(true); expect(formControl.value) .withContext('Expected value from first option to have been set on the model.') .toBe(firstOption.value); flush(); })); it('should select first/last options via the PAGE_DOWN/PAGE_UP keys on a closed select with less than 10 options', fakeAsync(() => { const formControl = fixture.componentInstance.control; const firstOption = fixture.componentInstance.options.first; const lastOption = fixture.componentInstance.options.last; expect(formControl.value).withContext('Expected no initial value.').toBeFalsy(); expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(-1); const endEvent = dispatchKeyboardEvent(select, 'keydown', PAGE_DOWN); expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(7); expect(endEvent.defaultPrevented).toBe(true); expect(lastOption.selected) .withContext('Expected last option to be selected.') .toBe(true); expect(formControl.value) .withContext('Expected value from last option to have been set on the model.') .toBe(lastOption.value); const homeEvent = dispatchKeyboardEvent(select, 'keydown', PAGE_UP); expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0); expect(homeEvent.defaultPrevented).toBe(true); expect(firstOption.selected) .withContext('Expected first option to be selected.') .toBe(true); expect(formControl.value) .withContext('Expected value from first option to have been set on the model.') .toBe(firstOption.value); 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).withContext('Expected no initial value.').toBeFalsy(); 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).withContext('Expected no initial value.').toBeFalsy(); dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW); expect(options[0].selected) .withContext('Expected first option to be selected.') .toBe(true); expect(formControl.value) .withContext('Expected value from first option to have been set on the model.') .toBe(options[0].value); dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW); dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW); // Note that the third option is skipped, because it is disabled. // Note that the third option is skipped, because it is disabled. expect(options[3].selected) .withContext('Expected fourth option to be selected.') .toBe(true); expect(formControl.value) .withContext('Expected value from fourth option to have been set on the model.') .toBe(options[3].value); dispatchKeyboardEvent(select, 'keydown', LEFT_ARROW); expect(options[1].selected) .withContext('Expected second option to be selected.') .toBe(true); expect(formControl.value) .withContext('Expected value from second option to have been set on the model.') .toBe(options[1].value); flush(); })); it('should announce changes via the keyboard on a closed select', fakeAsync(() => { const liveAnnouncer = TestBed.inject(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.changeDetectorRef.markForCheck(); 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', () => { const {control: formControl, select: selectInstance} = fixture.componentInstance; expect(selectInstance.panelOpen).withContext('Expected select to be closed.').toBe(false); expect(formControl.value).withContext('Expected no initial value.').toBeFalsy(); const event = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {alt: true}); dispatchEvent(select, event); expect(selectInstance.panelOpen).withContext('Expected select to be open.').toBe(true); expect(formControl.value).withContext('Expected value not to have changed.').toBeFalsy(); }); it('should open a single-selection select using ALT + UP_ARROW', () => { const {control: formControl, select: selectInstance} = fixture.componentInstance; expect(selectInstance.panelOpen).withContext('Expected select to be closed.').toBe(false); expect(formControl.value).withContext('Expected no initial value.').toBeFalsy(); const event = createKeyboardEvent('keydown', UP_ARROW, undefined, {alt: true}); dispatchEvent(select, event); expect(selectInstance.panelOpen).withContext('Expected select to be open.').toBe(true); expect(formControl.value).withContext('Expected value not to have changed.').toBeFalsy(); }); it('should close when pressing ALT + DOWN_ARROW', () => { const {select: selectInstance} = fixture.componentInstance; selectInstance.open(); fixture.detectChanges(); expect(selectInstance.panelOpen).withContext('Expected select to be open.').toBe(true); const event = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {alt: true}); dispatchEvent(select, event); expect(selectInstance.panelOpen).withContext('Expected select to be closed.').toBe(false); expect(event.defaultPrevented) .withContext('Expected default action to be prevented.') .toBe(true); }); it('should close when pressing ALT + UP_ARROW', () => { const {select: selectInstance} = fixture.componentInstance; selectInstance.open(); fixture.detectChanges(); expect(selectInstance.panelOpen).withContext('Expected select to be open.').toBe(true); const event = createKeyboardEvent('keydown', UP_ARROW, undefined, {alt: true}); dispatchEvent(select, event); expect(selectInstance.panelOpen).withContext('Expected select to be closed.').toBe(false); expect(event.defaultPrevented) .withContext('Expected default action to be prevented.') .toBe(true); }); 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).withContext('Expected no initial value.').toBeFalsy(); dispatchEvent(select, createKeyboardEvent('keydown', 80, 'p')); tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL); expect(options[1].selected) .withContext('Expected second option to be selected.') .toBe(true); expect(formControl.value) .withContext('Expected value from second option to have been set on the model.') .toBe(options[1].value); dispatchEvent(select, createKeyboardEvent('keydown', 69, 'e')); tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL); expect(options[5].selected) .withContext('Expected sixth option to be selected.') .toBe(true); expect(formControl.value) .withContext('Expected value from sixth option to have been set on the model.') .toBe(options[5].value); })); it('should not open the select when pressing space while typing', fakeAsync(() => { const selectInstance = fixture.componentInstance.select; fixture.componentInstance.typeaheadDebounceInterval = DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(selectInstance.panelOpen) .withContext('Expected select to be closed on init.') .toBe(false); dispatchEvent(select, createKeyboardEvent('keydown', 80, 'p')); tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL / 2); fixture.detectChanges(); dispatchKeyboardEvent(select, 'keydown', SPACE); fixture.detectChanges(); expect(selectInstance.panelOpen) .withContext('Expected select to remain closed after space was pressed.') .toBe(false); tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL / 2); fixture.detectChanges(); expect(selectInstance.panelOpen) .withContext('Expected select to be closed when the timer runs out.') .toBe(false); })); 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.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(formControl.value).withContext('Expected no initial value.').toBeFalsy(); dispatchEvent(select, createKeyboardEvent('keydown', 80, 'p')); tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL); expect(formControl.value) .withContext('Expected no value after a bit of time has passed.') .toBeFalsy(); tick(1337); expect(options[1].selected) .withContext('Expected second option to be selected after all the time has passed.') .toBe(true); expect(formControl.value) .withContext('Expected value from second option to have been set on the model.') .toBe(options[1].value); })); it('should cancel the typeahead selection on blur', fakeAsync(() => { const formControl = fixture.componentInstance.control; const options = fixture.componentInstance.options.toArray(); expect(formControl.value).withContext('Expected no initial value.').toBeFalsy(); dispatchEvent(select, createKeyboardEvent('keydown', 80, 'p')); dispatchFakeEvent(select, 'blur'); tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL); expect(options.some(o => o.selected)) .withContext('Expected no options to be selected.') .toBe(false); expect(formControl.value).withContext('Expected no value to be assigned.').toBeFalsy(); })); it('should open the panel when pressing a vertical arrow key on a closed multiple select', () => { 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).withContext('Expected panel to be closed.').toBe(false); const event = dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); expect(instance.select.panelOpen).withContext('Expected panel to be open.').toBe(true); expect(instance.control.value) .withContext('Expected value to stay the same.') .toBe(initialValue); expect(event.defaultPrevented) .withContext('Expected default to be prevented.') .toBe(true); }); it('should open the panel when pressing a horizontal arrow key on closed multiple select', () => { 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).withContext('Expected panel to be closed.').toBe(false); const event = dispatchKeyboardEvent(select, 'keydown', RIGHT_ARROW); expect(instance.select.panelOpen).withContext('Expected panel to be open.').toBe(true); expect(instance.control.value) .withContext('Expected value to stay the same.') .toBe(initialValue); expect(event.defaultPrevented) .withContext('Expected default to be prevented.') .toBe(true); }); it('should do nothing when typing on a closed multi-select', () => { 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).withContext('Expected panel to be closed.').toBe(false); dispatchEvent(select, createKeyboardEvent('keydown', 80, 'p')); expect(instance.select.panelOpen) .withContext('Expected panel to stay closed.') .toBe(false); expect(instance.control.value) .withContext('Expected value to stay the same.') .toBe(initialValue); }); it('should do nothing if the key manager did not change the active item', () => { const formControl = fixture.componentInstance.control; expect(formControl.value) .withContext('Expected form control value to be empty.') .toBeNull(); expect(formControl.pristine).withContext('Expected form control to be clean.').toBe(true); dispatchKeyboardEvent(select, 'keydown', 16); // Press a random key. expect(formControl.value) .withContext('Expected form control value to stay empty.') .toBeNull(); expect(formControl.pristine) .withContext('Expected form control to stay clean.') .toBe(true); }); 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', () => { 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; select.focus(); multiFixture.detectChanges(); multiFixture.componentInstance.select._keyManager.setActiveItem(3); multiFixture.detectChanges(); expect(document.activeElement) .withContext('Expected select to have DOM focus.') .toBe(select); expect(select.getAttribute('aria-activedescendant')) .withContext('Expected fourth option to be activated.') .toBe(options[3].id); multiFixture.componentInstance.control.setValue(['steak-0', 'sushi-7']); multiFixture.detectChanges(); expect(document.activeElement) .withContext('Expected select to have DOM focus.') .toBe(select); expect(select.getAttribute('aria-activedescendant')) .withContext('Expected fourth optino to remain activated.') .toBe(options[3].id); }); it('should not cycle through the options if the control is disabled', () => { const formControl = fixture.componentInstance.control; formControl.setValue('eggs-5'); formControl.disable(); dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); expect(formControl.value) .withContext('Expected value to remain unchaged.') .toBe('eggs-5'); }); it('should not wrap selection after reaching the end of the options', () => { const lastOption = fixture.componentInstance.options.last; fixture.componentInstance.options.forEach(() => { dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); }); expect(lastOption.selected) .withContext('Expected last option to be selected.') .toBe(true); dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW); expect(lastOption.selected) .withContext('Expected last option to stay selected.') .toBe(true); }); it('should not open a multiple select when tabbing through', () => { fixture.destroy(); const multiFixture = TestBed.createComponent(MultiSelect); multiFixture.detectChanges(); select = multiFixture.debugElement.query(By.css('mat-select'))!.nativeElement; expect(multiFixture.componentInstance.select.panelOpen) .withContext('Expected panel to be closed initially.') .toBe(false); dispatchKeyboardEvent(select, 'keydown', TAB); expect(multiFixture.componentInstance.select.panelOpen) .withContext('Expected panel to stay closed.') .toBe(false); }); 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, undefined, {shift: 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, undefined, {shift: 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', () => { const event = dispatchKeyboardEvent(select, 'keydown', SPACE); expect(event.defaultPrevented).toBe(true); }); it('should prevent the default action when pressing enter', () => { 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', () => { [ENTER, SPACE].forEach(key => { const event = createKeyboardEvent('keydown', key, undefined, {shift: 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', () => { document.body.focus(); // ensure that focus isn't on the trigger already fixture.componentInstance.select.focus(); expect(document.activeElement) .withContext('Expected select element to be focused.') .toBe(select); }); it('should set `aria-multiselectable` to true on the listbox inside 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(); flush(); const panel = document.querySelector('.mat-mdc-select-panel')!; expect(panel.getAttribute('aria-multiselectable')).toBe('true'); })); it('should set aria-multiselectable false on single-selection instances', fakeAsync(() => { fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); const panel = document.querySelector('.mat-mdc-select-panel')!; expect(panel.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')) .withContext('Expected no aria-activedescendant on init.') .toBe(false); fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); const options = overlayContainerElement.querySelectorAll('mat-option'); expect(host.getAttribute('aria-activedescendant')) .withContext('Expected aria-activedescendant to match the active option.') .toBe(options[4].id); fixture.componentInstance.select.close(); fixture.detectChanges(); flush(); expect(host.hasAttribute('aria-activedescendant')) .withContext('Expected no aria-activedescendant when closed.') .toBe(false); })); 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[3].id); dispatchKeyboardEvent(host, 'keydown', UP_ARROW); fixture.detectChanges(); expect(host.getAttribute('aria-activedescendant')).toBe(options[2].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', () => { 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) .withContext('Expected trigger to be focused.') .toBe(select); }); it('should set a role of listbox on the select panel', fakeAsync(() => { fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); const panel = document.querySelector('.mat-mdc-select-panel')!; expect(panel.getAttribute('role')).toBe('listbox'); })); it('should point the aria-labelledby of the panel to the field label', fakeAsync(() => { fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); const labelId = fixture.nativeElement.querySelector('label').id; const panel = document.querySelector('.mat-mdc-select-panel')!; expect(panel.getAttribute('aria-labelledby')).toBe(labelId); })); it('should add a custom aria-labelledby to the panel', fakeAsync(() => { fixture.componentInstance.ariaLabelledby = 'myLabelId'; fixture.changeDetectorRef.markForCheck(); fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); const labelId = fixture.nativeElement.querySelector('label').id; const panel = document.querySelector('.mat-mdc-select-panel')!; expect(panel.getAttribute('aria-labelledby')).toBe(`${labelId} myLabelId`); })); it('should trim the custom panel aria-labelledby when there is no label', fakeAsync(() => { fixture.componentInstance.hasLabel = false; fixture.componentInstance.ariaLabelledby = 'myLabelId'; fixture.changeDetectorRef.markForCheck(); fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); // Note that we assert that there are no spaces around the value. const panel = document.querySelector('.mat-mdc-select-panel')!; expect(panel.getAttribute('aria-labelledby')).toBe(`myLabelId`); })); it('should clear aria-labelledby from the panel if an aria-label is set', fakeAsync(() => { fixture.componentInstance.ariaLabel = 'My label'; fixture.changeDetectorRef.markForCheck(); fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); const panel = document.querySelector('.mat-mdc-select-panel')!; expect(panel.getAttribute('aria-label')).toBe('My label'); expect(panel.hasAttribute('aria-labelledby')).toBe(false); })); }); describe('for select inside a modal', () => { let fixture: ComponentFixture; beforeEach(() => { fixture = TestBed.createComponent(SelectInsideAModal); fixture.detectChanges(); }); it('should add the id of the select panel to the aria-owns of the modal', () => { fixture.componentInstance.select.open(); fixture.detectChanges(); const panelId = `${fixture.componentInstance.select.id}-panel`; const modalElement = fixture.componentInstance.modal.nativeElement; expect(modalElement.getAttribute('aria-owns')?.split(' ')) .withContext('expecting modal to own the select panel') .toContain(panelId); }); }); describe('for options', () => { let fixture: ComponentFixture; let trigger: HTMLElement; let options: HTMLElement[]; beforeEach(() => { fixture = TestBed.createComponent(BasicSelect); fixture.detectChanges(); trigger = fixture.debugElement.query(By.css('.mat-mdc-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.getAttribute('aria-selected') === 'false')) .withContext( 'Expected all unselected single-select options to have ' + 'aria-selected="false".', ) .toBe(true); options[1].click(); fixture.detectChanges(); trigger.click(); fixture.detectChanges(); flush(); expect(options[1].getAttribute('aria-selected')) .withContext( 'Expected selected single-select option to have ' + 'aria-selected="true".', ) .toEqual('true'); options.splice(1, 1); expect(options.every(option => option.getAttribute('aria-selected') === 'false')) .withContext( 'Expected all unselected single-select options to have ' + 'aria-selected="false".', ) .toBe(true); })); 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-mdc-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', ), ) .withContext( 'Expected all unselected multi-select options to have ' + 'aria-selected="false".', ) .toBe(true); options[1].click(); multiFixture.detectChanges(); trigger.click(); multiFixture.detectChanges(); flush(); expect(options[1].getAttribute('aria-selected')) .withContext('Expected selected multi-select option to have aria-selected="true".') .toEqual('true'); options.splice(1, 1); expect( options.every( option => option.hasAttribute('aria-selected') && option.getAttribute('aria-selected') === 'false', ), ) .withContext( 'Expected all unselected multi-select options to have ' + 'aria-selected="false".', ) .toBe(true); })); it('should omit the tabindex attribute on each option', fakeAsync(() => { expect(options[0].hasAttribute('tabindex')).toBeFalse(); expect(options[1].hasAttribute('tabindex')).toBeFalse(); expect(options[2].hasAttribute('tabindex')).toBeFalse(); })); 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.changeDetectorRef.markForCheck(); 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'); })); it('should remove the active state from options that have been deselected while closed', fakeAsync(() => { let activeOptions = options.filter(option => { return option.classList.contains('mat-mdc-option-active'); }); expect(activeOptions) .withContext('Expected first option to have active styles.') .toEqual([options[0]]); options[1].click(); fixture.detectChanges(); fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); activeOptions = options.filter(option => { return option.classList.contains('mat-mdc-option-active'); }); expect(activeOptions) .withContext( 'Expected only selected option to be marked as active after it is ' + 'clicked.', ) .toEqual([options[1]]); fixture.componentInstance.control.setValue(fixture.componentInstance.foods[7].value); fixture.detectChanges(); fixture.componentInstance.select.close(); fixture.detectChanges(); flush(); fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); activeOptions = options.filter(option => { return option.classList.contains('mat-mdc-option-active'); }); expect(activeOptions) .withContext( 'Expected only selected option to be marked as active after the ' + 'value has changed.', ) .toEqual([options[7]]); })); it('should render a checkmark on selected option', fakeAsync(() => { fixture.componentInstance.control.setValue(fixture.componentInstance.foods[2].value); fixture.detectChanges(); trigger.click(); fixture.detectChanges(); flush(); const pseudoCheckboxes = options .map(option => option.querySelector('.mat-pseudo-checkbox-minimal')) .filter((x): x is HTMLElement => !!x); const selectedOption = options[2]; expect(selectedOption.querySelector('.mat-pseudo-checkbox-minimal')).not.toBeNull(); expect(pseudoCheckboxes.length).toBe(1); })); it('should render checkboxes for multi-select', fakeAsync(() => { fixture.destroy(); const multiFixture = TestBed.createComponent(MultiSelect); multiFixture.detectChanges(); multiFixture.componentInstance.control.setValue([ multiFixture.componentInstance.foods[2].value, ]); multiFixture.detectChanges(); trigger = multiFixture.debugElement.query( By.css('.mat-mdc-select-trigger'), )!.nativeElement; trigger.click(); multiFixture.detectChanges(); flush(); options = Array.from(overlayContainerElement.querySelectorAll('mat-option')); const pseudoCheckboxes = options .map(option => option.querySelector('.mat-pseudo-checkbox.mat-pseudo-checkbox-full')) .filter((x): x is HTMLElement => !!x); const selectedPseudoCheckbox = pseudoCheckboxes[2]; expect(pseudoCheckboxes.length) .withContext('expecting each option to have a pseudo-checkbox with "full" appearance') .toEqual(options.length); expect(selectedPseudoCheckbox.classList) .withContext('expecting selected pseudo-checkbox to be checked') .toContain('mat-pseudo-checkbox-checked'); })); }); describe('for option groups', () => { let fixture: ComponentFixture; let trigger: HTMLElement; let groups: NodeListOf; beforeEach(() => { fixture = TestBed.createComponent(SelectWithGroups); fixture.detectChanges(); trigger = fixture.debugElement.query(By.css('.mat-mdc-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('.mat-mdc-optgroup-label') as HTMLElement; expect(label.getAttribute('id')) .withContext('Expected label to have an id.') .toBeTruthy(); expect(group.getAttribute('aria-labelledby')) .withContext('Expected `aria-labelledby` to match the label id.') .toBe(label.getAttribute('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 formField: HTMLElement; let trigger: HTMLElement; beforeEach(() => { fixture = TestBed.createComponent(BasicSelect); fixture.detectChanges(); formField = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement; trigger = formField.querySelector('.mat-mdc-select-trigger') as HTMLElement; }); 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(() => { formField.style.width = '200px'; trigger.click(); fixture.detectChanges(); flush(); const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; expect(pane.style.width).toBe('200px'); })); it('should update the width of the panel on resize', fakeAsync(() => { formField.style.width = '300px'; trigger.click(); fixture.detectChanges(); flush(); const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; const initialWidth = parseInt(pane.style.width || '0'); expect(initialWidth).toBeGreaterThan(0); formField.style.width = '400px'; dispatchFakeEvent(window, 'resize'); fixture.detectChanges(); tick(1000); fixture.detectChanges(); expect(parseInt(pane.style.width || '0')).toBeGreaterThan(initialWidth); })); it('should be able to set a custom width on the select panel', fakeAsync(() => { fixture.componentInstance.panelWidth = '42px'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); trigger.click(); fixture.detectChanges(); flush(); const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; expect(pane.style.width).toBe('42px'); })); it('should not set a width on the panel if panelWidth is null', fakeAsync(() => { fixture.componentInstance.panelWidth = null; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); trigger.click(); fixture.detectChanges(); flush(); const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; expect(pane.style.width).toBeFalsy(); })); it('should not set a width on the panel if panelWidth is an empty string', fakeAsync(() => { fixture.componentInstance.panelWidth = ''; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); trigger.click(); fixture.detectChanges(); flush(); const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement; expect(pane.style.width).toBeFalsy(); })); it('should not attempt to open a select that does not have any options', () => { fixture.componentInstance.foods = []; fixture.changeDetectorRef.markForCheck(); 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-mdc-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-mdc-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 focus the last option when pressing PAGE_DOWN with less than 10 options', fakeAsync(() => { fixture.componentInstance.control.setValue('pizza-1'); fixture.detectChanges(); trigger.click(); fixture.detectChanges(); flush(); const event = dispatchKeyboardEvent(trigger, 'keydown', PAGE_DOWN); fixture.detectChanges(); expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(7); expect(event.defaultPrevented).toBe(true); })); it('should focus the first option when pressing PAGE_UP with index < 10', fakeAsync(() => { fixture.componentInstance.control.setValue('pizza-1'); fixture.detectChanges(); trigger.click(); fixture.detectChanges(); flush(); expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBeLessThan(10); const event = dispatchKeyboardEvent(trigger, 'keydown', PAGE_UP); fixture.detectChanges(); expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0); expect(event.defaultPrevented).toBe(true); })); it('should be able to set extra classes on the panel', () => { trigger.click(); fixture.detectChanges(); const panel = overlayContainerElement.querySelector('.mat-mdc-select-panel') as HTMLElement; expect(panel.classList).toContain('custom-one'); expect(panel.classList).toContain('custom-two'); }); it('should update disableRipple properly on each option', () => { const options = fixture.componentInstance.options.toArray(); expect(options.every(option => option.disableRipple === false)) .withContext('Expected all options to have disableRipple set to false initially.') .toBeTruthy(); fixture.componentInstance.disableRipple = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(options.every(option => option.disableRipple === true)) .withContext('Expected all options to have disableRipple set to true.') .toBeTruthy(); }); it('should not show ripples if they were disabled', fakeAsync(() => { fixture.componentInstance.disableRipple = true; fixture.changeDetectorRef.markForCheck(); 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', () => { fixture.destroy(); const groupFixture = TestBed.createComponent(SelectWithGroupsAndNgContainer); groupFixture.detectChanges(); trigger = groupFixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement; trigger.click(); groupFixture.detectChanges(); expect(document.querySelectorAll('.cdk-overlay-container mat-option').length) .withContext('Expected at least one option to be rendered.') .toBeGreaterThan(0); }); 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-mdc-select'); const selectInstance = fixture.componentInstance.select; dispatchFakeEvent(selectElement, 'focus'); fixture.detectChanges(); expect(selectInstance.focused).withContext('Expected select to be focused.').toBe(true); selectInstance.open(); fixture.detectChanges(); flush(); dispatchFakeEvent(selectElement, 'blur'); fixture.detectChanges(); expect(selectInstance.focused) .withContext('Expected select element to remain focused.') .toBe(true); })); }); describe('selection logic', () => { let fixture: ComponentFixture; let trigger: HTMLElement; let formField: HTMLElement; let label: HTMLLabelElement; beforeEach(() => { fixture = TestBed.createComponent(BasicSelect); fixture.detectChanges(); trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement; formField = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement; label = formField.querySelector('label')!; }); it('should not float label if no option is selected', () => { expect(label.classList.contains('mat-form-field-should-float')) .withContext('Label should not be floating') .toBe(false); }); 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('mdc-list-item--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('mdc-list-item--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('mdc-list-item--selected'); expect(options[2].classList).not.toContain('mdc-list-item--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( 'mdc-list-item--selected', 'Expected first option to no longer be selected', ); expect(options[1].classList) .withContext('Expected second option to be selected') .toContain('mdc-list-item--selected'); const optionInstances = fixture.componentInstance.options.toArray(); expect(optionInstances[0].selected) .withContext('Expected first option to no longer be selected') .toBe(false); expect(optionInstances[1].selected) .withContext('Expected second option to be selected') .toBe(true); })); 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) .withContext('Expected first option to be selected.') .toBe(select.options.first); fixture.componentInstance.foods = []; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); flush(); expect(select.selected) .withContext('Expected selection to be removed when option no longer exists.') .toBeUndefined(); })); 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-mdc-select-value'))!.nativeElement; expect(label.classList.contains('mdc-floating-label--float-above')) .withContext('Label should be floating') .toBe(true); 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.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(trigger.textContent!.trim()).toBe('Calzone'); })); it('should update the trigger value if the text as a result of an expression change', fakeAsync(() => { fixture.componentInstance.control.setValue('pizza-1'); fixture.detectChanges(); expect(trigger.textContent!.trim()).toBe('Pizza'); fixture.componentInstance.capitalize = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.checkNoChanges(); expect(trigger.textContent!.trim()).toBe('PIZZA'); })); it('should not select disabled options', () => { 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('mdc-list-item--selected'); expect(fixture.componentInstance.select.selected).toBeUndefined(); }); it('should not select options inside a disabled group', () => { fixture.destroy(); const groupFixture = TestBed.createComponent(SelectWithGroups); groupFixture.detectChanges(); groupFixture.debugElement.query(By.css('.mat-mdc-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('mdc-list-item--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-mdc-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.changeDetectorRef.markForCheck(); fixture.detectChanges(); selectFirstOption(); expect(spy).toHaveBeenCalledTimes(1); fixture.componentInstance.foods = [{value: 'fruit-9', viewValue: 'Fruit'}]; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); selectFirstOption(); expect(spy).toHaveBeenCalledTimes(2); subscription!.unsubscribe(); })); it('should not indicate programmatic value changes as user interactions', () => { const events: MatOptionSelectionChange[] = []; const subscription = fixture.componentInstance.select.optionSelectionChanges.subscribe( (event: MatOptionSelectionChange) => events.push(event), ); fixture.componentInstance.control.setValue('eggs-5'); fixture.detectChanges(); expect(events.map(event => event.isUserInput)).toEqual([false]); subscription.unsubscribe(); }); }); describe('forms integration', () => { let fixture: ComponentFixture; let trigger: HTMLElement; beforeEach(() => { fixture = TestBed.createComponent(BasicSelect); fixture.detectChanges(); trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement; }); it('should take an initial view value with reactive forms', fakeAsync(() => { fixture.componentInstance.control = new FormControl('pizza-1'); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); const value = fixture.debugElement.query(By.css('.mat-mdc-select-value'))!; expect(value.nativeElement.textContent) .withContext(`Expected trigger to be populated by the control's initial value.`) .toContain('Pizza'); trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement; trigger.click(); fixture.detectChanges(); flush(); const options = overlayContainerElement.querySelectorAll( 'mat-option', ) as NodeListOf; expect(options[1].classList) .withContext(`Expected option with the control's initial value to be selected.`) .toContain('mdc-list-item--selected'); })); it('should set the view value from the form', fakeAsync(() => { let value = fixture.debugElement.query(By.css('.mat-mdc-select-value'))!; expect(value.nativeElement.textContent.trim()).toBe('Food'); fixture.componentInstance.control.setValue('pizza-1'); fixture.detectChanges(); value = fixture.debugElement.query(By.css('.mat-mdc-select-value'))!; expect(value.nativeElement.textContent) .withContext(`Expected trigger to be populated by the control's new value.`) .toContain('Pizza'); trigger.click(); fixture.detectChanges(); flush(); const options = overlayContainerElement.querySelectorAll( 'mat-option', ) as NodeListOf; expect(options[1].classList) .withContext(`Expected option with the control's new value to be selected.`) .toContain('mdc-list-item--selected'); })); it('should update the form value when the view changes', fakeAsync(() => { expect(fixture.componentInstance.control.value) .withContext(`Expected the control's value to be empty initially.`) .toEqual(null); trigger.click(); fixture.detectChanges(); flush(); const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; option.click(); fixture.detectChanges(); flush(); expect(fixture.componentInstance.control.value) .withContext(`Expected control's value to be set to the new option.`) .toEqual('steak-0'); })); 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-mdc-select-value'))!; expect(value.nativeElement.textContent.trim()) .withContext(`Expected trigger to show the placeholder.`) .toBe('Food'); 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( 'mdc-list-item--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-mdc-select-value'))!; expect(value.nativeElement.textContent.trim()) .withContext(`Expected trigger to show the placeholder.`) .toBe('Food'); 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( 'mdc-list-item--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) .withContext(`Expected the control to start off as untouched.`) .toEqual(false); trigger.click(); dispatchFakeEvent(trigger, 'blur'); fixture.detectChanges(); flush(); expect(fixture.componentInstance.control.touched) .withContext(`Expected the control to stay untouched when menu opened.`) .toEqual(false); const backdrop = overlayContainerElement.querySelector( '.cdk-overlay-backdrop', ) as HTMLElement; backdrop.click(); dispatchFakeEvent(trigger, 'blur'); fixture.detectChanges(); flush(); expect(fixture.componentInstance.control.touched) .withContext(`Expected the control to be touched as soon as focus left the select.`) .toEqual(true); })); it('should set the control to touched when the panel is closed', fakeAsync(() => { expect(fixture.componentInstance.control.touched) .withContext('Expected the control to start off as untouched.') .toBe(false); trigger.click(); dispatchFakeEvent(trigger, 'blur'); fixture.detectChanges(); flush(); expect(fixture.componentInstance.control.touched) .withContext('Expected the control to stay untouched when menu opened.') .toBe(false); fixture.componentInstance.select.close(); fixture.detectChanges(); flush(); expect(fixture.componentInstance.control.touched) .withContext('Expected the control to be touched when the panel was closed.') .toBe(true); })); it('should not set touched when a disabled select is touched', () => { expect(fixture.componentInstance.control.touched) .withContext('Expected the control to start off as untouched.') .toBe(false); fixture.componentInstance.control.disable(); dispatchFakeEvent(trigger, 'blur'); expect(fixture.componentInstance.control.touched) .withContext('Expected the control to stay untouched.') .toBe(false); }); it('should set the control to dirty when the select value changes in DOM', fakeAsync(() => { expect(fixture.componentInstance.control.dirty) .withContext(`Expected control to start out pristine.`) .toEqual(false); trigger.click(); fixture.detectChanges(); flush(); const option = overlayContainerElement.querySelector('mat-option') as HTMLElement; option.click(); fixture.detectChanges(); flush(); expect(fixture.componentInstance.control.dirty) .withContext(`Expected control to be dirty after value was changed by user.`) .toEqual(true); })); it('should not set the control to dirty when the value changes programmatically', () => { expect(fixture.componentInstance.control.dirty) .withContext(`Expected control to start out pristine.`) .toEqual(false); fixture.componentInstance.control.setValue('pizza-1'); expect(fixture.componentInstance.control.dirty) .withContext(`Expected control to stay pristine after programmatic change.`) .toEqual(false); }); it('should set an asterisk after the label if control is required', fakeAsync(() => { const label = fixture.nativeElement.querySelector('.mat-mdc-form-field label'); expect(label.querySelector('.mat-mdc-form-field-required-marker')) .withContext(`Expected label not to have an asterisk, as control was not required.`) .toBeFalsy(); fixture.componentInstance.isRequired = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(label.querySelector('.mat-mdc-form-field-required-marker')) .withContext(`Expected label to have an asterisk, as control was required.`) .toBeTruthy(); })); it('should propagate the value set through the `value` property to the form field', fakeAsync(() => { const control = fixture.componentInstance.control; expect(control.value).toBeFalsy(); fixture.componentInstance.select.value = 'pizza-1'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(control.value).toBe('pizza-1'); })); }); 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-mdc-select-trigger'))!.nativeElement; expect(getComputedStyle(trigger).getPropertyValue('cursor')) .withContext(`Expected cursor to be default arrow on disabled control.`) .toEqual('default'); trigger.click(); fixture.detectChanges(); flush(); expect(overlayContainerElement.textContent) .withContext(`Expected select panel to stay closed.`) .toEqual(''); expect(fixture.componentInstance.select.panelOpen) .withContext(`Expected select panelOpen property to stay false.`) .toBe(false); fixture.componentInstance.control.enable(); fixture.detectChanges(); expect(getComputedStyle(trigger).getPropertyValue('cursor')) .withContext(`Expected cursor to be a pointer on enabled control.`) .toEqual('pointer'); trigger.click(); fixture.detectChanges(); flush(); expect(overlayContainerElement.textContent) .withContext(`Expected select panel to open normally on re-enabled control`) .toContain('Steak'); expect(fixture.componentInstance.select.panelOpen) .withContext(`Expected select panelOpen property to become true.`) .toBe(true); })); it('should keep the disabled state in sync if the form group is swapped and disabled at the same time', () => { const fixture = TestBed.createComponent(SelectInsideDynamicFormGroup); fixture.detectChanges(); const instance = fixture.componentInstance; expect(instance.select.disabled).toBe(false); instance.assignGroup(true); fixture.detectChanges(); expect(instance.select.disabled).toBe(true); }); }); 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.changeDetectorRef.markForCheck(); fixture.detectChanges(); fixture.componentInstance.select.open(); fixture.detectChanges(); flush(); fixture.detectChanges(); host = fixture.debugElement.query(By.css('mat-select'))!.nativeElement; panel = overlayContainerElement.querySelector('.mat-mdc-select-panel')! as HTMLElement; })); it('should not scroll to options that are completely in the view', () => { const initialScrollPosition = panel.scrollTop; [1, 2, 3].forEach(() => { dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW); }); expect(panel.scrollTop) .withContext('Expected scroll position not to change') .toBe(initialScrollPosition); }); it('should scroll down to the active option', () => { for (let i = 0; i < 15; i++) { dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW); } // +