Skip to content

Commit 6a7fc81

Browse files
mmalerbajelbourn
authored andcommitted
feat(cdk-experimental/testing): Adds a HarnessPredicate class (#16319)
This enables users of harness authors to provide an API for querying harnesses based on arbitrary state as given by predicate functions.
1 parent 4b82786 commit 6a7fc81

File tree

9 files changed

+296
-80
lines changed

9 files changed

+296
-80
lines changed

src/cdk-experimental/testing/component-harness.ts

Lines changed: 106 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@
99
import {TestElement} from './test-element';
1010

1111
/** An async function that returns a promise when called. */
12-
export type AsyncFn<T> = () => Promise<T>;
12+
export type AsyncFactoryFn<T> = () => Promise<T>;
13+
14+
/** An async function that takes an item and returns a boolean promise */
15+
export type AsyncPredicate<T> = (item: T) => Promise<boolean>;
16+
17+
/** An async function that takes an item and an option value and returns a boolean promise. */
18+
export type AsyncOptionPredicate<T, O> = (item: T, option: O) => Promise<boolean>;
1319

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

5056
/**
5157
* Searches for all instances of the component corresponding to the given harness type under the
5258
* `HarnessLoader`'s root element, and returns a list `ComponentHarness` for each instance.
5359
* @param harnessType The type of harness to create
5460
* @return A list instances of the given harness type.
5561
*/
56-
getAllHarnesses<T extends ComponentHarness>(harnessType: ComponentHarnessConstructor<T>):
57-
Promise<T[]>;
62+
getAllHarnesses<T extends ComponentHarness>(
63+
harnessType: ComponentHarnessConstructor<T> | HarnessPredicate<T>): Promise<T[]>;
5864
}
5965

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

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

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

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

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

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

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

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

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

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

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

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

239245
protected locatorForAll(arg: any): any {
240246
return this.locatorFactory.locatorForAll(arg);
@@ -252,3 +258,82 @@ export interface ComponentHarnessConstructor<T extends ComponentHarness> {
252258
*/
253259
hostSelector: string;
254260
}
261+
262+
/**
263+
* A class used to associate a ComponentHarness class with predicates functions that can be used to
264+
* filter instances of the class.
265+
*/
266+
export class HarnessPredicate<T extends ComponentHarness> {
267+
private _predicates: AsyncPredicate<T>[] = [];
268+
private _descriptions: string[] = [];
269+
270+
constructor(public harnessType: ComponentHarnessConstructor<T>) {}
271+
272+
/**
273+
* Checks if a string matches the given pattern.
274+
* @param s The string to check, or a Promise for the string to check.
275+
* @param pattern The pattern the string is expected to match. If `pattern` is a string, `s` is
276+
* expected to match exactly. If `pattern` is a regex, a partial match is allowed.
277+
* @return A Promise that resolves to whether the string matches the pattern.
278+
*/
279+
static async stringMatches(s: string | Promise<string>, pattern: string | RegExp):
280+
Promise<boolean> {
281+
s = await s;
282+
return typeof pattern === 'string' ? s === pattern : pattern.test(s);
283+
}
284+
285+
/**
286+
* Adds a predicate function to be run against candidate harnesses.
287+
* @param description A description of this predicate that may be used in error messages.
288+
* @param predicate An async predicate function.
289+
* @return this (for method chaining).
290+
*/
291+
add(description: string, predicate: AsyncPredicate<T>) {
292+
this._descriptions.push(description);
293+
this._predicates.push(predicate);
294+
return this;
295+
}
296+
297+
/**
298+
* Adds a predicate function that depends on an option value to be run against candidate
299+
* harnesses. If the option value is undefined, the predicate will be ignored.
300+
* @param name The name of the option (may be used in error messages).
301+
* @param option The option value.
302+
* @param predicate The predicate function to run if the option value is not undefined.
303+
* @return this (for method chaining).
304+
*/
305+
addOption<O>(name: string, option: O | undefined, predicate: AsyncOptionPredicate<T, O>) {
306+
// Add quotes around strings to differentiate them from other values
307+
const value = typeof option === 'string' ? `"${option}"` : `${option}`;
308+
if (option !== undefined) {
309+
this.add(`${name} = ${value}`, item => predicate(item, option));
310+
}
311+
return this;
312+
}
313+
314+
/**
315+
* Filters a list of harnesses on this predicate.
316+
* @param harnesses The list of harnesses to filter.
317+
* @return A list of harnesses that satisfy this predicate.
318+
*/
319+
async filter(harnesses: T[]): Promise<T[]> {
320+
const results = await Promise.all(harnesses.map(h => this.evaluate(h)));
321+
return harnesses.filter((_, i) => results[i]);
322+
}
323+
324+
/**
325+
* Evaluates whether the given harness satisfies this predicate.
326+
* @param harness The harness to check
327+
* @return A promise that resolves to true if the harness satisfies this predicate,
328+
* and resolves to false otherwise.
329+
*/
330+
async evaluate(harness: T): Promise<boolean> {
331+
const results = await Promise.all(this._predicates.map(p => p(harness)));
332+
return results.reduce((combined, current) => combined && current, true);
333+
}
334+
335+
/** Gets a description of this predicate for use in error messages. */
336+
getDescription() {
337+
return this._descriptions.join(', ');
338+
}
339+
}

0 commit comments

Comments
 (0)