From 2613ca2a60b15b979387ba3a4a948e923f68aebd Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 21 May 2019 13:10:27 -0700 Subject: [PATCH 1/8] feat(cdk-experimental/testing): Bring in component harness Co-Authored-By: Yi Qi Co-Authored-By: Rado Kirov Co-Authored-By: Thomas Shafer Co-Authored-By: Caitlin Mott Co-Authored-By: Craig Nishina --- .../testing/component-harness.ts | 213 ++++++++++++++++++ src/cdk-experimental/testing/protractor.ts | 182 +++++++++++++++ src/cdk-experimental/testing/test-app/app.ts | 15 ++ .../harnesses/main-component-harness.ts | 45 ++++ .../harnesses/sub-component-harness.ts | 19 ++ .../testing/test-app/index.html | 12 + .../testing/test-app/main-component.ts | 60 +++++ src/cdk-experimental/testing/test-app/main.ts | 4 + .../testing/test-app/protractor.e2e.spec.ts | 183 +++++++++++++++ .../testing/test-app/sub-component.ts | 17 ++ .../testing/test-app/testbed.spec.ts | 158 +++++++++++++ src/cdk-experimental/testing/testbed.ts | 198 ++++++++++++++++ 12 files changed, 1106 insertions(+) create mode 100644 src/cdk-experimental/testing/component-harness.ts create mode 100644 src/cdk-experimental/testing/protractor.ts create mode 100644 src/cdk-experimental/testing/test-app/app.ts create mode 100644 src/cdk-experimental/testing/test-app/harnesses/main-component-harness.ts create mode 100644 src/cdk-experimental/testing/test-app/harnesses/sub-component-harness.ts create mode 100644 src/cdk-experimental/testing/test-app/index.html create mode 100644 src/cdk-experimental/testing/test-app/main-component.ts create mode 100644 src/cdk-experimental/testing/test-app/main.ts create mode 100644 src/cdk-experimental/testing/test-app/protractor.e2e.spec.ts create mode 100644 src/cdk-experimental/testing/test-app/sub-component.ts create mode 100644 src/cdk-experimental/testing/test-app/testbed.spec.ts create mode 100644 src/cdk-experimental/testing/testbed.ts diff --git a/src/cdk-experimental/testing/component-harness.ts b/src/cdk-experimental/testing/component-harness.ts new file mode 100644 index 000000000000..3634c386a4d8 --- /dev/null +++ b/src/cdk-experimental/testing/component-harness.ts @@ -0,0 +1,213 @@ +/** + * Test Element interface + * This is a wrapper of native element + */ +export interface TestElement { + blur(): Promise; + clear(): Promise; + click(): Promise; + focus(): Promise; + getCssValue(property: string): Promise; + hover(): Promise; + sendKeys(keys: string): Promise; + text(): Promise; + getAttribute(name: string): Promise; +} + +/** + * Extra searching options used by searching functions + * + * @param allowNull Optional, 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. + * + * @param global Optional. 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. + */ +export interface Options { + allowNull?: boolean; + global?: boolean; +} + +/** + * Type narrowing of Options to allow the overloads of ComponentHarness.find to + * return null only if allowNull is set to true. + */ +interface OptionsWithAllowNullSet extends Options { + allowNull: true; +} + +/** + * Locator interface + */ +export interface Locator { + /** + * Get the host element of locator. + */ + host(): TestElement; + + /** + * Search the first matched test element. + * @param css Selector of the test elements. + * @param options Optional, extra searching options + */ + find(css: string, options?: Options): Promise; + + /** + * Search all matched test elements under current root by css selector. + * @param css Selector of the test elements. + */ + findAll(css: string): Promise; + + /** + * 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( + componentHarness: ComponentHarnessType, root: string, + options?: Options): Promise; + + /** + * 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( + componentHarness: ComponentHarnessType, root: string): Promise; +} + +/** + * 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 class ComponentHarness { + constructor(private readonly locator: Locator) {} + + /** + * 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 css Css selector of the test element. + */ + protected find(css: string): () => Promise; + + /** + * Generate a function to find the first matched test element by css + * selector. + * @param css Css selector of the test element. + * @param options Extra searching options + */ + protected find(css: string, options: OptionsWithAllowNullSet): + () => Promise; + + /** + * Generate a function to find the first matched test element by css + * selector. + * @param css Css selector of the test element. + * @param options Extra searching options + */ + protected find(css: string, options: Options): () => Promise; + + /** + * 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: ComponentHarnessType, + root: string): () => Promise; + + /** + * 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( + componentHarness: ComponentHarnessType, root: string, + options: OptionsWithAllowNullSet): () => Promise; + + /** + * 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( + componentHarness: ComponentHarnessType, root: string, + options: Options): () => Promise; + + protected find( + cssOrComponentHarness: string|ComponentHarnessType, + cssOrOptions?: string|Options, + options?: Options): () => Promise { + if (typeof cssOrComponentHarness === 'string') { + const css = cssOrComponentHarness; + const options = cssOrOptions as Options; + return () => this.locator.find(css, options); + } else { + const componentHarness = cssOrComponentHarness; + const css = cssOrOptions as string; + return () => this.locator.load(componentHarness, css, options); + } + } + + /** + * Generate a function to find all matched test elements by css selector. + * @param css Css root selector of elements. It will locate + * elements under the current root. + */ + protected findAll(css: string): () => Promise; + + /** + * 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( + componentHarness: ComponentHarnessType, + root: string): () => Promise; + + protected findAll( + cssOrComponentHarness: string|ComponentHarnessType, + root?: string): () => Promise { + if (typeof cssOrComponentHarness === 'string') { + const css = cssOrComponentHarness; + return () => this.locator.findAll(css); + } else { + const componentHarness = cssOrComponentHarness; + return () => this.locator.loadAll(componentHarness, root as string); + } + } +} + +/** + * Type of ComponentHarness. + */ +export interface ComponentHarnessType { + new(locator: Locator): T; +} diff --git a/src/cdk-experimental/testing/protractor.ts b/src/cdk-experimental/testing/protractor.ts new file mode 100644 index 000000000000..5df84b13f5eb --- /dev/null +++ b/src/cdk-experimental/testing/protractor.ts @@ -0,0 +1,182 @@ +import {browser, by, element, ElementFinder} from 'protractor'; +import {promise as wdpromise} from 'selenium-webdriver'; + +import {ComponentHarness, ComponentHarnessType, Locator, Options, TestElement} from './component-harness'; + +/** + * 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: ComponentHarnessType, + 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: ComponentHarnessType, rootSelector?: string, + options?: Options): Promise; + +export async function load( + componentHarness: ComponentHarnessType, rootSelector = 'body', + options?: Options): Promise { + const root = await getElement(rootSelector, undefined, options); + if (root === null) { + return null; + } + const locator = new ProtractorLocator(root); + return new componentHarness(locator); +} + +/** + * Gets the corresponding ElementFinder for the root of a TestElement. + */ +export function getElementFinder(testElement: TestElement): ElementFinder { + if (testElement instanceof ProtractorElement) { + return testElement.element; + } + + throw new Error('Invalid element provided'); +} + +class ProtractorLocator implements Locator { + private root: ProtractorElement; + constructor(private rootFinder: ElementFinder) { + this.root = new ProtractorElement(this.rootFinder); + } + + host(): TestElement { + return this.root; + } + + async find(css: string, options?: Options): Promise { + const finder = await getElement(css, this.rootFinder, options); + if (finder === null) return null; + return new ProtractorElement(finder); + } + + async findAll(css: string): Promise { + const elementFinders = this.rootFinder.all(by.css(css)); + const res: TestElement[] = []; + await elementFinders.each(el => { + if (el) { + res.push(new ProtractorElement(el)); + } + }); + return res; + } + + async load( + componentHarness: ComponentHarnessType, css: string, + options?: Options): Promise { + const root = await getElement(css, this.rootFinder, options); + if (root === null) return null; + const locator = new ProtractorLocator(root); + return new componentHarness(locator); + } + + async loadAll( + componentHarness: ComponentHarnessType, + rootSelector: string, + ): Promise { + const roots = this.rootFinder.all(by.css(rootSelector)); + const res: T[] = []; + await roots.each(el => { + if (el) { + const locator = new ProtractorLocator(el); + res.push(new componentHarness(locator)); + } + }); + return res; + } +} + +class ProtractorElement implements TestElement { + constructor(readonly element: ElementFinder) {} + + blur(): Promise { + return toPromise(this.element['blur']()); + } + + clear(): Promise { + return toPromise(this.element.clear()); + } + + click(): Promise { + return toPromise(this.element.click()); + } + + focus(): Promise { + return toPromise(this.element['focus']()); + } + + getCssValue(property: string): Promise { + return toPromise(this.element.getCssValue(property)); + } + + async hover(): Promise { + return toPromise(browser.actions() + .mouseMove(await this.element.getWebElement()) + .perform()); + } + + sendKeys(keys: string): Promise { + return toPromise(this.element.sendKeys(keys)); + } + + text(): Promise { + return toPromise(this.element.getText()); + } + + getAttribute(name: string): Promise { + return toPromise(this.element.getAttribute(name)); + } +} + +function toPromise(p: wdpromise.Promise): Promise { + return new Promise((resolve, reject) => { + p.then(resolve, reject); + }); +} + +/** + * 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 css 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(css: string, root?: ElementFinder, options?: Options): + Promise { + const useGlobalRoot = options && !!options.global; + const elem = root === undefined || useGlobalRoot ? element(by.css(css)) : + root.element(by.css(css)); + const allowNull = options !== undefined && options.allowNull !== undefined ? + options.allowNull : + undefined; + if (allowNull !== undefined) { + // Only check isPresent when allowNull is set + if (!(await elem.isPresent())) { + if (allowNull) { + return null; + } + throw new Error('Cannot find element based on the css selector: ' + css); + } + return elem; + } + return elem; +} diff --git a/src/cdk-experimental/testing/test-app/app.ts b/src/cdk-experimental/testing/test-app/app.ts new file mode 100644 index 000000000000..f65bf661d480 --- /dev/null +++ b/src/cdk-experimental/testing/test-app/app.ts @@ -0,0 +1,15 @@ +import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {BrowserModule} from '@angular/platform-browser'; +import {MainComponent} from './main-component'; +import {SubComponent} from './sub-component'; + +@NgModule({ + imports: [FormsModule, BrowserModule], + declarations: [MainComponent, SubComponent], + exports: [MainComponent], + bootstrap: [MainComponent], +}) + +export class AppModule { +} diff --git a/src/cdk-experimental/testing/test-app/harnesses/main-component-harness.ts b/src/cdk-experimental/testing/test-app/harnesses/main-component-harness.ts new file mode 100644 index 000000000000..85f93bd0d309 --- /dev/null +++ b/src/cdk-experimental/testing/test-app/harnesses/main-component-harness.ts @@ -0,0 +1,45 @@ +import {ComponentHarness, TestElement} from '../../component-harness'; +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, '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, '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 { + const subComponent = await this.testTools(); + return subComponent.getItem(index); + } + + async getTestTools(): Promise { + const subComponent = await this.testTools(); + return subComponent.getItems(); + } +} diff --git a/src/cdk-experimental/testing/test-app/harnesses/sub-component-harness.ts b/src/cdk-experimental/testing/test-app/harnesses/sub-component-harness.ts new file mode 100644 index 000000000000..3b08e7255355 --- /dev/null +++ b/src/cdk-experimental/testing/test-app/harnesses/sub-component-harness.ts @@ -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 {ComponentHarness, TestElement} from '../../component-harness'; + +export class SubComponentHarness extends ComponentHarness { + readonly title = this.find('h2'); + readonly getItems = this.findAll('li'); + + async getItem(index: number): Promise { + const items = await this.getItems(); + return items[index]; + } +} diff --git a/src/cdk-experimental/testing/test-app/index.html b/src/cdk-experimental/testing/test-app/index.html new file mode 100644 index 000000000000..24164d7a48ec --- /dev/null +++ b/src/cdk-experimental/testing/test-app/index.html @@ -0,0 +1,12 @@ + + + + + angular2 page + + +
+I am a sibling! + + + diff --git a/src/cdk-experimental/testing/test-app/main-component.ts b/src/cdk-experimental/testing/test-app/main-component.ts new file mode 100644 index 000000000000..a08f48311aef --- /dev/null +++ b/src/cdk-experimental/testing/test-app/main-component.ts @@ -0,0 +1,60 @@ +import {Component, HostBinding, HostListener} from '@angular/core'; + +@Component({ + selector: 'main', + template: ` +

Main Component

+
Hello {{username}} from Angular 2!
+
+ +
{{counter}}
+ +
{{asyncCounter}}
+ +
Input:{{input}}
+ + + + ` +}) + +export class MainComponent { + username: string; + counter: number; + asyncCounter: number; + // TODO: remove '!'. + input!: string; + memo: string; + testTools: string[]; + testMethods: string[]; + // TODO: remove '!'. + @HostBinding('class.hovering') private isHovering!: boolean; + + @HostListener('mouseenter') + onMouseOver() { + this.isHovering = true; + } + + @HostListener('mouseout') + onMouseOut() { + this.isHovering = false; + } + constructor() { + console.log('Ng2Component instantiated.'); + 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; + }, 1000); + } + click() { + this.counter++; + setTimeout(() => { + this.asyncCounter++; + }, 500); + } +} diff --git a/src/cdk-experimental/testing/test-app/main.ts b/src/cdk-experimental/testing/test-app/main.ts new file mode 100644 index 000000000000..81d2aa7b15f7 --- /dev/null +++ b/src/cdk-experimental/testing/test-app/main.ts @@ -0,0 +1,4 @@ +import {platformBrowser} from '@angular/platform-browser'; +import {AppModuleNgFactory} from './app.ngfactory'; + +platformBrowser().bootstrapModuleFactory(AppModuleNgFactory); diff --git a/src/cdk-experimental/testing/test-app/protractor.e2e.spec.ts b/src/cdk-experimental/testing/test-app/protractor.e2e.spec.ts new file mode 100644 index 000000000000..49b1073f3d76 --- /dev/null +++ b/src/cdk-experimental/testing/test-app/protractor.e2e.spec.ts @@ -0,0 +1,183 @@ +import {getElementFinder, load} from '../protractor'; +import {browser, by, element} from 'protractor'; + +import {MainComponentHarness} from 'harnesses/main-component-harness'; + +describe('Protractor Helper Test:', () => { + let harness: MainComponentHarness; + + beforeEach(async () => { + await browser.get(browser.params.testUrl); + harness = await load(MainComponentHarness, '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()).toEqual('Main Component'); + }); + + it('should be able to locate all elements based on css selector', + async () => { + const labels = await harness.allLabels(); + expect(labels.length).toEqual(2); + expect(await labels[0].text()).toEqual('Count:'); + expect(await labels[1].text()).toEqual('AsyncCounter:'); + }); + + it('should be able to locate the sub harnesses', async () => { + const items = await harness.getTestTools(); + expect(items.length).toEqual(3); + expect(await items[0].text()).toEqual('Protractor'); + expect(await items[1].text()).toEqual('TestBed'); + expect(await items[2].text()).toEqual('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).toEqual(2); + expect(items1.length).toEqual(3); + expect(await items1[0].text()).toEqual('Protractor'); + expect(await items1[1].text()).toEqual('TestBed'); + expect(await items1[2].text()).toEqual('Other'); + expect(items2.length).toEqual(3); + expect(await items2[0].text()).toEqual('Unit Test'); + expect(await items2[1].text()).toEqual('Integration Test'); + expect(await items2[2].text()).toEqual('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')).toEqual('Yi'); + + await input.clear(); + expect(await input.getAttribute('value')).toEqual(''); + }); + + it('should be able to click', async () => { + const counter = await harness.counter(); + expect(await counter.text()).toEqual('0'); + await harness.increaseCounter(3); + expect(await counter.text()).toEqual('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')).toEqual('Yi'); + expect(await value.text()).toEqual('Input:Yi'); + }); + + it('focuses the element before sending key', async () => { + const input = await harness.input(); + await input.sendKeys('Yi'); + expect(await input.getAttribute('id')) + .toEqual(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')).toEqual(memoStr); + }); + + it('should be able to getCssValue', async () => { + const title = await harness.title(); + expect(await title.getCssValue('height')).toEqual('50px'); + }); + }); + + describe('Async operation ', () => { + it('should wait for async opeartion to complete', async () => { + const asyncCounter = await harness.asyncCounter(); + expect(await asyncCounter.text()).toEqual('5'); + await harness.increaseCounter(3); + expect(await asyncCounter.text()).toEqual('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) + .toEqual( + '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) + .toEqual( + '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('main')); + const elementFromHarness = getElementFinder(harness.host()); + expect(await elementFromHarness.getId()) + .toEqual(await mainElement.getId()); + }); + }); +}); diff --git a/src/cdk-experimental/testing/test-app/sub-component.ts b/src/cdk-experimental/testing/test-app/sub-component.ts new file mode 100644 index 000000000000..abebe0cfc12d --- /dev/null +++ b/src/cdk-experimental/testing/test-app/sub-component.ts @@ -0,0 +1,17 @@ +import {Component, Input} from '@angular/core'; + +@Component({ + selector: 'sub', + template: ` +

List of {{title}}

+
    +
  • {{item}}
  • +
` +}) + +export class SubComponent { + // TODO: remove '!'. + @Input() title!: string; + // TODO: remove '!'. + @Input() items!: string[]; +} diff --git a/src/cdk-experimental/testing/test-app/testbed.spec.ts b/src/cdk-experimental/testing/test-app/testbed.spec.ts new file mode 100644 index 000000000000..9d73fb84e130 --- /dev/null +++ b/src/cdk-experimental/testing/test-app/testbed.spec.ts @@ -0,0 +1,158 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {getNativeElement, load} from '../testbed'; + +import {TestAppModule} from './test-app'; +import {TestAppModuleNgSummary} from './test-app.ngsummary'; +import {MainComponent} from './main-component'; +import {MainComponentHarness} from './harnesses/main-component-harness'; + +describe('Testbed Helper Test:', () => { + let harness: MainComponentHarness; + let fixture: ComponentFixture<{}>; + beforeEach(async(() => { + TestBed + .configureTestingModule({ + imports: [TestAppModule], + aotSummaries: TestAppModuleNgSummary, + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(MainComponent); + 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()).toEqual('Main Component'); + }); + + it('should be able to locate all elements based on css selector', + async () => { + const labels = await harness.allLabels(); + expect(labels.length).toEqual(2); + expect(await labels[0].text()).toEqual('Count:'); + expect(await labels[1].text()).toEqual('AsyncCounter:'); + }); + + it('should be able to locate the sub harnesses', async () => { + const items = await harness.getTestTools(); + expect(items.length).toEqual(3); + expect(await items[0].text()).toEqual('Protractor'); + expect(await items[1].text()).toEqual('TestBed'); + expect(await items[2].text()).toEqual('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).toEqual(2); + expect(items1.length).toEqual(3); + expect(await items1[0].text()).toEqual('Protractor'); + expect(await items1[1].text()).toEqual('TestBed'); + expect(await items1[2].text()).toEqual('Other'); + expect(items2.length).toEqual(3); + expect(await items2[0].text()).toEqual('Unit Test'); + expect(await items2[1].text()).toEqual('Integration Test'); + expect(await items2[2].text()).toEqual('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')).toEqual('Yi'); + + await input.clear(); + expect(await input.getAttribute('value')).toEqual(''); + }); + + it('should be able to click', async () => { + const counter = await harness.counter(); + expect(await counter.text()).toEqual('0'); + await harness.increaseCounter(3); + expect(await counter.text()).toEqual('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')).toEqual('Yi'); + expect(await value.text()).toEqual('Input:Yi'); + }); + + it('focuses the element before sending key', async () => { + const input = await harness.input(); + await input.sendKeys('Yi'); + expect(await input.getAttribute('id')) + .toEqual(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')).toEqual(memoStr); + }); + + it('should be able to getCssValue', async () => { + const title = await harness.title(); + expect(await title.getCssValue('height')).toEqual('50px'); + }); + }); + + describe('Async operation ', () => { + it('should wait for async opeartion to complete', async () => { + const asyncCounter = await harness.asyncCounter(); + expect(await asyncCounter.text()).toEqual('5'); + await harness.increaseCounter(3); + expect(await asyncCounter.text()).toEqual('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) + .toEqual( + 'Cannot find element based on the css selector: wrong locator'); + } + }); + }); + + describe('getNativeElement', () => { + it('should return the native element', async () => { + expect(getNativeElement(harness.host())).toEqual(fixture.nativeElement); + }); + }); +}); diff --git a/src/cdk-experimental/testing/testbed.ts b/src/cdk-experimental/testing/testbed.ts new file mode 100644 index 000000000000..3e0fc1a2f2e8 --- /dev/null +++ b/src/cdk-experimental/testing/testbed.ts @@ -0,0 +1,198 @@ +import {ComponentFixture} from '@angular/core/testing'; + +import {ComponentHarness, ComponentHarnessType, Locator, Options, TestElement} from './component-harness'; + +/** + * 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: ComponentHarnessType, + fixture: ComponentFixture<{}>): T { + const root = fixture.nativeElement; + const stabilize = async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }; + const locator = new UnitTestLocator(root, stabilize); + return new componentHarness(locator); +} + +/** + * Gets the corresponding Element for the root of a TestElement. + */ +export function getNativeElement(testElement: TestElement): Element { + if (testElement instanceof UnitTestElement) { + return testElement.element; + } + + throw new Error('Invalid element provided'); +} + +/** + * Locator implementation for testbed. + * Note that, this locator is exposed for internal usage, please do not use it. + */ +export class UnitTestLocator implements Locator { + private rootElement: TestElement; + constructor(private root: Element, private stabilize: () => Promise) { + this.rootElement = new UnitTestElement(root, this.stabilize); + } + + host(): TestElement { + return this.rootElement; + } + + async find(css: string, options?: Options): Promise { + await this.stabilize(); + const e = getElement(css, this.root, options); + if (e === null) return null; + return new UnitTestElement(e, this.stabilize); + } + + async findAll(css: string): Promise { + await this.stabilize(); + const elements = this.root.querySelectorAll(css); + const res: TestElement[] = []; + for (let i = 0; i < elements.length; i++) { + res.push(new UnitTestElement(elements[i], this.stabilize)); + } + return res; + } + + async load( + componentHarness: ComponentHarnessType, css: string, + options?: Options): Promise { + const root = getElement(css, this.root, options); + if (root === null) { + return null; + } + const stabilize = this.stabilize; + const locator = new UnitTestLocator(root, stabilize); + return new componentHarness(locator); + } + + async loadAll( + componentHarness: ComponentHarnessType, + rootSelector: string): Promise { + await this.stabilize(); + const roots = this.root.querySelectorAll(rootSelector); + const res: T[] = []; + for (let i = 0; i < roots.length; i++) { + const root = roots[i]; + const stabilize = this.stabilize; + const locator = new UnitTestLocator(root, stabilize); + res.push(new componentHarness(locator)); + } + return res; + } +} + +class UnitTestElement implements TestElement { + constructor( + readonly element: Element, private stabilize: () => Promise) {} + + async blur(): Promise { + await this.stabilize(); + (this.element as HTMLElement).blur(); + await this.stabilize(); + } + + async clear(): Promise { + await this.stabilize(); + if (!(this.element instanceof HTMLInputElement || + this.element instanceof HTMLTextAreaElement)) { + throw new Error('Attempting to clear an invalid element'); + } + this.element.focus(); + this.element.value = ''; + this.element.dispatchEvent(new Event('input')); + await this.stabilize(); + } + + async click(): Promise { + await this.stabilize(); + (this.element as HTMLElement).click(); + await this.stabilize(); + } + + async focus(): Promise { + await this.stabilize(); + (this.element as HTMLElement).focus(); + await this.stabilize(); + } + + async getCssValue(property: string): Promise { + await this.stabilize(); + return Promise.resolve( + getComputedStyle(this.element).getPropertyValue(property)); + } + + async hover(): Promise { + await this.stabilize(); + this.element.dispatchEvent(new Event('mouseenter')); + await this.stabilize(); + } + + async sendKeys(keys: string): Promise { + await this.stabilize(); + (this.element as HTMLElement).focus(); + const e = this.element as HTMLInputElement; + for (const key of keys) { + const eventParams = {key, char: key, keyCode: key.charCodeAt(0)}; + e.dispatchEvent(new KeyboardEvent('keydown', eventParams)); + e.dispatchEvent(new KeyboardEvent('keypress', eventParams)); + e.value += key; + e.dispatchEvent(new KeyboardEvent('keyup', eventParams)); + e.dispatchEvent(new Event('input')); + } + await this.stabilize(); + } + + async text(): Promise { + await this.stabilize(); + return Promise.resolve(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) { + if (name in this.element) { + // tslint:disable-next-line:no-any handle unnecessary compile error + value = (this.element as any)[name]; + } + } + return value; + } +} + + +/** + * Get an element based on the css selector and root element. + * @param css 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(css: string, root: Element, options?: Options): Element| + null { + const useGlobalRoot = options && !!options.global; + const elem = (useGlobalRoot ? document : root).querySelector(css); + const allowNull = options !== undefined && options.allowNull !== undefined ? + options.allowNull : + undefined; + if (elem === null) { + if (allowNull) { + return null; + } + throw new Error('Cannot find element based on the css selector: ' + css); + } + return elem; +} From 3ecb9fc95887fbe00b960da8deefb6402d0ad816 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 21 May 2019 14:06:13 -0700 Subject: [PATCH 2/8] make sure code compiles and runs --- .github/CODEOWNERS | 3 +- src/cdk-experimental/testing/BUILD.bazel | 34 +++++++ .../testing/component-harness.ts | 11 ++- src/cdk-experimental/testing/index.ts | 9 ++ .../testing/protractor.conf.js | 12 +++ src/cdk-experimental/testing/protractor.ts | 48 +++++++--- src/cdk-experimental/testing/public-api.ts | 13 +++ .../testing/start-devserver.js | 42 +++++++++ .../testing/test-app/BUILD.bazel | 77 ++++++++++++++++ .../testing/test-app/devserver-configure.js | 6 ++ .../harnesses/main-component-harness.ts | 18 ++-- .../testing/test-app/index.html | 18 ++-- .../testing/test-app/main-component.ts | 39 ++++++-- src/cdk-experimental/testing/test-app/main.ts | 4 +- .../testing/test-app/protractor.e2e.spec.ts | 6 +- .../testing/test-app/sub-component.ts | 16 +++- .../testing/test-app/test-app-types.d.ts | 9 ++ .../testing/test-app/{app.ts => test-app.ts} | 10 ++- .../testing/test-app/testbed.spec.ts | 2 - .../testing/test-app/tsconfig.json | 8 ++ src/cdk-experimental/testing/testbed.ts | 89 +++++++++++-------- .../testing/tsconfig-build.json | 14 +++ test/karma-system-config.js | 3 +- tools/package-tools/rollup-globals.ts | 2 + 24 files changed, 412 insertions(+), 81 deletions(-) create mode 100644 src/cdk-experimental/testing/BUILD.bazel create mode 100644 src/cdk-experimental/testing/index.ts create mode 100644 src/cdk-experimental/testing/protractor.conf.js create mode 100644 src/cdk-experimental/testing/public-api.ts create mode 100644 src/cdk-experimental/testing/start-devserver.js create mode 100644 src/cdk-experimental/testing/test-app/BUILD.bazel create mode 100644 src/cdk-experimental/testing/test-app/devserver-configure.js create mode 100644 src/cdk-experimental/testing/test-app/test-app-types.d.ts rename src/cdk-experimental/testing/test-app/{app.ts => test-app.ts} (63%) create mode 100644 src/cdk-experimental/testing/test-app/tsconfig.json create mode 100644 src/cdk-experimental/testing/tsconfig-build.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index aa3502f15385..03e378f5ef18 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/src/cdk-experimental/testing/BUILD.bazel b/src/cdk-experimental/testing/BUILD.bazel new file mode 100644 index 000000000000..80a3f52060c3 --- /dev/null +++ b/src/cdk-experimental/testing/BUILD.bazel @@ -0,0 +1,34 @@ +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", "test-app/**"]), + module_name = "@angular/cdk-experimental/testing", + deps = [ + "@npm//@angular/core", + "@npm//protractor", + ], +) + +ng_web_test_suite( + name = "unit_tests", + deps = ["//src/cdk-experimental/testing/test-app:test_app_test_sources"], +) + +protractor_web_test_suite( + name = "e2e_tests", + configuration = ":protractor.conf.js", + on_prepare = ":start-devserver.js", + server = "//src/cdk-experimental/testing/test-app:devserver", + deps = [ + "@npm//protractor", + "//src/cdk-experimental/testing/test-app:test_app_e2e_test_sources", + ], + data = [ + "@npm//@angular/bazel", + ], +) diff --git a/src/cdk-experimental/testing/component-harness.ts b/src/cdk-experimental/testing/component-harness.ts index 3634c386a4d8..c3ac52abda0e 100644 --- a/src/cdk-experimental/testing/component-harness.ts +++ b/src/cdk-experimental/testing/component-harness.ts @@ -1,3 +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 + */ + /** * Test Element interface * This is a wrapper of native element @@ -165,8 +173,7 @@ export class ComponentHarness { options?: Options): () => Promise { if (typeof cssOrComponentHarness === 'string') { const css = cssOrComponentHarness; - const options = cssOrOptions as Options; - return () => this.locator.find(css, options); + return () => this.locator.find(css, cssOrOptions as Options); } else { const componentHarness = cssOrComponentHarness; const css = cssOrOptions as string; diff --git a/src/cdk-experimental/testing/index.ts b/src/cdk-experimental/testing/index.ts new file mode 100644 index 000000000000..676ca90f1ffa --- /dev/null +++ b/src/cdk-experimental/testing/index.ts @@ -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'; diff --git a/src/cdk-experimental/testing/protractor.conf.js b/src/cdk-experimental/testing/protractor.conf.js new file mode 100644 index 000000000000..e701601a0fb7 --- /dev/null +++ b/src/cdk-experimental/testing/protractor.conf.js @@ -0,0 +1,12 @@ +exports.config = { + useAllAngular2AppRoots: true, + allScriptsTimeout: 120000, + getPageTimeout: 120000, + jasmineNodeOpts: { + defaultTimeoutInterval: 120000, + }, + + // Since we want to use async/await we don't want to mix up with selenium's promise + // manager. In order to enforce this, we disable the promise manager. + SELENIUM_PROMISE_MANAGER: false, +}; diff --git a/src/cdk-experimental/testing/protractor.ts b/src/cdk-experimental/testing/protractor.ts index 5df84b13f5eb..a2c9363030bb 100644 --- a/src/cdk-experimental/testing/protractor.ts +++ b/src/cdk-experimental/testing/protractor.ts @@ -1,7 +1,24 @@ -import {browser, by, element, ElementFinder} from 'protractor'; +/** + * @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 + */ + +// TODO(mmalerba): Should this file be part of `@angular/cdk-experimental/testing` or a separate +// package? It depends on protractor which we don't want to put in the deps for cdk-experimental. + +import {browser, by, element as protractorElement, ElementFinder} from 'protractor'; import {promise as wdpromise} from 'selenium-webdriver'; -import {ComponentHarness, ComponentHarnessType, Locator, Options, TestElement} from './component-harness'; +import { + ComponentHarness, + ComponentHarnessType, + Locator, + Options, + TestElement +} from './component-harness'; /** * Component harness factory for protractor. @@ -52,23 +69,26 @@ export function getElementFinder(testElement: TestElement): ElementFinder { } class ProtractorLocator implements Locator { - private root: ProtractorElement; - constructor(private rootFinder: ElementFinder) { - this.root = new ProtractorElement(this.rootFinder); + private _root: ProtractorElement; + + constructor(private _rootFinder: ElementFinder) { + this._root = new ProtractorElement(this._rootFinder); } host(): TestElement { - return this.root; + return this._root; } async find(css: string, options?: Options): Promise { - const finder = await getElement(css, this.rootFinder, options); - if (finder === null) return null; + const finder = await getElement(css, this._rootFinder, options); + if (finder === null) { + return null; + } return new ProtractorElement(finder); } async findAll(css: string): Promise { - const elementFinders = this.rootFinder.all(by.css(css)); + const elementFinders = this._rootFinder.all(by.css(css)); const res: TestElement[] = []; await elementFinders.each(el => { if (el) { @@ -81,8 +101,10 @@ class ProtractorLocator implements Locator { async load( componentHarness: ComponentHarnessType, css: string, options?: Options): Promise { - const root = await getElement(css, this.rootFinder, options); - if (root === null) return null; + const root = await getElement(css, this._rootFinder, options); + if (root === null) { + return null; + } const locator = new ProtractorLocator(root); return new componentHarness(locator); } @@ -91,7 +113,7 @@ class ProtractorLocator implements Locator { componentHarness: ComponentHarnessType, rootSelector: string, ): Promise { - const roots = this.rootFinder.all(by.css(rootSelector)); + const roots = this._rootFinder.all(by.css(rootSelector)); const res: T[] = []; await roots.each(el => { if (el) { @@ -163,7 +185,7 @@ function toPromise(p: wdpromise.Promise): Promise { async function getElement(css: string, root?: ElementFinder, options?: Options): Promise { const useGlobalRoot = options && !!options.global; - const elem = root === undefined || useGlobalRoot ? element(by.css(css)) : + const elem = root === undefined || useGlobalRoot ? protractorElement(by.css(css)) : root.element(by.css(css)); const allowNull = options !== undefined && options.allowNull !== undefined ? options.allowNull : diff --git a/src/cdk-experimental/testing/public-api.ts b/src/cdk-experimental/testing/public-api.ts new file mode 100644 index 000000000000..4eec6438e103 --- /dev/null +++ b/src/cdk-experimental/testing/public-api.ts @@ -0,0 +1,13 @@ +/** + * @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 {protractor, testbed}; diff --git a/src/cdk-experimental/testing/start-devserver.js b/src/cdk-experimental/testing/start-devserver.js new file mode 100644 index 000000000000..cee1c15f9cac --- /dev/null +++ b/src/cdk-experimental/testing/start-devserver.js @@ -0,0 +1,42 @@ +const protractor = require('protractor'); +const utils = require('@angular/bazel/protractor-utils'); +const spawn = require('child_process').spawn; + +/** + * Runs the specified server binary from a given workspace and waits for the server + * being ready. The server binary will be resolved from the runfiles. + */ +async function runBazelServer(workspace, serverPath, timeout) { + const serverBinary = require.resolve(`${workspace}/${serverPath}`); + const port = await utils.findFreeTcpPort(); + + // Start the Bazel server binary with a random free TCP port. + const serverProcess = spawn(serverBinary, ['-port', port], {stdio: 'inherit'}); + + // In case the process exited with an error, we want to propagate the error. + serverProcess.on('exit', exitCode => { + if (exitCode !== 0) { + throw new Error(`Server exited with error code: ${exitCode}`); + } + }); + + // Wait for the server to be bound to the given port. + await utils.waitForServer(port, timeout); + + return port; +} + +/** + * Called by Protractor before starting any tests. This is script is responsible for + * starting up the devserver and updating the Protractor base URL to the proper port. + */ +module.exports = async function(config) { + const port = await runBazelServer(config.workspace, config.server); + const baseUrl = `http://localhost:${port}`; + const processedConfig = await protractor.browser.getProcessedConfig(); + + // Update the protractor "baseUrl" to match the new random TCP port. We need random TCP ports + // because otherwise Bazel could not execute protractor tests concurrently. + protractor.browser.baseUrl = baseUrl; + processedConfig.baseUrl = baseUrl; +}; diff --git a/src/cdk-experimental/testing/test-app/BUILD.bazel b/src/cdk-experimental/testing/test-app/BUILD.bazel new file mode 100644 index 000000000000..7b2069fe0220 --- /dev/null +++ b/src/cdk-experimental/testing/test-app/BUILD.bazel @@ -0,0 +1,77 @@ +package(default_visibility=["//visibility:public"]) + +load("@npm_bazel_typescript//:defs.bzl", "ts_devserver") +load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ts_library") +load("//:packages.bzl", "ANGULAR_LIBRARY_UMDS") + +ng_module( + name = "test-app", + srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts", "harnesses/**"]), + assets = glob(["**/*.html", "**/*.css"], exclude = ["index.html"]), + deps = [ + "@npm//@angular/core", + "@npm//@angular/forms", + ], +) + +ts_devserver( + name = "devserver", + port = 4200, + # Name of the AMD module that should be required on page load. + entry_module = "angular_material/src/cdk-experimental/testing/test-app/main", + # Serving path of the bundle that serves all files specified in "deps" and "scripts". + serving_path = "/bundle.js", + # Root paths can be used simplify the loading of files from external Bazel repositories + # (such as the Bazel managed deps repository called "npm") + additional_root_paths = [ + "npm/node_modules", + ], + # Files which should be provided by Bazel when running the devserver. These are not + # automatically served, but can be loaded manually through HTTP requests. + static_files = [ + "@npm//zone.js", + "@npm//core-js", + ":index.html", + ], + # Scripts which will be included in the serving_path bundle after "require.js" has been + # loaded. + # TODO(jelbourn): remove UMDs from here once we don't have to manually include them + scripts = [ + ":devserver-configure.js", + "//tools/rxjs:rxjs_umd_modules", + "@npm//node_modules/tslib:tslib.js", + ] + ANGULAR_LIBRARY_UMDS, + # Dependencies that produce JavaScript output will be automatically included in the + # serving_path bundle + deps = [":test-app"], +) + +ng_module( + name = "test_app_test_harnesses", + srcs = glob(["harnesses/*.ts"]), + deps = [ + "//src/cdk-experimental/testing", + ], +) + +ng_test_library( + name = "test_app_test_sources", + srcs = glob(["**/*.spec.ts"], exclude=["**/*.e2e.spec.ts"]), + deps = [ + ":test-app", + ":test_app_test_harnesses", + ], +) + +ts_library( + name = "test_app_e2e_test_sources", + srcs = glob(["**/*.e2e.spec.ts"]), + tsconfig = ":tsconfig.json", + deps = [ + "@npm//@types/jasmine", + "@npm//@types/selenium-webdriver", + "@npm//protractor", + ":test_app_test_harnesses", + "//src/cdk-experimental/testing", + ], +) diff --git a/src/cdk-experimental/testing/test-app/devserver-configure.js b/src/cdk-experimental/testing/test-app/devserver-configure.js new file mode 100644 index 000000000000..28e9670df91e --- /dev/null +++ b/src/cdk-experimental/testing/test-app/devserver-configure.js @@ -0,0 +1,6 @@ +// We need to configure AMD modules which are not named because otherwise "require.js" is not +// able to resolve AMD imports to such modules. +require.config({}); + +// Workaround until https://github.com/angular/components/issues/13883 has been addressed. +var module = {id: ''}; diff --git a/src/cdk-experimental/testing/test-app/harnesses/main-component-harness.ts b/src/cdk-experimental/testing/test-app/harnesses/main-component-harness.ts index 85f93bd0d309..4e5c3b767a2c 100644 --- a/src/cdk-experimental/testing/test-app/harnesses/main-component-harness.ts +++ b/src/cdk-experimental/testing/test-app/harnesses/main-component-harness.ts @@ -1,3 +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 + */ + import {ComponentHarness, TestElement} from '../../component-harness'; import {SubComponentHarness} from './sub-component-harness'; @@ -23,23 +31,23 @@ export class MainComponentHarness extends ComponentHarness { readonly nullGlobalEl = this.find('wrong locator', {global: true, allowNull: true}); - private button = this.find('button'); - private testTools = this.find(SubComponentHarness, 'sub'); + private _button = this.find('button'); + private _testTools = this.find(SubComponentHarness, 'sub'); async increaseCounter(times: number) { - const button = await this.button(); + const button = await this._button(); for (let i = 0; i < times; i++) { await button.click(); } } async getTestTool(index: number): Promise { - const subComponent = await this.testTools(); + const subComponent = await this._testTools(); return subComponent.getItem(index); } async getTestTools(): Promise { - const subComponent = await this.testTools(); + const subComponent = await this._testTools(); return subComponent.getItems(); } } diff --git a/src/cdk-experimental/testing/test-app/index.html b/src/cdk-experimental/testing/test-app/index.html index 24164d7a48ec..0ac9353bbff4 100644 --- a/src/cdk-experimental/testing/test-app/index.html +++ b/src/cdk-experimental/testing/test-app/index.html @@ -1,12 +1,20 @@ - - + + + + + Test App - angular2 page -
+
Loading...
I am a sibling! + + + + + + + - diff --git a/src/cdk-experimental/testing/test-app/main-component.ts b/src/cdk-experimental/testing/test-app/main-component.ts index a08f48311aef..503b35aa0755 100644 --- a/src/cdk-experimental/testing/test-app/main-component.ts +++ b/src/cdk-experimental/testing/test-app/main-component.ts @@ -1,6 +1,20 @@ -import {Component, HostBinding, HostListener} from '@angular/core'; +/** + * @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: 'main', template: `

Main Component

@@ -15,7 +29,14 @@ import {Component, HostBinding, HostListener} from '@angular/core'; - ` + `, + host: { + '[class.hovering]': '_isHovering', + '(mouseenter)': 'onMouseOver()', + '(mouseout)': 'onMouseOut()', + }, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, }) export class MainComponent { @@ -28,18 +49,17 @@ export class MainComponent { testTools: string[]; testMethods: string[]; // TODO: remove '!'. - @HostBinding('class.hovering') private isHovering!: boolean; + _isHovering!: boolean; - @HostListener('mouseenter') onMouseOver() { - this.isHovering = true; + this._isHovering = true; } - @HostListener('mouseout') onMouseOut() { - this.isHovering = false; + this._isHovering = false; } - constructor() { + + constructor(private _cdr: ChangeDetectorRef) { console.log('Ng2Component instantiated.'); this.username = 'Yi'; this.counter = 0; @@ -49,12 +69,15 @@ export class MainComponent { 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); } } diff --git a/src/cdk-experimental/testing/test-app/main.ts b/src/cdk-experimental/testing/test-app/main.ts index 81d2aa7b15f7..a395f71b066b 100644 --- a/src/cdk-experimental/testing/test-app/main.ts +++ b/src/cdk-experimental/testing/test-app/main.ts @@ -1,4 +1,4 @@ import {platformBrowser} from '@angular/platform-browser'; -import {AppModuleNgFactory} from './app.ngfactory'; +import {TestAppModuleNgFactory} from './test-app.ngfactory'; -platformBrowser().bootstrapModuleFactory(AppModuleNgFactory); +platformBrowser().bootstrapModuleFactory(TestAppModuleNgFactory); diff --git a/src/cdk-experimental/testing/test-app/protractor.e2e.spec.ts b/src/cdk-experimental/testing/test-app/protractor.e2e.spec.ts index 49b1073f3d76..0120808b6999 100644 --- a/src/cdk-experimental/testing/test-app/protractor.e2e.spec.ts +++ b/src/cdk-experimental/testing/test-app/protractor.e2e.spec.ts @@ -1,13 +1,13 @@ -import {getElementFinder, load} from '../protractor'; import {browser, by, element} from 'protractor'; -import {MainComponentHarness} from 'harnesses/main-component-harness'; +import {getElementFinder, load} from '../protractor'; +import {MainComponentHarness} from './harnesses/main-component-harness'; describe('Protractor Helper Test:', () => { let harness: MainComponentHarness; beforeEach(async () => { - await browser.get(browser.params.testUrl); + await browser.get('/'); harness = await load(MainComponentHarness, 'main'); }); diff --git a/src/cdk-experimental/testing/test-app/sub-component.ts b/src/cdk-experimental/testing/test-app/sub-component.ts index abebe0cfc12d..08efb62fb813 100644 --- a/src/cdk-experimental/testing/test-app/sub-component.ts +++ b/src/cdk-experimental/testing/test-app/sub-component.ts @@ -1,14 +1,24 @@ -import {Component, Input} from '@angular/core'; +/** + * @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: 'sub', template: `

List of {{title}}

  • {{item}}
  • -
` + `, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, }) - export class SubComponent { // TODO: remove '!'. @Input() title!: string; diff --git a/src/cdk-experimental/testing/test-app/test-app-types.d.ts b/src/cdk-experimental/testing/test-app/test-app-types.d.ts new file mode 100644 index 000000000000..c92ee049c207 --- /dev/null +++ b/src/cdk-experimental/testing/test-app/test-app-types.d.ts @@ -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 + */ + +declare var module: { id: string }; diff --git a/src/cdk-experimental/testing/test-app/app.ts b/src/cdk-experimental/testing/test-app/test-app.ts similarity index 63% rename from src/cdk-experimental/testing/test-app/app.ts rename to src/cdk-experimental/testing/test-app/test-app.ts index f65bf661d480..9a1947a67be1 100644 --- a/src/cdk-experimental/testing/test-app/app.ts +++ b/src/cdk-experimental/testing/test-app/test-app.ts @@ -1,3 +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 + */ + import {NgModule} from '@angular/core'; import {FormsModule} from '@angular/forms'; import {BrowserModule} from '@angular/platform-browser'; @@ -11,5 +19,5 @@ import {SubComponent} from './sub-component'; bootstrap: [MainComponent], }) -export class AppModule { +export class TestAppModule { } diff --git a/src/cdk-experimental/testing/test-app/testbed.spec.ts b/src/cdk-experimental/testing/test-app/testbed.spec.ts index 9d73fb84e130..82fbd34ff583 100644 --- a/src/cdk-experimental/testing/test-app/testbed.spec.ts +++ b/src/cdk-experimental/testing/test-app/testbed.spec.ts @@ -2,7 +2,6 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {getNativeElement, load} from '../testbed'; import {TestAppModule} from './test-app'; -import {TestAppModuleNgSummary} from './test-app.ngsummary'; import {MainComponent} from './main-component'; import {MainComponentHarness} from './harnesses/main-component-harness'; @@ -13,7 +12,6 @@ describe('Testbed Helper Test:', () => { TestBed .configureTestingModule({ imports: [TestAppModule], - aotSummaries: TestAppModuleNgSummary, }) .compileComponents() .then(() => { diff --git a/src/cdk-experimental/testing/test-app/tsconfig.json b/src/cdk-experimental/testing/test-app/tsconfig.json new file mode 100644 index 000000000000..170ed637d77a --- /dev/null +++ b/src/cdk-experimental/testing/test-app/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "lib": ["es2015"], + "types": [ + "jasmine" + ] + } +} diff --git a/src/cdk-experimental/testing/testbed.ts b/src/cdk-experimental/testing/testbed.ts index 3e0fc1a2f2e8..0f14a107844a 100644 --- a/src/cdk-experimental/testing/testbed.ts +++ b/src/cdk-experimental/testing/testbed.ts @@ -1,6 +1,24 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// TODO(mmalerba): Should this file be part of `@angular/cdk-experimental/testing` or a separate +// package? It depends on `@angular/core/testing` which we don't want to put in the deps for +// cdk-experimental. + import {ComponentFixture} from '@angular/core/testing'; -import {ComponentHarness, ComponentHarnessType, Locator, Options, TestElement} from './component-harness'; +import { + ComponentHarness, + ComponentHarnessType, + Locator, + Options, + TestElement +} from './component-harness'; /** * Component harness factory for testbed. @@ -35,28 +53,31 @@ export function getNativeElement(testElement: TestElement): Element { * Note that, this locator is exposed for internal usage, please do not use it. */ export class UnitTestLocator implements Locator { - private rootElement: TestElement; - constructor(private root: Element, private stabilize: () => Promise) { - this.rootElement = new UnitTestElement(root, this.stabilize); + private _rootElement: TestElement; + + constructor(private _root: Element, private _stabilize: () => Promise) { + this._rootElement = new UnitTestElement(_root, this._stabilize); } host(): TestElement { - return this.rootElement; + return this._rootElement; } async find(css: string, options?: Options): Promise { - await this.stabilize(); - const e = getElement(css, this.root, options); - if (e === null) return null; - return new UnitTestElement(e, this.stabilize); + await this._stabilize(); + const e = getElement(css, this._root, options); + if (e === null) { + return null; + } + return new UnitTestElement(e, this._stabilize); } async findAll(css: string): Promise { - await this.stabilize(); - const elements = this.root.querySelectorAll(css); + await this._stabilize(); + const elements = this._root.querySelectorAll(css); const res: TestElement[] = []; for (let i = 0; i < elements.length; i++) { - res.push(new UnitTestElement(elements[i], this.stabilize)); + res.push(new UnitTestElement(elements[i], this._stabilize)); } return res; } @@ -64,25 +85,23 @@ export class UnitTestLocator implements Locator { async load( componentHarness: ComponentHarnessType, css: string, options?: Options): Promise { - const root = getElement(css, this.root, options); + const root = getElement(css, this._root, options); if (root === null) { return null; } - const stabilize = this.stabilize; - const locator = new UnitTestLocator(root, stabilize); + const locator = new UnitTestLocator(root, this._stabilize); return new componentHarness(locator); } async loadAll( componentHarness: ComponentHarnessType, rootSelector: string): Promise { - await this.stabilize(); - const roots = this.root.querySelectorAll(rootSelector); + await this._stabilize(); + const roots = this._root.querySelectorAll(rootSelector); const res: T[] = []; for (let i = 0; i < roots.length; i++) { const root = roots[i]; - const stabilize = this.stabilize; - const locator = new UnitTestLocator(root, stabilize); + const locator = new UnitTestLocator(root, this._stabilize); res.push(new componentHarness(locator)); } return res; @@ -91,16 +110,16 @@ export class UnitTestLocator implements Locator { class UnitTestElement implements TestElement { constructor( - readonly element: Element, private stabilize: () => Promise) {} + readonly element: Element, private _stabilize: () => Promise) {} async blur(): Promise { - await this.stabilize(); + await this._stabilize(); (this.element as HTMLElement).blur(); - await this.stabilize(); + await this._stabilize(); } async clear(): Promise { - await this.stabilize(); + await this._stabilize(); if (!(this.element instanceof HTMLInputElement || this.element instanceof HTMLTextAreaElement)) { throw new Error('Attempting to clear an invalid element'); @@ -108,35 +127,35 @@ class UnitTestElement implements TestElement { this.element.focus(); this.element.value = ''; this.element.dispatchEvent(new Event('input')); - await this.stabilize(); + await this._stabilize(); } async click(): Promise { - await this.stabilize(); + await this._stabilize(); (this.element as HTMLElement).click(); - await this.stabilize(); + await this._stabilize(); } async focus(): Promise { - await this.stabilize(); + await this._stabilize(); (this.element as HTMLElement).focus(); - await this.stabilize(); + await this._stabilize(); } async getCssValue(property: string): Promise { - await this.stabilize(); + await this._stabilize(); return Promise.resolve( getComputedStyle(this.element).getPropertyValue(property)); } async hover(): Promise { - await this.stabilize(); + await this._stabilize(); this.element.dispatchEvent(new Event('mouseenter')); - await this.stabilize(); + await this._stabilize(); } async sendKeys(keys: string): Promise { - await this.stabilize(); + await this._stabilize(); (this.element as HTMLElement).focus(); const e = this.element as HTMLInputElement; for (const key of keys) { @@ -147,16 +166,16 @@ class UnitTestElement implements TestElement { e.dispatchEvent(new KeyboardEvent('keyup', eventParams)); e.dispatchEvent(new Event('input')); } - await this.stabilize(); + await this._stabilize(); } async text(): Promise { - await this.stabilize(); + await this._stabilize(); return Promise.resolve(this.element.textContent || ''); } async getAttribute(name: string): Promise { - await this.stabilize(); + 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 diff --git a/src/cdk-experimental/testing/tsconfig-build.json b/src/cdk-experimental/testing/tsconfig-build.json new file mode 100644 index 000000000000..f080b9236e51 --- /dev/null +++ b/src/cdk-experimental/testing/tsconfig-build.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig-build", + "files": [ + "public-api.ts" + ], + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "strictMetadataEmit": true, + "flatModuleOutFile": "index.js", + "flatModuleId": "@angular/cdk-experimental/testing", + "skipTemplateCodegen": true, + "fullTemplateTypeCheck": true + } +} diff --git a/test/karma-system-config.js b/test/karma-system-config.js index 81d6e83e2288..e56c6cd7792c 100644 --- a/test/karma-system-config.js +++ b/test/karma-system-config.js @@ -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', diff --git a/tools/package-tools/rollup-globals.ts b/tools/package-tools/rollup-globals.ts index 4e349718b1ca..c076ec371de5 100644 --- a/tools/package-tools/rollup-globals.ts +++ b/tools/package-tools/rollup-globals.ts @@ -47,6 +47,8 @@ const rollupCdkExperimentalEntryPoints = /** Map of globals that are used inside of the different packages. */ export const rollupGlobals = { 'moment': 'moment', + 'protractor': 'protractor', + 'selenium-webdriver': 'selenium-webdriver', 'tslib': 'tslib', 'protractor': 'protractor', 'selenium-webdriver': 'selenium-webdriver', From 84a9acfa24cd38b6a69648e41afba4131d2ab43c Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 31 May 2019 09:16:57 -0700 Subject: [PATCH 3/8] merge component harness test-app into e2e-app --- src/cdk-experimental/testing/BUILD.bazel | 14 ++-- .../testing/protractor.conf.js | 12 --- .../testing/start-devserver.js | 42 ---------- .../testing/test-app/BUILD.bazel | 77 ------------------- .../testing/test-app/devserver-configure.js | 6 -- .../testing/test-app/index.html | 20 ----- src/cdk-experimental/testing/test-app/main.ts | 4 - .../testing/test-app/test-app.ts | 23 ------ .../testing/test-app/tsconfig.json | 8 -- .../testing/tests/BUILD.bazel | 39 ++++++++++ .../harnesses/main-component-harness.ts | 4 +- .../harnesses/sub-component-harness.ts | 0 .../test-app-types.d.ts => tests/index.ts} | 4 +- .../protractor.e2e.spec.ts | 6 +- .../testing/tests/test-components-module.ts | 20 +++++ .../test-main-component.ts} | 12 +-- .../test-sub-component.ts} | 4 +- .../{test-app => tests}/testbed.spec.ts | 10 +-- src/e2e-app/BUILD.bazel | 1 + .../component-harness-e2e-module.ts | 19 +++++ .../component-harness-e2e.ts | 10 +++ src/e2e-app/e2e-app/e2e-app-layout.html | 1 + src/e2e-app/e2e-app/routes.ts | 4 +- src/e2e-app/index.html | 1 + src/e2e-app/main-module.ts | 6 +- tools/package-tools/rollup-globals.ts | 2 - 26 files changed, 126 insertions(+), 223 deletions(-) delete mode 100644 src/cdk-experimental/testing/protractor.conf.js delete mode 100644 src/cdk-experimental/testing/start-devserver.js delete mode 100644 src/cdk-experimental/testing/test-app/BUILD.bazel delete mode 100644 src/cdk-experimental/testing/test-app/devserver-configure.js delete mode 100644 src/cdk-experimental/testing/test-app/index.html delete mode 100644 src/cdk-experimental/testing/test-app/main.ts delete mode 100644 src/cdk-experimental/testing/test-app/test-app.ts delete mode 100644 src/cdk-experimental/testing/test-app/tsconfig.json create mode 100644 src/cdk-experimental/testing/tests/BUILD.bazel rename src/cdk-experimental/testing/{test-app => tests}/harnesses/main-component-harness.ts (92%) rename src/cdk-experimental/testing/{test-app => tests}/harnesses/sub-component-harness.ts (100%) rename src/cdk-experimental/testing/{test-app/test-app-types.d.ts => tests/index.ts} (63%) rename src/cdk-experimental/testing/{test-app => tests}/protractor.e2e.spec.ts (97%) create mode 100644 src/cdk-experimental/testing/tests/test-components-module.ts rename src/cdk-experimental/testing/{test-app/main-component.ts => tests/test-main-component.ts} (84%) rename src/cdk-experimental/testing/{test-app/sub-component.ts => tests/test-sub-component.ts} (92%) rename src/cdk-experimental/testing/{test-app => tests}/testbed.spec.ts (95%) create mode 100644 src/e2e-app/component-harness/component-harness-e2e-module.ts create mode 100644 src/e2e-app/component-harness/component-harness-e2e.ts diff --git a/src/cdk-experimental/testing/BUILD.bazel b/src/cdk-experimental/testing/BUILD.bazel index 80a3f52060c3..139ee3c8c8e9 100644 --- a/src/cdk-experimental/testing/BUILD.bazel +++ b/src/cdk-experimental/testing/BUILD.bazel @@ -6,7 +6,7 @@ load("@npm_angular_bazel//:index.bzl", "protractor_web_test_suite") ng_module( name = "testing", - srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts", "test-app/**"]), + srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts", "tests/**"]), module_name = "@angular/cdk-experimental/testing", deps = [ "@npm//@angular/core", @@ -16,19 +16,21 @@ ng_module( ng_web_test_suite( name = "unit_tests", - deps = ["//src/cdk-experimental/testing/test-app:test_app_test_sources"], + deps = ["//src/cdk-experimental/testing/tests:unit_test_sources"], ) protractor_web_test_suite( name = "e2e_tests", - configuration = ":protractor.conf.js", - on_prepare = ":start-devserver.js", - server = "//src/cdk-experimental/testing/test-app:devserver", + tags = ["e2e"], + configuration = "//src/e2e-app:protractor.conf.js", + on_prepare = "//src/e2e-app:start-devserver.js", + server = "//src/e2e-app:devserver", deps = [ "@npm//protractor", - "//src/cdk-experimental/testing/test-app:test_app_e2e_test_sources", + "//src/cdk-experimental/testing/tests:e2e_test_sources", ], data = [ "@npm//@angular/bazel", + "//tools/axe-protractor", ], ) diff --git a/src/cdk-experimental/testing/protractor.conf.js b/src/cdk-experimental/testing/protractor.conf.js deleted file mode 100644 index e701601a0fb7..000000000000 --- a/src/cdk-experimental/testing/protractor.conf.js +++ /dev/null @@ -1,12 +0,0 @@ -exports.config = { - useAllAngular2AppRoots: true, - allScriptsTimeout: 120000, - getPageTimeout: 120000, - jasmineNodeOpts: { - defaultTimeoutInterval: 120000, - }, - - // Since we want to use async/await we don't want to mix up with selenium's promise - // manager. In order to enforce this, we disable the promise manager. - SELENIUM_PROMISE_MANAGER: false, -}; diff --git a/src/cdk-experimental/testing/start-devserver.js b/src/cdk-experimental/testing/start-devserver.js deleted file mode 100644 index cee1c15f9cac..000000000000 --- a/src/cdk-experimental/testing/start-devserver.js +++ /dev/null @@ -1,42 +0,0 @@ -const protractor = require('protractor'); -const utils = require('@angular/bazel/protractor-utils'); -const spawn = require('child_process').spawn; - -/** - * Runs the specified server binary from a given workspace and waits for the server - * being ready. The server binary will be resolved from the runfiles. - */ -async function runBazelServer(workspace, serverPath, timeout) { - const serverBinary = require.resolve(`${workspace}/${serverPath}`); - const port = await utils.findFreeTcpPort(); - - // Start the Bazel server binary with a random free TCP port. - const serverProcess = spawn(serverBinary, ['-port', port], {stdio: 'inherit'}); - - // In case the process exited with an error, we want to propagate the error. - serverProcess.on('exit', exitCode => { - if (exitCode !== 0) { - throw new Error(`Server exited with error code: ${exitCode}`); - } - }); - - // Wait for the server to be bound to the given port. - await utils.waitForServer(port, timeout); - - return port; -} - -/** - * Called by Protractor before starting any tests. This is script is responsible for - * starting up the devserver and updating the Protractor base URL to the proper port. - */ -module.exports = async function(config) { - const port = await runBazelServer(config.workspace, config.server); - const baseUrl = `http://localhost:${port}`; - const processedConfig = await protractor.browser.getProcessedConfig(); - - // Update the protractor "baseUrl" to match the new random TCP port. We need random TCP ports - // because otherwise Bazel could not execute protractor tests concurrently. - protractor.browser.baseUrl = baseUrl; - processedConfig.baseUrl = baseUrl; -}; diff --git a/src/cdk-experimental/testing/test-app/BUILD.bazel b/src/cdk-experimental/testing/test-app/BUILD.bazel deleted file mode 100644 index 7b2069fe0220..000000000000 --- a/src/cdk-experimental/testing/test-app/BUILD.bazel +++ /dev/null @@ -1,77 +0,0 @@ -package(default_visibility=["//visibility:public"]) - -load("@npm_bazel_typescript//:defs.bzl", "ts_devserver") -load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ts_library") -load("//:packages.bzl", "ANGULAR_LIBRARY_UMDS") - -ng_module( - name = "test-app", - srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts", "harnesses/**"]), - assets = glob(["**/*.html", "**/*.css"], exclude = ["index.html"]), - deps = [ - "@npm//@angular/core", - "@npm//@angular/forms", - ], -) - -ts_devserver( - name = "devserver", - port = 4200, - # Name of the AMD module that should be required on page load. - entry_module = "angular_material/src/cdk-experimental/testing/test-app/main", - # Serving path of the bundle that serves all files specified in "deps" and "scripts". - serving_path = "/bundle.js", - # Root paths can be used simplify the loading of files from external Bazel repositories - # (such as the Bazel managed deps repository called "npm") - additional_root_paths = [ - "npm/node_modules", - ], - # Files which should be provided by Bazel when running the devserver. These are not - # automatically served, but can be loaded manually through HTTP requests. - static_files = [ - "@npm//zone.js", - "@npm//core-js", - ":index.html", - ], - # Scripts which will be included in the serving_path bundle after "require.js" has been - # loaded. - # TODO(jelbourn): remove UMDs from here once we don't have to manually include them - scripts = [ - ":devserver-configure.js", - "//tools/rxjs:rxjs_umd_modules", - "@npm//node_modules/tslib:tslib.js", - ] + ANGULAR_LIBRARY_UMDS, - # Dependencies that produce JavaScript output will be automatically included in the - # serving_path bundle - deps = [":test-app"], -) - -ng_module( - name = "test_app_test_harnesses", - srcs = glob(["harnesses/*.ts"]), - deps = [ - "//src/cdk-experimental/testing", - ], -) - -ng_test_library( - name = "test_app_test_sources", - srcs = glob(["**/*.spec.ts"], exclude=["**/*.e2e.spec.ts"]), - deps = [ - ":test-app", - ":test_app_test_harnesses", - ], -) - -ts_library( - name = "test_app_e2e_test_sources", - srcs = glob(["**/*.e2e.spec.ts"]), - tsconfig = ":tsconfig.json", - deps = [ - "@npm//@types/jasmine", - "@npm//@types/selenium-webdriver", - "@npm//protractor", - ":test_app_test_harnesses", - "//src/cdk-experimental/testing", - ], -) diff --git a/src/cdk-experimental/testing/test-app/devserver-configure.js b/src/cdk-experimental/testing/test-app/devserver-configure.js deleted file mode 100644 index 28e9670df91e..000000000000 --- a/src/cdk-experimental/testing/test-app/devserver-configure.js +++ /dev/null @@ -1,6 +0,0 @@ -// We need to configure AMD modules which are not named because otherwise "require.js" is not -// able to resolve AMD imports to such modules. -require.config({}); - -// Workaround until https://github.com/angular/components/issues/13883 has been addressed. -var module = {id: ''}; diff --git a/src/cdk-experimental/testing/test-app/index.html b/src/cdk-experimental/testing/test-app/index.html deleted file mode 100644 index 0ac9353bbff4..000000000000 --- a/src/cdk-experimental/testing/test-app/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - Test App - - - -
Loading...
-I am a sibling! - - - - - - - - - diff --git a/src/cdk-experimental/testing/test-app/main.ts b/src/cdk-experimental/testing/test-app/main.ts deleted file mode 100644 index a395f71b066b..000000000000 --- a/src/cdk-experimental/testing/test-app/main.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {platformBrowser} from '@angular/platform-browser'; -import {TestAppModuleNgFactory} from './test-app.ngfactory'; - -platformBrowser().bootstrapModuleFactory(TestAppModuleNgFactory); diff --git a/src/cdk-experimental/testing/test-app/test-app.ts b/src/cdk-experimental/testing/test-app/test-app.ts deleted file mode 100644 index 9a1947a67be1..000000000000 --- a/src/cdk-experimental/testing/test-app/test-app.ts +++ /dev/null @@ -1,23 +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 {NgModule} from '@angular/core'; -import {FormsModule} from '@angular/forms'; -import {BrowserModule} from '@angular/platform-browser'; -import {MainComponent} from './main-component'; -import {SubComponent} from './sub-component'; - -@NgModule({ - imports: [FormsModule, BrowserModule], - declarations: [MainComponent, SubComponent], - exports: [MainComponent], - bootstrap: [MainComponent], -}) - -export class TestAppModule { -} diff --git a/src/cdk-experimental/testing/test-app/tsconfig.json b/src/cdk-experimental/testing/test-app/tsconfig.json deleted file mode 100644 index 170ed637d77a..000000000000 --- a/src/cdk-experimental/testing/test-app/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "lib": ["es2015"], - "types": [ - "jasmine" - ] - } -} diff --git a/src/cdk-experimental/testing/tests/BUILD.bazel b/src/cdk-experimental/testing/tests/BUILD.bazel new file mode 100644 index 000000000000..28d98ee5c944 --- /dev/null +++ b/src/cdk-experimental/testing/tests/BUILD.bazel @@ -0,0 +1,39 @@ +package(default_visibility=["//visibility:public"]) + +load("//tools:defaults.bzl", "ng_module", "ts_library", "ng_test_library", "ng_e2e_test_library") + +ng_module( + name = "test_components", + module_name = "@angular/cdk-experimental/testing/tests", + srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts", "harnesses/**"]), + 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", + ], +) diff --git a/src/cdk-experimental/testing/test-app/harnesses/main-component-harness.ts b/src/cdk-experimental/testing/tests/harnesses/main-component-harness.ts similarity index 92% rename from src/cdk-experimental/testing/test-app/harnesses/main-component-harness.ts rename to src/cdk-experimental/testing/tests/harnesses/main-component-harness.ts index 4e5c3b767a2c..9871bf648552 100644 --- a/src/cdk-experimental/testing/test-app/harnesses/main-component-harness.ts +++ b/src/cdk-experimental/testing/tests/harnesses/main-component-harness.ts @@ -16,7 +16,7 @@ export class MainComponentHarness extends ComponentHarness { readonly input = this.find('#input'); readonly value = this.find('#value'); readonly allLabels = this.findAll('label'); - readonly allLists = this.findAll(SubComponentHarness, 'sub'); + readonly allLists = this.findAll(SubComponentHarness, 'test-sub'); readonly memo = this.find('textarea'); // Allow null for element readonly nullItem = this.find('wrong locator', {allowNull: true}); @@ -32,7 +32,7 @@ export class MainComponentHarness extends ComponentHarness { this.find('wrong locator', {global: true, allowNull: true}); private _button = this.find('button'); - private _testTools = this.find(SubComponentHarness, 'sub'); + private _testTools = this.find(SubComponentHarness, 'test-sub'); async increaseCounter(times: number) { const button = await this._button(); diff --git a/src/cdk-experimental/testing/test-app/harnesses/sub-component-harness.ts b/src/cdk-experimental/testing/tests/harnesses/sub-component-harness.ts similarity index 100% rename from src/cdk-experimental/testing/test-app/harnesses/sub-component-harness.ts rename to src/cdk-experimental/testing/tests/harnesses/sub-component-harness.ts diff --git a/src/cdk-experimental/testing/test-app/test-app-types.d.ts b/src/cdk-experimental/testing/tests/index.ts similarity index 63% rename from src/cdk-experimental/testing/test-app/test-app-types.d.ts rename to src/cdk-experimental/testing/tests/index.ts index c92ee049c207..adfc214ca0e8 100644 --- a/src/cdk-experimental/testing/test-app/test-app-types.d.ts +++ b/src/cdk-experimental/testing/tests/index.ts @@ -6,4 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -declare var module: { id: string }; +export * from './test-components-module'; +export * from './test-main-component'; +export * from './test-sub-component'; diff --git a/src/cdk-experimental/testing/test-app/protractor.e2e.spec.ts b/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts similarity index 97% rename from src/cdk-experimental/testing/test-app/protractor.e2e.spec.ts rename to src/cdk-experimental/testing/tests/protractor.e2e.spec.ts index 0120808b6999..9217fc7bdafa 100644 --- a/src/cdk-experimental/testing/test-app/protractor.e2e.spec.ts +++ b/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts @@ -7,8 +7,8 @@ describe('Protractor Helper Test:', () => { let harness: MainComponentHarness; beforeEach(async () => { - await browser.get('/'); - harness = await load(MainComponentHarness, 'main'); + await browser.get('/component-harness'); + harness = await load(MainComponentHarness, 'test-main'); }); describe('Locator ', () => { @@ -174,7 +174,7 @@ describe('Protractor Helper Test:', () => { describe('getElementFinder', () => { it('should return the element finder', async () => { - const mainElement = await element(by.css('main')); + const mainElement = await element(by.css('test-main')); const elementFromHarness = getElementFinder(harness.host()); expect(await elementFromHarness.getId()) .toEqual(await mainElement.getId()); diff --git a/src/cdk-experimental/testing/tests/test-components-module.ts b/src/cdk-experimental/testing/tests/test-components-module.ts new file mode 100644 index 000000000000..045c8633cbc7 --- /dev/null +++ b/src/cdk-experimental/testing/tests/test-components-module.ts @@ -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 {} diff --git a/src/cdk-experimental/testing/test-app/main-component.ts b/src/cdk-experimental/testing/tests/test-main-component.ts similarity index 84% rename from src/cdk-experimental/testing/test-app/main-component.ts rename to src/cdk-experimental/testing/tests/test-main-component.ts index 503b35aa0755..51cbbb0074de 100644 --- a/src/cdk-experimental/testing/test-app/main-component.ts +++ b/src/cdk-experimental/testing/tests/test-main-component.ts @@ -15,7 +15,7 @@ import { @Component({ moduleId: module.id, - selector: 'main', + selector: 'test-main', template: `

Main Component

Hello {{username}} from Angular 2!
@@ -24,11 +24,11 @@ import {
{{counter}}
{{asyncCounter}}
- +
Input:{{input}}
- - - + + + `, host: { '[class.hovering]': '_isHovering', @@ -39,7 +39,7 @@ import { changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MainComponent { +export class TestMainComponent { username: string; counter: number; asyncCounter: number; diff --git a/src/cdk-experimental/testing/test-app/sub-component.ts b/src/cdk-experimental/testing/tests/test-sub-component.ts similarity index 92% rename from src/cdk-experimental/testing/test-app/sub-component.ts rename to src/cdk-experimental/testing/tests/test-sub-component.ts index 08efb62fb813..8f0ad6a7f5f7 100644 --- a/src/cdk-experimental/testing/test-app/sub-component.ts +++ b/src/cdk-experimental/testing/tests/test-sub-component.ts @@ -10,7 +10,7 @@ import {ChangeDetectionStrategy, Component, Input, ViewEncapsulation} from '@ang @Component({ moduleId: module.id, - selector: 'sub', + selector: 'test-sub', template: `

List of {{title}}

    @@ -19,7 +19,7 @@ import {ChangeDetectionStrategy, Component, Input, ViewEncapsulation} from '@ang encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class SubComponent { +export class TestSubComponent { // TODO: remove '!'. @Input() title!: string; // TODO: remove '!'. diff --git a/src/cdk-experimental/testing/test-app/testbed.spec.ts b/src/cdk-experimental/testing/tests/testbed.spec.ts similarity index 95% rename from src/cdk-experimental/testing/test-app/testbed.spec.ts rename to src/cdk-experimental/testing/tests/testbed.spec.ts index 82fbd34ff583..a5b102f207ed 100644 --- a/src/cdk-experimental/testing/test-app/testbed.spec.ts +++ b/src/cdk-experimental/testing/tests/testbed.spec.ts @@ -1,21 +1,21 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {getNativeElement, load} from '../testbed'; - -import {TestAppModule} from './test-app'; -import {MainComponent} from './main-component'; 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: [TestAppModule], + imports: [TestComponentsModule], }) .compileComponents() .then(() => { - fixture = TestBed.createComponent(MainComponent); + fixture = TestBed.createComponent(TestMainComponent); harness = load(MainComponentHarness, fixture); }); })); diff --git a/src/e2e-app/BUILD.bazel b/src/e2e-app/BUILD.bazel index 88b45bc19113..606b0aa0dcc5 100644 --- a/src/e2e-app/BUILD.bazel +++ b/src/e2e-app/BUILD.bazel @@ -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", diff --git a/src/e2e-app/component-harness/component-harness-e2e-module.ts b/src/e2e-app/component-harness/component-harness-e2e-module.ts new file mode 100644 index 000000000000..529de6ffce6b --- /dev/null +++ b/src/e2e-app/component-harness/component-harness-e2e-module.ts @@ -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 { +} diff --git a/src/e2e-app/component-harness/component-harness-e2e.ts b/src/e2e-app/component-harness/component-harness-e2e.ts new file mode 100644 index 000000000000..91f810691a53 --- /dev/null +++ b/src/e2e-app/component-harness/component-harness-e2e.ts @@ -0,0 +1,10 @@ +import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; + +@Component({ + moduleId: module.id, + selector: 'component-harness-e2e', + template: ``, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ComponentHarnessE2e {} diff --git a/src/e2e-app/e2e-app/e2e-app-layout.html b/src/e2e-app/e2e-app/e2e-app-layout.html index c2716b6dc415..0ce5d0dc8956 100644 --- a/src/e2e-app/e2e-app/e2e-app-layout.html +++ b/src/e2e-app/e2e-app/e2e-app-layout.html @@ -5,6 +5,7 @@ Button Button Toggle Checkbox + Component Harness Dialog Expansion Grid list diff --git a/src/e2e-app/e2e-app/routes.ts b/src/e2e-app/e2e-app/routes.ts index a6d14b935578..fe1f7b9520e1 100644 --- a/src/e2e-app/e2e-app/routes.ts +++ b/src/e2e-app/e2e-app/routes.ts @@ -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}, ]; diff --git a/src/e2e-app/index.html b/src/e2e-app/index.html index 657bf35d0446..cd9a8ca410b5 100644 --- a/src/e2e-app/index.html +++ b/src/e2e-app/index.html @@ -24,6 +24,7 @@ Loading... + I am a sibling! diff --git a/src/e2e-app/main-module.ts b/src/e2e-app/main-module.ts index 6e10b7b4a300..228999c685d3 100644 --- a/src/e2e-app/main-module.ts +++ b/src/e2e-app/main-module.ts @@ -2,13 +2,12 @@ import {NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {RouterModule} from '@angular/router'; -import { - BlockScrollStrategyE2eModule -} from './block-scroll-strategy/block-scroll-strategy-e2e-module'; +import {BlockScrollStrategyE2eModule} from './block-scroll-strategy/block-scroll-strategy-e2e-module'; 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 +48,7 @@ import {VirtualScrollE2eModule} from './virtual-scroll/virtual-scroll-e2e-module ButtonToggleE2eModule, CardE2eModule, CheckboxE2eModule, + ComponentHarnessE2eModule, DialogE2eModule, ExpansionE2eModule, GridListE2eModule, diff --git a/tools/package-tools/rollup-globals.ts b/tools/package-tools/rollup-globals.ts index c076ec371de5..ac4b99811280 100644 --- a/tools/package-tools/rollup-globals.ts +++ b/tools/package-tools/rollup-globals.ts @@ -50,8 +50,6 @@ export const rollupGlobals = { 'protractor': 'protractor', 'selenium-webdriver': 'selenium-webdriver', 'tslib': 'tslib', - 'protractor': 'protractor', - 'selenium-webdriver': 'selenium-webdriver', // MDC Web '@material/animation': 'mdc.animation', From ee8140576c0e4c58b1000cfe969bee2b3451e6ff Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 31 May 2019 11:42:20 -0700 Subject: [PATCH 4/8] address @crisbeto's feedback --- .github/CODEOWNERS | 1 + src/cdk-experimental/testing/BUILD.bazel | 1 + .../testing/component-harness.ts | 72 +++++----- src/cdk-experimental/testing/protractor.ts | 124 +++++++---------- src/cdk-experimental/testing/testbed.ts | 131 ++++++++---------- .../testing/tests/BUILD.bazel | 1 + .../testing/tests/protractor.e2e.spec.ts | 82 +++++------ .../testing/tests/test-main-component.html | 12 ++ .../testing/tests/test-main-component.ts | 16 +-- .../testing/tests/testbed.spec.ts | 78 +++++------ .../testing/tsconfig-build.json | 14 -- src/cdk/testing/element-focus.ts | 19 +++ src/e2e-app/main-module.ts | 4 +- 13 files changed, 262 insertions(+), 293 deletions(-) create mode 100644 src/cdk-experimental/testing/tests/test-main-component.html delete mode 100644 src/cdk-experimental/testing/tsconfig-build.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 03e378f5ef18..84bd387d3d0b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -178,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 diff --git a/src/cdk-experimental/testing/BUILD.bazel b/src/cdk-experimental/testing/BUILD.bazel index 139ee3c8c8e9..f1d78d0c37b6 100644 --- a/src/cdk-experimental/testing/BUILD.bazel +++ b/src/cdk-experimental/testing/BUILD.bazel @@ -11,6 +11,7 @@ ng_module( deps = [ "@npm//@angular/core", "@npm//protractor", + "//src/cdk/testing", ], ) diff --git a/src/cdk-experimental/testing/component-harness.ts b/src/cdk-experimental/testing/component-harness.ts index c3ac52abda0e..c3c1550985fe 100644 --- a/src/cdk-experimental/testing/component-harness.ts +++ b/src/cdk-experimental/testing/component-harness.ts @@ -68,21 +68,21 @@ export interface Locator { /** * Search the first matched test element. - * @param css Selector of the test elements. + * @param selector The CSS selector of the test elements. * @param options Optional, extra searching options */ - find(css: string, options?: Options): Promise; + querySelector(selector: string, options?: Options): Promise; /** - * Search all matched test elements under current root by css selector. - * @param css Selector of the test elements. + * Search all matched test elements under current root by CSS selector. + * @param selector The CSS selector of the test elements. */ - findAll(css: string): Promise; + querySelectorAll(selector: string): Promise; /** * Load the first matched Component Harness. * @param componentHarness Type of user customized harness. - * @param root Css root selector of the new component harness. + * @param root CSS root selector of the new component harness. * @param options Optional, extra searching options */ load( @@ -92,7 +92,7 @@ export interface Locator { /** * Load all Component Harnesses under current root. * @param componentHarness Type of user customized harness. - * @param root Css root selector of the new component harnesses. + * @param root CSS root selector of the new component harnesses. */ loadAll( componentHarness: ComponentHarnessType, root: string): Promise; @@ -115,33 +115,33 @@ export class ComponentHarness { } /** - * Generate a function to find the first matched test element by css + * Generate a function to find the first matched test element by CSS * selector. - * @param css Css selector of the test element. + * @param selector The CSS selector of the test element. */ - protected find(css: string): () => Promise; + protected find(selector: string): () => Promise; /** - * Generate a function to find the first matched test element by css + * Generate a function to find the first matched test element by CSS * selector. - * @param css Css selector of the test element. + * @param selector The CSS selector of the test element. * @param options Extra searching options */ - protected find(css: string, options: OptionsWithAllowNullSet): + protected find(selector: string, options: OptionsWithAllowNullSet): () => Promise; /** - * Generate a function to find the first matched test element by css + * Generate a function to find the first matched test element by CSS * selector. - * @param css Css selector of the test element. + * @param selector The CSS selector of the test element. * @param options Extra searching options */ - protected find(css: string, options: Options): () => Promise; + protected find(selector: string, options: Options): () => Promise; /** * 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 root CSS root selector of the new component harness. */ protected find( componentHarness: ComponentHarnessType, @@ -150,7 +150,7 @@ export class ComponentHarness { /** * 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 root CSS root selector of the new component harness. * @param options Extra searching options */ protected find( @@ -160,7 +160,7 @@ export class ComponentHarness { /** * 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 root CSS root selector of the new component harness. * @param options Extra searching options */ protected find( @@ -168,31 +168,31 @@ export class ComponentHarness { options: Options): () => Promise; protected find( - cssOrComponentHarness: string|ComponentHarnessType, - cssOrOptions?: string|Options, + selectorOrComponentHarness: string|ComponentHarnessType, + selectorOrOptions?: string|Options, options?: Options): () => Promise { - if (typeof cssOrComponentHarness === 'string') { - const css = cssOrComponentHarness; - return () => this.locator.find(css, cssOrOptions as Options); + if (typeof selectorOrComponentHarness === 'string') { + const selector = selectorOrComponentHarness; + return () => this.locator.querySelector(selector, selectorOrOptions as Options); } else { - const componentHarness = cssOrComponentHarness; - const css = cssOrOptions as string; - return () => this.locator.load(componentHarness, css, options); + 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 css Css root selector of elements. It will locate + * 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(css: string): () => Promise; + protected findAll(selector: string): () => Promise; /** * 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 + * @param root CSS root selector of the new component harnesses. It will * locate harnesses under the current root. */ protected findAll( @@ -200,13 +200,13 @@ export class ComponentHarness { root: string): () => Promise; protected findAll( - cssOrComponentHarness: string|ComponentHarnessType, + selectorOrComponentHarness: string|ComponentHarnessType, root?: string): () => Promise { - if (typeof cssOrComponentHarness === 'string') { - const css = cssOrComponentHarness; - return () => this.locator.findAll(css); + if (typeof selectorOrComponentHarness === 'string') { + const selector = selectorOrComponentHarness; + return () => this.locator.querySelectorAll(selector); } else { - const componentHarness = cssOrComponentHarness; + const componentHarness = selectorOrComponentHarness; return () => this.locator.loadAll(componentHarness, root as string); } } diff --git a/src/cdk-experimental/testing/protractor.ts b/src/cdk-experimental/testing/protractor.ts index a2c9363030bb..ea0a3712a1e9 100644 --- a/src/cdk-experimental/testing/protractor.ts +++ b/src/cdk-experimental/testing/protractor.ts @@ -10,7 +10,6 @@ // package? It depends on protractor which we don't want to put in the deps for cdk-experimental. import {browser, by, element as protractorElement, ElementFinder} from 'protractor'; -import {promise as wdpromise} from 'selenium-webdriver'; import { ComponentHarness, @@ -28,33 +27,29 @@ import { * 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. + * @param rootSelector: Optional. CSS selector to specify the root of component. * Set to 'body' by default */ export async function load( - componentHarness: ComponentHarnessType, - rootSelector: string): Promise; + componentHarness: ComponentHarnessType, + 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. + * @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: ComponentHarnessType, rootSelector?: string, - options?: Options): Promise; + componentHarness: ComponentHarnessType, rootSelector?: string, + options?: Options): Promise; export async function load( - componentHarness: ComponentHarnessType, rootSelector = 'body', - options?: Options): Promise { + componentHarness: ComponentHarnessType, rootSelector = 'body', + options?: Options): Promise { const root = await getElement(rootSelector, undefined, options); - if (root === null) { - return null; - } - const locator = new ProtractorLocator(root); - return new componentHarness(locator); + return root && new componentHarness(new ProtractorLocator(root)); } /** @@ -69,7 +64,7 @@ export function getElementFinder(testElement: TestElement): ElementFinder { } class ProtractorLocator implements Locator { - private _root: ProtractorElement; + private readonly _root: ProtractorElement; constructor(private _rootFinder: ElementFinder) { this._root = new ProtractorElement(this._rootFinder); @@ -79,16 +74,13 @@ class ProtractorLocator implements Locator { return this._root; } - async find(css: string, options?: Options): Promise { - const finder = await getElement(css, this._rootFinder, options); - if (finder === null) { - return null; - } - return new ProtractorElement(finder); + async querySelector(selector: string, options?: Options): Promise { + const finder = await getElement(selector, this._rootFinder, options); + return finder && new ProtractorElement(finder); } - async findAll(css: string): Promise { - const elementFinders = this._rootFinder.all(by.css(css)); + async querySelectorAll(selector: string): Promise { + const elementFinders = this._rootFinder.all(by.css(selector)); const res: TestElement[] = []; await elementFinders.each(el => { if (el) { @@ -99,20 +91,15 @@ class ProtractorLocator implements Locator { } async load( - componentHarness: ComponentHarnessType, css: string, - options?: Options): Promise { - const root = await getElement(css, this._rootFinder, options); - if (root === null) { - return null; - } - const locator = new ProtractorLocator(root); - return new componentHarness(locator); + componentHarness: ComponentHarnessType, selector: string, + options?: Options): Promise { + const root = await getElement(selector, this._rootFinder, options); + return root && new componentHarness(new ProtractorLocator(root)); } async loadAll( - componentHarness: ComponentHarnessType, - rootSelector: string, - ): Promise { + componentHarness: ComponentHarnessType, + rootSelector: string): Promise { const roots = this._rootFinder.all(by.css(rootSelector)); const res: T[] = []; await roots.each(el => { @@ -128,77 +115,66 @@ class ProtractorLocator implements Locator { class ProtractorElement implements TestElement { constructor(readonly element: ElementFinder) {} - blur(): Promise { - return toPromise(this.element['blur']()); + async blur(): Promise { + return this.element['blur'](); } - clear(): Promise { - return toPromise(this.element.clear()); + async clear(): Promise { + return this.element.clear(); } - click(): Promise { - return toPromise(this.element.click()); + async click(): Promise { + return this.element.click(); } - focus(): Promise { - return toPromise(this.element['focus']()); + async focus(): Promise { + return this.element['focus'](); } - getCssValue(property: string): Promise { - return toPromise(this.element.getCssValue(property)); + async getCssValue(property: string): Promise { + return this.element.getCssValue(property); } async hover(): Promise { - return toPromise(browser.actions() - .mouseMove(await this.element.getWebElement()) - .perform()); + return browser.actions() + .mouseMove(await this.element.getWebElement()) + .perform(); } - sendKeys(keys: string): Promise { - return toPromise(this.element.sendKeys(keys)); + async sendKeys(keys: string): Promise { + return this.element.sendKeys(keys); } - text(): Promise { - return toPromise(this.element.getText()); + async text(): Promise { + return this.element.getText(); } - getAttribute(name: string): Promise { - return toPromise(this.element.getAttribute(name)); + async getAttribute(name: string): Promise { + return this.element.getAttribute(name); } } -function toPromise(p: wdpromise.Promise): Promise { - return new Promise((resolve, reject) => { - p.then(resolve, reject); - }); -} - /** - * Get an element finder based on the css selector and root element. + * 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 css the css selector + * @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(css: string, root?: ElementFinder, options?: Options): +async function getElement(selector: string, root?: ElementFinder, options?: Options): Promise { const useGlobalRoot = options && !!options.global; - const elem = root === undefined || useGlobalRoot ? protractorElement(by.css(css)) : - root.element(by.css(css)); + 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) { - // Only check isPresent when allowNull is set - if (!(await elem.isPresent())) { - if (allowNull) { - return null; - } - throw new Error('Cannot find element based on the css selector: ' + css); + options.allowNull : undefined; + if (allowNull !== undefined && !(await elem.isPresent())) { + if (allowNull) { + return null; } - return elem; + throw new Error('Cannot find element based on the CSS selector: ' + selector); } return elem; } diff --git a/src/cdk-experimental/testing/testbed.ts b/src/cdk-experimental/testing/testbed.ts index 0f14a107844a..349edb1d9c5c 100644 --- a/src/cdk-experimental/testing/testbed.ts +++ b/src/cdk-experimental/testing/testbed.ts @@ -10,6 +10,13 @@ // package? It depends on `@angular/core/testing` which we don't want to put in the deps for // cdk-experimental. +import { + dispatchFakeEvent, + dispatchKeyboardEvent, + dispatchMouseEvent, + triggerBlur, + triggerFocus +} from '@angular/cdk/testing'; import {ComponentFixture} from '@angular/core/testing'; import { @@ -26,15 +33,13 @@ import { * @param fixture: Component Fixture of the component to be tested. */ export function load( - componentHarness: ComponentHarnessType, - fixture: ComponentFixture<{}>): T { - const root = fixture.nativeElement; + componentHarness: ComponentHarnessType, + fixture: ComponentFixture<{}>): T { const stabilize = async () => { fixture.detectChanges(); await fixture.whenStable(); }; - const locator = new UnitTestLocator(root, stabilize); - return new componentHarness(locator); + return new componentHarness(new UnitTestLocator(fixture.nativeElement, stabilize)); } /** @@ -53,7 +58,7 @@ export function getNativeElement(testElement: TestElement): Element { * Note that, this locator is exposed for internal usage, please do not use it. */ export class UnitTestLocator implements Locator { - private _rootElement: TestElement; + private readonly _rootElement: TestElement; constructor(private _root: Element, private _stabilize: () => Promise) { this._rootElement = new UnitTestElement(_root, this._stabilize); @@ -63,127 +68,109 @@ export class UnitTestLocator implements Locator { return this._rootElement; } - async find(css: string, options?: Options): Promise { + async querySelector(selector: string, options?: Options): Promise { await this._stabilize(); - const e = getElement(css, this._root, options); - if (e === null) { - return null; - } - return new UnitTestElement(e, this._stabilize); + const e = getElement(selector, this._root, options); + return e && new UnitTestElement(e, this._stabilize); } - async findAll(css: string): Promise { + async querySelectorAll(selector: string): Promise { await this._stabilize(); - const elements = this._root.querySelectorAll(css); - const res: TestElement[] = []; - for (let i = 0; i < elements.length; i++) { - res.push(new UnitTestElement(elements[i], this._stabilize)); - } - return res; + return Array.prototype.map.call( + this._root.querySelectorAll(selector), + (e: Element) => new UnitTestElement(e, this._stabilize)); } async load( - componentHarness: ComponentHarnessType, css: string, - options?: Options): Promise { - const root = getElement(css, this._root, options); - if (root === null) { - return null; - } - const locator = new UnitTestLocator(root, this._stabilize); - return new componentHarness(locator); + componentHarness: ComponentHarnessType, selector: string, + options?: Options): Promise { + await this._stabilize(); + const root = getElement(selector, this._root, options); + return root && new componentHarness(new UnitTestLocator(root, this._stabilize)); } async loadAll( - componentHarness: ComponentHarnessType, - rootSelector: string): Promise { - await this._stabilize(); - const roots = this._root.querySelectorAll(rootSelector); - const res: T[] = []; - for (let i = 0; i < roots.length; i++) { - const root = roots[i]; - const locator = new UnitTestLocator(root, this._stabilize); - res.push(new componentHarness(locator)); - } - return res; + componentHarness: ComponentHarnessType, + 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) {} + constructor(readonly element: Element, private _stabilize: () => Promise) {} async blur(): Promise { await this._stabilize(); - (this.element as HTMLElement).blur(); + triggerBlur(this.element as HTMLElement); await this._stabilize(); } async clear(): Promise { await this._stabilize(); if (!(this.element instanceof HTMLInputElement || - this.element instanceof HTMLTextAreaElement)) { + this.element instanceof HTMLTextAreaElement)) { throw new Error('Attempting to clear an invalid element'); } - this.element.focus(); + triggerFocus(this.element); this.element.value = ''; - this.element.dispatchEvent(new Event('input')); + dispatchFakeEvent(this.element, 'input'); await this._stabilize(); } async click(): Promise { await this._stabilize(); - (this.element as HTMLElement).click(); + dispatchMouseEvent(this.element, 'click'); await this._stabilize(); } async focus(): Promise { await this._stabilize(); - (this.element as HTMLElement).focus(); + triggerFocus(this.element as HTMLElement); await this._stabilize(); } async getCssValue(property: string): Promise { await this._stabilize(); - return Promise.resolve( - getComputedStyle(this.element).getPropertyValue(property)); + // 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(); - this.element.dispatchEvent(new Event('mouseenter')); + dispatchMouseEvent(this.element, 'mouseenter'); await this._stabilize(); } async sendKeys(keys: string): Promise { await this._stabilize(); - (this.element as HTMLElement).focus(); - const e = this.element as HTMLInputElement; + triggerFocus(this.element as HTMLElement); for (const key of keys) { - const eventParams = {key, char: key, keyCode: key.charCodeAt(0)}; - e.dispatchEvent(new KeyboardEvent('keydown', eventParams)); - e.dispatchEvent(new KeyboardEvent('keypress', eventParams)); - e.value += key; - e.dispatchEvent(new KeyboardEvent('keyup', eventParams)); - e.dispatchEvent(new Event('input')); + const keyCode = key.charCodeAt(0); + dispatchKeyboardEvent(this.element, 'keydown', keyCode); + dispatchKeyboardEvent(this.element, 'keypress', keyCode); + (this.element as HTMLInputElement).value += key; + dispatchKeyboardEvent(this.element, 'keyup', keyCode); + dispatchFakeEvent(this.element, 'input'); } await this._stabilize(); } async text(): Promise { await this._stabilize(); - return Promise.resolve(this.element.textContent || ''); + 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) { - if (name in this.element) { - // tslint:disable-next-line:no-any handle unnecessary compile error - value = (this.element as any)[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) { + return (this.element as unknown as {[key: string]: string|null})[name]; } return value; } @@ -191,8 +178,8 @@ class UnitTestElement implements TestElement { /** - * Get an element based on the css selector and root element. - * @param css the css selector + * 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 @@ -200,18 +187,16 @@ class UnitTestElement implements TestElement { * to true, throw an error if Options.allowNull is set to false; otherwise, * return the element */ -function getElement(css: string, root: Element, options?: Options): Element| - null { +function getElement(selector: string, root: Element, options?: Options): Element|null { const useGlobalRoot = options && !!options.global; - const elem = (useGlobalRoot ? document : root).querySelector(css); + const elem = (useGlobalRoot ? document : root).querySelector(selector); const allowNull = options !== undefined && options.allowNull !== undefined ? - options.allowNull : - undefined; + options.allowNull : undefined; if (elem === null) { if (allowNull) { return null; } - throw new Error('Cannot find element based on the css selector: ' + css); + throw new Error('Cannot find element based on the CSS selector: ' + selector); } return elem; } diff --git a/src/cdk-experimental/testing/tests/BUILD.bazel b/src/cdk-experimental/testing/tests/BUILD.bazel index 28d98ee5c944..88073a1f8f64 100644 --- a/src/cdk-experimental/testing/tests/BUILD.bazel +++ b/src/cdk-experimental/testing/tests/BUILD.bazel @@ -6,6 +6,7 @@ ng_module( name = "test_components", module_name = "@angular/cdk-experimental/testing/tests", srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts", "harnesses/**"]), + assets = glob(["**/*.html"]), deps = [ "@npm//@angular/forms", ], diff --git a/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts b/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts index 9217fc7bdafa..cc70b96618da 100644 --- a/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts +++ b/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts @@ -3,7 +3,7 @@ import {browser, by, element} from 'protractor'; import {getElementFinder, load} from '../protractor'; import {MainComponentHarness} from './harnesses/main-component-harness'; -describe('Protractor Helper Test:', () => { +describe('Protractor Helper Test', () => { let harness: MainComponentHarness; beforeEach(async () => { @@ -11,59 +11,59 @@ describe('Protractor Helper Test:', () => { harness = await load(MainComponentHarness, 'test-main'); }); - describe('Locator ', () => { - it('should be able to locate a element based on css selector', async () => { + describe('Locator', () => { + it('should be able to locate a element based on CSS selector', async () => { const title = await harness.title(); - expect(await title.text()).toEqual('Main Component'); + expect(await title.text()).toBe('Main Component'); }); - it('should be able to locate all elements based on css selector', + it('should be able to locate all elements based on CSS selector', async () => { const labels = await harness.allLabels(); - expect(labels.length).toEqual(2); - expect(await labels[0].text()).toEqual('Count:'); - expect(await labels[1].text()).toEqual('AsyncCounter:'); + 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).toEqual(3); - expect(await items[0].text()).toEqual('Protractor'); - expect(await items[1].text()).toEqual('TestBed'); - expect(await items[2].text()).toEqual('Other'); + 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).toEqual(2); - expect(items1.length).toEqual(3); - expect(await items1[0].text()).toEqual('Protractor'); - expect(await items1[1].text()).toEqual('TestBed'); - expect(await items1[2].text()).toEqual('Other'); - expect(items2.length).toEqual(3); - expect(await items2[0].text()).toEqual('Unit Test'); - expect(await items2[1].text()).toEqual('Integration Test'); - expect(await items2[2].text()).toEqual('Performance Test'); + 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 ', () => { + describe('Test element', () => { it('should be able to clear', async () => { const input = await harness.input(); await input.sendKeys('Yi'); - expect(await input.getAttribute('value')).toEqual('Yi'); + expect(await input.getAttribute('value')).toBe('Yi'); await input.clear(); - expect(await input.getAttribute('value')).toEqual(''); + expect(await input.getAttribute('value')).toBe(''); }); it('should be able to click', async () => { const counter = await harness.counter(); - expect(await counter.text()).toEqual('0'); + expect(await counter.text()).toBe('0'); await harness.increaseCounter(3); - expect(await counter.text()).toEqual('3'); + expect(await counter.text()).toBe('3'); }); it('should be able to send key', async () => { @@ -71,15 +71,15 @@ describe('Protractor Helper Test:', () => { const value = await harness.value(); await input.sendKeys('Yi'); - expect(await input.getAttribute('value')).toEqual('Yi'); - expect(await value.text()).toEqual('Input: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')) - .toEqual(await browser.driver.switchTo().activeElement().getAttribute( + .toBe(await browser.driver.switchTo().activeElement().getAttribute( 'id')); }); @@ -99,25 +99,25 @@ describe('Protractor Helper Test:', () => { `; const memo = await harness.memo(); await memo.sendKeys(memoStr); - expect(await memo.getAttribute('value')).toEqual(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')).toEqual('50px'); + expect(await title.getCssValue('height')).toBe('50px'); }); }); - describe('Async operation ', () => { + describe('Async operation', () => { it('should wait for async opeartion to complete', async () => { const asyncCounter = await harness.asyncCounter(); - expect(await asyncCounter.text()).toEqual('5'); + expect(await asyncCounter.text()).toBe('5'); await harness.increaseCounter(3); - expect(await asyncCounter.text()).toEqual('8'); + expect(await asyncCounter.text()).toBe('8'); }); }); - describe('Allow null ', () => { + describe('Allow null', () => { it('should allow element to be null when setting allowNull', async () => { expect(await harness.nullItem()).toBe(null); }); @@ -153,21 +153,21 @@ describe('Protractor Helper Test:', () => { fail('Should throw error'); } catch (err) { expect(err.message) - .toEqual( - 'Cannot find element based on the css selector: wrong locator'); + .toBe( + 'Cannot find element based on the CSS selector: wrong locator'); } }); }); - describe('Throw error ', () => { + describe('Throw error', () => { it('should show the correct error', async () => { try { await harness.errorItem(); fail('Should throw error'); } catch (err) { expect(err.message) - .toEqual( - 'Cannot find element based on the css selector: wrong locator'); + .toBe( + 'Cannot find element based on the CSS selector: wrong locator'); } }); }); @@ -177,7 +177,7 @@ describe('Protractor Helper Test:', () => { const mainElement = await element(by.css('test-main')); const elementFromHarness = getElementFinder(harness.host()); expect(await elementFromHarness.getId()) - .toEqual(await mainElement.getId()); + .toBe(await mainElement.getId()); }); }); }); diff --git a/src/cdk-experimental/testing/tests/test-main-component.html b/src/cdk-experimental/testing/tests/test-main-component.html new file mode 100644 index 000000000000..6258c51786ef --- /dev/null +++ b/src/cdk-experimental/testing/tests/test-main-component.html @@ -0,0 +1,12 @@ +

    Main Component

    +
    Hello {{username}} from Angular 2!
    +
    + +
    {{counter}}
    + +
    {{asyncCounter}}
    + +
    Input: {{input}}
    + + + diff --git a/src/cdk-experimental/testing/tests/test-main-component.ts b/src/cdk-experimental/testing/tests/test-main-component.ts index 51cbbb0074de..59d2523ec1cc 100644 --- a/src/cdk-experimental/testing/tests/test-main-component.ts +++ b/src/cdk-experimental/testing/tests/test-main-component.ts @@ -16,20 +16,7 @@ import { @Component({ moduleId: module.id, selector: 'test-main', - template: ` -

    Main Component

    -
    Hello {{username}} from Angular 2!
    -
    - -
    {{counter}}
    - -
    {{asyncCounter}}
    - -
    Input:{{input}}
    - - - - `, + templateUrl: 'test-main-component.html', host: { '[class.hovering]': '_isHovering', '(mouseenter)': 'onMouseOver()', @@ -60,7 +47,6 @@ export class TestMainComponent { } constructor(private _cdr: ChangeDetectorRef) { - console.log('Ng2Component instantiated.'); this.username = 'Yi'; this.counter = 0; this.asyncCounter = 0; diff --git a/src/cdk-experimental/testing/tests/testbed.spec.ts b/src/cdk-experimental/testing/tests/testbed.spec.ts index a5b102f207ed..57a418e4b303 100644 --- a/src/cdk-experimental/testing/tests/testbed.spec.ts +++ b/src/cdk-experimental/testing/tests/testbed.spec.ts @@ -5,7 +5,7 @@ import {MainComponentHarness} from './harnesses/main-component-harness'; import {TestComponentsModule} from './test-components-module'; import {TestMainComponent} from './test-main-component'; -describe('Testbed Helper Test:', () => { +describe('Testbed Helper Test', () => { let harness: MainComponentHarness; let fixture: ComponentFixture<{}>; beforeEach(async(() => { @@ -20,59 +20,59 @@ describe('Testbed Helper Test:', () => { }); })); - describe('Locator ', () => { - it('should be able to locate a element based on css selector', async () => { + describe('Locator', () => { + it('should be able to locate a element based on CSS selector', async () => { const title = await harness.title(); - expect(await title.text()).toEqual('Main Component'); + expect(await title.text()).toBe('Main Component'); }); - it('should be able to locate all elements based on css selector', + it('should be able to locate all elements based on CSS selector', async () => { const labels = await harness.allLabels(); - expect(labels.length).toEqual(2); - expect(await labels[0].text()).toEqual('Count:'); - expect(await labels[1].text()).toEqual('AsyncCounter:'); + 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).toEqual(3); - expect(await items[0].text()).toEqual('Protractor'); - expect(await items[1].text()).toEqual('TestBed'); - expect(await items[2].text()).toEqual('Other'); + 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).toEqual(2); - expect(items1.length).toEqual(3); - expect(await items1[0].text()).toEqual('Protractor'); - expect(await items1[1].text()).toEqual('TestBed'); - expect(await items1[2].text()).toEqual('Other'); - expect(items2.length).toEqual(3); - expect(await items2[0].text()).toEqual('Unit Test'); - expect(await items2[1].text()).toEqual('Integration Test'); - expect(await items2[2].text()).toEqual('Performance Test'); + 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 ', () => { + describe('Test element', () => { it('should be able to clear', async () => { const input = await harness.input(); await input.sendKeys('Yi'); - expect(await input.getAttribute('value')).toEqual('Yi'); + expect(await input.getAttribute('value')).toBe('Yi'); await input.clear(); - expect(await input.getAttribute('value')).toEqual(''); + expect(await input.getAttribute('value')).toBe(''); }); it('should be able to click', async () => { const counter = await harness.counter(); - expect(await counter.text()).toEqual('0'); + expect(await counter.text()).toBe('0'); await harness.increaseCounter(3); - expect(await counter.text()).toEqual('3'); + expect(await counter.text()).toBe('3'); }); it('should be able to send key', async () => { @@ -80,15 +80,15 @@ describe('Testbed Helper Test:', () => { const value = await harness.value(); await input.sendKeys('Yi'); - expect(await input.getAttribute('value')).toEqual('Yi'); - expect(await value.text()).toEqual('Input: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')) - .toEqual(document.activeElement!.id); + .toBe(document.activeElement!.id); }); it('should be able to hover', async () => { @@ -107,25 +107,25 @@ describe('Testbed Helper Test:', () => { `; const memo = await harness.memo(); await memo.sendKeys(memoStr); - expect(await memo.getAttribute('value')).toEqual(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')).toEqual('50px'); + expect(await title.getCssValue('height')).toBe('50px'); }); }); - describe('Async operation ', () => { + describe('Async operation', () => { it('should wait for async opeartion to complete', async () => { const asyncCounter = await harness.asyncCounter(); - expect(await asyncCounter.text()).toEqual('5'); + expect(await asyncCounter.text()).toBe('5'); await harness.increaseCounter(3); - expect(await asyncCounter.text()).toEqual('8'); + expect(await asyncCounter.text()).toBe('8'); }); }); - describe('Allow null ', () => { + describe('Allow null', () => { it('should allow element to be null when setting allowNull', async () => { expect(await harness.nullItem()).toBe(null); }); @@ -135,22 +135,22 @@ describe('Testbed Helper Test:', () => { }); }); - describe('Throw error ', () => { + describe('Throw error', () => { it('should show the correct error', async () => { try { await harness.errorItem(); fail('Should throw error'); } catch (err) { expect(err.message) - .toEqual( - 'Cannot find element based on the css selector: wrong locator'); + .toBe( + 'Cannot find element based on the CSS selector: wrong locator'); } }); }); describe('getNativeElement', () => { it('should return the native element', async () => { - expect(getNativeElement(harness.host())).toEqual(fixture.nativeElement); + expect(getNativeElement(harness.host())).toBe(fixture.nativeElement); }); }); }); diff --git a/src/cdk-experimental/testing/tsconfig-build.json b/src/cdk-experimental/testing/tsconfig-build.json deleted file mode 100644 index f080b9236e51..000000000000 --- a/src/cdk-experimental/testing/tsconfig-build.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../tsconfig-build", - "files": [ - "public-api.ts" - ], - "angularCompilerOptions": { - "annotateForClosureCompiler": true, - "strictMetadataEmit": true, - "flatModuleOutFile": "index.js", - "flatModuleId": "@angular/cdk-experimental/testing", - "skipTemplateCodegen": true, - "fullTemplateTypeCheck": true - } -} diff --git a/src/cdk/testing/element-focus.ts b/src/cdk/testing/element-focus.ts index 280166dfe709..d7ba09db14f3 100644 --- a/src/cdk/testing/element-focus.ts +++ b/src/cdk/testing/element-focus.ts @@ -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'); +} diff --git a/src/e2e-app/main-module.ts b/src/e2e-app/main-module.ts index 228999c685d3..6a35c2866688 100644 --- a/src/e2e-app/main-module.ts +++ b/src/e2e-app/main-module.ts @@ -2,7 +2,9 @@ import {NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {RouterModule} from '@angular/router'; -import {BlockScrollStrategyE2eModule} from './block-scroll-strategy/block-scroll-strategy-e2e-module'; +import { + BlockScrollStrategyE2eModule +} from './block-scroll-strategy/block-scroll-strategy-e2e-module'; import {ButtonToggleE2eModule} from './button-toggle/button-toggle-e2e-module'; import {ButtonE2eModule} from './button/button-e2e-module'; import {CardE2eModule} from './card/card-e2e-module'; From ae85b374a7ec0e1a8f5f840fab5b8955988cd740 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 31 May 2019 16:22:07 -0700 Subject: [PATCH 5/8] address @jelbourn's comments --- .../testing/component-harness.ts | 118 +++++++----------- src/cdk-experimental/testing/protractor.ts | 57 ++++----- src/cdk-experimental/testing/public-api.ts | 1 + src/cdk-experimental/testing/test-element.ts | 46 +++++++ src/cdk-experimental/testing/testbed.ts | 31 ++--- .../tests/harnesses/main-component-harness.ts | 3 +- .../tests/harnesses/sub-component-harness.ts | 3 +- 7 files changed, 136 insertions(+), 123 deletions(-) create mode 100644 src/cdk-experimental/testing/test-element.ts diff --git a/src/cdk-experimental/testing/component-harness.ts b/src/cdk-experimental/testing/component-harness.ts index c3c1550985fe..e1c385561cd3 100644 --- a/src/cdk-experimental/testing/component-harness.ts +++ b/src/cdk-experimental/testing/component-harness.ts @@ -6,61 +6,33 @@ * found in the LICENSE file at https://angular.io/license */ -/** - * Test Element interface - * This is a wrapper of native element - */ -export interface TestElement { - blur(): Promise; - clear(): Promise; - click(): Promise; - focus(): Promise; - getCssValue(property: string): Promise; - hover(): Promise; - sendKeys(keys: string): Promise; - text(): Promise; - getAttribute(name: string): Promise; -} +import {TestElement} from './test-element'; -/** - * Extra searching options used by searching functions - * - * @param allowNull Optional, 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. - * - * @param global Optional. 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. - */ -export interface Options { +/** 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; } -/** - * Type narrowing of Options to allow the overloads of ComponentHarness.find to - * return null only if allowNull is set to true. - */ -interface OptionsWithAllowNullSet extends Options { - allowNull: true; -} - -/** - * Locator interface - */ -export interface Locator { +/** Interface that is used to find elements in the DOM and create harnesses for them. */ +export interface HarnessLocator { /** * Get the host element of locator. */ @@ -71,7 +43,7 @@ export interface Locator { * @param selector The CSS selector of the test elements. * @param options Optional, extra searching options */ - querySelector(selector: string, options?: Options): Promise; + querySelector(selector: string, options?: QueryOptions): Promise; /** * Search all matched test elements under current root by CSS selector. @@ -86,8 +58,8 @@ export interface Locator { * @param options Optional, extra searching options */ load( - componentHarness: ComponentHarnessType, root: string, - options?: Options): Promise; + componentHarness: ComponentHarnessConstructor, root: string, + options?: QueryOptions): Promise; /** * Load all Component Harnesses under current root. @@ -95,7 +67,7 @@ export interface Locator { * @param root CSS root selector of the new component harnesses. */ loadAll( - componentHarness: ComponentHarnessType, root: string): Promise; + componentHarness: ComponentHarnessConstructor, root: string): Promise; } /** @@ -104,8 +76,8 @@ export interface Locator { * sub-component harness. It should be inherited when defining user's own * harness. */ -export class ComponentHarness { - constructor(private readonly locator: Locator) {} +export abstract class ComponentHarness { + constructor(private readonly locator: HarnessLocator) {} /** * Get the host element of component harness. @@ -127,7 +99,7 @@ export class ComponentHarness { * @param selector The CSS selector of the test element. * @param options Extra searching options */ - protected find(selector: string, options: OptionsWithAllowNullSet): + protected find(selector: string, options: QueryOptions & {allowNull: true}): () => Promise; /** @@ -136,7 +108,7 @@ export class ComponentHarness { * @param selector The CSS selector of the test element. * @param options Extra searching options */ - protected find(selector: string, options: Options): () => Promise; + protected find(selector: string, options: QueryOptions): () => Promise; /** * Generate a function to find the first matched Component Harness. @@ -144,7 +116,7 @@ export class ComponentHarness { * @param root CSS root selector of the new component harness. */ protected find( - componentHarness: ComponentHarnessType, + componentHarness: ComponentHarnessConstructor, root: string): () => Promise; /** @@ -154,8 +126,8 @@ export class ComponentHarness { * @param options Extra searching options */ protected find( - componentHarness: ComponentHarnessType, root: string, - options: OptionsWithAllowNullSet): () => Promise; + componentHarness: ComponentHarnessConstructor, root: string, + options: QueryOptions & {allowNull: true}): () => Promise; /** * Generate a function to find the first matched Component Harness. @@ -164,16 +136,16 @@ export class ComponentHarness { * @param options Extra searching options */ protected find( - componentHarness: ComponentHarnessType, root: string, - options: Options): () => Promise; + componentHarness: ComponentHarnessConstructor, root: string, + options: QueryOptions): () => Promise; protected find( - selectorOrComponentHarness: string|ComponentHarnessType, - selectorOrOptions?: string|Options, - options?: Options): () => Promise { + selectorOrComponentHarness: string|ComponentHarnessConstructor, + selectorOrOptions?: string|QueryOptions, + options?: QueryOptions): () => Promise { if (typeof selectorOrComponentHarness === 'string') { const selector = selectorOrComponentHarness; - return () => this.locator.querySelector(selector, selectorOrOptions as Options); + return () => this.locator.querySelector(selector, selectorOrOptions as QueryOptions); } else { const componentHarness = selectorOrComponentHarness; const selector = selectorOrOptions as string; @@ -196,11 +168,11 @@ export class ComponentHarness { * locate harnesses under the current root. */ protected findAll( - componentHarness: ComponentHarnessType, + componentHarness: ComponentHarnessConstructor, root: string): () => Promise; protected findAll( - selectorOrComponentHarness: string|ComponentHarnessType, + selectorOrComponentHarness: string|ComponentHarnessConstructor, root?: string): () => Promise { if (typeof selectorOrComponentHarness === 'string') { const selector = selectorOrComponentHarness; @@ -212,9 +184,7 @@ export class ComponentHarness { } } -/** - * Type of ComponentHarness. - */ -export interface ComponentHarnessType { - new(locator: Locator): T; +/** Constructor for a ComponentHarness subclass. */ +export interface ComponentHarnessConstructor { + new(locator: HarnessLocator): T; } diff --git a/src/cdk-experimental/testing/protractor.ts b/src/cdk-experimental/testing/protractor.ts index ea0a3712a1e9..0cf30b4e9b5e 100644 --- a/src/cdk-experimental/testing/protractor.ts +++ b/src/cdk-experimental/testing/protractor.ts @@ -13,11 +13,11 @@ import {browser, by, element as protractorElement, ElementFinder} from 'protract import { ComponentHarness, - ComponentHarnessType, - Locator, - Options, - TestElement + ComponentHarnessConstructor, + HarnessLocator, + QueryOptions } from './component-harness'; +import {TestElement} from './test-element'; /** * Component harness factory for protractor. @@ -31,7 +31,7 @@ import { * Set to 'body' by default */ export async function load( - componentHarness: ComponentHarnessType, + componentHarness: ComponentHarnessConstructor, rootSelector: string): Promise; /** @@ -42,12 +42,12 @@ export async function load( * @param options Optional. Extra searching options */ export async function load( - componentHarness: ComponentHarnessType, rootSelector?: string, - options?: Options): Promise; + componentHarness: ComponentHarnessConstructor, rootSelector?: string, + options?: QueryOptions): Promise; export async function load( - componentHarness: ComponentHarnessType, rootSelector = 'body', - options?: Options): Promise { + componentHarness: ComponentHarnessConstructor, rootSelector = 'body', + options?: QueryOptions): Promise { const root = await getElement(rootSelector, undefined, options); return root && new componentHarness(new ProtractorLocator(root)); } @@ -60,10 +60,10 @@ export function getElementFinder(testElement: TestElement): ElementFinder { return testElement.element; } - throw new Error('Invalid element provided'); + throw Error(`Expected an instance of ProtractorElement, got ${testElement}`); } -class ProtractorLocator implements Locator { +class ProtractorLocator implements HarnessLocator { private readonly _root: ProtractorElement; constructor(private _rootFinder: ElementFinder) { @@ -74,41 +74,34 @@ class ProtractorLocator implements Locator { return this._root; } - async querySelector(selector: string, options?: Options): Promise { + 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)); - const res: TestElement[] = []; - await elementFinders.each(el => { - if (el) { - res.push(new ProtractorElement(el)); - } - }); - return res; + return elementFinders.reduce( + (result: TestElement[], el: ElementFinder) => + el ? result.concat([new ProtractorElement(el)]) : result, + []); } async load( - componentHarness: ComponentHarnessType, selector: string, - options?: Options): Promise { + 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: ComponentHarnessType, + componentHarness: ComponentHarnessConstructor, rootSelector: string): Promise { const roots = this._rootFinder.all(by.css(rootSelector)); - const res: T[] = []; - await roots.each(el => { - if (el) { - const locator = new ProtractorLocator(el); - res.push(new componentHarness(locator)); - } - }); - return res; + return roots.reduce( + (result: T[], el: ElementFinder) => + el ? result.concat(new componentHarness(new ProtractorLocator(el))) : result, + []); } } @@ -163,7 +156,7 @@ class ProtractorElement implements TestElement { * 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?: Options): +async function getElement(selector: string, root?: ElementFinder, options?: QueryOptions): Promise { const useGlobalRoot = options && !!options.global; const elem = root === undefined || useGlobalRoot ? @@ -174,7 +167,7 @@ async function getElement(selector: string, root?: ElementFinder, options?: Opti if (allowNull) { return null; } - throw new Error('Cannot find element based on the CSS selector: ' + selector); + throw Error('Cannot find element based on the CSS selector: ' + selector); } return elem; } diff --git a/src/cdk-experimental/testing/public-api.ts b/src/cdk-experimental/testing/public-api.ts index 4eec6438e103..bedfe9b759cb 100644 --- a/src/cdk-experimental/testing/public-api.ts +++ b/src/cdk-experimental/testing/public-api.ts @@ -10,4 +10,5 @@ import * as protractor from './protractor'; import * as testbed from './testbed'; export * from './component-harness'; +export * from './test-element'; export {protractor, testbed}; diff --git a/src/cdk-experimental/testing/test-element.ts b/src/cdk-experimental/testing/test-element.ts new file mode 100644 index 000000000000..1c6ae8de55cb --- /dev/null +++ b/src/cdk-experimental/testing/test-element.ts @@ -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; + + /** Clear the element's input (for input elements only). */ + clear(): Promise; + + /** Click the element. */ + click(): Promise; + + /** Focus the element. */ + focus(): Promise; + + /** Get the computed value of the given CSS property for the element. */ + getCssValue(property: string): Promise; + + /** Hovers the mouse over the element. */ + hover(): Promise; + + /** + * 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; + + /** Gets the text from the element. */ + text(): Promise; + + /** + * 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; +} diff --git a/src/cdk-experimental/testing/testbed.ts b/src/cdk-experimental/testing/testbed.ts index 349edb1d9c5c..807f323e8b40 100644 --- a/src/cdk-experimental/testing/testbed.ts +++ b/src/cdk-experimental/testing/testbed.ts @@ -21,11 +21,11 @@ import {ComponentFixture} from '@angular/core/testing'; import { ComponentHarness, - ComponentHarnessType, - Locator, - Options, - TestElement + ComponentHarnessConstructor, + HarnessLocator, + QueryOptions } from './component-harness'; +import {TestElement} from './test-element'; /** * Component harness factory for testbed. @@ -33,7 +33,7 @@ import { * @param fixture: Component Fixture of the component to be tested. */ export function load( - componentHarness: ComponentHarnessType, + componentHarness: ComponentHarnessConstructor, fixture: ComponentFixture<{}>): T { const stabilize = async () => { fixture.detectChanges(); @@ -50,14 +50,14 @@ export function getNativeElement(testElement: TestElement): Element { return testElement.element; } - throw new Error('Invalid element provided'); + 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 Locator { +export class UnitTestLocator implements HarnessLocator { private readonly _rootElement: TestElement; constructor(private _root: Element, private _stabilize: () => Promise) { @@ -68,7 +68,7 @@ export class UnitTestLocator implements Locator { return this._rootElement; } - async querySelector(selector: string, options?: Options): Promise { + async querySelector(selector: string, options?: QueryOptions): Promise { await this._stabilize(); const e = getElement(selector, this._root, options); return e && new UnitTestElement(e, this._stabilize); @@ -82,15 +82,15 @@ export class UnitTestLocator implements Locator { } async load( - componentHarness: ComponentHarnessType, selector: string, - options?: Options): Promise { + 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: ComponentHarnessType, + componentHarness: ComponentHarnessConstructor, rootSelector: string): Promise { await this._stabilize(); return Array.prototype.map.call( @@ -112,7 +112,7 @@ class UnitTestElement implements TestElement { await this._stabilize(); if (!(this.element instanceof HTMLInputElement || this.element instanceof HTMLTextAreaElement)) { - throw new Error('Attempting to clear an invalid element'); + throw Error('Attempting to clear an invalid element'); } triggerFocus(this.element); this.element.value = ''; @@ -170,6 +170,7 @@ class UnitTestElement implements TestElement { // 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; @@ -187,8 +188,8 @@ class UnitTestElement implements TestElement { * to true, throw an error if Options.allowNull is set to false; otherwise, * return the element */ -function getElement(selector: string, root: Element, options?: Options): Element|null { - const useGlobalRoot = options && !!options.global; +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; @@ -196,7 +197,7 @@ function getElement(selector: string, root: Element, options?: Options): Element if (allowNull) { return null; } - throw new Error('Cannot find element based on the CSS selector: ' + selector); + throw Error('Cannot find element based on the CSS selector: ' + selector); } return elem; } 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 9871bf648552..b31421814c90 100644 --- a/src/cdk-experimental/testing/tests/harnesses/main-component-harness.ts +++ b/src/cdk-experimental/testing/tests/harnesses/main-component-harness.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {ComponentHarness, TestElement} from '../../component-harness'; +import {ComponentHarness} from '../../component-harness'; +import {TestElement} from '../../test-element'; import {SubComponentHarness} from './sub-component-harness'; export class MainComponentHarness extends ComponentHarness { 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 3b08e7255355..a70ffd3418a3 100644 --- a/src/cdk-experimental/testing/tests/harnesses/sub-component-harness.ts +++ b/src/cdk-experimental/testing/tests/harnesses/sub-component-harness.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {ComponentHarness, TestElement} from '../../component-harness'; +import {ComponentHarness} from '../../component-harness'; +import {TestElement} from '../../test-element'; export class SubComponentHarness extends ComponentHarness { readonly title = this.find('h2'); From 3a6efc86bfb81673a625444ecb7329f26e8d6323 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 3 Jun 2019 11:28:53 -0700 Subject: [PATCH 6/8] fix bazel lint --- src/cdk-experimental/testing/BUILD.bazel | 55 ++++++++-------- .../testing/tests/BUILD.bazel | 63 +++++++++++-------- 2 files changed, 66 insertions(+), 52 deletions(-) diff --git a/src/cdk-experimental/testing/BUILD.bazel b/src/cdk-experimental/testing/BUILD.bazel index f1d78d0c37b6..7727dd4d60d7 100644 --- a/src/cdk-experimental/testing/BUILD.bazel +++ b/src/cdk-experimental/testing/BUILD.bazel @@ -1,37 +1,42 @@ -package(default_visibility=["//visibility:public"]) +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 = [ - "@npm//@angular/core", - "@npm//protractor", - "//src/cdk/testing", - ], + 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"], + name = "unit_tests", + deps = ["//src/cdk-experimental/testing/tests:unit_test_sources"], ) protractor_web_test_suite( - name = "e2e_tests", - tags = ["e2e"], - configuration = "//src/e2e-app:protractor.conf.js", - on_prepare = "//src/e2e-app:start-devserver.js", - server = "//src/e2e-app:devserver", - deps = [ - "@npm//protractor", - "//src/cdk-experimental/testing/tests:e2e_test_sources", - ], - data = [ - "@npm//@angular/bazel", - "//tools/axe-protractor", - ], + 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", + ], ) diff --git a/src/cdk-experimental/testing/tests/BUILD.bazel b/src/cdk-experimental/testing/tests/BUILD.bazel index 88073a1f8f64..19556f7d3d1d 100644 --- a/src/cdk-experimental/testing/tests/BUILD.bazel +++ b/src/cdk-experimental/testing/tests/BUILD.bazel @@ -1,40 +1,49 @@ -package(default_visibility=["//visibility:public"]) +package(default_visibility = ["//visibility:public"]) -load("//tools:defaults.bzl", "ng_module", "ts_library", "ng_test_library", "ng_e2e_test_library") +load("//tools:defaults.bzl", "ng_e2e_test_library", "ng_module", "ng_test_library", "ts_library") ng_module( - name = "test_components", - module_name = "@angular/cdk-experimental/testing/tests", - srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts", "harnesses/**"]), - assets = glob(["**/*.html"]), - deps = [ - "@npm//@angular/forms", - ], + 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", - ], + 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", - ], + 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", - ], + name = "e2e_test_sources", + srcs = glob(["**/*.e2e.spec.ts"]), + deps = [ + ":test_harnesses", + "//src/cdk-experimental/testing", + ], ) From a666f344e0b367c04d96ec1ea1254f29b6f5e33b Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 3 Jun 2019 20:16:13 -0700 Subject: [PATCH 7/8] remove some TODOs that are no longer issues --- src/cdk-experimental/testing/protractor.ts | 3 --- src/cdk-experimental/testing/testbed.ts | 4 ---- src/cdk-experimental/testing/tests/test-main-component.ts | 6 ++---- src/cdk-experimental/testing/tests/test-sub-component.ts | 6 ++---- 4 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/cdk-experimental/testing/protractor.ts b/src/cdk-experimental/testing/protractor.ts index 0cf30b4e9b5e..f3fd769d243d 100644 --- a/src/cdk-experimental/testing/protractor.ts +++ b/src/cdk-experimental/testing/protractor.ts @@ -6,9 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -// TODO(mmalerba): Should this file be part of `@angular/cdk-experimental/testing` or a separate -// package? It depends on protractor which we don't want to put in the deps for cdk-experimental. - import {browser, by, element as protractorElement, ElementFinder} from 'protractor'; import { diff --git a/src/cdk-experimental/testing/testbed.ts b/src/cdk-experimental/testing/testbed.ts index 807f323e8b40..d4a95d70c17f 100644 --- a/src/cdk-experimental/testing/testbed.ts +++ b/src/cdk-experimental/testing/testbed.ts @@ -6,10 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -// TODO(mmalerba): Should this file be part of `@angular/cdk-experimental/testing` or a separate -// package? It depends on `@angular/core/testing` which we don't want to put in the deps for -// cdk-experimental. - import { dispatchFakeEvent, dispatchKeyboardEvent, diff --git a/src/cdk-experimental/testing/tests/test-main-component.ts b/src/cdk-experimental/testing/tests/test-main-component.ts index 59d2523ec1cc..355caf5e08b5 100644 --- a/src/cdk-experimental/testing/tests/test-main-component.ts +++ b/src/cdk-experimental/testing/tests/test-main-component.ts @@ -30,13 +30,11 @@ export class TestMainComponent { username: string; counter: number; asyncCounter: number; - // TODO: remove '!'. - input!: string; + input: string; memo: string; testTools: string[]; testMethods: string[]; - // TODO: remove '!'. - _isHovering!: boolean; + _isHovering: boolean; onMouseOver() { this._isHovering = true; diff --git a/src/cdk-experimental/testing/tests/test-sub-component.ts b/src/cdk-experimental/testing/tests/test-sub-component.ts index 8f0ad6a7f5f7..4e9b5051cb6b 100644 --- a/src/cdk-experimental/testing/tests/test-sub-component.ts +++ b/src/cdk-experimental/testing/tests/test-sub-component.ts @@ -20,8 +20,6 @@ import {ChangeDetectionStrategy, Component, Input, ViewEncapsulation} from '@ang changeDetection: ChangeDetectionStrategy.OnPush, }) export class TestSubComponent { - // TODO: remove '!'. - @Input() title!: string; - // TODO: remove '!'. - @Input() items!: string[]; + @Input() title: string; + @Input() items: string[]; } From 8db9e6ed0f215c860cdac7239ede9f4a9943229a Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 5 Jun 2019 14:01:30 -0700 Subject: [PATCH 8/8] address latest round of feedback --- src/cdk-experimental/testing/testbed.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/cdk-experimental/testing/testbed.ts b/src/cdk-experimental/testing/testbed.ts index d4a95d70c17f..d2731cf60b13 100644 --- a/src/cdk-experimental/testing/testbed.ts +++ b/src/cdk-experimental/testing/testbed.ts @@ -106,11 +106,10 @@ class UnitTestElement implements TestElement { async clear(): Promise { await this._stabilize(); - if (!(this.element instanceof HTMLInputElement || - this.element instanceof HTMLTextAreaElement)) { + if (!this._isTextInput(this.element)) { throw Error('Attempting to clear an invalid element'); } - triggerFocus(this.element); + triggerFocus(this.element as HTMLElement); this.element.value = ''; dispatchFakeEvent(this.element, 'input'); await this._stabilize(); @@ -148,9 +147,13 @@ class UnitTestElement implements TestElement { const keyCode = key.charCodeAt(0); dispatchKeyboardEvent(this.element, 'keydown', keyCode); dispatchKeyboardEvent(this.element, 'keypress', keyCode); - (this.element as HTMLInputElement).value += key; + if (this._isTextInput(this.element)) { + this.element.value += key; + } dispatchKeyboardEvent(this.element, 'keyup', keyCode); - dispatchFakeEvent(this.element, 'input'); + if (this._isTextInput(this.element)) { + dispatchFakeEvent(this.element, 'input'); + } } await this._stabilize(); } @@ -171,6 +174,11 @@ class UnitTestElement implements TestElement { } return value; } + + private _isTextInput(element: Element): element is HTMLInputElement | HTMLTextAreaElement { + return element.nodeName.toLowerCase() === 'input' || + element.nodeName.toLowerCase() === 'textarea' ; + } }