Skip to content

feat(cdk-experimental/testing): add support for matching selector on TestElement #16848

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/cdk-experimental/testing/component-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ export interface ComponentHarnessConstructor<T extends ComponentHarness> {
hostSelector: string;
}

export interface BaseHarnessFilters {
selector?: string;
}

/**
* A class used to associate a ComponentHarness class with predicates functions that can be used to
* filter instances of the class.
Expand All @@ -267,7 +271,14 @@ export class HarnessPredicate<T extends ComponentHarness> {
private _predicates: AsyncPredicate<T>[] = [];
private _descriptions: string[] = [];

constructor(public harnessType: ComponentHarnessConstructor<T>) {}
constructor(public harnessType: ComponentHarnessConstructor<T>, options: BaseHarnessFilters) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why we make the options required here? I could imagine having {} as the default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want people to forget to pass it to the super constructor, so I figured if I leave off the default they can't forget 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough 😄

const selector = options.selector;
if (selector !== undefined) {
this.add(`selector matches "${selector}"`, async item => {
return (await item.host()).matchesSelector(selector);
});
}
}

/**
* Checks if a string matches the given pattern.
Expand Down
5 changes: 3 additions & 2 deletions src/cdk-experimental/testing/harness-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function _getErrorForMissingSelector(selector: string): Error {
function _getErrorForMissingHarness<T extends ComponentHarness>(
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): Error {
const harnessPredicate =
harnessType instanceof HarnessPredicate ? harnessType : new HarnessPredicate(harnessType);
harnessType instanceof HarnessPredicate ? harnessType : new HarnessPredicate(harnessType, {});
const {name, hostSelector} = harnessPredicate.harnessType;
const restrictions = harnessPredicate.getDescription();
let message = `Expected to find element for ${name} matching selector: "${hostSelector}"`;
Expand Down Expand Up @@ -160,7 +160,8 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac
private async _getAllHarnesses<T extends ComponentHarness>(
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): Promise<T[]> {
const harnessPredicate =
harnessType instanceof HarnessPredicate ? harnessType : new HarnessPredicate(harnessType);
harnessType instanceof HarnessPredicate ?
harnessType : new HarnessPredicate(harnessType, {});
const elements = await this.getAllRawElements(harnessPredicate.harnessType.hostSelector);
return harnessPredicate.filter(elements.map(
element => this.createComponentHarness(harnessPredicate.harnessType, element)));
Expand Down
11 changes: 11 additions & 0 deletions src/cdk-experimental/testing/protractor/protractor-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,16 @@ export class ProtractorElement implements TestElement {
return browser.executeScript(`return arguments[0][arguments[1]]`, this.element, name);
}

async matchesSelector(selector: string): Promise<boolean> {
return browser.executeScript(`
return (Element.prototype.matches ||
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector).call(arguments[0], arguments[1])
`, this.element, selector);
}

async forceStabilize(): Promise<void> {}
}
3 changes: 3 additions & 0 deletions src/cdk-experimental/testing/test-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ export interface TestElement {
/** Gets the value of a property of an element. */
getProperty(name: string): Promise<any>;

/** Checks whether this element matches the given selector. */
matchesSelector(selector: string): Promise<boolean>;

/**
* Flushes change detection and async tasks.
* In most cases it should not be necessary to call this. However, there may be some edge cases
Expand Down
11 changes: 11 additions & 0 deletions src/cdk-experimental/testing/testbed/unit-test-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,17 @@ export class UnitTestElement implements TestElement {
return (this.element as any)[name];
}

async matchesSelector(selector: string): Promise<boolean> {
await this._stabilize();
const elementPrototype = Element.prototype as any;
return (elementPrototype['matches'] ||
elementPrototype['matchesSelector'] ||
elementPrototype['mozMatchesSelector'] ||
elementPrototype['msMatchesSelector'] ||
elementPrototype['oMatchesSelector'] ||
elementPrototype['webkitMatchesSelector']).call(this.element, selector);
}

async forceStabilize(): Promise<void> {
return this._stabilize();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export class MainComponentHarness extends ComponentHarness {
readonly testLists = this.locatorForAll(SubComponentHarness.with({title: /test/}));
readonly requiredFourIteamToolsLists =
this.locatorFor(SubComponentHarness.with({title: 'List of test tools', itemCount: 4}));
readonly lastList = this.locatorFor(SubComponentHarness.with({selector: ':last-child'}));
readonly specaialKey = this.locatorFor('.special-key');

private _testTools = this.locatorFor(SubComponentHarness);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@
* found in the LICENSE file at https://angular.io/license
*/

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

