import {A, D, DOWN_ARROW, END, ENTER, HOME, SPACE, UP_ARROW} from '@angular/cdk/keycodes';
import {
  createKeyboardEvent,
  dispatchEvent,
  dispatchFakeEvent,
  dispatchKeyboardEvent,
  dispatchMouseEvent,
} from '@angular/cdk/testing/private';
import {
  ChangeDetectionStrategy,
  Component,
  DebugElement,
  QueryList,
  ViewChildren,
} from '@angular/core';
import {
  ComponentFixture,
  TestBed,
  fakeAsync,
  flush,
  tick,
  waitForAsync,
} from '@angular/core/testing';
import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms';
import {ThemePalette} from '@angular/material/core';
import {By} from '@angular/platform-browser';
import {
  MAT_LIST_CONFIG,
  MatListConfig,
  MatListModule,
  MatListOption,
  MatListOptionTogglePosition,
  MatSelectionList,
  MatSelectionListChange,
} from './index';

describe('MatSelectionList without forms', () => {
  const typeaheadInterval = 200;

  describe('with list option', () => {
    let fixture: ComponentFixture<SelectionListWithListOptions>;
    let listOptions: DebugElement[];
    let selectionList: DebugElement;

    beforeEach(waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [
          MatListModule,
          SelectionListWithListOptions,
          SelectionListWithCheckboxPositionAfter,
          SelectionListWithListDisabled,
          SelectionListWithOnlyOneOption,
          SelectionListWithIndirectChildOptions,
          SelectionListWithSelectedOptionAndValue,
        ],
      });
    }));

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

      listOptions = fixture.debugElement.queryAll(By.directive(MatListOption));
      selectionList = fixture.debugElement.query(By.directive(MatSelectionList))!;
    }));

    function getFocusIndex() {
      return listOptions.findIndex(o => document.activeElement === o.nativeElement);
    }

    it('should be able to set a value on a list option', () => {
      const optionValues = ['inbox', 'starred', 'sent-mail', 'archive', 'drafts'];

      optionValues.forEach((optionValue, index) => {
        expect(listOptions[index].componentInstance.value).toBe(optionValue);
      });
    });

    it('should not emit a selectionChange event if an option changed programmatically', () => {
      spyOn(fixture.componentInstance, 'onSelectionChange');

      expect(fixture.componentInstance.onSelectionChange).toHaveBeenCalledTimes(0);

      listOptions[2].componentInstance.toggle();
      fixture.detectChanges();

      expect(fixture.componentInstance.onSelectionChange).toHaveBeenCalledTimes(0);
    });

    it('should emit a selectionChange event if an option got clicked', () => {
      spyOn(fixture.componentInstance, 'onSelectionChange');

      expect(fixture.componentInstance.onSelectionChange).toHaveBeenCalledTimes(0);

      dispatchMouseEvent(listOptions[2].nativeElement, 'click');
      fixture.detectChanges();

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

    it('should be able to dispatch one selected item', () => {
      let testListItem = listOptions[2].injector.get<MatListOption>(MatListOption);
      let selectList =
        selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;

      expect(selectList.selected.length).toBe(0);
      expect(listOptions[2].nativeElement.getAttribute('aria-selected')).toBe('false');

      testListItem.toggle();
      fixture.detectChanges();

      expect(listOptions[2].nativeElement.getAttribute('aria-selected')).toBe('true');
      expect(listOptions[2].nativeElement.getAttribute('aria-disabled')).toBe('false');
      expect(selectList.selected.length).toBe(1);
    });

    it('should be able to dispatch multiple selected items', () => {
      let testListItem = listOptions[2].injector.get<MatListOption>(MatListOption);
      let testListItem2 = listOptions[1].injector.get<MatListOption>(MatListOption);
      let selectList =
        selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;

      expect(selectList.selected.length).toBe(0);
      expect(listOptions[2].nativeElement.getAttribute('aria-selected')).toBe('false');
      expect(listOptions[1].nativeElement.getAttribute('aria-selected')).toBe('false');

      testListItem.toggle();
      fixture.detectChanges();

      testListItem2.toggle();
      fixture.detectChanges();

      expect(selectList.selected.length).toBe(2);
      expect(listOptions[2].nativeElement.getAttribute('aria-selected')).toBe('true');
      expect(listOptions[1].nativeElement.getAttribute('aria-selected')).toBe('true');
      expect(listOptions[1].nativeElement.getAttribute('aria-disabled')).toBe('false');
      expect(listOptions[2].nativeElement.getAttribute('aria-disabled')).toBe('false');
    });

    it('should be able to specify a color for list options', () => {
      const optionNativeElements = listOptions.map(option => option.nativeElement);

      expect(optionNativeElements.every(option => !option.classList.contains('mat-primary'))).toBe(
        true,
      );
      expect(optionNativeElements.every(option => !option.classList.contains('mat-warn'))).toBe(
        true,
      );

      // All options will be set to the "warn" color.
      fixture.componentInstance.selectionListColor = 'warn';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(optionNativeElements.every(option => !option.classList.contains('mat-primary'))).toBe(
        true,
      );
      expect(optionNativeElements.every(option => option.classList.contains('mat-warn'))).toBe(
        true,
      );

      // Color will be set explicitly for an option and should take precedence.
      fixture.componentInstance.firstOptionColor = 'primary';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(optionNativeElements[0].classList).not.toContain('mat-accent');
      expect(optionNativeElements[0].classList).not.toContain('mat-warn');
      expect(
        optionNativeElements.slice(1).every(option => option.classList.contains('mat-warn')),
      ).toBe(true);
    });

    it('should explicitly set the `accent` color', () => {
      const classList = listOptions[0].nativeElement.classList;

      fixture.componentInstance.firstOptionColor = 'primary';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(classList).not.toContain('mat-accent');
      expect(classList).not.toContain('mat-warn');

      fixture.componentInstance.firstOptionColor = 'accent';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(classList).not.toContain('mat-primary');
      expect(classList).toContain('mat-accent');
      expect(classList).not.toContain('mat-warn');
    });

    it('should be able to deselect an option', () => {
      let testListItem = listOptions[2].injector.get<MatListOption>(MatListOption);
      let selectList =
        selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;

      expect(selectList.selected.length).toBe(0);

      testListItem.toggle();
      fixture.detectChanges();

      expect(selectList.selected.length).toBe(1);

      testListItem.toggle();
      fixture.detectChanges();

      expect(selectList.selected.length).toBe(0);
    });

    it('should not add the mdc-list-item--selected class (in multiple mode)', () => {
      let testListItem = listOptions[2].injector.get<MatListOption>(MatListOption);

      dispatchMouseEvent(testListItem._hostElement, 'click');
      fixture.detectChanges();

      expect(listOptions[2].nativeElement.classList.contains('mdc-list-item--selected')).toBe(
        false,
      );
    });

    it('should not allow selection of disabled items', () => {
      let testListItem = listOptions[0].injector.get<MatListOption>(MatListOption);
      let selectList =
        selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;

      expect(selectList.selected.length).withContext('before click').toBe(0);
      expect(listOptions[0].nativeElement.getAttribute('aria-disabled')).toBe('true');

      dispatchMouseEvent(testListItem._hostElement, 'click');
      fixture.detectChanges();

      expect(selectList.selected.length).withContext('after click').toBe(0);
    });

    it('should be able to un-disable disabled items', () => {
      let testListItem = listOptions[0].injector.get<MatListOption>(MatListOption);

      expect(listOptions[0].nativeElement.getAttribute('aria-disabled')).toBe('true');

      testListItem.disabled = false;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(listOptions[0].nativeElement.getAttribute('aria-disabled')).toBe('false');
    });

    it('should be able to use keyboard select with SPACE', () => {
      const testListItem = listOptions[1].nativeElement as HTMLElement;
      const selectList =
        selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;
      expect(selectList.selected.length).toBe(0);

      testListItem.focus();
      expect(getFocusIndex()).toBe(1);

      const event = dispatchKeyboardEvent(testListItem, 'keydown', SPACE);
      fixture.detectChanges();

      expect(selectList.selected.length).toBe(1);
      expect(event.defaultPrevented).toBe(true);
    });

    it('should be able to select an item using ENTER', () => {
      const testListItem = listOptions[1].nativeElement as HTMLElement;
      const selectList =
        selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;
      expect(selectList.selected.length).toBe(0);

      testListItem.focus();
      expect(getFocusIndex()).toBe(1);

      const event = dispatchKeyboardEvent(testListItem, 'keydown', ENTER);
      fixture.detectChanges();

      expect(selectList.selected.length).toBe(1);
      expect(event.defaultPrevented).toBe(true);
    });

    it('should not be able to toggle a disabled option using SPACE', () => {
      const testListItem = listOptions[1].nativeElement as HTMLElement;
      const selectionModel = selectionList.componentInstance.selectedOptions;

      expect(selectionModel.selected.length).toBe(0);

      listOptions[1].componentInstance.disabled = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      testListItem.focus();
      expect(getFocusIndex()).toBe(1);

      dispatchKeyboardEvent(testListItem, 'keydown', SPACE);
      fixture.detectChanges();

      expect(selectionModel.selected.length).toBe(0);
    });

    it('should focus the first option when the list takes focus for the first time', () => {
      expect(listOptions[0].nativeElement.tabIndex).toBe(0);
      expect(listOptions.slice(1).every(o => o.nativeElement.tabIndex === -1)).toBe(true);
    });

    it('should focus the first selected option when list receives focus', fakeAsync(() => {
      dispatchMouseEvent(listOptions[2].nativeElement, 'click');
      fixture.detectChanges();

      expect(listOptions.map(o => o.nativeElement.tabIndex)).toEqual([-1, -1, 0, -1, -1]);

      dispatchMouseEvent(listOptions[1].nativeElement, 'click');
      fixture.detectChanges();

      expect(listOptions.map(o => o.nativeElement.tabIndex)).toEqual([-1, 0, -1, -1, -1]);

      // De-select both options to ensure that the first item in the list-item
      // becomes the designated option for focus.
      dispatchMouseEvent(listOptions[1].nativeElement, 'click');
      dispatchMouseEvent(listOptions[2].nativeElement, 'click');
      fixture.detectChanges();

      expect(listOptions.map(o => o.nativeElement.tabIndex)).toEqual([0, -1, -1, -1, -1]);
    }));

    it('should focus previous item when press UP ARROW', () => {
      listOptions[2].nativeElement.focus();
      expect(getFocusIndex()).toEqual(2);

      dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', UP_ARROW);
      fixture.detectChanges();

      expect(getFocusIndex()).toEqual(1);
    });

    it('should focus next item when press DOWN ARROW', () => {
      listOptions[2].nativeElement.focus();
      expect(getFocusIndex()).toEqual(2);

      dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', DOWN_ARROW);
      fixture.detectChanges();

      expect(getFocusIndex()).toEqual(3);
    });

    it('should be able to focus the first item when pressing HOME', () => {
      listOptions[2].nativeElement.focus();
      expect(getFocusIndex()).toBe(2);

      const event = dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', HOME);
      fixture.detectChanges();

      expect(getFocusIndex()).toBe(0);
      expect(event.defaultPrevented).toBe(true);
    });

    it('should focus the last item when pressing END', () => {
      listOptions[2].nativeElement.focus();
      expect(getFocusIndex()).toBe(2);

      const event = dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', END);
      fixture.detectChanges();

      expect(getFocusIndex()).toBe(4);
      expect(event.defaultPrevented).toBe(true);
    });

    it('should select all items using ctrl + a', () => {
      listOptions.forEach(option => (option.componentInstance.disabled = false));
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(listOptions.some(option => option.componentInstance.selected)).toBe(false);

      listOptions[2].nativeElement.focus();
      dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', A, 'A', {control: true});
      fixture.detectChanges();

      expect(listOptions.every(option => option.componentInstance.selected)).toBe(true);
    });

    it('should not select disabled items when pressing ctrl + a', () => {
      listOptions.slice(0, 2).forEach(option => (option.componentInstance.disabled = true));
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(listOptions.map(option => option.componentInstance.selected)).toEqual([
        false,
        false,
        false,
        false,
        false,
      ]);

      listOptions[3].nativeElement.focus();
      dispatchKeyboardEvent(listOptions[3].nativeElement, 'keydown', A, 'A', {control: true});
      fixture.detectChanges();

      expect(listOptions.map(option => option.componentInstance.selected)).toEqual([
        false,
        false,
        true,
        true,
        true,
      ]);
    });

    it('should select all items using ctrl + a if some items are selected', () => {
      listOptions.slice(0, 2).forEach(option => (option.componentInstance.selected = true));
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(listOptions.some(option => option.componentInstance.selected)).toBe(true);

      listOptions[2].nativeElement.focus();
      dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', A, 'A', {control: true});
      fixture.detectChanges();

      expect(listOptions.every(option => option.componentInstance.selected)).toBe(true);
    });

    it('should deselect all with ctrl + a if all options are selected', () => {
      listOptions.forEach(option => (option.componentInstance.selected = true));
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(listOptions.every(option => option.componentInstance.selected)).toBe(true);

      listOptions[2].nativeElement.focus();
      dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', A, 'A', {control: true});
      fixture.detectChanges();

      expect(listOptions.every(option => option.componentInstance.selected)).toBe(false);
    });

    it('should dispatch the selectionChange event when selecting via ctrl + a', () => {
      const spy = spyOn(fixture.componentInstance, 'onSelectionChange');
      listOptions.forEach(option => (option.componentInstance.disabled = false));
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      listOptions[2].nativeElement.focus();
      dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', A, 'A', {control: true});
      fixture.detectChanges();

      expect(spy).toHaveBeenCalledTimes(1);
      expect(spy).toHaveBeenCalledWith(
        jasmine.objectContaining({
          options: listOptions.map(option => option.componentInstance),
        }),
      );
    });

    it('should be able to jump focus down to an item by typing', fakeAsync(() => {
      const firstOption = listOptions[0].nativeElement;

      firstOption.focus();
      expect(getFocusIndex()).toBe(0);

      dispatchEvent(firstOption, createKeyboardEvent('keydown', 83, 's'));
      fixture.detectChanges();
      tick(typeaheadInterval);

      expect(getFocusIndex()).toBe(1);

      dispatchEvent(firstOption, createKeyboardEvent('keydown', 68, 'd'));
      fixture.detectChanges();
      tick(typeaheadInterval);

      expect(getFocusIndex()).toBe(4);
    }));

    it('should be able to skip to an item by typing', fakeAsync(() => {
      listOptions[0].nativeElement.focus();
      expect(getFocusIndex()).toBe(0);

      dispatchKeyboardEvent(listOptions[0].nativeElement, 'keydown', D, 'd');
      fixture.detectChanges();
      tick(typeaheadInterval);

      expect(getFocusIndex()).toBe(4);
    }));

    // Test for "A" specifically, because it's a special case that can be used
    // to select all values.
    it('should be able to skip to an item by typing the letter "A"', fakeAsync(() => {
      listOptions[0].nativeElement.focus();
      expect(getFocusIndex()).toBe(0);

      dispatchKeyboardEvent(listOptions[0].nativeElement, 'keydown', A, 'a');
      fixture.detectChanges();
      tick(typeaheadInterval);

      expect(getFocusIndex()).toBe(3);
    }));

    it('should not select items while using the typeahead', fakeAsync(() => {
      const testListItem = listOptions[1].nativeElement as HTMLElement;
      const model = selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;

      testListItem.focus();
      dispatchFakeEvent(testListItem, 'focus');
      fixture.detectChanges();

      expect(getFocusIndex()).toBe(1);
      expect(model.isEmpty()).toBe(true);

      dispatchKeyboardEvent(testListItem, 'keydown', D, 'd');
      fixture.detectChanges();
      tick(typeaheadInterval / 2); // Tick only half the typeahead timeout.

      dispatchKeyboardEvent(testListItem, 'keydown', SPACE);
      fixture.detectChanges();
      // Tick the buffer timeout again as a new key has been pressed that resets
      // the buffer timeout.
      tick(typeaheadInterval);

      expect(getFocusIndex()).toBe(4);
      expect(model.isEmpty()).toBe(true);
    }));

    it('should be able to select all options', () => {
      const list: MatSelectionList = selectionList.componentInstance;

      expect(list.options.toArray().every(option => option.selected)).toBe(false);

      const result = list.selectAll();
      fixture.detectChanges();

      expect(list.options.toArray().every(option => option.selected)).toBe(true);
      expect(result).toEqual(list.options.toArray());
    });

    it('should be able to select all options, even if they are disabled', () => {
      const list: MatSelectionList = selectionList.componentInstance;

      list.options.forEach(option => (option.disabled = true));
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(list.options.toArray().every(option => option.selected)).toBe(false);

      list.selectAll();
      fixture.detectChanges();

      expect(list.options.toArray().every(option => option.selected)).toBe(true);
    });

    it('should be able to deselect all options', () => {
      const list: MatSelectionList = selectionList.componentInstance;

      list.options.forEach(option => option.toggle());
      expect(list.options.toArray().every(option => option.selected)).toBe(true);

      const result = list.deselectAll();
      fixture.detectChanges();

      expect(list.options.toArray().every(option => option.selected)).toBe(false);
      expect(result).toEqual(list.options.toArray());
    });

    it('should be able to deselect all options, even if they are disabled', () => {
      const list: MatSelectionList = selectionList.componentInstance;

      list.options.forEach(option => option.toggle());
      expect(list.options.toArray().every(option => option.selected)).toBe(true);

      list.options.forEach(option => (option.disabled = true));
      fixture.detectChanges();

      list.deselectAll();
      fixture.detectChanges();

      expect(list.options.toArray().every(option => option.selected)).toBe(false);
    });

    it('should update the list value when an item is selected programmatically', () => {
      const list: MatSelectionList = selectionList.componentInstance;

      expect(list.selectedOptions.isEmpty()).toBe(true);

      listOptions[0].componentInstance.selected = true;
      listOptions[2].componentInstance.selected = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(list.selectedOptions.isEmpty()).toBe(false);
      expect(list.selectedOptions.isSelected(listOptions[0].componentInstance)).toBe(true);
      expect(list.selectedOptions.isSelected(listOptions[2].componentInstance)).toBe(true);
    });

    it('should update the item selected state when it is selected via the model', () => {
      const list: MatSelectionList = selectionList.componentInstance;
      const item: MatListOption = listOptions[0].componentInstance;

      expect(item.selected).toBe(false);

      list.selectedOptions.select(item);
      fixture.detectChanges();

      expect(item.selected).toBe(true);
    });

    it('should set aria-multiselectable to true on the selection list element', () => {
      expect(selectionList.nativeElement.getAttribute('aria-multiselectable')).toBe('true');
    });

    it('should be able to reach list options that are indirect descendants', () => {
      const descendatsFixture = TestBed.createComponent(SelectionListWithIndirectChildOptions);
      descendatsFixture.detectChanges();
      listOptions = descendatsFixture.debugElement.queryAll(By.directive(MatListOption));
      selectionList = descendatsFixture.debugElement.query(By.directive(MatSelectionList))!;
      const list: MatSelectionList = selectionList.componentInstance;

      expect(list.options.toArray().every(option => option.selected)).toBe(false);

      list.selectAll();
      descendatsFixture.detectChanges();

      expect(list.options.toArray().every(option => option.selected)).toBe(true);
    });

    it('should disable list item ripples when the ripples on the list have been disabled', fakeAsync(() => {
      const rippleTarget = fixture.nativeElement.querySelector(
        '.mat-mdc-list-option:not(.mdc-list-item--disabled)',
      );
      dispatchMouseEvent(rippleTarget, 'mousedown');
      dispatchMouseEvent(rippleTarget, 'mouseup');

      // Flush the ripple enter animation.
      dispatchFakeEvent(rippleTarget.querySelector('.mat-ripple-element')!, 'transitionend');

      expect(rippleTarget.querySelectorAll('.mat-ripple-element').length)
        .withContext('Expected ripples to be enabled by default.')
        .toBe(1);

      // Flush the ripple exit animation.
      dispatchFakeEvent(rippleTarget.querySelector('.mat-ripple-element')!, 'transitionend');

      expect(rippleTarget.querySelectorAll('.mat-ripple-element').length)
        .withContext('Expected ripples to go away.')
        .toBe(0);

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

      dispatchMouseEvent(rippleTarget, 'mousedown');
      dispatchMouseEvent(rippleTarget, 'mouseup');

      expect(rippleTarget.querySelectorAll('.mat-ripple-element').length)
        .withContext('Expected no ripples after list ripples are disabled.')
        .toBe(0);
    }));

    it('can bind both selected and value at the same time', () => {
      const componentFixture = TestBed.createComponent(SelectionListWithSelectedOptionAndValue);
      componentFixture.detectChanges();
      const listItemEl = componentFixture.debugElement.query(By.directive(MatListOption))!;
      expect(listItemEl.componentInstance.selected).toBe(true);
      expect(listItemEl.componentInstance.value).toBe(componentFixture.componentInstance.itemValue);
    });

    it('should have a focus indicator', () => {
      const optionNativeElements = listOptions.map(option => option.nativeElement as HTMLElement);

      expect(
        optionNativeElements.every(
          element => element.querySelector('.mat-focus-indicator') !== null,
        ),
      ).toBe(true);
    });

    it('should hide the internal SVG', () => {
      listOptions.forEach(option => {
        const svg = option.nativeElement.querySelector('.mdc-checkbox svg');
        expect(svg.getAttribute('aria-hidden')).toBe('true');
      });
    });
  });

  describe('multiple-selection with list option selected', () => {
    let fixture: ComponentFixture<SelectionListWithSelectedOption>;
    let listOptionElements: DebugElement[];
    let selectionList: DebugElement;

    beforeEach(waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [MatListModule, SelectionListWithSelectedOption],
      });
    }));

    beforeEach(waitForAsync(() => {
      fixture = TestBed.createComponent(SelectionListWithSelectedOption);
      listOptionElements = fixture.debugElement.queryAll(By.directive(MatListOption))!;
      selectionList = fixture.debugElement.query(By.directive(MatSelectionList))!;
      fixture.detectChanges();
    }));

    it('should set its initial selected state in the selectedOptions', () => {
      let options = listOptionElements.map(optionEl =>
        optionEl.injector.get<MatListOption>(MatListOption),
      );
      let selectedOptions = selectionList.componentInstance.selectedOptions;
      expect(selectedOptions.isSelected(options[0])).toBeFalse();
      expect(selectedOptions.isSelected(options[1])).toBeTrue();
      expect(selectedOptions.isSelected(options[2])).toBeTrue();
      expect(selectedOptions.isSelected(options[3])).toBeFalse();
    });

    it('should focus the first selected option on first focus if an item is pre-selected', fakeAsync(() => {
      // MDC manages the focus through setting a `tabindex` on the designated list item. We
      // assert that the proper tabindex is set on the pre-selected option at index 1, and
      // ensure that other options are not reachable through tab.
      expect(listOptionElements.map(el => el.nativeElement.tabIndex)).toEqual([-1, 0, -1, -1]);
    }));
  });

  describe('single-selection with list option selected', () => {
    let fixture: ComponentFixture<SingleSelectionListWithSelectedOption>;
    let listOptionElements: DebugElement[];

    beforeEach(waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [MatListModule, SingleSelectionListWithSelectedOption],
      });
    }));

    beforeEach(waitForAsync(() => {
      fixture = TestBed.createComponent(SingleSelectionListWithSelectedOption);
      listOptionElements = fixture.debugElement.queryAll(By.directive(MatListOption))!;
      fixture.detectChanges();
    }));

    it('displays radio indicators by default', () => {
      expect(
        listOptionElements[0].nativeElement.querySelector('input[type="radio"]'),
      ).not.toBeNull();
      expect(
        listOptionElements[1].nativeElement.querySelector('input[type="radio"]'),
      ).not.toBeNull();

      expect(listOptionElements[0].nativeElement.classList).not.toContain(
        'mdc-list-item--selected',
      );
      expect(listOptionElements[1].nativeElement.classList).not.toContain(
        'mdc-list-item--selected',
      );
    });
  });

  describe('with token to hide radio indicators', () => {
    let fixture: ComponentFixture<SingleSelectionListWithSelectedOption>;
    let listOptionElements: DebugElement[];

    beforeEach(waitForAsync(() => {
      const matListConfig: MatListConfig = {hideSingleSelectionIndicator: true};

      TestBed.configureTestingModule({
        imports: [MatListModule, SingleSelectionListWithSelectedOption],
        providers: [{provide: MAT_LIST_CONFIG, useValue: matListConfig}],
      });
    }));

    beforeEach(waitForAsync(() => {
      fixture = TestBed.createComponent(SingleSelectionListWithSelectedOption);
      listOptionElements = fixture.debugElement.queryAll(By.directive(MatListOption))!;
      fixture.detectChanges();
    }));

    it('does not display radio indicators', () => {
      expect(listOptionElements[0].nativeElement.querySelector('input[type="radio"]')).toBeNull();
      expect(listOptionElements[1].nativeElement.querySelector('input[type="radio"]')).toBeNull();

      expect(listOptionElements[0].nativeElement.classList).not.toContain(
        'mdc-list-item--selected',
      );

      expect(listOptionElements[1].nativeElement.getAttribute('aria-selected'))
        .withContext('Expected second option to be selected')
        .toBe('true');
      expect(listOptionElements[1].nativeElement.classList).toContain('mdc-list-item--selected');
    });
  });

  describe('with option disabled', () => {
    let fixture: ComponentFixture<SelectionListWithDisabledOption>;
    let listOptionEl: HTMLElement;
    let listOption: MatListOption;

    beforeEach(waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [MatListModule, SelectionListWithDisabledOption],
      });
    }));

    beforeEach(waitForAsync(() => {
      fixture = TestBed.createComponent(SelectionListWithDisabledOption);

      const listOptionDebug = fixture.debugElement.query(By.directive(MatListOption))!;

      listOption = listOptionDebug.componentInstance;
      listOptionEl = listOptionDebug.nativeElement;

      fixture.detectChanges();
    }));

    it('should disable ripples for disabled option', () => {
      expect(listOption.rippleDisabled)
        .withContext('Expected ripples to be enabled by default')
        .toBe(false);

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

      expect(listOption.rippleDisabled)
        .withContext('Expected ripples to be disabled if option is disabled')
        .toBe(true);
    });

    it('should apply the "mat-list-item-disabled" class properly', () => {
      expect(listOptionEl.classList).not.toContain('mdc-list-item--disabled');

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

      expect(listOptionEl.classList).toContain('mdc-list-item--disabled');
    });
  });

  describe('with list disabled', () => {
    let fixture: ComponentFixture<SelectionListWithListDisabled>;
    let listOption: DebugElement[];
    let selectionList: DebugElement;

    beforeEach(waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [
          MatListModule,
          SelectionListWithListOptions,
          SelectionListWithCheckboxPositionAfter,
          SelectionListWithListDisabled,
          SelectionListWithOnlyOneOption,
        ],
      });
    }));

    beforeEach(waitForAsync(() => {
      fixture = TestBed.createComponent(SelectionListWithListDisabled);
      listOption = fixture.debugElement.queryAll(By.directive(MatListOption));
      selectionList = fixture.debugElement.query(By.directive(MatSelectionList))!;
      fixture.detectChanges();
    }));

    it('should not allow selection on disabled selection-list', () => {
      let testListItem = listOption[2].injector.get<MatListOption>(MatListOption);
      let selectList =
        selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;

      expect(selectList.selected.length).toBe(0);

      dispatchMouseEvent(testListItem._hostElement, 'click');
      fixture.detectChanges();

      expect(selectList.selected.length).toBe(0);
    });

    it('should update state of options if list state has changed', () => {
      const testOption = listOption[2].componentInstance as MatListOption;

      expect(testOption.rippleDisabled)
        .withContext('Expected ripples of list option to be disabled')
        .toBe(true);

      fixture.componentInstance.disabled = false;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(testOption.rippleDisabled)
        .withContext('Expected ripples of list option to be enabled')
        .toBe(false);
    });

    // when the entire list is disabled, its listitems should always have tabindex="-1"
    it('should remove all listitems from the tab order when disabled state is enabled', () => {
      fixture.componentInstance.disabled = false;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      let testListItem = listOption[2].injector.get<MatListOption>(MatListOption);
      testListItem.focus();
      fixture.detectChanges();

      expect(
        listOption.filter(option => option.nativeElement.getAttribute('tabindex') === '0').length,
      )
        .withContext('Expected at least one list option to be in the tab order')
        .toBeGreaterThanOrEqual(1);

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

      expect(
        listOption.filter(option => option.nativeElement.getAttribute('tabindex') !== '-1').length,
      )
        .withContext('Expected all list options to be excluded from the tab order')
        .toBe(0);
    });

    it('should not allow focusin event to change the tabindex', () => {
      fixture.componentInstance.disabled = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(
        listOption.filter(option => option.nativeElement.getAttribute('tabindex') !== '-1').length,
      )
        .withContext('Expected all list options to be excluded from the tab order')
        .toBe(0);

      listOption[1].triggerEventHandler('focusin', {target: listOption[1].nativeElement});
      fixture.detectChanges();

      expect(
        listOption.filter(option => option.nativeElement.getAttribute('tabindex') !== '-1').length,
      )
        .withContext('Expected all list options to be excluded from the tab order')
        .toBe(0);
    });
  });

  describe('with checkbox position after', () => {
    let fixture: ComponentFixture<SelectionListWithCheckboxPositionAfter>;

    beforeEach(waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [
          MatListModule,
          SelectionListWithListOptions,
          SelectionListWithCheckboxPositionAfter,
          SelectionListWithListDisabled,
          SelectionListWithOnlyOneOption,
        ],
      });
    }));

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

    it('should be able to customize checkbox position', () => {
      expect(fixture.nativeElement.querySelector('.mdc-list-item__end .mdc-checkbox'))
        .withContext('Expected checkbox to show up after content.')
        .toBeTruthy();
      expect(fixture.nativeElement.querySelector('.mdc-list-item__start .mdc-checkbox'))
        .withContext('Expected no checkbox to show up before content.')
        .toBeFalsy();
    });
  });

  describe('with list item elements', () => {
    beforeEach(waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [MatListModule, SelectionListWithAvatar, SelectionListWithIcon],
      });
    }));

    function expectCheckboxAtPosition(
      listItemElement: HTMLElement,
      position: MatListOptionTogglePosition,
    ) {
      const containerSelector =
        position === 'before' ? '.mdc-list-item__start' : 'mdc-list-item__end';
      const checkboxPositionClass =
        position === 'before'
          ? 'mdc-list-item--with-leading-checkbox'
          : 'mdc-list-item--with-trailing-checkbox';
      expect(listItemElement.querySelector(`${containerSelector} .mdc-checkbox`))
        .withContext(`Expected checkbox to be aligned ${position}`)
        .toBeDefined();
      expect(listItemElement.classList).toContain(checkboxPositionClass);
    }

    /**
     * Expects an icon to be shown at the given position. Also
     * ensures no avatar is shown at the specified position.
     */
    function expectIconAt(item: HTMLElement, position: 'before' | 'after') {
      const icon = item.querySelector('.mat-mdc-list-item-icon')!;

      expect(item.classList).not.toContain('mdc-list-item--with-leading-avatar');
      expect(item.classList).not.toContain('mat-mdc-list-option-with-trailing-avatar');

      if (position === 'before') {
        expect(icon.classList).toContain('mdc-list-item__start');
        expect(item.classList).toContain('mdc-list-item--with-leading-icon');
        expect(item.classList).not.toContain('mdc-list-item--with-trailing-icon');
      } else {
        expect(icon.classList).toContain('mdc-list-item__end');
        expect(item.classList).toContain('mdc-list-item--with-trailing-icon');
        expect(item.classList).not.toContain('mdc-list-item--with-leading-icon');
      }
    }

    /**
     * Expects an avatar to be shown at the given position. Also
     * ensures that no icon is shown at the specified position.
     */
    function expectAvatarAt(item: HTMLElement, position: 'before' | 'after') {
      const avatar = item.querySelector('.mat-mdc-list-item-avatar')!;

      expect(item.classList).not.toContain('mdc-list-item--with-leading-icon');
      expect(item.classList).not.toContain('mdc-list-item--with-trailing-icon');

      if (position === 'before') {
        expect(avatar.classList).toContain('mdc-list-item__start');
        expect(item.classList).toContain('mdc-list-item--with-leading-avatar');
        expect(item.classList).not.toContain('mat-mdc-list-option-with-trailing-avatar');
      } else {
        expect(avatar.classList).toContain('mdc-list-item__end');
        expect(item.classList).toContain('mat-mdc-list-option-with-trailing-avatar');
        expect(item.classList).not.toContain('mdc-list-item--with-leading-avatar');
      }
    }

    it('should add a class to reflect that it has an avatar', () => {
      const fixture = TestBed.createComponent(SelectionListWithAvatar);
      fixture.detectChanges();

      const listOption = fixture.nativeElement.querySelector('.mat-mdc-list-option');
      expect(listOption.classList).toContain('mdc-list-item--with-leading-avatar');
    });

    it('should add a class to reflect that it has an icon', () => {
      const fixture = TestBed.createComponent(SelectionListWithIcon);
      fixture.detectChanges();

      const listOption = fixture.nativeElement.querySelector('.mat-mdc-list-option');
      expect(listOption.classList).toContain('mdc-list-item--with-leading-icon');
    });

    it('should align icons properly together with checkbox', () => {
      const fixture = TestBed.createComponent(SelectionListWithIcon);
      fixture.detectChanges();
      const listOption = fixture.nativeElement.querySelector(
        '.mat-mdc-list-option',
      )! as HTMLElement;

      expectCheckboxAtPosition(listOption, 'after');
      expectIconAt(listOption, 'before');

      fixture.componentInstance.togglePosition = 'before';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      expectCheckboxAtPosition(listOption, 'before');
      expectIconAt(listOption, 'after');

      fixture.componentInstance.togglePosition = 'after';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      expectCheckboxAtPosition(listOption, 'after');
      expectIconAt(listOption, 'before');
    });

    it('should align avatars properly together with checkbox', () => {
      const fixture = TestBed.createComponent(SelectionListWithAvatar);
      fixture.detectChanges();
      const listOption = fixture.nativeElement.querySelector(
        '.mat-mdc-list-option',
      )! as HTMLElement;

      expectCheckboxAtPosition(listOption, 'after');
      expectAvatarAt(listOption, 'before');

      fixture.componentInstance.togglePosition = 'before';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      expectCheckboxAtPosition(listOption, 'before');
      expectAvatarAt(listOption, 'after');

      fixture.componentInstance.togglePosition = 'after';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      expectCheckboxAtPosition(listOption, 'after');
      expectAvatarAt(listOption, 'before');
    });
  });

  describe('with single selection', () => {
    let fixture: ComponentFixture<SelectionListWithListOptions>;
    let listOptions: DebugElement[];
    let selectionList: DebugElement;

    beforeEach(waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [MatListModule, SelectionListWithListOptions],
      });

      fixture = TestBed.createComponent(SelectionListWithListOptions);
      fixture.componentInstance.multiple = false;
      fixture.changeDetectorRef.markForCheck();
      listOptions = fixture.debugElement.queryAll(By.directive(MatListOption));
      selectionList = fixture.debugElement.query(By.directive(MatSelectionList))!;
      fixture.detectChanges();
    }));

    function getFocusIndex() {
      return listOptions.findIndex(o => document.activeElement === o.nativeElement);
    }

    it('should select one option at a time', () => {
      const testListItem1 = listOptions[1].injector.get<MatListOption>(MatListOption);
      const testListItem2 = listOptions[2].injector.get<MatListOption>(MatListOption);
      const selectList =
        selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;

      expect(selectList.selected.length).toBe(0);

      dispatchMouseEvent(testListItem1._hostElement, 'click');
      fixture.detectChanges();

      expect(selectList.selected).toEqual([testListItem1]);
      expect(listOptions[1].nativeElement.getAttribute('aria-selected')).toBe('true');

      dispatchMouseEvent(testListItem2._hostElement, 'click');
      fixture.detectChanges();

      expect(selectList.selected).toEqual([testListItem2]);
      expect(listOptions[1].nativeElement.getAttribute('aria-selected')).toBe('false');
      expect(listOptions[2].nativeElement.getAttribute('aria-selected')).toBe('true');
    });

    it('should not show check boxes', () => {
      expect(fixture.nativeElement.querySelector('mat-pseudo-checkbox')).toBeFalsy();
    });

    it('should not deselect the target option on click', () => {
      const testListItem1 = listOptions[1].injector.get<MatListOption>(MatListOption);
      const selectList =
        selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;

      expect(selectList.selected.length).toBe(0);

      dispatchMouseEvent(testListItem1._hostElement, 'click');
      fixture.detectChanges();

      expect(selectList.selected).toEqual([testListItem1]);

      dispatchMouseEvent(testListItem1._hostElement, 'click');
      fixture.detectChanges();

      expect(selectList.selected).toEqual([testListItem1]);
    });

    it('throws an exception when toggling single/multiple mode after bootstrap', () => {
      fixture.componentInstance.multiple = true;
      fixture.changeDetectorRef.markForCheck();
      expect(() => fixture.detectChanges()).toThrow(
        new Error('Cannot change `multiple` mode of mat-selection-list after initialization.'),
      );
    });

    it('should do nothing when pressing ctrl + a', () => {
      const event = createKeyboardEvent('keydown', A, undefined, {control: true});

      expect(listOptions.every(option => !option.componentInstance.selected)).toBe(true);

      dispatchEvent(selectionList.nativeElement, event);
      fixture.detectChanges();

      expect(listOptions.every(option => !option.componentInstance.selected)).toBe(true);
    });

    it(
      'should focus, but not toggle, the next item when pressing SHIFT + UP_ARROW in single ' +
        'selection mode',
      () => {
        listOptions[3].nativeElement.focus();
        expect(getFocusIndex()).toBe(3);

        expect(listOptions[1].componentInstance.selected).toBe(false);
        expect(listOptions[2].componentInstance.selected).toBe(false);

        dispatchKeyboardEvent(listOptions[3].nativeElement, 'keydown', UP_ARROW, undefined, {
          shift: true,
        });
        fixture.detectChanges();

        expect(listOptions[1].componentInstance.selected).toBe(false);
        expect(listOptions[2].componentInstance.selected).toBe(false);
      },
    );

    it(
      'should focus, but not toggle, the next item when pressing SHIFT + DOWN_ARROW ' +
        'in single selection mode',
      () => {
        listOptions[3].nativeElement.focus();
        expect(getFocusIndex()).toBe(3);

        expect(listOptions[1].componentInstance.selected).toBe(false);
        expect(listOptions[2].componentInstance.selected).toBe(false);

        dispatchKeyboardEvent(listOptions[3].nativeElement, 'keydown', DOWN_ARROW, undefined, {
          shift: true,
        });
        fixture.detectChanges();

        expect(listOptions[1].componentInstance.selected).toBe(false);
        expect(listOptions[2].componentInstance.selected).toBe(false);
      },
    );
  });

  describe('with single selection', () => {
    let fixture: ComponentFixture<ListOptionWithTwoWayBinding>;
    let optionElement: HTMLElement;
    let option: MatListOption;

    beforeEach(waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [MatListModule, ListOptionWithTwoWayBinding],
      });

      fixture = TestBed.createComponent(ListOptionWithTwoWayBinding);
      fixture.detectChanges();
      const optionDebug = fixture.debugElement.query(By.directive(MatListOption));
      option = optionDebug.componentInstance;
      optionElement = optionDebug.nativeElement;
    }));

    it('should sync the value from the view to the option', () => {
      expect(option.selected).toBe(false);

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

      expect(option.selected).toBe(true);
    });

    it('should sync the value from the option to the view', () => {
      expect(fixture.componentInstance.selected).toBe(false);

      optionElement.click();
      fixture.detectChanges();

      expect(fixture.componentInstance.selected).toBe(true);
    });
  });
});

