sass-references/angular-material/material/chips/chip-option.spec.ts

444 lines
15 KiB
TypeScript
Raw Normal View History

2024-12-06 10:42:08 +08:00
import {Directionality} from '@angular/cdk/bidi';
import {ENTER, SPACE} from '@angular/cdk/keycodes';
import {dispatchFakeEvent, dispatchKeyboardEvent} from '@angular/cdk/testing/private';
import {Component, DebugElement, ViewChild} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync, flush, waitForAsync} from '@angular/core/testing';
import {MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions} from '@angular/material/core';
import {By} from '@angular/platform-browser';
import {Subject} from 'rxjs';
import {
MAT_CHIPS_DEFAULT_OPTIONS,
MatChipEvent,
MatChipListbox,
MatChipOption,
MatChipSelectionChange,
MatChipsDefaultOptions,
MatChipsModule,
} from './index';
describe('Option Chips', () => {
let fixture: ComponentFixture<any>;
let chipDebugElement: DebugElement;
let chipNativeElement: HTMLElement;
let primaryAction: HTMLElement;
let chipInstance: MatChipOption;
let globalRippleOptions: RippleGlobalOptions;
let dir = 'ltr';
let hideSingleSelectionIndicator: boolean | undefined;
beforeEach(waitForAsync(() => {
globalRippleOptions = {};
const defaultOptions: MatChipsDefaultOptions = {
separatorKeyCodes: [ENTER, SPACE],
hideSingleSelectionIndicator,
};
TestBed.configureTestingModule({
imports: [MatChipsModule],
providers: [
{provide: MAT_RIPPLE_GLOBAL_OPTIONS, useFactory: () => globalRippleOptions},
{
provide: Directionality,
useFactory: () => ({
value: dir,
change: new Subject(),
}),
},
{provide: MAT_CHIPS_DEFAULT_OPTIONS, useFactory: () => defaultOptions},
],
declarations: [SingleChip],
});
}));
describe('MatChipOption', () => {
let testComponent: SingleChip;
beforeEach(() => {
fixture = TestBed.createComponent(SingleChip);
fixture.detectChanges();
chipDebugElement = fixture.debugElement.query(By.directive(MatChipOption))!;
chipNativeElement = chipDebugElement.nativeElement;
chipInstance = chipDebugElement.injector.get<MatChipOption>(MatChipOption);
primaryAction = chipNativeElement.querySelector('.mdc-evolution-chip__action--primary')!;
testComponent = fixture.debugElement.componentInstance;
});
describe('basic behaviors', () => {
it('adds the `mat-chip` class', () => {
expect(chipNativeElement.classList).toContain('mat-mdc-chip');
});
it('emits focus only once for multiple clicks', () => {
let counter = 0;
chipInstance._onFocus.subscribe(() => {
counter++;
});
primaryAction.focus();
primaryAction.focus();
fixture.detectChanges();
expect(counter).toBe(1);
});
it('emits destroy on destruction', () => {
spyOn(testComponent, 'chipDestroy').and.callThrough();
// Force a destroy callback
testComponent.shouldShow = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(testComponent.chipDestroy).toHaveBeenCalledTimes(1);
});
it('allows color customization', () => {
expect(chipNativeElement.classList).toContain('mat-primary');
testComponent.color = 'warn';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(chipNativeElement.classList).not.toContain('mat-primary');
expect(chipNativeElement.classList).toContain('mat-warn');
});
it('allows selection', () => {
spyOn(testComponent, 'chipSelectionChange');
expect(chipNativeElement.classList).not.toContain('mat-mdc-chip-selected');
testComponent.selected = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(chipNativeElement.classList).toContain('mat-mdc-chip-selected');
expect(testComponent.chipSelectionChange).toHaveBeenCalledWith({
source: chipInstance,
isUserInput: false,
selected: true,
});
});
it('should not prevent the default click action', () => {
const event = dispatchFakeEvent(chipNativeElement, 'click');
fixture.detectChanges();
expect(event.defaultPrevented).toBe(false);
});
it('should not dispatch `selectionChange` event when deselecting a non-selected chip', () => {
chipInstance.deselect();
const spy = jasmine.createSpy('selectionChange spy');
const subscription = chipInstance.selectionChange.subscribe(spy);
chipInstance.deselect();
expect(spy).not.toHaveBeenCalled();
subscription.unsubscribe();
});
it('should not dispatch `selectionChange` event when selecting a selected chip', () => {
chipInstance.select();
const spy = jasmine.createSpy('selectionChange spy');
const subscription = chipInstance.selectionChange.subscribe(spy);
chipInstance.select();
expect(spy).not.toHaveBeenCalled();
subscription.unsubscribe();
});
it(
'should not dispatch `selectionChange` event when selecting a selected chip via ' +
'user interaction',
() => {
chipInstance.select();
const spy = jasmine.createSpy('selectionChange spy');
const subscription = chipInstance.selectionChange.subscribe(spy);
chipInstance.selectViaInteraction();
expect(spy).not.toHaveBeenCalled();
subscription.unsubscribe();
},
);
it('should not dispatch `selectionChange` through setter if the value did not change', () => {
chipInstance.selected = false;
const spy = jasmine.createSpy('selectionChange spy');
const subscription = chipInstance.selectionChange.subscribe(spy);
chipInstance.selected = false;
expect(spy).not.toHaveBeenCalled();
subscription.unsubscribe();
});
it('should be able to disable ripples through ripple global options at runtime', () => {
expect(chipInstance._isRippleDisabled())
.withContext('Expected chip ripples to be enabled.')
.toBe(false);
globalRippleOptions.disabled = true;
expect(chipInstance._isRippleDisabled())
.withContext('Expected chip ripples to be disabled.')
.toBe(true);
});
it('should have the correct role', () => {
expect(chipNativeElement.getAttribute('role')).toBe('presentation');
});
it('should be able to set a custom role', () => {
chipInstance.role = 'button';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(chipNativeElement.getAttribute('role')).toBe('button');
});
});
describe('keyboard behavior', () => {
describe('when selectable is true', () => {
beforeEach(() => {
testComponent.selectable = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
});
it('should selects/deselects the currently focused chip on SPACE', () => {
const CHIP_SELECTED_EVENT: MatChipSelectionChange = {
source: chipInstance,
isUserInput: true,
selected: true,
};
const CHIP_DESELECTED_EVENT: MatChipSelectionChange = {
source: chipInstance,
isUserInput: true,
selected: false,
};
spyOn(testComponent, 'chipSelectionChange');
// Use the spacebar to select the chip
dispatchKeyboardEvent(primaryAction, 'keydown', SPACE);
fixture.detectChanges();
expect(chipInstance.selected).toBeTruthy();
expect(testComponent.chipSelectionChange).toHaveBeenCalledTimes(1);
expect(testComponent.chipSelectionChange).toHaveBeenCalledWith(CHIP_SELECTED_EVENT);
// Use the spacebar to deselect the chip
dispatchKeyboardEvent(primaryAction, 'keydown', SPACE);
fixture.detectChanges();
expect(chipInstance.selected).toBeFalsy();
expect(testComponent.chipSelectionChange).toHaveBeenCalledTimes(2);
expect(testComponent.chipSelectionChange).toHaveBeenCalledWith(CHIP_DESELECTED_EVENT);
});
it('should have correct aria-selected in single selection mode', () => {
expect(primaryAction.getAttribute('aria-selected')).toBe('false');
testComponent.selected = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(primaryAction.getAttribute('aria-selected')).toBe('true');
});
it('should have the correct aria-selected in multi-selection mode', fakeAsync(() => {
testComponent.chipList.multiple = true;
fixture.changeDetectorRef.markForCheck();
flush();
fixture.detectChanges();
expect(primaryAction.getAttribute('aria-selected')).toBe('false');
testComponent.selected = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(primaryAction.getAttribute('aria-selected')).toBe('true');
}));
it('should disable focus on the checkmark', fakeAsync(() => {
// The checkmark is only shown in multi selection mode.
testComponent.chipList.multiple = true;
fixture.changeDetectorRef.markForCheck();
flush();
fixture.detectChanges();
const checkmark = chipNativeElement.querySelector('.mdc-evolution-chip__checkmark-svg')!;
expect(checkmark.getAttribute('focusable')).toBe('false');
}));
});
describe('when selectable is false', () => {
beforeEach(() => {
testComponent.selectable = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
});
it('SPACE ignores selection', () => {
spyOn(testComponent, 'chipSelectionChange');
// Use the spacebar to attempt to select the chip
dispatchKeyboardEvent(primaryAction, 'keydown', SPACE);
fixture.detectChanges();
expect(chipInstance.selected).toBe(false);
expect(testComponent.chipSelectionChange).not.toHaveBeenCalled();
});
it('should not have the aria-selected attribute', () => {
expect(primaryAction.hasAttribute('aria-selected')).toBe(false);
});
});
it('should update the aria-disabled for disabled chips', () => {
expect(primaryAction.getAttribute('aria-disabled')).toBe('false');
testComponent.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(primaryAction.getAttribute('aria-disabled')).toBe('true');
});
it('should display checkmark graphic by default', () => {
expect(
fixture.debugElement.injector.get(MAT_CHIPS_DEFAULT_OPTIONS)
?.hideSingleSelectionIndicator,
)
.withContext(
'expected not to have a default value set for `hideSingleSelectionIndicator`',
)
.toBeUndefined();
expect(chipNativeElement.querySelector('.mat-mdc-chip-graphic')).toBeTruthy();
expect(chipNativeElement.classList).toContain('mdc-evolution-chip--with-primary-graphic');
});
});
describe('a11y', () => {
it('should apply `ariaLabel` and `ariaDesciption` to the element with option role', () => {
testComponent.ariaLabel = 'option name';
testComponent.ariaDescription = 'option description';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const optionElement = fixture.nativeElement.querySelector('[role="option"]') as HTMLElement;
expect(optionElement)
.withContext('expected to find an element with option role')
.toBeTruthy();
expect(optionElement.getAttribute('aria-label')).toMatch(/option name/i);
const optionElementDescribedBy = optionElement!.getAttribute('aria-describedby');
expect(optionElementDescribedBy)
.withContext('expected primary grid cell to have a non-empty aria-describedby attribute')
.toBeTruthy();
const optionElementDescriptions = Array.from(
(fixture.nativeElement as HTMLElement).querySelectorAll(
optionElementDescribedBy!
.split(/\s+/g)
.map(x => `#${x}`)
.join(','),
),
);
const optionElementDescription = optionElementDescriptions
.map(x => x.textContent?.trim())
.join(' ')
.trim();
expect(optionElementDescription).toMatch(/option description/i);
});
it('should display checkmark graphic by default', () => {
expect(chipNativeElement.querySelector('.mat-mdc-chip-graphic')).toBeTruthy();
expect(chipNativeElement.classList).toContain('mdc-evolution-chip--with-primary-graphic');
});
});
describe('with token to hide single-selection checkmark indicator', () => {
beforeAll(() => {
hideSingleSelectionIndicator = true;
});
afterAll(() => {
hideSingleSelectionIndicator = undefined;
});
it('does not display checkmark graphic', () => {
expect(chipNativeElement.querySelector('.mat-mdc-chip-graphic')).toBeNull();
expect(chipNativeElement.classList).not.toContain(
'mdc-evolution-chip--with-primary-graphic',
);
});
it('displays checkmark graphic when avatar is provided', () => {
testComponent.selected = true;
testComponent.avatarLabel = 'A';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(chipNativeElement.querySelector('.mat-mdc-chip-graphic')).toBeTruthy();
expect(chipNativeElement.classList).toContain('mdc-evolution-chip--with-primary-graphic');
});
});
it('should contain a focus indicator inside the text label', () => {
const label = chipNativeElement.querySelector('.mdc-evolution-chip__text-label');
expect(label?.querySelector('.mat-focus-indicator')).toBeTruthy();
});
});
});
@Component({
template: `
<mat-chip-listbox>
@if (shouldShow) {
<div>
<mat-chip-option [selectable]="selectable"
[color]="color" [selected]="selected" [disabled]="disabled"
(destroyed)="chipDestroy($event)"
(selectionChange)="chipSelectionChange($event)"
[aria-label]="ariaLabel" [aria-description]="ariaDescription">
@if (avatarLabel) {
<span class="avatar" matChipAvatar>{{avatarLabel}}</span>
}
{{name}}
</mat-chip-option>
</div>
}
</mat-chip-listbox>`,
standalone: false,
})
class SingleChip {
@ViewChild(MatChipListbox) chipList: MatChipListbox;
disabled: boolean = false;
name: string = 'Test';
color: string = 'primary';
selected: boolean = false;
selectable: boolean = true;
shouldShow: boolean = true;
ariaLabel: string | null = null;
ariaDescription: string | null = null;
avatarLabel: string | null = null;
chipDestroy: (event?: MatChipEvent) => void = () => {};
chipSelectionChange: (event?: MatChipSelectionChange) => void = () => {};
}