export interface SubComponentHarnessFilters extends BaseHarnessFilters {
title?: string | RegExp;
itemCount?: number;
}

/** @dynamic */
export class SubComponentHarness extends ComponentHarness {
static readonly hostSelector = 'test-sub';

static with(options: {title?: string | RegExp, itemCount?: number} = {}) {
return new HarnessPredicate(SubComponentHarness)
static with(options: SubComponentHarnessFilters = {}) {
return new HarnessPredicate(SubComponentHarness, options)
.addOption('title', options.title,
async (harness, title) =>
HarnessPredicate.stringMatches((await harness.title()).text(), title))
Expand Down
11 changes: 11 additions & 0 deletions src/cdk-experimental/testing/tests/protractor.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,12 @@ describe('ProtractorHarnessEnvironment', () => {
await input.sendKeys('Hello');
expect(await input.getProperty('value')).toBe('Hello');
});

it('should check if selector matches', async () => {
const button = await harness.button();
expect(await button.matchesSelector('button:not(.fake-class)')).toBe(true);
expect(await button.matchesSelector('button:disabled')).toBe(false);
});
});

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

it('should find subcomponents that match selector', async () => {
const lastList = await harness.lastList();
expect(await (await lastList.title()).text()).toBe('List of test methods');
});

it('should error if predicate does not match but a harness is required', async () => {
try {
await harness.requiredFourIteamToolsLists();
Expand Down
13 changes: 12 additions & 1 deletion src/cdk-experimental/testing/tests/testbed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ describe('TestbedHarnessEnvironment', () => {
});

it('should focus and blur element', async () => {
let button = await harness.button();
const button = await harness.button();
expect(activeElementText()).not.toBe(await button.text());
await button.focus();
expect(activeElementText()).toBe(await button.text());
Expand All @@ -277,6 +277,12 @@ describe('TestbedHarnessEnvironment', () => {
await input.sendKeys('Hello');
expect(await input.getProperty('value')).toBe('Hello');
});

it('should check if selector matches', async () => {
const button = await harness.button();
expect(await button.matchesSelector('button:not(.fake-class)')).toBe(true);
expect(await button.matchesSelector('button:disabled')).toBe(false);
});
});

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

it('should find subcomponents that match selector', async () => {
const lastList = await harness.lastList();
expect(await (await lastList.title()).text()).toBe('List of test methods');
});

it('should error if predicate does not match but a harness is required', async () => {
try {
await harness.requiredFourIteamToolsLists();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/

export type ButtonHarnessFilters = {
text?: string | RegExp
};
import {BaseHarnessFilters} from '@angular/cdk-experimental/testing';

export interface ButtonHarnessFilters extends BaseHarnessFilters {
text?: string | RegExp;
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ export class MatButtonHarness extends ComponentHarness {
/**
* Gets a `HarnessPredicate` that can be used to search for a button with specific attributes.
* @param options Options for narrowing the search:
* - `selector` finds a button whose host element matches the given selector.
* - `text` finds a button with specific text content.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: ButtonHarnessFilters = {}): HarnessPredicate<MatButtonHarness> {
return new HarnessPredicate(MatButtonHarness)
return new HarnessPredicate(MatButtonHarness, options)
.addOption('text', options.text,
(harness, text) => HarnessPredicate.stringMatches(harness.getText(), text));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ export class MatButtonHarness extends ComponentHarness {
/**
* Gets a `HarnessPredicate` that can be used to search for a button with specific attributes.
* @param options Options for narrowing the search:
* - `selector` finds a button whose host element matches the given selector.
* - `text` finds a button with specific text content.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: ButtonHarnessFilters = {}): HarnessPredicate<MatButtonHarness> {
return new HarnessPredicate(MatButtonHarness)
return new HarnessPredicate(MatButtonHarness, options)
.addOption('text', options.text,
(harness, text) => HarnessPredicate.stringMatches(harness.getText(), text));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/

export type CheckboxHarnessFilters = {
label?: string|RegExp;
import {BaseHarnessFilters} from '@angular/cdk-experimental/testing';

export interface CheckboxHarnessFilters extends BaseHarnessFilters {
label?: string | RegExp;
name?: string;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ export class MatCheckboxHarness extends ComponentHarness {
/**
* Gets a `HarnessPredicate` that can be used to search for a checkbox with specific attributes.
* @param options Options for narrowing the search:
* - `selector` finds a checkbox whose host element matches the given selector.
* - `label` finds a checkbox with specific label text.
* - `name` finds a checkbox with specific name.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: CheckboxHarnessFilters = {}): HarnessPredicate<MatCheckboxHarness> {
return new HarnessPredicate(MatCheckboxHarness)
return new HarnessPredicate(MatCheckboxHarness, options)
.addOption(
'label', options.label,
(harness, label) => HarnessPredicate.stringMatches(harness.getLabelText(), label))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ export class MatCheckboxHarness extends ComponentHarness {
/**
* Gets a `HarnessPredicate` that can be used to search for a checkbox with specific attributes.
* @param options Options for narrowing the search:
* - `selector` finds a checkbox whose host element matches the given selector.
* - `label` finds a checkbox with specific label text.
* - `name` finds a checkbox with specific name.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: CheckboxHarnessFilters = {}): HarnessPredicate<MatCheckboxHarness> {
return new HarnessPredicate(MatCheckboxHarness)
return new HarnessPredicate(MatCheckboxHarness, options)
.addOption(
'label', options.label,
(harness, label) => HarnessPredicate.stringMatches(harness.getLabelText(), label))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/

export type InputHarnessFilters = {
id?: string;
name?: string;
import {BaseHarnessFilters} from '@angular/cdk-experimental/testing';

export interface InputHarnessFilters extends BaseHarnessFilters {
value?: string;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@ function runTests() {
});

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

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

Expand Down Expand Up @@ -157,14 +158,14 @@ function runTests() {
});

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

it('should be able to blur input', async () => {
const input = await loader.getHarness(inputHarness.with({name: 'favorite-food'}));
const input = await loader.getHarness(inputHarness.with({selector: '[name="favorite-food"]'}));
expect(getActiveElementTagName()).not.toBe('input');
await input.focus();
expect(getActiveElementTagName()).toBe('input');
Expand Down
6 changes: 1 addition & 5 deletions src/material-experimental/mdc-input/harness/input-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,7 @@ export class MatInputHarness extends ComponentHarness {
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: InputHarnessFilters = {}): HarnessPredicate<MatInputHarness> {
// TODO(devversion): "name" and "id" can be removed once components#16848 is merged.
return new HarnessPredicate(MatInputHarness)
.addOption(
'name', options.name, async (harness, name) => (await harness.getName()) === name)
.addOption('id', options.id, async (harness, id) => (await harness.getId()) === id)
return new HarnessPredicate(MatInputHarness, options)
.addOption(
'value', options.value, async (harness, value) => (await harness.getValue()) === value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ export class MatMenuHarness extends ComponentHarness {
/**
* Gets a `HarnessPredicate` that can be used to search for a menu with specific attributes.
* @param options Options for narrowing the search:
* - `selector` finds a menu whose host element matches the given selector.
* - `label` finds a menu with specific label text.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: MenuHarnessFilters = {}): HarnessPredicate<MatMenuHarness> {
return new HarnessPredicate(MatMenuHarness)
return new HarnessPredicate(MatMenuHarness, options)
.addOption('text', options.triggerText,
(harness, text) => HarnessPredicate.stringMatches(harness.getTriggerText(), text));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ export class MatMenuItemHarness extends ComponentHarness {
/**
* Gets a `HarnessPredicate` that can be used to search for a menu with specific attributes.
* @param options Options for narrowing the search:
* - `label` finds a menu with specific label text.
* - `selector` finds a menu item whose host element matches the given selector.
* - `label` finds a menu item with specific label text.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: MenuItemHarnessFilters = {}): HarnessPredicate<MatMenuItemHarness> {
return new HarnessPredicate(MatMenuItemHarness); // TODO: add options here
return new HarnessPredicate(MatMenuItemHarness, options); // TODO: add options here
}

/** Gets a boolean promise indicating if the menu is disabled. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/

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

export type MenuItemHarnessFilters = {
text?: string | RegExp
};
export interface MenuHarnessFilters extends BaseHarnessFilters {
triggerText?: string | RegExp;
}

export interface MenuItemHarnessFilters extends BaseHarnessFilters {
text?: string | RegExp;
}
3 changes: 2 additions & 1 deletion src/material-experimental/mdc-menu/harness/menu-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ export class MatMenuHarness extends ComponentHarness {
/**
* Gets a `HarnessPredicate` that can be used to search for a menu with specific attributes.
* @param options Options for narrowing the search:
* - `selector` finds a menu whose host element matches the given selector.
* - `label` finds a menu with specific label text.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: MenuHarnessFilters = {}): HarnessPredicate<MatMenuHarness> {
return new HarnessPredicate(MatMenuHarness)
return new HarnessPredicate(MatMenuHarness, options)
.addOption('text', options.triggerText,
(harness, text) => HarnessPredicate.stringMatches(harness.getTriggerText(), text));
}
Expand Down
Loading