Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions src/cdk-experimental/testing/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
],
)
190 changes: 190 additions & 0 deletions src/cdk-experimental/testing/component-harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {TestElement} from './test-element';

/** Options that can be specified when querying for an Element. */
export interface QueryOptions {
/**
* Whether the found element can be null. If allowNull is set, the searching function will always
* try to fetch the element at once. When the element cannot be found, the searching function
* should return null if allowNull is set to true, throw an error if allowNull is set to false.
* If allowNull is not set, the framework will choose the behaviors that make more sense for each
* test type (e.g. for unit test, the framework will make sure the element is not null; otherwise
* throw an error); however, the internal behavior is not guaranteed and user should not rely on
* it. Note that in most cases, you don't need to care about whether an element is present when
* loading the element and don't need to set this parameter unless you do want to check whether
* the element is present when calling the searching function. e.g. you want to make sure some
* element is not there when loading the element in order to check whether a "ngif" works well.
*/
allowNull?: boolean;
/**
* If global is set to true, the selector will match any element on the page and is not limited to
* the root of the harness. If global is unset or set to false, the selector will only find
* elements under the current root.
*/
global?: boolean;
}

/** Interface that is used to find elements in the DOM and create harnesses for them. */
export interface HarnessLocator {
/**
* Get the host element of locator.
*/
host(): TestElement;

/**
* Search the first matched test element.
* @param selector The CSS selector of the test elements.
* @param options Optional, extra searching options
*/
querySelector(selector: string, options?: QueryOptions): Promise<TestElement|null>;

/**
* Search all matched test elements under current root by CSS selector.
* @param selector The CSS selector of the test elements.
*/
querySelectorAll(selector: string): Promise<TestElement[]>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should querySelectorAll accept options as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm planning to address this when I make some API changes in the next PR. I think this is currently implemented like this because allowNull doesn't really make sense for querySelectorAll


/**
* Load the first matched Component Harness.
* @param componentHarness Type of user customized harness.
* @param root CSS root selector of the new component harness.
* @param options Optional, extra searching options
*/
load<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>, root: string,
options?: QueryOptions): Promise<T|null>;

/**
* Load all Component Harnesses under current root.
* @param componentHarness Type of user customized harness.
* @param root CSS root selector of the new component harnesses.
*/
loadAll<T extends ComponentHarness>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why no options on this one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, my guess is maybe because allowNull doesn't make sense for loadAll, global would still make sense though. We can discuss what the load APIs should look like, there's some changes I want to make anyways.

componentHarness: ComponentHarnessConstructor<T>, root: string): Promise<T[]>;
}

/**
* Base Component Harness
* This base component harness provides the basic ability to locate element and
* sub-component harness. It should be inherited when defining user's own
* harness.
*/
export abstract class ComponentHarness {
constructor(private readonly locator: HarnessLocator) {}

/**
* Get the host element of component harness.
*/
host(): TestElement {
return this.locator.host();
}

/**
* Generate a function to find the first matched test element by CSS
* selector.
* @param selector The CSS selector of the test element.
*/
protected find(selector: string): () => Promise<TestElement>;

/**
* Generate a function to find the first matched test element by CSS
* selector.
* @param selector The CSS selector of the test element.
* @param options Extra searching options
*/
protected find(selector: string, options: QueryOptions & {allowNull: true}):
() => Promise<TestElement|null>;

/**
* Generate a function to find the first matched test element by CSS
* selector.
* @param selector The CSS selector of the test element.
* @param options Extra searching options
*/
protected find(selector: string, options: QueryOptions): () => Promise<TestElement>;

/**
* Generate a function to find the first matched Component Harness.
* @param componentHarness Type of user customized harness.
* @param root CSS root selector of the new component harness.
*/
protected find<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>,
root: string): () => Promise<T>;

/**
* Generate a function to find the first matched Component Harness.
* @param componentHarness Type of user customized harness.
* @param root CSS root selector of the new component harness.
* @param options Extra searching options
*/
protected find<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>, root: string,
options: QueryOptions & {allowNull: true}): () => Promise<T|null>;

/**
* Generate a function to find the first matched Component Harness.
* @param componentHarness Type of user customized harness.
* @param root CSS root selector of the new component harness.
* @param options Extra searching options
*/
protected find<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>, root: string,
options: QueryOptions): () => Promise<T>;

protected find<T extends ComponentHarness>(
selectorOrComponentHarness: string|ComponentHarnessConstructor<T>,
selectorOrOptions?: string|QueryOptions,
options?: QueryOptions): () => Promise<TestElement|T|null> {
if (typeof selectorOrComponentHarness === 'string') {
const selector = selectorOrComponentHarness;
return () => this.locator.querySelector(selector, selectorOrOptions as QueryOptions);
} else {
const componentHarness = selectorOrComponentHarness;
const selector = selectorOrOptions as string;
return () => this.locator.load(componentHarness, selector, options);
}
}

/**
* Generate a function to find all matched test elements by CSS selector.
* @param selector The CSS root selector of elements. It will locate
* elements under the current root.
*/
protected findAll(selector: string): () => Promise<TestElement[]>;

/**
* Generate a function to find all Component Harnesses under current
* component harness.
* @param componentHarness Type of user customized harness.
* @param root CSS root selector of the new component harnesses. It will
* locate harnesses under the current root.
*/
protected findAll<T extends ComponentHarness>(
componentHarness: ComponentHarnessConstructor<T>,
root: string): () => Promise<T[]>;

protected findAll<T extends ComponentHarness>(
selectorOrComponentHarness: string|ComponentHarnessConstructor<T>,
root?: string): () => Promise<TestElement[]|T[]> {
if (typeof selectorOrComponentHarness === 'string') {
const selector = selectorOrComponentHarness;
return () => this.locator.querySelectorAll(selector);
} else {
const componentHarness = selectorOrComponentHarness;
return () => this.locator.loadAll(componentHarness, root as string);
}
}
}

/** Constructor for a ComponentHarness subclass. */
export interface ComponentHarnessConstructor<T extends ComponentHarness> {
new(locator: HarnessLocator): T;
}
9 changes: 9 additions & 0 deletions src/cdk-experimental/testing/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Loading