Skip to content

Commit db47bfa

Browse files
committed
feat(material/tabs): Added unit tests for new behavior
1 parent 5d1a18a commit db47bfa

File tree

14 files changed

+432
-10
lines changed

14 files changed

+432
-10
lines changed

src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.spec.ts

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
import {SPACE} from '@angular/cdk/keycodes';
12
import {waitForAsync, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
23
import {Component, QueryList, ViewChild, ViewChildren} from '@angular/core';
34
import {
45
MAT_RIPPLE_GLOBAL_OPTIONS,
56
RippleGlobalOptions,
67
} from '@angular/material-experimental/mdc-core';
78
import {By} from '@angular/platform-browser';
8-
import {dispatchFakeEvent, dispatchMouseEvent} from '../../../cdk/testing/private';
9+
import {
10+
dispatchFakeEvent,
11+
dispatchKeyboardEvent,
12+
dispatchMouseEvent,
13+
} from '../../../cdk/testing/private';
914
import {Direction, Directionality} from '@angular/cdk/bidi';
1015
import {Subject} from 'rxjs';
1116
import {MatTabsModule} from '../module';
@@ -30,6 +35,7 @@ describe('MDC-based MatTabNavBar', () => {
3035
TabLinkWithTabIndexBinding,
3136
TabLinkWithNativeTabindexAttr,
3237
TabBarWithInactiveTabsOnInit,
38+
TabBarWithPanel,
3339
],
3440
providers: [
3541
{provide: MAT_RIPPLE_GLOBAL_OPTIONS, useFactory: () => globalRippleOptions},
@@ -309,6 +315,123 @@ describe('MDC-based MatTabNavBar', () => {
309315
expect(instance.tabNavBar.selectedIndex).toBe(1);
310316
});
311317

318+
describe('without panel', () => {
319+
let fixture: ComponentFixture<SimpleTabNavBarTestApp>;
320+
321+
beforeEach(() => {
322+
fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
323+
fixture.detectChanges();
324+
});
325+
326+
it('should have no explicit roles', () => {
327+
const tabBar = fixture.nativeElement.querySelector('.mat-mdc-tab-nav-bar')!;
328+
expect(tabBar.getAttribute('role')).toBe(null);
329+
330+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
331+
expect(tabLinks[0].getAttribute('role')).toBe(null);
332+
expect(tabLinks[1].getAttribute('role')).toBe(null);
333+
expect(tabLinks[2].getAttribute('role')).toBe(null);
334+
});
335+
336+
it('should not setup aria-controls', () => {
337+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
338+
expect(tabLinks[0].getAttribute('aria-controls')).toBe(null);
339+
expect(tabLinks[1].getAttribute('aria-controls')).toBe(null);
340+
expect(tabLinks[2].getAttribute('aria-controls')).toBe(null);
341+
});
342+
343+
it('should not manage aria-selected', () => {
344+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
345+
expect(tabLinks[0].getAttribute('aria-selected')).toBe(null);
346+
expect(tabLinks[1].getAttribute('aria-selected')).toBe(null);
347+
expect(tabLinks[2].getAttribute('aria-selected')).toBe(null);
348+
});
349+
350+
it('should not activate a link when space is pressed', () => {
351+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
352+
expect(tabLinks[1].classList.contains('mdc-tab--active')).toBe(false);
353+
354+
dispatchKeyboardEvent(tabLinks[1], 'keydown', SPACE);
355+
fixture.detectChanges();
356+
357+
expect(tabLinks[1].classList.contains('mdc-tab--active')).toBe(false);
358+
});
359+
});
360+
361+
describe('with panel', () => {
362+
let fixture: ComponentFixture<TabBarWithPanel>;
363+
364+
beforeEach(() => {
365+
fixture = TestBed.createComponent(TabBarWithPanel);
366+
fixture.detectChanges();
367+
});
368+
369+
it('should have the proper roles', () => {
370+
const tabBar = fixture.nativeElement.querySelector('.mat-mdc-tab-nav-bar')!;
371+
expect(tabBar.getAttribute('role')).toBe('tablist');
372+
373+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
374+
expect(tabLinks[0].getAttribute('role')).toBe('tab');
375+
expect(tabLinks[1].getAttribute('role')).toBe('tab');
376+
expect(tabLinks[2].getAttribute('role')).toBe('tab');
377+
378+
const tabPanel = fixture.nativeElement.querySelector('.mat-mdc-tab-nav-panel')!;
379+
expect(tabPanel.getAttribute('role')).toBe('tabpanel');
380+
});
381+
382+
it('should manage tabindex properly', () => {
383+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
384+
expect(tabLinks[0].tabIndex).toBe(0);
385+
expect(tabLinks[1].tabIndex).toBe(-1);
386+
expect(tabLinks[2].tabIndex).toBe(-1);
387+
388+
tabLinks[1].click();
389+
fixture.detectChanges();
390+
391+
expect(tabLinks[0].tabIndex).toBe(-1);
392+
expect(tabLinks[1].tabIndex).toBe(0);
393+
expect(tabLinks[2].tabIndex).toBe(-1);
394+
});
395+
396+
it('should setup aria-controls properly', () => {
397+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
398+
expect(tabLinks[0].getAttribute('aria-controls')).toBe('tab-panel');
399+
expect(tabLinks[1].getAttribute('aria-controls')).toBe('tab-panel');
400+
expect(tabLinks[2].getAttribute('aria-controls')).toBe('tab-panel');
401+
});
402+
403+
it('should not manage aria-current', () => {
404+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
405+
expect(tabLinks[0].getAttribute('aria-current')).toBe(null);
406+
expect(tabLinks[1].getAttribute('aria-current')).toBe(null);
407+
expect(tabLinks[2].getAttribute('aria-current')).toBe(null);
408+
});
409+
410+
it('should manage aria-selected properly', () => {
411+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
412+
expect(tabLinks[0].getAttribute('aria-selected')).toBe('true');
413+
expect(tabLinks[1].getAttribute('aria-selected')).toBe('false');
414+
expect(tabLinks[2].getAttribute('aria-selected')).toBe('false');
415+
416+
tabLinks[1].click();
417+
fixture.detectChanges();
418+
419+
expect(tabLinks[0].getAttribute('aria-selected')).toBe('false');
420+
expect(tabLinks[1].getAttribute('aria-selected')).toBe('true');
421+
expect(tabLinks[2].getAttribute('aria-selected')).toBe('false');
422+
});
423+
424+
it('should activate a link when space is pressed', () => {
425+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
426+
expect(tabLinks[1].classList.contains('mdc-tab--active')).toBe(false);
427+
428+
dispatchKeyboardEvent(tabLinks[1], 'keydown', SPACE);
429+
fixture.detectChanges();
430+
431+
expect(tabLinks[1].classList.contains('mdc-tab--active')).toBe(true);
432+
});
433+
});
434+
312435
describe('ripples', () => {
313436
let fixture: ComponentFixture<SimpleTabNavBarTestApp>;
314437

@@ -532,3 +655,21 @@ class TabLinkWithNativeTabindexAttr {}
532655
class TabBarWithInactiveTabsOnInit {
533656
tabs = [0, 1, 2];
534657
}
658+
659+
@Component({
660+
template: `
661+
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
662+
<a mat-tab-link
663+
*ngFor="let tab of tabs; let index = index"
664+
[active]="index === activeIndex"
665+
(click)="activeIndex = index">
666+
Tab link
667+
</a>
668+
</nav>
669+
<mat-tab-nav-panel #tabPanel id="tab-panel">Tab panel</mat-tab-nav-panel>
670+
`,
671+
})
672+
class TabBarWithPanel {
673+
tabs = [0, 1, 2];
674+
activeIndex = 0;
675+
}

src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ export class MatTabNav extends _MatTabNavBase implements AfterContentInit {
142142
},
143143
})
144144
export class MatTabLink extends _MatTabLinkBase implements MatInkBarItem, OnInit, OnDestroy {
145+
/** Unique id for the tab. */
146+
@Input() override id = `mat-mdc-tab-link-${nextUniqueId++}`;
147+
145148
_foundation = new MatInkBarFoundation(this.elementRef.nativeElement, this._document);
146149

147150
private readonly _destroyed = new Subject<void>();
@@ -187,14 +190,15 @@ let nextUniqueId = 0;
187190
host: {
188191
'[attr.aria-labelledby]': '_activeTabId',
189192
'[attr.id]': 'id',
193+
'class': 'mat-mdc-tab-nav-panel',
190194
'role': 'tabpanel',
191195
},
192196
encapsulation: ViewEncapsulation.None,
193197
changeDetection: ChangeDetectionStrategy.OnPush,
194198
})
195199
export class MatTabNavPanel {
196200
/** Unique id for the tab panel. */
197-
@Input() id = `mat-tab-nav-panel-${nextUniqueId++}`;
201+
@Input() id = `mat-mdc-tab-nav-panel-${nextUniqueId++}`;
198202

199203
/** Id of the active tab in the nav bar. */
200204
_activeTabId?: string;

src/material-experimental/mdc-tabs/testing/tab-harness-filters.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ export interface TabLinkHarnessFilters extends BaseHarnessFilters {
2727

2828
/** A set of criteria that can be used to filter a list of `MatTabNavBarHarness` instances. */
2929
export interface TabNavBarHarnessFilters extends BaseHarnessFilters {}
30+
31+
/** A set of criteria that can be used to filter a list of `MatTabNavBarHarness` instances. */
32+
export interface TabNavPanelHarnessFilters extends BaseHarnessFilters {}

src/material-experimental/mdc-tabs/testing/tab-link-harness.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ export class MatTabLinkHarness extends ComponentHarness {
3333
return (await this.host()).text();
3434
}
3535

36+
/** Gets the value of the `aria-controls` attribute. */
37+
async getAriaControls(): Promise<string | null> {
38+
const host = await this.host();
39+
return host.getAttribute('aria-controls');
40+
}
41+
3642
/** Whether the link is active. */
3743
async isActive(): Promise<boolean> {
3844
const host = await this.host();

src/material-experimental/mdc-tabs/testing/tab-nav-bar-harness.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@
77
*/
88

99
import {ComponentHarness, HarnessPredicate, parallel} from '@angular/cdk/testing';
10-
import {TabNavBarHarnessFilters, TabLinkHarnessFilters} from './tab-harness-filters';
10+
import {
11+
TabNavBarHarnessFilters,
12+
TabNavPanelHarnessFilters,
13+
TabLinkHarnessFilters,
14+
} from './tab-harness-filters';
1115
import {MatTabLinkHarness} from './tab-link-harness';
16+
import {MatTabNavPanelHarness} from './tab-nav-panel-harness';
1217

1318
/** Harness for interacting with an MDC-based mat-tab-nav-bar in tests. */
1419
export class MatTabNavBarHarness extends ComponentHarness {
@@ -57,4 +62,16 @@ export class MatTabNavBarHarness extends ComponentHarness {
5762
}
5863
await tabs[0].click();
5964
}
65+
66+
/** Gets the panel associated with the nav bar. */
67+
async getPanel(): Promise<MatTabNavPanelHarness> {
68+
const link = await this.getActiveLink();
69+
const panelId = await link.getAriaControls();
70+
if (!panelId) {
71+
throw Error('No panel is controlled by the nav bar.');
72+
}
73+
74+
const filter: TabNavPanelHarnessFilters = {selector: `#${panelId}`};
75+
return await this.documentRootLocatorFactory().locatorFor(MatTabNavPanelHarness.with(filter))();
76+
}
6077
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
10+
import {TabNavPanelHarnessFilters} from './tab-harness-filters';
11+
12+
/** Harness for interacting with a standard mat-tab-nav-panel in tests. */
13+
export class MatTabNavPanelHarness extends ComponentHarness {
14+
/** The selector for the host element of a `MatTabNavPanel` instance. */
15+
static hostSelector = '.mat-mdc-tab-nav-panel';
16+
17+
/**
18+
* Gets a `HarnessPredicate` that can be used to search for a `MatTabNavPanel` that meets
19+
* certain criteria.
20+
* @param options Options for filtering which tab nav panel instances are considered a match.
21+
* @return a `HarnessPredicate` configured with the given options.
22+
*/
23+
static with(options: TabNavPanelHarnessFilters = {}): HarnessPredicate<MatTabNavPanelHarness> {
24+
return new HarnessPredicate(MatTabNavPanelHarness, options);
25+
}
26+
27+
/** Gets the tab panel text content. */
28+
async getTextContent(): Promise<string> {
29+
return (await this.host()).text();
30+
}
31+
}

0 commit comments

Comments
 (0)