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<string>};
  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<BasicSelect>;
        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<HTMLElement>;

          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<SelectInsideAModal>;

        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<BasicSelect>;
        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<SelectWithGroups>;
        let trigger: HTMLElement;
        let groups: NodeListOf<HTMLElement>;

        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<HTMLElement>;
        });

        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<BasicSelect>;
      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<BasicSelect>;
      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<HTMLElement> =
          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<HTMLElement>;

        options[0].click();
        fixture.detectChanges();
        flush();

        trigger.click();
        fixture.detectChanges();
        flush();

        options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
        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<HTMLElement>;

        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<HTMLElement>;

        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<HTMLElement>;
        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<HTMLElement>;
        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<BasicSelect>;
      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<HTMLElement>;
        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<HTMLElement>;
        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<HTMLElement>;
        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<HTMLElement>;
        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<BasicSelect>;
      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);
        }

        // <top padding> + <option index * height> - <panel height> = 8 + 16 * 48 - 275 = 501
        expect(panel.scrollTop).withContext('Expected scroll to be at the 16th option.').toBe(501);
      });

      it('should scroll up to the active option', () => {
        // Scroll to the bottom.
        for (let i = 0; i < fixture.componentInstance.foods.length; i++) {
          dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW);
        }

        for (let i = 0; i < 20; i++) {
          dispatchKeyboardEvent(host, 'keydown', UP_ARROW);
        }

        // <top padding> + <option index * height> = 8 + 9 * 48 = 440
        expect(panel.scrollTop).withContext('Expected scroll to be at the 9th option.').toBe(440);
      });

      it('should skip option group labels', fakeAsync(() => {
        fixture.destroy();

        const groupFixture = TestBed.createComponent(SelectWithGroups);

        groupFixture.detectChanges();
        groupFixture.componentInstance.select.open();
        groupFixture.detectChanges();
        flush();

        host = groupFixture.debugElement.query(By.css('mat-select'))!.nativeElement;
        panel = overlayContainerElement.querySelector('.mat-mdc-select-panel')! as HTMLElement;

        for (let i = 0; i < 8; i++) {
          dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW);
          flush();
        }

        // <top padding> + <(option index + group labels) * height> - <panel height> =
        //    8 + (8 + 3) * 48 - 275 = 309
        expect(panel.scrollTop).withContext('Expected scroll to be at the 9th option.').toBe(309);
      }));

      it('should scroll to the top when pressing HOME', () => {
        for (let i = 0; i < 20; i++) {
          dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW);
          fixture.detectChanges();
        }

        expect(panel.scrollTop)
          .withContext('Expected panel to be scrolled down.')
          .toBeGreaterThan(0);

        dispatchKeyboardEvent(host, 'keydown', HOME);
        fixture.detectChanges();

        // 8px is the top padding of the panel.
        expect(panel.scrollTop).withContext('Expected panel to be scrolled to the top').toBe(8);
      });

      it('should scroll to the bottom of the panel when pressing END', () => {
        dispatchKeyboardEvent(host, 'keydown', END);
        fixture.detectChanges();

        // <top padding> + <option amount> * <option height> - <panel height> =
        //    8 + 30 * 48 - 275 = 1173
        expect(panel.scrollTop)
          .withContext('Expected panel to be scrolled to the bottom')
          .toBe(1173);
      });

      it('should scroll 10 to the top or to first element when pressing PAGE_UP', () => {
        for (let i = 0; i < 18; i++) {
          dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW);
          fixture.detectChanges();
        }

        expect(panel.scrollTop)
          .withContext('Expected panel to be scrolled down.')
          .toBeGreaterThan(0);

        expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(18);

        dispatchKeyboardEvent(host, 'keydown', PAGE_UP);
        fixture.detectChanges();

        //  <top padding> + <option amount> * <option height>
        // 8 + 8 × 48
        expect(panel.scrollTop).withContext('Expected panel to be scrolled to the top').toBe(392);
        expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(8);

        dispatchKeyboardEvent(host, 'keydown', PAGE_UP);
        fixture.detectChanges();

        // 8px is the top padding of the panel.
        expect(panel.scrollTop).withContext('Expected panel to be scrolled to the top').toBe(8);
        expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0);
      });

      it('should scroll 10 to the bottom of the panel when pressing PAGE_DOWN', () => {
        dispatchKeyboardEvent(host, 'keydown', PAGE_DOWN);
        fixture.detectChanges();

        // <top padding> + <option amount> * <option height> - <panel height> =
        //    8 + 11 * 48 - 275 = 261
        expect(panel.scrollTop)
          .withContext('Expected panel to be scrolled 10 to the bottom')
          .toBe(261);
        expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(10);

        dispatchKeyboardEvent(host, 'keydown', PAGE_DOWN);
        fixture.detectChanges();

        // <top padding> + <option amount> * <option height> - <panel height> =
        //    8 + 21 * 48 - 275 = 741
        expect(panel.scrollTop)
          .withContext('Expected panel to be scrolled 10 to the bottom')
          .toBe(741);
        expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(20);

        dispatchKeyboardEvent(host, 'keydown', PAGE_DOWN);
        fixture.detectChanges();

        // <top padding> + <option amount> * <option height> - <panel height> =
        //    8 + 30 * 48 - 275 = 1173
        expect(panel.scrollTop)
          .withContext('Expected panel to be scrolled 10 to the bottom')
          .toBe(1173);
        expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(29);
      });

      it('should scroll to the active option when typing', fakeAsync(() => {
        for (let i = 0; i < 15; i++) {
          // Press the letter 'o' 15 times since all the options are named 'Option <index>'
          dispatchEvent(host, createKeyboardEvent('keydown', 79, 'o'));
          fixture.detectChanges();
          tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL);
        }
        flush();

        // <top padding> + <option index * height> - <panel height> = 8 + 16 * 48 - 275 = 501
        expect(panel.scrollTop).withContext('Expected scroll to be at the 16th option.').toBe(501);
      }));

      it('should scroll to top when going to first option in top group', fakeAsync(() => {
        fixture.destroy();
        const groupFixture = TestBed.createComponent(SelectWithGroups);
        groupFixture.detectChanges();
        groupFixture.componentInstance.select.open();
        groupFixture.detectChanges();
        flush();

        host = groupFixture.debugElement.query(By.css('mat-select'))!.nativeElement;
        panel = overlayContainerElement.querySelector('.mat-mdc-select-panel')! as HTMLElement;

        for (let i = 0; i < 5; i++) {
          dispatchKeyboardEvent(host, 'keydown', DOWN_ARROW);
        }

        expect(panel.scrollTop).toBeGreaterThan(0);

        for (let i = 0; i < 5; i++) {
          dispatchKeyboardEvent(host, 'keydown', UP_ARROW);
          flush();
        }

        expect(panel.scrollTop).toBe(0);
      }));
    });
  });

  describe('when initialized without options', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectInitWithoutOptions])));

    it('should select the proper option when option list is initialized later', fakeAsync(() => {
      const fixture = TestBed.createComponent(SelectInitWithoutOptions);
      const instance = fixture.componentInstance;

      fixture.detectChanges();
      flush();

      // Wait for the initial writeValue promise.
      expect(instance.select.selected).toBeFalsy();

      instance.addOptions();
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      flush();

      // Wait for the next writeValue promise.
      expect(instance.select.selected).toBe(instance.options.toArray()[1]);
    }));
  });

  describe('with a selectionChange event handler', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectWithChangeEvent])));

    let fixture: ComponentFixture<SelectWithChangeEvent>;
    let trigger: HTMLElement;

    beforeEach(() => {
      fixture = TestBed.createComponent(SelectWithChangeEvent);
      fixture.detectChanges();

      trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
    });

    it('should emit an event when the selected option has changed', () => {
      trigger.click();
      fixture.detectChanges();

      (overlayContainerElement.querySelector('mat-option') as HTMLElement).click();

      expect(fixture.componentInstance.changeListener).toHaveBeenCalled();
    });

    it('should not emit multiple change events for the same option', () => {
      trigger.click();
      fixture.detectChanges();

      const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;

      option.click();
      option.click();

      expect(fixture.componentInstance.changeListener).toHaveBeenCalledTimes(1);
    });

    it('should only emit one event when pressing arrow keys on closed select', fakeAsync(() => {
      const select = fixture.debugElement.query(By.css('mat-select'))!.nativeElement;
      dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);

      expect(fixture.componentInstance.changeListener).toHaveBeenCalledTimes(1);

      flush();
    }));
  });

  describe('with ngModel', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([NgModelSelect])));

    it('should disable itself when control is disabled using the property', fakeAsync(() => {
      const fixture = TestBed.createComponent(NgModelSelect);
      fixture.detectChanges();

      fixture.componentInstance.isDisabled = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      flush();

      fixture.detectChanges();
      const 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.isDisabled = false;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      flush();

      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);
    }));
  });

  describe('with ngIf', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([NgIfSelect])));

    it('should handle nesting in an ngIf', fakeAsync(() => {
      const fixture = TestBed.createComponent(NgIfSelect);
      fixture.detectChanges();

      fixture.componentInstance.isShowing = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      const formField = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
      const trigger = formField.querySelector('.mat-mdc-select-trigger');
      formField.style.width = '300px';

      trigger.click();
      flush();
      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');

      const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
      expect(pane.style.width).toEqual('300px');

      expect(fixture.componentInstance.select.panelOpen).toBe(true);
      expect(overlayContainerElement.textContent).toContain('Steak');
      expect(overlayContainerElement.textContent).toContain('Pizza');
      expect(overlayContainerElement.textContent).toContain('Tacos');
    }));
  });

  describe('with multiple mat-select elements in one view', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([ManySelects])));

    let fixture: ComponentFixture<ManySelects>;
    let triggers: DebugElement[];
    let options: NodeListOf<HTMLElement>;

    beforeEach(fakeAsync(() => {
      fixture = TestBed.createComponent(ManySelects);
      fixture.detectChanges();
      triggers = fixture.debugElement.queryAll(By.css('.mat-mdc-select-trigger'));

      triggers[0].nativeElement.click();
      fixture.detectChanges();
      flush();

      options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
    }));

    it('should set the option id', fakeAsync(() => {
      let firstOptionID = options[0].id;

      expect(options[0].id)
        .withContext(`Expected option ID to have the correct prefix.`)
        .toContain('mat-option');
      expect(options[0].id).not.toEqual(options[1].id, `Expected option IDs to be unique.`);

      const backdrop = overlayContainerElement.querySelector(
        '.cdk-overlay-backdrop',
      ) as HTMLElement;
      backdrop.click();
      fixture.detectChanges();
      flush();

      triggers[1].nativeElement.click();
      fixture.detectChanges();
      flush();

      options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
      expect(options[0].id)
        .withContext(`Expected option ID to have the correct prefix.`)
        .toContain('mat-option');
      expect(options[0].id).not.toEqual(firstOptionID, `Expected option IDs to be unique.`);
      expect(options[0].id).not.toEqual(options[1].id, `Expected option IDs to be unique.`);
    }));
  });

  describe('with floatLabel', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([FloatLabelSelect])));

    it('should be able to always float the label', () => {
      const fixture = TestBed.createComponent(FloatLabelSelect);
      fixture.detectChanges();
      const label = fixture.nativeElement.querySelector('.mat-mdc-form-field label');

      expect(fixture.componentInstance.control.value).toBeFalsy();

      fixture.componentInstance.floatLabel = 'always';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(label.classList.contains('mdc-floating-label--float-above'))
        .withContext('Label should be floating')
        .toBe(true);
    });

    it('should default to global floating label type', () => {
      TestBed.resetTestingModule();
      TestBed.configureTestingModule({
        imports: [
          MatFormFieldModule,
          MatSelectModule,
          ReactiveFormsModule,
          FormsModule,
          NoopAnimationsModule,
        ],
        providers: [{provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {floatLabel: 'always'}}],
        declarations: [FloatLabelSelect],
      });

      const fixture = TestBed.createComponent(FloatLabelSelect);
      fixture.componentInstance.floatLabel = null;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      const label = fixture.nativeElement.querySelector('.mat-mdc-form-field label');

      expect(label.classList.contains('mdc-floating-label--float-above'))
        .withContext('Label should be floating')
        .toBe(true);
    });

    it('should float the label on focus if it has a placeholder', () => {
      const fixture = TestBed.createComponent(FloatLabelSelect);
      fixture.detectChanges();
      expect(fixture.componentInstance.placeholder).toBeTruthy();

      fixture.componentInstance.floatLabel = 'auto';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      dispatchFakeEvent(fixture.nativeElement.querySelector('.mat-mdc-select'), 'focus');
      fixture.detectChanges();

      const label = fixture.nativeElement.querySelector('.mat-mdc-form-field label');
      expect(label.classList.contains('mdc-floating-label--float-above'))
        .withContext('Label should be floating')
        .toBe(true);
    });
  });

  describe('with a sibling component that throws an error', () => {
    beforeEach(waitForAsync(() =>
      configureMatSelectTestingModule([SelectWithErrorSibling, ThrowsErrorOnInit])));

    it('should not crash the browser when a sibling throws an error on init', () => {
      // Note that this test can be considered successful if the error being thrown didn't
      // end up crashing the testing setup altogether.
      expect(() => {
        TestBed.createComponent(SelectWithErrorSibling).detectChanges();
      }).toThrowError(new RegExp('Oh no!', 'g'));
    });
  });

  describe('with tabindex', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectWithPlainTabindex])));

    it('should be able to set the tabindex via the native attribute', () => {
      const fixture = TestBed.createComponent(SelectWithPlainTabindex);
      fixture.detectChanges();

      const select = fixture.debugElement.query(By.css('mat-select'))!.nativeElement;
      expect(select.getAttribute('tabindex')).toBe('5');
    });
  });

  describe('change events', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectWithPlainTabindex])));

    it('should complete the stateChanges stream on destroy', () => {
      const fixture = TestBed.createComponent(SelectWithPlainTabindex);
      fixture.detectChanges();

      const debugElement = fixture.debugElement.query(By.directive(MatSelect))!;
      const select = debugElement.componentInstance;

      const spy = jasmine.createSpy('stateChanges complete');
      const subscription = select.stateChanges.subscribe(undefined, undefined, spy);

      fixture.destroy();
      expect(spy).toHaveBeenCalled();
      subscription.unsubscribe();
    });
  });

  describe('when initially hidden', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([BasicSelectInitiallyHidden])));

    it('should set the width of the overlay if the element was hidden initially', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicSelectInitiallyHidden);
      fixture.detectChanges();

      const formField = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
      const trigger = formField.querySelector('.mat-mdc-select-trigger');
      formField.style.width = '300px';
      fixture.componentInstance.isVisible = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      trigger.click();
      fixture.detectChanges();
      flush();

      const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
      expect(pane.style.width).toBe('300px');
    }));
  });

  describe('with no placeholder', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([BasicSelectNoPlaceholder])));

    it('should set the width of the overlay if there is no placeholder', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicSelectNoPlaceholder);

      fixture.detectChanges();
      const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;

      trigger.click();
      fixture.detectChanges();
      flush();

      const pane = overlayContainerElement.querySelector('.cdk-overlay-pane') as HTMLElement;
      expect(parseInt(pane.style.width as string)).toBeGreaterThan(0);
    }));
  });

  describe('with theming', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([BasicSelectWithTheming])));

    let fixture: ComponentFixture<BasicSelectWithTheming>;

    beforeEach(() => {
      fixture = TestBed.createComponent(BasicSelectWithTheming);
      fixture.detectChanges();
    });

    it('should transfer the theme to the select panel', () => {
      fixture.componentInstance.theme = 'warn';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      fixture.componentInstance.select.open();
      fixture.detectChanges();

      const panel = overlayContainerElement.querySelector('.mat-mdc-select-panel')! as HTMLElement;
      expect(panel.classList).toContain('mat-warn');
    });
  });

  describe('when invalid inside a form', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([InvalidSelectInForm])));

    it('should not throw SelectionModel errors in addition to ngModel errors', () => {
      const fixture = TestBed.createComponent(InvalidSelectInForm);

      // The first change detection run will throw the "ngModel is missing a name" error.
      expect(() => fixture.detectChanges()).toThrowError(/the name attribute must be set/g);
      fixture.changeDetectorRef.markForCheck();

      // The second run shouldn't throw selection-model related errors.
      expect(() => fixture.detectChanges()).not.toThrow();
    });
  });

  describe('with ngModel using compareWith', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([NgModelCompareWithSelect])));

    let fixture: ComponentFixture<NgModelCompareWithSelect>;
    let instance: NgModelCompareWithSelect;

    beforeEach(fakeAsync(() => {
      fixture = TestBed.createComponent(NgModelCompareWithSelect);
      instance = fixture.componentInstance;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      flush();
    }));

    describe('comparing by value', () => {
      it('should have a selection', fakeAsync(() => {
        const selectedOption = instance.select.selected as MatOption;
        expect(selectedOption.value.value).toEqual('pizza-1');
      }));

      it('should update when making a new selection', fakeAsync(() => {
        instance.options.last._selectViaInteraction();
        fixture.detectChanges();
        flush();

        const selectedOption = instance.select.selected as MatOption;
        expect(instance.selectedFood.value).toEqual('tacos-2');
        expect(selectedOption.value.value).toEqual('tacos-2');
      }));
    });

    describe('comparing by reference', () => {
      beforeEach(fakeAsync(() => {
        spyOn(instance, 'compareByReference').and.callThrough();
        instance.useCompareByReference();
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        flush();
      }));

      it('should use the comparator', () => {
        expect(instance.compareByReference).toHaveBeenCalled();
      });

      it('should initialize with no selection despite having a value', () => {
        expect(instance.selectedFood.value).toBe('pizza-1');
        expect(instance.select.selected).toBeUndefined();
      });

      it('should not update the selection if value is copied on change', fakeAsync(() => {
        instance.options.first._selectViaInteraction();
        fixture.detectChanges();
        flush();

        expect(instance.selectedFood.value).toEqual('steak-0');
        expect(instance.select.selected).toBeUndefined();
      }));

      it('should throw an error when using a non-function comparator', () => {
        instance.useNullComparator();

        expect(() => {
          fixture.changeDetectorRef.markForCheck();
          fixture.detectChanges();
        }).toThrowError(wrappedErrorMessage(getMatSelectNonFunctionValueError()));
      });
    });
  });

  describe(`when the select's value is accessed on initialization`, () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectEarlyAccessSibling])));

    it('should not throw when trying to access the selected value on init in the view', () => {
      expect(() => {
        TestBed.createComponent(SelectEarlyAccessSibling).detectChanges();
      }).not.toThrow();
    });

    it('should not throw when reading selected value programmatically in single selection mode', () => {
      expect(() => {
        const fixture = TestBed.createComponent(SelectEarlyAccessSibling);
        const select = fixture.debugElement.query(By.directive(MatSelect)).componentInstance;
        // We're checking that accessing the getter won't throw.
        select.multiple = false;
        return select.selected;
      }).not.toThrow();
    });

    it('should not throw when reading selected value programmatically in multi selection mode', () => {
      expect(() => {
        const fixture = TestBed.createComponent(SelectEarlyAccessSibling);
        const select = fixture.debugElement.query(By.directive(MatSelect)).componentInstance;
        // We're checking that accessing the getter won't throw.
        select.multiple = true;
        return select.selected;
      }).not.toThrow();
    });
  });

  describe('with ngIf and mat-label', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectWithNgIfAndLabel])));

    it('should not throw when using ngIf on a select with an associated label', () => {
      expect(() => {
        const fixture = TestBed.createComponent(SelectWithNgIfAndLabel);
        fixture.detectChanges();
      }).not.toThrow();
    });
  });

  describe('inside of a form group', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectInsideFormGroup])));

    let fixture: ComponentFixture<SelectInsideFormGroup>;
    let testComponent: SelectInsideFormGroup;
    let select: HTMLElement;

    beforeEach(() => {
      fixture = TestBed.createComponent(SelectInsideFormGroup);
      fixture.detectChanges();
      testComponent = fixture.componentInstance;
      select = fixture.debugElement.query(By.css('mat-select'))!.nativeElement;
    });

    it('should not set the invalid class on a clean select', fakeAsync(() => {
      expect(testComponent.formGroup.untouched)
        .withContext('Expected the form to be untouched.')
        .toBe(true);
      expect(testComponent.formControl.invalid)
        .withContext('Expected form control to be invalid.')
        .toBe(true);
      expect(select.classList).not.toContain(
        'mat-mdc-select-invalid',
        'Expected select not to appear invalid.',
      );
      expect(select.getAttribute('aria-invalid'))
        .withContext('Expected aria-invalid to be set to false.')
        .toBe('false');
    }));

    it('should appear as invalid if it becomes touched', fakeAsync(() => {
      expect(select.classList).not.toContain(
        'mat-mdc-select-invalid',
        'Expected select not to appear invalid.',
      );
      expect(select.getAttribute('aria-invalid'))
        .withContext('Expected aria-invalid to be set to false.')
        .toBe('false');

      testComponent.formControl.markAsTouched();
      fixture.detectChanges();

      expect(select.classList)
        .withContext('Expected select to appear invalid.')
        .toContain('mat-mdc-select-invalid');
      expect(select.getAttribute('aria-invalid'))
        .withContext('Expected aria-invalid to be set to true.')
        .toBe('true');
    }));

    it('should not have the invalid class when the select becomes valid', fakeAsync(() => {
      testComponent.formControl.markAsTouched();
      fixture.detectChanges();

      expect(select.classList)
        .withContext('Expected select to appear invalid.')
        .toContain('mat-mdc-select-invalid');
      expect(select.getAttribute('aria-invalid'))
        .withContext('Expected aria-invalid to be set to true.')
        .toBe('true');

      testComponent.formControl.setValue('pizza-1');
      fixture.detectChanges();

      expect(select.classList).not.toContain(
        'mat-mdc-select-invalid',
        'Expected select not to appear invalid.',
      );
      expect(select.getAttribute('aria-invalid'))
        .withContext('Expected aria-invalid to be set to false.')
        .toBe('false');
    }));

    it('should appear as invalid when the parent form group is submitted', fakeAsync(() => {
      expect(select.classList).not.toContain(
        'mat-mdc-select-invalid',
        'Expected select not to appear invalid.',
      );
      expect(select.getAttribute('aria-invalid'))
        .withContext('Expected aria-invalid to be set to false.')
        .toBe('false');

      dispatchFakeEvent(fixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
      fixture.detectChanges();

      expect(select.classList)
        .withContext('Expected select to appear invalid.')
        .toContain('mat-mdc-select-invalid');
      expect(select.getAttribute('aria-invalid'))
        .withContext('Expected aria-invalid to be set to true.')
        .toBe('true');
    }));

    it('should render the error messages when the parent form is submitted', fakeAsync(() => {
      const debugEl = fixture.debugElement.nativeElement;

      expect(debugEl.querySelectorAll('mat-error').length)
        .withContext('Expected no error messages')
        .toBe(0);

      dispatchFakeEvent(fixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
      fixture.detectChanges();

      expect(debugEl.querySelectorAll('mat-error').length)
        .withContext('Expected one error message')
        .toBe(1);
    }));

    it('should override error matching behavior via injection token', () => {
      const errorStateMatcher: ErrorStateMatcher = {
        isErrorState: jasmine.createSpy('error state matcher').and.returnValue(true),
      };

      fixture.destroy();

      TestBed.resetTestingModule().configureTestingModule({
        imports: [MatSelectModule, ReactiveFormsModule, FormsModule, NoopAnimationsModule],
        declarations: [SelectInsideFormGroup],
        providers: [{provide: ErrorStateMatcher, useValue: errorStateMatcher}],
      });

      const errorFixture = TestBed.createComponent(SelectInsideFormGroup);
      const component = errorFixture.componentInstance;

      errorFixture.detectChanges();

      expect(component.select.errorState).toBe(true);
      expect(errorStateMatcher.isErrorState).toHaveBeenCalled();
    });

    it('should notify that the state changed when the options have changed', fakeAsync(() => {
      testComponent.formControl.setValue('pizza-1');
      fixture.detectChanges();

      const spy = jasmine.createSpy('stateChanges spy');
      const subscription = testComponent.select.stateChanges.subscribe(spy);

      testComponent.options = [];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

      expect(spy).toHaveBeenCalled();
      subscription.unsubscribe();
    }));

    it('should set an asterisk after the label if the FormControl is required', fakeAsync(() => {
      const label = fixture.nativeElement.querySelector('.mat-mdc-form-field label');
      expect(label.querySelector('.mat-mdc-form-field-required-marker')).toBeTruthy();
    }));
  });

  describe('with custom error behavior', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([CustomErrorBehaviorSelect])));

    it('should be able to override the error matching behavior via an @Input', () => {
      const fixture = TestBed.createComponent(CustomErrorBehaviorSelect);
      const component = fixture.componentInstance;
      const matcher = jasmine.createSpy('error state matcher').and.returnValue(true);

      fixture.detectChanges();

      expect(component.control.invalid).toBe(false);
      expect(component.select.errorState).toBe(false);

      fixture.componentInstance.errorStateMatcher = {isErrorState: matcher};
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(component.select.errorState).toBe(true);
      expect(matcher).toHaveBeenCalled();
    });
  });

  describe('with preselected array values', () => {
    beforeEach(waitForAsync(() =>
      configureMatSelectTestingModule([SingleSelectWithPreselectedArrayValues])));

    it('should be able to preselect an array value in single-selection mode', fakeAsync(() => {
      const fixture = TestBed.createComponent(SingleSelectWithPreselectedArrayValues);
      fixture.detectChanges();
      flush();
      fixture.detectChanges();

      const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;

      expect(trigger.textContent).toContain('Pizza');
      expect(fixture.componentInstance.options.toArray()[1].selected).toBe(true);
    }));
  });

  describe('with custom value accessor', () => {
    beforeEach(waitForAsync(() =>
      configureMatSelectTestingModule([CompWithCustomSelect, CustomSelectAccessor])));

    it('should support use inside a custom value accessor', () => {
      const fixture = TestBed.createComponent(CompWithCustomSelect);
      spyOn(fixture.componentInstance.customAccessor, 'writeValue');
      fixture.detectChanges();

      expect(fixture.componentInstance.customAccessor.select.ngControl)
        .withContext('Expected mat-select NOT to inherit control from parent value accessor.')
        .toBeFalsy();
      expect(fixture.componentInstance.customAccessor.writeValue).toHaveBeenCalled();
    });
  });

  describe('with a falsy value', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([FalsyValueSelect])));

    it('should be able to programmatically select a falsy option', fakeAsync(() => {
      const fixture = TestBed.createComponent(FalsyValueSelect);

      fixture.detectChanges();
      fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement.click();
      fixture.componentInstance.control.setValue(0);
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.options.first.selected)
        .withContext('Expected first option to be selected')
        .toBe(true);
      expect(overlayContainerElement.querySelectorAll('mat-option')[0].classList)
        .withContext('Expected first option to be selected')
        .toContain('mdc-list-item--selected');
    }));
  });

  describe('with OnPush', () => {
    beforeEach(waitForAsync(() =>
      configureMatSelectTestingModule([BasicSelectOnPush, BasicSelectOnPushPreselected])));

    it('should set the trigger text based on the value when initialized', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicSelectOnPushPreselected);

      fixture.detectChanges();
      flush();

      const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;

      fixture.detectChanges();

      expect(trigger.textContent).toContain('Pizza');
    }));

    it('should update the trigger based on the value', () => {
      const fixture = TestBed.createComponent(BasicSelectOnPush);
      fixture.detectChanges();
      const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;

      fixture.componentInstance.control.setValue('pizza-1');
      fixture.detectChanges();

      expect(trigger.textContent).toContain('Pizza');

      fixture.componentInstance.control.reset();
      fixture.detectChanges();

      expect(trigger.textContent).not.toContain('Pizza');
    });

    it('should sync up the form control value with the component value', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicSelectOnPushPreselected);
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.control.value).toBe('pizza-1');
      expect(fixture.componentInstance.select.value).toBe('pizza-1');
    }));
  });

  describe('with custom trigger', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectWithCustomTrigger])));

    it('should allow the user to customize the label', () => {
      const fixture = TestBed.createComponent(SelectWithCustomTrigger);
      fixture.detectChanges();

      fixture.componentInstance.control.setValue('pizza-1');
      fixture.detectChanges();

      const label = fixture.debugElement.query(By.css('.mat-mdc-select-value'))!.nativeElement;

      expect(label.textContent)
        .withContext('Expected the displayed text to be "Pizza" in reverse.')
        .toContain('azziP');
    });
  });

  describe('when reseting the value by setting null or undefined', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([ResetValuesSelect])));

    let fixture: ComponentFixture<ResetValuesSelect>;
    let trigger: HTMLElement;
    let formField: HTMLElement;
    let options: NodeListOf<HTMLElement>;
    let label: HTMLLabelElement;

    beforeEach(fakeAsync(() => {
      fixture = TestBed.createComponent(ResetValuesSelect);
      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')!;

      trigger.click();
      fixture.detectChanges();
      flush();

      options = overlayContainerElement.querySelectorAll('mat-option') as NodeListOf<HTMLElement>;
      options[0].click();
      fixture.detectChanges();
      flush();
    }));

    it('should reset when an option with an undefined value is selected', fakeAsync(() => {
      options[4].click();
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.control.value).toBeUndefined();
      expect(fixture.componentInstance.select.selected).toBeFalsy();
      expect(label.classList).not.toContain('mdc-floating-label--float-above');
      expect(trigger.textContent).not.toContain('Undefined');
    }));

    it('should reset when an option with a null value is selected', fakeAsync(() => {
      options[5].click();
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.control.value).toBeNull();
      expect(fixture.componentInstance.select.selected).toBeFalsy();
      expect(label.classList).not.toContain('mdc-floating-label--float-above');
      expect(trigger.textContent).not.toContain('Null');
    }));

    it('should reset when a blank option is selected', fakeAsync(() => {
      options[6].click();
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.control.value).toBeUndefined();
      expect(fixture.componentInstance.select.selected).toBeFalsy();
      expect(label.classList).not.toContain('mdc-floating-label--float-above');
      expect(trigger.textContent).not.toContain('None');
    }));

    it('should not mark the reset option as selected ', fakeAsync(() => {
      options[5].click();
      fixture.detectChanges();
      flush();

      fixture.componentInstance.select.open();
      fixture.detectChanges();
      flush();

      expect(options[5].classList).not.toContain('mdc-list-item--selected');
    }));

    it('should not reset when any other falsy option is selected', fakeAsync(() => {
      options[3].click();
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.control.value).toBe(false);
      expect(fixture.componentInstance.select.selected).toBeTruthy();
      expect(label.classList).toContain('mdc-floating-label--float-above');
      expect(trigger.textContent).toContain('Falsy');
    }));

    it('should not consider the reset values as selected when resetting the form control', () => {
      expect(label.classList).toContain('mdc-floating-label--float-above');

      fixture.componentInstance.control.reset();
      fixture.detectChanges();

      expect(fixture.componentInstance.control.value).toBeNull();
      expect(fixture.componentInstance.select.selected).toBeFalsy();
      expect(label.classList).not.toContain('mdc-floating-label--float-above');
      expect(trigger.textContent).not.toContain('Null');
      expect(trigger.textContent).not.toContain('Undefined');
    });
  });

  describe('with reset option and a form control', () => {
    let fixture: ComponentFixture<SelectWithResetOptionAndFormControl>;
    let options: HTMLElement[];
    let trigger: HTMLElement;

    beforeEach(() => {
      configureMatSelectTestingModule([SelectWithResetOptionAndFormControl]);
      fixture = TestBed.createComponent(SelectWithResetOptionAndFormControl);
      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 select value', fakeAsync(() => {
      fixture.componentInstance.control.setValue('a');
      fixture.detectChanges();
      expect(fixture.componentInstance.select.value).toBe('a');
    }));

    it('should reset the control value', fakeAsync(() => {
      fixture.componentInstance.control.setValue('a');
      fixture.detectChanges();

      options[0].click();
      fixture.detectChanges();
      flush();
      expect(fixture.componentInstance.control.value).toBeUndefined();
    }));

    it('should reflect the value in the form control', fakeAsync(() => {
      options[1].click();
      fixture.detectChanges();
      flush();
      expect(fixture.componentInstance.select.value).toBe('a');
      expect(fixture.componentInstance.control.value).toBe('a');
    }));

    it('should deselect the reset option when a value is assigned through the form control', fakeAsync(() => {
      expect(options[0].classList).toContain('mat-mdc-option-active');

      options[0].click();
      fixture.detectChanges();
      flush();

      fixture.componentInstance.control.setValue('c');
      fixture.detectChanges();
      trigger.click();
      flush();
      fixture.detectChanges();

      expect(options[0].classList).not.toContain('mat-mdc-option-active');
      expect(options[3].classList).toContain('mat-mdc-option-active');
    }));
  });

  describe('without Angular forms', () => {
    beforeEach(waitForAsync(() =>
      configureMatSelectTestingModule([
        BasicSelectWithoutForms,
        BasicSelectWithoutFormsPreselected,
        BasicSelectWithoutFormsMultiple,
      ])));

    it('should set the value when options are clicked', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicSelectWithoutForms);

      fixture.detectChanges();
      expect(fixture.componentInstance.selectedFood).toBeFalsy();

      const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;

      trigger.click();
      fixture.detectChanges();
      flush();

      (overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.selectedFood).toBe('steak-0');
      expect(fixture.componentInstance.select.value).toBe('steak-0');
      expect(trigger.textContent).toContain('Steak');

      trigger.click();
      fixture.detectChanges();
      flush();

      (overlayContainerElement.querySelectorAll('mat-option')[2] as HTMLElement).click();
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.selectedFood).toBe('sandwich-2');
      expect(fixture.componentInstance.select.value).toBe('sandwich-2');
      expect(trigger.textContent).toContain('Sandwich');
    }));

    it('should mark options as selected when the value is set', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicSelectWithoutForms);

      fixture.detectChanges();
      fixture.componentInstance.selectedFood = 'sandwich-2';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
      expect(trigger.textContent).toContain('Sandwich');

      trigger.click();
      fixture.detectChanges();
      flush();

      const option = overlayContainerElement.querySelectorAll('mat-option')[2];

      expect(option.classList).toContain('mdc-list-item--selected');
      expect(fixture.componentInstance.select.value).toBe('sandwich-2');
    }));

    it('should reset the label when a null value is set', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicSelectWithoutForms);

      fixture.detectChanges();
      expect(fixture.componentInstance.selectedFood).toBeFalsy();

      const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;

      trigger.click();
      fixture.detectChanges();
      flush();

      (overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.selectedFood).toBe('steak-0');
      expect(fixture.componentInstance.select.value).toBe('steak-0');
      expect(trigger.textContent).toContain('Steak');

      fixture.componentInstance.selectedFood = null;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(fixture.componentInstance.select.value).toBeNull();
      expect(trigger.textContent).not.toContain('Steak');
    }));

    it('should reflect the preselected value', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicSelectWithoutFormsPreselected);

      fixture.detectChanges();
      flush();

      const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
      fixture.detectChanges();
      expect(trigger.textContent).toContain('Pizza');

      trigger.click();
      fixture.detectChanges();
      flush();

      const option = overlayContainerElement.querySelectorAll('mat-option')[1];

      expect(option.classList).toContain('mdc-list-item--selected');
      expect(fixture.componentInstance.select.value).toBe('pizza-1');
    }));

    it('should be able to select multiple values', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicSelectWithoutFormsMultiple);

      fixture.detectChanges();
      expect(fixture.componentInstance.selectedFoods).toBeFalsy();

      const trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;

      trigger.click();
      fixture.detectChanges();
      flush();

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;

      options[0].click();
      fixture.detectChanges();

      expect(fixture.componentInstance.selectedFoods).toEqual(['steak-0']);
      expect(fixture.componentInstance.select.value).toEqual(['steak-0']);
      expect(trigger.textContent).toContain('Steak');

      options[2].click();
      fixture.detectChanges();

      expect(fixture.componentInstance.selectedFoods).toEqual(['steak-0', 'sandwich-2']);
      expect(fixture.componentInstance.select.value).toEqual(['steak-0', 'sandwich-2']);
      expect(trigger.textContent).toContain('Steak, Sandwich');

      options[1].click();
      fixture.detectChanges();

      expect(fixture.componentInstance.selectedFoods).toEqual(['steak-0', 'pizza-1', 'sandwich-2']);
      expect(fixture.componentInstance.select.value).toEqual(['steak-0', 'pizza-1', 'sandwich-2']);
      expect(trigger.textContent).toContain('Steak, Pizza, Sandwich');
    }));

    it('should restore focus to the host element', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicSelectWithoutForms);

      fixture.detectChanges();
      fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement.click();
      fixture.detectChanges();
      flush();

      (overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
      fixture.detectChanges();
      flush();

      const select = fixture.debugElement.nativeElement.querySelector('mat-select');

      expect(document.activeElement).withContext('Expected trigger to be focused.').toBe(select);
    }));

    it('should not restore focus to the host element when clicking outside', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicSelectWithoutForms);
      const select = fixture.debugElement.nativeElement.querySelector('mat-select');

      fixture.detectChanges();
      select.focus(); // Focus manually since the programmatic click might not do it.
      fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement.click();
      fixture.detectChanges();
      flush();

      expect(document.activeElement).withContext('Expected trigger to be focused.').toBe(select);

      select.blur(); // Blur manually since the programmatic click might not do it.
      (overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement).click();
      fixture.detectChanges();
      flush();

      expect(document.activeElement).not.toBe(select, 'Expected trigger not to be focused.');
    }));

    it('should update the data binding before emitting the change event', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicSelectWithoutForms);
      const instance = fixture.componentInstance;
      const spy = jasmine.createSpy('change spy');

      fixture.detectChanges();
      instance.select.selectionChange.subscribe(() => spy(instance.selectedFood));

      expect(instance.selectedFood).toBeFalsy();

      fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement.click();
      fixture.detectChanges();
      flush();

      (overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
      fixture.detectChanges();
      flush();

      expect(instance.selectedFood).toBe('steak-0');
      expect(spy).toHaveBeenCalledWith('steak-0');
    }));

    it('should select the active option when tabbing away while open', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicSelectWithoutForms);
      fixture.detectChanges();
      const select = fixture.nativeElement.querySelector('.mat-mdc-select');

      expect(fixture.componentInstance.selectedFood).toBeFalsy();

      const trigger = fixture.nativeElement.querySelector('.mat-mdc-select-trigger');

      trigger.click();
      fixture.detectChanges();
      flush();

      dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
      fixture.detectChanges();
      dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
      fixture.detectChanges();

      dispatchKeyboardEvent(select, 'keydown', TAB);
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.selectedFood).toBe('sandwich-2');
      expect(fixture.componentInstance.select.value).toBe('sandwich-2');
      expect(trigger.textContent).toContain('Sandwich');
    }));

    it('should not select the active option when tabbing away while close', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicSelectWithoutForms);
      fixture.detectChanges();
      const select = fixture.nativeElement.querySelector('.mat-mdc-select');

      expect(fixture.componentInstance.selectedFood).toBeFalsy();

      const trigger = fixture.nativeElement.querySelector('.mat-mdc-select-trigger');

      trigger.click();
      fixture.detectChanges();
      flush();

      dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
      fixture.detectChanges();
      dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
      fixture.detectChanges();
      dispatchKeyboardEvent(select, 'keydown', ESCAPE);
      fixture.detectChanges();

      dispatchKeyboardEvent(select, 'keydown', TAB);
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.selectedFood).toBeFalsy();
    }));

    it('should not change the multiple value selection when tabbing away', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicSelectWithoutFormsMultiple);
      fixture.detectChanges();

      expect(fixture.componentInstance.selectedFoods)
        .withContext('Expected no value on init.')
        .toBeFalsy();

      const select = fixture.nativeElement.querySelector('.mat-mdc-select');
      const trigger = fixture.nativeElement.querySelector('.mat-mdc-select-trigger');
      trigger.click();
      fixture.detectChanges();

      dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
      fixture.detectChanges();
      dispatchKeyboardEvent(select, 'keydown', DOWN_ARROW);
      fixture.detectChanges();

      dispatchKeyboardEvent(select, 'keydown', TAB);
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.selectedFoods)
        .withContext('Expected no value after tabbing away.')
        .toBeFalsy();
    }));

    it('should emit once when a reset value is selected', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicSelectWithoutForms);
      const instance = fixture.componentInstance;
      const spy = jasmine.createSpy('change spy');

      instance.selectedFood = 'sandwich-2';
      instance.foods[0].value = null;
      fixture.detectChanges();

      const subscription = instance.select.selectionChange.subscribe(spy);

      fixture.debugElement.query(By.css('.mat-mdc-select-trigger')).nativeElement.click();
      fixture.detectChanges();
      flush();

      (overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
      fixture.detectChanges();
      flush();

      expect(spy).toHaveBeenCalledTimes(1);

      subscription.unsubscribe();
    }));

    it(
      'should not emit the change event multiple times when a reset option is ' +
        'selected twice in a row',
      fakeAsync(() => {
        const fixture = TestBed.createComponent(BasicSelectWithoutForms);
        const instance = fixture.componentInstance;
        const spy = jasmine.createSpy('change spy');

        instance.foods[0].value = null;
        fixture.detectChanges();

        const subscription = instance.select.selectionChange.subscribe(spy);

        fixture.debugElement.query(By.css('.mat-mdc-select-trigger')).nativeElement.click();
        fixture.detectChanges();
        flush();

        (overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
        fixture.detectChanges();
        flush();

        expect(spy).not.toHaveBeenCalled();

        fixture.debugElement.query(By.css('.mat-mdc-select-trigger')).nativeElement.click();
        fixture.detectChanges();
        flush();

        (overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
        fixture.detectChanges();
        flush();

        expect(spy).not.toHaveBeenCalled();

        subscription.unsubscribe();
      }),
    );
  });

  describe('with option centering disabled', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([SelectWithoutOptionCentering])));

    let fixture: ComponentFixture<SelectWithoutOptionCentering>;
    let trigger: HTMLElement;

    beforeEach(fakeAsync(() => {
      fixture = TestBed.createComponent(SelectWithoutOptionCentering);
      fixture.detectChanges();
      flush();
      trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
    }));

    it('should not align the active option with the trigger if centering is disabled', fakeAsync(() => {
      trigger.click();
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      flush();

      const scrollContainer = document.querySelector('.cdk-overlay-pane .mat-mdc-select-panel')!;

      // The panel should be scrolled to 0 because centering the option disabled.
      // The panel should be scrolled to 0 because centering the option disabled.
      expect(scrollContainer.scrollTop)
        .withContext(`Expected panel not to be scrolled.`)
        .toEqual(0);
      // The trigger should contain 'Pizza' because it was preselected
      expect(trigger.textContent).toContain('Pizza');
      // The selected index should be 1 because it was preselected
      expect(fixture.componentInstance.options.toArray()[1].selected).toBe(true);
    }));
  });

  describe('positioning', () => {
    beforeEach(waitForAsync(() => configureMatSelectTestingModule([BasicSelect])));

    let fixture: ComponentFixture<BasicSelect>;
    let trigger: HTMLElement;
    let formField: HTMLElement;
    let formFieldWrapper: HTMLElement;

    beforeEach(fakeAsync(() => {
      fixture = TestBed.createComponent(BasicSelect);
      fixture.detectChanges();
      flush();
      formField = fixture.nativeElement.querySelector('.mat-mdc-form-field');
      formFieldWrapper = formField.querySelector('.mat-mdc-text-field-wrapper') as HTMLElement;
      trigger = formFieldWrapper.querySelector('.mat-mdc-select-trigger') as HTMLElement;
    }));

    it('should position the panel under the form field by default', fakeAsync(() => {
      formField.style.position = 'fixed';
      formField.style.left = formField.style.top = '10%';
      trigger.click();
      fixture.detectChanges();
      flush();

      const panel = overlayContainerElement.querySelector('.cdk-overlay-pane')!;
      const paneRect = panel.getBoundingClientRect();
      const formFieldWrapperRect = formFieldWrapper.getBoundingClientRect();

      expect(panel.classList).not.toContain('mat-mdc-select-panel-above');
      expect(Math.floor(paneRect.width)).toBe(Math.floor(formFieldWrapperRect.width));
      expect(Math.floor(paneRect.left)).toBe(Math.floor(formFieldWrapperRect.left));
      expect(Math.floor(paneRect.top)).toBe(Math.floor(formFieldWrapperRect.bottom));
    }));

    it('should position the panel under the form field by default', fakeAsync(() => {
      formField.style.position = 'fixed';
      formField.style.left = '10%';
      formField.style.bottom = '0';
      trigger.click();
      fixture.detectChanges();
      flush();

      const panel = overlayContainerElement.querySelector('.cdk-overlay-pane')!;
      const paneRect = panel.getBoundingClientRect();
      const formFieldWrapperRect = formFieldWrapper.getBoundingClientRect();

      expect(panel.classList).toContain('mat-mdc-select-panel-above');
      expect(Math.floor(paneRect.width)).toBe(Math.floor(formFieldWrapperRect.width));
      expect(Math.floor(paneRect.left)).toBe(Math.floor(formFieldWrapperRect.left));
      expect(Math.floor(paneRect.bottom)).toBe(Math.floor(formFieldWrapperRect.top));
    }));
  });

  describe('with multiple selection', () => {
    beforeEach(waitForAsync(() =>
      configureMatSelectTestingModule([MultiSelect, MultiSelectWithLotsOfOptions])));

    let fixture: ComponentFixture<MultiSelect>;
    let testInstance: MultiSelect;
    let trigger: HTMLElement;

    beforeEach(() => {
      fixture = TestBed.createComponent(MultiSelect);
      testInstance = fixture.componentInstance;
      fixture.detectChanges();
      trigger = fixture.debugElement.query(By.css('.mat-mdc-select-trigger'))!.nativeElement;
    });

    it('should be able to select multiple values', () => {
      trigger.click();
      fixture.detectChanges();

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;

      options[0].click();
      options[2].click();
      options[5].click();
      fixture.detectChanges();

      expect(testInstance.control.value).toEqual(['steak-0', 'tacos-2', 'eggs-5']);
    });

    it('should be able to toggle an option on and off', () => {
      trigger.click();
      fixture.detectChanges();

      const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;

      option.click();
      fixture.detectChanges();

      expect(testInstance.control.value).toEqual(['steak-0']);

      option.click();
      fixture.detectChanges();

      expect(testInstance.control.value).toEqual([]);
    });

    it('should update the label', fakeAsync(() => {
      trigger.click();
      fixture.detectChanges();
      flush();

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;

      options[0].click();
      options[2].click();
      options[5].click();
      fixture.detectChanges();

      expect(trigger.textContent).toContain('Steak, Tacos, Eggs');

      options[2].click();
      fixture.detectChanges();

      expect(trigger.textContent).toContain('Steak, Eggs');
    }));

    it('should be able to set the selected value by taking an array', () => {
      trigger.click();
      testInstance.control.setValue(['steak-0', 'eggs-5']);
      fixture.detectChanges();

      const optionNodes = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;

      const optionInstances = testInstance.options.toArray();

      expect(optionNodes[0].classList).toContain('mdc-list-item--selected');
      expect(optionNodes[5].classList).toContain('mdc-list-item--selected');

      expect(optionInstances[0].selected).toBe(true);
      expect(optionInstances[5].selected).toBe(true);
    });

    it('should override the previously-selected value when setting an array', () => {
      trigger.click();
      fixture.detectChanges();

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;

      options[0].click();
      fixture.detectChanges();

      expect(options[0].classList).toContain('mdc-list-item--selected');

      testInstance.control.setValue(['eggs-5']);
      fixture.detectChanges();

      expect(options[0].classList).not.toContain('mdc-list-item--selected');
      expect(options[5].classList).toContain('mdc-list-item--selected');
    });

    it('should not close the panel when clicking on options', () => {
      trigger.click();
      fixture.detectChanges();

      expect(testInstance.select.panelOpen).toBe(true);

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;

      options[0].click();
      options[1].click();
      fixture.detectChanges();

      expect(testInstance.select.panelOpen).toBe(true);
    });

    it('should sort the selected options based on their order in the panel', fakeAsync(() => {
      trigger.click();
      fixture.detectChanges();
      flush();

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;

      options[2].click();
      options[0].click();
      options[1].click();
      fixture.detectChanges();

      expect(trigger.textContent).toContain('Steak, Pizza, Tacos');
      expect(fixture.componentInstance.control.value).toEqual(['steak-0', 'pizza-1', 'tacos-2']);
    }));

    it('should sort the selected options in reverse in rtl', fakeAsync(() => {
      dir.value = 'rtl';
      trigger.click();
      fixture.detectChanges();
      flush();

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;

      options[2].click();
      options[0].click();
      options[1].click();
      fixture.detectChanges();

      expect(trigger.textContent).toContain('Tacos, Pizza, Steak');
      expect(fixture.componentInstance.control.value).toEqual(['steak-0', 'pizza-1', 'tacos-2']);
    }));

    it('should be able to customize the value sorting logic', fakeAsync(() => {
      fixture.componentInstance.sortComparator = (a, b, optionsArray) => {
        return optionsArray.indexOf(b) - optionsArray.indexOf(a);
      };
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      trigger.click();
      fixture.detectChanges();
      flush();

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;

      for (let i = 0; i < 3; i++) {
        options[i].click();
      }
      fixture.detectChanges();

      // Expect the items to be in reverse order.
      expect(trigger.textContent).toContain('Tacos, Pizza, Steak');
      expect(fixture.componentInstance.control.value).toEqual(['tacos-2', 'pizza-1', 'steak-0']);
    }));

    it('should sort the values that get set via the model based on the panel order', () => {
      trigger.click();
      fixture.detectChanges();

      testInstance.control.setValue(['tacos-2', 'steak-0', 'pizza-1']);
      fixture.detectChanges();

      expect(trigger.textContent).toContain('Steak, Pizza, Tacos');
    });

    it('should reverse sort the values, that get set via the model in rtl', () => {
      dir.value = 'rtl';
      trigger.click();
      fixture.detectChanges();

      testInstance.control.setValue(['tacos-2', 'steak-0', 'pizza-1']);
      fixture.detectChanges();

      expect(trigger.textContent).toContain('Tacos, Pizza, Steak');
    });

    it('should throw an exception when trying to set a non-array value', () => {
      expect(() => {
        testInstance.control.setValue('not-an-array' as any);
      }).toThrowError(wrappedErrorMessage(getMatSelectNonArrayValueError()));
    });

    it('should throw an exception when trying to change multiple mode after init', () => {
      expect(() => {
        testInstance.select.multiple = false;
      }).toThrowError(wrappedErrorMessage(getMatSelectDynamicMultipleError()));
    });

    it('should pass the `multiple` value to all of the option instances', fakeAsync(() => {
      trigger.click();
      fixture.detectChanges();
      flush();

      expect(testInstance.options.toArray().every(option => !!option.multiple))
        .withContext('Expected `multiple` to have been added to initial set of options.')
        .toBe(true);

      testInstance.foods.push({value: 'cake-8', viewValue: 'Cake'});
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      flush();

      expect(testInstance.options.toArray().every(option => !!option.multiple))
        .withContext('Expected `multiple` to have been set on dynamically-added option.')
        .toBe(true);
    }));

    it('should update the active item index on click', fakeAsync(() => {
      trigger.click();
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0);

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;

      options[2].click();
      fixture.detectChanges();

      expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(2);
    }));

    it('should be to select an option with a `null` value', () => {
      fixture.componentInstance.foods = [
        {value: null, viewValue: 'Steak'},
        {value: 'pizza-1', viewValue: 'Pizza'},
        {value: null, viewValue: 'Tacos'},
      ];
      fixture.changeDetectorRef.markForCheck();

      fixture.detectChanges();
      trigger.click();
      fixture.detectChanges();

      const options = overlayContainerElement.querySelectorAll(
        'mat-option',
      ) as NodeListOf<HTMLElement>;

      options[0].click();
      options[1].click();
      options[2].click();
      fixture.detectChanges();

      expect(testInstance.control.value).toEqual([null!, 'pizza-1', null!]);
    });

    it('should select all options when pressing ctrl + a', () => {
      const selectElement = fixture.nativeElement.querySelector('mat-select');
      const options = fixture.componentInstance.options.toArray();

      expect(testInstance.control.value).toBeFalsy();
      expect(options.every(option => option.selected)).toBe(false);

      fixture.componentInstance.select.open();
      fixture.detectChanges();

      const event = createKeyboardEvent('keydown', A, undefined, {control: true});
      dispatchEvent(selectElement, event);
      fixture.detectChanges();

      expect(options.every(option => option.selected)).toBe(true);
      expect(testInstance.control.value).toEqual([
        'steak-0',
        'pizza-1',
        'tacos-2',
        'sandwich-3',
        'chips-4',
        'eggs-5',
        'pasta-6',
        'sushi-7',
      ]);
    });

    it('should skip disabled options when using ctrl + a', () => {
      const selectElement = fixture.nativeElement.querySelector('mat-select');
      const options = fixture.componentInstance.options.toArray();

      for (let i = 0; i < 3; i++) {
        options[i].disabled = true;
      }

      expect(testInstance.control.value).toBeFalsy();

      fixture.componentInstance.select.open();
      fixture.detectChanges();

      const event = createKeyboardEvent('keydown', A, undefined, {control: true});
      dispatchEvent(selectElement, event);
      fixture.detectChanges();

      expect(testInstance.control.value).toEqual([
        'sandwich-3',
        'chips-4',
        'eggs-5',
        'pasta-6',
        'sushi-7',
      ]);
    });

    it('should select all options when pressing ctrl + a when some options are selected', () => {
      const selectElement = fixture.nativeElement.querySelector('mat-select');
      const options = fixture.componentInstance.options.toArray();

      options[0].select();
      fixture.detectChanges();

      expect(testInstance.control.value).toEqual(['steak-0']);
      expect(options.some(option => option.selected)).toBe(true);

      fixture.componentInstance.select.open();
      fixture.detectChanges();

      const event = createKeyboardEvent('keydown', A, undefined, {control: true});
      dispatchEvent(selectElement, event);
      fixture.detectChanges();

      expect(options.every(option => option.selected)).toBe(true);
      expect(testInstance.control.value).toEqual([
        'steak-0',
        'pizza-1',
        'tacos-2',
        'sandwich-3',
        'chips-4',
        'eggs-5',
        'pasta-6',
        'sushi-7',
      ]);
    });

    it('should deselect all options with ctrl + a if all options are selected', () => {
      const selectElement = fixture.nativeElement.querySelector('mat-select');
      const options = fixture.componentInstance.options.toArray();

      options.forEach(option => option.select());
      fixture.detectChanges();

      expect(testInstance.control.value).toEqual([
        'steak-0',
        'pizza-1',
        'tacos-2',
        'sandwich-3',
        'chips-4',
        'eggs-5',
        'pasta-6',
        'sushi-7',
      ]);
      expect(options.every(option => option.selected)).toBe(true);

      fixture.componentInstance.select.open();
      fixture.detectChanges();

      const event = createKeyboardEvent('keydown', A, undefined, {control: true});
      dispatchEvent(selectElement, event);
      fixture.detectChanges();

      expect(options.some(option => option.selected)).toBe(false);
      expect(testInstance.control.value).toEqual([]);
    });

    it('should not throw when selecting a large amount of options', fakeAsync(() => {
      fixture.destroy();

      const lotsOfOptionsFixture = TestBed.createComponent(MultiSelectWithLotsOfOptions);

      expect(() => {
        lotsOfOptionsFixture.componentInstance.checkAll();
        lotsOfOptionsFixture.detectChanges();
        flush();
      }).not.toThrow();
    }));

    it('should be able to programmatically set an array with duplicate values', () => {
      testInstance.foods = [
        {value: 'steak-0', viewValue: 'Steak'},
        {value: 'pizza-1', viewValue: 'Pizza'},
        {value: 'pizza-1', viewValue: 'Pizza'},
        {value: 'pizza-1', viewValue: 'Pizza'},
        {value: 'pizza-1', viewValue: 'Pizza'},
        {value: 'pizza-1', viewValue: 'Pizza'},
      ];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      testInstance.control.setValue(['steak-0', 'pizza-1', 'pizza-1', 'pizza-1']);
      fixture.detectChanges();

      trigger.click();
      fixture.detectChanges();

      const optionNodes = Array.from(overlayContainerElement.querySelectorAll('mat-option'));
      const optionInstances = testInstance.options.toArray();

      expect(optionNodes.map(node => node.classList.contains('mdc-list-item--selected'))).toEqual([
        true,
        true,
        true,
        true,
        false,
        false,
      ]);

      expect(optionInstances.map(instance => instance.selected)).toEqual([
        true,
        true,
        true,
        true,
        false,
        false,
      ]);
    });

    it('should update the option selected state if the same array is mutated and passed back in', () => {
      const value: string[] = [];
      trigger.click();
      testInstance.control.setValue(value);
      fixture.detectChanges();

      const optionNodes = Array.from<HTMLElement>(
        overlayContainerElement.querySelectorAll('mat-option'),
      );
      const optionInstances = testInstance.options.toArray();

      expect(
        optionNodes.some(option => {
          return option.classList.contains('mdc-list-item--selected');
        }),
      ).toBe(false);
      expect(optionInstances.some(option => option.selected)).toBe(false);

      value.push('eggs-5');
      testInstance.control.setValue(value);
      fixture.detectChanges();

      expect(optionNodes[5].classList).toContain('mdc-list-item--selected');
      expect(optionInstances[5].selected).toBe(true);
    });
  });

  it('should be able to provide default values through an injection token', fakeAsync(() => {
    configureMatSelectTestingModule(
      [NgModelSelect],
      [
        {
          provide: MAT_SELECT_CONFIG,
          useValue: {
            disableOptionCentering: true,
            typeaheadDebounceInterval: 1337,
            overlayPanelClass: 'test-panel-class',
            panelWidth: null,
          } as MatSelectConfig,
        },
      ],
    );
    const fixture = TestBed.createComponent(NgModelSelect);
    fixture.detectChanges();
    const select = fixture.componentInstance.select;
    select.open();
    fixture.detectChanges();
    flush();

    expect(select.disableOptionCentering).toBe(true);
    expect(select.typeaheadDebounceInterval).toBe(1337);
    expect(document.querySelector('.cdk-overlay-pane')?.classList).toContain('test-panel-class');
    expect(select.panelWidth).toBeNull();
  }));

  it('should be able to hide checkmark icon through an injection token', () => {
    const matSelectConfig: MatSelectConfig = {hideSingleSelectionIndicator: true};
    configureMatSelectTestingModule(
      [NgModelSelect],
      [
        {
          provide: MAT_SELECT_CONFIG,
          useValue: matSelectConfig,
        },
      ],
    );
    const fixture = TestBed.createComponent(NgModelSelect);
    fixture.detectChanges();
    const select = fixture.componentInstance.select;

    fixture.componentInstance.select.value = fixture.componentInstance.foods[0].value;
    fixture.changeDetectorRef.markForCheck();
    select.open();
    fixture.detectChanges();

    // Select the first value to ensure selection state is displayed. That way this test ensures
    // that the selection state hides the checkmark icon, rather than hiding the checkmark icon
    // because nothing is selected.
    expect(document.querySelector('mat-option[aria-selected="true"]'))
      .withContext('expecting selection state to be displayed')
      .not.toBeNull();

    const pseudoCheckboxes = document.querySelectorAll('.mat-pseudo-checkbox');

    expect(pseudoCheckboxes.length)
      .withContext('expecting not to display a pseudo-checkbox')
      .toBe(0);
  });

  it('should not not throw if the select is inside an ng-container with ngIf', () => {
    configureMatSelectTestingModule([SelectInNgContainer]);
    const fixture = TestBed.createComponent(SelectInNgContainer);
    expect(() => fixture.detectChanges()).not.toThrow();
  });

  describe('page up/down with disabled options', () => {
    let fixture: ComponentFixture<BasicSelectWithFirstAndLastOptionDisabled>;
    let host: HTMLElement;

    beforeEach(waitForAsync(() =>
      configureMatSelectTestingModule([BasicSelectWithFirstAndLastOptionDisabled])));

    beforeEach(fakeAsync(() => {
      fixture = TestBed.createComponent(BasicSelectWithFirstAndLastOptionDisabled);

      fixture.detectChanges();
      fixture.componentInstance.select.open();
      fixture.detectChanges();
      flush();
      fixture.detectChanges();

      host = fixture.debugElement.query(By.css('mat-select'))!.nativeElement;
    }));

    it('should be able to scroll to disabled option when pressing PAGE_UP', fakeAsync(() => {
      expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(1);

      dispatchKeyboardEvent(host, 'keydown', PAGE_UP);
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0);

      dispatchKeyboardEvent(host, 'keydown', PAGE_UP);
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(0);
    }));

    it('should be able to scroll to disabled option when pressing PAGE_DOWN', fakeAsync(() => {
      dispatchKeyboardEvent(host, 'keydown', PAGE_DOWN);
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(7);

      dispatchKeyboardEvent(host, 'keydown', PAGE_DOWN);
      fixture.detectChanges();
      flush();

      expect(fixture.componentInstance.select._keyManager.activeItemIndex).toBe(7);
    }));
  });
});

