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

447 lines
15 KiB
TypeScript

import {Directionality} from '@angular/cdk/bidi';
import {BACKSPACE, DELETE, ENTER, SPACE} from '@angular/cdk/keycodes';
import {
createKeyboardEvent,
dispatchEvent,
dispatchFakeEvent,
dispatchKeyboardEvent,
} from '@angular/cdk/testing/private';
import {Component, DebugElement, ElementRef, ViewChild} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync, flush, waitForAsync} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {Subject} from 'rxjs';
import {
MatChipEditInput,
MatChipEditedEvent,
MatChipEvent,
MatChipGrid,
MatChipRow,
MatChipsModule,
} from './index';
describe('Row Chips', () => {
let fixture: ComponentFixture<any>;
let chipDebugElement: DebugElement;
let chipNativeElement: HTMLElement;
let chipInstance: MatChipRow;
let dir = 'ltr';
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [MatChipsModule, SingleChip],
providers: [
{
provide: Directionality,
useFactory: () => ({
value: dir,
change: new Subject(),
}),
},
],
});
}));
describe('MatChipRow', () => {
let testComponent: SingleChip;
beforeEach(() => {
fixture = TestBed.createComponent(SingleChip);
fixture.detectChanges();
chipDebugElement = fixture.debugElement.query(By.directive(MatChipRow))!;
chipNativeElement = chipDebugElement.nativeElement;
chipInstance = chipDebugElement.injector.get<MatChipRow>(MatChipRow);
testComponent = fixture.debugElement.componentInstance;
});
describe('basic behaviors', () => {
it('adds the `mat-mdc-chip` class', () => {
expect(chipNativeElement.classList).toContain('mat-mdc-chip');
});
it('does not add the `mat-basic-chip` class', () => {
expect(chipNativeElement.classList).not.toContain('mat-basic-chip');
});
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 removal', () => {
spyOn(testComponent, 'chipRemove');
chipInstance.remove();
fixture.detectChanges();
expect(testComponent.chipRemove).toHaveBeenCalledWith({chip: chipInstance});
});
it('should have a tabindex', () => {
expect(chipNativeElement.getAttribute('tabindex')).toBe('-1');
});
it('should have the correct role', () => {
expect(chipNativeElement.getAttribute('role')).toBe('row');
});
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 removable is true', () => {
beforeEach(() => {
testComponent.removable = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
});
it('DELETE emits the (removed) event', () => {
const DELETE_EVENT = createKeyboardEvent('keydown', DELETE);
spyOn(testComponent, 'chipRemove');
dispatchEvent(chipNativeElement, DELETE_EVENT);
fixture.detectChanges();
expect(testComponent.chipRemove).toHaveBeenCalled();
});
it('BACKSPACE emits the (removed) event', () => {
const BACKSPACE_EVENT = createKeyboardEvent('keydown', BACKSPACE);
spyOn(testComponent, 'chipRemove');
dispatchEvent(chipNativeElement, BACKSPACE_EVENT);
fixture.detectChanges();
expect(testComponent.chipRemove).toHaveBeenCalled();
});
it('should not remove for repeated BACKSPACE event', () => {
const BACKSPACE_EVENT = createKeyboardEvent('keydown', BACKSPACE);
Object.defineProperty(BACKSPACE_EVENT, 'repeat', {
get: () => true,
});
spyOn(testComponent, 'chipRemove');
dispatchEvent(chipNativeElement, BACKSPACE_EVENT);
fixture.detectChanges();
expect(testComponent.chipRemove).not.toHaveBeenCalled();
});
});
describe('when removable is false', () => {
beforeEach(() => {
testComponent.removable = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
});
it('DELETE does not emit the (removed) event', () => {
const DELETE_EVENT = createKeyboardEvent('keydown', DELETE);
spyOn(testComponent, 'chipRemove');
dispatchEvent(chipNativeElement, DELETE_EVENT);
fixture.detectChanges();
expect(testComponent.chipRemove).not.toHaveBeenCalled();
});
it('BACKSPACE does not emit the (removed) event', () => {
const BACKSPACE_EVENT = createKeyboardEvent('keydown', BACKSPACE);
spyOn(testComponent, 'chipRemove');
// Use the delete to remove the chip
dispatchEvent(chipNativeElement, BACKSPACE_EVENT);
fixture.detectChanges();
expect(testComponent.chipRemove).not.toHaveBeenCalled();
});
});
it('should update the aria-label for disabled chips', () => {
const primaryActionElement = chipNativeElement.querySelector(
'.mdc-evolution-chip__action--primary',
)!;
expect(primaryActionElement.getAttribute('aria-disabled')).toBe('false');
testComponent.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(primaryActionElement.getAttribute('aria-disabled')).toBe('true');
});
describe('focus management', () => {
it('sends focus to first grid cell on root chip focus', () => {
dispatchFakeEvent(chipNativeElement, 'focus');
fixture.detectChanges();
expect(document.activeElement).toHaveClass('mdc-evolution-chip__action--primary');
});
it('emits focus only once for multiple focus() calls', () => {
let counter = 0;
chipInstance._onFocus.subscribe(() => {
counter++;
});
chipInstance.focus();
chipInstance.focus();
fixture.detectChanges();
expect(counter).toBe(1);
});
});
});
describe('editable behavior', () => {
beforeEach(() => {
testComponent.editable = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
});
it('should begin editing on double click', () => {
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
dispatchFakeEvent(chipNativeElement, 'dblclick');
fixture.detectChanges();
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeTruthy();
});
it('should begin editing on ENTER', () => {
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
dispatchKeyboardEvent(chipNativeElement, 'keydown', ENTER);
fixture.detectChanges();
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeTruthy();
});
});
describe('editing behavior', () => {
let editInputInstance: MatChipEditInput;
let primaryAction: HTMLElement;
beforeEach(fakeAsync(() => {
testComponent.editable = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
dispatchFakeEvent(chipNativeElement, 'dblclick');
fixture.detectChanges();
flush();
spyOn(testComponent, 'chipEdit');
const editInputDebugElement = fixture.debugElement.query(By.directive(MatChipEditInput))!;
editInputInstance = editInputDebugElement.injector.get<MatChipEditInput>(MatChipEditInput);
primaryAction = chipNativeElement.querySelector('.mdc-evolution-chip__action--primary')!;
}));
function keyDownOnPrimaryAction(keyCode: number, key: string) {
const keyDownEvent = createKeyboardEvent('keydown', keyCode, key);
dispatchEvent(primaryAction, keyDownEvent);
fixture.detectChanges();
}
function getEditInput(): HTMLElement {
return chipNativeElement.querySelector('.mat-chip-edit-input')!;
}
it('should set the role of the primary action to gridcell', () => {
testComponent.editable = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(primaryAction.getAttribute('role')).toBe('gridcell');
testComponent.editable = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
// Test regression of bug where element is mislabeled as a button role. Element that does not perform its
// action on click event is not a button by ARIA spec (#27106).
expect(primaryAction.getAttribute('role')).toBe('gridcell');
});
it('should not delete the chip on DELETE or BACKSPACE', () => {
spyOn(testComponent, 'chipDestroy');
keyDownOnPrimaryAction(DELETE, 'Delete');
keyDownOnPrimaryAction(BACKSPACE, 'Backspace');
expect(testComponent.chipDestroy).not.toHaveBeenCalled();
});
it('should stop editing on blur', fakeAsync(() => {
chipInstance._onBlur.next();
flush();
expect(testComponent.chipEdit).toHaveBeenCalled();
}));
it('should stop editing on ENTER', fakeAsync(() => {
dispatchKeyboardEvent(getEditInput(), 'keydown', ENTER);
fixture.detectChanges();
flush();
expect(testComponent.chipEdit).toHaveBeenCalled();
}));
it('should emit the new chip value when editing completes', fakeAsync(() => {
const chipValue = 'chip value';
editInputInstance.setValue(chipValue);
dispatchKeyboardEvent(getEditInput(), 'keydown', ENTER);
flush();
const expectedValue = jasmine.objectContaining({value: chipValue});
expect(testComponent.chipEdit).toHaveBeenCalledWith(expectedValue);
}));
it('should use the projected edit input if provided', () => {
expect(editInputInstance.getNativeElement()).toHaveClass('projected-edit-input');
});
it('should use the default edit input if none is projected', () => {
keyDownOnPrimaryAction(ENTER, 'Enter');
testComponent.useCustomEditInput = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
dispatchFakeEvent(chipNativeElement, 'dblclick');
fixture.detectChanges();
const editInputDebugElement = fixture.debugElement.query(By.directive(MatChipEditInput))!;
const editInputNoProject =
editInputDebugElement.injector.get<MatChipEditInput>(MatChipEditInput);
expect(editInputNoProject.getNativeElement()).not.toHaveClass('projected-edit-input');
});
it('should focus the chip content if the edit input has focus on completion', fakeAsync(() => {
const chipValue = 'chip value';
editInputInstance.setValue(chipValue);
dispatchKeyboardEvent(getEditInput(), 'keydown', ENTER);
fixture.detectChanges();
flush();
expect(document.activeElement).toBe(primaryAction);
}));
it('should not change focus if another element has focus on completion', fakeAsync(() => {
const chipValue = 'chip value';
editInputInstance.setValue(chipValue);
testComponent.chipInput.nativeElement.focus();
keyDownOnPrimaryAction(ENTER, 'Enter');
flush();
expect(document.activeElement).not.toBe(primaryAction);
}));
it('should not prevent SPACE events when editing', fakeAsync(() => {
const event = dispatchKeyboardEvent(getEditInput(), 'keydown', SPACE);
fixture.detectChanges();
flush();
expect(event.defaultPrevented).toBe(false);
}));
});
describe('a11y', () => {
it('should apply `ariaLabel` and `ariaDesciption` to the primary gridcell', () => {
fixture.componentInstance.ariaLabel = 'chip name';
fixture.componentInstance.ariaDescription = 'chip description';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const primaryGridCell = (fixture.nativeElement as HTMLElement).querySelector(
'[role="gridcell"].mdc-evolution-chip__cell--primary.mat-mdc-chip-action',
);
expect(primaryGridCell)
.withContext('expected to find the grid cell for the primary chip action')
.toBeTruthy();
expect(primaryGridCell!.getAttribute('aria-label')).toMatch(/chip name/i);
const primaryGridCellDescribedBy = primaryGridCell!.getAttribute('aria-describedby');
expect(primaryGridCellDescribedBy)
.withContext('expected primary grid cell to have a non-empty aria-describedby attribute')
.toBeTruthy();
const primaryGridCellDescriptions = Array.from(
(fixture.nativeElement as HTMLElement).querySelectorAll(
primaryGridCellDescribedBy!
.split(/\s+/g)
.map(x => `#${x}`)
.join(','),
),
);
const primaryGridCellDescription = primaryGridCellDescriptions
.map(x => x.textContent?.trim())
.join(' ')
.trim();
expect(primaryGridCellDescription).toMatch(/chip description/i);
});
});
});
});
@Component({
template: `
<mat-chip-grid #chipGrid>
@if (shouldShow) {
<div>
<mat-chip-row [removable]="removable"
[color]="color" [disabled]="disabled" [editable]="editable"
(destroyed)="chipDestroy($event)"
(removed)="chipRemove($event)" (edited)="chipEdit($event)"
[aria-label]="ariaLabel" [aria-description]="ariaDescription">
{{name}}
<button matChipRemove>x</button>
@if (useCustomEditInput) {
<span class="projected-edit-input" matChipEditInput></span>
}
</mat-chip-row>
<input matInput [matChipInputFor]="chipGrid" #chipInput>
</div>
}
</mat-chip-grid>`,
standalone: true,
imports: [MatChipsModule],
})
class SingleChip {
@ViewChild(MatChipGrid) chipList: MatChipGrid;
@ViewChild('chipInput') chipInput: ElementRef;
disabled: boolean = false;
name: string = 'Test';
color: string = 'primary';
removable: boolean = true;
shouldShow: boolean = true;
editable: boolean = false;
useCustomEditInput: boolean = true;
ariaLabel: string | null = null;
ariaDescription: string | null = null;
chipDestroy: (event?: MatChipEvent) => void = () => {};
chipRemove: (event?: MatChipEvent) => void = () => {};
chipEdit: (event?: MatChipEditedEvent) => void = () => {};
}