describe('MatSelectionList with forms', () => {
  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      imports: [
        MatListModule,
        FormsModule,
        ReactiveFormsModule,
        SelectionListWithModel,
        SelectionListWithFormControl,
        SelectionListWithPreselectedOption,
        SelectionListWithPreselectedOptionAndModel,
        SelectionListWithPreselectedFormControlOnPush,
        SelectionListWithCustomComparator,
      ],
    });
  }));

  describe('and ngModel', () => {
    let fixture: ComponentFixture<SelectionListWithModel>;
    let selectionListDebug: DebugElement;
    let listOptions: MatListOption[];
    let ngModel: NgModel;

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

      selectionListDebug = fixture.debugElement.query(By.directive(MatSelectionList))!;
      ngModel = selectionListDebug.injector.get<NgModel>(NgModel);
      listOptions = fixture.debugElement
        .queryAll(By.directive(MatListOption))
        .map(optionDebugEl => optionDebugEl.componentInstance);
    });

    it('should update the model if an option got selected programmatically', fakeAsync(() => {
      expect(fixture.componentInstance.selectedOptions.length)
        .withContext('Expected no options to be selected by default')
        .toBe(0);

      listOptions[0].toggle();
      fixture.detectChanges();

      tick();

      expect(fixture.componentInstance.selectedOptions.length)
        .withContext('Expected first list option to be selected')
        .toBe(1);
    }));

    it('should update the model if an option got clicked', fakeAsync(() => {
      expect(fixture.componentInstance.selectedOptions.length)
        .withContext('Expected no options to be selected by default')
        .toBe(0);

      dispatchMouseEvent(listOptions[0]._hostElement, 'click');
      fixture.detectChanges();

      tick();

      expect(fixture.componentInstance.selectedOptions.length)
        .withContext('Expected first list option to be selected')
        .toBe(1);
    }));

    it('should update the options if a model value is set', fakeAsync(() => {
      expect(fixture.componentInstance.selectedOptions.length)
        .withContext('Expected no options to be selected by default')
        .toBe(0);

      fixture.componentInstance.selectedOptions = ['opt3'];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      tick();

      expect(fixture.componentInstance.selectedOptions.length)
        .withContext('Expected first list option to be selected')
        .toBe(1);
    }));

    it('should focus the first option when the list items are changed', fakeAsync(() => {
      fixture.componentInstance.options = ['first option', 'second option'];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      tick();

      const listElements = fixture.debugElement
        .queryAll(By.directive(MatListOption))
        .map(optionDebugEl => optionDebugEl.nativeElement);

      expect(listElements.length).toBe(2);
      expect(listElements[0].tabIndex).toBe(0);
      expect(listElements[1].tabIndex).toBe(-1);
    }));

    it('should not mark the model as touched when the list is blurred', fakeAsync(() => {
      expect(ngModel.touched)
        .withContext('Expected the selection-list to be untouched by default.')
        .toBe(false);

      dispatchFakeEvent(selectionListDebug.nativeElement, 'blur');
      fixture.detectChanges();
      tick();

      expect(ngModel.touched)
        .withContext('Expected the selection-list to remain untouched.')
        .toBe(false);
    }));

    it('should mark the model as touched when a list item is blurred', fakeAsync(() => {
      expect(ngModel.touched)
        .withContext('Expected the selection-list to be untouched by default.')
        .toBe(false);

      dispatchFakeEvent(fixture.nativeElement.querySelector('.mat-mdc-list-option'), 'blur');
      fixture.detectChanges();
      tick();

      expect(ngModel.touched)
        .withContext('Expected the selection-list to be touched after an item is blurred.')
        .toBe(true);
    }));

    it('should be pristine by default', fakeAsync(() => {
      fixture = TestBed.createComponent(SelectionListWithModel);
      fixture.componentInstance.selectedOptions = ['opt2'];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      ngModel = fixture.debugElement
        .query(By.directive(MatSelectionList))!
        .injector.get<NgModel>(NgModel);
      listOptions = fixture.debugElement
        .queryAll(By.directive(MatListOption))
        .map(optionDebugEl => optionDebugEl.componentInstance);

      // Flush the initial tick to ensure that every action from the ControlValueAccessor
      // happened before the actual test starts.
      tick();

      expect(ngModel.pristine)
        .withContext('Expected the selection-list to be pristine by default.')
        .toBe(true);

      listOptions[1].toggle();
      fixture.detectChanges();

      tick();

      expect(ngModel.pristine)
        .withContext('Expected the selection-list to be dirty after state change.')
        .toBe(false);
    }));

    it('should remove a selected option from the value on destroy', fakeAsync(() => {
      listOptions[1].selected = true;
      listOptions[2].selected = true;

      fixture.detectChanges();

      expect(fixture.componentInstance.selectedOptions).toEqual(['opt2', 'opt3']);

      fixture.componentInstance.options.pop();
      fixture.detectChanges();
      tick();

      expect(fixture.componentInstance.selectedOptions).toEqual(['opt2']);
    }));

    it('should update the model if an option got selected via the model', fakeAsync(() => {
      expect(fixture.componentInstance.selectedOptions).toEqual([]);

      selectionListDebug.componentInstance.selectedOptions.select(listOptions[0]);
      fixture.detectChanges();
      tick();

      expect(fixture.componentInstance.selectedOptions).toEqual(['opt1']);
    }));

    it('should not dispatch the model change event if nothing changed using selectAll', () => {
      expect(fixture.componentInstance.modelChangeSpy).not.toHaveBeenCalled();

      selectionListDebug.componentInstance.selectAll();
      fixture.detectChanges();

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

      selectionListDebug.componentInstance.selectAll();
      fixture.detectChanges();

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

    it('should not dispatch the model change event if nothing changed using selectAll', () => {
      expect(fixture.componentInstance.modelChangeSpy).not.toHaveBeenCalled();

      selectionListDebug.componentInstance.deselectAll();
      fixture.detectChanges();

      expect(fixture.componentInstance.modelChangeSpy).not.toHaveBeenCalled();
    });

    it('should be able to programmatically set an array with duplicate values', fakeAsync(() => {
      fixture.componentInstance.options = ['one', 'two', 'two', 'two', 'three'];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

      listOptions = fixture.debugElement
        .queryAll(By.directive(MatListOption))
        .map(optionDebugEl => optionDebugEl.componentInstance);

      fixture.componentInstance.selectedOptions = ['one', 'two', 'two'];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

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

    it('should dispatch one change event per change when updating a single-selection list', fakeAsync(() => {
      fixture.destroy();
      fixture = TestBed.createComponent(SelectionListWithModel);
      fixture.componentInstance.multiple = false;
      fixture.componentInstance.selectedOptions = ['opt3'];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      const options = fixture.debugElement
        .queryAll(By.directive(MatListOption))
        .map(optionDebugEl => optionDebugEl.nativeElement);

      expect(fixture.componentInstance.modelChangeSpy).not.toHaveBeenCalled();

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

      expect(fixture.componentInstance.modelChangeSpy).toHaveBeenCalledTimes(1);
      expect(fixture.componentInstance.selectedOptions).toEqual(['opt1']);

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

      expect(fixture.componentInstance.modelChangeSpy).toHaveBeenCalledTimes(2);
      expect(fixture.componentInstance.selectedOptions).toEqual(['opt2']);
    }));
  });

  describe('and formControl', () => {
    let fixture: ComponentFixture<SelectionListWithFormControl>;
    let listOptions: MatListOption[];
    let selectionList: MatSelectionList;

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

      selectionList = fixture.debugElement.query(By.directive(MatSelectionList))!.componentInstance;
      listOptions = fixture.debugElement
        .queryAll(By.directive(MatListOption))
        .map(optionDebugEl => optionDebugEl.componentInstance);
    });

    it('should be able to disable options from the control', () => {
      selectionList.focus();
      expect(selectionList.disabled)
        .withContext('Expected the selection list to be enabled.')
        .toBe(false);
      expect(listOptions.every(option => !option.disabled))
        .withContext('Expected every list option to be enabled.')
        .toBe(true);
      expect(
        listOptions.some(
          option => option._elementRef.nativeElement.getAttribute('tabindex') === '0',
        ),
      )
        .withContext('Expected one list item to be in the tab order')
        .toBe(true);

      fixture.componentInstance.formControl.disable();
      fixture.detectChanges();

      expect(selectionList.disabled)
        .withContext('Expected the selection list to be disabled.')
        .toBe(true);
      expect(listOptions.every(option => option.disabled))
        .withContext('Expected every list option to be disabled.')
        .toBe(true);
      expect(
        listOptions.every(
          option => option._elementRef.nativeElement.getAttribute('tabindex') === '-1',
        ),
      )
        .withContext('Expected every list option to be removed from the tab order')
        .toBe(true);
    });

    it('should be able to update the disabled property after form control disabling', () => {
      expect(listOptions.every(option => !option.disabled))
        .withContext('Expected every list option to be enabled.')
        .toBe(true);

      fixture.componentInstance.formControl.disable();
      fixture.detectChanges();

      expect(listOptions.every(option => option.disabled))
        .withContext('Expected every list option to be disabled.')
        .toBe(true);

      // Previously the selection list has been disabled through FormControl#disable. Now we
      // want to verify that we can still change the disabled state through updating the disabled
      // property. Calling FormControl#disable should not lock the disabled property.
      // See: https://github.com/angular/material2/issues/12107
      selectionList.disabled = false;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(listOptions.every(option => !option.disabled))
        .withContext('Expected every list option to be enabled.')
        .toBe(true);
    });

    it('should be able to set the value through the form control', () => {
      expect(listOptions.every(option => !option.selected))
        .withContext('Expected every list option to be unselected.')
        .toBe(true);

      fixture.componentInstance.formControl.setValue(['opt2', 'opt3']);
      fixture.detectChanges();

      expect(listOptions[1].selected)
        .withContext('Expected second option to be selected.')
        .toBe(true);
      expect(listOptions[2].selected)
        .withContext('Expected third option to be selected.')
        .toBe(true);

      fixture.componentInstance.formControl.setValue(null);
      fixture.detectChanges();

      expect(listOptions.every(option => !option.selected))
        .withContext('Expected every list option to be unselected.')
        .toBe(true);
    });

    it('should deselect option whose value no longer matches', () => {
      const option = listOptions[1];

      fixture.componentInstance.formControl.setValue(['opt2']);
      fixture.detectChanges();

      expect(option.selected).withContext('Expected option to be selected.').toBe(true);

      option.value = 'something-different';
      fixture.detectChanges();

      expect(option.selected).withContext('Expected option not to be selected.').toBe(false);
      expect(fixture.componentInstance.formControl.value).toEqual([]);
    });

    it('should mark options as selected when the value is set before they are initialized', () => {
      fixture.destroy();
      fixture = TestBed.createComponent(SelectionListWithFormControl);

      fixture.componentInstance.formControl.setValue(['opt2', 'opt3']);
      fixture.detectChanges();

      listOptions = fixture.debugElement
        .queryAll(By.directive(MatListOption))
        .map(optionDebugEl => optionDebugEl.componentInstance);

      expect(listOptions[1].selected)
        .withContext('Expected second option to be selected.')
        .toBe(true);
      expect(listOptions[2].selected)
        .withContext('Expected third option to be selected.')
        .toBe(true);
    });

    it('should not clear the form control when the list is destroyed', fakeAsync(() => {
      const option = listOptions[1];

      option.selected = true;
      fixture.detectChanges();

      expect(fixture.componentInstance.formControl.value).toEqual(['opt2']);

      fixture.componentInstance.renderList = false;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      expect(fixture.componentInstance.formControl.value).toEqual(['opt2']);
    }));

    it('should mark options added at a later point as selected', () => {
      fixture.componentInstance.formControl.setValue(['opt4']);
      fixture.detectChanges();

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

      listOptions = fixture.debugElement
        .queryAll(By.directive(MatListOption))
        .map(optionDebugEl => optionDebugEl.componentInstance);

      expect(listOptions.length).toBe(4);
      expect(listOptions[3].selected).toBe(true);
    });
  });

  describe('preselected values', () => {
    it('should add preselected options to the model value', fakeAsync(() => {
      const fixture = TestBed.createComponent(SelectionListWithPreselectedOption);
      const listOptions = fixture.debugElement
        .queryAll(By.directive(MatListOption))
        .map(optionDebugEl => optionDebugEl.componentInstance);

      fixture.detectChanges();
      tick();

      expect(listOptions[1].selected).toBe(true);
      expect(fixture.componentInstance.selectedOptions).toEqual(['opt2']);
    }));

    it('should handle preselected option both through the model and the view', fakeAsync(() => {
      const fixture = TestBed.createComponent(SelectionListWithPreselectedOptionAndModel);
      const listOptions = fixture.debugElement
        .queryAll(By.directive(MatListOption))
        .map(optionDebugEl => optionDebugEl.componentInstance);

      fixture.detectChanges();
      tick();

      expect(listOptions[0].selected).toBe(true);
      expect(listOptions[1].selected).toBe(true);
      expect(fixture.componentInstance.selectedOptions).toEqual(['opt1', 'opt2']);
    }));

    it('should show the item as selected when preselected inside OnPush parent', fakeAsync(() => {
      const fixture = TestBed.createComponent(SelectionListWithPreselectedFormControlOnPush);
      fixture.detectChanges();

      const option = fixture.debugElement.queryAll(By.directive(MatListOption))[1];
      const checkbox = option.nativeElement.querySelector(
        '.mdc-checkbox__native-control',
      ) as HTMLInputElement;

      fixture.detectChanges();
      flush();
      fixture.detectChanges();

      expect(option.componentInstance.selected).toBe(true);
      expect(checkbox.checked).toBe(true);
    }));
  });

  describe('with custom compare function', () => {
    it('should use a custom comparator to determine which options are selected', fakeAsync(() => {
      const fixture = TestBed.createComponent(SelectionListWithCustomComparator);
      const testComponent = fixture.componentInstance;

      testComponent.compareWith = jasmine
        .createSpy('comparator', (o1: any, o2: any) => {
          return o1 && o2 && o1.id === o2.id;
        })
        .and.callThrough();

      testComponent.selectedOptions = [{id: 2, label: 'Two'}];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

      expect(testComponent.compareWith).toHaveBeenCalled();
      expect(testComponent.optionInstances.toArray()[1].selected).toBe(true);
    }));
  });
});

