sass-references/angular-material/material/list/selection-list.spec.ts

2049 lines
71 KiB
TypeScript
Raw Permalink Normal View History

2024-12-06 10:42:08 +08:00
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;
}