feat(cdk-experimental/testing): Bring in component harness (#16089)

Co-Authored-By: Yi Qi <qiyi@google.com>
Co-Authored-By: Rado Kirov <radokirov@google.com>
Co-Authored-By: Thomas Shafer <tshafer@google.com>
Co-Authored-By: Caitlin Mott <caitlinmott@google.com>
Co-Authored-By: Craig Nishina <cnishina@google.com>
This commit is contained in:
mmalerba
2019-06-05 14:39:09 -07:00
committed by Joey Perrott
parent 57b9902778
commit 7baac276ce
28 changed files with 1337 additions and 4 deletions
+3 -1
View File
@@ -98,8 +98,9 @@
# CDK experimental package
/src/cdk-experimental/** @jelbourn
/src/cdk-experimental/dialog/** @jelbourn @josephperrott @crisbeto
/src/cdk-experimental/scrolling/** @mmalerba
/src/cdk-experimental/popover-edit/** @kseamon @andrewseguin
/src/cdk-experimental/scrolling/** @mmalerba
/src/cdk-experimental/testing/** @mmalerba
# Docs examples & guides
/guides/** @jelbourn
@@ -177,6 +178,7 @@
/src/e2e-app/button-toggle/** @jelbourn
/src/e2e-app/card/** @jelbourn
/src/e2e-app/checkbox/** @jelbourn @devversion
/src/e2e-app/component-harness/** @mmalerba
/src/e2e-app/dialog/** @jelbourn @crisbeto
/src/e2e-app/e2e-app/** @jelbourn
/src/e2e-app/example-viewer/** @andrewseguin
+42
View File
@@ -0,0 +1,42 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ng_module", "ng_web_test_suite")
load("@npm_angular_bazel//:index.bzl", "protractor_web_test_suite")
ng_module(
name = "testing",
srcs = glob(
["**/*.ts"],
exclude = [
"**/*.spec.ts",
"tests/**",
],
),
module_name = "@angular/cdk-experimental/testing",
deps = [
"//src/cdk/testing",
"@npm//@angular/core",
"@npm//protractor",
],
)
ng_web_test_suite(
name = "unit_tests",
deps = ["//src/cdk-experimental/testing/tests:unit_test_sources"],
)
protractor_web_test_suite(
name = "e2e_tests",
configuration = "//src/e2e-app:protractor.conf.js",
data = [
"//tools/axe-protractor",
"@npm//@angular/bazel",
],
on_prepare = "//src/e2e-app:start-devserver.js",
server = "//src/e2e-app:devserver",
tags = ["e2e"],
deps = [
"//src/cdk-experimental/testing/tests:e2e_test_sources",
"@npm//protractor",
],
)
@@ -0,0 +1,190 @@
/**
* @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
*/
import {TestElement} from './test-element';
/** Options that can be specified when querying for an Element. */
export interface QueryOptions {
/**
* Whether the found element can be null. If allowNull is set, the searching function will always
* try to fetch the element at once. When the element cannot be found, the searching function
* should return null if allowNull is set to true, throw an error if allowNull is set to false.
* If allowNull is not set, the framework will choose the behaviors that make more sense for each
* test type (e.g. for unit test, the framework will make sure the element is not null; otherwise
* throw an error); however, the internal behavior is not guaranteed and user should not rely on
* it. Note that in most cases, you don't need to care about whether an element is present when
* loading the element and don't need to set this parameter unless you do want to check whether
* the element is present when calling the searching function. e.g. you want to make sure some
* element is not there when loading the element in order to check whether a "ngif" works well.
*/
allowNull?: boolean;
/**
* If global is set to true, the selector will match any element on the page and is not limited to
* the root of the harness. If global is unset or set to false, the selector will only find
* elements under the current root.
*/
global?: boolean;
}
/** Interface that is used to find elements in the DOM and create harnesses for them. */
export interface HarnessLocator {
/**
* Get the host element of locator.
*/
host(): TestElement;
/**
* Search the first matched test element.
* @param selector The CSS selector of the test elements.
* @param options Optional, extra searching options
*/
querySelector(selector: string, options?: QueryOptions): Promise<TestElement|null>;
/**
* Search all matched test elements under current root by CSS selector.
* @param selector The CSS selector of the test elements.
*/
querySelectorAll(selector: string): Promise<TestElement[]>;
/**
* Load the first matched Component Harness.
* @param componentHarness Type of user customized harness.
* @param root CSS root selector of the new component harness.
* @param options Optional, extra searching options
*/
load<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>, root: string,
options?: QueryOptions): Promise<T|null>;
/**
* Load all Component Harnesses under current root.
* @param componentHarness Type of user customized harness.
* @param root CSS root selector of the new component harnesses.
*/
loadAll<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>, root: string): Promise<T[]>;
}
/**
* Base Component Harness
* This base component harness provides the basic ability to locate element and
* sub-component harness. It should be inherited when defining user's own
* harness.
*/
export abstract class ComponentHarness {
constructor(private readonly locator: HarnessLocator) {}
/**
* Get the host element of component harness.
*/
host(): TestElement {
return this.locator.host();
}
/**
* Generate a function to find the first matched test element by CSS
* selector.
* @param selector The CSS selector of the test element.
*/
protected find(selector: string): () => Promise<TestElement>;
/**
* Generate a function to find the first matched test element by CSS
* selector.
* @param selector The CSS selector of the test element.
* @param options Extra searching options
*/
protected find(selector: string, options: QueryOptions & {allowNull: true}):
() => Promise<TestElement|null>;
/**
* Generate a function to find the first matched test element by CSS
* selector.
* @param selector The CSS selector of the test element.
* @param options Extra searching options
*/
protected find(selector: string, options: QueryOptions): () => Promise<TestElement>;
/**
* Generate a function to find the first matched Component Harness.
* @param componentHarness Type of user customized harness.
* @param root CSS root selector of the new component harness.
*/
protected find<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>,
root: string): () => Promise<T>;
/**
* Generate a function to find the first matched Component Harness.
* @param componentHarness Type of user customized harness.
* @param root CSS root selector of the new component harness.
* @param options Extra searching options
*/
protected find<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>, root: string,
options: QueryOptions & {allowNull: true}): () => Promise<T|null>;
/**
* Generate a function to find the first matched Component Harness.
* @param componentHarness Type of user customized harness.
* @param root CSS root selector of the new component harness.
* @param options Extra searching options
*/
protected find<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>, root: string,
options: QueryOptions): () => Promise<T>;
protected find<T extends ComponentHarness>(
selectorOrComponentHarness: string|ComponentHarnessConstructor<T>,
selectorOrOptions?: string|QueryOptions,
options?: QueryOptions): () => Promise<TestElement|T|null> {
if (typeof selectorOrComponentHarness === 'string') {
const selector = selectorOrComponentHarness;
return () => this.locator.querySelector(selector, selectorOrOptions as QueryOptions);
} else {
const componentHarness = selectorOrComponentHarness;
const selector = selectorOrOptions as string;
return () => this.locator.load(componentHarness, selector, options);
}
}
/**
* Generate a function to find all matched test elements by CSS selector.
* @param selector The CSS root selector of elements. It will locate
* elements under the current root.
*/
protected findAll(selector: string): () => Promise<TestElement[]>;
/**
* Generate a function to find all Component Harnesses under current
* component harness.
* @param componentHarness Type of user customized harness.
* @param root CSS root selector of the new component harnesses. It will
* locate harnesses under the current root.
*/
protected findAll<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>,
root: string): () => Promise<T[]>;
protected findAll<T extends ComponentHarness>(
selectorOrComponentHarness: string|ComponentHarnessConstructor<T>,
root?: string): () => Promise<TestElement[]|T[]> {
if (typeof selectorOrComponentHarness === 'string') {
const selector = selectorOrComponentHarness;
return () => this.locator.querySelectorAll(selector);
} else {
const componentHarness = selectorOrComponentHarness;
return () => this.locator.loadAll(componentHarness, root as string);
}
}
}
/** Constructor for a ComponentHarness subclass. */
export interface ComponentHarnessConstructor<T extends ComponentHarness> {
new(locator: HarnessLocator): T;
}
+9
View File
@@ -0,0 +1,9 @@
/**
* @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
*/
export * from './public-api';
+170
View File
@@ -0,0 +1,170 @@
/**
* @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
*/
import {browser, by, element as protractorElement, ElementFinder} from 'protractor';
import {
ComponentHarness,
ComponentHarnessConstructor,
HarnessLocator,
QueryOptions
} from './component-harness';
import {TestElement} from './test-element';
/**
* Component harness factory for protractor.
* The function will not try to fetch the host element of harness at once, which
* is for performance purpose; however, this is the most common way to load
* protractor harness. If you do care whether the host element is present when
* loading harness, using the load function that accepts extra searching
* options.
* @param componentHarness: Type of user defined harness.
* @param rootSelector: Optional. CSS selector to specify the root of component.
* Set to 'body' by default
*/
export async function load<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>,
rootSelector: string): Promise<T>;
/**
* Component harness factory for protractor.
* @param componentHarness: Type of user defined harness.
* @param rootSelector: Optional. CSS selector to specify the root of component.
* Set to 'body' by default.
* @param options Optional. Extra searching options
*/
export async function load<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>, rootSelector?: string,
options?: QueryOptions): Promise<T|null>;
export async function load<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>, rootSelector = 'body',
options?: QueryOptions): Promise<T|null> {
const root = await getElement(rootSelector, undefined, options);
return root && new componentHarness(new ProtractorLocator(root));
}
/**
* Gets the corresponding ElementFinder for the root of a TestElement.
*/
export function getElementFinder(testElement: TestElement): ElementFinder {
if (testElement instanceof ProtractorElement) {
return testElement.element;
}
throw Error(`Expected an instance of ProtractorElement, got ${testElement}`);
}
class ProtractorLocator implements HarnessLocator {
private readonly _root: ProtractorElement;
constructor(private _rootFinder: ElementFinder) {
this._root = new ProtractorElement(this._rootFinder);
}
host(): TestElement {
return this._root;
}
async querySelector(selector: string, options?: QueryOptions): Promise<TestElement|null> {
const finder = await getElement(selector, this._rootFinder, options);
return finder && new ProtractorElement(finder);
}
async querySelectorAll(selector: string): Promise<TestElement[]> {
const elementFinders = this._rootFinder.all(by.css(selector));
return elementFinders.reduce(
(result: TestElement[], el: ElementFinder) =>
el ? result.concat([new ProtractorElement(el)]) : result,
[]);
}
async load<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>, selector: string,
options?: QueryOptions): Promise<T|null> {
const root = await getElement(selector, this._rootFinder, options);
return root && new componentHarness(new ProtractorLocator(root));
}
async loadAll<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>,
rootSelector: string): Promise<T[]> {
const roots = this._rootFinder.all(by.css(rootSelector));
return roots.reduce(
(result: T[], el: ElementFinder) =>
el ? result.concat(new componentHarness(new ProtractorLocator(el))) : result,
[]);
}
}
class ProtractorElement implements TestElement {
constructor(readonly element: ElementFinder) {}
async blur(): Promise<void> {
return this.element['blur']();
}
async clear(): Promise<void> {
return this.element.clear();
}
async click(): Promise<void> {
return this.element.click();
}
async focus(): Promise<void> {
return this.element['focus']();
}
async getCssValue(property: string): Promise<string> {
return this.element.getCssValue(property);
}
async hover(): Promise<void> {
return browser.actions()
.mouseMove(await this.element.getWebElement())
.perform();
}
async sendKeys(keys: string): Promise<void> {
return this.element.sendKeys(keys);
}
async text(): Promise<string> {
return this.element.getText();
}
async getAttribute(name: string): Promise<string|null> {
return this.element.getAttribute(name);
}
}
/**
* Get an element finder based on the CSS selector and root element.
* Note that it will check whether the element is present only when
* Options.allowNull is set. This is for performance purpose.
* @param selector The CSS selector
* @param root Optional Search element under the root element. If not set,
* search element globally. If options.global is set, root is ignored.
* @param options Optional, extra searching options
*/
async function getElement(selector: string, root?: ElementFinder, options?: QueryOptions):
Promise<ElementFinder|null> {
const useGlobalRoot = options && !!options.global;
const elem = root === undefined || useGlobalRoot ?
protractorElement(by.css(selector)) : root.element(by.css(selector));
const allowNull = options !== undefined && options.allowNull !== undefined ?
options.allowNull : undefined;
if (allowNull !== undefined && !(await elem.isPresent())) {
if (allowNull) {
return null;
}
throw Error('Cannot find element based on the CSS selector: ' + selector);
}
return elem;
}
@@ -0,0 +1,14 @@
/**
* @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
*/
import * as protractor from './protractor';
import * as testbed from './testbed';
export * from './component-harness';
export * from './test-element';
export {protractor, testbed};
@@ -0,0 +1,46 @@
/**
* @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
*/
/**
* This acts as a common interface for DOM elements across both unit and e2e tests. It is the
* interface through which the ComponentHarness interacts with the component's DOM.
*/
export interface TestElement {
/** Blur the element. */
blur(): Promise<void>;
/** Clear the element's input (for input elements only). */
clear(): Promise<void>;
/** Click the element. */
click(): Promise<void>;
/** Focus the element. */
focus(): Promise<void>;
/** Get the computed value of the given CSS property for the element. */
getCssValue(property: string): Promise<string>;
/** Hovers the mouse over the element. */
hover(): Promise<void>;
/**
* Sends the given string to the input as a series of key presses. Also fires input events
* and attempts to add the string to the Element's value.
*/
sendKeys(keys: string): Promise<void>;
/** Gets the text from the element. */
text(): Promise<string>;
/**
* Gets the value for the given attribute from the element. If the attribute does not exist,
* falls back to reading the property.
*/
getAttribute(name: string): Promise<string | null>;
}
+207
View File
@@ -0,0 +1,207 @@
/**
* @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
*/
import {
dispatchFakeEvent,
dispatchKeyboardEvent,
dispatchMouseEvent,
triggerBlur,
triggerFocus
} from '@angular/cdk/testing';
import {ComponentFixture} from '@angular/core/testing';
import {
ComponentHarness,
ComponentHarnessConstructor,
HarnessLocator,
QueryOptions
} from './component-harness';
import {TestElement} from './test-element';
/**
* Component harness factory for testbed.
* @param componentHarness: Type of user defined harness.
* @param fixture: Component Fixture of the component to be tested.
*/
export function load<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>,
fixture: ComponentFixture<{}>): T {
const stabilize = async () => {
fixture.detectChanges();
await fixture.whenStable();
};
return new componentHarness(new UnitTestLocator(fixture.nativeElement, stabilize));
}
/**
* Gets the corresponding Element for the root of a TestElement.
*/
export function getNativeElement(testElement: TestElement): Element {
if (testElement instanceof UnitTestElement) {
return testElement.element;
}
throw Error(`Expected an instance of UnitTestElement, got ${testElement}`);
}
/**
* Locator implementation for testbed.
* Note that, this locator is exposed for internal usage, please do not use it.
*/
export class UnitTestLocator implements HarnessLocator {
private readonly _rootElement: TestElement;
constructor(private _root: Element, private _stabilize: () => Promise<void>) {
this._rootElement = new UnitTestElement(_root, this._stabilize);
}
host(): TestElement {
return this._rootElement;
}
async querySelector(selector: string, options?: QueryOptions): Promise<TestElement|null> {
await this._stabilize();
const e = getElement(selector, this._root, options);
return e && new UnitTestElement(e, this._stabilize);
}
async querySelectorAll(selector: string): Promise<TestElement[]> {
await this._stabilize();
return Array.prototype.map.call(
this._root.querySelectorAll(selector),
(e: Element) => new UnitTestElement(e, this._stabilize));
}
async load<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>, selector: string,
options?: QueryOptions): Promise<T|null> {
await this._stabilize();
const root = getElement(selector, this._root, options);
return root && new componentHarness(new UnitTestLocator(root, this._stabilize));
}
async loadAll<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>,
rootSelector: string): Promise<T[]> {
await this._stabilize();
return Array.prototype.map.call(
this._root.querySelectorAll(rootSelector),
(e: Element) => new componentHarness(new UnitTestLocator(e, this._stabilize)));
}
}
class UnitTestElement implements TestElement {
constructor(readonly element: Element, private _stabilize: () => Promise<void>) {}
async blur(): Promise<void> {
await this._stabilize();
triggerBlur(this.element as HTMLElement);
await this._stabilize();
}
async clear(): Promise<void> {
await this._stabilize();
if (!this._isTextInput(this.element)) {
throw Error('Attempting to clear an invalid element');
}
triggerFocus(this.element as HTMLElement);
this.element.value = '';
dispatchFakeEvent(this.element, 'input');
await this._stabilize();
}
async click(): Promise<void> {
await this._stabilize();
dispatchMouseEvent(this.element, 'click');
await this._stabilize();
}
async focus(): Promise<void> {
await this._stabilize();
triggerFocus(this.element as HTMLElement);
await this._stabilize();
}
async getCssValue(property: string): Promise<string> {
await this._stabilize();
// TODO(mmalerba): Consider adding value normalization if we run into common cases where its
// needed.
return getComputedStyle(this.element).getPropertyValue(property);
}
async hover(): Promise<void> {
await this._stabilize();
dispatchMouseEvent(this.element, 'mouseenter');
await this._stabilize();
}
async sendKeys(keys: string): Promise<void> {
await this._stabilize();
triggerFocus(this.element as HTMLElement);
for (const key of keys) {
const keyCode = key.charCodeAt(0);
dispatchKeyboardEvent(this.element, 'keydown', keyCode);
dispatchKeyboardEvent(this.element, 'keypress', keyCode);
if (this._isTextInput(this.element)) {
this.element.value += key;
}
dispatchKeyboardEvent(this.element, 'keyup', keyCode);
if (this._isTextInput(this.element)) {
dispatchFakeEvent(this.element, 'input');
}
}
await this._stabilize();
}
async text(): Promise<string> {
await this._stabilize();
return this.element.textContent || '';
}
async getAttribute(name: string): Promise<string|null> {
await this._stabilize();
let value = this.element.getAttribute(name);
// If cannot find attribute in the element, also try to find it in property,
// this is useful for input/textarea tags.
if (value === null && name in this.element) {
// We need to cast the element so we can access its properties via string indexing.
return (this.element as unknown as {[key: string]: string|null})[name];
}
return value;
}
private _isTextInput(element: Element): element is HTMLInputElement | HTMLTextAreaElement {
return element.nodeName.toLowerCase() === 'input' ||
element.nodeName.toLowerCase() === 'textarea' ;
}
}
/**
* Get an element based on the CSS selector and root element.
* @param selector The CSS selector
* @param root Search element under the root element. If options.global is set,
* root is ignored.
* @param options Optional, extra searching options
* @return When element is not present, return null if Options.allowNull is set
* to true, throw an error if Options.allowNull is set to false; otherwise,
* return the element
*/
function getElement(selector: string, root: Element, options?: QueryOptions): Element|null {
const useGlobalRoot = options && options.global;
const elem = (useGlobalRoot ? document : root).querySelector(selector);
const allowNull = options !== undefined && options.allowNull !== undefined ?
options.allowNull : undefined;
if (elem === null) {
if (allowNull) {
return null;
}
throw Error('Cannot find element based on the CSS selector: ' + selector);
}
return elem;
}
@@ -0,0 +1,49 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ng_e2e_test_library", "ng_module", "ng_test_library", "ts_library")
ng_module(
name = "test_components",
srcs = glob(
["**/*.ts"],
exclude = [
"**/*.spec.ts",
"harnesses/**",
],
),
assets = glob(["**/*.html"]),
module_name = "@angular/cdk-experimental/testing/tests",
deps = [
"@npm//@angular/forms",
],
)
ts_library(
name = "test_harnesses",
srcs = glob(["harnesses/**/*.ts"]),
deps = [
"//src/cdk-experimental/testing",
],
)
ng_test_library(
name = "unit_test_sources",
srcs = glob(
["**/*.spec.ts"],
exclude = ["**/*.e2e.spec.ts"],
),
deps = [
":test_components",
":test_harnesses",
"//src/cdk-experimental/testing",
],
)
ng_e2e_test_library(
name = "e2e_test_sources",
srcs = glob(["**/*.e2e.spec.ts"]),
deps = [
":test_harnesses",
"//src/cdk-experimental/testing",
],
)
@@ -0,0 +1,54 @@
/**
* @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
*/
import {ComponentHarness} from '../../component-harness';
import {TestElement} from '../../test-element';
import {SubComponentHarness} from './sub-component-harness';
export class MainComponentHarness extends ComponentHarness {
readonly title = this.find('h1');
readonly asyncCounter = this.find('#asyncCounter');
readonly counter = this.find('#counter');
readonly input = this.find('#input');
readonly value = this.find('#value');
readonly allLabels = this.findAll('label');
readonly allLists = this.findAll(SubComponentHarness, 'test-sub');
readonly memo = this.find('textarea');
// Allow null for element
readonly nullItem = this.find('wrong locator', {allowNull: true});
// Allow null for component harness
readonly nullComponentHarness =
this.find(SubComponentHarness, 'wrong locator', {allowNull: true});
readonly errorItem = this.find('wrong locator', {allowNull: false});
readonly globalEl = this.find('.sibling', {global: true});
readonly errorGlobalEl =
this.find('wrong locator', {global: true, allowNull: false});
readonly nullGlobalEl =
this.find('wrong locator', {global: true, allowNull: true});
private _button = this.find('button');
private _testTools = this.find(SubComponentHarness, 'test-sub');
async increaseCounter(times: number) {
const button = await this._button();
for (let i = 0; i < times; i++) {
await button.click();
}
}
async getTestTool(index: number): Promise<TestElement> {
const subComponent = await this._testTools();
return subComponent.getItem(index);
}
async getTestTools(): Promise<TestElement[]> {
const subComponent = await this._testTools();
return subComponent.getItems();
}
}
@@ -0,0 +1,20 @@
/**
* @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
*/
import {ComponentHarness} from '../../component-harness';
import {TestElement} from '../../test-element';
export class SubComponentHarness extends ComponentHarness {
readonly title = this.find('h2');
readonly getItems = this.findAll('li');
async getItem(index: number): Promise<TestElement> {
const items = await this.getItems();
return items[index];
}
}
@@ -0,0 +1,11 @@
/**
* @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
*/
export * from './test-components-module';
export * from './test-main-component';
export * from './test-sub-component';
@@ -0,0 +1,183 @@
import {browser, by, element} from 'protractor';
import {getElementFinder, load} from '../protractor';
import {MainComponentHarness} from './harnesses/main-component-harness';
describe('Protractor Helper Test', () => {
let harness: MainComponentHarness;
beforeEach(async () => {
await browser.get('/component-harness');
harness = await load(MainComponentHarness, 'test-main');
});
describe('Locator', () => {
it('should be able to locate a element based on CSS selector', async () => {
const title = await harness.title();
expect(await title.text()).toBe('Main Component');
});
it('should be able to locate all elements based on CSS selector',
async () => {
const labels = await harness.allLabels();
expect(labels.length).toBe(2);
expect(await labels[0].text()).toBe('Count:');
expect(await labels[1].text()).toBe('AsyncCounter:');
});
it('should be able to locate the sub harnesses', async () => {
const items = await harness.getTestTools();
expect(items.length).toBe(3);
expect(await items[0].text()).toBe('Protractor');
expect(await items[1].text()).toBe('TestBed');
expect(await items[2].text()).toBe('Other');
});
it('should be able to locate all sub harnesses', async () => {
const alllists = await harness.allLists();
const items1 = await alllists[0].getItems();
const items2 = await alllists[1].getItems();
expect(alllists.length).toBe(2);
expect(items1.length).toBe(3);
expect(await items1[0].text()).toBe('Protractor');
expect(await items1[1].text()).toBe('TestBed');
expect(await items1[2].text()).toBe('Other');
expect(items2.length).toBe(3);
expect(await items2[0].text()).toBe('Unit Test');
expect(await items2[1].text()).toBe('Integration Test');
expect(await items2[2].text()).toBe('Performance Test');
});
});
describe('Test element', () => {
it('should be able to clear', async () => {
const input = await harness.input();
await input.sendKeys('Yi');
expect(await input.getAttribute('value')).toBe('Yi');
await input.clear();
expect(await input.getAttribute('value')).toBe('');
});
it('should be able to click', async () => {
const counter = await harness.counter();
expect(await counter.text()).toBe('0');
await harness.increaseCounter(3);
expect(await counter.text()).toBe('3');
});
it('should be able to send key', async () => {
const input = await harness.input();
const value = await harness.value();
await input.sendKeys('Yi');
expect(await input.getAttribute('value')).toBe('Yi');
expect(await value.text()).toBe('Input: Yi');
});
it('focuses the element before sending key', async () => {
const input = await harness.input();
await input.sendKeys('Yi');
expect(await input.getAttribute('id'))
.toBe(await browser.driver.switchTo().activeElement().getAttribute(
'id'));
});
it('should be able to hover', async () => {
const host = await harness.host();
let classAttr = await host.getAttribute('class');
expect(classAttr).not.toContain('hovering');
await host.hover();
classAttr = await host.getAttribute('class');
expect(classAttr).toContain('hovering');
});
it('should be able to getAttribute', async () => {
const memoStr = `
This is an example that shows how to use component harness
You should use getAttribute('value') to retrieve the text in textarea
`;
const memo = await harness.memo();
await memo.sendKeys(memoStr);
expect(await memo.getAttribute('value')).toBe(memoStr);
});
it('should be able to getCssValue', async () => {
const title = await harness.title();
expect(await title.getCssValue('height')).toBe('50px');
});
});
describe('Async operation', () => {
it('should wait for async opeartion to complete', async () => {
const asyncCounter = await harness.asyncCounter();
expect(await asyncCounter.text()).toBe('5');
await harness.increaseCounter(3);
expect(await asyncCounter.text()).toBe('8');
});
});
describe('Allow null', () => {
it('should allow element to be null when setting allowNull', async () => {
expect(await harness.nullItem()).toBe(null);
});
it('should allow main harness to be null when setting allowNull',
async () => {
const nullMainHarness = await load(
MainComponentHarness, 'harness not present', {allowNull: true});
expect(nullMainHarness).toBe(null);
});
it('should allow sub-harness to be null when setting allowNull',
async () => {
expect(await harness.nullComponentHarness()).toBe(null);
});
});
describe('with the global option', () => {
it('should find an element outside the root of the harness', async () => {
const globalEl = await harness.globalEl();
expect(await globalEl.text()).toBe('I am a sibling!');
});
it('should return null for a selector that does not exist', async () => {
expect(await harness.nullGlobalEl()).toBeNull();
});
it('should throw an error for a selctor that does not exist ' +
'with allowNull = false',
async () => {
try {
await harness.errorGlobalEl();
fail('Should throw error');
} catch (err) {
expect(err.message)
.toBe(
'Cannot find element based on the CSS selector: wrong locator');
}
});
});
describe('Throw error', () => {
it('should show the correct error', async () => {
try {
await harness.errorItem();
fail('Should throw error');
} catch (err) {
expect(err.message)
.toBe(
'Cannot find element based on the CSS selector: wrong locator');
}
});
});
describe('getElementFinder', () => {
it('should return the element finder', async () => {
const mainElement = await element(by.css('test-main'));
const elementFromHarness = getElementFinder(harness.host());
expect(await elementFromHarness.getId())
.toBe(await mainElement.getId());
});
});
});
@@ -0,0 +1,20 @@
/**
* @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
*/
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {TestMainComponent} from './test-main-component';
import {TestSubComponent} from './test-sub-component';
@NgModule({
imports: [CommonModule, FormsModule],
declarations: [TestMainComponent, TestSubComponent],
exports: [TestMainComponent, TestSubComponent]
})
export class TestComponentsModule {}
@@ -0,0 +1,12 @@
<h1 style="height: 50px">Main Component</h1>
<div id="username">Hello {{username}} from Angular 2!</div>
<button (click)="click()">Up</button><br>
<label>Count:</label>
<div id="counter">{{counter}}</div>
<label>AsyncCounter:</label>
<div id="asyncCounter">{{asyncCounter}}</div>
<input [(ngModel)]="input" id="input" aria-label="input">
<div id="value">Input: {{input}}</div>
<textarea id="memo" aria-label="memo">{{memo}}</textarea>
<test-sub title="test tools" [items]="testTools"></test-sub>
<test-sub title="test methods" [items]="testMethods"></test-sub>
@@ -0,0 +1,67 @@
/**
* @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
*/
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ViewEncapsulation
} from '@angular/core';
@Component({
moduleId: module.id,
selector: 'test-main',
templateUrl: 'test-main-component.html',
host: {
'[class.hovering]': '_isHovering',
'(mouseenter)': 'onMouseOver()',
'(mouseout)': 'onMouseOut()',
},
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TestMainComponent {
username: string;
counter: number;
asyncCounter: number;
input: string;
memo: string;
testTools: string[];
testMethods: string[];
_isHovering: boolean;
onMouseOver() {
this._isHovering = true;
}
onMouseOut() {
this._isHovering = false;
}
constructor(private _cdr: ChangeDetectorRef) {
this.username = 'Yi';
this.counter = 0;
this.asyncCounter = 0;
this.memo = '';
this.testTools = ['Protractor', 'TestBed', 'Other'];
this.testMethods = ['Unit Test', 'Integration Test', 'Performance Test'];
setTimeout(() => {
this.asyncCounter = 5;
this._cdr.markForCheck();
}, 1000);
}
click() {
this.counter++;
setTimeout(() => {
this.asyncCounter++;
this._cdr.markForCheck();
}, 500);
}
}
@@ -0,0 +1,25 @@
/**
* @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
*/
import {ChangeDetectionStrategy, Component, Input, ViewEncapsulation} from '@angular/core';
@Component({
moduleId: module.id,
selector: 'test-sub',
template: `
<h2>List of {{title}}</h2>
<ul>
<li *ngFor="let item of items">{{item}}</li>
</ul>`,
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TestSubComponent {
@Input() title: string;
@Input() items: string[];
}
@@ -0,0 +1,156 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {getNativeElement, load} from '../testbed';
import {MainComponentHarness} from './harnesses/main-component-harness';
import {TestComponentsModule} from './test-components-module';
import {TestMainComponent} from './test-main-component';
describe('Testbed Helper Test', () => {
let harness: MainComponentHarness;
let fixture: ComponentFixture<{}>;
beforeEach(async(() => {
TestBed
.configureTestingModule({
imports: [TestComponentsModule],
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(TestMainComponent);
harness = load(MainComponentHarness, fixture);
});
}));
describe('Locator', () => {
it('should be able to locate a element based on CSS selector', async () => {
const title = await harness.title();
expect(await title.text()).toBe('Main Component');
});
it('should be able to locate all elements based on CSS selector',
async () => {
const labels = await harness.allLabels();
expect(labels.length).toBe(2);
expect(await labels[0].text()).toBe('Count:');
expect(await labels[1].text()).toBe('AsyncCounter:');
});
it('should be able to locate the sub harnesses', async () => {
const items = await harness.getTestTools();
expect(items.length).toBe(3);
expect(await items[0].text()).toBe('Protractor');
expect(await items[1].text()).toBe('TestBed');
expect(await items[2].text()).toBe('Other');
});
it('should be able to locate all sub harnesses', async () => {
const alllists = await harness.allLists();
const items1 = await alllists[0].getItems();
const items2 = await alllists[1].getItems();
expect(alllists.length).toBe(2);
expect(items1.length).toBe(3);
expect(await items1[0].text()).toBe('Protractor');
expect(await items1[1].text()).toBe('TestBed');
expect(await items1[2].text()).toBe('Other');
expect(items2.length).toBe(3);
expect(await items2[0].text()).toBe('Unit Test');
expect(await items2[1].text()).toBe('Integration Test');
expect(await items2[2].text()).toBe('Performance Test');
});
});
describe('Test element', () => {
it('should be able to clear', async () => {
const input = await harness.input();
await input.sendKeys('Yi');
expect(await input.getAttribute('value')).toBe('Yi');
await input.clear();
expect(await input.getAttribute('value')).toBe('');
});
it('should be able to click', async () => {
const counter = await harness.counter();
expect(await counter.text()).toBe('0');
await harness.increaseCounter(3);
expect(await counter.text()).toBe('3');
});
it('should be able to send key', async () => {
const input = await harness.input();
const value = await harness.value();
await input.sendKeys('Yi');
expect(await input.getAttribute('value')).toBe('Yi');
expect(await value.text()).toBe('Input: Yi');
});
it('focuses the element before sending key', async () => {
const input = await harness.input();
await input.sendKeys('Yi');
expect(await input.getAttribute('id'))
.toBe(document.activeElement!.id);
});
it('should be able to hover', async () => {
const host = await harness.host();
let classAttr = await host.getAttribute('class');
expect(classAttr).not.toContain('hovering');
await host.hover();
classAttr = await host.getAttribute('class');
expect(classAttr).toContain('hovering');
});
it('should be able to getAttribute', async () => {
const memoStr = `
This is an example that shows how to use component harness
You should use getAttribute('value') to retrieve the text in textarea
`;
const memo = await harness.memo();
await memo.sendKeys(memoStr);
expect(await memo.getAttribute('value')).toBe(memoStr);
});
it('should be able to getCssValue', async () => {
const title = await harness.title();
expect(await title.getCssValue('height')).toBe('50px');
});
});
describe('Async operation', () => {
it('should wait for async opeartion to complete', async () => {
const asyncCounter = await harness.asyncCounter();
expect(await asyncCounter.text()).toBe('5');
await harness.increaseCounter(3);
expect(await asyncCounter.text()).toBe('8');
});
});
describe('Allow null', () => {
it('should allow element to be null when setting allowNull', async () => {
expect(await harness.nullItem()).toBe(null);
});
it('should allow harness to be null when setting allowNull', async () => {
expect(await harness.nullComponentHarness()).toBe(null);
});
});
describe('Throw error', () => {
it('should show the correct error', async () => {
try {
await harness.errorItem();
fail('Should throw error');
} catch (err) {
expect(err.message)
.toBe(
'Cannot find element based on the CSS selector: wrong locator');
}
});
});
describe('getNativeElement', () => {
it('should return the native element', async () => {
expect(getNativeElement(harness.host())).toBe(fixture.nativeElement);
});
});
});
+19
View File
@@ -17,3 +17,22 @@ export function patchElementFocus(element: HTMLElement) {
element.focus = () => dispatchFakeEvent(element, 'focus');
element.blur = () => dispatchFakeEvent(element, 'blur');
}
function triggerFocusChange(element: HTMLElement, event: 'focus' | 'blur') {
let eventFired = false;
const handler = () => eventFired = true;
element.addEventListener(event, handler);
element[event]();
element.removeEventListener(event, handler);
if (!eventFired) {
dispatchFakeEvent(element, event);
}
}
export function triggerFocus(element: HTMLElement) {
triggerFocusChange(element, 'focus');
}
export function triggerBlur(element: HTMLElement) {
triggerFocusChange(element, 'blur');
}
+1
View File
@@ -26,6 +26,7 @@ ng_module(
deps = [
"//src/cdk-experimental/dialog",
"//src/cdk-experimental/scrolling",
"//src/cdk-experimental/testing/tests:test_components",
"//src/cdk/drag-drop",
"//src/cdk/overlay",
"//src/cdk/scrolling",
@@ -0,0 +1,19 @@
/**
* @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
*/
import {TestComponentsModule} from '@angular/cdk-experimental/testing/tests';
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {ComponentHarnessE2e} from './component-harness-e2e';
@NgModule({
imports: [CommonModule, FormsModule, TestComponentsModule],
declarations: [ComponentHarnessE2e],
})
export class ComponentHarnessE2eModule {
}
@@ -0,0 +1,10 @@
import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core';
@Component({
moduleId: module.id,
selector: 'component-harness-e2e',
template: `<test-main></test-main>`,
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ComponentHarnessE2e {}
+1
View File
@@ -5,6 +5,7 @@
<a mat-list-item [routerLink]="['button']">Button</a>
<a mat-list-item [routerLink]="['button-toggle']">Button Toggle</a>
<a mat-list-item [routerLink]="['checkbox']">Checkbox</a>
<a mat-list-item [routerLink]="['component-harness']">Component Harness</a>
<a mat-list-item [routerLink]="['dialog']">Dialog</a>
<a mat-list-item [routerLink]="['expansion']">Expansion</a>
<a mat-list-item [routerLink]="['grid-list']">Grid list</a>
+3 -1
View File
@@ -4,6 +4,7 @@ import {ButtonToggleE2e} from '../button-toggle/button-toggle-e2e';
import {ButtonE2E} from '../button/button-e2e';
import {CardE2e} from '../card/card-e2e';
import {SimpleCheckboxes} from '../checkbox/checkbox-e2e';
import {ComponentHarnessE2e} from '../component-harness/component-harness-e2e';
import {DialogE2E} from '../dialog/dialog-e2e';
import {ExpansionE2e} from '../expansion/expansion-e2e';
import {GridListE2E} from '../grid-list/grid-list-e2e';
@@ -34,7 +35,9 @@ export const E2E_APP_ROUTES: Routes = [
{path: 'block-scroll-strategy', component: BlockScrollStrategyE2E},
{path: 'button', component: ButtonE2E},
{path: 'button-toggle', component: ButtonToggleE2e},
{path: 'cards', component: CardE2e},
{path: 'checkbox', component: SimpleCheckboxes},
{path: 'component-harness', component: ComponentHarnessE2e},
{path: 'dialog', component: DialogE2E},
{path: 'expansion', component: ExpansionE2e},
{path: 'grid-list', component: GridListE2E},
@@ -56,7 +59,6 @@ export const E2E_APP_ROUTES: Routes = [
{path: 'slide-toggle', component: SlideToggleE2E},
{path: 'stepper', component: StepperE2e},
{path: 'tabs', component: BasicTabs},
{path: 'cards', component: CardE2e},
{path: 'toolbar', component: ToolbarE2e},
{path: 'virtual-scroll', component: VirtualScrollE2E},
];
+1
View File
@@ -24,6 +24,7 @@
</head>
<body>
<e2e-app>Loading...</e2e-app>
<span class="sibling">I am a sibling!</span>
<!-- Static scripts which are not imported through AMD nor part of the ConcatJS bundle. -->
<script src="core-js/client/core.min.js"></script>
+2
View File
@@ -9,6 +9,7 @@ import {ButtonToggleE2eModule} from './button-toggle/button-toggle-e2e-module';
import {ButtonE2eModule} from './button/button-e2e-module';
import {CardE2eModule} from './card/card-e2e-module';
import {CheckboxE2eModule} from './checkbox/checkbox-e2e-module';
import {ComponentHarnessE2eModule} from './component-harness/component-harness-e2e-module';
import {DialogE2eModule} from './dialog/dialog-e2e-module';
import {E2eApp} from './e2e-app';
import {E2eAppModule} from './e2e-app/e2e-app-module';
@@ -49,6 +50,7 @@ import {VirtualScrollE2eModule} from './virtual-scroll/virtual-scroll-e2e-module
ButtonToggleE2eModule,
CardE2eModule,
CheckboxE2eModule,
ComponentHarnessE2eModule,
DialogE2eModule,
ExpansionE2eModule,
GridListE2eModule,
+2 -1
View File
@@ -89,9 +89,10 @@ System.config({
'@angular/cdk/text-field': 'dist/packages/cdk/text-field/index.js',
'@angular/cdk/tree': 'dist/packages/cdk/tree/index.js',
'@angular/cdk-experimental/scrolling': 'dist/packages/cdk-experimental/scrolling/index.js',
'@angular/cdk-experimental/dialog': 'dist/packages/cdk-experimental/dialog/index.js',
'@angular/cdk-experimental/popover-edit': 'dist/packages/cdk-experimental/popover-edit/index.js',
'@angular/cdk-experimental/scrolling': 'dist/packages/cdk-experimental/scrolling/index.js',
'@angular/cdk-experimental/testing': 'dist/packages/cdk-experimental/testing/index.js',
'@angular/material/autocomplete': 'dist/packages/material/autocomplete/index.js',
'@angular/material/badge': 'dist/packages/material/badge/index.js',
+1 -1
View File
@@ -47,9 +47,9 @@ const rollupCdkExperimentalEntryPoints =
/** Map of globals that are used inside of the different packages. */
export const rollupGlobals = {
'moment': 'moment',
'tslib': 'tslib',
'protractor': 'protractor',
'selenium-webdriver': 'selenium-webdriver',
'tslib': 'tslib',
// MDC Web
'@material/animation': 'mdc.animation',