@Component({
  template: `
  <mat-selection-list
    id="selection-list-1"
    (selectionChange)="onSelectionChange($event)"
    [disableRipple]="listRippleDisabled"
    [color]="selectionListColor"
    [multiple]="multiple">
    <mat-list-option togglePosition="before" disabled="true" value="inbox"
                     [color]="firstOptionColor">
      Inbox (disabled selection-option)
    </mat-list-option>
    <mat-list-option id="testSelect" togglePosition="before" class="test-native-focus"
                    value="starred">
      Starred
    </mat-list-option>
    <mat-list-option togglePosition="before" value="sent-mail">
      Sent Mail
    </mat-list-option>
    <mat-list-option togglePosition="before" value="archive">
      Archive
    </mat-list-option>
    @if (showLastOption) {
      <mat-list-option togglePosition="before" value="drafts">
        Drafts
      </mat-list-option>
    }
  </mat-selection-list>`,
  standalone: true,
  imports: [MatListModule],
})
class SelectionListWithListOptions {
  showLastOption = true;
  listRippleDisabled = false;
  multiple = true;
  selectionListColor: ThemePalette;
  firstOptionColor: ThemePalette;

  onSelectionChange(_change: MatSelectionListChange) {}
}

@Component({
  template: `
  <mat-selection-list id="selection-list-2">
    <mat-list-option togglePosition="after">
      Inbox (disabled selection-option)
    </mat-list-option>
    <mat-list-option id="testSelect" togglePosition="after">
      Starred
    </mat-list-option>
    <mat-list-option togglePosition="after">
      Sent Mail
    </mat-list-option>
    <mat-list-option togglePosition="after">
      Drafts
    </mat-list-option>
  </mat-selection-list>`,
  standalone: true,
  imports: [MatListModule],
})
class SelectionListWithCheckboxPositionAfter {}

