Skip to content

Commit 784756d

Browse files
mmalerbajelbourn
authored andcommitted
feat(cdk-experimental/testing): add support for matching selector on TestElement (#16848)
1 parent 52c33c7 commit 784756d

39 files changed

+188
-113
lines changed

src/cdk-experimental/testing/component-harness.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,10 @@ export interface ComponentHarnessConstructor<T extends ComponentHarness> {
259259
hostSelector: string;
260260
}
261261

262+
export interface BaseHarnessFilters {
263+
selector?: string;
264+
}
265+
262266
/**
263267
* A class used to associate a ComponentHarness class with predicates functions that can be used to
264268
* filter instances of the class.
@@ -267,7 +271,14 @@ export class HarnessPredicate<T extends ComponentHarness> {
267271
private _predicates: AsyncPredicate<T>[] = [];
268272
private _descriptions: string[] = [];
269273

270-
constructor(public harnessType: ComponentHarnessConstructor<T>) {}
274+
constructor(public harnessType: ComponentHarnessConstructor<T>, options: BaseHarnessFilters) {
275+
const selector = options.selector;
276+
if (selector !== undefined) {
277+
this.add(`selector matches "${selector}"`, async item => {
278+
return (await item.host()).matchesSelector(selector);
279+
});
280+
}
281+
}
271282

272283
/**
273284
* Checks if a string matches the given pattern.

src/cdk-experimental/testing/harness-environment.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function _getErrorForMissingSelector(selector: string): Error {
2323
function _getErrorForMissingHarness<T extends ComponentHarness>(
2424
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): Error {
2525
const harnessPredicate =
26-
harnessType instanceof HarnessPredicate ? harnessType : new HarnessPredicate(harnessType);
26+
harnessType instanceof HarnessPredicate ? harnessType : new HarnessPredicate(harnessType, {});
2727
const {name, hostSelector} = harnessPredicate.harnessType;
2828
const restrictions = harnessPredicate.getDescription();
2929
let message = `Expected to find element for ${name} matching selector: "${hostSelector}"`;
@@ -160,7 +160,8 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac
160160
private async _getAllHarnesses<T extends ComponentHarness>(
161161
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): Promise<T[]> {
162162
const harnessPredicate =
163-
harnessType instanceof HarnessPredicate ? harnessType : new HarnessPredicate(harnessType);
163+
harnessType instanceof HarnessPredicate ?
164+
harnessType : new HarnessPredicate(harnessType, {});
164165
const elements = await this.getAllRawElements(harnessPredicate.harnessType.hostSelector);
165166
return harnessPredicate.filter(elements.map(
166167
element => this.createComponentHarness(harnessPredicate.harnessType, element)));

src/cdk-experimental/testing/protractor/protractor-element.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,5 +142,16 @@ export class ProtractorElement implements TestElement {
142142
return browser.executeScript(`return arguments[0][arguments[1]]`, this.element, name);
143143
}
144144

145+
async matchesSelector(selector: string): Promise<boolean> {
146+
return browser.executeScript(`
147+
return (Element.prototype.matches ||
148+
Element.prototype.matchesSelector ||
149+
Element.prototype.mozMatchesSelector ||
150+
Element.prototype.msMatchesSelector ||
151+
Element.prototype.oMatchesSelector ||
152+
Element.prototype.webkitMatchesSelector).call(arguments[0], arguments[1])
153+
`, this.element, selector);
154+
}
155+
145156
async forceStabilize(): Promise<void> {}
146157
}

src/cdk-experimental/testing/test-element.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ export interface TestElement {
102102
/** Gets the value of a property of an element. */
103103
getProperty(name: string): Promise<any>;
104104

105+
/** Checks whether this element matches the given selector. */
106+
matchesSelector(selector: string): Promise<boolean>;
107+
105108
/**
106109
* Flushes change detection and async tasks.
107110
* In most cases it should not be necessary to call this. However, there may be some edge cases

src/cdk-experimental/testing/testbed/unit-test-element.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,17 @@ export class UnitTestElement implements TestElement {
138138
return (this.element as any)[name];
139139
}
140140

141+
async matchesSelector(selector: string): Promise<boolean> {
142+
await this._stabilize();
143+
const elementPrototype = Element.prototype as any;
144+
return (elementPrototype['matches'] ||
145+
elementPrototype['matchesSelector'] ||
146+
elementPrototype['mozMatchesSelector'] ||
147+
elementPrototype['msMatchesSelector'] ||
148+
elementPrototype['oMatchesSelector'] ||
149+
elementPrototype['webkitMatchesSelector']).call(this.element, selector);
150+
}
151+
141152
async forceStabilize(): Promise<void> {
142153
return this._stabilize();
143154
}

src/cdk-experimental/testing/tests/harnesses/main-component-harness.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export class MainComponentHarness extends ComponentHarness {
4949
readonly testLists = this.locatorForAll(SubComponentHarness.with({title: /test/}));
5050
readonly requiredFourIteamToolsLists =
5151
this.locatorFor(SubComponentHarness.with({title: 'List of test tools', itemCount: 4}));
52+
readonly lastList = this.locatorFor(SubComponentHarness.with({selector: ':last-child'}));
5253
readonly specaialKey = this.locatorFor('.special-key');
5354

5455
private _testTools = this.locatorFor(SubComponentHarness);

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

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

9-
import {ComponentHarness, HarnessPredicate} from '../../component-harness';
9+
import {BaseHarnessFilters, ComponentHarness, HarnessPredicate} from '../../component-harness';
1010
import {TestElement} from '../../test-element';
1111

12+
export interface SubComponentHarnessFilters extends BaseHarnessFilters {
13+
title?: string | RegExp;
14+
itemCount?: number;
15+
}
16+
1217
/** @dynamic */
1318
export class SubComponentHarness extends ComponentHarness {
1419
static readonly hostSelector = 'test-sub';
1520

16-
static with(options: {title?: string | RegExp, itemCount?: number} = {}) {
17-
return new HarnessPredicate(SubComponentHarness)
21+
static with(options: SubComponentHarnessFilters = {}) {
22+
return new HarnessPredicate(SubComponentHarness, options)
1823
.addOption('title', options.title,
1924
async (harness, title) =>
2025
HarnessPredicate.stringMatches((await harness.title()).text(), title))

src/cdk-experimental/testing/tests/protractor.e2e.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,12 @@ describe('ProtractorHarnessEnvironment', () => {
260260
await input.sendKeys('Hello');
261261
expect(await input.getProperty('value')).toBe('Hello');
262262
});
263+
264+
it('should check if selector matches', async () => {
265+
const button = await harness.button();
266+
expect(await button.matchesSelector('button:not(.fake-class)')).toBe(true);
267+
expect(await button.matchesSelector('button:disabled')).toBe(false);
268+
});
263269
});
264270

265271
describe('HarnessPredicate', () => {
@@ -293,6 +299,11 @@ describe('ProtractorHarnessEnvironment', () => {
293299
expect(await (await testLists[1].title()).text()).toBe('List of test methods');
294300
});
295301

302+
it('should find subcomponents that match selector', async () => {
303+
const lastList = await harness.lastList();
304+
expect(await (await lastList.title()).text()).toBe('List of test methods');
305+
});
306+
296307
it('should error if predicate does not match but a harness is required', async () => {
297308
try {
298309
await harness.requiredFourIteamToolsLists();

src/cdk-experimental/testing/tests/testbed.spec.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ describe('TestbedHarnessEnvironment', () => {
264264
});
265265

266266
it('should focus and blur element', async () => {
267-
let button = await harness.button();
267+
const button = await harness.button();
268268
expect(activeElementText()).not.toBe(await button.text());
269269
await button.focus();
270270
expect(activeElementText()).toBe(await button.text());
@@ -277,6 +277,12 @@ describe('TestbedHarnessEnvironment', () => {
277277
await input.sendKeys('Hello');
278278
expect(await input.getProperty('value')).toBe('Hello');
279279
});
280+
281+
it('should check if selector matches', async () => {
282+
const button = await harness.button();
283+
expect(await button.matchesSelector('button:not(.fake-class)')).toBe(true);
284+
expect(await button.matchesSelector('button:disabled')).toBe(false);
285+
});
280286
});
281287

282288
describe('HarnessPredicate', () => {
@@ -311,6 +317,11 @@ describe('TestbedHarnessEnvironment', () => {
311317
expect(await (await testLists[1].title()).text()).toBe('List of test methods');
312318
});
313319

320+
it('should find subcomponents that match selector', async () => {
321+
const lastList = await harness.lastList();
322+
expect(await (await lastList.title()).text()).toBe('List of test methods');
323+
});
324+
314325
it('should error if predicate does not match but a harness is required', async () => {
315326
try {
316327
await harness.requiredFourIteamToolsLists();

src/material-experimental/mdc-button/harness/button-harness-filters.ts

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

9-
export type ButtonHarnessFilters = {
10-
text?: string | RegExp
11-
};
9+
import {BaseHarnessFilters} from '@angular/cdk-experimental/testing';
10+
11+
export interface ButtonHarnessFilters extends BaseHarnessFilters {
12+
text?: string | RegExp;
13+
}

src/material-experimental/mdc-button/harness/button-harness.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ export class MatButtonHarness extends ComponentHarness {
3030
/**
3131
* Gets a `HarnessPredicate` that can be used to search for a button with specific attributes.
3232
* @param options Options for narrowing the search:
33+
* - `selector` finds a button whose host element matches the given selector.
3334
* - `text` finds a button with specific text content.
3435
* @return a `HarnessPredicate` configured with the given options.
3536
*/
3637
static with(options: ButtonHarnessFilters = {}): HarnessPredicate<MatButtonHarness> {
37-
return new HarnessPredicate(MatButtonHarness)
38+
return new HarnessPredicate(MatButtonHarness, options)
3839
.addOption('text', options.text,
3940
(harness, text) => HarnessPredicate.stringMatches(harness.getText(), text));
4041
}

src/material-experimental/mdc-button/harness/mdc-button-harness.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ export class MatButtonHarness extends ComponentHarness {
3030
/**
3131
* Gets a `HarnessPredicate` that can be used to search for a button with specific attributes.
3232
* @param options Options for narrowing the search:
33+
* - `selector` finds a button whose host element matches the given selector.
3334
* - `text` finds a button with specific text content.
3435
* @return a `HarnessPredicate` configured with the given options.
3536
*/
3637
static with(options: ButtonHarnessFilters = {}): HarnessPredicate<MatButtonHarness> {
37-
return new HarnessPredicate(MatButtonHarness)
38+
return new HarnessPredicate(MatButtonHarness, options)
3839
.addOption('text', options.text,
3940
(harness, text) => HarnessPredicate.stringMatches(harness.getText(), text));
4041
}

src/material-experimental/mdc-checkbox/harness/checkbox-harness-filters.ts

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

9-
export type CheckboxHarnessFilters = {
10-
label?: string|RegExp;
9+
import {BaseHarnessFilters} from '@angular/cdk-experimental/testing';
10+
11+
export interface CheckboxHarnessFilters extends BaseHarnessFilters {
12+
label?: string | RegExp;
1113
name?: string;
12-
};
14+
}

src/material-experimental/mdc-checkbox/harness/checkbox-harness.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ export class MatCheckboxHarness extends ComponentHarness {
2020
/**
2121
* Gets a `HarnessPredicate` that can be used to search for a checkbox with specific attributes.
2222
* @param options Options for narrowing the search:
23+
* - `selector` finds a checkbox whose host element matches the given selector.
2324
* - `label` finds a checkbox with specific label text.
2425
* - `name` finds a checkbox with specific name.
2526
* @return a `HarnessPredicate` configured with the given options.
2627
*/
2728
static with(options: CheckboxHarnessFilters = {}): HarnessPredicate<MatCheckboxHarness> {
28-
return new HarnessPredicate(MatCheckboxHarness)
29+
return new HarnessPredicate(MatCheckboxHarness, options)
2930
.addOption(
3031
'label', options.label,
3132
(harness, label) => HarnessPredicate.stringMatches(harness.getLabelText(), label))

src/material-experimental/mdc-checkbox/harness/mdc-checkbox-harness.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ export class MatCheckboxHarness extends ComponentHarness {
2020
/**
2121
* Gets a `HarnessPredicate` that can be used to search for a checkbox with specific attributes.
2222
* @param options Options for narrowing the search:
23+
* - `selector` finds a checkbox whose host element matches the given selector.
2324
* - `label` finds a checkbox with specific label text.
2425
* - `name` finds a checkbox with specific name.
2526
* @return a `HarnessPredicate` configured with the given options.
2627
*/
2728
static with(options: CheckboxHarnessFilters = {}): HarnessPredicate<MatCheckboxHarness> {
28-
return new HarnessPredicate(MatCheckboxHarness)
29+
return new HarnessPredicate(MatCheckboxHarness, options)
2930
.addOption(
3031
'label', options.label,
3132
(harness, label) => HarnessPredicate.stringMatches(harness.getLabelText(), label))

src/material-experimental/mdc-input/harness/input-harness-filters.ts

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

9-
export type InputHarnessFilters = {
10-
id?: string;
11-
name?: string;
9+
import {BaseHarnessFilters} from '@angular/cdk-experimental/testing';
10+
11+
export interface InputHarnessFilters extends BaseHarnessFilters {
1212
value?: string;
13-
};
13+
}

src/material-experimental/mdc-input/harness/input-harness.spec.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,13 @@ function runTests() {
4646
});
4747

4848
it('should load input with specific id', async () => {
49-
const inputs = await loader.getAllHarnesses(inputHarness.with({id: 'myTextarea'}));
49+
const inputs = await loader.getAllHarnesses(inputHarness.with({selector: '#myTextarea'}));
5050
expect(inputs.length).toBe(1);
5151
});
5252

5353
it('should load input with specific name', async () => {
54-
const inputs = await loader.getAllHarnesses(inputHarness.with({name: 'favorite-food'}));
54+
const inputs = await loader.getAllHarnesses(
55+
inputHarness.with({selector: '[name="favorite-food"]'}));
5556
expect(inputs.length).toBe(1);
5657
});
5758

@@ -157,14 +158,14 @@ function runTests() {
157158
});
158159

159160
it('should be able to focus input', async () => {
160-
const input = await loader.getHarness(inputHarness.with({name: 'favorite-food'}));
161+
const input = await loader.getHarness(inputHarness.with({selector: '[name="favorite-food"]'}));
161162
expect(getActiveElementTagName()).not.toBe('input');
162163
await input.focus();
163164
expect(getActiveElementTagName()).toBe('input');
164165
});
165166

166167
it('should be able to blur input', async () => {
167-
const input = await loader.getHarness(inputHarness.with({name: 'favorite-food'}));
168+
const input = await loader.getHarness(inputHarness.with({selector: '[name="favorite-food"]'}));
168169
expect(getActiveElementTagName()).not.toBe('input');
169170
await input.focus();
170171
expect(getActiveElementTagName()).toBe('input');

src/material-experimental/mdc-input/harness/input-harness.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,7 @@ export class MatInputHarness extends ComponentHarness {
2626
* @return a `HarnessPredicate` configured with the given options.
2727
*/
2828
static with(options: InputHarnessFilters = {}): HarnessPredicate<MatInputHarness> {
29-
// TODO(devversion): "name" and "id" can be removed once components#16848 is merged.
30-
return new HarnessPredicate(MatInputHarness)
31-
.addOption(
32-
'name', options.name, async (harness, name) => (await harness.getName()) === name)
33-
.addOption('id', options.id, async (harness, id) => (await harness.getId()) === id)
29+
return new HarnessPredicate(MatInputHarness, options)
3430
.addOption(
3531
'value', options.value, async (harness, value) => (await harness.getValue()) === value);
3632
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ export class MatMenuHarness extends ComponentHarness {
2323
/**
2424
* Gets a `HarnessPredicate` that can be used to search for a menu with specific attributes.
2525
* @param options Options for narrowing the search:
26+
* - `selector` finds a menu whose host element matches the given selector.
2627
* - `label` finds a menu with specific label text.
2728
* @return a `HarnessPredicate` configured with the given options.
2829
*/
2930
static with(options: MenuHarnessFilters = {}): HarnessPredicate<MatMenuHarness> {
30-
return new HarnessPredicate(MatMenuHarness)
31+
return new HarnessPredicate(MatMenuHarness, options)
3132
.addOption('text', options.triggerText,
3233
(harness, text) => HarnessPredicate.stringMatches(harness.getTriggerText(), text));
3334
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ export class MatMenuItemHarness extends ComponentHarness {
2121
/**
2222
* Gets a `HarnessPredicate` that can be used to search for a menu with specific attributes.
2323
* @param options Options for narrowing the search:
24-
* - `label` finds a menu with specific label text.
24+
* - `selector` finds a menu item whose host element matches the given selector.
25+
* - `label` finds a menu item with specific label text.
2526
* @return a `HarnessPredicate` configured with the given options.
2627
*/
2728
static with(options: MenuItemHarnessFilters = {}): HarnessPredicate<MatMenuItemHarness> {
28-
return new HarnessPredicate(MatMenuItemHarness); // TODO: add options here
29+
return new HarnessPredicate(MatMenuItemHarness, options); // TODO: add options here
2930
}
3031

3132
/** Gets a boolean promise indicating if the menu is disabled. */

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

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

9-
export type MenuHarnessFilters = {
10-
triggerText?: string | RegExp
11-
};
9+
import {BaseHarnessFilters} from '@angular/cdk-experimental/testing';
1210

13-
export type MenuItemHarnessFilters = {
14-
text?: string | RegExp
15-
};
11+
export interface MenuHarnessFilters extends BaseHarnessFilters {
12+
triggerText?: string | RegExp;
13+
}
14+
15+
export interface MenuItemHarnessFilters extends BaseHarnessFilters {
16+
text?: string | RegExp;
17+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ export class MatMenuHarness extends ComponentHarness {
2323
/**
2424
* Gets a `HarnessPredicate` that can be used to search for a menu with specific attributes.
2525
* @param options Options for narrowing the search:
26+
* - `selector` finds a menu whose host element matches the given selector.
2627
* - `label` finds a menu with specific label text.
2728
* @return a `HarnessPredicate` configured with the given options.
2829
*/
2930
static with(options: MenuHarnessFilters = {}): HarnessPredicate<MatMenuHarness> {
30-
return new HarnessPredicate(MatMenuHarness)
31+
return new HarnessPredicate(MatMenuHarness, options)
3132
.addOption('text', options.triggerText,
3233
(harness, text) => HarnessPredicate.stringMatches(harness.getTriggerText(), text));
3334
}

0 commit comments

Comments
 (0)