/
githubmirror
/
components
Обзор
Документация
Войти
/
githubmirror
/
components
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/material/chips/chip-listbox.spec.ts
1 149 строк
40 KB
Kristiyan Kostadinov
test(material/chips): fix flakes and re-enable test (#33426)
20 июн 2026, 10:23
Не верифицирован
20 июн 2026, 10:23
88b24e0
Код
Авторство
О чём код?
import {Direction} from '@angular/cdk/bidi'; import {END, HOME, LEFT_ARROW, RIGHT_ARROW, SPACE, TAB} from '@angular/cdk/keycodes'; import { dispatchFakeEvent, dispatchKeyboardEvent, patchElementFocus, provideFakeDirectionality, } from '@angular/cdk/testing/private'; import { Component, DebugElement, QueryList, signal, Type, ViewChild, ViewChildren, WritableSignal, ChangeDetectionStrategy, } from '@angular/core'; import {AsyncPipe} from '@angular/common'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {By} from '@angular/platform-browser'; import {asyncScheduler, BehaviorSubject, Observable} from 'rxjs'; import {observeOn} from 'rxjs/operators'; import {MatChipListbox, MatChipOption} from './index'; describe('MatChipListbox', () => { let fixture: ComponentFixture<any>; let chipListboxDebugElement: DebugElement; let chipListboxNativeElement: HTMLElement; let chipListboxInstance: MatChipListbox; let testComponent: StandardChipListbox; let chips: QueryList<MatChipOption>; let directionality: WritableSignal<Direction>; let primaryActions: NodeListOf<HTMLElement>; function wait(milliseconds: number) { return new Promise(resolve => setTimeout(resolve, milliseconds)); } describe('StandardChipList', () => { describe('basic behaviors', () => { beforeEach(() => { createComponent(StandardChipListbox); }); it('should add the `mat-mdc-chip-set` class', () => { expect(chipListboxNativeElement.classList).toContain('mat-mdc-chip-set'); }); it('should not have the aria-selected attribute when it is not selectable', async () => { testComponent.selectable = false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await fixture.whenStable(); const chipsValid = chips .toArray() .every( chip => !chip.selectable && !chip._elementRef.nativeElement.hasAttribute('aria-selected'), ); expect(chipsValid).toBe(true); }); it('should toggle the chips disabled state based on whether it is disabled', () => { expect(chips.toArray().every(chip => chip.disabled)).toBe(false); chipListboxInstance.disabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chips.toArray().every(chip => chip.disabled)).toBe(true); chipListboxInstance.disabled = false; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chips.toArray().every(chip => chip.disabled)).toBe(false); }); it('should disable a chip that is added after the listbox became disabled', async () => { expect(chips.toArray().every(chip => chip.disabled)).toBe(false); chipListboxInstance.disabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chips.toArray().every(chip => chip.disabled)).toBe(true); fixture.componentInstance.chips.push(5, 6); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await fixture.whenStable(); fixture.detectChanges(); expect(chips.toArray().every(chip => chip.disabled)).toBe(true); }); it('should not set a role on the grid when the list is empty', () => { testComponent.chips = []; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipListboxNativeElement.hasAttribute('role')).toBe(false); }); it('should be able to set a custom role', () => { testComponent.role = 'grid'; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipListboxNativeElement.getAttribute('role')).toBe('grid'); }); it('should not set aria-required when it does not have a role', () => { testComponent.chips = []; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipListboxNativeElement.hasAttribute('role')).toBe(false); expect(chipListboxNativeElement.hasAttribute('aria-required')).toBe(false); }); it('should disable a chip that is individually disabled inside a form', async () => { fixture.destroy(); TestBed.resetTestingModule(); const disabledFixture = createComponent(IndividuallyDisabledChipInsideForm); disabledFixture.detectChanges(); await disabledFixture.whenStable(); expect(disabledFixture.componentInstance.chip.disabled).toBe(true); }); }); describe('with selected chips', () => { beforeEach(() => { fixture = createComponent(SelectedChipListbox); }); it('should not override chips selected', () => { const instanceChips = fixture.componentInstance.chips.toArray(); expect(instanceChips[0].selected) .withContext('Expected first option to be selected.') .toBe(true); expect(instanceChips[1].selected) .withContext('Expected second option to be not selected.') .toBe(false); expect(instanceChips[2].selected) .withContext('Expected third option to be selected.') .toBe(true); }); it('should have role listbox', () => { expect(chipListboxNativeElement.getAttribute('role')).toBe('listbox'); }); it('should not have role when empty', () => { fixture.componentInstance.foods = []; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipListboxNativeElement.getAttribute('role')) .withContext('Expect no role attribute') .toBeNull(); }); }); describe('focus behaviors', () => { beforeEach(() => { createComponent(StandardChipListbox); }); it('should focus the first chip on focus', () => { chipListboxInstance.focus(); fixture.detectChanges(); expect(document.activeElement).toBe(primaryActions[0]); }); it('should focus the primary action when calling the `focus` method', () => { chips.last.focus(); fixture.detectChanges(); expect(document.activeElement).toBe(primaryActions[primaryActions.length - 1]); }); it('should not be able to become focused when disabled', () => { expect(chipListboxInstance.focused) .withContext('Expected listbox to not be focused.') .toBe(false); chipListboxInstance.disabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); chipListboxInstance.focus(); fixture.detectChanges(); expect(chipListboxInstance.focused) .withContext('Expected listbox to continue not to be focused') .toBe(false); }); it('should remove the tabindex from the listbox if it is disabled', () => { expect(chipListboxNativeElement.getAttribute('tabindex')).toBe('0'); chipListboxInstance.disabled = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipListboxNativeElement.getAttribute('tabindex')).toBe('-1'); }); describe('on chip destroy', () => { it('should focus the next item', () => { const midItem = chips.get(2)!; // Focus the middle item patchElementFocus(midItem.primaryAction!._elementRef.nativeElement); midItem.focus(); // Destroy the middle item testComponent.chips.splice(2, 1); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // It focuses the 4th item expect(document.activeElement).toBe(primaryActions[3]); }); it('should focus the previous item', () => { // Focus the last item patchElementFocus(chips.last.primaryAction!._elementRef.nativeElement); chips.last.focus(); // Destroy the last item testComponent.chips.pop(); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // It focuses the next-to-last item expect(document.activeElement).toBe(primaryActions[primaryActions.length - 2]); }); it('should not focus if chip listbox is not focused', () => { const midItem = chips.get(2)!; // Focus and blur the middle item midItem.focus(); (document.activeElement as HTMLElement).blur(); // Destroy the middle item testComponent.chips.splice(2, 1); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); // Should not have focus expect(chipListboxNativeElement.contains(document.activeElement)).toBe(false); }); it('should focus the listbox if the last focused item is removed', () => { testComponent.chips = [0]; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); spyOn(chipListboxInstance, 'focus'); patchElementFocus(chips.last.primaryAction!._elementRef.nativeElement); chips.last.focus(); testComponent.chips.pop(); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipListboxInstance.focus).toHaveBeenCalled(); }); }); }); describe('keyboard behavior', () => { describe('LTR (default)', () => { beforeEach(() => { createComponent(StandardChipListbox); }); it('should focus previous item when press LEFT ARROW', () => { const lastIndex = primaryActions.length - 1; // Focus the last item in the array chips.last.focus(); expect(document.activeElement).toBe(primaryActions[lastIndex]); // Press the LEFT arrow dispatchKeyboardEvent(primaryActions[lastIndex], 'keydown', LEFT_ARROW); fixture.detectChanges(); // It focuses the next-to-last item expect(document.activeElement).toBe(primaryActions[lastIndex - 1]); }); it('should focus next item when press RIGHT ARROW', () => { // Focus the last item in the array chips.first.focus(); expect(document.activeElement).toBe(primaryActions[0]); // Press the RIGHT arrow dispatchKeyboardEvent(primaryActions[0], 'keydown', RIGHT_ARROW); fixture.detectChanges(); // It focuses the next-to-last item expect(document.activeElement).toBe(primaryActions[1]); }); it('should not handle arrow key events from non-chip elements', () => { const previousActiveElement = document.activeElement; dispatchKeyboardEvent(chipListboxNativeElement, 'keydown', RIGHT_ARROW); fixture.detectChanges(); expect(document.activeElement) .withContext('Expected focused item not to have changed.') .toBe(previousActiveElement); }); it('should focus the first item when pressing HOME', () => { const lastAction = primaryActions[primaryActions.length - 1]; chips.last.focus(); expect(document.activeElement).toBe(lastAction); const event = dispatchKeyboardEvent(lastAction, 'keydown', HOME); fixture.detectChanges(); expect(document.activeElement).toBe(primaryActions[0]); expect(event.defaultPrevented).toBe(true); }); it('should focus the last item when pressing END', () => { chips.first.focus(); expect(document.activeElement).toBe(primaryActions[0]); const event = dispatchKeyboardEvent(primaryActions[0], 'keydown', END); fixture.detectChanges(); expect(document.activeElement).toBe(primaryActions[primaryActions.length - 1]); expect(event.defaultPrevented).toBe(true); }); }); describe('RTL', () => { beforeEach(() => { createComponent(StandardChipListbox, 'rtl'); }); it('should focus previous item when press RIGHT ARROW', () => { const lastIndex = primaryActions.length - 1; // Focus the last item in the array chips.last.focus(); expect(document.activeElement).toBe(primaryActions[lastIndex]); // Press the RIGHT arrow dispatchKeyboardEvent(primaryActions[lastIndex], 'keydown', RIGHT_ARROW); fixture.detectChanges(); // It focuses the next-to-last item expect(document.activeElement).toBe(primaryActions[lastIndex - 1]); }); it('should focus next item when press LEFT ARROW', () => { // Focus the last item in the array chips.first.focus(); expect(document.activeElement).toBe(primaryActions[0]); // Press the LEFT arrow dispatchKeyboardEvent(primaryActions[0], 'keydown', LEFT_ARROW); fixture.detectChanges(); // It focuses the next-to-last item expect(document.activeElement).toBe(primaryActions[1]); }); it('should allow focus to escape when tabbing away', async () => { dispatchKeyboardEvent(chipListboxNativeElement, 'keydown', TAB); expect(chipListboxNativeElement.tabIndex) .withContext('Expected tabIndex to be set to -1 temporarily.') .toBe(-1); await wait(0); expect(chipListboxNativeElement.tabIndex) .withContext('Expected tabIndex to be reset back to 0') .toBe(0); }); it('should use user defined tabIndex', async () => { chipListboxInstance.tabIndex = 4; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipListboxNativeElement.tabIndex) .withContext('Expected tabIndex to be set to user defined value 4.') .toBe(4); dispatchKeyboardEvent(chipListboxNativeElement, 'keydown', TAB); expect(chipListboxNativeElement.tabIndex) .withContext('Expected tabIndex to be set to -1 temporarily.') .toBe(-1); await wait(0); expect(chipListboxNativeElement.tabIndex) .withContext('Expected tabIndex to be reset back to 4') .toBe(4); }); }); it('should account for the direction changing', () => { createComponent(StandardChipListbox); chips.first.focus(); expect(document.activeElement).toBe(primaryActions[0]); dispatchKeyboardEvent(primaryActions[0], 'keydown', RIGHT_ARROW); fixture.detectChanges(); expect(document.activeElement).toBe(primaryActions[1]); directionality.set('rtl'); fixture.detectChanges(); dispatchKeyboardEvent(primaryActions[1], 'keydown', RIGHT_ARROW); fixture.detectChanges(); expect(document.activeElement).toBe(primaryActions[0]); }); }); describe('selection logic', () => { it('should remove selection if chip has been removed', () => { fixture = createComponent(BasicChipListbox); const instanceChips = fixture.componentInstance.chips; const chipListbox = fixture.componentInstance.chipListbox; dispatchKeyboardEvent(primaryActions[0], 'keydown', SPACE); fixture.detectChanges(); expect(instanceChips.first.selected) .withContext('Expected first option to be selected.') .toBe(true); expect(chipListbox.selected) .withContext('Expected first option to be selected.') .toBe(chips.first); fixture.componentInstance.foods = []; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); expect(chipListbox.selected) .withContext('Expected selection to be removed when option no longer exists.') .toBe(undefined); }); it('should select an option that was added after initialization', () => { fixture = createComponent(BasicChipListbox); fixture.componentInstance.foods.push({viewValue: 'Potatoes', value: 'potatoes-8'}); fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); primaryActions = chipListboxNativeElement.querySelectorAll<HTMLElement>( '.mdc-evolution-chip__action--primary', ); dispatchKeyboardEvent(primaryActions[8], 'keydown', SPACE); fixture.detectChanges(); expect(fixture.componentInstance.chipListbox.value) .withContext('Expect value contain the value of the last option') .toContain('potatoes-8'); expect(fixture.componentInstance.chips.last.selected) .withContext('Expect last option selected') .toBeTruthy(); }); it('should not select disabled chips', () => { fixture = createComponent(BasicChipListbox); const array = chips.toArray(); dispatchKeyboardEvent(primaryActions[2], 'keydown', SPACE); fixture.detectChanges(); expect(fixture.componentInstance.chipListbox.value) .withContext('Expect value to be undefined') .toBeUndefined(); expect(array[2].selected).withContext('Expect disabled chip not selected').toBeFalsy(); expect(fixture.componentInstance.chipListbox.selected) .withContext('Expect no selected chips') .toBeUndefined(); }); it('should not select when is not selectable', async () => { const falsyFixture = createComponent(FalsyBasicChipListbox); falsyFixture.detectChanges(); await falsyFixture.whenStable(); falsyFixture.detectChanges(); const chipListboxElement = falsyFixture.debugElement.query(By.directive(MatChipListbox))!; const _chips = chipListboxElement.componentInstance._chips; const nativeChips = ( chipListboxElement.nativeElement as HTMLElement ).querySelectorAll<HTMLElement>('.mdc-evolution-chip__action--primary'); expect(_chips.first.selected) .withContext('Expected first option not to be selected') .toBe(false); dispatchKeyboardEvent(nativeChips[0], 'keydown', SPACE); falsyFixture.detectChanges(); await falsyFixture.whenStable(); expect(_chips.first.selected) .withContext('Expected first option not to be selected.') .toBe(false); }); it('should set `aria-selected` based on the selection state in single selection mode', () => { const getAriaSelected = () => Array.from(primaryActions).map(action => action.getAttribute('aria-selected')); fixture = createComponent(BasicChipListbox); // Use a shorter list so we can keep the assertions smaller fixture.componentInstance.foods = [ {value: 'steak-0', viewValue: 'Steak'}, {value: 'pizza-1', viewValue: 'Pizza'}, {value: 'tacos-2', viewValue: 'Tacos'}, ]; fixture.componentInstance.selectable = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); primaryActions = chipListboxNativeElement.querySelectorAll<HTMLElement>( '.mdc-evolution-chip__action--primary', ); expect(getAriaSelected()).toEqual(['false', 'false', 'false']); primaryActions[1].click(); fixture.detectChanges(); expect(getAriaSelected()).toEqual(['false', 'true', 'false']); primaryActions[2].click(); fixture.detectChanges(); expect(getAriaSelected()).toEqual(['false', 'false', 'true']); primaryActions[0].click(); fixture.detectChanges(); expect(getAriaSelected()).toEqual(['true', 'false', 'false']); }); it('should set `aria-selected` based on the selection state in multi-selection mode', () => { const getAriaSelected = () => Array.from(primaryActions).map(action => action.getAttribute('aria-selected')); fixture = createComponent(MultiSelectionChipListbox); fixture.detectChanges(); // Use a shorter list so we can keep the assertions smaller fixture.componentInstance.foods = [ {value: 'steak-0', viewValue: 'Steak'}, {value: 'pizza-1', viewValue: 'Pizza'}, {value: 'tacos-2', viewValue: 'Tacos'}, ]; fixture.componentInstance.selectable = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); primaryActions = chipListboxNativeElement.querySelectorAll<HTMLElement>( '.mdc-evolution-chip__action--primary', ); expect(getAriaSelected()).toEqual(['false', 'false', 'false']); primaryActions[1].click(); fixture.detectChanges(); expect(getAriaSelected()).toEqual(['false', 'true', 'false']); primaryActions[2].click(); fixture.detectChanges(); expect(getAriaSelected()).toEqual(['false', 'true', 'true']); primaryActions[0].click(); fixture.detectChanges(); expect(getAriaSelected()).toEqual(['true', 'true', 'true']); primaryActions[1].click(); fixture.detectChanges(); expect(getAriaSelected()).toEqual(['true', 'false', 'true']); }); }); describe('chip list with chip input', () => { describe('single selection', () => { beforeEach(() => { fixture = createComponent(BasicChipListbox); }); it('should take an initial view value with reactive forms', async () => { fixture.componentInstance.control = new FormControl('pizza-1'); fixture.componentInstance.selectable = true; fixture.changeDetectorRef.markForCheck(); fixture.detectChanges(); await fixture.whenStable(); const array = chips.toArray(); expect(array[1].selected).withContext('Expect pizza-1 chip to be selected').toBeTruthy(); dispatchKeyboardEvent(primaryActions[1], 'keydown', SPACE); fixture.detectChanges(); await fixture.whenStable(); expect(array[1].selected) .withContext('Expect chip to be not selected after toggle selected') .toBeFalsy(); }); it('should set the view value from the form', () => { const chipListbox = fixture.componentInstance.chipListbox; const array = chips.toArray(); expect(chipListbox.value) .withContext('Expect chip listbox to have no initial value') .toBeFalsy(); fixture.componentInstance.control.setValue('pizza-1'); fixture.detectChanges(); expect(array[1].selected).withContext('Expect chip to be selected').toBeTruthy(); }); it('should update the form value when the view changes', () => { expect(fixture.componentInstance.control.value) .withContext(`Expected the control's value to be empty initially.`) .toEqual(null); dispatchKeyboardEvent(primaryActions[0], 'keydown', SPACE); fixture.detectChanges(); expect(fixture.componentInstance.control.value) .withContext(`Expected control's value to be set to the new option.`) .toEqual('steak-0'); }); it('should clear the selection when a nonexistent option value is selected', () => { const array = chips.toArray(); fixture.componentInstance.control.setValue('pizza-1'); fixture.detectChanges(); expect(array[1].selected) .withContext(`Expected chip with the value to be selected.`) .toBeTruthy(); fixture.componentInstance.control.setValue('gibberish'); fixture.detectChanges(); expect(array[1].selected) .withContext(`Expected chip with the old value not to be selected.`) .toBeFalsy(); }); it('should clear the selection when the control is reset', () => { const array = chips.toArray(); fixture.componentInstance.control.setValue('pizza-1'); fixture.detectChanges(); fixture.componentInstance.control.reset(); fixture.detectChanges(); expect(array[1].selected) .withContext(`Expected chip with the old value not to be selected.`) .toBeFalsy(); }); it('should set the control to touched when the chip listbox is touched', async () => { expect(fixture.componentInstance.control.touched) .withContext('Expected the control to start off as untouched.') .toBe(false); const nativeChipListbox = fixture.debugElement.query( By.css('mat-chip-listbox'), )!.nativeElement; dispatchFakeEvent(nativeChipListbox, 'blur'); await wait(0); expect(fixture.componentInstance.control.touched) .withContext('Expected the control to be touched.') .toBe(true); }); it('should not set touched when a disabled chip listbox is touched', async () => { expect(fixture.componentInstance.control.touched) .withContext('Expected the control to start off as untouched.') .toBe(false); fixture.componentInstance.control.disable(); const nativeChipListbox = fixture.debugElement.query( By.css('mat-chip-listbox'), )!.nativeElement; dispatchFakeEvent(nativeChipListbox, 'blur'); await wait(0); expect(fixture.componentInstance.control.touched) .withContext('Expected the control to stay untouched.') .toBe(false); }); it("should set the control to dirty when the chip listbox's value changes in the DOM", () => { expect(fixture.componentInstance.control.dirty) .withContext(`Expected control to start out pristine.`) .toEqual(false); dispatchKeyboardEvent(primaryActions[1], 'keydown', SPACE); fixture.detectChanges(); expect(fixture.componentInstance.control.dirty) .withContext(`Expected control to be dirty after value was changed by user.`) .toEqual(true); }); it('should not set the control to dirty when the value changes programmatically', () => { expect(fixture.componentInstance.control.dirty) .withContext(`Expected control to start out pristine.`) .toEqual(false); fixture.componentInstance.control.setValue('pizza-1'); expect(fixture.componentInstance.control.dirty) .withContext(`Expected control to stay pristine after programmatic change.`) .toEqual(false); }); it('should be able to programmatically select a falsy option', () => { fixture.destroy(); TestBed.resetTestingModule(); const falsyFixture = createComponent(FalsyValueChipListbox); falsyFixture.componentInstance.control.setValue([0]); falsyFixture.detectChanges(); falsyFixture.detectChanges(); expect(falsyFixture.componentInstance.chips.first.selected) .withContext('Expected first option to be selected') .toBe(true); }); it('should not focus the active chip when the value is set programmatically', () => { const chipArray = fixture.componentInstance.chips.toArray(); spyOn(chipArray[4], 'focus').and.callThrough(); fixture.componentInstance.control.setValue('chips-4'); fixture.detectChanges(); expect(chipArray[4].focus).not.toHaveBeenCalled(); }); }); describe('multiple selection', () => { it('should take an initial view value with reactive forms', async () => { fixture = createComponent(MultiSelectionChipListbox, undefined, initFixture => { initFixture.componentInstance.control = new FormControl(['pizza-1', 'pasta-6']); initFixture.componentInstance.selectable = true; }); fixture.detectChanges(); await fixture.whenStable(); const array = fixture.componentInstance.chips.toArray(); expect(array[1].selected).withContext('Expect pizza-1 chip to be selected').toBe(true); expect(array[6].selected).withContext('Expect pasta-6 chip to be selected').toBe(true); dispatchKeyboardEvent(primaryActions[1], 'keydown', SPACE); fixture.detectChanges(); expect(array[1].selected) .withContext('Expect pizza-1 chip to no longer be selected') .toBe(false); expect(array[6].selected) .withContext('Expect pasta-6 chip to remain selected') .toBe(true); }); it('should set the view value from the form', () => { fixture = createComponent(MultiSelectionChipListbox); const chipListbox = fixture.componentInstance.chipListbox; const array = fixture.componentInstance.chips.toArray(); expect(chipListbox.value) .withContext('Expect chip listbox to have no initial value') .toBeFalsy(); fixture.componentInstance.control.setValue(['pizza-1']); fixture.detectChanges(); expect(array[1].selected).withContext('Expect chip to be selected').toBeTruthy(); }); it('should update the form value when the view changes', () => { fixture = createComponent(MultiSelectionChipListbox); expect(fixture.componentInstance.control.value) .withContext(`Expected the control's value to be empty initially.`) .toEqual(null); dispatchKeyboardEvent(primaryActions[0], 'keydown', SPACE); fixture.detectChanges(); expect(fixture.componentInstance.control.value) .withContext(`Expected control's value to be set to the new option.`) .toEqual(['steak-0']); }); it('should clear the selection when a nonexistent option value is selected', () => { fixture = createComponent(MultiSelectionChipListbox); chips = fixture.componentInstance.chips; const array = fixture.componentInstance.chips.toArray(); fixture.componentInstance.control.setValue(['pizza-1']); fixture.detectChanges(); expect(array[1].selected) .withContext(`Expected chip with the value to be selected.`) .toBeTruthy(); fixture.componentInstance.control.setValue(['gibberish']); fixture.detectChanges(); expect(array[1].selected) .withContext(`Expected chip with the old value not to be selected.`) .toBeFalsy(); }); it('should clear the selection when the control is reset', () => { fixture = createComponent(MultiSelectionChipListbox); const array = fixture.componentInstance.chips.toArray(); fixture.componentInstance.control.setValue(['pizza-1']); fixture.detectChanges(); fixture.componentInstance.control.reset(); fixture.detectChanges(); expect(array[1].selected) .withContext(`Expected chip with the old value not to be selected.`) .toBeFalsy(); }); }); describe('async multiple selection', () => { it('should select initial async chips', async () => { fixture = createComponent(AsyncMultiSelectionChipListbox, undefined, initFixture => { initFixture.componentInstance.control = new FormControl(['tutorial-1', 'tutorial-2']); }); fixture.detectChanges(); await wait(400); fixture.detectChanges(); let array = fixture.componentInstance.chips.toArray(); expect(array.length).withContext('Expect chips not to be rendered yet').toBe(0); await wait(100); fixture.detectChanges(); await fixture.whenStable(); array = fixture.componentInstance.chips.toArray(); expect(array[0].selected) .withContext('Expect "tutorial-1" chip to be selected') .toBe(true); expect(array[1].selected) .withContext('Expect "tutorial-2" chip to be selected') .toBe(true); }); it('should select async chips that changed over time', async () => { fixture = createComponent(AsyncMultiSelectionChipListbox, undefined, initFixture => { initFixture.componentInstance.control = new FormControl(['tutorial-1']); }); fixture.detectChanges(); await fixture.whenStable(); await wait(500); fixture.detectChanges(); fixture.componentInstance.control.setValue(['tutorial-4']); fixture.componentInstance.updateChips(['tutorial-3', 'tutorial-4']); await wait(500); fixture.detectChanges(); await fixture.whenStable(); const array = fixture.componentInstance.chips.toArray(); expect(array[1].selected) .withContext('Expect "tutorial-4" chip to be selected') .toBe(true); }); }); }); }); function createComponent<T>( component: Type<T>, direction: Direction = 'ltr', beforeInitialChangeDetection?: (fixture: ComponentFixture<T>) => void, ): ComponentFixture<T> { directionality = signal(direction); TestBed.configureTestingModule({ providers: [provideFakeDirectionality(directionality)], }); fixture = TestBed.createComponent<T>(component); beforeInitialChangeDetection?.(fixture); fixture.detectChanges(); chipListboxDebugElement = fixture.debugElement.query(By.directive(MatChipListbox))!; chipListboxNativeElement = chipListboxDebugElement.nativeElement; chipListboxInstance = chipListboxDebugElement.componentInstance; testComponent = fixture.debugElement.componentInstance; chips = chipListboxInstance._chips; primaryActions = chipListboxNativeElement.querySelectorAll<HTMLElement>( '.mdc-evolution-chip__action--primary', ); return fixture; } }); @Component({ template: ` <mat-chip-listbox [tabIndex]="tabIndex" [selectable]="selectable" [role]="role"> @for (i of chips; track i) { <mat-chip-option (select)="chipSelect(i)" (deselect)="chipDeselect(i)"> {{name}} {{i + 1}} </mat-chip-option> } </mat-chip-listbox>`, imports: [MatChipListbox, MatChipOption], changeDetection: ChangeDetectionStrategy.Eager, }) class StandardChipListbox { name: string = 'Test'; selectable: boolean = true; chipSelect: (index?: number) => void = () => {}; chipDeselect: (index?: number) => void = () => {}; tabIndex: number = 0; chips = [0, 1, 2, 3, 4]; role: string | null = null; } @Component({ template: ` <mat-chip-listbox [formControl]="control" [required]="isRequired" [tabIndex]="tabIndexOverride" [selectable]="selectable"> @for (food of foods; track food) { <mat-chip-option [value]="food.value" [disabled]="food.disabled"> {{ food.viewValue }} </mat-chip-option> } </mat-chip-listbox> `, imports: [MatChipListbox, MatChipOption, ReactiveFormsModule], changeDetection: ChangeDetectionStrategy.Eager, }) class BasicChipListbox { foods: any[] = [ {value: 'steak-0', viewValue: 'Steak'}, {value: 'pizza-1', viewValue: 'Pizza'}, {value: 'tacos-2', viewValue: 'Tacos', disabled: true}, {value: 'sandwich-3', viewValue: 'Sandwich'}, {value: 'chips-4', viewValue: 'Chips'}, {value: 'eggs-5', viewValue: 'Eggs'}, {value: 'pasta-6', viewValue: 'Pasta'}, {value: 'sushi-7', viewValue: 'Sushi'}, ]; control = new FormControl<string | null>(null); isRequired = false; tabIndexOverride!: number; selectable = false; @ViewChild(MatChipListbox) chipListbox!: MatChipListbox; @ViewChildren(MatChipOption) chips!: QueryList<MatChipOption>; } @Component({ template: ` <mat-chip-listbox [multiple]="true" [formControl]="control" [required]="isRequired" [tabIndex]="tabIndexOverride" [selectable]="selectable"> @for (food of foods; track food) { <mat-chip-option [value]="food.value" [disabled]="food.disabled"> {{ food.viewValue }} </mat-chip-option> } </mat-chip-listbox> `, imports: [MatChipListbox, MatChipOption, ReactiveFormsModule], changeDetection: ChangeDetectionStrategy.Eager, }) class MultiSelectionChipListbox { foods: any[] = [ {value: 'steak-0', viewValue: 'Steak'}, {value: 'pizza-1', viewValue: 'Pizza'}, {value: 'tacos-2', viewValue: 'Tacos', disabled: true}, {value: 'sandwich-3', viewValue: 'Sandwich'}, {value: 'chips-4', viewValue: 'Chips'}, {value: 'eggs-5', viewValue: 'Eggs'}, {value: 'pasta-6', viewValue: 'Pasta'}, {value: 'sushi-7', viewValue: 'Sushi'}, ]; control = new FormControl<string[] | null>(null); isRequired = false; tabIndexOverride!: number; selectable = false; @ViewChild(MatChipListbox) chipListbox!: MatChipListbox; @ViewChildren(MatChipOption) chips!: QueryList<MatChipOption>; } @Component({ template: ` <mat-chip-listbox [multiple]="true" [formControl]="control"> @for (chip of chips$ | async; track $index) { <mat-chip-option [value]="chip">{{ chip }}</mat-chip-option> } </mat-chip-listbox> `, imports: [MatChipListbox, MatChipOption, AsyncPipe, ReactiveFormsModule], changeDetection: ChangeDetectionStrategy.Eager, }) class AsyncMultiSelectionChipListbox { private _chipsSubject = new BehaviorSubject(['tutorial-1', 'tutorial-2', 'tutorial-3']); chips$: Observable<string[]> = this._chipsSubject.pipe(observeOn(asyncScheduler, 500)); control = new FormControl<string[] | null>(null); @ViewChild(MatChipListbox) chipListbox!: MatChipListbox; @ViewChildren(MatChipOption) chips!: QueryList<MatChipOption>; updateChips(chips: string[]): void { this._chipsSubject.next(chips); } } @Component({ template: ` <mat-chip-listbox [formControl]="control"> @for (food of foods; track food) { <mat-chip-option [value]="food.value">{{ food.viewValue }}</mat-chip-option> } </mat-chip-listbox> `, imports: [MatChipListbox, MatChipOption, ReactiveFormsModule], changeDetection: ChangeDetectionStrategy.Eager, }) class FalsyValueChipListbox { foods: any[] = [ {value: 0, viewValue: 'Steak'}, {value: 1, viewValue: 'Pizza'}, ]; control = new FormControl([] as number[]); @ViewChildren(MatChipOption) chips!: QueryList<MatChipOption>; } @Component({ template: ` <mat-chip-listbox> @for (food of foods; track food) { <mat-chip-option [value]="food.value" [selected]="food.selected"> {{ food.viewValue }} </mat-chip-option> } </mat-chip-listbox> `, imports: [MatChipListbox, MatChipOption], changeDetection: ChangeDetectionStrategy.Eager, }) class SelectedChipListbox { foods: any[] = [ {value: 0, viewValue: 'Steak', selected: true}, {value: 1, viewValue: 'Pizza', selected: false}, {value: 2, viewValue: 'Pasta', selected: true}, ]; @ViewChildren(MatChipOption) chips!: QueryList<MatChipOption>; } @Component({ template: ` <mat-chip-listbox [formControl]="control" [required]="isRequired" [tabIndex]="tabIndexOverride" [selectable]="selectable"> @for (food of foods; track food) { <mat-chip-option [value]="food.value" [disabled]="food.disabled"> {{ food.viewValue }} </mat-chip-option> } </mat-chip-listbox> `, imports: [MatChipListbox, MatChipOption, ReactiveFormsModule], changeDetection: ChangeDetectionStrategy.Eager, }) class FalsyBasicChipListbox { foods: any[] = [ {value: 'steak-0', viewValue: 'Steak'}, {value: 'pizza-1', viewValue: 'Pizza'}, {value: 'tacos-2', viewValue: 'Tacos', disabled: true}, {value: 'sandwich-3', viewValue: 'Sandwich'}, {value: 'chips-4', viewValue: 'Chips'}, {value: 'eggs-5', viewValue: 'Eggs'}, {value: 'pasta-6', viewValue: 'Pasta'}, {value: 'sushi-7', viewValue: 'Sushi'}, ]; control = new FormControl<string | null>(null); isRequired = false; tabIndexOverride!: number; selectable = false; @ViewChild(MatChipListbox) chipListbox!: MatChipListbox; @ViewChildren(MatChipOption) chips!: QueryList<MatChipOption>; } // Based on #29783. @Component({ template: ` <form> <mat-chip-listbox name="test" [ngModel]="null"> <mat-chip-option value="1" disabled>Hello</mat-chip-option> </mat-chip-listbox> </form> `, imports: [MatChipListbox, MatChipOption, FormsModule], changeDetection: ChangeDetectionStrategy.Eager, }) class IndividuallyDisabledChipInsideForm { @ViewChild(MatChipOption) chip!: MatChipOption; }