@Component({
  template: `
  <mat-selection-list id="selection-list-3" [disabled]="disabled">
    <mat-list-option togglePosition="after">
      Inbox (disabled selection-option)
    </mat-list-option>
    <mat-list-option id="testSelect" togglePosition="after">
      Starred
    </mat-list-option>
    <mat-list-option togglePosition="after">
      Sent Mail
    </mat-list-option>
    <mat-list-option togglePosition="after">
      Drafts
    </mat-list-option>
  </mat-selection-list>`,
  standalone: true,
  imports: [MatListModule],
})
class SelectionListWithListDisabled {
  disabled: boolean = true;
}

@Component({
  template: `
  <mat-selection-list>
    <mat-list-option [disabled]="disableItem">Item</mat-list-option>
  </mat-selection-list>
  `,
  standalone: true,
  imports: [MatListModule],
})
class SelectionListWithDisabledOption {
  disableItem: boolean = false;
}

@Component({
  template: `
  <mat-selection-list>
    <mat-list-option>Not selected - Item #1</mat-list-option>
    <mat-list-option [selected]="true">Pre-selected - Item #2</mat-list-option>
    <mat-list-option [selected]="true">Pre-selected - Item #3</mat-list-option>
    <mat-list-option>Not selected - Item #4</mat-list-option>
  </mat-selection-list>`,
  standalone: true,
  imports: [MatListModule],
})
class SelectionListWithSelectedOption {}