@Component({
  selector: 'basic-select',
  template: `
    <div [style.height.px]="heightAbove"></div>
    <mat-form-field>
      @if (hasLabel) {
        <mat-label>Select a food</mat-label>
      }
      <mat-select placeholder="Food" [formControl]="control" [required]="isRequired"
        [tabIndex]="tabIndexOverride" [aria-describedby]="ariaDescribedBy"
        [aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby"
        [panelClass]="panelClass" [disableRipple]="disableRipple"
        [typeaheadDebounceInterval]="typeaheadDebounceInterval"
        [panelWidth]="panelWidth">
        @for (food of foods; track food) {
          <mat-option [value]="food.value" [disabled]="food.disabled">
            {{ capitalize ? food.viewValue.toUpperCase() : food.viewValue }}
          </mat-option>
        }
      </mat-select>
      @if (hint) {
        <mat-hint>{{ hint }}</mat-hint>
      }
    </mat-form-field>
    <div [style.height.px]="heightBelow"></div>
  `,
  standalone: false,
})
class BasicSelect {
  foods: any[] = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
    {value: 'tacos-2', viewValue: 'Tacos', disabled: true},
    {value: 'sandwich-3', viewValue: 'Sandwich'},
    {value: 'chips-4', viewValue: 'Chips'},
    {value: 'eggs-5', viewValue: 'Eggs'},
    {value: 'pasta-6', viewValue: 'Pasta'},
    {value: 'sushi-7', viewValue: 'Sushi'},
  ];
  control = new FormControl<string | null>(null);
  isRequired: boolean;
  heightAbove = 0;
  heightBelow = 0;
  hasLabel = true;
  hint: string;
  tabIndexOverride: number;
  ariaDescribedBy: string;
  ariaLabel: string;
  ariaLabelledby: string;
  panelClass = ['custom-one', 'custom-two'];
  disableRipple: boolean;
  typeaheadDebounceInterval: number;
  capitalize = false;
  panelWidth: string | null | number = 'auto';

  @ViewChild(MatSelect, {static: true}) select: MatSelect;
  @ViewChildren(MatOption) options: QueryList<MatOption>;
}

