Skip to content

Commit 05600a2

Browse files
authored
feat(material/tabs/testing): polish harness API (#17417)
* feat(material/tabs/testing): polish harness API * replace `getSelectorForContent` with `getHarnessLoaderForContent` * update @angular/cdk/testing API guards * address comments * add `getTextContent` to `MatTabHarness`
1 parent bb9a3a8 commit 05600a2

File tree

8 files changed

+122
-33
lines changed

8 files changed

+122
-33
lines changed

src/cdk/testing/component-harness.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,29 @@ export interface LocatorFactory {
143143
locatorForAll<T extends ComponentHarness>(
144144
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T[]>;
145145

146+
/**
147+
* Gets a `HarnessLoader` instance for an element under the root of this `LocatorFactory`.
148+
* @param selector The selector for the root element.
149+
* @return A `HarnessLoader` rooted at the first element matching the given selector.
150+
* @throws If no matching element is found for the given selector.
151+
*/
152+
harnessLoaderFor(selector: string): Promise<HarnessLoader>;
153+
154+
/**
155+
* Gets a `HarnessLoader` instance for an element under the root of this `LocatorFactory`
156+
* @param selector The selector for the root element.
157+
* @return A `HarnessLoader` rooted at the first element matching the given selector, or null if
158+
* no matching element is found.
159+
*/
160+
harnessLoaderForOptional(selector: string): Promise<HarnessLoader | null>;
161+
162+
/**
163+
* Gets a list of `HarnessLoader` instances, one for each matching element.
164+
* @param selector The selector for the root element.
165+
* @return A list of `HarnessLoader`, one rooted at each element matching the given selector.
166+
*/
167+
harnessLoaderForAll(selector: string): Promise<HarnessLoader[]>;
168+
146169
/**
147170
* Flushes change detection and async tasks.
148171
* In most cases it should not be necessary to call this manually. However, there may be some edge

src/cdk/testing/harness-environment.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,23 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac
8282
};
8383
}
8484

85+
// Implemented as part of the `LocatorFactory` interface.
86+
async harnessLoaderFor(selector: string): Promise<HarnessLoader> {
87+
return this.createEnvironment(await this._assertElementFound(selector));
88+
}
89+
90+
// Implemented as part of the `LocatorFactory` interface.
91+
async harnessLoaderForOptional(selector: string): Promise<HarnessLoader | null> {
92+
const elements = await this.getAllRawElements(selector);
93+
return elements[0] ? this.createEnvironment(elements[0]) : null;
94+
}
95+
96+
// Implemented as part of the `LocatorFactory` interface.
97+
async harnessLoaderForAll(selector: string): Promise<HarnessLoader[]> {
98+
const elements = await this.getAllRawElements(selector);
99+
return elements.map(element => this.createEnvironment(element));
100+
}
101+
85102
// Implemented as part of the `HarnessLoader` interface.
86103
getHarness<T extends ComponentHarness>(
87104
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): Promise<T> {

src/material/tabs/testing/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ ng_test_library(
2525
srcs = ["shared.spec.ts"],
2626
deps = [
2727
":testing",
28+
"//src/cdk/private/testing",
2829
"//src/cdk/testing",
2930
"//src/cdk/testing/testbed",
3031
"//src/material/tabs",

src/material/tabs/testing/shared.spec.ts

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import {HarnessLoader} from '@angular/cdk/testing';
1+
import {expectAsyncError} from '@angular/cdk/private/testing';
2+
import {ComponentHarness, HarnessLoader} from '@angular/cdk/testing';
23
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
34
import {Component} from '@angular/core';
45
import {ComponentFixture, TestBed} from '@angular/core/testing';
@@ -51,6 +52,26 @@ export function runHarnessTests(
5152
expect(tabs.length).toBe(3);
5253
});
5354

55+
it('should be able to get filtered tabs', async () => {
56+
const tabGroup = await loader.getHarness(tabGroupHarness);
57+
const tabs = await tabGroup.getTabs({label: 'Third'});
58+
expect(tabs.length).toBe(1);
59+
expect(await tabs[0].getLabel()).toBe('Third');
60+
});
61+
62+
it('should be able to select tab from tab-group', async () => {
63+
const tabGroup = await loader.getHarness(tabGroupHarness);
64+
expect(await (await tabGroup.getSelectedTab()).getLabel()).toBe('First');
65+
await tabGroup.selectTab({label: 'Second'});
66+
expect(await (await tabGroup.getSelectedTab()).getLabel()).toBe('Second');
67+
});
68+
69+
it('should throw error when attempting to select invalid tab', async () => {
70+
const tabGroup = await loader.getHarness(tabGroupHarness);
71+
await expectAsyncError(() => tabGroup.selectTab({label: 'Fake'}),
72+
/Error: Cannot find mat-tab matching filter {"label":"Fake"}/);
73+
});
74+
5475
it('should be able to get label of tabs', async () => {
5576
const tabGroup = await loader.getHarness(tabGroupHarness);
5677
const tabs = await tabGroup.getTabs();
@@ -75,16 +96,13 @@ export function runHarnessTests(
7596
expect(await tabs[2].getAriaLabelledby()).toBe('tabLabelId');
7697
});
7798

78-
it('should be able to get content element of active tab', async () => {
99+
it('should be able to get harness loader for content element of active tab', async () => {
79100
const tabGroup = await loader.getHarness(tabGroupHarness);
80101
const tabs = await tabGroup.getTabs();
81-
expect(await (await tabs[0].getContentElement()).text()).toBe('Content 1');
82-
});
83-
84-
it('should be able to get content element of active tab', async () => {
85-
const tabGroup = await loader.getHarness(tabGroupHarness);
86-
const tabs = await tabGroup.getTabs();
87-
expect(await (await tabs[0].getContentElement()).text()).toBe('Content 1');
102+
expect(await tabs[0].getTextContent()).toBe('Content 1');
103+
const tabContentLoader = await tabs[0].getHarnessLoaderForContent();
104+
const tabContentHarness = await tabContentLoader.getHarness(TestTabContentHarness);
105+
expect(await (await tabContentHarness.host()).text()).toBe('Content 1');
88106
});
89107

90108
it('should be able to get disabled state of tab', async () => {
@@ -136,15 +154,23 @@ export function runHarnessTests(
136154
@Component({
137155
template: `
138156
<mat-tab-group>
139-
<mat-tab label="First" aria-label="First tab">Content 1</mat-tab>
140-
<mat-tab label="Second" aria-label="Second tab">Content 2</mat-tab>
157+
<mat-tab label="First" aria-label="First tab">
158+
<span class="test-tab-content">Content 1</span>
159+
</mat-tab>
160+
<mat-tab label="Second" aria-label="Second tab">
161+
<span class="test-tab-content">Content 2</span>
162+
</mat-tab>
141163
<mat-tab label="Third" aria-labelledby="tabLabelId" [disabled]="isDisabled">
142164
<ng-template matTabLabel>Third</ng-template>
143-
Content 3
165+
<span class="test-tab-content">Content 3</span>
144166
</mat-tab>
145167
</mat-tab-group>
146168
`
147169
})
148170
class TabGroupHarnessTest {
149171
isDisabled = false;
150172
}
173+
174+
class TestTabContentHarness extends ComponentHarness {
175+
static hostSelector = '.test-tab-content';
176+
}

src/material/tabs/testing/tab-group-harness.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
10-
import {TabGroupHarnessFilters} from './tab-harness-filters';
10+
import {TabGroupHarnessFilters, TabHarnessFilters} from './tab-harness-filters';
1111
import {MatTabHarness} from './tab-harness';
1212

1313
/**
@@ -34,11 +34,9 @@ export class MatTabGroupHarness extends ComponentHarness {
3434
});
3535
}
3636

37-
private _tabs = this.locatorForAll(MatTabHarness);
38-
3937
/** Gets all tabs of the tab group. */
40-
async getTabs(): Promise<MatTabHarness[]> {
41-
return this._tabs();
38+
async getTabs(filter: TabHarnessFilters = {}): Promise<MatTabHarness[]> {
39+
return this.locatorForAll(MatTabHarness.with(filter))();
4240
}
4341

4442
/** Gets the selected tab of the tab group. */
@@ -52,4 +50,13 @@ export class MatTabGroupHarness extends ComponentHarness {
5250
}
5351
throw new Error('No selected tab could be found.');
5452
}
53+
54+
/** Selects a tab in this tab group. */
55+
async selectTab(filter: TabHarnessFilters = {}): Promise<void> {
56+
const tabs = await this.getTabs(filter);
57+
if (!tabs.length) {
58+
throw Error(`Cannot find mat-tab matching filter ${JSON.stringify(filter)}`);
59+
}
60+
await tabs[0].select();
61+
}
5562
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
*/
88
import {BaseHarnessFilters} from '@angular/cdk/testing';
99

10-
export interface TabHarnessFilters extends BaseHarnessFilters {}
10+
export interface TabHarnessFilters extends BaseHarnessFilters {
11+
label?: string | RegExp;
12+
}
1113

1214
export interface TabGroupHarnessFilters extends BaseHarnessFilters {
1315
selectedTabLabel?: string | RegExp;

src/material/tabs/testing/tab-harness.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {ComponentHarness, HarnessPredicate, TestElement} from '@angular/cdk/testing';
9+
import {ComponentHarness, HarnessLoader, HarnessPredicate} from '@angular/cdk/testing';
1010
import {TabHarnessFilters} from './tab-harness-filters';
1111

1212
/**
@@ -20,11 +20,11 @@ export class MatTabHarness extends ComponentHarness {
2020
* Gets a `HarnessPredicate` that can be used to search for a tab with specific attributes.
2121
*/
2222
static with(options: TabHarnessFilters = {}): HarnessPredicate<MatTabHarness> {
23-
return new HarnessPredicate(MatTabHarness, options);
23+
return new HarnessPredicate(MatTabHarness, options)
24+
.addOption('label', options.label,
25+
(harness, label) => HarnessPredicate.stringMatches(harness.getLabel(), label));
2426
}
2527

26-
private _rootLocatorFactory = this.documentRootLocatorFactory();
27-
2828
/** Gets the label of the tab. */
2929
async getLabel(): Promise<string> {
3030
return (await this.host()).text();
@@ -40,15 +40,6 @@ export class MatTabHarness extends ComponentHarness {
4040
return (await this.host()).getAttribute('aria-labelledby');
4141
}
4242

43-
/**
44-
* Gets the content element of the given tab. Note that the element will be empty
45-
* until the tab is selected. This is an implementation detail of the tab-group
46-
* in order to avoid rendering of non-active tabs.
47-
*/
48-
async getContentElement(): Promise<TestElement> {
49-
return this._rootLocatorFactory.locatorFor(`#${await this._getContentId()}`)();
50-
}
51-
5243
/** Whether the tab is selected. */
5344
async isSelected(): Promise<boolean> {
5445
const hostEl = await this.host();
@@ -69,6 +60,22 @@ export class MatTabHarness extends ComponentHarness {
6960
await (await this.host()).click();
7061
}
7162

63+
/** Gets the text content of the tab. */
64+
async getTextContent(): Promise<string> {
65+
const contentId = await this._getContentId();
66+
const contentEl = await this.documentRootLocatorFactory().locatorFor(`#${contentId}`)();
67+
return contentEl.text();
68+
}
69+
70+
/**
71+
* Gets a `HarnessLoader` that can be used to load harnesses for components within the tab's
72+
* content area.
73+
*/
74+
async getHarnessLoaderForContent(): Promise<HarnessLoader> {
75+
const contentId = await this._getContentId();
76+
return this.documentRootLocatorFactory().harnessLoaderFor(`#${contentId}`);
77+
}
78+
7279
/** Gets the element id for the content of the current tab. */
7380
private async _getContentId(): Promise<string> {
7481
const hostEl = await this.host();

tools/public_api_guard/cdk/testing.d.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ export declare abstract class HarnessEnvironment<E> implements HarnessLoader, Lo
6363
getChildLoader(selector: string): Promise<HarnessLoader>;
6464
protected abstract getDocumentRoot(): E;
6565
getHarness<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): Promise<T>;
66+
harnessLoaderFor(selector: string): Promise<HarnessLoader>;
67+
harnessLoaderForAll(selector: string): Promise<HarnessLoader[]>;
68+
harnessLoaderForOptional(selector: string): Promise<HarnessLoader | null>;
6669
locatorFor<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T>;
6770
locatorFor(selector: string): AsyncFactoryFn<TestElement>;
6871
locatorForAll<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T[]>;
@@ -96,10 +99,13 @@ export interface LocatorFactory {
9699
rootElement: TestElement;
97100
documentRootLocatorFactory(): LocatorFactory;
98101
forceStabilize(): Promise<void>;
99-
locatorFor(selector: string): AsyncFactoryFn<TestElement>;
102+
harnessLoaderFor(selector: string): Promise<HarnessLoader>;
103+
harnessLoaderForAll(selector: string): Promise<HarnessLoader[]>;
104+
harnessLoaderForOptional(selector: string): Promise<HarnessLoader | null>;
100105
locatorFor<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T>;
101-
locatorForAll(selector: string): AsyncFactoryFn<TestElement[]>;
106+
locatorFor(selector: string): AsyncFactoryFn<TestElement>;
102107
locatorForAll<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T[]>;
108+
locatorForAll(selector: string): AsyncFactoryFn<TestElement[]>;
103109
locatorForOptional(selector: string): AsyncFactoryFn<TestElement | null>;
104110
locatorForOptional<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T | null>;
105111
}

0 commit comments

Comments
 (0)