Skip to content

Commit

Permalink
test(cdk/testing): port the protractor tests to the webdriver env (#2…
Browse files Browse the repository at this point in the history
  • Loading branch information
mmalerba committed Mar 31, 2021
1 parent b403d0e commit e0d8193
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 64 deletions.
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
*/
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

0 comments on commit e0d8193

Please sign in to comment.