Skip to content

Commit

Permalink
feat(cdk-experimental/testing): Adds a HarnessPredicate class (angu…
Browse files Browse the repository at this point in the history
…lar#16319)

This enables users of harness authors to provide an API for querying harnesses based on arbitrary state as given by predicate functions.
  • Loading branch information
mmalerba authored and jelbourn committed Jun 19, 2019
1 parent 4b82786 commit 6a7fc81
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 80 deletions.
127 changes: 106 additions & 21 deletions src/cdk-experimental/testing/component-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@
import {TestElement} from './test-element';

/** An async function that returns a promise when called. */
export type AsyncFn<T> = () => Promise<T>;
export type AsyncFactoryFn<T> = () => Promise<T>;

/** An async function that takes an item and returns a boolean promise */
export type AsyncPredicate<T> = (item: T) => Promise<boolean>;

/** An async function that takes an item and an option value and returns a boolean promise. */
export type AsyncOptionPredicate<T, O> = (item: T, option: O) => Promise<boolean>;

/**
* Interface used to load ComponentHarness objects. This interface is used by test authors to
Expand Down Expand Up @@ -44,17 +50,17 @@ export interface HarnessLoader {
* @return An instance of the given harness type
* @throws If a matching component instance can't be found.
*/
getHarness<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T>):
Promise<T>;
getHarness<T extends ComponentHarness>(
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): Promise<T>;

/**
* Searches for all instances of the component corresponding to the given harness type under the
* `HarnessLoader`'s root element, and returns a list `ComponentHarness` for each instance.
* @param harnessType The type of harness to create
* @return A list instances of the given harness type.
*/
getAllHarnesses<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T>):
Promise<T[]>;
getAllHarnesses<T extends ComponentHarness>(
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): Promise<T[]>;
}

/**
Expand All @@ -78,7 +84,7 @@ export interface LocatorFactory {
* @return An asynchronous locator function that searches for elements with the given selector,
* and either finds one or throws an error
*/
locatorFor(selector: string): AsyncFn<TestElement>;
locatorFor(selector: string): AsyncFactoryFn<TestElement>;

/**
* Creates an asynchronous locator function that can be used to find a `ComponentHarness` for a
Expand All @@ -89,8 +95,8 @@ export interface LocatorFactory {
* @return An asynchronous locator function that searches components matching the given harness
* type, and either returns a `ComponentHarness` for the component, or throws an error.
*/
locatorFor<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T>):
AsyncFn<T>;
locatorFor<T extends ComponentHarness>(
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T>;

/**
* Creates an asynchronous locator function that can be used to search for elements with the given
Expand All @@ -101,7 +107,7 @@ export interface LocatorFactory {
* @return An asynchronous locator function that searches for elements with the given selector,
* and either finds one or returns null.
*/
locatorForOptional(selector: string): AsyncFn<TestElement | null>;
locatorForOptional(selector: string): AsyncFactoryFn<TestElement | null>;

/**
* Creates an asynchronous locator function that can be used to find a `ComponentHarness` for a
Expand All @@ -112,8 +118,8 @@ export interface LocatorFactory {
* @return An asynchronous locator function that searches components matching the given harness
* type, and either returns a `ComponentHarness` for the component, or null if none is found.
*/
locatorForOptional<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T>):
AsyncFn<T | null>;
locatorForOptional<T extends ComponentHarness>(
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T | null>;

/**
* Creates an asynchronous locator function that can be used to search for a list of elements with
Expand All @@ -123,7 +129,7 @@ export interface LocatorFactory {
* @return An asynchronous locator function that searches for elements with the given selector,
* and either finds one or throws an error
*/
locatorForAll(selector: string): AsyncFn<TestElement[]>;
locatorForAll(selector: string): AsyncFactoryFn<TestElement[]>;

/**
* Creates an asynchronous locator function that can be used to find a list of
Expand All @@ -134,8 +140,8 @@ export interface LocatorFactory {
* @return An asynchronous locator function that searches components matching the given harness
* type, and returns a list of `ComponentHarness`es.
*/
locatorForAll<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T>):
AsyncFn<T[]>;
locatorForAll<T extends ComponentHarness>(
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T[]>;
}

/**
Expand Down Expand Up @@ -169,7 +175,7 @@ export abstract class ComponentHarness {
* @return An asynchronous locator function that searches for elements with the given selector,
* and either finds one or throws an error
*/
protected locatorFor(selector: string): AsyncFn<TestElement>;
protected locatorFor(selector: string): AsyncFactoryFn<TestElement>;

/**
* Creates an asynchronous locator function that can be used to find a `ComponentHarness` for a
Expand All @@ -181,7 +187,7 @@ export abstract class ComponentHarness {
* type, and either returns a `ComponentHarness` for the component, or throws an error.
*/
protected locatorFor<T extends ComponentHarness>(
harnessType: ComponentHarnessConstructor<T>): AsyncFn<T>;
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T>;

