Skip to content
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

test(cdk/testing): port the protractor tests to the webdriver env #22375

Merged
merged 6 commits into from
Mar 31, 2021
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
7 changes: 4 additions & 3 deletions src/cdk/testing/tests/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ ng_module(
["**/*.ts"],
exclude = [
"**/*.spec.ts",
"**/*.spec.d.ts",
"harnesses/**",
],
),
Expand Down Expand Up @@ -44,7 +43,6 @@ ng_test_library(
srcs = glob(
[
"**/*.spec.ts",
"**/*.spec.d.ts",
],
exclude = [
"cross-environment.spec.ts",
Expand All @@ -69,7 +67,6 @@ ng_e2e_test_library(
srcs = glob(
[
"**/*.e2e.spec.ts",
"**/*.spec.d.ts",
],
exclude = ["webdriver.e2e.spec.ts"],
),
Expand All @@ -87,6 +84,10 @@ ts_library(
testonly = 1,
srcs = ["webdriver.e2e.spec.ts"],
deps = [
":cross_environment_specs",
":test_harnesses",
"//src/cdk/testing",
"//src/cdk/testing/webdriver",
"@npm//@types/jasmine",
"@npm//@types/node",
"@npm//@types/selenium-webdriver",
Expand Down
13 changes: 0 additions & 13 deletions src/cdk/testing/tests/kagekiri.spec.d.ts

This file was deleted.

4 changes: 1 addition & 3 deletions src/cdk/testing/tests/protractor.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ import {MainComponentHarness} from './harnesses/main-component-harness';

// Kagekiri is available globally in the browser. We declare it here so we can use it in the
// browser-side script passed to `by.js`.
// TODO(mmalerba): Replace with type-only import once TS 3.8 is available, see:
// https://devblogs.microsoft.com/typescript/announcing-typescript-3-8-beta/#type-only-imports-exports
declare const kagekiri: {
querySelectorAll: (selector: string, root: Element) => NodeListOf<Element>;
querySelectorAll: (selector: string, root: Element) => NodeListOf<Element>
};

const piercingQueryFn = (selector: string, root: ElementFinder) => protractorElement.all(by.js(
Expand Down
108 changes: 99 additions & 9 deletions src/cdk/testing/tests/webdriver.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import {Builder, By, Capabilities, WebDriver} from 'selenium-webdriver';
import {HarnessLoader, parallel} from '@angular/cdk/testing';
import {WebDriverHarnessEnvironment} from '@angular/cdk/testing/webdriver';
import * as webdriver from 'selenium-webdriver';
import {crossEnvironmentSpecs} from './cross-environment.spec';
import {MainComponentHarness} from './harnesses/main-component-harness';

/**
* Metadata file generated by `rules_webtesting` for browser tests.
Expand All @@ -7,7 +11,7 @@ import {Builder, By, Capabilities, WebDriver} from 'selenium-webdriver';
* https://github.com/bazelbuild/rules_webtesting/blob/06023bb3/web/internal/metadata.bzl#L69-L82
*/
interface WebTestMetadata {
capabilities: Capabilities;
capabilities: webdriver.Capabilities;
}

if (process.env['WEB_TEST_METADATA'] === undefined) {
Expand All @@ -20,11 +24,30 @@ const webTestMetadata: WebTestMetadata =
require(runfiles.resolve(process.env['WEB_TEST_METADATA']));
const port = process.env['E2E_APP_PORT'];

describe('Webdriver test', () => {
let wd: WebDriver;
// Kagekiri is available globally in the browser. We declare it here so we can use it in the
// browser-side script passed to `By.js`.
declare const kagekiri: {
querySelectorAll: (selector: string, root: Element) => NodeListOf<Element>
};

describe('WebDriverHarnessEnvironment', () => {
let wd: webdriver.WebDriver;

function getUrl(path: string) {
return wd.get(`http://localhost:${port}${path}`);
}

async function piercingQueryFn(selector: string, root: () => webdriver.WebElement) {
return wd.findElements(webdriver.By.js(
(s: string, r: Element) => kagekiri.querySelectorAll(s, r), selector, root()));
}

async function activeElement() {
return wd.switchTo().activeElement();
}

beforeAll(async () => {
wd = await new Builder()
wd = await new webdriver.Builder()
.usingServer(process.env.WEB_TEST_WEBDRIVER_SERVER!)
.withCapabilities(webTestMetadata.capabilities)
.build();
Expand All @@ -34,9 +57,76 @@ describe('Webdriver test', () => {
await wd.quit();
});

it('works', async () => {
await wd.get(`http://localhost:${port}`);
const body = await wd.findElement(By.css('body'));
expect(await body.getText()).toBe('Toggle Navigation Links\ne2e website!\nI am a sibling!');
beforeEach(async () => {
await getUrl('/component-harness');
});

describe('environment specific', () => {
describe('HarnessLoader', () => {
let loader: HarnessLoader;

beforeEach(() => {
loader = WebDriverHarnessEnvironment.loader(wd);
});

it('should create HarnessLoader from WebDriverHarnessEnvironment', () => {
expect(loader).not.toBeNull();
});
});

describe('ComponentHarness', () => {
let harness: MainComponentHarness;

beforeEach(async () => {
harness = await WebDriverHarnessEnvironment.loader(wd).getHarness(MainComponentHarness);
});

it('can get elements outside of host', async () => {
const globalEl = await harness.globalEl();
expect(await globalEl.text()).toBe('I am a sibling!');
});

it('should get correct text excluding certain selectors', async () => {
const results = await harness.subcomponentAndSpecialHarnesses();
const subHarnessHost = await results[0].host();

expect(await subHarnessHost.text({exclude: 'h2'})).toBe('ProtractorTestBedOther');
expect(await subHarnessHost.text({exclude: 'li'})).toBe('List of test tools');
});

it('should be able to retrieve the WebElement from a WebDriverElement', async () => {
const element = WebDriverHarnessEnvironment.getNativeElement(await harness.host());
expect(await element.getTagName()).toBe('test-main');
});
});

describe('shadow DOM interaction', () => {
it('should not pierce shadow boundary by default', async () => {
const harness = await WebDriverHarnessEnvironment.loader(wd)
.getHarness(MainComponentHarness);
expect(await harness.shadows()).toEqual([]);
});

it('should pierce shadow boundary when using piercing query', async () => {
const harness = await WebDriverHarnessEnvironment.loader(wd, {queryFn: piercingQueryFn})
.getHarness(MainComponentHarness);
const shadows = await harness.shadows();
expect(await parallel(() => {
return shadows.map(el => el.text());
})).toEqual(['Shadow 1', 'Shadow 2']);
});

it('should allow querying across shadow boundary', async () => {
const harness = await WebDriverHarnessEnvironment.loader(wd, {queryFn: piercingQueryFn})
.getHarness(MainComponentHarness);
expect(await (await harness.deepShadow()).text()).toBe('Shadow 2');
});
});
});

describe('environment independent', () => crossEnvironmentSpecs(
() => WebDriverHarnessEnvironment.loader(wd),
() => WebDriverHarnessEnvironment.loader(wd).getHarness(MainComponentHarness),
async () => (await activeElement()).getAttribute('id'),
));
});
24 changes: 24 additions & 0 deletions src/cdk/testing/webdriver/framework-stabilizers.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

/**
* An Angular framework stabilizer function that takes a callback and calls it when the application
* is stable, passing a boolean indicating if any work was done.
*/
declare interface FrameworkStabilizer {
(callback: (didWork: boolean) => void): void;
}

/**
* These hooks are exposed by Angular to register a callback for when the application is stable (no
* more pending tasks).
*
* For the implementation, see:
* https://github.com/angular/angular/blob/master/packages/platform-browser/src/browser/testability.ts#L30-L49
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Use a perma link in case the location changes.

*/
declare const frameworkStabilizers: FrameworkStabilizer[];
71 changes: 48 additions & 23 deletions src/cdk/testing/webdriver/webdriver-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,40 +20,50 @@ import {getWebDriverModifierKeys, webDriverKeyMap} from './webdriver-keys';

/** A `TestElement` implementation for WebDriver. */
export class WebDriverElement implements TestElement {
constructor(private readonly _webElement: () => webdriver.WebElement) {}
constructor(
readonly element: () => webdriver.WebElement,
private _stabilize: () => Promise<void>) {}

async blur(): Promise<void> {
return this._executeScript(((element: HTMLElement) => element.blur()), this._webElement());
await this._executeScript(((element: HTMLElement) => element.blur()), this.element());
await this._stabilize();
}

async clear(): Promise<void> {
return this._webElement().clear();
await this.element().clear();
await this._stabilize();
}

async click(...args: [ModifierKeys?] | ['center', ModifierKeys?] |
[number, number, ModifierKeys?]): Promise<void> {
await this._dispatchClickEventSequence(args, webdriver.Button.LEFT);
await this._stabilize();
}

async rightClick(...args: [ModifierKeys?] | ['center', ModifierKeys?] |
[number, number, ModifierKeys?]): Promise<void> {
await this._dispatchClickEventSequence(args, webdriver.Button.RIGHT);
await this._stabilize();
}

async focus(): Promise<void> {
return this._executeScript((element: HTMLElement) => element.blur(), this._webElement());
await this._executeScript((element: HTMLElement) => element.focus(), this.element());
await this._stabilize();
}

async getCssValue(property: string): Promise<string> {
return this._webElement().getCssValue(property);
await this._stabilize();
return this.element().getCssValue(property);
}

async hover(): Promise<void> {
return this._actions().mouseMove(this._webElement()).perform();
await this._actions().mouseMove(this.element()).perform();
await this._stabilize();
}

async mouseAway(): Promise<void> {
return this._actions().mouseMove(this._webElement(), {x: -1, y: -1}).perform();
await this._actions().mouseMove(this.element(), {x: -1, y: -1}).perform();
await this._stabilize();
}

async sendKeys(...keys: (string | TestKey)[]): Promise<void>;
Expand All @@ -77,47 +87,55 @@ export class WebDriverElement implements TestElement {
// so avoid it if no modifier keys are required.
.map(k => modifierKeys.length > 0 ? webdriver.Key.chord(...modifierKeys, k) : k);

return this._webElement().sendKeys(...keys);
await this.element().sendKeys(...keys);
await this._stabilize();
}

async text(options?: TextOptions): Promise<string> {
await this._stabilize();
if (options?.exclude) {
return this._executeScript(_getTextWithExcludedElements, this._webElement(), options.exclude);
return this._executeScript(_getTextWithExcludedElements, this.element(), options.exclude);
}
return this._webElement().getText();
return this.element().getText();
}

async getAttribute(name: string): Promise<string|null> {
await this._stabilize();
return this._executeScript(
(element: Element, attribute: string) => element.getAttribute(attribute),
this._webElement(), name);
this.element(), name);
}

async hasClass(name: string): Promise<boolean> {
await this._stabilize();
const classes = (await this.getAttribute('class')) || '';
return new Set(classes.split(/\s+/).filter(c => c)).has(name);
}

async getDimensions(): Promise<ElementDimensions> {
const {width, height} = await this._webElement().getSize();
const {x: left, y: top} = await this._webElement().getLocation();
await this._stabilize();
const {width, height} = await this.element().getSize();
const {x: left, y: top} = await this.element().getLocation();
return {width, height, left, top};
}

async getProperty(name: string): Promise<any> {
await this._stabilize();
return this._executeScript(
(element: Element, property: keyof Element) => element[property],
this._webElement(), name);
this.element(), name);
}

async setInputValue(newValue: string): Promise<void> {
return this._executeScript(
await this._executeScript(
(element: HTMLInputElement, value: string) => element.value = value,
this._webElement(), newValue);
this.element(), newValue);
await this._stabilize();
}

async selectOptions(...optionIndexes: number[]): Promise<void> {
const options = await this._webElement().findElements(webdriver.By.css('option'));
await this._stabilize();
const options = await this.element().findElements(webdriver.By.css('option'));
const indexes = new Set(optionIndexes); // Convert to a set to remove duplicates.

if (options.length && indexes.size) {
Expand All @@ -134,31 +152,38 @@ export class WebDriverElement implements TestElement {
await this._actions().keyUp(webdriver.Key.CONTROL).perform();
}
}

await this._stabilize();
}
}

async matchesSelector(selector: string): Promise<boolean> {
await this._stabilize();
return this._executeScript((element: Element, s: string) =>
(Element.prototype.matches || (Element.prototype as any).msMatchesSelector)
.call(element, s),
this._webElement(), selector);
this.element(), selector);
}

async isFocused(): Promise<boolean> {
await this._stabilize();
return webdriver.WebElement.equals(
this._webElement(), this._webElement().getDriver().switchTo().activeElement());
this.element(), this.element().getDriver().switchTo().activeElement());
}

async dispatchEvent(name: string, data?: Record<string, EventData>): Promise<void> {
return this._executeScript(dispatchEvent, name, this._webElement(), data);
await this._executeScript(dispatchEvent, name, this.element(), data);
await this._stabilize();
}

/** Gets the webdriver action sequence. */
private _actions() {
return this._webElement().getDriver().actions();
return this.element().getDriver().actions();
}

/** Executes a function in the browser. */
private async _executeScript<T>(script: Function, ...var_args: any[]): Promise<T> {
return this._webElement().getDriver().executeScript(script, ...var_args);
return this.element().getDriver().executeScript(script, ...var_args);
}

/** Dispatches all the events that are part of a click event sequence. */
Expand All @@ -177,7 +202,7 @@ export class WebDriverElement implements TestElement {
const offsetArgs = (args.length === 2 ?
[{x: args[0], y: args[1]}] : []) as [{x: number, y: number}];

let actions = this._actions().mouseMove(this._webElement(), ...offsetArgs);
let actions = this._actions().mouseMove(this.element(), ...offsetArgs);

for (const modifierKey of modifierKeys) {
actions = actions.keyDown(modifierKey);
Expand Down
Loading