Skip to content

Commit 8b670ab

Browse files
refactor(testing): add methods for accessing projected content in component container harnesses (#20556)
Switches over all the harnesses for components that can contain projected user content to extend `ContentContainerComponentHarness` which will allow consumers to query for harnesses inside the element. I've also deprecated a few similar methods that were added before `ContentContainerComponentHarness` was available. That being said, I wasn't totally sure whether the testing APIs fall under the same deprecation policy as everything else so I played it safe. Finally, I cleaned up some `@dynamic` annotations which weren't doing anything, as far as I could tell. Co-authored-by: Wagner Maciel <wagnermaciel@google.com>
1 parent 1328420 commit 8b670ab

File tree

32 files changed

+236
-88
lines changed

32 files changed

+236
-88
lines changed

src/cdk/testing/test-harnesses.md

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ testing.
1111

1212
`@angular/cdk/testing` contains infrastructure for creating and using component test harnesses. You
1313
can create test harnesses for any component, ranging from small reusable widgets to full application
14-
pages.
14+
pages.
1515

1616
The component harness system supports multiple testing environments. You can use the same harness
1717
implementation in both unit and end-to-end tests. This means that users only need to learn one API,
@@ -26,7 +26,7 @@ This document provides guidance for three types of developers:
2626
1. [Test authors](#api-for-test-authors)
2727
2. [Component harness authors](#api-for-component-harness-authors)
2828
3. [Harness environment authors](#api-for-harness-environment-authors)
29-
29+
3030
Since many developers fall into only one of these categories, the relevant APIs are broken out by
3131
developer type in the sections below.
3232

@@ -67,7 +67,7 @@ create `ComponentHarness` instances directly.
6767
In most cases, you can create a `HarnessLoader` in the `beforeEach` block using
6868
`TestbedHarnessEnvironment.loader(fixture)` and then use that `HarnessLoader` to create any
6969
necessary `ComponentHarness` instances. The other methods cover special cases as shown in this
70-
example:
70+
example:
7171

7272
Consider a reusable dialog-button component that opens a dialog on click, containing the following
7373
components, each with a corresponding harness:
@@ -136,7 +136,7 @@ are used to create `ComponentHarness` instances for elements under this root ele
136136
| `getHarness<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> \| HarnessPredicate<T>): Promise<T>` | Searches for an instance of the given `ComponentHarness` class or `HarnessPredicate` below the root element of this `HarnessLoader` and returns an instance of the harness corresponding to the first matching element |
137137
| `getAllHarnesses<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T> \| HarnessPredicate<T>): Promise<T[]>` | Acts like `getHarness`, but returns an array of harness instances, one for each matching element, rather than just the first matching element |
138138

139-
Calls to `getHarness` and `getAllHarnesses` can either take `ComponentHarness` subclass or a
139+
Calls to `getHarness` and `getAllHarnesses` can either take `ComponentHarness` subclass or a
140140
`HarnessPredicate`. `HarnessPredicate` applies additional restrictions to the search (e.g. searching
141141
for a button that has some particular text, etc). The
142142
[details of `HarnessPredicate`](#filtering-harness-instances-with-harnesspredicate) are discussed in
@@ -149,7 +149,7 @@ created manually.
149149

150150
To support both unit and end-to-end tests, and to insulate tests against changes in
151151
asynchronous behavior, almost all harness methods are asynchronous and return a `Promise`;
152-
therefore, the Angular team recommends using
152+
therefore, the Angular team recommends using
153153
[ES2017 `async`/`await` syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)
154154
to improve the test readability.
155155

@@ -226,7 +226,7 @@ corresponding component. You can access the component's host element via the `ho
226226
the `ComponentHarness` base class.
227227

228228
`ComponentHarness` additionally offers several methods for locating elements within the component's
229-
DOM. These methods are `locatorFor`, `locatorForOptional`, and `locatorForAll`.
229+
DOM. These methods are `locatorFor`, `locatorForOptional`, and `locatorForAll`.
230230
Note, though, that these methods do not directly find elements. Instead, they _create functions_
231231
that find elements. This approach safeguards against caching references to out-of-date elements. For
232232
example, when an `ngIf` hides and then shows an element, the result is a new DOM element; using
@@ -395,7 +395,7 @@ for adding options.
395395
| `add(description: string, predicate: (harness: T) => Promise<boolean>): HarnessPredicate<T>` | Creates a new `HarnessPredicate` that enforces all of the conditions of the current one, plus the new constraint specified by the `predicate` parameter. |
396396

397397
For example, when working with a menu it would likely be useful to add a way to filter based on
398-
trigger text and to filter menu items based on their text:
398+
trigger text and to filter menu items based on their text:
399399

400400
```ts
401401
interface MyMenuHarnessFilters extends BaseHarnessFilters {
@@ -482,17 +482,14 @@ several APIs that can be used to create `HarnessLoader` instances for cases like
482482
| `harnessLoaderForOptional(selector: string): Promise<HarnessLoader \| null>` | Gets a `Promise` for a `HarnessLoader` rooted at the first element matching the given selector, if no element is found the `Promise` resolves to `null`. |
483483
| `harnessLoaderForAll(selector: string): Promise<HarnessLoader[]>` | Gets a `Promise` for a list of `HarnessLoader`, one rooted at each element matching the given selector. |
484484

485+
485486
The `MyPopup` component discussed earlier is a good example of a component with arbitrary content
486-
that users may want to load harnesses for. `MyPopupHarness` could add support for this:
487+
that users may want to load harnesses for. `MyPopupHarness` could add support for this by
488+
extending `ContentContainerComponentHarness`.
487489

488490
```ts
489-
class MyPopupHarness extends ComponentHarness {
491+
class MyPopupHarness extends ContentContainerComponentHarness<string> {
490492
static hostSelector = 'my-popup';
491-
492-
/** Gets a `HarnessLoader` whose root element is the popup's content element. */
493-
async getHarnessLoaderForContent(): Promise<HarnessLoader> {
494-
return this.harnessLoaderFor('my-popup-content');
495-
}
496493
}
497494
```
498495

@@ -550,7 +547,7 @@ may need to explicitly wait for tasks outside `NgZone`, as this does not happen
550547
Harness environment authors are developers who want to add support for using component harnesses in
551548
additional testing environments. Out-of-the-box, Angular CDK's component harnesses can be used in
552549
Protractor E2E tests and Karma unit tests. Developers can support additional environments by
553-
creating custom implementations of `TestElement` and `HarnessEnvironment`.
550+
creating custom implementations of `TestElement` and `HarnessEnvironment`.
554551

555552
#### Creating a `TestElement` implementation for the environment
556553

@@ -585,15 +582,15 @@ implementing the `sendKeys` method, is that the key codes in the `TestKey`
585582
enum likely differ from the key codes used in the test environment. Environment authors should
586583
maintain a mapping from `TestKey` codes to the codes used in the particular testing environment.
587584

588-
The
585+
The
589586
[`UnitTestElement`](https://github.com/angular/components/blob/master/src/cdk/testing/testbed/unit-test-element.ts#L57)
590-
and
587+
and
591588
[`ProtractorElement`](https://github.com/angular/components/blob/master/src/cdk/testing/protractor/protractor-element.ts#L67)
592589
implementations in Angular CDK serve as good examples of implementations of this interface.
593590

594591
#### Creating a `HarnessEnvironemnt` implementation for the environment
595592

596-
Test authors use `HarnessEnvironemnt` to create component harness instances for use in tests.
593+
Test authors use `HarnessEnvironemnt` to create component harness instances for use in tests.
597594

598595
`HarnessEnvironment` is an abstract class that must be extended to create a concrete subclass for
599596
the new environment. When supporting a new test environment, you must create a `HarnessEnvironment`
@@ -623,8 +620,8 @@ require arguments to be passed. (e.g. the `loader` method on `TestbedHarnessEnvi
623620
`ComponentFixture`, and the class provides additional static methods called `documentRootLoader` and
624621
`harnessForFixture`).
625622

626-
The
623+
The
627624
[`TestbedHarnessEnvironment`](https://github.com/angular/components/blob/master/src/cdk/testing/testbed/testbed-harness-environment.ts#L20)
628-
and
625+
and
629626
[`ProtractorHarnessEnvironment`](https://github.com/angular/components/blob/master/src/cdk/testing/protractor/protractor-harness-environment.ts#L16)
630627
implementations in Angular CDK serve as good examples of implementations of this interface.

src/cdk/testing/tests/harnesses/sub-component-harness.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ export class SubComponentHarness extends ComponentHarness {
4040
}
4141
}
4242

43-
/** @dynamic */
4443
export class SubComponentSpecialHarness extends SubComponentHarness {
4544
static readonly hostSelector = 'test-sub.test-special';
4645
}

src/material-experimental/mdc-menu/testing/menu-harness.ts

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

9-
import {ComponentHarness, HarnessPredicate, TestElement, TestKey} from '@angular/cdk/testing';
9+
import {
10+
ComponentHarness,
11+
ContentContainerComponentHarness,
12+
HarnessLoader,
13+
HarnessPredicate,
14+
HarnessQuery,
15+
TestElement,
16+
TestKey,
17+
} from '@angular/cdk/testing';
1018
import {coerceBooleanProperty} from '@angular/cdk/coercion';
1119
import {MenuHarnessFilters, MenuItemHarnessFilters} from '@angular/material/menu/testing';
1220

1321
/** Harness for interacting with an MDC-based mat-menu in tests. */
14-
export class MatMenuHarness extends ComponentHarness {
22+
export class MatMenuHarness extends ContentContainerComponentHarness<string> {
1523
/** The selector for the host element of a `MatMenu` instance. */
1624
static hostSelector = '.mat-menu-trigger';
1725

@@ -119,6 +127,28 @@ export class MatMenuHarness extends ComponentHarness {
119127
return menu.clickItem(...subItemFilters as [Omit<MenuItemHarnessFilters, 'ancestor'>]);
120128
}
121129

130+
async getChildLoader(selector: string): Promise<HarnessLoader> {
131+
return (await this._getPanelLoader()).getChildLoader(selector);
132+
}
133+
134+
async getAllChildLoaders(selector: string): Promise<HarnessLoader[]> {
135+
return (await this._getPanelLoader()).getAllChildLoaders(selector);
136+
}
137+
138+
async getHarness<T extends ComponentHarness>(query: HarnessQuery<T>): Promise<T> {
139+
return (await this._getPanelLoader()).getHarness(query);
140+
}
141+
142+
async getAllHarnesses<T extends ComponentHarness>(query: HarnessQuery<T>): Promise<T[]> {
143+
return (await this._getPanelLoader()).getAllHarnesses(query);
144+
}
145+
146+
/** Gets the element id for the content of the current step. */
147+
private async _getPanelLoader(): Promise<HarnessLoader> {
148+
const panelId = await this._getPanelId();
149+
return this.documentRootLocatorFactory().harnessLoaderFor(`#${panelId}`);
150+
}
151+
122152
/** Gets the menu panel associated with this menu. */
123153
private async _getMenuPanel(): Promise<TestElement | null> {
124154
const panelId = await this._getPanelId();

src/material-experimental/mdc-table/testing/table-harness.ts

Lines changed: 2 additions & 2 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} from '@angular/cdk/testing';
9+
import {ContentContainerComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
1010
import {TableHarnessFilters, RowHarnessFilters} from './table-harness-filters';
1111
import {
1212
MatRowHarness,
@@ -25,7 +25,7 @@ export interface MatTableHarnessColumnsText {
2525
}
2626

2727
/** Harness for interacting with an MDC-based mat-table in tests. */
28-
export class MatTableHarness extends ComponentHarness {
28+
export class MatTableHarness extends ContentContainerComponentHarness<string> {
2929
/** The selector for the host element of a `MatTableHarness` instance. */
3030
static hostSelector = '.mat-mdc-table';
3131

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

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

9-
import {ComponentHarness, HarnessLoader, HarnessPredicate} from '@angular/cdk/testing';
9+
import {
10+
ComponentHarness,
11+
ContentContainerComponentHarness,
12+
HarnessLoader,
13+
HarnessPredicate,
14+
HarnessQuery,
15+
} from '@angular/cdk/testing';
1016
import {TabHarnessFilters} from './tab-harness-filters';
1117

1218
/** Harness for interacting with an MDC_based Angular Material tab in tests. */
13-
export class MatTabHarness extends ComponentHarness {
19+
export class MatTabHarness extends ContentContainerComponentHarness<string> {
1420
/** The selector for the host element of a `MatTab` instance. */
1521
static hostSelector = '.mat-mdc-tab';
1622

@@ -68,12 +74,30 @@ export class MatTabHarness extends ComponentHarness {
6874
/**
6975
* Gets a `HarnessLoader` that can be used to load harnesses for components within the tab's
7076
* content area.
77+
* @deprecated Use `getHarness` or `getChildLoader` instead.
78+
* @breaking-change 12.0.0
7179
*/
7280
async getHarnessLoaderForContent(): Promise<HarnessLoader> {
7381
const contentId = await this._getContentId();
7482
return this.documentRootLocatorFactory().harnessLoaderFor(`#${contentId}`);
7583
}
7684

85+
async getChildLoader(selector: string): Promise<HarnessLoader> {
86+
return (await this.getHarnessLoaderForContent()).getChildLoader(selector);
87+
}
88+
89+
async getAllChildLoaders(selector: string): Promise<HarnessLoader[]> {
90+
return (await this.getHarnessLoaderForContent()).getAllChildLoaders(selector);
91+
}
92+
93+
async getHarness<T extends ComponentHarness>(query: HarnessQuery<T>): Promise<T> {
94+
return (await this.getHarnessLoaderForContent()).getHarness(query);
95+
}
96+
97+
async getAllHarnesses<T extends ComponentHarness>(query: HarnessQuery<T>): Promise<T[]> {
98+
return (await this.getHarnessLoaderForContent()).getAllHarnesses(query);
99+
}
100+
77101
/** Gets the element id for the content of the current tab. */
78102
private async _getContentId(): Promise<string> {
79103
const hostEl = await this.host();

src/material/badge/testing/badge-harness.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ import {MatBadgePosition, MatBadgeSize} from '@angular/material/badge';
1111
import {BadgeHarnessFilters} from './badge-harness-filters';
1212

1313

14-
/**
15-
* Harness for interacting with a standard Material badge in tests.
16-
* @dynamic
17-
*/
14+
/** Harness for interacting with a standard Material badge in tests. */
1815
export class MatBadgeHarness extends ComponentHarness {
1916
static hostSelector = '.mat-badge';
2017

src/material/bottom-sheet/testing/bottom-sheet-harness.ts

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

9-
import {ComponentHarness, HarnessPredicate, TestKey} from '@angular/cdk/testing';
9+
import {ContentContainerComponentHarness, HarnessPredicate, TestKey} from '@angular/cdk/testing';
1010
import {BottomSheetHarnessFilters} from './bottom-sheet-harness-filters';
1111

12-
/**
13-
* Harness for interacting with a standard MatBottomSheet in tests.
14-
* @dynamic
15-
*/
16-
export class MatBottomSheetHarness extends ComponentHarness {
12+
/** Harness for interacting with a standard MatBottomSheet in tests. */
13+
export class MatBottomSheetHarness extends ContentContainerComponentHarness<string> {
1714
// Developers can provide a custom component or template for the
1815
// bottom sheet. The canonical parent is the ".mat-bottom-sheet-container".
1916
static hostSelector = '.mat-bottom-sheet-container';

src/material/dialog/testing/dialog-harness.ts

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

9-
import {ComponentHarness, HarnessPredicate, TestKey} from '@angular/cdk/testing';
9+
import {ContentContainerComponentHarness, HarnessPredicate, TestKey} from '@angular/cdk/testing';
1010
import {DialogRole} from '@angular/material/dialog';
1111
import {DialogHarnessFilters} from './dialog-harness-filters';
1212

1313
/** Harness for interacting with a standard `MatDialog` in tests. */
14-
export class MatDialogHarness extends ComponentHarness {
14+
export class MatDialogHarness extends ContentContainerComponentHarness<string> {
1515
// Developers can provide a custom component or template for the
1616
// dialog. The canonical dialog parent is the "MatDialogContainer".
1717
/** The selector for the host element of a `MatDialog` instance. */

src/material/divider/testing/divider-harness.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@
99
import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
1010
import {DividerHarnessFilters} from './divider-harness-filters';
1111

12-
/**
13-
* Harness for interacting with a `mat-divider`.
14-
* @dynamic
15-
*/
12+
/** Harness for interacting with a `mat-divider`. */
1613
export class MatDividerHarness extends ComponentHarness {
1714
static hostSelector = '.mat-divider';
1815

src/material/expansion/testing/expansion-harness.ts

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

9-
import {ComponentHarness, HarnessLoader, HarnessPredicate} from '@angular/cdk/testing';
9+
import {
10+
ContentContainerComponentHarness,
11+
HarnessLoader,
12+
HarnessPredicate,
13+
} from '@angular/cdk/testing';
1014
import {ExpansionPanelHarnessFilters} from './expansion-harness-filters';
1115

12-
const EXPANSION_PANEL_CONTENT_SELECTOR = '.mat-expansion-panel-content';
16+
/** Selectors for the various `mat-expansion-panel` sections that may contain user content. */
17+
export const enum MatExpansionPanelSection {
18+
HEADER = '.mat-expansion-panel-header',
19+
TITLE = '.mat-expansion-panel-header-title',
20+
DESCRIPTION = '.mat-expansion-panel-header-description',
21+
CONTENT = '.mat-expansion-panel-content'
22+
}
1323

1424
/** Harness for interacting with a standard mat-expansion-panel in tests. */
15-
export class MatExpansionPanelHarness extends ComponentHarness {
25+
export class MatExpansionPanelHarness extends
26+
ContentContainerComponentHarness<MatExpansionPanelSection> {
1627
static hostSelector = '.mat-expansion-panel';
1728

18-
private _header = this.locatorFor('.mat-expansion-panel-header');
19-
private _title = this.locatorForOptional('.mat-expansion-panel-header-title');
20-
private _description = this.locatorForOptional('.mat-expansion-panel-header-description');
29+
private _header = this.locatorFor(MatExpansionPanelSection.HEADER);
30+
private _title = this.locatorForOptional(MatExpansionPanelSection.TITLE);
31+
private _description = this.locatorForOptional(MatExpansionPanelSection.DESCRIPTION);
2132
private _expansionIndicator = this.locatorForOptional('.mat-expansion-indicator');
22-
private _content = this.locatorFor(EXPANSION_PANEL_CONTENT_SELECTOR);
33+
private _content = this.locatorFor(MatExpansionPanelSection.CONTENT);
2334

2435
/**
2536
* Gets a `HarnessPredicate` that can be used to search for an expansion-panel
@@ -110,9 +121,12 @@ export class MatExpansionPanelHarness extends ComponentHarness {
110121
/**
111122
* Gets a `HarnessLoader` that can be used to load harnesses for
112123
* components within the panel's content area.
124+
* @deprecated Use either `getChildLoader(MatExpansionPanelSection.CONTENT)`, `getHarness` or
125+
* `getAllHarnesses` instead.
126+
* @breaking-change 12.0.0
113127
*/
114128
async getHarnessLoaderForContent(): Promise<HarnessLoader> {
115-
return this.locatorFactory.harnessLoaderFor(EXPANSION_PANEL_CONTENT_SELECTOR);
129+
return this.getChildLoader(MatExpansionPanelSection.CONTENT);
116130
}
117131

118132
/** Focuses the panel. */

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,7 @@ export function runHarnessTests(
187187
it('should be able to get harness loader for content of panel', async () => {
188188
const panel =
189189
await loader.getHarness(expansionPanelHarness.with({selector: '#standalonePanel'}));
190-
const contentHarnessLoader = await panel.getHarnessLoaderForContent();
191-
const matchedHarnesses = await contentHarnessLoader.getAllHarnesses(TestContentHarness);
190+
const matchedHarnesses = await panel.getAllHarnesses(TestContentHarness);
192191
expect(matchedHarnesses.length).toBe(1);
193192
expect(await matchedHarnesses[0].getText()).toBe('Part of expansion panel');
194193
});

0 commit comments

Comments
 (0)