sass-references/angular-material/material/tabs/tab-nav-bar/tab-nav-bar.spec.ts

640 lines
22 KiB
TypeScript

import {Direction, Directionality} from '@angular/cdk/bidi';
import {ENTER, SPACE} from '@angular/cdk/keycodes';
import {SharedResizeObserver} from '@angular/cdk/observers/private';
import {
dispatchFakeEvent,
dispatchKeyboardEvent,
dispatchMouseEvent,
} from '@angular/cdk/testing/private';
import {Component, QueryList, ViewChild, ViewChildren} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync, tick, waitForAsync} from '@angular/core/testing';
import {MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions} from '@angular/material/core';
import {By} from '@angular/platform-browser';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {Subject} from 'rxjs';
import {MAT_TABS_CONFIG} from '../index';
import {MatTabsModule} from '../module';
import {MatTabLink, MatTabNav} from './tab-nav-bar';
describe('MatTabNavBar', () => {
let dir: Direction = 'ltr';
let dirChange = new Subject();
let globalRippleOptions: RippleGlobalOptions;
let resizeEvents: Subject<ResizeObserverEntry[]>;
beforeEach(waitForAsync(() => {
globalRippleOptions = {};
TestBed.configureTestingModule({
imports: [
MatTabsModule,
SimpleTabNavBarTestApp,
TabLinkWithNgIf,
TabBarWithInactiveTabsOnInit,
],
providers: [
{provide: MAT_RIPPLE_GLOBAL_OPTIONS, useFactory: () => globalRippleOptions},
{provide: Directionality, useFactory: () => ({value: dir, change: dirChange})},
],
});
resizeEvents = new Subject();
spyOn(TestBed.inject(SharedResizeObserver), 'observe').and.returnValue(resizeEvents);
}));
describe('basic behavior', () => {
let fixture: ComponentFixture<SimpleTabNavBarTestApp>;
beforeEach(() => {
fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
fixture.detectChanges();
});
it('should change active index on click', () => {
// select the second link
let tabLink = fixture.debugElement.queryAll(By.css('a'))[1];
tabLink.nativeElement.click();
expect(fixture.componentInstance.activeIndex).toBe(1);
// select the third link
tabLink = fixture.debugElement.queryAll(By.css('a'))[2];
tabLink.nativeElement.click();
expect(fixture.componentInstance.activeIndex).toBe(2);
});
it('should add the active class if active', () => {
let tabLink1 = fixture.debugElement.queryAll(By.css('a'))[0];
let tabLink2 = fixture.debugElement.queryAll(By.css('a'))[1];
const tabLinkElements = fixture.debugElement
.queryAll(By.css('a'))
.map(tabLinkDebugEl => tabLinkDebugEl.nativeElement);
tabLink1.nativeElement.click();
fixture.detectChanges();
expect(tabLinkElements[0].classList.contains('mdc-tab--active')).toBeTruthy();
expect(tabLinkElements[1].classList.contains('mdc-tab--active')).toBeFalsy();
tabLink2.nativeElement.click();
fixture.detectChanges();
expect(tabLinkElements[0].classList.contains('mdc-tab--active')).toBeFalsy();
expect(tabLinkElements[1].classList.contains('mdc-tab--active')).toBeTruthy();
});
it('should update aria-disabled if disabled', () => {
const tabLinkElements = fixture.debugElement
.queryAll(By.css('a'))
.map(tabLinkDebugEl => tabLinkDebugEl.nativeElement);
expect(tabLinkElements.every(tabLink => tabLink.getAttribute('aria-disabled') === 'false'))
.withContext('Expected aria-disabled to be set to "false" by default.')
.toBe(true);
fixture.componentInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(tabLinkElements.every(tabLink => tabLink.getAttribute('aria-disabled') === 'true'))
.withContext('Expected aria-disabled to be set to "true" if link is disabled.')
.toBe(true);
});
it('should update the tabindex if links are disabled', () => {
const tabLinkElements = fixture.debugElement
.queryAll(By.css('a'))
.map(tabLinkDebugEl => tabLinkDebugEl.nativeElement);
expect(tabLinkElements.map(tabLink => tabLink.tabIndex))
.withContext('Expected first element to be keyboard focusable by default')
.toEqual([0, -1, -1]);
fixture.componentInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(tabLinkElements.every(tabLink => tabLink.tabIndex === -1))
.withContext('Expected element to no longer be keyboard focusable if disabled.')
.toBe(true);
});
it('should mark disabled links', () => {
const tabLinkElement = fixture.debugElement.query(By.css('a')).nativeElement;
expect(tabLinkElement.classList).not.toContain('mat-mdc-tab-disabled');
fixture.componentInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(tabLinkElement.classList).toContain('mat-mdc-tab-disabled');
});
it('should prevent default keyboard actions on disabled links', () => {
const link = fixture.debugElement.query(By.css('a')).nativeElement;
fixture.componentInstance.disabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const spaceEvent = dispatchKeyboardEvent(link, 'keydown', SPACE);
fixture.detectChanges();
expect(spaceEvent.defaultPrevented).toBe(true);
const enterEvent = dispatchKeyboardEvent(link, 'keydown', ENTER);
fixture.detectChanges();
expect(enterEvent.defaultPrevented).toBe(true);
});
it('should re-align the ink bar when the direction changes', fakeAsync(() => {
const inkBar = fixture.componentInstance.tabNavBar._inkBar;
spyOn(inkBar, 'alignToElement');
dirChange.next();
tick();
fixture.detectChanges();
expect(inkBar.alignToElement).toHaveBeenCalled();
}));
it('should re-align the ink bar when the tabs list change', fakeAsync(() => {
const inkBar = fixture.componentInstance.tabNavBar._inkBar;
spyOn(inkBar, 'alignToElement');
fixture.componentInstance.tabs = [1, 2, 3, 4];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick();
expect(inkBar.alignToElement).toHaveBeenCalled();
}));
it('should re-align the ink bar when the tab labels change the width', done => {
const inkBar = fixture.componentInstance.tabNavBar._inkBar;
const spy = spyOn(inkBar, 'alignToElement').and.callFake(() => {
expect(spy.calls.any()).toBe(true);
done();
});
fixture.componentInstance.label = 'label change';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(spy.calls.any()).toBe(false);
});
it('should re-align the ink bar when the nav bar is resized', fakeAsync(() => {
const inkBar = fixture.componentInstance.tabNavBar._inkBar;
spyOn(inkBar, 'alignToElement');
resizeEvents.next([]);
fixture.detectChanges();
tick(32);
expect(inkBar.alignToElement).toHaveBeenCalled();
}));
it('should hide the ink bar when all the links are inactive', () => {
const inkBar = fixture.componentInstance.tabNavBar._inkBar;
spyOn(inkBar, 'hide');
fixture.componentInstance.tabLinks.forEach(link => (link.active = false));
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(inkBar.hide).toHaveBeenCalled();
});
it('should update the focusIndex when a tab receives focus directly', () => {
const thirdLink = fixture.debugElement.queryAll(By.css('a'))[2];
dispatchFakeEvent(thirdLink.nativeElement, 'focus');
fixture.detectChanges();
expect(fixture.componentInstance.tabNavBar.focusIndex).toBe(2);
});
});
it('should hide the ink bar if no tabs are active on init', fakeAsync(() => {
const fixture = TestBed.createComponent(TabBarWithInactiveTabsOnInit);
fixture.detectChanges();
tick(20); // Angular turns rAF calls into 16.6ms timeouts in tests.
fixture.detectChanges();
expect(fixture.nativeElement.querySelectorAll('.mdc-tab-indicator--active').length).toBe(0);
}));
it('should clean up the ripple event handlers on destroy', () => {
let fixture: ComponentFixture<TabLinkWithNgIf> = TestBed.createComponent(TabLinkWithNgIf);
fixture.detectChanges();
let link = fixture.debugElement.nativeElement.querySelector('.mat-mdc-tab-link');
fixture.componentInstance.isDestroyed = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
dispatchMouseEvent(link, 'mousedown');
expect(link.querySelector('.mat-ripple-element'))
.withContext('Expected no ripple to be created when ripple target is destroyed.')
.toBeFalsy();
});
it('should select the proper tab, if the tabs come in after init', () => {
const fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
const instance = fixture.componentInstance;
instance.tabs = [];
instance.activeIndex = 1;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(instance.tabNavBar.selectedIndex).toBe(-1);
instance.tabs = [0, 1, 2];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(instance.tabNavBar.selectedIndex).toBe(1);
});
it('should have the proper roles', () => {
const fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
fixture.detectChanges();
const tabBar = fixture.nativeElement.querySelector('.mat-mdc-tab-nav-bar')!;
expect(tabBar.getAttribute('role')).toBe('tablist');
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
expect(tabLinks[0].getAttribute('role')).toBe('tab');
expect(tabLinks[1].getAttribute('role')).toBe('tab');
expect(tabLinks[2].getAttribute('role')).toBe('tab');
const tabPanel = fixture.nativeElement.querySelector('.mat-mdc-tab-nav-panel')!;
expect(tabPanel.getAttribute('role')).toBe('tabpanel');
});
it('should manage tabindex properly', () => {
const fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
fixture.detectChanges();
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
expect(tabLinks[0].tabIndex).toBe(0);
expect(tabLinks[1].tabIndex).toBe(-1);
expect(tabLinks[2].tabIndex).toBe(-1);
tabLinks[1].click();
fixture.detectChanges();
expect(tabLinks[0].tabIndex).toBe(-1);
expect(tabLinks[1].tabIndex).toBe(0);
expect(tabLinks[2].tabIndex).toBe(-1);
});
it('should setup aria-controls properly', () => {
const fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
fixture.detectChanges();
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
expect(tabLinks[0].getAttribute('aria-controls')).toBe('tab-panel');
expect(tabLinks[1].getAttribute('aria-controls')).toBe('tab-panel');
expect(tabLinks[2].getAttribute('aria-controls')).toBe('tab-panel');
});
it('should not manage aria-current', () => {
const fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
fixture.detectChanges();
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
expect(tabLinks[0].getAttribute('aria-current')).toBe(null);
expect(tabLinks[1].getAttribute('aria-current')).toBe(null);
expect(tabLinks[2].getAttribute('aria-current')).toBe(null);
});
it('should manage aria-selected properly', () => {
const fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
fixture.detectChanges();
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
expect(tabLinks[0].getAttribute('aria-selected')).toBe('true');
expect(tabLinks[1].getAttribute('aria-selected')).toBe('false');
expect(tabLinks[2].getAttribute('aria-selected')).toBe('false');
tabLinks[1].click();
fixture.detectChanges();
expect(tabLinks[0].getAttribute('aria-selected')).toBe('false');
expect(tabLinks[1].getAttribute('aria-selected')).toBe('true');
expect(tabLinks[2].getAttribute('aria-selected')).toBe('false');
});
it('should activate a link when space is pressed', () => {
const fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
fixture.detectChanges();
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
expect(tabLinks[1].classList.contains('mdc-tab--active')).toBe(false);
dispatchKeyboardEvent(tabLinks[1], 'keydown', SPACE);
fixture.detectChanges();
expect(tabLinks[1].classList.contains('mdc-tab--active')).toBe(true);
});
it('should activate a link when enter is pressed', () => {
const fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
fixture.detectChanges();
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
expect(tabLinks[1].classList.contains('mdc-tab--active')).toBe(false);
dispatchKeyboardEvent(tabLinks[1], 'keydown', ENTER);
fixture.detectChanges();
expect(tabLinks[1].classList.contains('mdc-tab--active')).toBe(true);
});
it('should re-show the ink bar if the same tab is cleared and re-activated', fakeAsync(() => {
const getInkBars = () =>
fixture.nativeElement.querySelectorAll('.mdc-tab-indicator--active').length;
const fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
fixture.componentInstance.activeIndex = 0;
fixture.detectChanges();
tick(20);
expect(getInkBars()).toBe(1);
fixture.componentInstance.activeIndex = -1;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick(20);
expect(getInkBars()).toBe(0);
fixture.componentInstance.activeIndex = 0;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
tick(20);
expect(getInkBars()).toBe(1);
}));
describe('ripples', () => {
let fixture: ComponentFixture<SimpleTabNavBarTestApp>;
beforeEach(() => {
fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
fixture.detectChanges();
});
it('should be disabled on all tab links when they are disabled on the nav bar', () => {
expect(fixture.componentInstance.tabLinks.toArray().every(tabLink => !tabLink.rippleDisabled))
.withContext('Expected every tab link to have ripples enabled')
.toBe(true);
fixture.componentInstance.disableRippleOnBar = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(fixture.componentInstance.tabLinks.toArray().every(tabLink => tabLink.rippleDisabled))
.withContext('Expected every tab link to have ripples disabled')
.toBe(true);
});
it('should have the `disableRipple` from the tab take precedence over the nav bar', () => {
const firstTab = fixture.componentInstance.tabLinks.first;
expect(firstTab.rippleDisabled)
.withContext('Expected ripples to be enabled on first tab')
.toBe(false);
firstTab.disableRipple = true;
fixture.componentInstance.disableRippleOnBar = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(firstTab.rippleDisabled)
.withContext('Expected ripples to be disabled on first tab')
.toBe(true);
});
it('should show up for tab link elements on mousedown', () => {
const tabLink = fixture.debugElement.nativeElement.querySelector('.mat-mdc-tab-link');
dispatchMouseEvent(tabLink, 'mousedown');
dispatchMouseEvent(tabLink, 'mouseup');
expect(tabLink.querySelectorAll('.mat-ripple-element').length)
.withContext('Expected one ripple to show up if user clicks on tab link.')
.toBe(1);
});
it('should be able to disable ripples on an individual tab link', () => {
const tabLinkDebug = fixture.debugElement.query(By.css('a'));
const tabLinkElement = tabLinkDebug.nativeElement;
fixture.componentInstance.disableRippleOnLink = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
dispatchMouseEvent(tabLinkElement, 'mousedown');
dispatchMouseEvent(tabLinkElement, 'mouseup');
expect(tabLinkElement.querySelectorAll('.mat-ripple-element').length)
.withContext('Expected no ripple to show up if ripples are disabled.')
.toBe(0);
});
it('should be able to disable ripples through global options at runtime', () => {
expect(fixture.componentInstance.tabLinks.toArray().every(tabLink => !tabLink.rippleDisabled))
.withContext('Expected every tab link to have ripples enabled')
.toBe(true);
globalRippleOptions.disabled = true;
expect(fixture.componentInstance.tabLinks.toArray().every(tabLink => tabLink.rippleDisabled))
.withContext('Expected every tab link to have ripples disabled')
.toBe(true);
});
it('should have a focus indicator', () => {
const tabLinkNativeElements = [
...fixture.debugElement.nativeElement.querySelectorAll('.mat-mdc-tab-link'),
];
expect(
tabLinkNativeElements.every(element => element.classList.contains('mat-focus-indicator')),
).toBe(true);
});
});
describe('with the ink bar fit to content', () => {
let fixture: ComponentFixture<SimpleTabNavBarTestApp>;
beforeEach(() => {
fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
fixture.componentInstance.fitInkBarToContent = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
});
it('should properly nest the ink bar when fit to content', () => {
const tabElement = fixture.nativeElement.querySelector('.mdc-tab');
const contentElement = tabElement.querySelector('.mdc-tab__content');
const indicatorElement = tabElement.querySelector('.mdc-tab-indicator');
expect(indicatorElement.parentElement).toBeTruthy();
expect(indicatorElement.parentElement).toBe(contentElement);
});
it('should be able to move the ink bar between content and full', () => {
fixture.componentInstance.fitInkBarToContent = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const tabElement = fixture.nativeElement.querySelector('.mdc-tab');
const indicatorElement = tabElement.querySelector('.mdc-tab-indicator');
expect(indicatorElement.parentElement).toBeTruthy();
expect(indicatorElement.parentElement).toBe(tabElement);
fixture.componentInstance.fitInkBarToContent = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
const contentElement = tabElement.querySelector('.mdc-tab__content');
expect(indicatorElement.parentElement).toBeTruthy();
expect(indicatorElement.parentElement).toBe(contentElement);
});
});
});
describe('MatTabNavBar with a default config', () => {
let fixture: ComponentFixture<TabLinkWithNgIf>;
beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [MatTabsModule, BrowserAnimationsModule, TabLinkWithNgIf],
providers: [{provide: MAT_TABS_CONFIG, useValue: {fitInkBarToContent: true}}],
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(TabLinkWithNgIf);
fixture.detectChanges();
});
it('should set whether the ink bar fits to content', () => {
const tabElement = fixture.nativeElement.querySelector('.mdc-tab');
const contentElement = tabElement.querySelector('.mdc-tab__content');
const indicatorElement = tabElement.querySelector('.mdc-tab-indicator');
expect(indicatorElement.parentElement).toBeTruthy();
expect(indicatorElement.parentElement).toBe(contentElement);
});
});
describe('MatTabNavBar with enabled animations', () => {
beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [MatTabsModule, BrowserAnimationsModule, TabsWithCustomAnimationDuration],
});
}));
it('should not throw when setting an animationDuration without units', fakeAsync(() => {
expect(() => {
let fixture = TestBed.createComponent(TabsWithCustomAnimationDuration);
fixture.detectChanges();
tick();
}).not.toThrow();
}));
it('should set appropiate css variable given a specified animationDuration', fakeAsync(() => {
let fixture = TestBed.createComponent(TabsWithCustomAnimationDuration);
fixture.detectChanges();
tick();
const tabNavBar = fixture.nativeElement.querySelector('.mat-mdc-tab-nav-bar');
expect(tabNavBar.style.getPropertyValue('--mat-tab-animation-duration')).toBe('500ms');
}));
});
@Component({
selector: 'test-app',
template: `
<nav mat-tab-nav-bar
[disableRipple]="disableRippleOnBar"
[fitInkBarToContent]="fitInkBarToContent"
[tabPanel]="tabPanel">
@for (tab of tabs; track tab; let index = $index) {
<a mat-tab-link
[active]="activeIndex === index"
[disabled]="disabled"
(click)="activeIndex = index"
[disableRipple]="disableRippleOnLink">Tab link {{label}}</a>
}
</nav>
<mat-tab-nav-panel #tabPanel id="tab-panel">Tab panel</mat-tab-nav-panel>
`,
standalone: true,
imports: [MatTabsModule],
})
class SimpleTabNavBarTestApp {
@ViewChild(MatTabNav) tabNavBar: MatTabNav;
@ViewChildren(MatTabLink) tabLinks: QueryList<MatTabLink>;
label = '';
disabled = false;
disableRippleOnBar = false;
disableRippleOnLink = false;
tabs = [0, 1, 2];
fitInkBarToContent = false;
activeIndex = 0;
}
@Component({
template: `
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
@if (!isDestroyed) {
<a mat-tab-link>Link</a>
}
</nav>
<mat-tab-nav-panel #tabPanel>Tab panel</mat-tab-nav-panel>
`,
standalone: true,
imports: [MatTabsModule],
})
class TabLinkWithNgIf {
isDestroyed = false;
}
@Component({
template: `
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
@for (tab of tabs; track tab) {
<a mat-tab-link [active]="false">Tab link {{label}}</a>
}
</nav>
<mat-tab-nav-panel #tabPanel>Tab panel</mat-tab-nav-panel>
`,
standalone: true,
imports: [MatTabsModule],
})
class TabBarWithInactiveTabsOnInit {
tabs = [0, 1, 2];
}
@Component({
template: `
<nav [animationDuration]="500" mat-tab-nav-bar [tabPanel]="tabPanel">
@for (link of links; track link) {
<a mat-tab-link>{{link}}</a>
}
</nav>
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel>,
`,
standalone: true,
imports: [MatTabsModule],
})
class TabsWithCustomAnimationDuration {
links = ['First', 'Second', 'Third'];
}