sass-references/angular-material/material/expansion/expansion.spec.ts

680 lines
22 KiB
TypeScript

import {ENTER, SPACE} from '@angular/cdk/keycodes';
import {
createKeyboardEvent,
dispatchEvent,
dispatchKeyboardEvent,
} from '@angular/cdk/testing/private';
import {Component, ViewChild} from '@angular/core';
import {
ComponentFixture,
TestBed,
fakeAsync,
flush,
tick,
waitForAsync,
} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {
MAT_EXPANSION_PANEL_DEFAULT_OPTIONS,
MatExpansionModule,
MatExpansionPanel,
MatExpansionPanelHeader,
} from './index';
describe('MatExpansionPanel', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
MatExpansionModule,
NoopAnimationsModule,
PanelWithContent,
PanelWithContentInNgIf,
PanelWithCustomMargin,
LazyPanelWithContent,
LazyPanelOpenOnLoad,
PanelWithTwoWayBinding,
PanelWithHeaderTabindex,
NestedLazyPanelWithContent,
],
});
}));
it('should expand and collapse the panel', fakeAsync(() => {
const fixture = TestBed.createComponent(PanelWithContent);
const headerEl = fixture.nativeElement.querySelector('.mat-expansion-panel-header');
fixture.detectChanges();
expect(headerEl.classList).not.toContain('mat-expanded');
fixture.componentInstance.expanded = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
flush();
expect(headerEl.classList).toContain('mat-expanded');
}));
it('should add strong focus indication', () => {
const fixture = TestBed.createComponent(PanelWithContent);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.mat-expansion-panel-header').classList).toContain(
'mat-focus-indicator',
);
});
it('should be able to render panel content lazily', () => {
const fixture = TestBed.createComponent(LazyPanelWithContent);
const content = fixture.debugElement.query(
By.css('.mat-expansion-panel-content'),
)!.nativeElement;
fixture.detectChanges();
expect(content.textContent.trim())
.withContext('Expected content element to be empty.')
.toBe('');
fixture.componentInstance.expanded = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(content.textContent.trim())
.withContext('Expected content to be rendered.')
.toContain('Some content');
});
it('should render the content for a lazy-loaded panel that is opened on init', () => {
const fixture = TestBed.createComponent(LazyPanelOpenOnLoad);
const content = fixture.debugElement.query(
By.css('.mat-expansion-panel-content'),
)!.nativeElement;
fixture.detectChanges();
expect(content.textContent.trim())
.withContext('Expected content to be rendered.')
.toContain('Some content');
});
it('should not render lazy content from a child panel inside the parent', () => {
const fixture = TestBed.createComponent(NestedLazyPanelWithContent);
fixture.componentInstance.parentExpanded = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const parentContent: HTMLElement = fixture.nativeElement.querySelector(
'.parent-panel .mat-expansion-panel-content',
);
const childContent: HTMLElement = fixture.nativeElement.querySelector(
'.child-panel .mat-expansion-panel-content',
);
expect(parentContent.textContent!.trim()).toBe(
'Parent content',
'Expected only parent content to be rendered.',
);
expect(childContent.textContent!.trim()).toBe(
'',
'Expected child content element to be empty.',
);
fixture.componentInstance.childExpanded = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(childContent.textContent!.trim()).toBe(
'Child content',
'Expected child content element to be rendered.',
);
});
it('emit correct events for change in panel expanded state', () => {
const fixture = TestBed.createComponent(PanelWithContent);
fixture.componentInstance.expanded = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(fixture.componentInstance.openCallback).toHaveBeenCalled();
fixture.componentInstance.expanded = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(fixture.componentInstance.closeCallback).toHaveBeenCalled();
});
it('should create a unique panel id for each panel', () => {
const fixtureOne = TestBed.createComponent(PanelWithContent);
const headerElOne = fixtureOne.nativeElement.querySelector('.mat-expansion-panel-header');
const fixtureTwo = TestBed.createComponent(PanelWithContent);
const headerElTwo = fixtureTwo.nativeElement.querySelector('.mat-expansion-panel-header');
fixtureOne.detectChanges();
fixtureTwo.detectChanges();
const panelIdOne = headerElOne.getAttribute('aria-controls');
const panelIdTwo = headerElTwo.getAttribute('aria-controls');
expect(panelIdOne).not.toBe(panelIdTwo);
});
it('should set `aria-labelledby` of the content to the header id', () => {
const fixture = TestBed.createComponent(PanelWithContent);
const headerEl = fixture.nativeElement.querySelector('.mat-expansion-panel-header');
const contentEl = fixture.nativeElement.querySelector('.mat-expansion-panel-content');
fixture.detectChanges();
const headerId = headerEl.getAttribute('id');
const contentLabel = contentEl.getAttribute('aria-labelledby');
expect(headerId).toBeTruthy();
expect(contentLabel).toBeTruthy();
expect(headerId).toBe(contentLabel);
});
it('should set the proper role on the content element', () => {
const fixture = TestBed.createComponent(PanelWithContent);
const contentEl = fixture.nativeElement.querySelector('.mat-expansion-panel-content');
expect(contentEl.getAttribute('role')).toBe('region');
});
it('should toggle the panel when pressing SPACE on the header', () => {
const fixture = TestBed.createComponent(PanelWithContent);
fixture.detectChanges();
const headerEl = fixture.nativeElement.querySelector('.mat-expansion-panel-header');
spyOn(fixture.componentInstance.panel, 'toggle');
const event = dispatchKeyboardEvent(headerEl, 'keydown', SPACE);
fixture.detectChanges();
expect(fixture.componentInstance.panel.toggle).toHaveBeenCalled();
expect(event.defaultPrevented).toBe(true);
});
it('should toggle the panel when pressing ENTER on the header', () => {
const fixture = TestBed.createComponent(PanelWithContent);
fixture.detectChanges();
const headerEl = fixture.nativeElement.querySelector('.mat-expansion-panel-header');
spyOn(fixture.componentInstance.panel, 'toggle');
const event = dispatchKeyboardEvent(headerEl, 'keydown', ENTER);
fixture.detectChanges();
expect(fixture.componentInstance.panel.toggle).toHaveBeenCalled();
expect(event.defaultPrevented).toBe(true);
});
it('should not toggle if a modifier key is pressed', () => {
const fixture = TestBed.createComponent(PanelWithContent);
fixture.detectChanges();
const headerEl = fixture.nativeElement.querySelector('.mat-expansion-panel-header');
spyOn(fixture.componentInstance.panel, 'toggle');
['altKey', 'metaKey', 'shiftKey', 'ctrlKey'].forEach(modifier => {
const event = createKeyboardEvent('keydown', ENTER);
Object.defineProperty(event, modifier, {get: () => true});
dispatchEvent(headerEl, event);
fixture.detectChanges();
expect(fixture.componentInstance.panel.toggle).not.toHaveBeenCalled();
expect(event.defaultPrevented).toBe(false);
});
});
it('should not be able to focus content while closed', () => {
const fixture = TestBed.createComponent(PanelWithContent);
fixture.componentInstance.expanded = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const wrapper = fixture.nativeElement.querySelector('.mat-expansion-panel-content-wrapper');
expect(wrapper.hasAttribute('inert')).toBe(false);
fixture.componentInstance.expanded = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(wrapper.hasAttribute('inert')).toBe(true);
});
it('should restore focus to header if focused element is inside panel on close', fakeAsync(() => {
const fixture = TestBed.createComponent(PanelWithContent);
fixture.componentInstance.expanded = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick(250);
const button = fixture.debugElement.query(By.css('button'))!.nativeElement;
const header = fixture.debugElement.query(By.css('mat-expansion-panel-header'))!.nativeElement;
button.focus();
expect(document.activeElement)
.withContext('Expected button to start off focusable.')
.toBe(button);
fixture.componentInstance.expanded = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick(250);
expect(document.activeElement).withContext('Expected header to be focused.').toBe(header);
}));
it('should not change focus origin if origin not specified', fakeAsync(() => {
const fixture = TestBed.createComponent(PanelWithContent);
fixture.componentInstance.expanded = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick(250);
const header = fixture.debugElement.query(By.css('mat-expansion-panel-header'))!;
const headerInstance = header.componentInstance;
headerInstance.focus('mouse');
headerInstance.focus();
fixture.detectChanges();
tick(250);
expect(header.nativeElement.classList).toContain('cdk-focused');
expect(header.nativeElement.classList).toContain('cdk-mouse-focused');
}));
it('should not override the panel margin if it is not inside an accordion', fakeAsync(() => {
const fixture = TestBed.createComponent(PanelWithCustomMargin);
fixture.detectChanges();
const panel = fixture.debugElement.query(By.css('mat-expansion-panel'))!;
let styles = getComputedStyle(panel.nativeElement);
expect(panel.componentInstance._hasSpacing()).toBe(false);
expect(styles.marginTop).toBe('13px');
expect(styles.marginBottom).toBe('13px');
expect(styles.marginLeft).toBe('37px');
expect(styles.marginRight).toBe('37px');
fixture.componentInstance.expanded = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick(250);
styles = getComputedStyle(panel.nativeElement);
expect(panel.componentInstance._hasSpacing()).toBe(false);
expect(styles.marginTop).toBe('13px');
expect(styles.marginBottom).toBe('13px');
expect(styles.marginLeft).toBe('37px');
expect(styles.marginRight).toBe('37px');
}));
it('should be able to hide the toggle', () => {
const fixture = TestBed.createComponent(PanelWithContent);
const header = fixture.debugElement.query(By.css('.mat-expansion-panel-header'))!.nativeElement;
const content = fixture.debugElement.query(By.css('.mat-content'))!.nativeElement;
fixture.detectChanges();
expect(header.querySelector('.mat-expansion-indicator'))
.withContext('Expected indicator to be shown.')
.toBeTruthy();
expect(content.classList).not.toContain('mat-content-hide-toggle');
fixture.componentInstance.hideToggle = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(header.querySelector('.mat-expansion-indicator'))
.withContext('Expected indicator to be hidden.')
.toBeFalsy();
expect(content.classList).toContain('mat-content-hide-toggle');
});
it('should make sure accordion item runs ngOnDestroy when expansion panel is destroyed', () => {
const fixture = TestBed.createComponent(PanelWithContentInNgIf);
fixture.detectChanges();
let destroyedOk = false;
fixture.componentInstance.panel.destroyed.subscribe(() => (destroyedOk = true));
fixture.componentInstance.expansionShown = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(destroyedOk).toBe(true);
});
it('should support two-way binding of the `expanded` property', () => {
const fixture = TestBed.createComponent(PanelWithTwoWayBinding);
const header = fixture.debugElement.query(By.css('mat-expansion-panel-header'))!.nativeElement;
fixture.detectChanges();
expect(fixture.componentInstance.expanded).toBe(false);
header.click();
fixture.detectChanges();
expect(fixture.componentInstance.expanded).toBe(true);
header.click();
fixture.detectChanges();
expect(fixture.componentInstance.expanded).toBe(false);
});
it('should emit events for body expanding and collapsing animations', fakeAsync(() => {
const fixture = TestBed.createComponent(PanelWithContent);
fixture.detectChanges();
let afterExpand = 0;
let afterCollapse = 0;
fixture.componentInstance.panel.afterExpand.subscribe(() => afterExpand++);
fixture.componentInstance.panel.afterCollapse.subscribe(() => afterCollapse++);
fixture.componentInstance.expanded = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
flush();
expect(afterExpand).toBe(1);
expect(afterCollapse).toBe(0);
fixture.componentInstance.expanded = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
flush();
expect(afterExpand).toBe(1);
expect(afterCollapse).toBe(1);
}));
it('should be able to set the default options through the injection token', () => {
TestBed.resetTestingModule().configureTestingModule({
imports: [MatExpansionModule, NoopAnimationsModule],
providers: [
{
provide: MAT_EXPANSION_PANEL_DEFAULT_OPTIONS,
useValue: {
hideToggle: true,
expandedHeight: '10px',
collapsedHeight: '16px',
},
},
],
});
const fixture = TestBed.createComponent(PanelWithTwoWayBinding);
fixture.detectChanges();
const panel = fixture.debugElement.query(By.directive(MatExpansionPanel))!;
const header = fixture.debugElement.query(By.directive(MatExpansionPanelHeader))!;
expect(panel.componentInstance.hideToggle).toBe(true);
expect(header.componentInstance.expandedHeight).toBe('10px');
expect(header.componentInstance.collapsedHeight).toBe('16px');
expect(header.nativeElement.style.height).toBe('16px');
fixture.componentInstance.expanded = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(header.nativeElement.style.height).toBe('10px');
});
it('should be able to set a custom tabindex on the header', () => {
const fixture = TestBed.createComponent(PanelWithHeaderTabindex);
const headerEl = fixture.nativeElement.querySelector('.mat-expansion-panel-header');
fixture.detectChanges();
expect(headerEl.getAttribute('tabindex')).toBe('7');
});
describe('disabled state', () => {
let fixture: ComponentFixture<PanelWithContent>;
let panel: HTMLElement;
let header: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(PanelWithContent);
fixture.detectChanges();
panel = fixture.debugElement.query(By.css('mat-expansion-panel'))!.nativeElement;
header = fixture.debugElement.query(By.css('mat-expansion-panel-header'))!.nativeElement;
});
it('should toggle the aria-disabled attribute on the header', () => {
expect(header.getAttribute('aria-disabled')).toBe('false');
fixture.componentInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(header.getAttribute('aria-disabled')).toBe('true');
});
it('should toggle the expansion indicator', () => {
expect(panel.querySelector('.mat-expansion-indicator')).toBeTruthy();
fixture.componentInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(panel.querySelector('.mat-expansion-indicator')).toBeFalsy();
});
it('should not be able to toggle the panel via a user action if disabled', () => {
expect(fixture.componentInstance.panel.expanded).toBe(false);
expect(header.classList).not.toContain('mat-expanded');
fixture.componentInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
header.click();
fixture.detectChanges();
expect(fixture.componentInstance.panel.expanded).toBe(false);
expect(header.classList).not.toContain('mat-expanded');
});
it('should be able to toggle a disabled expansion panel programmatically', () => {
expect(fixture.componentInstance.panel.expanded).toBe(false);
expect(header.classList).not.toContain('mat-expanded');
fixture.componentInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
fixture.componentInstance.expanded = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(fixture.componentInstance.panel.expanded).toBe(true);
expect(header.classList).toContain('mat-expanded');
});
it(
'should be able to toggle a disabled expansion panel programmatically via the ' +
'open/close methods',
() => {
const panelInstance = fixture.componentInstance.panel;
expect(panelInstance.expanded).toBe(false);
expect(header.classList).not.toContain('mat-expanded');
fixture.componentInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
panelInstance.open();
fixture.detectChanges();
expect(panelInstance.expanded).toBe(true);
expect(header.classList).toContain('mat-expanded');
panelInstance.close();
fixture.detectChanges();
expect(panelInstance.expanded).toBe(false);
expect(header.classList).not.toContain('mat-expanded');
},
);
it(
'should be able to toggle a disabled expansion panel programmatically via the ' +
'toggle method',
() => {
const panelInstance = fixture.componentInstance.panel;
expect(panelInstance.expanded).toBe(false);
expect(header.classList).not.toContain('mat-expanded');
fixture.componentInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
panelInstance.toggle();
fixture.detectChanges();
expect(panelInstance.expanded).toBe(true);
expect(header.classList).toContain('mat-expanded');
panelInstance.toggle();
fixture.detectChanges();
expect(panelInstance.expanded).toBe(false);
expect(header.classList).not.toContain('mat-expanded');
},
);
it('should update the tabindex if the header becomes disabled', () => {
expect(header.getAttribute('tabindex')).toBe('0');
fixture.componentInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(header.getAttribute('tabindex')).toBe('-1');
});
});
});
@Component({
template: `
<mat-expansion-panel [expanded]="expanded"
[hideToggle]="hideToggle"
[disabled]="disabled"
(opened)="openCallback()"
(closed)="closeCallback()">
<mat-expansion-panel-header>Panel Title</mat-expansion-panel-header>
<p>Some content</p>
<button>I am a button</button>
</mat-expansion-panel>`,
standalone: true,
imports: [MatExpansionModule],
})
class PanelWithContent {
expanded = false;
hideToggle = false;
disabled = false;
openCallback = jasmine.createSpy('openCallback');
closeCallback = jasmine.createSpy('closeCallback');
@ViewChild(MatExpansionPanel) panel: MatExpansionPanel;
}
@Component({
template: `
@if (expansionShown) {
<div>
<mat-expansion-panel>
<mat-expansion-panel-header>Panel Title</mat-expansion-panel-header>
</mat-expansion-panel>
</div>
}
`,
standalone: true,
imports: [MatExpansionModule],
})
class PanelWithContentInNgIf {
expansionShown = true;
@ViewChild(MatExpansionPanel) panel: MatExpansionPanel;
}
@Component({
styles: `mat-expansion-panel {
margin: 13px 37px;
}`,
template: `
<mat-expansion-panel [expanded]="expanded">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolores officia, aliquam dicta
corrupti maxime voluptate accusamus impedit atque incidunt pariatur.
</mat-expansion-panel>`,
standalone: true,
imports: [MatExpansionModule],
})
class PanelWithCustomMargin {
expanded = false;
}
@Component({
template: `
<mat-expansion-panel [expanded]="expanded">
<mat-expansion-panel-header>Panel Title</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<p>Some content</p>
<button>I am a button</button>
</ng-template>
</mat-expansion-panel>`,
standalone: true,
imports: [MatExpansionModule],
})
class LazyPanelWithContent {
expanded = false;
}
@Component({
template: `
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>Panel Title</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<p>Some content</p>
</ng-template>
</mat-expansion-panel>`,
standalone: true,
imports: [MatExpansionModule],
})
class LazyPanelOpenOnLoad {}
@Component({
template: `
<mat-expansion-panel [(expanded)]="expanded">
<mat-expansion-panel-header>Panel Title</mat-expansion-panel-header>
</mat-expansion-panel>`,
standalone: true,
imports: [MatExpansionModule],
})
class PanelWithTwoWayBinding {
expanded = false;
}
@Component({
template: `
<mat-expansion-panel>
<mat-expansion-panel-header tabindex="7">Panel Title</mat-expansion-panel-header>
</mat-expansion-panel>`,
standalone: true,
imports: [MatExpansionModule],
})
class PanelWithHeaderTabindex {}
@Component({
template: `
<mat-expansion-panel class="parent-panel" [expanded]="parentExpanded">
Parent content
<mat-expansion-panel class="child-panel" [expanded]="childExpanded">
<ng-template matExpansionPanelContent>Child content</ng-template>
</mat-expansion-panel>
</mat-expansion-panel>
`,
standalone: true,
imports: [MatExpansionModule],
})
class NestedLazyPanelWithContent {
parentExpanded = false;
childExpanded = false;
}