diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index aa3502f15385..84bd387d3d0b 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 @@ -177,6 +178,7 @@ /src/e2e-app/button-toggle/** @jelbourn /src/e2e-app/card/** @jelbourn /src/e2e-app/checkbox/** @jelbourn @devversion +/src/e2e-app/component-harness/** @mmalerba /src/e2e-app/dialog/** @jelbourn @crisbeto /src/e2e-app/e2e-app/** @jelbourn /src/e2e-app/example-viewer/** @andrewseguin diff --git a/src/cdk-experimental/testing/BUILD.bazel b/src/cdk-experimental/testing/BUILD.bazel new file mode 100644 index 000000000000..7727dd4d60d7 --- /dev/null +++ b/src/cdk-experimental/testing/BUILD.bazel @@ -0,0 +1,42 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ng_module", "ng_web_test_suite") +load("@npm_angular_bazel//:index.bzl", "protractor_web_test_suite") + +ng_module( + name = "testing", + srcs = glob( + ["**/*.ts"], + exclude = [ + "**/*.spec.ts", + "tests/**", + ], + ), + module_name = "@angular/cdk-experimental/testing", + deps = [ + "//src/cdk/testing", + "@npm//@angular/core", + "@npm//protractor", + ], +) + +ng_web_test_suite( + name = "unit_tests", + deps = ["//src/cdk-experimental/testing/tests:unit_test_sources"], +) + +protractor_web_test_suite( + name = "e2e_tests", + configuration = "//src/e2e-app:protractor.conf.js", + data = [ + "//tools/axe-protractor", + "@npm//@angular/bazel", + ], + on_prepare = "//src/e2e-app:start-devserver.js", + server = "//src/e2e-app:devserver", + tags = ["e2e"], + deps = [ + "//src/cdk-experimental/testing/tests:e2e_test_sources", + "@npm//protractor", + ], +) diff --git a/src/cdk-experimental/testing/component-harness.ts b/src/cdk-experimental/testing/component-harness.ts new file mode 100644 index 000000000000..e1c385561cd3 --- /dev/null +++ b/src/cdk-experimental/testing/component-harness.ts @@ -0,0 +1,190 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {TestElement} from './test-element'; + +/** Options that can be specified when querying for an Element. */ +export interface QueryOptions { + /** + * Whether the found element can be null. If allowNull is set, the searching function will always + * try to fetch the element at once. When the element cannot be found, the searching function + * should return null if allowNull is set to true, throw an error if allowNull is set to false. + * If allowNull is not set, the framework will choose the behaviors that make more sense for each + * test type (e.g. for unit test, the framework will make sure the element is not null; otherwise + * throw an error); however, the internal behavior is not guaranteed and user should not rely on + * it. Note that in most cases, you don't need to care about whether an element is present when + * loading the element and don't need to set this parameter unless you do want to check whether + * the element is present when calling the searching function. e.g. you want to make sure some + * element is not there when loading the element in order to check whether a "ngif" works well. + */ + allowNull?: boolean; + /** + * If global is set to true, the selector will match any element on the page and is not limited to + * the root of the harness. If global is unset or set to false, the selector will only find + * elements under the current root. + */ + global?: boolean; +} + +/** Interface that is used to find elements in the DOM and create harnesses for them. */ +export interface HarnessLocator { + /** + * Get the host element of locator. + */ + host(): TestElement; + + /** + * Search the first matched test element. + * @param selector The CSS selector of the test elements. + * @param options Optional, extra searching options + */ + querySelector(selector: string, options?: QueryOptions): Promise; + + /** + * Search all matched test elements under current root by CSS selector. + * @param selector The CSS selector of the test elements. + */ + 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 options Optional, extra searching options + */ + load( + componentHarness: ComponentHarnessConstructor, root: string, + options?: QueryOptions): 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: ComponentHarnessConstructor, 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 abstract class ComponentHarness { + constructor(private readonly locator: HarnessLocator) {} + + /** + * Get the host element of component harness. + */ + host(): TestElement { + return this.locator.host(); + } + + /** + * Generate a function to find the first matched test element by CSS + * selector. + * @param selector The CSS selector of the test element. + */ + protected find(selector: string): () => Promise; + + /** + * Generate a function to find the first matched test element by CSS + * selector. + * @param selector The CSS selector of the test element. + * @param options Extra searching options + */ + protected find(selector: string, options: QueryOptions & {allowNull: true}): + () => Promise; + + /** + * Generate a function to find the first matched test element by CSS + * selector. + * @param selector The CSS selector of the test element. + * @param options Extra searching options + */ + protected find(selector: string, options: QueryOptions): () => Promise; + + /** + * Generate a function to find the first matched Component Harness. + * @param componentHarness Type of user customized harness. + * @param root CSS root selector of the new component harness. + */ + protected find( + componentHarness: ComponentHarnessConstructor, + root: string): () => Promise; + + /** + * 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: ComponentHarnessConstructor, root: string, + options: QueryOptions & {allowNull: true}): () => 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: ComponentHarnessConstructor, root: string, + options: QueryOptions): () => Promise; + + protected find( + selectorOrComponentHarness: string|ComponentHarnessConstructor, + selectorOrOptions?: string|QueryOptions, + options?: QueryOptions): () => Promise { + if (typeof selectorOrComponentHarness === 'string') { + const selector = selectorOrComponentHarness; + return () => this.locator.querySelector(selector, selectorOrOptions as QueryOptions); + } else { + const componentHarness = selectorOrComponentHarness; + const selector = selectorOrOptions as string; + return () => this.locator.load(componentHarness, selector, options); + } + } + + /** + * Generate a function to find all matched test elements by CSS selector. + * @param selector The CSS root selector of elements. It will locate + * elements under the current root. + */ + protected findAll(selector: string): () => Promise; + + /** + * 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: ComponentHarnessConstructor, + root: string): () => Promise; + + protected findAll( + selectorOrComponentHarness: string|ComponentHarnessConstructor, + root?: string): () => Promise { + if (typeof selectorOrComponentHarness === 'string') { + const selector = selectorOrComponentHarness; + return () => this.locator.querySelectorAll(selector); + } else { + const componentHarness = selectorOrComponentHarness; + return () => this.locator.loadAll(componentHarness, root as string); + } + } +} + +/** Constructor for a ComponentHarness subclass. */ +export interface ComponentHarnessConstructor { + new(locator: HarnessLocator): T; +} 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.ts b/src/cdk-experimental/testing/protractor.ts new file mode 100644 index 000000000000..f3fd769d243d --- /dev/null +++ b/src/cdk-experimental/testing/protractor.ts @@ -0,0 +1,170 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {browser, by, element as protractorElement, ElementFinder} from 'protractor'; + +import { + ComponentHarness, + ComponentHarnessConstructor, + HarnessLocator, + QueryOptions +} from './component-harness'; +import {TestElement} from './test-element'; + +/** + * Component harness factory for protractor. + * The function will not try to fetch the host element of harness at once, which + * is for performance purpose; however, this is the most common way to load + * protractor harness. If you do care whether the host element is present when + * loading harness, using the load function that accepts extra searching + * options. + * @param componentHarness: Type of user defined harness. + * @param rootSelector: Optional. CSS selector to specify the root of component. + * Set to 'body' by default + */ +export async function load( + componentHarness: ComponentHarnessConstructor, + rootSelector: string): Promise; + +/** + * Component harness factory for protractor. + * @param componentHarness: Type of user defined harness. + * @param rootSelector: Optional. CSS selector to specify the root of component. + * Set to 'body' by default. + * @param options Optional. Extra searching options + */ +export async function load( + componentHarness: ComponentHarnessConstructor, rootSelector?: string, + options?: QueryOptions): Promise; + +export async function load( + componentHarness: ComponentHarnessConstructor, rootSelector = 'body', + options?: QueryOptions): Promise { + const root = await getElement(rootSelector, undefined, options); + return root && new componentHarness(new ProtractorLocator(root)); +} + +/** + * Gets the corresponding ElementFinder for the root of a TestElement. + */ +export function getElementFinder(testElement: TestElement): ElementFinder { + if (testElement instanceof ProtractorElement) { + return testElement.element; + } + + throw Error(`Expected an instance of ProtractorElement, got ${testElement}`); +} + +class ProtractorLocator implements HarnessLocator { + private readonly _root: ProtractorElement; + + constructor(private _rootFinder: ElementFinder) { + this._root = new ProtractorElement(this._rootFinder); + } + + host(): TestElement { + return this._root; + } + + async querySelector(selector: string, options?: QueryOptions): Promise { + const finder = await getElement(selector, this._rootFinder, options); + return finder && new ProtractorElement(finder); + } + + async querySelectorAll(selector: string): Promise { + const elementFinders = this._rootFinder.all(by.css(selector)); + return elementFinders.reduce( + (result: TestElement[], el: ElementFinder) => + el ? result.concat([new ProtractorElement(el)]) : result, + []); + } + + async load( + componentHarness: ComponentHarnessConstructor, selector: string, + options?: QueryOptions): Promise { + const root = await getElement(selector, this._rootFinder, options); + return root && new componentHarness(new ProtractorLocator(root)); + } + + async loadAll( + componentHarness: ComponentHarnessConstructor, + rootSelector: string): Promise { + const roots = this._rootFinder.all(by.css(rootSelector)); + return roots.reduce( + (result: T[], el: ElementFinder) => + el ? result.concat(new componentHarness(new ProtractorLocator(el))) : result, + []); + } +} + +class ProtractorElement implements TestElement { + constructor(readonly element: ElementFinder) {} + + async blur(): Promise { + return this.element['blur'](); + } + + async clear(): Promise { + return this.element.clear(); + } + + async click(): Promise { + return this.element.click(); + } + + async focus(): Promise { + return this.element['focus'](); + } + + async getCssValue(property: string): Promise { + return this.element.getCssValue(property); + } + + async hover(): Promise { + return browser.actions() + .mouseMove(await this.element.getWebElement()) + .perform(); + } + + async sendKeys(keys: string): Promise { + return this.element.sendKeys(keys); + } + + async text(): Promise { + return this.element.getText(); + } + + async getAttribute(name: string): Promise { + return this.element.getAttribute(name); + } +} + +/** + * Get an element finder based on the CSS selector and root element. + * Note that it will check whether the element is present only when + * Options.allowNull is set. This is for performance purpose. + * @param selector The CSS selector + * @param root Optional Search element under the root element. If not set, + * search element globally. If options.global is set, root is ignored. + * @param options Optional, extra searching options + */ +async function getElement(selector: string, root?: ElementFinder, options?: QueryOptions): + Promise { + const useGlobalRoot = options && !!options.global; + const elem = root === undefined || useGlobalRoot ? + protractorElement(by.css(selector)) : root.element(by.css(selector)); + const allowNull = options !== undefined && options.allowNull !== undefined ? + options.allowNull : undefined; + if (allowNull !== undefined && !(await elem.isPresent())) { + if (allowNull) { + return null; + } + throw Error('Cannot find element based on the CSS selector: ' + selector); + } + return elem; +} diff --git a/src/cdk-experimental/testing/public-api.ts b/src/cdk-experimental/testing/public-api.ts new file mode 100644 index 000000000000..bedfe9b759cb --- /dev/null +++ b/src/cdk-experimental/testing/public-api.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as protractor from './protractor'; +import * as testbed from './testbed'; + +export * from './component-harness'; +export * from './test-element'; +export {protractor, testbed}; 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 new file mode 100644 index 000000000000..d2731cf60b13 --- /dev/null +++ b/src/cdk-experimental/testing/testbed.ts @@ -0,0 +1,207 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { + dispatchFakeEvent, + dispatchKeyboardEvent, + dispatchMouseEvent, + triggerBlur, + triggerFocus +} from '@angular/cdk/testing'; +import {ComponentFixture} from '@angular/core/testing'; + +import { + ComponentHarness, + ComponentHarnessConstructor, + HarnessLocator, + QueryOptions +} from './component-harness'; +import {TestElement} from './test-element'; + +/** + * Component harness factory for testbed. + * @param componentHarness: Type of user defined harness. + * @param fixture: Component Fixture of the component to be tested. + */ +export function load( + componentHarness: ComponentHarnessConstructor, + fixture: ComponentFixture<{}>): T { + const stabilize = async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }; + return new componentHarness(new UnitTestLocator(fixture.nativeElement, stabilize)); +} + +/** + * Gets the corresponding Element for the root of a TestElement. + */ +export function getNativeElement(testElement: TestElement): Element { + if (testElement instanceof UnitTestElement) { + return testElement.element; + } + + throw Error(`Expected an instance of UnitTestElement, got ${testElement}`); +} + +/** + * Locator implementation for testbed. + * Note that, this locator is exposed for internal usage, please do not use it. + */ +export class UnitTestLocator implements HarnessLocator { + private readonly _rootElement: TestElement; + + constructor(private _root: Element, private _stabilize: () => Promise) { + this._rootElement = new UnitTestElement(_root, this._stabilize); + } + + host(): TestElement { + return this._rootElement; + } + + async querySelector(selector: string, options?: QueryOptions): Promise { + await this._stabilize(); + const e = getElement(selector, this._root, options); + return e && new UnitTestElement(e, this._stabilize); + } + + async querySelectorAll(selector: string): Promise { + await this._stabilize(); + return Array.prototype.map.call( + this._root.querySelectorAll(selector), + (e: Element) => new UnitTestElement(e, this._stabilize)); + } + + async load( + componentHarness: ComponentHarnessConstructor, selector: string, + options?: QueryOptions): Promise { + await this._stabilize(); + const root = getElement(selector, this._root, options); + return root && new componentHarness(new UnitTestLocator(root, this._stabilize)); + } + + async loadAll( + componentHarness: ComponentHarnessConstructor, + rootSelector: string): Promise { + await this._stabilize(); + return Array.prototype.map.call( + this._root.querySelectorAll(rootSelector), + (e: Element) => new componentHarness(new UnitTestLocator(e, this._stabilize))); + } +} + +class UnitTestElement implements TestElement { + constructor(readonly element: Element, private _stabilize: () => Promise) {} + + async blur(): Promise { + await this._stabilize(); + triggerBlur(this.element as HTMLElement); + await this._stabilize(); + } + + async clear(): Promise { + await this._stabilize(); + if (!this._isTextInput(this.element)) { + throw Error('Attempting to clear an invalid element'); + } + triggerFocus(this.element as HTMLElement); + this.element.value = ''; + dispatchFakeEvent(this.element, 'input'); + await this._stabilize(); + } + + async click(): Promise { + await this._stabilize(); + dispatchMouseEvent(this.element, 'click'); + await this._stabilize(); + } + + async focus(): Promise { + await this._stabilize(); + triggerFocus(this.element as HTMLElement); + await this._stabilize(); + } + + async getCssValue(property: string): Promise { + await this._stabilize(); + // TODO(mmalerba): Consider adding value normalization if we run into common cases where its + // needed. + return getComputedStyle(this.element).getPropertyValue(property); + } + + async hover(): Promise { + await this._stabilize(); + dispatchMouseEvent(this.element, 'mouseenter'); + await this._stabilize(); + } + + async sendKeys(keys: string): Promise { + await this._stabilize(); + triggerFocus(this.element as HTMLElement); + for (const key of keys) { + const keyCode = key.charCodeAt(0); + dispatchKeyboardEvent(this.element, 'keydown', keyCode); + dispatchKeyboardEvent(this.element, 'keypress', keyCode); + if (this._isTextInput(this.element)) { + this.element.value += key; + } + dispatchKeyboardEvent(this.element, 'keyup', keyCode); + if (this._isTextInput(this.element)) { + dispatchFakeEvent(this.element, 'input'); + } + } + await this._stabilize(); + } + + async text(): Promise { + await this._stabilize(); + return this.element.textContent || ''; + } + + async getAttribute(name: string): Promise { + await this._stabilize(); + let value = this.element.getAttribute(name); + // If cannot find attribute in the element, also try to find it in property, + // this is useful for input/textarea tags. + if (value === null && name in this.element) { + // We need to cast the element so we can access its properties via string indexing. + return (this.element as unknown as {[key: string]: string|null})[name]; + } + return value; + } + + private _isTextInput(element: Element): element is HTMLInputElement | HTMLTextAreaElement { + return element.nodeName.toLowerCase() === 'input' || + element.nodeName.toLowerCase() === 'textarea' ; + } +} + + +/** + * Get an element based on the CSS selector and root element. + * @param selector The CSS selector + * @param root Search element under the root element. If options.global is set, + * root is ignored. + * @param options Optional, extra searching options + * @return When element is not present, return null if Options.allowNull is set + * to true, throw an error if Options.allowNull is set to false; otherwise, + * return the element + */ +function getElement(selector: string, root: Element, options?: QueryOptions): Element|null { + const useGlobalRoot = options && options.global; + const elem = (useGlobalRoot ? document : root).querySelector(selector); + const allowNull = options !== undefined && options.allowNull !== undefined ? + options.allowNull : undefined; + if (elem === null) { + if (allowNull) { + return null; + } + throw Error('Cannot find element based on the CSS selector: ' + selector); + } + return elem; +} diff --git a/src/cdk-experimental/testing/tests/BUILD.bazel b/src/cdk-experimental/testing/tests/BUILD.bazel new file mode 100644 index 000000000000..19556f7d3d1d --- /dev/null +++ b/src/cdk-experimental/testing/tests/BUILD.bazel @@ -0,0 +1,49 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ng_e2e_test_library", "ng_module", "ng_test_library", "ts_library") + +ng_module( + name = "test_components", + srcs = glob( + ["**/*.ts"], + exclude = [ + "**/*.spec.ts", + "harnesses/**", + ], + ), + assets = glob(["**/*.html"]), + module_name = "@angular/cdk-experimental/testing/tests", + deps = [ + "@npm//@angular/forms", + ], +) + +ts_library( + name = "test_harnesses", + srcs = glob(["harnesses/**/*.ts"]), + deps = [ + "//src/cdk-experimental/testing", + ], +) + +ng_test_library( + name = "unit_test_sources", + srcs = glob( + ["**/*.spec.ts"], + exclude = ["**/*.e2e.spec.ts"], + ), + deps = [ + ":test_components", + ":test_harnesses", + "//src/cdk-experimental/testing", + ], +) + +ng_e2e_test_library( + name = "e2e_test_sources", + srcs = glob(["**/*.e2e.spec.ts"]), + deps = [ + ":test_harnesses", + "//src/cdk-experimental/testing", + ], +) diff --git a/src/cdk-experimental/testing/tests/harnesses/main-component-harness.ts b/src/cdk-experimental/testing/tests/harnesses/main-component-harness.ts new file mode 100644 index 000000000000..b31421814c90 --- /dev/null +++ b/src/cdk-experimental/testing/tests/harnesses/main-component-harness.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ComponentHarness} from '../../component-harness'; +import {TestElement} from '../../test-element'; +import {SubComponentHarness} from './sub-component-harness'; + +export class MainComponentHarness extends ComponentHarness { + readonly title = this.find('h1'); + readonly asyncCounter = this.find('#asyncCounter'); + readonly counter = this.find('#counter'); + readonly input = this.find('#input'); + readonly value = this.find('#value'); + readonly allLabels = this.findAll('label'); + readonly allLists = this.findAll(SubComponentHarness, 'test-sub'); + readonly memo = this.find('textarea'); + // Allow null for element + readonly nullItem = this.find('wrong locator', {allowNull: true}); + // Allow null for component harness + readonly nullComponentHarness = + this.find(SubComponentHarness, 'wrong locator', {allowNull: true}); + readonly errorItem = this.find('wrong locator', {allowNull: false}); + + readonly globalEl = this.find('.sibling', {global: true}); + readonly errorGlobalEl = + this.find('wrong locator', {global: true, allowNull: false}); + readonly nullGlobalEl = + this.find('wrong locator', {global: true, allowNull: true}); + + private _button = this.find('button'); + private _testTools = this.find(SubComponentHarness, 'test-sub'); + + async increaseCounter(times: number) { + const button = await this._button(); + for (let i = 0; i < times; i++) { + await button.click(); + } + } + + async getTestTool(index: number): Promise { + 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/tests/harnesses/sub-component-harness.ts b/src/cdk-experimental/testing/tests/harnesses/sub-component-harness.ts new file mode 100644 index 000000000000..a70ffd3418a3 --- /dev/null +++ b/src/cdk-experimental/testing/tests/harnesses/sub-component-harness.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 {ComponentHarness} from '../../component-harness'; +import {TestElement} from '../../test-element'; + +export class SubComponentHarness extends ComponentHarness { + readonly title = this.find('h2'); + readonly getItems = this.findAll('li'); + + async getItem(index: number): Promise { + const items = await this.getItems(); + return items[index]; + } +} diff --git a/src/cdk-experimental/testing/tests/index.ts b/src/cdk-experimental/testing/tests/index.ts new file mode 100644 index 000000000000..adfc214ca0e8 --- /dev/null +++ b/src/cdk-experimental/testing/tests/index.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './test-components-module'; +export * from './test-main-component'; +export * from './test-sub-component'; diff --git a/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts b/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts new file mode 100644 index 000000000000..cc70b96618da --- /dev/null +++ b/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts @@ -0,0 +1,183 @@ +import {browser, by, element} from 'protractor'; + +import {getElementFinder, load} from '../protractor'; +import {MainComponentHarness} from './harnesses/main-component-harness'; + +describe('Protractor Helper Test', () => { + let harness: MainComponentHarness; + + beforeEach(async () => { + await browser.get('/component-harness'); + harness = await load(MainComponentHarness, 'test-main'); + }); + + describe('Locator', () => { + it('should be able to locate a element based on CSS selector', async () => { + const title = await harness.title(); + expect(await title.text()).toBe('Main Component'); + }); + + it('should be able to locate all elements based on CSS selector', + async () => { + const labels = await harness.allLabels(); + expect(labels.length).toBe(2); + expect(await labels[0].text()).toBe('Count:'); + expect(await labels[1].text()).toBe('AsyncCounter:'); + }); + + it('should be able to locate the sub harnesses', async () => { + const items = await harness.getTestTools(); + expect(items.length).toBe(3); + expect(await items[0].text()).toBe('Protractor'); + expect(await items[1].text()).toBe('TestBed'); + expect(await items[2].text()).toBe('Other'); + }); + + it('should be able to locate all sub harnesses', async () => { + const alllists = await harness.allLists(); + const items1 = await alllists[0].getItems(); + const items2 = await alllists[1].getItems(); + expect(alllists.length).toBe(2); + expect(items1.length).toBe(3); + expect(await items1[0].text()).toBe('Protractor'); + expect(await items1[1].text()).toBe('TestBed'); + expect(await items1[2].text()).toBe('Other'); + expect(items2.length).toBe(3); + expect(await items2[0].text()).toBe('Unit Test'); + expect(await items2[1].text()).toBe('Integration Test'); + expect(await items2[2].text()).toBe('Performance Test'); + }); + }); + + describe('Test element', () => { + it('should be able to clear', async () => { + const input = await harness.input(); + await input.sendKeys('Yi'); + expect(await input.getAttribute('value')).toBe('Yi'); + + await input.clear(); + expect(await input.getAttribute('value')).toBe(''); + }); + + it('should be able to click', async () => { + const counter = await harness.counter(); + expect(await counter.text()).toBe('0'); + await harness.increaseCounter(3); + expect(await counter.text()).toBe('3'); + }); + + it('should be able to send key', async () => { + const input = await harness.input(); + const value = await harness.value(); + await input.sendKeys('Yi'); + + expect(await input.getAttribute('value')).toBe('Yi'); + expect(await value.text()).toBe('Input: Yi'); + }); + + it('focuses the element before sending key', async () => { + const input = await harness.input(); + await input.sendKeys('Yi'); + expect(await input.getAttribute('id')) + .toBe(await browser.driver.switchTo().activeElement().getAttribute( + 'id')); + }); + + it('should be able to hover', async () => { + const host = await harness.host(); + let classAttr = await host.getAttribute('class'); + expect(classAttr).not.toContain('hovering'); + await host.hover(); + classAttr = await host.getAttribute('class'); + expect(classAttr).toContain('hovering'); + }); + + it('should be able to getAttribute', async () => { + const memoStr = ` + This is an example that shows how to use component harness + You should use getAttribute('value') to retrieve the text in textarea + `; + const memo = await harness.memo(); + await memo.sendKeys(memoStr); + expect(await memo.getAttribute('value')).toBe(memoStr); + }); + + it('should be able to getCssValue', async () => { + const title = await harness.title(); + expect(await title.getCssValue('height')).toBe('50px'); + }); + }); + + describe('Async operation', () => { + it('should wait for async opeartion to complete', async () => { + const asyncCounter = await harness.asyncCounter(); + expect(await asyncCounter.text()).toBe('5'); + await harness.increaseCounter(3); + expect(await asyncCounter.text()).toBe('8'); + }); + }); + + describe('Allow null', () => { + it('should allow element to be null when setting allowNull', async () => { + expect(await harness.nullItem()).toBe(null); + }); + + it('should allow main harness to be null when setting allowNull', + async () => { + const nullMainHarness = await load( + MainComponentHarness, 'harness not present', {allowNull: true}); + expect(nullMainHarness).toBe(null); + }); + + it('should allow sub-harness to be null when setting allowNull', + async () => { + expect(await harness.nullComponentHarness()).toBe(null); + }); + }); + + describe('with the global option', () => { + it('should find an element outside the root of the harness', async () => { + const globalEl = await harness.globalEl(); + expect(await globalEl.text()).toBe('I am a sibling!'); + }); + + it('should return null for a selector that does not exist', async () => { + expect(await harness.nullGlobalEl()).toBeNull(); + }); + + it('should throw an error for a selctor that does not exist ' + + 'with allowNull = false', + async () => { + try { + await harness.errorGlobalEl(); + fail('Should throw error'); + } catch (err) { + expect(err.message) + .toBe( + 'Cannot find element based on the CSS selector: wrong locator'); + } + }); + }); + + describe('Throw error', () => { + it('should show the correct error', async () => { + try { + await harness.errorItem(); + fail('Should throw error'); + } catch (err) { + expect(err.message) + .toBe( + 'Cannot find element based on the CSS selector: wrong locator'); + } + }); + }); + + describe('getElementFinder', () => { + it('should return the element finder', async () => { + const mainElement = await element(by.css('test-main')); + const elementFromHarness = getElementFinder(harness.host()); + expect(await elementFromHarness.getId()) + .toBe(await mainElement.getId()); + }); + }); +}); 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/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 new file mode 100644 index 000000000000..355caf5e08b5 --- /dev/null +++ b/src/cdk-experimental/testing/tests/test-main-component.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ViewEncapsulation +} from '@angular/core'; + +@Component({ + moduleId: module.id, + selector: 'test-main', + templateUrl: 'test-main-component.html', + host: { + '[class.hovering]': '_isHovering', + '(mouseenter)': 'onMouseOver()', + '(mouseout)': 'onMouseOut()', + }, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, +}) + +export class TestMainComponent { + username: string; + counter: number; + asyncCounter: number; + input: string; + memo: string; + testTools: string[]; + testMethods: string[]; + _isHovering: boolean; + + onMouseOver() { + this._isHovering = true; + } + + onMouseOut() { + this._isHovering = false; + } + + constructor(private _cdr: ChangeDetectorRef) { + this.username = 'Yi'; + this.counter = 0; + this.asyncCounter = 0; + this.memo = ''; + this.testTools = ['Protractor', 'TestBed', 'Other']; + this.testMethods = ['Unit Test', 'Integration Test', 'Performance Test']; + setTimeout(() => { + this.asyncCounter = 5; + this._cdr.markForCheck(); + }, 1000); + } + + click() { + this.counter++; + setTimeout(() => { + this.asyncCounter++; + this._cdr.markForCheck(); + }, 500); + } +} diff --git a/src/cdk-experimental/testing/tests/test-sub-component.ts b/src/cdk-experimental/testing/tests/test-sub-component.ts new file mode 100644 index 000000000000..4e9b5051cb6b --- /dev/null +++ b/src/cdk-experimental/testing/tests/test-sub-component.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ChangeDetectionStrategy, Component, Input, ViewEncapsulation} from '@angular/core'; + +@Component({ + moduleId: module.id, + selector: 'test-sub', + template: ` +

List of {{title}}

+
    +
  • {{item}}
  • +
`, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TestSubComponent { + @Input() title: string; + @Input() items: string[]; +} diff --git a/src/cdk-experimental/testing/tests/testbed.spec.ts b/src/cdk-experimental/testing/tests/testbed.spec.ts new file mode 100644 index 000000000000..57a418e4b303 --- /dev/null +++ b/src/cdk-experimental/testing/tests/testbed.spec.ts @@ -0,0 +1,156 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {getNativeElement, load} from '../testbed'; +import {MainComponentHarness} from './harnesses/main-component-harness'; + +import {TestComponentsModule} from './test-components-module'; +import {TestMainComponent} from './test-main-component'; + +describe('Testbed Helper Test', () => { + let harness: MainComponentHarness; + let fixture: ComponentFixture<{}>; + beforeEach(async(() => { + TestBed + .configureTestingModule({ + imports: [TestComponentsModule], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(TestMainComponent); + harness = load(MainComponentHarness, fixture); + }); + })); + + describe('Locator', () => { + it('should be able to locate a element based on CSS selector', async () => { + const title = await harness.title(); + expect(await title.text()).toBe('Main Component'); + }); + + it('should be able to locate all elements based on CSS selector', + async () => { + const labels = await harness.allLabels(); + expect(labels.length).toBe(2); + expect(await labels[0].text()).toBe('Count:'); + expect(await labels[1].text()).toBe('AsyncCounter:'); + }); + + it('should be able to locate the sub harnesses', async () => { + const items = await harness.getTestTools(); + expect(items.length).toBe(3); + expect(await items[0].text()).toBe('Protractor'); + expect(await items[1].text()).toBe('TestBed'); + expect(await items[2].text()).toBe('Other'); + }); + + it('should be able to locate all sub harnesses', async () => { + const alllists = await harness.allLists(); + const items1 = await alllists[0].getItems(); + const items2 = await alllists[1].getItems(); + expect(alllists.length).toBe(2); + expect(items1.length).toBe(3); + expect(await items1[0].text()).toBe('Protractor'); + expect(await items1[1].text()).toBe('TestBed'); + expect(await items1[2].text()).toBe('Other'); + expect(items2.length).toBe(3); + expect(await items2[0].text()).toBe('Unit Test'); + expect(await items2[1].text()).toBe('Integration Test'); + expect(await items2[2].text()).toBe('Performance Test'); + }); + }); + + describe('Test element', () => { + it('should be able to clear', async () => { + const input = await harness.input(); + await input.sendKeys('Yi'); + expect(await input.getAttribute('value')).toBe('Yi'); + + await input.clear(); + expect(await input.getAttribute('value')).toBe(''); + }); + + it('should be able to click', async () => { + const counter = await harness.counter(); + expect(await counter.text()).toBe('0'); + await harness.increaseCounter(3); + expect(await counter.text()).toBe('3'); + }); + + it('should be able to send key', async () => { + const input = await harness.input(); + const value = await harness.value(); + await input.sendKeys('Yi'); + + expect(await input.getAttribute('value')).toBe('Yi'); + expect(await value.text()).toBe('Input: Yi'); + }); + + it('focuses the element before sending key', async () => { + const input = await harness.input(); + await input.sendKeys('Yi'); + expect(await input.getAttribute('id')) + .toBe(document.activeElement!.id); + }); + + it('should be able to hover', async () => { + const host = await harness.host(); + let classAttr = await host.getAttribute('class'); + expect(classAttr).not.toContain('hovering'); + await host.hover(); + classAttr = await host.getAttribute('class'); + expect(classAttr).toContain('hovering'); + }); + + it('should be able to getAttribute', async () => { + const memoStr = ` + This is an example that shows how to use component harness + You should use getAttribute('value') to retrieve the text in textarea + `; + const memo = await harness.memo(); + await memo.sendKeys(memoStr); + expect(await memo.getAttribute('value')).toBe(memoStr); + }); + + it('should be able to getCssValue', async () => { + const title = await harness.title(); + expect(await title.getCssValue('height')).toBe('50px'); + }); + }); + + describe('Async operation', () => { + it('should wait for async opeartion to complete', async () => { + const asyncCounter = await harness.asyncCounter(); + expect(await asyncCounter.text()).toBe('5'); + await harness.increaseCounter(3); + expect(await asyncCounter.text()).toBe('8'); + }); + }); + + describe('Allow null', () => { + it('should allow element to be null when setting allowNull', async () => { + expect(await harness.nullItem()).toBe(null); + }); + + it('should allow harness to be null when setting allowNull', async () => { + expect(await harness.nullComponentHarness()).toBe(null); + }); + }); + + describe('Throw error', () => { + it('should show the correct error', async () => { + try { + await harness.errorItem(); + fail('Should throw error'); + } catch (err) { + expect(err.message) + .toBe( + 'Cannot find element based on the CSS selector: wrong locator'); + } + }); + }); + + describe('getNativeElement', () => { + it('should return the native element', async () => { + expect(getNativeElement(harness.host())).toBe(fixture.nativeElement); + }); + }); +}); 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/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..6a35c2866688 100644 --- a/src/e2e-app/main-module.ts +++ b/src/e2e-app/main-module.ts @@ -9,6 +9,7 @@ import {ButtonToggleE2eModule} from './button-toggle/button-toggle-e2e-module'; import {ButtonE2eModule} from './button/button-e2e-module'; import {CardE2eModule} from './card/card-e2e-module'; import {CheckboxE2eModule} from './checkbox/checkbox-e2e-module'; +import {ComponentHarnessE2eModule} from './component-harness/component-harness-e2e-module'; import {DialogE2eModule} from './dialog/dialog-e2e-module'; import {E2eApp} from './e2e-app'; import {E2eAppModule} from './e2e-app/e2e-app-module'; @@ -49,6 +50,7 @@ import {VirtualScrollE2eModule} from './virtual-scroll/virtual-scroll-e2e-module ButtonToggleE2eModule, CardE2eModule, CheckboxE2eModule, + ComponentHarnessE2eModule, DialogE2eModule, ExpansionE2eModule, GridListE2eModule, 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..ac4b99811280 100644 --- a/tools/package-tools/rollup-globals.ts +++ b/tools/package-tools/rollup-globals.ts @@ -47,9 +47,9 @@ const rollupCdkExperimentalEntryPoints = /** Map of globals that are used inside of the different packages. */ export const rollupGlobals = { 'moment': 'moment', - 'tslib': 'tslib', 'protractor': 'protractor', 'selenium-webdriver': 'selenium-webdriver', + 'tslib': 'tslib', // MDC Web '@material/animation': 'mdc.animation',