@Component({
  template: `
  <mat-selection-list [multiple]="false">
    <mat-list-option>Not selected - Item #1</mat-list-option>
    <mat-list-option [selected]="true">Pre-selected - Item #2</mat-list-option>
  </mat-selection-list>`,
  standalone: true,
  imports: [MatListModule],
})
class SingleSelectionListWithSelectedOption {}

@Component({
  template: `
  <mat-selection-list>
    <mat-list-option [selected]="true" [value]="itemValue">Item</mat-list-option>
  </mat-selection-list>`,
  standalone: true,
  imports: [MatListModule],
})
class SelectionListWithSelectedOptionAndValue {
  itemValue = 'item1';
}

@Component({
  template: `
  <mat-selection-list id="selection-list-4">
    <mat-list-option togglePosition="after" class="test-focus" id="123">
      Inbox
    </mat-list-option>
  </mat-selection-list>`,
  standalone: true,
  imports: [MatListModule],
})
class SelectionListWithOnlyOneOption {}

@Component({
  template: `
    <mat-selection-list
      [(ngModel)]="selectedOptions"
      (ngModelChange)="modelChangeSpy()"
      [multiple]="multiple">
      @for (option of options; track option) {
        <mat-list-option [value]="option">{{option}}</mat-list-option>
      }
    </mat-selection-list>`,
  standalone: true,
  imports: [MatListModule, FormsModule, ReactiveFormsModule],
})
class SelectionListWithModel {
  modelChangeSpy = jasmine.createSpy('model change spy');
  selectedOptions: string[] = [];
  multiple = true;
  options = ['opt1', 'opt2', 'opt3'];
}

