diff --git a/src/cdk-experimental/testing/component-harness.ts b/src/cdk-experimental/testing/component-harness.ts index e1c385561cd3..65c50e671002 100644 --- a/src/cdk-experimental/testing/component-harness.ts +++ b/src/cdk-experimental/testing/component-harness.ts @@ -8,183 +8,247 @@ 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; +/** An async function that returns a promise when called. */ +export type AsyncFn = () => Promise; + +/** + * Interface used to load ComponentHarness objects. This interface is used by test authors to + * instantiate `ComponentHarness`es. + */ +export interface HarnessLoader { + /** + * Searches for an element with the given selector under the current instances's root element, + * and returns a `HarnessLoader` rooted at the matching element. If multiple elements match the + * selector, the first is used. If no elements match, an error is thrown. + * @param selector The selector for the root element of the new `HarnessLoader` + * @return A `HarnessLoader` rooted at the element matching the given selector. + * @throws If a matching element can't be found. + */ + getChildLoader(selector: string): Promise; + + /** + * Searches for all elements with the given selector under the current instances's root element, + * and returns an array of `HarnessLoader`s, one for each matching element, rooted at that + * element. + * @param selector The selector for the root element of the new `HarnessLoader` + * @return A list of `HarnessLoader`s, one for each matching element, rooted at that element. + */ + getAllChildLoaders(selector: string): Promise; + + /** + * Searches for an instance of the component corresponding to the given harness type under the + * `HarnessLoader`'s root element, and returns a `ComponentHarness` for that instance. If multiple + * matching components are found, a harness for the first one is returned. If no matching + * component is found, an error is thrown. + * @param harnessType The type of harness to create + * @return An instance of the given harness type + * @throws If a matching component instance can't be found. + */ + getHarness(harnessType: ComponentHarnessConstructor): + Promise; + + /** + * Searches for all instances of the component corresponding to the given harness type under the + * `HarnessLoader`'s root element, and returns a list `ComponentHarness` for each instance. + * @param harnessType The type of harness to create + * @return A list instances of the given harness type. + */ + getAllHarnesses(harnessType: ComponentHarnessConstructor): + Promise; } -/** Interface that is used to find elements in the DOM and create harnesses for them. */ -export interface HarnessLocator { +/** + * Interface used to create asynchronous locator functions used find elements and component + * harnesses. This interface is used by `ComponentHarness` authors to create locator functions for + * their `ComponentHarenss` subclass. + */ +export interface LocatorFactory { + /** Gets a locator factory rooted at the document root. */ + documentRootLocatorFactory(): LocatorFactory; + + /** The root element of this `LocatorFactory` as a `TestElement`. */ + rootElement: TestElement; + + /** + * Creates an asynchronous locator function that can be used to search for elements with the given + * selector under the root element of this `LocatorFactory`. When the resulting locator function + * is invoked, if multiple matching elements are found, the first element is returned. If no + * elements are found, an error is thrown. + * @param selector The selector for the element that the locator function should search for. + * @return An asynchronous locator function that searches for elements with the given selector, + * and either finds one or throws an error + */ + locatorFor(selector: string): AsyncFn; + /** - * Get the host element of locator. + * Creates an asynchronous locator function that can be used to find a `ComponentHarness` for a + * component matching the given harness type under the root element of this `LocatorFactory`. + * When the resulting locator function is invoked, if multiple matching components are found, a + * harness for the first one is returned. If no components are found, an error is thrown. + * @param harnessType The type of harness to search for. + * @return An asynchronous locator function that searches components matching the given harness + * type, and either returns a `ComponentHarness` for the component, or throws an error. */ - host(): TestElement; + locatorFor(harnessType: ComponentHarnessConstructor): + AsyncFn; /** - * Search the first matched test element. - * @param selector The CSS selector of the test elements. - * @param options Optional, extra searching options + * Creates an asynchronous locator function that can be used to search for elements with the given + * selector under the root element of this `LocatorFactory`. When the resulting locator function + * is invoked, if multiple matching elements are found, the first element is returned. If no + * elements are found, null is returned. + * @param selector The selector for the element that the locator function should search for. + * @return An asynchronous locator function that searches for elements with the given selector, + * and either finds one or returns null. */ - querySelector(selector: string, options?: QueryOptions): Promise; + locatorForOptional(selector: string): AsyncFn; /** - * Search all matched test elements under current root by CSS selector. - * @param selector The CSS selector of the test elements. + * Creates an asynchronous locator function that can be used to find a `ComponentHarness` for a + * component matching the given harness type under the root element of this `LocatorFactory`. + * When the resulting locator function is invoked, if multiple matching components are found, a + * harness for the first one is returned. If no components are found, null is returned. + * @param harnessType The type of harness to search for. + * @return An asynchronous locator function that searches components matching the given harness + * type, and either returns a `ComponentHarness` for the component, or null if none is found. */ - querySelectorAll(selector: string): Promise; + locatorForOptional(harnessType: ComponentHarnessConstructor): + AsyncFn; /** - * 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 + * Creates an asynchronous locator function that can be used to search for a list of elements with + * the given selector under the root element of this `LocatorFactory`. When the resulting locator + * function is invoked, a list of matching elements is returned. + * @param selector The selector for the element that the locator function should search for. + * @return An asynchronous locator function that searches for elements with the given selector, + * and either finds one or throws an error */ - load( - componentHarness: ComponentHarnessConstructor, root: string, - options?: QueryOptions): Promise; + locatorForAll(selector: string): AsyncFn; /** - * Load all Component Harnesses under current root. - * @param componentHarness Type of user customized harness. - * @param root CSS root selector of the new component harnesses. + * Creates an asynchronous locator function that can be used to find a list of + * `ComponentHarness`es for all components matching the given harness type under the root element + * of this `LocatorFactory`. When the resulting locator function is invoked, a list of + * `ComponentHarness`es for the matching components is returned. + * @param harnessType The type of harness to search for. + * @return An asynchronous locator function that searches components matching the given harness + * type, and returns a list of `ComponentHarness`es. */ - loadAll( - componentHarness: ComponentHarnessConstructor, root: string): Promise; + locatorForAll(harnessType: ComponentHarnessConstructor): + AsyncFn; } /** - * 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. + * Base class for component harnesses that all component harness authors should extend. 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) {} + constructor(private readonly locatorFactory: LocatorFactory) {} - /** - * Get the host element of component harness. - */ - host(): TestElement { - return this.locator.host(); + /** Gets a `Promise` for the `TestElement` representing the host element of the component. */ + async host(): Promise { + return this.locatorFactory.rootElement; } /** - * Generate a function to find the first matched test element by CSS - * selector. - * @param selector The CSS selector of the test element. + * Gets a `LocatorFactory` for the document root element. This factory can be used to create + * locators for elements that a component creates outside of its own root element. (e.g. by + * appending to document.body). */ - protected find(selector: string): () => Promise; + protected documentRootLocatorFactory(): LocatorFactory { + return this.locatorFactory.documentRootLocatorFactory(); + } /** - * 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 + * Creates an asynchronous locator function that can be used to search for elements with the given + * selector under the host element of this `ComponentHarness`. When the resulting locator function + * is invoked, if multiple matching elements are found, the first element is returned. If no + * elements are found, an error is thrown. + * @param selector The selector for the element that the locator function should search for. + * @return An asynchronous locator function that searches for elements with the given selector, + * and either finds one or throws an error */ - protected find(selector: string, options: QueryOptions & {allowNull: true}): - () => Promise; + protected locatorFor(selector: string): AsyncFn; /** - * 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 + * Creates an asynchronous locator function that can be used to find a `ComponentHarness` for a + * component matching the given harness type under the host element of this `ComponentHarness`. + * When the resulting locator function is invoked, if multiple matching components are found, a + * harness for the first one is returned. If no components are found, an error is thrown. + * @param harnessType The type of harness to search for. + * @return An asynchronous locator function that searches components matching the given harness + * type, and either returns a `ComponentHarness` for the component, or throws an error. */ - protected find(selector: string, options: QueryOptions): () => Promise; + protected locatorFor( + harnessType: ComponentHarnessConstructor): AsyncFn; - /** - * 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( - componentHarness: ComponentHarnessConstructor, - root: string): () => Promise; + protected locatorFor(arg: any): any { + return this.locatorFactory.locatorFor(arg); + } /** - * 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 + * Creates an asynchronous locator function that can be used to search for elements with the given + * selector under the host element of this `ComponentHarness`. When the resulting locator function + * is invoked, if multiple matching elements are found, the first element is returned. If no + * elements are found, null is returned. + * @param selector The selector for the element that the locator function should search for. + * @return An asynchronous locator function that searches for elements with the given selector, + * and either finds one or returns null. */ - protected find( - componentHarness: ComponentHarnessConstructor, root: string, - options: QueryOptions & {allowNull: true}): () => Promise; + protected locatorForOptional(selector: string): AsyncFn; /** - * 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 + * Creates an asynchronous locator function that can be used to find a `ComponentHarness` for a + * component matching the given harness type under the host element of this `ComponentHarness`. + * When the resulting locator function is invoked, if multiple matching components are found, a + * harness for the first one is returned. If no components are found, null is returned. + * @param harnessType The type of harness to search for. + * @return An asynchronous locator function that searches components matching the given harness + * type, and either returns a `ComponentHarness` for the component, or null if none is found. */ - protected find( - componentHarness: ComponentHarnessConstructor, root: string, - options: QueryOptions): () => Promise; + protected locatorForOptional( + harnessType: ComponentHarnessConstructor): AsyncFn; - protected find( - selectorOrComponentHarness: string|ComponentHarnessConstructor, - selectorOrOptions?: string|QueryOptions, - options?: QueryOptions): () => Promise { - 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); - } + protected locatorForOptional(arg: any): any { + return this.locatorFactory.locatorForOptional(arg); } /** - * 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. + * Creates an asynchronous locator function that can be used to search for a list of elements with + * the given selector under the host element of this `ComponentHarness`. When the resulting + * locator function is invoked, a list of matching elements is returned. + * @param selector The selector for the element that the locator function should search for. + * @return An asynchronous locator function that searches for elements with the given selector, + * and either finds one or throws an error */ - protected findAll(selector: string): () => Promise; + protected locatorForAll(selector: string): AsyncFn; /** - * 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. + * Creates an asynchronous locator function that can be used to find a list of + * `ComponentHarness`es for all components matching the given harness type under the host element + * of this `ComponentHarness`. When the resulting locator function is invoked, a list of + * `ComponentHarness`es for the matching components is returned. + * @param harnessType The type of harness to search for. + * @return An asynchronous locator function that searches components matching the given harness + * type, and returns a list of `ComponentHarness`es. */ - protected findAll( - componentHarness: ComponentHarnessConstructor, - root: string): () => Promise; + protected locatorForAll(harnessType: ComponentHarnessConstructor): + AsyncFn; - protected findAll( - selectorOrComponentHarness: string|ComponentHarnessConstructor, - root?: string): () => Promise { - if (typeof selectorOrComponentHarness === 'string') { - const selector = selectorOrComponentHarness; - return () => this.locator.querySelectorAll(selector); - } else { - const componentHarness = selectorOrComponentHarness; - return () => this.locator.loadAll(componentHarness, root as string); - } + protected locatorForAll(arg: any): any { + return this.locatorFactory.locatorForAll(arg); } } /** Constructor for a ComponentHarness subclass. */ export interface ComponentHarnessConstructor { - new(locator: HarnessLocator): T; + new(locatorFactory: LocatorFactory): T; + + /** + * `ComponentHarness` subclasses must specify a static `hostSelector` property that is used to + * find the host element for the corresponding component. This property should match the selector + * for the Angular component. + */ + hostSelector: string; } diff --git a/src/cdk-experimental/testing/harness-environment.ts b/src/cdk-experimental/testing/harness-environment.ts new file mode 100644 index 000000000000..ff243d9dd07e --- /dev/null +++ b/src/cdk-experimental/testing/harness-environment.ts @@ -0,0 +1,144 @@ +/** + * @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 { + AsyncFn, + ComponentHarness, + ComponentHarnessConstructor, + HarnessLoader, + LocatorFactory +} from './component-harness'; +import {TestElement} from './test-element'; + +/** + * Base harness environment class that can be extended to allow `ComponentHarness`es to be used in + * different test environments (e.g. testbed, protractor, etc.). This class implements the + * functionality of both a `HarnessLoader` and `LocatorFactory`. This class is generic on the raw + * element type, `E`, used by the particular test environment. + */ +export abstract class HarnessEnvironment implements HarnessLoader, LocatorFactory { + // Implemented as part of the `LocatorFactory` interface. + rootElement: TestElement; + + protected constructor(protected rawRootElement: E) { + this.rootElement = this.createTestElement(rawRootElement); + } + + // Implemented as part of the `LocatorFactory` interface. + documentRootLocatorFactory(): LocatorFactory { + return this.createEnvironment(this.getDocumentRoot()); + } + + // Implemented as part of the `LocatorFactory` interface. + locatorFor(selector: string): AsyncFn; + locatorFor(harnessType: ComponentHarnessConstructor): + AsyncFn; + locatorFor( + arg: string | ComponentHarnessConstructor): AsyncFn { + return async () => { + if (typeof arg === 'string') { + const element = await this.getRawElement(arg); + if (element) { + return this.createTestElement(element); + } + } else { + const element = await this.getRawElement(arg.hostSelector); + if (element) { + return this.createComponentHarness(arg, element); + } + } + const selector = typeof arg === 'string' ? arg : arg.hostSelector; + throw Error(`Expected to find element matching selector: "${selector}", but none was found`); + }; + } + + // Implemented as part of the `LocatorFactory` interface. + locatorForOptional(selector: string): AsyncFn; + locatorForOptional(harnessType: ComponentHarnessConstructor): + AsyncFn; + locatorForOptional( + arg: string | ComponentHarnessConstructor): AsyncFn { + return async () => { + if (typeof arg === 'string') { + const element = await this.getRawElement(arg); + return element ? this.createTestElement(element) : null; + } else { + const element = await this.getRawElement(arg.hostSelector); + return element ? this.createComponentHarness(arg, element) : null; + } + }; + } + + // Implemented as part of the `LocatorFactory` interface. + locatorForAll(selector: string): AsyncFn; + locatorForAll(harnessType: ComponentHarnessConstructor): + AsyncFn; + locatorForAll( + arg: string | ComponentHarnessConstructor): AsyncFn { + return async () => { + if (typeof arg === 'string') { + return (await this.getAllRawElements(arg)).map(e => this.createTestElement(e)); + } else { + return (await this.getAllRawElements(arg.hostSelector)) + .map(e => this.createComponentHarness(arg, e)); + } + }; + } + + // Implemented as part of the `HarnessLoader` interface. + getHarness(harnessType: ComponentHarnessConstructor): + Promise { + return this.locatorFor(harnessType)(); + } + + // Implemented as part of the `HarnessLoader` interface. + getAllHarnesses(harnessType: ComponentHarnessConstructor): + Promise { + return this.locatorForAll(harnessType)(); + } + + // Implemented as part of the `HarnessLoader` interface. + async getChildLoader(selector: string): Promise { + const element = await this.getRawElement(selector); + if (element) { + return this.createEnvironment(element); + } + throw Error(`Expected to find element matching selector: "${selector}", but none was found`); + } + + // Implemented as part of the `HarnessLoader` interface. + async getAllChildLoaders(selector: string): Promise { + return (await this.getAllRawElements(selector)).map(e => this.createEnvironment(e)); + } + + /** Creates a `ComponentHarness` for the given harness type with the given raw host element. */ + protected createComponentHarness( + harnessType: ComponentHarnessConstructor, element: E): T { + return new harnessType(this.createEnvironment(element)); + } + + /** Gets the root element for the document. */ + protected abstract getDocumentRoot(): E; + + /** Creates a `TestElement` from a raw element. */ + protected abstract createTestElement(element: E): TestElement; + + /** Creates a `HarnessLoader` rooted at the given raw element. */ + protected abstract createEnvironment(element: E): HarnessEnvironment; + + /** + * Gets the first element matching the given selector under this environment's root element, or + * null if no elements match. + */ + protected abstract getRawElement(selector: string): Promise; + + /** + * Gets a list of all elements matching the given selector under this environment's root element. + */ + protected abstract getAllRawElements(selector: string): Promise; +} diff --git a/src/cdk-experimental/testing/protractor.ts b/src/cdk-experimental/testing/protractor.ts deleted file mode 100644 index f3fd769d243d..000000000000 --- a/src/cdk-experimental/testing/protractor.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * @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( - componentHarness: ComponentHarnessConstructor, - rootSelector: string): Promise; - -/** - * 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( - componentHarness: ComponentHarnessConstructor, rootSelector?: string, - options?: QueryOptions): Promise; - -export async function load( - componentHarness: ComponentHarnessConstructor, rootSelector = 'body', - options?: QueryOptions): Promise { - 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 { - const finder = await getElement(selector, this._rootFinder, options); - return finder && new ProtractorElement(finder); - } - - async querySelectorAll(selector: string): Promise { - const elementFinders = this._rootFinder.all(by.css(selector)); - return elementFinders.reduce( - (result: TestElement[], el: ElementFinder) => - el ? result.concat([new ProtractorElement(el)]) : result, - []); - } - - async load( - componentHarness: ComponentHarnessConstructor, selector: string, - options?: QueryOptions): Promise { - const root = await getElement(selector, this._rootFinder, options); - return root && new componentHarness(new ProtractorLocator(root)); - } - - async loadAll( - componentHarness: ComponentHarnessConstructor, - rootSelector: string): Promise { - 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 { - return this.element['blur'](); - } - - async clear(): Promise { - return this.element.clear(); - } - - async click(): Promise { - return this.element.click(); - } - - async focus(): Promise { - return this.element['focus'](); - } - - async getCssValue(property: string): Promise { - return this.element.getCssValue(property); - } - - async hover(): Promise { - return browser.actions() - .mouseMove(await this.element.getWebElement()) - .perform(); - } - - async sendKeys(keys: string): Promise { - return this.element.sendKeys(keys); - } - - async text(): Promise { - return this.element.getText(); - } - - async getAttribute(name: string): Promise { - 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 { - 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; -} diff --git a/src/cdk-experimental/testing/protractor/index.ts b/src/cdk-experimental/testing/protractor/index.ts new file mode 100644 index 000000000000..7b4ab8f52b30 --- /dev/null +++ b/src/cdk-experimental/testing/protractor/index.ts @@ -0,0 +1,10 @@ +/** + * @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 './protractor-element'; +export * from './protractor-harness-environment'; diff --git a/src/cdk-experimental/testing/protractor/protractor-element.ts b/src/cdk-experimental/testing/protractor/protractor-element.ts new file mode 100644 index 000000000000..1a89bde62bcb --- /dev/null +++ b/src/cdk-experimental/testing/protractor/protractor-element.ts @@ -0,0 +1,53 @@ +/** + * @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, ElementFinder} from 'protractor'; +import {TestElement} from '../test-element'; + +/** A `TestElement` implementation for Protractor. */ +export class ProtractorElement implements TestElement { + constructor(readonly element: ElementFinder) {} + + async blur(): Promise { + return browser.executeScript('arguments[0].blur()', this.element); + } + + async clear(): Promise { + return this.element.clear(); + } + + async click(): Promise { + return this.element.click(); + } + + async focus(): Promise { + return browser.executeScript('arguments[0].focus()', this.element); + } + + async getCssValue(property: string): Promise { + return this.element.getCssValue(property); + } + + async hover(): Promise { + return browser.actions() + .mouseMove(await this.element.getWebElement()) + .perform(); + } + + async sendKeys(keys: string): Promise { + return this.element.sendKeys(keys); + } + + async text(): Promise { + return this.element.getText(); + } + + async getAttribute(name: string): Promise { + return this.element.getAttribute(name); + } +} diff --git a/src/cdk-experimental/testing/protractor/protractor-harness-environment.ts b/src/cdk-experimental/testing/protractor/protractor-harness-environment.ts new file mode 100644 index 000000000000..3a8fec9a4774 --- /dev/null +++ b/src/cdk-experimental/testing/protractor/protractor-harness-environment.ts @@ -0,0 +1,48 @@ +/** + * @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 {by, element as protractorElement, ElementFinder} from 'protractor'; +import {HarnessLoader} from '../component-harness'; +import {HarnessEnvironment} from '../harness-environment'; +import {TestElement} from '../test-element'; +import {ProtractorElement} from './protractor-element'; + +/** A `HarnessEnvironment` implementation for Protractor. */ +export class ProtractorHarnessEnvironment extends HarnessEnvironment { + protected constructor(rawRootElement: ElementFinder) { + super(rawRootElement); + } + + /** Creates a `HarnessLoader` rooted at the document root. */ + static loader(): HarnessLoader { + return new ProtractorHarnessEnvironment(protractorElement(by.css('body'))); + } + + protected getDocumentRoot(): ElementFinder { + return protractorElement(by.css('body')); + } + + protected createTestElement(element: ElementFinder): TestElement { + return new ProtractorElement(element); + } + + protected createEnvironment(element: ElementFinder): HarnessEnvironment { + return new ProtractorHarnessEnvironment(element); + } + + protected async getRawElement(selector: string): Promise { + const element = this.rawRootElement.element(by.css(selector)); + return await element.isPresent() ? element : null; + } + + protected async getAllRawElements(selector: string): Promise { + const elements = this.rawRootElement.all(by.css(selector)); + return elements.reduce( + (result: ElementFinder[], el: ElementFinder) => el ? result.concat([el]) : result, []); + } +} diff --git a/src/cdk-experimental/testing/public-api.ts b/src/cdk-experimental/testing/public-api.ts index bedfe9b759cb..d90b5ede9b41 100644 --- a/src/cdk-experimental/testing/public-api.ts +++ b/src/cdk-experimental/testing/public-api.ts @@ -6,9 +6,8 @@ * 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 './harness-environment'; +export * from './protractor'; export * from './test-element'; -export {protractor, testbed}; +export * from './testbed'; diff --git a/src/cdk-experimental/testing/testbed.ts b/src/cdk-experimental/testing/testbed.ts deleted file mode 100644 index d2731cf60b13..000000000000 --- a/src/cdk-experimental/testing/testbed.ts +++ /dev/null @@ -1,207 +0,0 @@ -/** - * @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( - componentHarness: ComponentHarnessConstructor, - 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) { - this._rootElement = new UnitTestElement(_root, this._stabilize); - } - - host(): TestElement { - return this._rootElement; - } - - async querySelector(selector: string, options?: QueryOptions): Promise { - await this._stabilize(); - const e = getElement(selector, this._root, options); - return e && new UnitTestElement(e, this._stabilize); - } - - async querySelectorAll(selector: string): Promise { - await this._stabilize(); - return Array.prototype.map.call( - this._root.querySelectorAll(selector), - (e: Element) => new UnitTestElement(e, this._stabilize)); - } - - async load( - componentHarness: ComponentHarnessConstructor, selector: string, - options?: QueryOptions): Promise { - await this._stabilize(); - const root = getElement(selector, this._root, options); - return root && new componentHarness(new UnitTestLocator(root, this._stabilize)); - } - - async loadAll( - componentHarness: ComponentHarnessConstructor, - rootSelector: string): Promise { - 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) {} - - async blur(): Promise { - await this._stabilize(); - triggerBlur(this.element as HTMLElement); - await this._stabilize(); - } - - async clear(): Promise { - 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 { - await this._stabilize(); - dispatchMouseEvent(this.element, 'click'); - await this._stabilize(); - } - - async focus(): Promise { - await this._stabilize(); - triggerFocus(this.element as HTMLElement); - await this._stabilize(); - } - - async getCssValue(property: string): Promise { - 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 { - await this._stabilize(); - dispatchMouseEvent(this.element, 'mouseenter'); - await this._stabilize(); - } - - async sendKeys(keys: string): Promise { - 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 { - await this._stabilize(); - return this.element.textContent || ''; - } - - async getAttribute(name: string): Promise { - 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; -} diff --git a/src/cdk-experimental/testing/testbed/index.ts b/src/cdk-experimental/testing/testbed/index.ts new file mode 100644 index 000000000000..0422b3097bf3 --- /dev/null +++ b/src/cdk-experimental/testing/testbed/index.ts @@ -0,0 +1,10 @@ +/** + * @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 './testbed-harness-environment'; +export * from './unit-test-element'; diff --git a/src/cdk-experimental/testing/testbed/testbed-harness-environment.ts b/src/cdk-experimental/testing/testbed/testbed-harness-environment.ts new file mode 100644 index 000000000000..18c7b09950ea --- /dev/null +++ b/src/cdk-experimental/testing/testbed/testbed-harness-environment.ts @@ -0,0 +1,65 @@ +/** + * @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 {ComponentFixture} from '@angular/core/testing'; +import {ComponentHarness, ComponentHarnessConstructor, HarnessLoader} from '../component-harness'; +import {HarnessEnvironment} from '../harness-environment'; +import {TestElement} from '../test-element'; +import {UnitTestElement} from './unit-test-element'; + +/** A `HarnessEnvironment` implementation for Angular's Testbed. */ +export class TestbedHarnessEnvironment extends HarnessEnvironment { + protected constructor(rawRootElement: Element, private _fixture: ComponentFixture) { + super(rawRootElement); + } + + /** Creates a `HarnessLoader` rooted at the given fixture's root element. */ + static loader(fixture: ComponentFixture): HarnessLoader { + return new TestbedHarnessEnvironment(fixture.nativeElement, fixture); + } + + /** + * Creates an instance of the given harness type, using the fixture's root element as the + * harness's host element. This method should be used when creating a harness for the root element + * of a fixture, as components do not have the correct selector when they are created as the root + * of the fixture. + */ + static async harnessForFixture( + fixture: ComponentFixture, harnessType: ComponentHarnessConstructor): Promise { + const environment = new TestbedHarnessEnvironment(fixture.nativeElement, fixture); + await environment._stabilize(); + return environment.createComponentHarness(harnessType, fixture.nativeElement); + } + + protected getDocumentRoot(): Element { + return this._fixture.nativeElement; + } + + protected createTestElement(element: Element): TestElement { + return new UnitTestElement(element, this._stabilize.bind(this)); + } + + protected createEnvironment(element: Element): HarnessEnvironment { + return new TestbedHarnessEnvironment(element, this._fixture); + } + + protected async getRawElement(selector: string): Promise { + await this._stabilize(); + return this.rawRootElement.querySelector(selector) || null; + } + + protected async getAllRawElements(selector: string): Promise { + await this._stabilize(); + return Array.from(this.rawRootElement.querySelectorAll(selector)); + } + + private async _stabilize(): Promise { + this._fixture.detectChanges(); + await this._fixture.whenStable(); + } +} diff --git a/src/cdk-experimental/testing/testbed/unit-test-element.ts b/src/cdk-experimental/testing/testbed/unit-test-element.ts new file mode 100644 index 000000000000..b778ef307d0e --- /dev/null +++ b/src/cdk-experimental/testing/testbed/unit-test-element.ts @@ -0,0 +1,103 @@ +/** + * @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 {TestElement} from '../test-element'; + +function isTextInput(element: Element): element is HTMLInputElement | HTMLTextAreaElement { + return element.nodeName.toLowerCase() === 'input' || + element.nodeName.toLowerCase() === 'textarea' ; +} + +/** A `TestElement` implementation for unit tests. */ +export class UnitTestElement implements TestElement { + constructor(readonly element: Element, private _stabilize: () => Promise) {} + + async blur(): Promise { + await this._stabilize(); + triggerBlur(this.element as HTMLElement); + await this._stabilize(); + } + + async clear(): Promise { + await this._stabilize(); + if (!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 { + await this._stabilize(); + dispatchMouseEvent(this.element, 'click'); + await this._stabilize(); + } + + async focus(): Promise { + await this._stabilize(); + triggerFocus(this.element as HTMLElement); + await this._stabilize(); + } + + async getCssValue(property: string): Promise { + 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 { + await this._stabilize(); + dispatchMouseEvent(this.element, 'mouseenter'); + await this._stabilize(); + } + + async sendKeys(keys: string): Promise { + 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 (isTextInput(this.element)) { + this.element.value += key; + } + dispatchKeyboardEvent(this.element, 'keyup', keyCode); + if (isTextInput(this.element)) { + dispatchFakeEvent(this.element, 'input'); + } + } + await this._stabilize(); + } + + async text(): Promise { + await this._stabilize(); + return this.element.textContent || ''; + } + + async getAttribute(name: string): Promise { + 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; + } +} diff --git a/src/cdk-experimental/testing/tests/harnesses/main-component-harness.ts b/src/cdk-experimental/testing/tests/harnesses/main-component-harness.ts index b31421814c90..fecd954fc8c9 100644 --- a/src/cdk-experimental/testing/tests/harnesses/main-component-harness.ts +++ b/src/cdk-experimental/testing/tests/harnesses/main-component-harness.ts @@ -10,33 +10,40 @@ import {ComponentHarness} from '../../component-harness'; import {TestElement} from '../../test-element'; import {SubComponentHarness} from './sub-component-harness'; +export class WrongComponentHarness extends ComponentHarness { + static readonly hostSelector = 'wrong-selector'; +} + 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'); + static readonly hostSelector = 'test-main'; + + readonly title = this.locatorFor('h1'); + readonly button = this.locatorFor('button'); + readonly asyncCounter = this.locatorFor('#asyncCounter'); + readonly counter = this.locatorFor('#counter'); + readonly input = this.locatorFor('#input'); + readonly value = this.locatorFor('#value'); + readonly allLabels = this.locatorForAll('label'); + readonly allLists = this.locatorForAll(SubComponentHarness); + readonly memo = this.locatorFor('textarea'); // Allow null for element - readonly nullItem = this.find('wrong locator', {allowNull: true}); + readonly nullItem = this.locatorForOptional('wrong locator'); // Allow null for component harness - readonly nullComponentHarness = - this.find(SubComponentHarness, 'wrong locator', {allowNull: true}); - readonly errorItem = this.find('wrong locator', {allowNull: false}); + readonly nullComponentHarness = this.locatorForOptional(WrongComponentHarness); + readonly errorItem = this.locatorFor('wrong locator'); + + readonly globalEl = this.documentRootLocatorFactory().locatorFor('.sibling'); + readonly errorGlobalEl = this.documentRootLocatorFactory().locatorFor('wrong locator'); + readonly nullGlobalEl = this.documentRootLocatorFactory().locatorForOptional('wrong locator'); - 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}); + readonly optionalDiv = this.locatorForOptional('div'); + readonly optionalSubComponent = this.locatorForOptional(SubComponentHarness); + readonly errorSubComponent = this.locatorFor(WrongComponentHarness); - private _button = this.find('button'); - private _testTools = this.find(SubComponentHarness, 'test-sub'); + private _testTools = this.locatorFor(SubComponentHarness); async increaseCounter(times: number) { - const button = await this._button(); + const button = await this.button(); for (let i = 0; i < times; i++) { await button.click(); } diff --git a/src/cdk-experimental/testing/tests/harnesses/sub-component-harness.ts b/src/cdk-experimental/testing/tests/harnesses/sub-component-harness.ts index a70ffd3418a3..b34ab41f2f94 100644 --- a/src/cdk-experimental/testing/tests/harnesses/sub-component-harness.ts +++ b/src/cdk-experimental/testing/tests/harnesses/sub-component-harness.ts @@ -10,8 +10,11 @@ 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'); + static readonly hostSelector = 'test-sub'; + + readonly title = this.locatorFor('h2'); + readonly getItems = this.locatorForAll('li'); + readonly globalElement = this.documentRootLocatorFactory().locatorFor('#username'); async getItem(index: number): Promise { const items = await this.getItems(); diff --git a/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts b/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts index cc70b96618da..ef240a8c6052 100644 --- a/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts +++ b/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts @@ -1,31 +1,106 @@ -import {browser, by, element} from 'protractor'; - -import {getElementFinder, load} from '../protractor'; +import {browser} from 'protractor'; +import {HarnessLoader} from '../component-harness'; +import {ProtractorHarnessEnvironment} from '../protractor'; import {MainComponentHarness} from './harnesses/main-component-harness'; +import {SubComponentHarness} from './harnesses/sub-component-harness'; -describe('Protractor Helper Test', () => { - let harness: MainComponentHarness; - +describe('ProtractorHarnessEnvironment', () => { 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 () => { + describe('HarnessLoader', () => { + let loader: HarnessLoader; + + beforeEach(async () => { + loader = ProtractorHarnessEnvironment.loader(); + }); + + it('should create HarnessLoader', async () => { + expect(loader).not.toBeNull(); + }); + + it('should find required HarnessLoader for child element', async () => { + const subcomponentsLoader = await loader.getChildLoader('.subcomponents'); + expect(subcomponentsLoader).not.toBeNull(); + }); + + it('should error after failing to find required HarnessLoader for child element', async () => { + try { + await loader.getChildLoader('error'); + fail('Expected to throw'); + } catch (e) { + expect(e.message) + .toBe('Expected to find element matching selector: "error", but none was found'); + } + }); + + it('should find all HarnessLoaders for child elements', async () => { + const loaders = await loader.getAllChildLoaders('.subcomponents,.counters'); + expect(loaders.length).toBe(2); + }); + + it('should get first matching component for required harness', async () => { + const harness = await loader.getHarness(SubComponentHarness); + expect(harness).not.toBeNull(); + expect(await (await harness.title()).text()).toBe('List of test tools'); + }); + + it('should throw if no matching component found for required harness', async () => { + const countersLoader = await loader.getChildLoader('.counters'); + try { + await countersLoader.getHarness(SubComponentHarness); + fail('Expected to throw'); + } catch (e) { + expect(e.message) + .toBe('Expected to find element matching selector: "test-sub", but none was found'); + } + }); + + it('should get all matching components for all harnesses', async () => { + const harnesses = await loader.getAllHarnesses(SubComponentHarness); + expect(harnesses.length).toBe(2); + }); + }); + + describe('ComponentHarness', () => { + let harness: MainComponentHarness; + + beforeEach(async () => { + harness = await ProtractorHarnessEnvironment.loader().getHarness(MainComponentHarness); + }); + + it('should locate a required 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 throw when failing to locate a required element based on CSS selector', async () => { + try { + await harness.errorItem(); + fail('Expected to throw'); + } catch (e) { + expect(e.message).toBe( + 'Expected to find element matching selector: "wrong locator", but none was found'); + } + }); + + it('should locate an optional element based on CSS selector', async () => { + const present = await harness.optionalDiv(); + const missing = await harness.nullItem(); + expect(present).not.toBeNull(); + expect(await present!.text()).toBe('Hello Yi from Angular 2!'); + expect(missing).toBeNull(); + }); - it('should be able to locate the sub harnesses', async () => { + it('should 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 locate required sub harnesses', async () => { const items = await harness.getTestTools(); expect(items.length).toBe(3); expect(await items[0].text()).toBe('Protractor'); @@ -33,7 +108,25 @@ describe('Protractor Helper Test', () => { expect(await items[2].text()).toBe('Other'); }); - it('should be able to locate all sub harnesses', async () => { + it('should throw when failing to locate required sub harnesses', async () => { + try { + await harness.errorSubComponent(); + fail('Expected to throw'); + } catch (e) { + expect(e.message).toBe( + 'Expected to find element matching selector: "wrong-selector", but none was found'); + } + }); + + it('should locate optional sub harnesses', async () => { + const present = await harness.optionalSubComponent(); + const missing = await harness.nullComponentHarness(); + expect(present).not.toBeNull(); + expect(await (await present!.title()).text()).toBe('List of test tools'); + expect(missing).toBeNull(); + }); + + it('should locate all sub harnesses', async () => { const alllists = await harness.allLists(); const items1 = await alllists[0].getItems(); const items2 = await alllists[1].getItems(); @@ -47,9 +140,27 @@ describe('Protractor Helper Test', () => { expect(await items2[1].text()).toBe('Integration Test'); expect(await items2[2].text()).toBe('Performance Test'); }); + + it('should wait for async operation to complete', async () => { + const asyncCounter = await harness.asyncCounter(); + expect(await asyncCounter.text()).toBe('5'); + await harness.increaseCounter(3); + expect(await asyncCounter.text()).toBe('8'); + }); + + it('can get elements outside of host', async () => { + const globalEl = await harness.globalEl(); + expect(await globalEl.text()).toBe('I am a sibling!'); + }); }); - describe('Test element', () => { + describe('TestElement', () => { + let harness: MainComponentHarness; + + beforeEach(async () => { + harness = await ProtractorHarnessEnvironment.loader().getHarness(MainComponentHarness); + }); + it('should be able to clear', async () => { const input = await harness.input(); await input.sendKeys('Yi'); @@ -79,8 +190,7 @@ describe('Protractor Helper Test', () => { const input = await harness.input(); await input.sendKeys('Yi'); expect(await input.getAttribute('id')) - .toBe(await browser.driver.switchTo().activeElement().getAttribute( - 'id')); + .toBe(await browser.driver.switchTo().activeElement().getAttribute('id')); }); it('should be able to hover', async () => { @@ -106,78 +216,16 @@ describe('Protractor Helper Test', () => { 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()); + it('should focus and blur element', async () => { + let button = await harness.button(); + expect(await (await browser.switchTo().activeElement()).getText()) + .not.toBe(await button.text()); + await button.focus(); + expect(await (await browser.switchTo().activeElement()).getText()).toBe(await button.text()); + await button.blur(); + expect(await (await browser.switchTo().activeElement()).getText()) + .not.toBe(await button.text()); }); }); }); diff --git a/src/cdk-experimental/testing/tests/test-main-component.html b/src/cdk-experimental/testing/tests/test-main-component.html index 6258c51786ef..38df12151213 100644 --- a/src/cdk-experimental/testing/tests/test-main-component.html +++ b/src/cdk-experimental/testing/tests/test-main-component.html @@ -1,12 +1,18 @@

Main Component

Hello {{username}} from Angular 2!
-
- -
{{counter}}
- -
{{asyncCounter}}
- -
Input: {{input}}
- - - +
+
+ +
{{counter}}
+ +
{{asyncCounter}}
+
+
+ +
Input: {{input}}
+ +
+
+ + +
diff --git a/src/cdk-experimental/testing/tests/testbed.spec.ts b/src/cdk-experimental/testing/tests/testbed.spec.ts index 57a418e4b303..638b4df9cd7a 100644 --- a/src/cdk-experimental/testing/tests/testbed.spec.ts +++ b/src/cdk-experimental/testing/tests/testbed.spec.ts @@ -1,40 +1,122 @@ -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import {getNativeElement, load} from '../testbed'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {HarnessLoader} from '../component-harness'; +import {TestbedHarnessEnvironment} from '../testbed/index'; import {MainComponentHarness} from './harnesses/main-component-harness'; - +import {SubComponentHarness} from './harnesses/sub-component-harness'; import {TestComponentsModule} from './test-components-module'; import {TestMainComponent} from './test-main-component'; -describe('Testbed Helper Test', () => { - let harness: MainComponentHarness; +function activeElementText() { + return document.activeElement && (document.activeElement as HTMLElement).innerText || ''; +} + +describe('TestbedHarnessEnvironment', () => { 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 () => { + + beforeEach(async () => { + await TestBed.configureTestingModule({imports: [TestComponentsModule]}).compileComponents(); + fixture = TestBed.createComponent(TestMainComponent); + }); + + describe('HarnessLoader', () => { + let loader: HarnessLoader; + + beforeEach(async () => { + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + it('should create HarnessLoader from fixture', async () => { + expect(loader).not.toBeNull(); + }); + + it('should create ComponentHarness for fixture', async () => { + const harness = + await TestbedHarnessEnvironment.harnessForFixture(fixture, MainComponentHarness); + expect(harness).not.toBeNull(); + }); + + it('should find required HarnessLoader for child element', async () => { + const subcomponentsLoader = await loader.getChildLoader('.subcomponents'); + expect(subcomponentsLoader).not.toBeNull(); + }); + + it('should error after failing to find required HarnessLoader for child element', async () => { + try { + await loader.getChildLoader('error'); + fail('Expected to throw'); + } catch (e) { + expect(e.message) + .toBe('Expected to find element matching selector: "error", but none was found'); + } + }); + + it('should find all HarnessLoaders for child elements', async () => { + const loaders = await loader.getAllChildLoaders('.subcomponents,.counters'); + expect(loaders.length).toBe(2); + }); + + it('should get first matching component for required harness', async () => { + const harness = await loader.getHarness(SubComponentHarness); + expect(harness).not.toBeNull(); + expect(await (await harness.title()).text()).toBe('List of test tools'); + }); + + it('should throw if no matching component found for required harness', async () => { + const countersLoader = await loader.getChildLoader('.counters'); + try { + await countersLoader.getHarness(SubComponentHarness); + fail('Expected to throw'); + } catch (e) { + expect(e.message) + .toBe('Expected to find element matching selector: "test-sub", but none was found'); + } + }); + + it('should get all matching components for all harnesses', async () => { + const harnesses = await loader.getAllHarnesses(SubComponentHarness); + expect(harnesses.length).toBe(2); + }); + }); + + describe('ComponentHarness', () => { + let harness: MainComponentHarness; + + beforeEach(async () => { + harness = + await TestbedHarnessEnvironment.harnessForFixture(fixture, MainComponentHarness); + }); + + it('should locate a required 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 throw when failing to locate a required element based on CSS selector', async () => { + try { + await harness.errorItem(); + fail('Expected to throw'); + } catch (e) { + expect(e.message).toBe( + 'Expected to find element matching selector: "wrong locator", but none was found'); + } + }); + + it('should locate an optional element based on CSS selector', async () => { + const present = await harness.optionalDiv(); + const missing = await harness.nullItem(); + expect(present).not.toBeNull(); + expect(await present!.text()).toBe('Hello Yi from Angular 2!'); + expect(missing).toBeNull(); + }); + + it('should 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 () => { + it('should locate required sub harnesses', async () => { const items = await harness.getTestTools(); expect(items.length).toBe(3); expect(await items[0].text()).toBe('Protractor'); @@ -42,7 +124,25 @@ describe('Testbed Helper Test', () => { expect(await items[2].text()).toBe('Other'); }); - it('should be able to locate all sub harnesses', async () => { + it('should throw when failing to locate required sub harnesses', async () => { + try { + await harness.errorSubComponent(); + fail('Expected to throw'); + } catch (e) { + expect(e.message).toBe( + 'Expected to find element matching selector: "wrong-selector", but none was found'); + } + }); + + it('should locate optional sub harnesses', async () => { + const present = await harness.optionalSubComponent(); + const missing = await harness.nullComponentHarness(); + expect(present).not.toBeNull(); + expect(await (await present!.title()).text()).toBe('List of test tools'); + expect(missing).toBeNull(); + }); + + it('should locate all sub harnesses', async () => { const alllists = await harness.allLists(); const items1 = await alllists[0].getItems(); const items2 = await alllists[1].getItems(); @@ -56,9 +156,31 @@ describe('Testbed Helper Test', () => { expect(await items2[1].text()).toBe('Integration Test'); expect(await items2[2].text()).toBe('Performance Test'); }); + + it('should wait for async operation to complete', async () => { + const asyncCounter = await harness.asyncCounter(); + expect(await asyncCounter.text()).toBe('5'); + await harness.increaseCounter(3); + expect(await asyncCounter.text()).toBe('8'); + }); + + it('can get elements outside of host', async () => { + const subcomponents = await harness.allLists(); + expect(subcomponents[0]).not.toBeNull(); + const globalEl = await subcomponents[0]!.globalElement(); + expect(globalEl).not.toBeNull(); + expect(await globalEl.text()).toBe('Hello Yi from Angular 2!'); + }); }); - describe('Test element', () => { + describe('TestElement', () => { + let harness: MainComponentHarness; + + beforeEach(async () => { + harness = + await TestbedHarnessEnvironment.harnessForFixture(fixture, MainComponentHarness); + }); + it('should be able to clear', async () => { const input = await harness.input(); await input.sendKeys('Yi'); @@ -87,8 +209,7 @@ describe('Testbed Helper Test', () => { 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); + expect(await input.getAttribute('id')).toBe(document.activeElement!.id); }); it('should be able to hover', async () => { @@ -114,43 +235,14 @@ describe('Testbed Helper Test', () => { 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); + it('should focus and blur element', async () => { + let button = await harness.button(); + expect(activeElementText()).not.toBe(await button.text()); + await button.focus(); + expect(activeElementText()).toBe(await button.text()); + await button.blur(); + expect(activeElementText()).not.toBe(await button.text()); }); }); });