Skip to content

Commit b60f427

Browse files
committed
feat(aria/tabs): add test harnesses
1 parent 61f9ac0 commit b60f427

8 files changed

Lines changed: 376 additions & 0 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
## API Report File for "@angular/aria_tabs_testing"
2+
3+
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4+
5+
```ts
6+
7+
import { BaseHarnessFilters } from '@angular/cdk/testing';
8+
import { ComponentHarness } from '@angular/cdk/testing';
9+
import { ContentContainerComponentHarness } from '@angular/cdk/testing';
10+
import { HarnessLoader } from '@angular/cdk/testing';
11+
import { HarnessPredicate } from '@angular/cdk/testing';
12+
13+
// @public
14+
export class TabHarness extends ContentContainerComponentHarness {
15+
// (undocumented)
16+
protected getRootHarnessLoader(): Promise<HarnessLoader>;
17+
getTitle(): Promise<string>;
18+
// (undocumented)
19+
static hostSelector: string;
20+
isActive(): Promise<boolean>;
21+
isDisabled(): Promise<boolean>;
22+
isSelected(): Promise<boolean>;
23+
select(): Promise<void>;
24+
static with(options?: TabHarnessFilters): HarnessPredicate<TabHarness>;
25+
}
26+
27+
// @public
28+
export interface TabHarnessFilters extends BaseHarnessFilters {
29+
disabled?: boolean;
30+
selected?: boolean;
31+
title?: string | RegExp;
32+
}
33+
34+
// @public
35+
export class TabsHarness extends ComponentHarness {
36+
getSelectedTab(): Promise<TabHarness | null>;
37+
getTabs(filters?: TabHarnessFilters): Promise<TabHarness[]>;
38+
// (undocumented)
39+
static hostSelector: string;
40+
static with(options?: TabsHarnessFilters): HarnessPredicate<TabsHarness>;
41+
}
42+
43+
// @public
44+
export interface TabsHarnessFilters extends BaseHarnessFilters {
45+
}
46+
47+
// (No @packageDocumentation comment for this package)
48+
49+
```

