Skip to content

Commit acfa174

Browse files
devversionjelbourn
authored andcommitted
feat(material/expansion): add test harness (#17691)
1 parent d908c9f commit acfa174

File tree

13 files changed

+669
-11
lines changed

13 files changed

+669
-11
lines changed

src/cdk/testing/component-harness.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -406,16 +406,23 @@ export class HarnessPredicate<T extends ComponentHarness> {
406406
}
407407

408408
/**
409-
* Checks if a string matches the given pattern.
410-
* @param s The string to check, or a Promise for the string to check.
411-
* @param pattern The pattern the string is expected to match. If `pattern` is a string, `s` is
412-
* expected to match exactly. If `pattern` is a regex, a partial match is allowed.
413-
* @return A Promise that resolves to whether the string matches the pattern.
409+
* Checks if the specified nullable string value matches the given pattern.
410+
* @param value The nullable string value to check, or a Promise resolving to the
411+
* nullable string value.
412+
* @param pattern The pattern the value is expected to match. If `pattern` is a string,
413+
* `value` is expected to match exactly. If `pattern` is a regex, a partial match is
414+
* allowed. If `pattern` is `null`, the value is expected to be `null`.
415+
* @return A Promise that resolves to whether the value matches the pattern.
414416
*/
415-
static async stringMatches(s: string | Promise<string>, pattern: string | RegExp):
416-
Promise<boolean> {
417-
s = await s;
418-
return typeof pattern === 'string' ? s === pattern : pattern.test(s);
417+
static async stringMatches(value: string | null | Promise<string | null>,
418+
pattern: string | RegExp | null): Promise<boolean> {
419+
value = await value;
420+
if (pattern === null) {
421+
return value === null;
422+
} else if (value === null) {
423+
return false;
424+
}
425+
return typeof pattern === 'string' ? value === pattern : pattern.test(value);
419426
}
420427

421428
/**

src/material/config.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ entryPoints = [
1616
"dialog/testing",
1717
"divider",
1818
"expansion",
19+
"expansion/testing",
1920
"form-field",
2021
"grid-list",
2122
"icon",

src/material/expansion/accordion.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ import {MatExpansionPanelHeader} from './expansion-panel-header';
3232
useExisting: MatAccordion
3333
}],
3434
host: {
35-
class: 'mat-accordion'
35+
class: 'mat-accordion',
36+
// Class binding which is only used by the test harness as there is no other
37+
// way for the harness to detect if multiple panel support is enabled.
38+
'[class.mat-accordion-multi]': 'this.multi',
3639
}
3740
})
3841
export class MatAccordion extends CdkAccordion implements MatAccordionBase, AfterContentInit {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ng_test_library", "ng_web_test_suite", "ts_library")
4+
5+
ts_library(
6+
name = "testing",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
module_name = "@angular/material/expansion/testing",
12+
deps = [
13+
"//src/cdk/coercion",
14+
"//src/cdk/testing",
15+
],
16+
)
17+
18+
filegroup(
19+
name = "source-files",
20+
srcs = glob(["**/*.ts"]),
21+
)
22+
23+
ng_test_library(
24+
name = "harness_tests_lib",
25+
srcs = ["shared.spec.ts"],
26+
deps = [
27+
":testing",
28+
"//src/cdk/testing",
29+
"//src/cdk/testing/testbed",
30+
"//src/material/expansion",
31+
"@npm//@angular/platform-browser",
32+
],
33+
)
34+
35+
ng_test_library(
36+
name = "unit_tests_lib",
37+
srcs = glob(
38+
["**/*.spec.ts"],
39+
exclude = ["shared.spec.ts"],
40+
),
41+
deps = [
42+
":harness_tests_lib",
43+
":testing",
44+
"//src/material/expansion",
45+
],
46+
)
47+
48+
ng_web_test_suite(
49+
name = "unit_tests",
50+
deps = [":unit_tests_lib"],
51+
)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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 {MatExpansionPanelHarness} from './expansion-harness';
11+
import {AccordionHarnessFilters, ExpansionPanelHarnessFilters} from './expansion-harness-filters';
12+
13+
/** Harness for interacting with a standard mat-accordion in tests. */
14+
export class MatAccordionHarness extends ComponentHarness {
15+
static hostSelector = '.mat-accordion';
16+
17+
/**
18+
* Gets a `HarnessPredicate` that can be used to search for an accordion
19+
* with specific attributes.
20+
* @param options Options for narrowing the search.
21+
* @return a `HarnessPredicate` configured with the given options.
22+
*/
23+
static with(options: AccordionHarnessFilters = {}): HarnessPredicate<MatAccordionHarness> {
24+
return new HarnessPredicate(MatAccordionHarness, options);
25+
}
26+
27+
/** Gets all expansion panels which are part of the accordion. */
28+
async getExpansionPanels(filter: ExpansionPanelHarnessFilters = {}):
29+
Promise<MatExpansionPanelHarness[]> {
30+
return this.locatorForAll(MatExpansionPanelHarness.with(filter))();
31+
}
32+
33+
/** Whether the accordion allows multiple expanded panels simultaneously. */
34+
async isMulti(): Promise<boolean> {
35+
return (await this.host()).hasClass('mat-accordion-multi');
36+
}
37+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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 {BaseHarnessFilters} from '@angular/cdk/testing';
10+
11+
export interface AccordionHarnessFilters extends BaseHarnessFilters {}
12+
13+
export interface ExpansionPanelHarnessFilters extends BaseHarnessFilters {
14+
title?: string|RegExp|null;
15+
description?: string|RegExp|null;
16+
content?: string|RegExp;
17+
expanded?: boolean;
18+
disabled?: boolean;
19+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {MatExpansionModule} from '@angular/material/expansion';
2+
3+
import {MatAccordionHarness} from './accordion-harness';
4+
import {MatExpansionPanelHarness} from './expansion-harness';
5+
import {runHarnessTests} from './shared.spec';
6+
7+
describe('Non-MDC-based expansion harnesses', () => {
8+
runHarnessTests(MatExpansionModule, MatAccordionHarness, MatExpansionPanelHarness);
9+
});
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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, HarnessLoader, HarnessPredicate} from '@angular/cdk/testing';
10+
import {ExpansionPanelHarnessFilters} from './expansion-harness-filters';
11+
12+
const EXPANSION_PANEL_CONTENT_SELECTOR = '.mat-expansion-panel-content';
13+
14+
/** Harness for interacting with a standard mat-expansion-panel in tests. */
15+
export class MatExpansionPanelHarness extends ComponentHarness {
16+
static hostSelector = '.mat-expansion-panel';
17+
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');
21+
private _expansionIndicator = this.locatorForOptional('.mat-expansion-indicator');
22+
private _content = this.locatorFor(EXPANSION_PANEL_CONTENT_SELECTOR);
23+
24+
/**
25+
* Gets a `HarnessPredicate` that can be used to search for an expansion-panel
26+
* with specific attributes.
27+
* @param options Options for narrowing the search:
28+
* - `title` finds an expansion-panel with a specific title text.
29+
* - `description` finds an expansion-panel with a specific description text.
30+
* - `expanded` finds an expansion-panel that is currently expanded.
31+
* - `disabled` finds an expansion-panel that is disabled.
32+
* @return a `HarnessPredicate` configured with the given options.
33+
*/
34+
static with(options: ExpansionPanelHarnessFilters = {}):
35+
HarnessPredicate<MatExpansionPanelHarness> {
36+
return new HarnessPredicate(MatExpansionPanelHarness, options)
37+
.addOption(
38+
'title', options.title,
39+
(harness, title) => HarnessPredicate.stringMatches(harness.getTitle(), title))
40+
.addOption(
41+
'description', options.description,
42+
(harness, description) =>
43+
HarnessPredicate.stringMatches(harness.getDescription(), description))
44+
.addOption(
45+
'content', options.content,
46+
(harness, content) => HarnessPredicate.stringMatches(harness.getTextContent(), content))
47+
.addOption(
48+
'expanded', options.expanded,
49+
async (harness, expanded) => (await harness.isExpanded()) === expanded)
50+
.addOption(
51+
'disabled', options.disabled,
52+
async (harness, disabled) => (await harness.isDisabled()) === disabled);
53+
}
54+
55+
/** Whether the panel is expanded. */
56+
async isExpanded(): Promise<boolean> {
57+
return (await this.host()).hasClass('mat-expanded');
58+
}
59+
60+
/**
61+
* Gets the title text of the panel.
62+
* @returns Title text or `null` if no title is set up.
63+
*/
64+
async getTitle(): Promise<string|null> {
65+
const titleEl = await this._title();
66+
return titleEl ? titleEl.text() : null;
67+
}
68+
69+
/**
70+
* Gets the description text of the panel.
71+
* @returns Description text or `null` if no description is set up.
72+
*/
73+
async getDescription(): Promise<string|null> {
74+
const descriptionEl = await this._description();
75+
return descriptionEl ? descriptionEl.text() : null;
76+
}
77+
78+
/** Whether the panel is disabled. */
79+
async isDisabled(): Promise<boolean> {
80+
return await (await this._header()).getAttribute('aria-disabled') === 'true';
81+
}
82+
83+
/**
84+
* Toggles the expanded state of the panel by clicking on the panel
85+
* header. This method will not work if the panel is disabled.
86+
*/
87+
async toggle(): Promise<void> {
88+
await (await this._header()).click();
89+
}
90+
91+
/** Expands the expansion panel if collapsed. */
92+
async expand(): Promise<void> {
93+
if (!await this.isExpanded()) {
94+
await this.toggle();
95+
}
96+
}
97+
98+
/** Collapses the expansion panel if expanded. */
99+
async collapse(): Promise<void> {
100+
if (await this.isExpanded()) {
101+
await this.toggle();
102+
}
103+
}
104+
105+
/** Gets the text content of the panel. */
106+
async getTextContent(): Promise<string> {
107+
return (await this._content()).text();
108+
}
109+
110+
/**
111+
* Gets a `HarnessLoader` that can be used to load harnesses for
112+
* components within the panel's content area.
113+
*/
114+
async getHarnessLoaderForContent(): Promise<HarnessLoader> {
115+
return this.locatorFactory.harnessLoaderFor(EXPANSION_PANEL_CONTENT_SELECTOR);
116+
}
117+
118+
/** Focuses the panel. */
119+
async focus(): Promise<void> {
120+
return (await this._header()).focus();
121+
}
122+
123+
/** Blurs the panel. */
124+
async blur(): Promise<void> {
125+
return (await this._header()).blur();
126+
}
127+
128+
/** Whether the panel has a toggle indicator displayed. */
129+
async hasToggleIndicator(): Promise<boolean> {
130+
return (await this._expansionIndicator()) !== null;
131+
}
132+
133+
/** Gets the position of the toggle indicator. */
134+
async getToggleIndicatorPosition(): Promise<'before'|'after'> {
135+
// By default the expansion indicator will show "after" the panel header content.
136+
if (await (await this._header()).hasClass('mat-expansion-toggle-indicator-before')) {
137+
return 'before';
138+
}
139+
return 'after';
140+
}
141+
}
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.io/license
7+
*/
8+
9+
export * from './public-api';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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+
export * from './accordion-harness';
10+
export * from './expansion-harness';
11+
export * from './expansion-harness-filters';

0 commit comments

Comments
 (0)