@Component({
  template: `
    @if (renderList) {
      <mat-selection-list [formControl]="formControl">
        <mat-list-option value="opt1">Option 1</mat-list-option>
        <mat-list-option value="opt2">Option 2</mat-list-option>
        <mat-list-option value="opt3">Option 3</mat-list-option>
        @if (renderExtraOption) {
          <mat-list-option value="opt4">Option 4</mat-list-option>
        }
      </mat-selection-list>
    }
  `,
  standalone: true,
  imports: [MatListModule, FormsModule, ReactiveFormsModule],
})
class SelectionListWithFormControl {
  formControl = new FormControl([] as string[]);
  renderList = true;
  renderExtraOption = false;
}

@Component({
  template: `
    <mat-selection-list [(ngModel)]="selectedOptions">
      <mat-list-option value="opt1">Option 1</mat-list-option>
      <mat-list-option value="opt2" selected>Option 2</mat-list-option>
    </mat-selection-list>`,
  standalone: true,
  imports: [MatListModule, FormsModule, ReactiveFormsModule],
})
class SelectionListWithPreselectedOption {
  selectedOptions: string[];
}

@Component({
  template: `
    <mat-selection-list [(ngModel)]="selectedOptions">
      <mat-list-option value="opt1">Option 1</mat-list-option>
      <mat-list-option value="opt2" selected>Option 2</mat-list-option>
    </mat-selection-list>`,
  standalone: true,
  imports: [MatListModule, FormsModule, ReactiveFormsModule],
})
class SelectionListWithPreselectedOptionAndModel {
  selectedOptions = ['opt1'];
}

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <mat-selection-list [formControl]="formControl">
      @for (opt of opts; track opt) {
        <mat-list-option [value]="opt">{{opt}}</mat-list-option>
      }
    </mat-selection-list>
  `,
  standalone: true,
  imports: [MatListModule, FormsModule, ReactiveFormsModule],
})
class SelectionListWithPreselectedFormControlOnPush {
  opts = ['opt1', 'opt2', 'opt3'];
  formControl = new FormControl(['opt2']);
}

@Component({
  template: `
    <mat-selection-list [(ngModel)]="selectedOptions" [compareWith]="compareWith">
      @for (option of options; track option) {
        <mat-list-option [value]="option">{{option.label}}</mat-list-option>
      }
    </mat-selection-list>`,
  standalone: true,
  imports: [MatListModule, FormsModule, ReactiveFormsModule],
})
class SelectionListWithCustomComparator {
  @ViewChildren(MatListOption) optionInstances: QueryList<MatListOption>;
  selectedOptions: {id: number; label: string}[] = [];
  compareWith?: (o1: any, o2: any) => boolean;
  options = [
    {id: 1, label: 'One'},
    {id: 2, label: 'Two'},
    {id: 3, label: 'Three'},
  ];
}

@Component({
  template: `
    <mat-selection-list>
      <mat-list-option [togglePosition]="togglePosition">
        <div matListItemAvatar>I</div>
        Inbox
      </mat-list-option>
    </mat-selection-list>
  `,
  standalone: true,
  imports: [MatListModule],
})
class SelectionListWithAvatar {
  togglePosition: MatListOptionTogglePosition | undefined;
}

@Component({
  template: `
    <mat-selection-list>
      <mat-list-option [togglePosition]="togglePosition">
        <div matListItemIcon>I</div>
        Inbox
      </mat-list-option>
    </mat-selection-list>
  `,
  standalone: true,
  imports: [MatListModule],
})
class SelectionListWithIcon {
  togglePosition: MatListOptionTogglePosition | undefined;
}

@Component({
  // Note the blank `@if` which we need in order to hit the bug that we're testing.
  template: `
    <mat-selection-list>
      @if (true) {
        <mat-list-option [value]="1">One</mat-list-option>
        <mat-list-option [value]="2">Two</mat-list-option>
      }
    </mat-selection-list>`,
  standalone: true,
  imports: [MatListModule],
})
class SelectionListWithIndirectChildOptions {
  @ViewChildren(MatListOption) optionInstances: QueryList<MatListOption>;
}

@Component({
  template: `
  <mat-selection-list>
    <mat-list-option [(selected)]="selected">Item</mat-list-option>
  </mat-selection-list>
`,
  standalone: true,
  imports: [MatListModule],
})
class ListOptionWithTwoWayBinding {
  selected = false;
}
