mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
+3
-1
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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>
|
||||
|
||||
@@ -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},
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user