@Component({
  selector: 'ng-model-select',
  template: `
    <mat-form-field>
      <mat-select placeholder="Food" ngModel [disabled]="isDisabled">
        @for (food of foods; track food) {
          <mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
        }
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class NgModelSelect {
  foods: any[] = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
    {value: 'tacos-2', viewValue: 'Tacos'},
  ];
  isDisabled: boolean;

  @ViewChild(MatSelect) select: MatSelect;
  @ViewChildren(MatOption) options: QueryList<MatOption>;
}

@Component({
  selector: 'many-selects',
  template: `
    <mat-form-field>
      <mat-select placeholder="First">
        <mat-option value="one">one</mat-option>
        <mat-option value="two">two</mat-option>
      </mat-select>
    </mat-form-field>
    <mat-form-field>
      <mat-select placeholder="Second">
        <mat-option value="three">three</mat-option>
        <mat-option value="four">four</mat-option>
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class ManySelects {}

@Component({
  selector: 'ng-if-select',
  template: `
    @if (isShowing) {
      <div>
        <mat-form-field>
          <mat-select placeholder="Food I want to eat right now" [formControl]="control">
            @for (food of foods; track food) {
              <mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
            }
          </mat-select>
        </mat-form-field>
      </div>
    }
  `,
  standalone: false,
})
class NgIfSelect {
  isShowing = false;
  foods: any[] = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
    {value: 'tacos-2', viewValue: 'Tacos'},
  ];
  control = new FormControl('pizza-1');

  @ViewChild(MatSelect) select: MatSelect;
}

@Component({
  selector: 'select-with-change-event',
  template: `
    <mat-form-field>
      <mat-select (selectionChange)="changeListener($event)">
        @for (food of foods; track food) {
          <mat-option [value]="food">{{ food }}</mat-option>
        }
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class SelectWithChangeEvent {
  foods: string[] = [
    'steak-0',
    'pizza-1',
    'tacos-2',
    'sandwich-3',
    'chips-4',
    'eggs-5',
    'pasta-6',
    'sushi-7',
  ];

  changeListener = jasmine.createSpy('MatSelect change listener');
}

@Component({
  selector: 'select-init-without-options',
  template: `
    <mat-form-field>
      <mat-select placeholder="Food I want to eat right now" [formControl]="control">
        @for (food of foods; track food) {
          <mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
        }
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class SelectInitWithoutOptions {
  foods: any[];
  control = new FormControl('pizza-1');

  @ViewChild(MatSelect) select: MatSelect;
  @ViewChildren(MatOption) options: QueryList<MatOption>;

  addOptions() {
    this.foods = [
      {value: 'steak-0', viewValue: 'Steak'},
      {value: 'pizza-1', viewValue: 'Pizza'},
      {value: 'tacos-2', viewValue: 'Tacos'},
    ];
  }
}

@Component({
  selector: 'custom-select-accessor',
  template: `<mat-form-field><mat-select></mat-select></mat-form-field>`,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: CustomSelectAccessor,
      multi: true,
    },
  ],
  standalone: false,
})
class CustomSelectAccessor implements ControlValueAccessor {
  @ViewChild(MatSelect) select: MatSelect;

  writeValue: (value?: any) => void = () => {};
  registerOnChange: (changeFn?: (value: any) => void) => void = () => {};
  registerOnTouched: (touchedFn?: () => void) => void = () => {};
}

@Component({
  selector: 'comp-with-custom-select',
  template: `<custom-select-accessor [formControl]="ctrl"></custom-select-accessor>`,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: CustomSelectAccessor,
      multi: true,
    },
  ],
  standalone: false,
})
class CompWithCustomSelect {
  ctrl = new FormControl('initial value');
  @ViewChild(CustomSelectAccessor, {static: true}) customAccessor: CustomSelectAccessor;
}