protected locatorFor(arg: any): any {
return this.locatorFactory.locatorFor(arg);
Expand All @@ -196,7 +202,7 @@ export abstract class ComponentHarness {
* @return An asynchronous locator function that searches for elements with the given selector,
* and either finds one or returns null.
*/
protected locatorForOptional(selector: string): AsyncFn<TestElement | null>;
protected locatorForOptional(selector: string): AsyncFactoryFn<TestElement | null>;

/**
* Creates an asynchronous locator function that can be used to find a `ComponentHarness` for a
Expand All @@ -208,7 +214,7 @@ export abstract class ComponentHarness {
* type, and either returns a `ComponentHarness` for the component, or null if none is found.
*/
protected locatorForOptional<T extends ComponentHarness>(
harnessType: ComponentHarnessConstructor<T>): AsyncFn<T | null>;
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T | null>;

protected locatorForOptional(arg: any): any {
return this.locatorFactory.locatorForOptional(arg);
Expand All @@ -222,7 +228,7 @@ export abstract class ComponentHarness {
* @return An asynchronous locator function that searches for elements with the given selector,
* and either finds one or throws an error
*/
protected locatorForAll(selector: string): AsyncFn<TestElement[]>;
protected locatorForAll(selector: string): AsyncFactoryFn<TestElement[]>;

/**
* Creates an asynchronous locator function that can be used to find a list of
Expand All @@ -233,8 +239,8 @@ export abstract class ComponentHarness {
* @return An asynchronous locator function that searches components matching the given harness
* type, and returns a list of `ComponentHarness`es.
*/
protected locatorForAll<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T>):
AsyncFn<T[]>;
protected locatorForAll<T extends ComponentHarness>(
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): AsyncFactoryFn<T[]>;

protected locatorForAll(arg: any): any {
return this.locatorFactory.locatorForAll(arg);
Expand All @@ -252,3 +258,82 @@ export interface ComponentHarnessConstructor<T extends ComponentHarness> {
*/
hostSelector: string;
}

/**
* A class used to associate a ComponentHarness class with predicates functions that can be used to
* filter instances of the class.
*/
export class HarnessPredicate<T extends ComponentHarness> {
private _predicates: AsyncPredicate<T>[] = [];
private _descriptions: string[] = [];

constructor(public harnessType: ComponentHarnessConstructor<T>) {}

/**
* Checks if a string matches the given pattern.
* @param s The string to check, or a Promise for the string to check.
* @param pattern The pattern the string is expected to match. If `pattern` is a string, `s` is
* expected to match exactly. If `pattern` is a regex, a partial match is allowed.
* @return A Promise that resolves to whether the string matches the pattern.
*/
static async stringMatches(s: string | Promise<string>, pattern: string | RegExp):
Promise<boolean> {
s = await s;
return typeof pattern === 'string' ? s === pattern : pattern.test(s);
}

/**
* Adds a predicate function to be run against candidate harnesses.
* @param description A description of this predicate that may be used in error messages.
* @param predicate An async predicate function.
* @return this (for method chaining).
*/
add(description: string, predicate: AsyncPredicate<T>) {
this._descriptions.push(description);
this._predicates.push(predicate);
return this;
}

/**
* Adds a predicate function that depends on an option value to be run against candidate
* harnesses. If the option value is undefined, the predicate will be ignored.
* @param name The name of the option (may be used in error messages).
* @param option The option value.
* @param predicate The predicate function to run if the option value is not undefined.
* @return this (for method chaining).
*/
addOption<O>(name: string, option: O | undefined, predicate: AsyncOptionPredicate<T, O>) {
// Add quotes around strings to differentiate them from other values
const value = typeof option === 'string' ? `"${option}"` : `${option}`;
if (option !== undefined) {
this.add(`${name} = ${value}`, item => predicate(item, option));
}
return this;
}

/**
* Filters a list of harnesses on this predicate.
* @param harnesses The list of harnesses to filter.
* @return A list of harnesses that satisfy this predicate.
*/
async filter(harnesses: T[]): Promise<T[]> {
const results = await Promise.all(harnesses.map(h => this.evaluate(h)));
return harnesses.filter((_, i) => results[i]);
}

/**
* Evaluates whether the given harness satisfies this predicate.
* @param harness The harness to check
* @return A promise that resolves to true if the harness satisfies this predicate,
* and resolves to false otherwise.
*/
async evaluate(harness: T): Promise<boolean> {
const results = await Promise.all(this._predicates.map(p => p(harness)));
return results.reduce((combined, current) => combined && current, true);
}

/** Gets a description of this predicate for use in error messages. */
getDescription() {
return this._descriptions.join(', ');
}
}
Loading

0 comments on commit 6a7fc81

Please sign in to comment.