src/aria/config.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ ARIA_ENTRYPOINTS = [
99
"menu",
1010
"menu/testing",
1111
"tabs",
12+
"tabs/testing",
1213
"toolbar",
1314
"toolbar/testing",
1415
"tree",

src/aria/tabs/testing/BUILD.bazel

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
load("//tools:defaults.bzl", "ng_project", "ng_web_test_suite", "ts_project")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ts_project(
6+
name = "testing",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
deps = [
12+
"//src/cdk/testing",
13+
],
14+
)
15+
16+
filegroup(
17+
name = "source-files",
18+
srcs = glob(["**/*.ts"]),
19+
)
20+
21+
ng_project(
22+
name = "unit_tests_lib",
23+
testonly = True,
24+
srcs = glob(["**/*.spec.ts"]),
25+
deps = [
26+
":testing",
27+
"//:node_modules/@angular/common",
28+
"//:node_modules/@angular/core",
29+
"//src/aria/tabs",
30+
"//src/cdk/testing",
31+
"//src/cdk/testing/testbed",
32+
],
33+
)
34+
35+
ng_web_test_suite(
36+
name = "unit_tests",
37+
deps = [":unit_tests_lib"],
38+
)

src/aria/tabs/testing/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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.dev/license
7+
*/
8+
9+
export * from './public-api';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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.dev/license
7+
*/
8+
9+
export * from './tabs-harness';
10+
export * from './tabs-harness-filters';
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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.dev/license
7+
*/
8+
9+
import {BaseHarnessFilters} from '@angular/cdk/testing';
10+
11+
/** A set of criteria that can be used to filter a list of `TabsHarness` instances. */
12+
export interface TabsHarnessFilters extends BaseHarnessFilters {}
13+
14+
/** A set of criteria that can be used to filter a list of `TabHarness` instances. */
15+
export interface TabHarnessFilters extends BaseHarnessFilters {
16+
/** Only find instances whose title matches the given value. */
17+
title?: string | RegExp;
18+
/** Only find instances that are selected. */
19+
selected?: boolean;
20+
/** Only find instances that are disabled. */
21+
disabled?: boolean;
22+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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.dev/license
7+
*/
8+
9+
import {Component} from '@angular/core';
10+
import {ComponentFixture, TestBed} from '@angular/core/testing';
11+
import {ComponentHarness, HarnessLoader} from '@angular/cdk/testing';
12+
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
13+
import {Tabs, TabList, Tab, TabPanel, TabContent} from '../../tabs';
14+
import {TabsHarness} from './tabs-harness';
15+
16+
class TestContentHarness extends ComponentHarness {
17+
static hostSelector = '.test-content';
18+
async getText(): Promise<string> {
19+
return (await this.host()).text();
20+
}
21+
}
22+
23+
describe('TabsHarness', () => {
24+
let fixture: ComponentFixture<TabsHarnessTest>;
25+
let loader: HarnessLoader;
26+
27+
beforeEach(() => {
28+
fixture = TestBed.createComponent(TabsHarnessTest);
29+
fixture.detectChanges();
30+
loader = TestbedHarnessEnvironment.loader(fixture);
31+
});
32+
33+
it('should load harness with tabs container', async () => {
34+
await expectAsync(loader.getHarness(TabsHarness)).toBeResolved();
35+
});
36+
37+
it('should get tabs', async () => {
38+
const tabs = await loader.getHarness(TabsHarness);
39+
40+
const tabItems = await tabs.getTabs();
41+
42+
expect(tabItems.length).toBe(3);
43+
});
44+
45+
it('should get tab panel content via ContentContainerComponentHarness', async () => {
46+
const tabs = await loader.getHarness(TabsHarness);
47+
const tabItems = await tabs.getTabs();
48+
49+
const contentHarness = await tabItems[0].getHarness(TestContentHarness);
50+
51+
expect(await contentHarness.getText()).toBe('Content 1');
52+
});
53+
54+
it('should get selected tab', async () => {
55+
const tabs = await loader.getHarness(TabsHarness);
56+
57+
const selectedTab = await tabs.getSelectedTab();
58+
59+
expect(await selectedTab?.getTitle()).toBe('Tab 1');
60+
});
61+
62+
it('should switch tabs on click', async () => {
63+
const tabs = await loader.getHarness(TabsHarness);
64+
const tabItems = await tabs.getTabs();
65+
expect(await tabItems[0].isSelected()).toBe(true);
66+
expect(await tabItems[1].isSelected()).toBe(false);
67+
68+
await tabItems[1].select();
69+
70+
expect(await tabItems[0].isSelected()).toBe(false);
71+
expect(await tabItems[1].isSelected()).toBe(true);
72+
});
73+
74+
it('should check disabled state', async () => {
75+
const tabs = await loader.getHarness(TabsHarness);
76+
const tabItems = await tabs.getTabs();
77+
78+
expect(await tabItems[0].isDisabled()).toBe(false);
79+
expect(await tabItems[2].isDisabled()).toBe(true);
80+
});
81+
82+
it('should check active state', async () => {
83+
const tabs = await loader.getHarness(TabsHarness);
84+
const tabItems = await tabs.getTabs();
85+
86+
expect(await tabItems[0].isActive()).toBe(true);
87+
expect(await tabItems[1].isActive()).toBe(false);
88+
});
89+
90+
it('should filter tabs by title', async () => {
91+
const tabs = await loader.getHarness(TabsHarness);
92+
93+
const filteredTabs = await tabs.getTabs({title: 'Tab 2'});
94+
95+
expect(filteredTabs.length).toBe(1);
96+
expect(await filteredTabs[0].getTitle()).toBe('Tab 2');
97+
});
98+
99+
it('should filter tabs by selected state', async () => {
100+
const tabs = await loader.getHarness(TabsHarness);
101+
102+
const filteredTabs = await tabs.getTabs({selected: true});
103+
104+
expect(filteredTabs.length).toBe(1);
105+
expect(await filteredTabs[0].getTitle()).toBe('Tab 1');
106+
});
107+
108+
it('should filter tabs by disabled state', async () => {
109+
const tabs = await loader.getHarness(TabsHarness);
110+
111+
const filteredTabs = await tabs.getTabs({disabled: true});
112+
113+
expect(filteredTabs.length).toBe(1);
114+
expect(await filteredTabs[0].getTitle()).toBe('Tab 3');
115+
});
116+
});
117+
118+
@Component({
119+
template: `
120+
<div ngTabs>
121+
<ul ngTabList [selectedTab]="'tab1'">
122+
<li ngTab value="tab1">Tab 1</li>
123+
<li ngTab value="tab2">Tab 2</li>
124+
<li ngTab value="tab3" [disabled]="true">Tab 3</li>
125+
</ul>
126+
127+
128+
<div ngTabPanel value="tab1">
129+
<ng-template ngTabContent>
130+
<div class="test-content">Content 1</div>
131+
</ng-template>
132+
</div>
133+
<div ngTabPanel value="tab2">
134+
<ng-template ngTabContent>Content 2</ng-template>
135+
</div>
136+
<div ngTabPanel value="tab3">
137+
<ng-template ngTabContent>Content 3</ng-template>
138+
</div>
139+
</div>
140+
`,
141+
imports: [Tabs, TabList, Tab, TabPanel, TabContent],
142+
})
143+
class TabsHarnessTest {}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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.dev/license
7+
*/
8+
9+
import {
10+
ComponentHarness,
11+
ContentContainerComponentHarness,
12+
HarnessLoader,
13+
HarnessPredicate,
14+
} from '@angular/cdk/testing';
15+
import {TabsHarnessFilters, TabHarnessFilters} from './tabs-harness-filters';
16+
17+
/** Harness for interacting with an Aria tab in tests. */
18+
export class TabHarness extends ContentContainerComponentHarness {
19+
static hostSelector = '[ngTab]';
20+
21+
/**
22+
* Gets a `HarnessPredicate` that can be used to search for a `TabHarness`
23+
* that meets certain criteria.
24+
* @param options Options for filtering which tab instances are considered a match.
25+
* @return a `HarnessPredicate` configured with the given options.
26+
*/
27+
static with(options: TabHarnessFilters = {}): HarnessPredicate<TabHarness> {
28+
return new HarnessPredicate(TabHarness, options)
29+
.addOption('title', options.title, (harness, title) =>
30+
HarnessPredicate.stringMatches(harness.getTitle(), title),
31+
)
32+
.addOption(
33+
'selected',
34+
options.selected,
35+
async (harness, selected) => (await harness.isSelected()) === selected,
36+
)
37+
.addOption(
38+
'disabled',
39+
options.disabled,
40+
async (harness, disabled) => (await harness.isDisabled()) === disabled,
41+
);
42+
}
43+
44+
/** Gets the tab's title text. */
45+
async getTitle(): Promise<string> {
46+
return (await this.host()).text();
47+
}
48+
49+
/** Clicks the tab to select it. */
50+
async select(): Promise<void> {
51+
return (await this.host()).click();
52+
}
53+
54+
/** Gets whether the tab is selected. */
55+
async isSelected(): Promise<boolean> {
56+
const host = await this.host();
57+
return (await host.getAttribute('aria-selected')) === 'true';
58+
}
59+
60+
/** Gets whether the tab is disabled. */
61+
async isDisabled(): Promise<boolean> {
62+
const host = await this.host();
63+
return (await host.getAttribute('aria-disabled')) === 'true';
64+
}
65+
66+
/** Gets whether the tab is active. */
67+
async isActive(): Promise<boolean> {
68+
const host = await this.host();
69+
return (await host.getAttribute('data-active')) === 'true';
70+
}
71+
72+
protected override async getRootHarnessLoader(): Promise<HarnessLoader> {
73+
const host = await this.host();
74+
const controlsId = await host.getAttribute('aria-controls');
75+
const documentRoot = await this.documentRootLocatorFactory().rootHarnessLoader();
76+
return await documentRoot.getChildLoader(`[ngTabPanel][id="${controlsId}"]`);
77+
}
78+
}
79+
80+
/** Harness for interacting with an Aria tabs container in tests. */
81+
export class TabsHarness extends ComponentHarness {
82+
static hostSelector = '[ngTabs]';
83+
84+
/**
85+
* Gets a `HarnessPredicate` that can be used to search for a `TabsHarness`
86+
* that meets certain criteria.
87+
* @param options Options for filtering which tabs instances are considered a match.
88+
* @return a `HarnessPredicate` configured with the given options.
89+
*/
90+
static with(options: TabsHarnessFilters = {}): HarnessPredicate<TabsHarness> {
91+
return new HarnessPredicate(TabsHarness, options);
92+
}
93+
94+
/** Gets all tabs inside the tabs container. */
95+
async getTabs(filters: TabHarnessFilters = {}): Promise<TabHarness[]> {
96+
return await this.locatorForAll(TabHarness.with(filters))();
97+
}
98+
99+
/** Gets the currently selected tab. */
100+
async getSelectedTab(): Promise<TabHarness | null> {
101+
const tabs = await this.getTabs({selected: true});
102+
return tabs.length > 0 ? tabs[0] : null;
103+
}
104+
}

0 commit comments

Comments
 (0)