@Component({
  selector: 'select-infinite-loop',
  template: `
    <mat-form-field>
      <mat-select [(ngModel)]="value"></mat-select>
    </mat-form-field>
    <throws-error-on-init></throws-error-on-init>
  `,
  standalone: false,
})
class SelectWithErrorSibling {
  value: string;
}

@Component({
  selector: 'throws-error-on-init',
  template: '',
  standalone: false,
})
class ThrowsErrorOnInit implements OnInit {
  ngOnInit() {
    throw Error('Oh no!');
  }
}

@Component({
  selector: 'basic-select-on-push',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <mat-form-field>
      <mat-select placeholder="Food" [formControl]="control">
        @for (food of foods; track food) {
          <mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
        }
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class BasicSelectOnPush {
  foods: any[] = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
    {value: 'tacos-2', viewValue: 'Tacos'},
  ];
  control = new FormControl('');
}

@Component({
  selector: 'basic-select-on-push-preselected',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <mat-form-field>
      <mat-select placeholder="Food" [formControl]="control">
        @for (food of foods; track food) {
          <mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
        }
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class BasicSelectOnPushPreselected {
  @ViewChild(MatSelect) select: MatSelect;
  foods: any[] = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
    {value: 'tacos-2', viewValue: 'Tacos'},
  ];
  control = new FormControl('pizza-1');
}

@Component({
  selector: 'floating-label-select',
  template: `
    <mat-form-field [floatLabel]="floatLabel">
      <mat-label>Select a food</mat-label>
      <mat-select [placeholder]="placeholder" [formControl]="control">
        @for (food of foods; track food) {
          <mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
        }
      </mat-select>
    </mat-form-field>
    `,
  standalone: false,
})
class FloatLabelSelect {
  floatLabel: FloatLabelType | null = 'auto';
  control = new FormControl('');
  placeholder = 'Food I want to eat right now';
  foods: any[] = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
    {value: 'tacos-2', viewValue: 'Tacos'},
  ];

  @ViewChild(MatSelect) select: MatSelect;
}

@Component({
  selector: 'multi-select',
  template: `
    <mat-form-field>
      <mat-select multiple placeholder="Food" [formControl]="control"
        [sortComparator]="sortComparator">
        @for (food of foods; track food) {
          <mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
        }
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class MultiSelect {
  foods: any[] = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
    {value: 'tacos-2', viewValue: 'Tacos'},
    {value: 'sandwich-3', viewValue: 'Sandwich'},
    {value: 'chips-4', viewValue: 'Chips'},
    {value: 'eggs-5', viewValue: 'Eggs'},
    {value: 'pasta-6', viewValue: 'Pasta'},
    {value: 'sushi-7', viewValue: 'Sushi'},
  ];
  control = new FormControl<string[] | null>(null);

  @ViewChild(MatSelect) select: MatSelect;
  @ViewChildren(MatOption) options: QueryList<MatOption>;
  sortComparator: (a: MatOption, b: MatOption, options: MatOption[]) => number;
}

@Component({
  selector: 'select-with-plain-tabindex',
  template: `<mat-form-field><mat-select tabindex="5"></mat-select></mat-form-field>`,
  standalone: false,
})
class SelectWithPlainTabindex {}

@Component({
  selector: 'select-early-sibling-access',
  template: `
    <mat-form-field>
      <mat-select #select="matSelect"></mat-select>
    </mat-form-field>
    @if (select.selected) {
      <div></div>
    }
  `,
  standalone: false,
})
class SelectEarlyAccessSibling {}

@Component({
  selector: 'basic-select-initially-hidden',
  template: `
    <mat-form-field>
      <mat-select [style.display]="isVisible ? 'block' : 'none'">
        <mat-option value="value">There are no other options</mat-option>
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class BasicSelectInitiallyHidden {
  isVisible = false;
}

@Component({
  selector: 'basic-select-no-placeholder',
  template: `
    <mat-form-field>
      <mat-select>
        <mat-option value="value">There are no other options</mat-option>
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class BasicSelectNoPlaceholder {}

@Component({
  selector: 'basic-select-with-theming',
  template: `
    <mat-form-field [color]="theme">
      <mat-select placeholder="Food">
        <mat-option value="steak-0">Steak</mat-option>
        <mat-option value="pizza-1">Pizza</mat-option>
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class BasicSelectWithTheming {
  @ViewChild(MatSelect) select: MatSelect;
  theme: string;
}

@Component({
  selector: 'reset-values-select',
  template: `
    <mat-form-field>
      <mat-label>Select a food</mat-label>
      <mat-select [formControl]="control">
        @for (food of foods; track food) {
          <mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
        }
        <mat-option>None</mat-option>
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class ResetValuesSelect {
  foods: any[] = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
    {value: 'tacos-2', viewValue: 'Tacos'},
    {value: false, viewValue: 'Falsy'},
    {viewValue: 'Undefined'},
    {value: null, viewValue: 'Null'},
  ];
  control = new FormControl('' as string | boolean | null);

  @ViewChild(MatSelect) select: MatSelect;
}

@Component({
  template: `
    <mat-form-field>
      <mat-select [formControl]="control">
        @for (food of foods; track food) {
          <mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
        }
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class FalsyValueSelect {
  foods: any[] = [
    {value: 0, viewValue: 'Steak'},
    {value: 1, viewValue: 'Pizza'},
  ];
  control = new FormControl<number | null>(null);
  @ViewChildren(MatOption) options: QueryList<MatOption>;
}

@Component({
  selector: 'select-with-groups',
  template: `
    <mat-form-field>
      <mat-select placeholder="Pokemon" [formControl]="control">
        @for (group of pokemonTypes; track group) {
          <mat-optgroup [label]="group.name" [disabled]="group.disabled">
            @for (pokemon of group.pokemon; track pokemon) {
              <mat-option [value]="pokemon.value">{{ pokemon.viewValue }}</mat-option>
            }
          </mat-optgroup>
        }
        <mat-option value="mime-11">Mr. Mime</mat-option>
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class SelectWithGroups {
  control = new FormControl('');
  pokemonTypes = [
    {
      name: 'Grass',
      pokemon: [
        {value: 'bulbasaur-0', viewValue: 'Bulbasaur'},
        {value: 'oddish-1', viewValue: 'Oddish'},
        {value: 'bellsprout-2', viewValue: 'Bellsprout'},
      ],
    },
    {
      name: 'Water',
      disabled: true,
      pokemon: [
        {value: 'squirtle-3', viewValue: 'Squirtle'},
        {value: 'psyduck-4', viewValue: 'Psyduck'},
        {value: 'horsea-5', viewValue: 'Horsea'},
      ],
    },
    {
      name: 'Fire',
      pokemon: [
        {value: 'charmander-6', viewValue: 'Charmander'},
        {value: 'vulpix-7', viewValue: 'Vulpix'},
        {value: 'flareon-8', viewValue: 'Flareon'},
      ],
    },
    {
      name: 'Psychic',
      pokemon: [
        {value: 'mew-9', viewValue: 'Mew'},
        {value: 'mewtwo-10', viewValue: 'Mewtwo'},
      ],
    },
  ];

  @ViewChild(MatSelect) select: MatSelect;
  @ViewChildren(MatOption) options: QueryList<MatOption>;
}

@Component({
  selector: 'select-with-groups',
  template: `
    <mat-form-field>
      <mat-select placeholder="Pokemon" [formControl]="control">
        @for (group of pokemonTypes; track group) {
          <mat-optgroup [label]="group.name">
            @for (pokemon of group.pokemon; track pokemon) {
              <mat-option [value]="pokemon.value">{{ pokemon.viewValue }}</mat-option>
            }
          </mat-optgroup>
        }
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class SelectWithGroupsAndNgContainer {
  control = new FormControl('');
  pokemonTypes = [
    {
      name: 'Grass',
      pokemon: [{value: 'bulbasaur-0', viewValue: 'Bulbasaur'}],
    },
  ];
}

@Component({
  template: `
    <form>
      <mat-form-field>
        <mat-select [(ngModel)]="value"></mat-select>
      </mat-form-field>
    </form>
  `,
  standalone: false,
})
class InvalidSelectInForm {
  value: any;
}

@Component({
  template: `
    <form [formGroup]="formGroup">
      <mat-form-field>
        <mat-label>Food</mat-label>
        <mat-select formControlName="food">
          @for (option of options; track option) {
            <mat-option [value]="option.value">{{option.viewValue}}</mat-option>
          }
        </mat-select>

        <mat-error>This field is required</mat-error>
      </mat-form-field>
    </form>
  `,
  standalone: false,
})
class SelectInsideFormGroup {
  @ViewChild(FormGroupDirective) formGroupDirective: FormGroupDirective;
  @ViewChild(MatSelect) select: MatSelect;
  options = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
  ];
  formControl = new FormControl('', Validators.required);
  formGroup = new FormGroup({
    food: this.formControl,
  });
}

@Component({
  template: `
    <mat-form-field>
      <mat-select placeholder="Food" [(value)]="selectedFood">
        @for (food of foods; track food) {
          <mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
        }
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class BasicSelectWithoutForms {
  selectedFood: string | null;
  foods: any[] = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
    {value: 'sandwich-2', viewValue: 'Sandwich'},
  ];

  @ViewChild(MatSelect) select: MatSelect;
}

@Component({
  template: `
    <mat-form-field>
      <mat-select placeholder="Food" [(value)]="selectedFood">
        @for (food of foods; track food) {
          <mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
        }
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class BasicSelectWithoutFormsPreselected {
  selectedFood = 'pizza-1';
  foods: any[] = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
  ];

  @ViewChild(MatSelect) select: MatSelect;
}

@Component({
  template: `
    <mat-form-field>
      <mat-select placeholder="Food" [(value)]="selectedFoods" multiple>
        @for (food of foods; track food) {
          <mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
        }
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class BasicSelectWithoutFormsMultiple {
  selectedFoods: string[];
  foods: any[] = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
    {value: 'sandwich-2', viewValue: 'Sandwich'},
  ];

  @ViewChild(MatSelect) select: MatSelect;
}

@Component({
  selector: 'select-with-custom-trigger',
  template: `
    <mat-form-field>
      <mat-select placeholder="Food" [formControl]="control" #select="matSelect">
        <mat-select-trigger>
          {{ select.selected?.viewValue.split('').reverse().join('') }}
        </mat-select-trigger>
        @for (food of foods; track food) {
          <mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
        }
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class SelectWithCustomTrigger {
  foods: any[] = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
  ];
  control = new FormControl('');
}

@Component({
  selector: 'ng-model-compare-with',
  template: `
    <mat-form-field>
      <mat-select [ngModel]="selectedFood" (ngModelChange)="setFoodByCopy($event)"
                 [compareWith]="comparator">
        @for (food of foods; track food) {
          <mat-option [value]="food">{{ food.viewValue }}</mat-option>
        }
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class NgModelCompareWithSelect {
  foods: {value: string; viewValue: string}[] = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
    {value: 'tacos-2', viewValue: 'Tacos'},
  ];
  selectedFood: {value: string; viewValue: string} = {value: 'pizza-1', viewValue: 'Pizza'};
  comparator: ((f1: any, f2: any) => boolean) | null = this.compareByValue;

  @ViewChild(MatSelect) select: MatSelect;
  @ViewChildren(MatOption) options: QueryList<MatOption>;

  useCompareByValue() {
    this.comparator = this.compareByValue;
  }

  useCompareByReference() {
    this.comparator = this.compareByReference;
  }

  useNullComparator() {
    this.comparator = null;
  }

  compareByValue(f1: any, f2: any) {
    return f1 && f2 && f1.value === f2.value;
  }

  compareByReference(f1: any, f2: any) {
    return f1 === f2;
  }

  setFoodByCopy(newValue: {value: string; viewValue: string}) {
    this.selectedFood = {...{}, ...newValue};
  }
}

@Component({
  template: `
    <mat-select placeholder="Food" [formControl]="control" [errorStateMatcher]="errorStateMatcher">
      @for (food of foods; track food) {
        <mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
      }
    </mat-select>
  `,
  standalone: false,
})
class CustomErrorBehaviorSelect {
  @ViewChild(MatSelect) select: MatSelect;
  control = new FormControl('');
  foods: any[] = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
  ];
  errorStateMatcher: ErrorStateMatcher;
}

@Component({
  template: `
    <mat-form-field>
      <mat-select placeholder="Food" [(ngModel)]="selectedFoods">
        @for (food of foods; track food) {
          <mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
        }
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class SingleSelectWithPreselectedArrayValues {
  foods: any[] = [
    {value: ['steak-0', 'steak-1'], viewValue: 'Steak'},
    {value: ['pizza-1', 'pizza-2'], viewValue: 'Pizza'},
    {value: ['tacos-2', 'tacos-3'], viewValue: 'Tacos'},
  ];

  selectedFoods = this.foods[1].value;

  @ViewChild(MatSelect) select: MatSelect;
  @ViewChildren(MatOption) options: QueryList<MatOption>;
}

@Component({
  selector: 'select-without-option-centering',
  template: `
    <mat-form-field>
      <mat-select placeholder="Food" [formControl]="control" disableOptionCentering>
        @for (food of foods; track food) {
          <mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
        }
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class SelectWithoutOptionCentering {
  foods: any[] = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
    {value: 'tacos-2', viewValue: 'Tacos'},
    {value: 'sandwich-3', viewValue: 'Sandwich'},
    {value: 'chips-4', viewValue: 'Chips'},
    {value: 'eggs-5', viewValue: 'Eggs'},
    {value: 'pasta-6', viewValue: 'Pasta'},
    {value: 'sushi-7', viewValue: 'Sushi'},
  ];
  control = new FormControl('pizza-1');

  @ViewChild(MatSelect) select: MatSelect;
  @ViewChildren(MatOption) options: QueryList<MatOption>;
}

@Component({
  template: `
    <mat-form-field>
      <mat-label>Select a thing</mat-label>

      <mat-select [placeholder]="placeholder">
        <mat-option value="thing">A thing</mat-option>
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class SelectWithFormFieldLabel {
  placeholder: string;
}

@Component({
  template: `
    <mat-form-field appearance="fill">
      <mat-label>Select something</mat-label>
      @if (showSelect) {
        <mat-select>
          <mat-option value="1">One</mat-option>
        </mat-select>
      }
    </mat-form-field>
  `,
  standalone: false,
})
class SelectWithNgIfAndLabel {
  showSelect = true;
}

@Component({
  template: `
    <mat-form-field>
      <mat-select multiple [ngModel]="value">
        @for (item of items; track item) {
          <mat-option [value]="item">{{item}}</mat-option>
        }
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class MultiSelectWithLotsOfOptions {
  items = new Array(100).fill(0).map((_, i) => i);
  value: number[] = [];

  checkAll() {
    this.value = [...this.items];
  }

  uncheckAll() {
    this.value = [];
  }
}

@Component({
  selector: 'basic-select-with-reset',
  template: `
    <mat-form-field>
      <mat-select [formControl]="control">
        <mat-option>Reset</mat-option>
        <mat-option value="a">A</mat-option>
        <mat-option value="b">B</mat-option>
        <mat-option value="c">C</mat-option>
      </mat-select>
    </mat-form-field>
  `,
  standalone: false,
})
class SelectWithResetOptionAndFormControl {
  @ViewChild(MatSelect) select: MatSelect;
  @ViewChildren(MatOption) options: QueryList<MatOption>;
  control = new FormControl('');
}

@Component({
  selector: 'select-with-placeholder-in-ngcontainer-with-ngIf',
  template: `
    <mat-form-field>
      @if (true) {
        <mat-select placeholder="Product Area">
          <mat-option value="a">A</mat-option>
          <mat-option value="b">B</mat-option>
          <mat-option value="c">C</mat-option>
        </mat-select>
      }
    </mat-form-field>
  `,
  standalone: false,
})
class SelectInNgContainer {}

@Component({
  template: `
    <form [formGroup]="form">
      <mat-form-field>
        <mat-select formControlName="control">
          <mat-option value="1">One</mat-option>
        </mat-select>
      </mat-form-field>
    </form>
  `,
  standalone: false,
})
class SelectInsideDynamicFormGroup {
  private _formBuilder = inject(FormBuilder);

  @ViewChild(MatSelect) select: MatSelect;
  form: FormGroup;

  private readonly _changeDetectorRef = inject(ChangeDetectorRef);

  constructor() {
    this.assignGroup(false);
  }

  assignGroup(isDisabled: boolean) {
    this.form = this._formBuilder.group({
      control: {value: '', disabled: isDisabled},
    });
    this._changeDetectorRef.markForCheck();
  }
}
@Component({
  selector: 'basic-select',
  template: `
    <div [style.height.px]="heightAbove"></div>
    <mat-form-field>
      @if (hasLabel) {
        <mat-label>Select a food</mat-label>
      }
      <mat-select placeholder="Food" [formControl]="control" [required]="isRequired"
        [tabIndex]="tabIndexOverride" [aria-describedby]="ariaDescribedBy"
        [aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby"
        [panelClass]="panelClass" [disableRipple]="disableRipple"
        [typeaheadDebounceInterval]="typeaheadDebounceInterval">
        @for (food of foods; track food) {
          <mat-option [value]="food.value" [disabled]="food.disabled">
            {{ food.viewValue }}
          </mat-option>
        }
      </mat-select>
      @if (hint) {
        <mat-hint>{{ hint }}</mat-hint>
      }
    </mat-form-field>
    <div [style.height.px]="heightBelow"></div>
  `,
  standalone: false,
})
class BasicSelectWithFirstAndLastOptionDisabled {
  foods: any[] = [
    {value: 'steak-0', viewValue: 'Steak', disabled: true},
    {value: 'pizza-1', viewValue: 'Pizza'},
    {value: 'tacos-2', viewValue: 'Tacos'},
    {value: 'sandwich-3', viewValue: 'Sandwich'},
    {value: 'chips-4', viewValue: 'Chips'},
    {value: 'eggs-5', viewValue: 'Eggs'},
    {value: 'pasta-6', viewValue: 'Pasta'},
    {value: 'sushi-7', viewValue: 'Sushi', disabled: true},
  ];
  control = new FormControl<string | null>(null);
  isRequired: boolean;
  heightAbove = 0;
  heightBelow = 0;
  hasLabel = true;
  hint: string;
  tabIndexOverride: number;
  ariaDescribedBy: string;
  ariaLabel: string;
  ariaLabelledby: string;
  panelClass = ['custom-one', 'custom-two'];
  disableRipple: boolean;
  typeaheadDebounceInterval: number;

  @ViewChild(MatSelect, {static: true}) select: MatSelect;
  @ViewChildren(MatOption) options: QueryList<MatOption>;
}

@Component({
  selector: 'select-inside-a-modal',
  template: `
    <button cdkOverlayOrigin #trigger="cdkOverlayOrigin">open dialog</button>
    <ng-template cdkConnectedOverlay [cdkConnectedOverlayOpen]="true"
      [cdkConnectedOverlayOrigin]="trigger">
      <div role="dialog" [attr.aria-modal]="'true'" #modal>
        <mat-form-field>
          <mat-label>Select a food</mat-label>
          <mat-select placeholder="Food" ngModel>
            @for (food of foods; track food) {
              <mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
            }
          </mat-select>
        </mat-form-field>
      </div>
    </ng-template>
  `,
  standalone: false,
})
class SelectInsideAModal {
  foods = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
    {value: 'tacos-2', viewValue: 'Tacos'},
  ];

  @ViewChild(MatSelect) select: MatSelect;
  @ViewChildren(MatOption) options: QueryList<MatOption>;
  @ViewChild('modal') modal: ElementRef;
}
