Skip to content

Commit 165622e

Browse files
authored
feat(chips): add test harness (#20028)
Adds test harnesses for the Material chips and the related components.
1 parent a6f1a33 commit 165622e

File tree

14 files changed

+827
-0
lines changed

14 files changed

+827
-0
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@
293293
/tools/public_api_guard/material/card.d.ts @jelbourn
294294
/tools/public_api_guard/material/checkbox.d.ts @jelbourn @devversion
295295
/tools/public_api_guard/material/chips.d.ts @jelbourn
296+
/tools/public_api_guard/material/chips/testing.d.ts @jelbourn
296297
/tools/public_api_guard/material/core.d.ts @jelbourn
297298
/tools/public_api_guard/material/datepicker.d.ts @mmalerba
298299
/tools/public_api_guard/material/dialog.d.ts @jelbourn @crisbeto

src/material/chips/chip-list.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,10 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo
418418
/** Associates an HTML input element with this chip list. */
419419
registerInput(inputElement: MatChipTextControl): void {
420420
this._chipInput = inputElement;
421+
422+
// We use this attribute to match the chip list to its input in test harnesses.
423+
// Set the attribute directly here to avoid "changed after checked" errors.
424+
this._elementRef.nativeElement.setAttribute('data-mat-chip-input', inputElement.id);
421425
}
422426

423427
/**
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
load("//tools:defaults.bzl", "ng_test_library", "ng_web_test_suite", "ts_library")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ts_library(
6+
name = "testing",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
module_name = "@angular/material/chips/testing",
12+
deps = [
13+
"//src/cdk/testing",
14+
],
15+
)
16+
17+
filegroup(
18+
name = "source-files",
19+
srcs = glob(["**/*.ts"]),
20+
)
21+
22+
ng_test_library(
23+
name = "harness_tests_lib",
24+
srcs = ["shared.spec.ts"],
25+
deps = [
26+
":testing",
27+
"//src/cdk/testing",
28+
"//src/cdk/testing/private",
29+
"//src/cdk/testing/testbed",
30+
"//src/material/chips",
31+
"//src/material/form-field",
32+
"@npm//@angular/platform-browser",
33+
],
34+
)
35+
36+
ng_test_library(
37+
name = "unit_tests_lib",
38+
srcs = glob(
39+
["**/*.spec.ts"],
40+
exclude = ["shared.spec.ts"],
41+
),
42+
deps = [
43+
":harness_tests_lib",
44+
":testing",
45+
"//src/material/chips",
46+
],
47+
)
48+
49+
ng_web_test_suite(
50+
name = "unit_tests",
51+
deps = [":unit_tests_lib"],
52+
)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
import {BaseHarnessFilters} from '@angular/cdk/testing';
9+
10+
/** A set of criteria that can be used to filter a list of `MatChipHarness` instances. */
11+
export interface ChipHarnessFilters extends BaseHarnessFilters {
12+
/** Only find instances whose text matches the given value. */
13+
text?: string | RegExp;
14+
/** Only find chip instances whose selected state matches the given value. */
15+
selected?: boolean;
16+
}
17+
18+
/** A set of criteria that can be used to filter a list of `MatChipListHarness` instances. */
19+
export interface ChipListHarnessFilters extends BaseHarnessFilters {}
20+
21+
/** A set of criteria that can be used to filter a list of `MatChipListInputHarness` instances. */
22+
export interface ChipInputHarnessFilters extends BaseHarnessFilters {
23+
/** Filters based on the value of the input. */
24+
value?: string | RegExp;
25+
/** Filters based on the placeholder text of the input. */
26+
placeholder?: string | RegExp;
27+
}
28+
29+
/** A set of criteria that can be used to filter a list of `MatChipRemoveHarness` instances. */
30+
export interface ChipRemoveHarnessFilters extends BaseHarnessFilters {}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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, TestKey} from '@angular/cdk/testing';
10+
import {ChipHarnessFilters, ChipRemoveHarnessFilters} from './chip-harness-filters';
11+
import {MatChipRemoveHarness} from './chip-remove-harness';
12+
13+
/** Harness for interacting with a standard Angular Material chip in tests. */
14+
export class MatChipHarness extends ComponentHarness {
15+
/** The selector for the host element of a `MatChip` instance. */
16+
static hostSelector = '.mat-chip';
17+
18+
/**
19+
* Gets a `HarnessPredicate` that can be used to search for a `MatChipHarness` that meets
20+
* certain criteria.
21+
* @param options Options for filtering which chip instances are considered a match.
22+
* @return a `HarnessPredicate` configured with the given options.
23+
*/
24+
static with(options: ChipHarnessFilters = {}): HarnessPredicate<MatChipHarness> {
25+
return new HarnessPredicate(MatChipHarness, options)
26+
.addOption('text', options.text,
27+
(harness, label) => HarnessPredicate.stringMatches(harness.getText(), label))
28+
.addOption('selected', options.selected,
29+
async (harness, selected) => (await harness.isSelected()) === selected);
30+
}
31+
32+
/** Gets the text of the chip. */
33+
async getText(): Promise<string> {
34+
return (await this.host()).text();
35+
}
36+
37+
/** Whether the chip is selected. */
38+
async isSelected(): Promise<boolean> {
39+
return (await this.host()).hasClass('mat-chip-selected');
40+
}
41+
42+
/** Whether the chip is disabled. */
43+
async isDisabled(): Promise<boolean> {
44+
return (await this.host()).hasClass('mat-chip-disabled');
45+
}
46+
47+
/** Selects the given chip. Only applies if it's selectable. */
48+
async select(): Promise<void> {
49+
if (!(await this.isSelected())) {
50+
await this.toggle();
51+
}
52+
}
53+
54+
/** Deselects the given chip. Only applies if it's selectable. */
55+
async deselect(): Promise<void> {
56+
if (await this.isSelected()) {
57+
await this.toggle();
58+
}
59+
}
60+
61+
/** Toggles the selected state of the given chip. Only applies if it's selectable. */
62+
async toggle(): Promise<void> {
63+
return (await this.host()).sendKeys(' ');
64+
}
65+
66+
/** Removes the given chip. Only applies if it's removable. */
67+
async remove(): Promise<void> {
68+
await (await this.host()).sendKeys(TestKey.DELETE);
69+
}
70+
71+
/**
72+
* Gets the remove button inside of a chip.
73+
* @param filter Optionally filters which chips are included.
74+
*/
75+
async getRemoveButton(filter: ChipRemoveHarnessFilters = {}): Promise<MatChipRemoveHarness> {
76+
return this.locatorFor(MatChipRemoveHarness.with(filter))();
77+
}
78+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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 {HarnessPredicate, ComponentHarness, TestKey} from '@angular/cdk/testing';
10+
import {ChipInputHarnessFilters} from './chip-harness-filters';
11+
12+
/** Harness for interacting with a standard Material chip inputs in tests. */
13+
export class MatChipInputHarness extends ComponentHarness {
14+
static hostSelector = '.mat-chip-input';
15+
16+
/**
17+
* Gets a `HarnessPredicate` that can be used to search for a `MatChipInputHarness` that meets
18+
* certain criteria.
19+
* @param options Options for filtering which input instances are considered a match.
20+
* @return a `HarnessPredicate` configured with the given options.
21+
*/
22+
static with(options: ChipInputHarnessFilters = {}): HarnessPredicate<MatChipInputHarness> {
23+
return new HarnessPredicate(MatChipInputHarness, options)
24+
.addOption('value', options.value, async (harness, value) => {
25+
return (await harness.getValue()) === value;
26+
})
27+
.addOption('placeholder', options.placeholder, async (harness, placeholder) => {
28+
return (await harness.getPlaceholder()) === placeholder;
29+
});
30+
}
31+
32+
/** Whether the input is disabled. */
33+
async isDisabled(): Promise<boolean> {
34+
return (await this.host()).getProperty('disabled')!;
35+
}
36+
37+
/** Whether the input is required. */
38+
async isRequired(): Promise<boolean> {
39+
return (await this.host()).getProperty('required')!;
40+
}
41+
42+
/** Gets the value of the input. */
43+
async getValue(): Promise<string> {
44+
// The "value" property of the native input is never undefined.
45+
return (await (await this.host()).getProperty('value'))!;
46+
}
47+
48+
/** Gets the placeholder of the input. */
49+
async getPlaceholder(): Promise<string> {
50+
return (await (await this.host()).getProperty('placeholder'));
51+
}
52+
53+
/**
54+
* Focuses the input and returns a promise that indicates when the
55+
* action is complete.
56+
*/
57+
async focus(): Promise<void> {
58+
return (await this.host()).focus();
59+
}
60+
61+
/**
62+
* Blurs the input and returns a promise that indicates when the
63+
* action is complete.
64+
*/
65+
async blur(): Promise<void> {
66+
return (await this.host()).blur();
67+
}
68+
69+
/** Whether the input is focused. */
70+
async isFocused(): Promise<boolean> {
71+
return (await this.host()).isFocused();
72+
}
73+
74+
/**
75+
* Sets the value of the input. The value will be set by simulating
76+
* keypresses that correspond to the given value.
77+
*/
78+
async setValue(newValue: string): Promise<void> {
79+
const inputEl = await this.host();
80+
await inputEl.clear();
81+
82+
// We don't want to send keys for the value if the value is an empty
83+
// string in order to clear the value. Sending keys with an empty string
84+
// still results in unnecessary focus events.
85+
if (newValue) {
86+
await inputEl.sendKeys(newValue);
87+
}
88+
}
89+
90+
/** Sends a chip separator key to the input element. */
91+
async sendSeparatorKey(key: TestKey | string): Promise<void> {
92+
const inputEl = await this.host();
93+
return inputEl.sendKeys(key);
94+
}
95+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {MatChipsModule} from '@angular/material/chips';
2+
import {runHarnessTests} from '@angular/material/chips/testing/shared.spec';
3+
import {MatChipListHarness} from './chip-list-harness';
4+
import {MatChipHarness} from './chip-harness';
5+
import {MatChipInputHarness} from './chip-input-harness';
6+
import {MatChipRemoveHarness} from './chip-remove-harness';
7+
8+
describe('Non-MDC-based MatChipListHarness', () => {
9+
runHarnessTests(MatChipsModule, MatChipListHarness, MatChipHarness, MatChipInputHarness,
10+
MatChipRemoveHarness);
11+
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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 {MatChipHarness} from './chip-harness';
11+
import {MatChipInputHarness} from './chip-input-harness';
12+
import {
13+
ChipListHarnessFilters,
14+
ChipHarnessFilters,
15+
ChipInputHarnessFilters,
16+
} from './chip-harness-filters';
17+
18+
/** Harness for interacting with a standard chip list in tests. */
19+
export class MatChipListHarness extends ComponentHarness {
20+
/** The selector for the host element of a `MatChipList` instance. */
21+
static hostSelector = '.mat-chip-list';
22+
23+
/**
24+
* Gets a `HarnessPredicate` that can be used to search for a `MatChipListHarness` that meets
25+
* certain criteria.
26+
* @param options Options for filtering which chip list instances are considered a match.
27+
* @return a `HarnessPredicate` configured with the given options.
28+
*/
29+
static with(options: ChipListHarnessFilters = {}): HarnessPredicate<MatChipListHarness> {
30+
return new HarnessPredicate(MatChipListHarness, options);
31+
}
32+
33+
/** Gets whether the chip list is disabled. */
34+
async isDisabled(): Promise<boolean> {
35+
return await (await this.host()).getAttribute('aria-disabled') === 'true';
36+
}
37+
38+
/** Gets whether the chip list is required. */
39+
async isRequired(): Promise<boolean> {
40+
return await (await this.host()).getAttribute('aria-required') === 'true';
41+
}
42+
43+
/** Gets whether the chip list is invalid. */
44+
async isInvalid(): Promise<boolean> {
45+
return await (await this.host()).getAttribute('aria-invalid') === 'true';
46+
}
47+
48+
/** Gets whether the chip list is in multi selection mode. */
49+
async isMultiple(): Promise<boolean> {
50+
return await (await this.host()).getAttribute('aria-multiselectable') === 'true';
51+
}
52+
53+
/** Gets whether the orientation of the chip list. */
54+
async getOrientation(): Promise<'horizontal' | 'vertical'> {
55+
const orientation = await (await this.host()).getAttribute('aria-orientation');
56+
return orientation === 'vertical' ? 'vertical' : 'horizontal';
57+
}
58+
59+
/**
60+
* Gets the list of chips inside the chip list.
61+
* @param filter Optionally filters which chips are included.
62+
*/
63+
async getChips(filter: ChipHarnessFilters = {}): Promise<MatChipHarness[]> {
64+
return this.locatorForAll(MatChipHarness.with(filter))();
65+
}
66+
67+
/**
68+
* Selects a chip inside the chip list.
69+
* @param filter An optional filter to apply to the child chips.
70+
* All the chips matching the filter will be selected.
71+
*/
72+
async selectChips(filter: ChipHarnessFilters = {}): Promise<void> {
73+
const chips = await this.getChips(filter);
74+
if (!chips.length) {
75+
throw Error(`Cannot find mat-chip matching filter ${JSON.stringify(filter)}`);
76+
}
77+
await Promise.all(chips.map(chip => chip.select()));
78+
}
79+
80+
/**
81+
* Gets the `MatChipInput` inside the chip list.
82+
* @param filter Optionally filters which chip input is included.
83+
*/
84+
async getInput(filter: ChipInputHarnessFilters = {}): Promise<MatChipInputHarness> {
85+
// The input isn't required to be a descendant of the chip list so we have to look it up by id.
86+
const inputId = await (await this.host()).getAttribute('data-mat-chip-input');
87+
88+
if (!inputId) {
89+
throw Error(`Chip list is not associated with an input`);
90+
}
91+
92+
return this.documentRootLocatorFactory().locatorFor(
93+
MatChipInputHarness.with({...filter, selector: `#${inputId}`}))();
94+
}
95+
}

0 commit comments

Comments
 (0)