Skip to content

Commit 7a748fc

Browse files
authored
feat(material/autocomplete/testing): polish harness API (#17350)
* feat(material/autocomplete/testing): polish harness API * address comments * automatically focus the input when calling `selectOption` * address comments * rename `MatAutocompleteOptionHarness` `click` method to `select`
1 parent 1cc3a79 commit 7a748fc

File tree

5 files changed

+76
-51
lines changed

5 files changed

+76
-51
lines changed

src/material/autocomplete/testing/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ ng_test_library(
2626
deps = [
2727
":testing",
2828
"//src/cdk/overlay",
29+
"//src/cdk/private/testing",
2930
"//src/cdk/testing",
3031
"//src/cdk/testing/testbed",
3132
"//src/material/autocomplete",

src/material/autocomplete/testing/autocomplete-harness-filters.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@
88

99
import {BaseHarnessFilters} from '@angular/cdk/testing';
1010

11-
export interface AutocompleteHarnessFilters extends BaseHarnessFilters {}
11+
export interface AutocompleteHarnessFilters extends BaseHarnessFilters {
12+
value?: string | RegExp;
13+
}

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

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

9-
import {ComponentHarness, HarnessPredicate, TestElement} from '@angular/cdk/testing';
109
import {coerceBooleanProperty} from '@angular/cdk/coercion';
10+
import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
1111
import {AutocompleteHarnessFilters} from './autocomplete-harness-filters';
12-
import {MatAutocompleteOptionHarness, MatAutocompleteOptionGroupHarness} from './option-harness';
12+
import {
13+
MatAutocompleteOptionGroupHarness,
14+
MatAutocompleteOptionHarness,
15+
OptionGroupHarnessFilters,
16+
OptionHarnessFilters
17+
} from './option-harness';
1318

1419
/** Selector for the autocomplete panel. */
1520
const PANEL_SELECTOR = '.mat-autocomplete-panel';
@@ -20,10 +25,7 @@ const PANEL_SELECTOR = '.mat-autocomplete-panel';
2025
*/
2126
export class MatAutocompleteHarness extends ComponentHarness {
2227
private _documentRootLocator = this.documentRootLocatorFactory();
23-
private _panel = this._documentRootLocator.locatorFor(PANEL_SELECTOR);
2428
private _optionalPanel = this._documentRootLocator.locatorForOptional(PANEL_SELECTOR);
25-
private _options = this._documentRootLocator.locatorForAll(MatAutocompleteOptionHarness);
26-
private _groups = this._documentRootLocator.locatorForAll(MatAutocompleteOptionGroupHarness);
2729

2830
static hostSelector = '.mat-autocomplete-trigger';
2931

@@ -35,11 +37,14 @@ export class MatAutocompleteHarness extends ComponentHarness {
3537
* @return a `HarnessPredicate` configured with the given options.
3638
*/
3739
static with(options: AutocompleteHarnessFilters = {}): HarnessPredicate<MatAutocompleteHarness> {
38-
return new HarnessPredicate(MatAutocompleteHarness, options);
40+
return new HarnessPredicate(MatAutocompleteHarness, options)
41+
.addOption('value', options.value,
42+
(harness, value) => HarnessPredicate.stringMatches(harness.getValue(), value));
3943
}
4044

41-
async getAttribute(attributeName: string): Promise<string|null> {
42-
return (await this.host()).getAttribute(attributeName);
45+
/** Gets the value of the autocomplete input. */
46+
async getValue(): Promise<string> {
47+
return (await this.host()).getProperty('value');
4348
}
4449

4550
/** Gets a boolean promise indicating if the autocomplete input is disabled. */
@@ -48,11 +53,6 @@ export class MatAutocompleteHarness extends ComponentHarness {
4853
return coerceBooleanProperty(await disabled);
4954
}
5055

51-
/** Gets a promise for the autocomplete's text. */
52-
async getText(): Promise<string> {
53-
return (await this.host()).getProperty('value');
54-
}
55-
5656
/** Focuses the input and returns a void promise that indicates when the action is complete. */
5757
async focus(): Promise<void> {
5858
return (await this.host()).focus();
@@ -68,28 +68,31 @@ export class MatAutocompleteHarness extends ComponentHarness {
6868
return (await this.host()).sendKeys(value);
6969
}
7070

71-
/** Gets the autocomplete panel. */
72-
async getPanel(): Promise<TestElement> {
73-
return this._panel();
74-
}
75-
7671
/** Gets the options inside the autocomplete panel. */
77-
async getOptions(): Promise<MatAutocompleteOptionHarness[]> {
78-
return this._options();
72+
async getOptions(filters: OptionHarnessFilters = {}): Promise<MatAutocompleteOptionHarness[]> {
73+
return this._documentRootLocator.locatorForAll(MatAutocompleteOptionHarness.with(filters))();
7974
}
8075

8176
/** Gets the groups of options inside the panel. */
82-
async getOptionGroups(): Promise<MatAutocompleteOptionGroupHarness[]> {
83-
return this._groups();
77+
async getOptionGroups(filters: OptionGroupHarnessFilters = {}):
78+
Promise<MatAutocompleteOptionGroupHarness[]> {
79+
return this._documentRootLocator.locatorForAll(
80+
MatAutocompleteOptionGroupHarness.with(filters))();
8481
}
8582

86-
/** Gets whether the autocomplete panel is visible. */
87-
async isPanelVisible(): Promise<boolean> {
88-
return (await this._panel()).hasClass('mat-autocomplete-visible');
83+
/** Selects the first option matching the given filters. */
84+
async selectOption(filters: OptionHarnessFilters): Promise<void> {
85+
await this.focus(); // Focus the input to make sure the autocomplete panel is shown.
86+
const options = await this.getOptions(filters);
87+
if (!options.length) {
88+
throw Error(`Could not find a mat-option matching ${JSON.stringify(filters)}`);
89+
}
90+
await options[0].select();
8991
}
9092

9193
/** Gets whether the autocomplete is open. */
9294
async isOpen(): Promise<boolean> {
93-
return !!(await this._optionalPanel());
95+
const panel = await this._optionalPanel();
96+
return !!panel && await panel.hasClass('mat-autocomplete-visible');
9497
}
9598
}

src/material/autocomplete/testing/option-harness.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ import {ComponentHarness, HarnessPredicate, BaseHarnessFilters} from '@angular/c
1212
// and expand to cover all states once we have experimental/core.
1313

1414
export interface OptionHarnessFilters extends BaseHarnessFilters {
15-
text?: string;
15+
text?: string | RegExp;
1616
}
1717

1818
export interface OptionGroupHarnessFilters extends BaseHarnessFilters {
19-
labelText?: string;
19+
labelText?: string | RegExp;
2020
}
2121

2222
/**
@@ -29,12 +29,11 @@ export class MatAutocompleteOptionHarness extends ComponentHarness {
2929
static with(options: OptionHarnessFilters = {}) {
3030
return new HarnessPredicate(MatAutocompleteOptionHarness, options)
3131
.addOption('text', options.text,
32-
async (harness, title) =>
33-
HarnessPredicate.stringMatches(await harness.getText(), title));
32+
(harness, text) => HarnessPredicate.stringMatches(harness.getText(), text));
3433
}
3534

3635
/** Clicks the option. */
37-
async click(): Promise<void> {
36+
async select(): Promise<void> {
3837
return (await this.host()).click();
3938
}
4039

@@ -55,13 +54,11 @@ export class MatAutocompleteOptionGroupHarness extends ComponentHarness {
5554
static with(options: OptionGroupHarnessFilters = {}) {
5655
return new HarnessPredicate(MatAutocompleteOptionGroupHarness, options)
5756
.addOption('labelText', options.labelText,
58-
async (harness, title) =>
59-
HarnessPredicate.stringMatches(await harness.getLabelText(), title));
57+
(harness, label) => HarnessPredicate.stringMatches(harness.getLabelText(), label));
6058
}
6159

6260
/** Gets a promise for the option group's label text. */
6361
async getLabelText(): Promise<string> {
6462
return (await this._label()).text();
6563
}
6664
}
67-

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

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {OverlayContainer} from '@angular/cdk/overlay';
2+
import {expectAsyncError} from '@angular/cdk/private/testing';
23
import {HarnessLoader} from '@angular/cdk/testing';
34
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
45
import {Component} from '@angular/core';
@@ -42,9 +43,15 @@ export function runHarnessTests(
4243
expect(inputs.length).toBe(5);
4344
});
4445

45-
it('should be able to get text inside the input', async () => {
46+
it('should load harness for autocomplete with value', async () => {
47+
const ac = await loader.getHarness(autocompleteHarness.with({value: /Prefilled/}));
48+
const id = await (await ac.host()).getAttribute('id');
49+
expect(id).toBe('prefilled');
50+
});
51+
52+
it('should be able to get value of the input', async () => {
4653
const input = await loader.getHarness(autocompleteHarness.with({selector: '#prefilled'}));
47-
expect(await input.getText()).toBe('Prefilled value');
54+
expect(await input.getValue()).toBe('Prefilled value');
4855
});
4956

5057
it('should get disabled state', async () => {
@@ -67,13 +74,7 @@ export function runHarnessTests(
6774
it('should be able to type in an input', async () => {
6875
const input = await loader.getHarness(autocompleteHarness.with({selector: '#plain'}));
6976
await input.enterText('Hello there');
70-
expect(await input.getText()).toBe('Hello there');
71-
});
72-
73-
it('should be able to get the autocomplete panel', async () => {
74-
const input = await loader.getHarness(autocompleteHarness.with({selector: '#plain'}));
75-
await input.focus();
76-
expect(await input.getPanel()).toBeTruthy();
77+
expect(await input.getValue()).toBe('Hello there');
7778
});
7879

7980
it('should be able to get the autocomplete panel options', async () => {
@@ -85,6 +86,15 @@ export function runHarnessTests(
8586
expect(await options[5].getText()).toBe('New York');
8687
});
8788

89+
it('should be able to get filtered options', async () => {
90+
const input = await loader.getHarness(autocompleteHarness.with({selector: '#plain'}));
91+
await input.focus();
92+
const options = await input.getOptions({text: /New/});
93+
94+
expect(options.length).toBe(1);
95+
expect(await options[0].getText()).toBe('New York');
96+
});
97+
8898
it('should be able to get the autocomplete panel groups', async () => {
8999
const input = await loader.getHarness(autocompleteHarness.with({selector: '#grouped'}));
90100
await input.focus();
@@ -95,14 +105,13 @@ export function runHarnessTests(
95105
expect(options.length).toBe(11);
96106
});
97107

98-
it('should be able to get the autocomplete panel', async () => {
99-
// Focusing without any options will render the panel, but it'll be invisible.
100-
fixture.componentInstance.states = [];
101-
fixture.detectChanges();
102-
103-
const input = await loader.getHarness(autocompleteHarness.with({selector: '#plain'}));
108+
it('should be able to get filtered panel groups', async () => {
109+
const input = await loader.getHarness(autocompleteHarness.with({selector: '#grouped'}));
104110
await input.focus();
105-
expect(await input.isPanelVisible()).toBe(false);
111+
const groups = await input.getOptionGroups({labelText: 'Two'});
112+
113+
expect(groups.length).toBe(1);
114+
expect(await groups[0].getLabelText()).toBe('Two');
106115
});
107116

108117
it('should be able to get whether the autocomplete is open', async () => {
@@ -112,6 +121,19 @@ export function runHarnessTests(
112121
await input.focus();
113122
expect(await input.isOpen()).toBe(true);
114123
});
124+
125+
it('should be able to select option', async () => {
126+
const input = await loader.getHarness(autocompleteHarness.with({selector: '#plain'}));
127+
await input.selectOption({text: 'New York'});
128+
expect(await input.getValue()).toBe('NY');
129+
});
130+
131+
it('should throw when selecting an option that is not available', async () => {
132+
const input = await loader.getHarness(autocompleteHarness.with({selector: '#plain'}));
133+
await input.enterText('New');
134+
await expectAsyncError(() => input.selectOption({text: 'Texas'}),
135+
/Error: Could not find a mat-option matching {"text":"Texas"}/);
136+
});
115137
}
116138

117139
function getActiveElementId() {

0 commit comments

Comments
 (0)