From 6a7fc8156e99c9d3e48e18ab7eef80c25a5c87fc Mon Sep 17 00:00:00 2001 From: mmalerba Date: Wed, 19 Jun 2019 13:48:33 -0700 Subject: [PATCH] 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. --- .../testing/component-harness.ts | 127 +++++++++++++++--- .../testing/harness-environment.ts | 96 ++++++++----- .../protractor-harness-environment.ts | 15 +-- .../testbed/testbed-harness-environment.ts | 5 - .../tests/harnesses/main-component-harness.ts | 8 ++ .../tests/harnesses/sub-component-harness.ts | 12 +- .../testing/tests/protractor.e2e.spec.ts | 55 +++++++- .../testing/tests/test-main-component.ts | 2 +- .../testing/tests/testbed.spec.ts | 56 +++++++- 9 files changed, 296 insertions(+), 80 deletions(-) diff --git a/src/cdk-experimental/testing/component-harness.ts b/src/cdk-experimental/testing/component-harness.ts index 65c50e671002..8c9fd6384449 100644 --- a/src/cdk-experimental/testing/component-harness.ts +++ b/src/cdk-experimental/testing/component-harness.ts @@ -9,7 +9,13 @@ import {TestElement} from './test-element'; /** An async function that returns a promise when called. */ -export type AsyncFn = () => Promise; +export type AsyncFactoryFn = () => Promise; + +/** An async function that takes an item and returns a boolean promise */ +export type AsyncPredicate = (item: T) => Promise; + +/** An async function that takes an item and an option value and returns a boolean promise. */ +export type AsyncOptionPredicate = (item: T, option: O) => Promise; /** * Interface used to load ComponentHarness objects. This interface is used by test authors to @@ -44,8 +50,8 @@ export interface HarnessLoader { * @return An instance of the given harness type * @throws If a matching component instance can't be found. */ - getHarness(harnessType: ComponentHarnessConstructor): - Promise; + getHarness( + harnessType: ComponentHarnessConstructor | HarnessPredicate): Promise; /** * Searches for all instances of the component corresponding to the given harness type under the @@ -53,8 +59,8 @@ export interface HarnessLoader { * @param harnessType The type of harness to create * @return A list instances of the given harness type. */ - getAllHarnesses(harnessType: ComponentHarnessConstructor): - Promise; + getAllHarnesses( + harnessType: ComponentHarnessConstructor | HarnessPredicate): Promise; } /** @@ -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; + locatorFor(selector: string): AsyncFactoryFn; /** * Creates an asynchronous locator function that can be used to find a `ComponentHarness` for a @@ -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(harnessType: ComponentHarnessConstructor): - AsyncFn; + locatorFor( + harnessType: ComponentHarnessConstructor | HarnessPredicate): AsyncFactoryFn; /** * Creates an asynchronous locator function that can be used to search for elements with the given @@ -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; + locatorForOptional(selector: string): AsyncFactoryFn; /** * Creates an asynchronous locator function that can be used to find a `ComponentHarness` for a @@ -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(harnessType: ComponentHarnessConstructor): - AsyncFn; + locatorForOptional( + harnessType: ComponentHarnessConstructor | HarnessPredicate): AsyncFactoryFn; /** * Creates an asynchronous locator function that can be used to search for a list of elements with @@ -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; + locatorForAll(selector: string): AsyncFactoryFn; /** * Creates an asynchronous locator function that can be used to find a list of @@ -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(harnessType: ComponentHarnessConstructor): - AsyncFn; + locatorForAll( + harnessType: ComponentHarnessConstructor | HarnessPredicate): AsyncFactoryFn; } /** @@ -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; + protected locatorFor(selector: string): AsyncFactoryFn; /** * Creates an asynchronous locator function that can be used to find a `ComponentHarness` for a @@ -181,7 +187,7 @@ export abstract class ComponentHarness { * type, and either returns a `ComponentHarness` for the component, or throws an error. */ protected locatorFor( - harnessType: ComponentHarnessConstructor): AsyncFn; + harnessType: ComponentHarnessConstructor | HarnessPredicate): AsyncFactoryFn; protected locatorFor(arg: any): any { return this.locatorFactory.locatorFor(arg); @@ -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; + protected locatorForOptional(selector: string): AsyncFactoryFn; /** * Creates an asynchronous locator function that can be used to find a `ComponentHarness` for a @@ -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( - harnessType: ComponentHarnessConstructor): AsyncFn; + harnessType: ComponentHarnessConstructor | HarnessPredicate): AsyncFactoryFn; protected locatorForOptional(arg: any): any { return this.locatorFactory.locatorForOptional(arg); @@ -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; + protected locatorForAll(selector: string): AsyncFactoryFn; /** * Creates an asynchronous locator function that can be used to find a list of @@ -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(harnessType: ComponentHarnessConstructor): - AsyncFn; + protected locatorForAll( + harnessType: ComponentHarnessConstructor | HarnessPredicate): AsyncFactoryFn; protected locatorForAll(arg: any): any { return this.locatorFactory.locatorForAll(arg); @@ -252,3 +258,82 @@ export interface ComponentHarnessConstructor { */ 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 { + private _predicates: AsyncPredicate[] = []; + private _descriptions: string[] = []; + + constructor(public harnessType: ComponentHarnessConstructor) {} + + /** + * 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, pattern: string | RegExp): + Promise { + 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) { + 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(name: string, option: O | undefined, predicate: AsyncOptionPredicate) { + // 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 { + 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 { + 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(', '); + } +} diff --git a/src/cdk-experimental/testing/harness-environment.ts b/src/cdk-experimental/testing/harness-environment.ts index ff243d9dd07e..757f3afd558d 100644 --- a/src/cdk-experimental/testing/harness-environment.ts +++ b/src/cdk-experimental/testing/harness-environment.ts @@ -7,14 +7,33 @@ */ import { - AsyncFn, + AsyncFactoryFn, ComponentHarness, ComponentHarnessConstructor, HarnessLoader, + HarnessPredicate, LocatorFactory } from './component-harness'; import {TestElement} from './test-element'; +function _getErrorForMissingSelector(selector: string): Error { + return Error(`Expected to find element matching selector: "${selector}", but none was found`); +} + +function _getErrorForMissingHarness( + harnessType: ComponentHarnessConstructor | HarnessPredicate): Error { + const harnessPredicate = + harnessType instanceof HarnessPredicate ? harnessType : new HarnessPredicate(harnessType); + const {name, hostSelector} = harnessPredicate.harnessType; + const restrictions = harnessPredicate.getDescription(); + let message = `Expected to find element for ${name} matching selector: "${hostSelector}"`; + if (restrictions) { + message += ` (with restrictions: ${restrictions})`; + } + message += ', but none was found'; + return Error(message); +} + /** * Base harness environment class that can be extended to allow `ComponentHarness`es to be used in * different test environments (e.g. testbed, protractor, etc.). This class implements the @@ -35,80 +54,82 @@ export abstract class HarnessEnvironment implements HarnessLoader, LocatorFac } // Implemented as part of the `LocatorFactory` interface. - locatorFor(selector: string): AsyncFn; - locatorFor(harnessType: ComponentHarnessConstructor): - AsyncFn; + locatorFor(selector: string): AsyncFactoryFn; + locatorFor( + harnessType: ComponentHarnessConstructor | HarnessPredicate): AsyncFactoryFn; locatorFor( - arg: string | ComponentHarnessConstructor): AsyncFn { + arg: string | ComponentHarnessConstructor | HarnessPredicate): + AsyncFactoryFn { return async () => { if (typeof arg === 'string') { - const element = await this.getRawElement(arg); + const element = (await this.getAllRawElements(arg))[0]; if (element) { return this.createTestElement(element); } + throw _getErrorForMissingSelector(arg); } else { - const element = await this.getRawElement(arg.hostSelector); - if (element) { - return this.createComponentHarness(arg, element); + const harness = (await this._getAllHarnesses(arg))[0]; + if (harness) { + return harness; } + throw _getErrorForMissingHarness(arg); } - const selector = typeof arg === 'string' ? arg : arg.hostSelector; - throw Error(`Expected to find element matching selector: "${selector}", but none was found`); }; } // Implemented as part of the `LocatorFactory` interface. - locatorForOptional(selector: string): AsyncFn; - locatorForOptional(harnessType: ComponentHarnessConstructor): - AsyncFn; + locatorForOptional(selector: string): AsyncFactoryFn; locatorForOptional( - arg: string | ComponentHarnessConstructor): AsyncFn { + harnessType: ComponentHarnessConstructor | HarnessPredicate): AsyncFactoryFn; + locatorForOptional( + arg: string | ComponentHarnessConstructor | HarnessPredicate): + AsyncFactoryFn { return async () => { if (typeof arg === 'string') { - const element = await this.getRawElement(arg); + const element = (await this.getAllRawElements(arg))[0]; return element ? this.createTestElement(element) : null; } else { - const element = await this.getRawElement(arg.hostSelector); - return element ? this.createComponentHarness(arg, element) : null; + const candidates = await this._getAllHarnesses(arg); + return candidates[0] || null; } }; } // Implemented as part of the `LocatorFactory` interface. - locatorForAll(selector: string): AsyncFn; - locatorForAll(harnessType: ComponentHarnessConstructor): - AsyncFn; + locatorForAll(selector: string): AsyncFactoryFn; + locatorForAll( + harnessType: ComponentHarnessConstructor | HarnessPredicate): AsyncFactoryFn; locatorForAll( - arg: string | ComponentHarnessConstructor): AsyncFn { + arg: string | ComponentHarnessConstructor | HarnessPredicate): + AsyncFactoryFn { return async () => { if (typeof arg === 'string') { return (await this.getAllRawElements(arg)).map(e => this.createTestElement(e)); } else { - return (await this.getAllRawElements(arg.hostSelector)) - .map(e => this.createComponentHarness(arg, e)); + return this._getAllHarnesses(arg); } }; } // Implemented as part of the `HarnessLoader` interface. - getHarness(harnessType: ComponentHarnessConstructor): - Promise { + getHarness( + harnessType: ComponentHarnessConstructor | HarnessPredicate): Promise { return this.locatorFor(harnessType)(); } // Implemented as part of the `HarnessLoader` interface. - getAllHarnesses(harnessType: ComponentHarnessConstructor): - Promise { + getAllHarnesses( + harnessType: ComponentHarnessConstructor | HarnessPredicate): Promise { return this.locatorForAll(harnessType)(); } // Implemented as part of the `HarnessLoader` interface. async getChildLoader(selector: string): Promise { - const element = await this.getRawElement(selector); + const element = (await this.getAllRawElements(selector))[0]; if (element) { return this.createEnvironment(element); } - throw Error(`Expected to find element matching selector: "${selector}", but none was found`); + throw _getErrorForMissingSelector(selector); } // Implemented as part of the `HarnessLoader` interface. @@ -131,14 +152,17 @@ export abstract class HarnessEnvironment implements HarnessLoader, LocatorFac /** Creates a `HarnessLoader` rooted at the given raw element. */ protected abstract createEnvironment(element: E): HarnessEnvironment; - /** - * Gets the first element matching the given selector under this environment's root element, or - * null if no elements match. - */ - protected abstract getRawElement(selector: string): Promise; - /** * Gets a list of all elements matching the given selector under this environment's root element. */ protected abstract getAllRawElements(selector: string): Promise; + + private async _getAllHarnesses( + harnessType: ComponentHarnessConstructor | HarnessPredicate): Promise { + const harnessPredicate = + harnessType instanceof HarnessPredicate ? harnessType : new HarnessPredicate(harnessType); + const elements = await this.getAllRawElements(harnessPredicate.harnessType.hostSelector); + return harnessPredicate.filter(elements.map( + element => this.createComponentHarness(harnessPredicate.harnessType, element))); + } } diff --git a/src/cdk-experimental/testing/protractor/protractor-harness-environment.ts b/src/cdk-experimental/testing/protractor/protractor-harness-environment.ts index 3a8fec9a4774..3b6c097cf15b 100644 --- a/src/cdk-experimental/testing/protractor/protractor-harness-environment.ts +++ b/src/cdk-experimental/testing/protractor/protractor-harness-environment.ts @@ -35,14 +35,13 @@ export class ProtractorHarnessEnvironment extends HarnessEnvironment { - const element = this.rawRootElement.element(by.css(selector)); - return await element.isPresent() ? element : null; - } - protected async getAllRawElements(selector: string): Promise { - const elements = this.rawRootElement.all(by.css(selector)); - return elements.reduce( - (result: ElementFinder[], el: ElementFinder) => el ? result.concat([el]) : result, []); + const elementFinderArray = this.rawRootElement.all(by.css(selector)); + const length = await elementFinderArray.count(); + const elements: ElementFinder[] = []; + for (let i = 0; i < length; i++) { + elements.push(elementFinderArray.get(i)); + } + return elements; } } diff --git a/src/cdk-experimental/testing/testbed/testbed-harness-environment.ts b/src/cdk-experimental/testing/testbed/testbed-harness-environment.ts index 18c7b09950ea..dabe3693fe1e 100644 --- a/src/cdk-experimental/testing/testbed/testbed-harness-environment.ts +++ b/src/cdk-experimental/testing/testbed/testbed-harness-environment.ts @@ -48,11 +48,6 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment { return new TestbedHarnessEnvironment(element, this._fixture); } - protected async getRawElement(selector: string): Promise { - await this._stabilize(); - return this.rawRootElement.querySelector(selector) || null; - } - protected async getAllRawElements(selector: string): Promise { await this._stabilize(); return Array.from(this.rawRootElement.querySelectorAll(selector)); 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 fecd954fc8c9..d636e246379a 100644 --- a/src/cdk-experimental/testing/tests/harnesses/main-component-harness.ts +++ b/src/cdk-experimental/testing/tests/harnesses/main-component-harness.ts @@ -40,6 +40,14 @@ export class MainComponentHarness extends ComponentHarness { readonly optionalSubComponent = this.locatorForOptional(SubComponentHarness); readonly errorSubComponent = this.locatorFor(WrongComponentHarness); + readonly fourItemLists = this.locatorForAll(SubComponentHarness.with({itemCount: 4})); + readonly toolsLists = this.locatorForAll(SubComponentHarness.with({title: 'List of test tools'})); + readonly fourItemToolsLists = + this.locatorForAll(SubComponentHarness.with({title: 'List of test tools', itemCount: 4})); + readonly testLists = this.locatorForAll(SubComponentHarness.with({title: /test/})); + readonly requiredFourIteamToolsLists = + this.locatorFor(SubComponentHarness.with({title: 'List of test tools', itemCount: 4})); + private _testTools = this.locatorFor(SubComponentHarness); async increaseCounter(times: number) { 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 b34ab41f2f94..654d5234db26 100644 --- a/src/cdk-experimental/testing/tests/harnesses/sub-component-harness.ts +++ b/src/cdk-experimental/testing/tests/harnesses/sub-component-harness.ts @@ -6,12 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ -import {ComponentHarness} from '../../component-harness'; +import {ComponentHarness, HarnessPredicate} from '../../component-harness'; import {TestElement} from '../../test-element'; +/** @dynamic */ export class SubComponentHarness extends ComponentHarness { static readonly hostSelector = 'test-sub'; + static with(options: {title?: string | RegExp, itemCount?: number} = {}) { + return new HarnessPredicate(SubComponentHarness) + .addOption('title', options.title, + async (harness, title) => + HarnessPredicate.stringMatches((await harness.title()).text(), title)) + .addOption('item count', options.itemCount, + async (harness, count) => (await harness.getItems()).length === count); + } + readonly title = this.locatorFor('h2'); readonly getItems = this.locatorForAll('li'); readonly globalElement = this.documentRootLocatorFactory().locatorFor('#username'); diff --git a/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts b/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts index ef240a8c6052..7909f50f3c61 100644 --- a/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts +++ b/src/cdk-experimental/testing/tests/protractor.e2e.spec.ts @@ -52,8 +52,9 @@ describe('ProtractorHarnessEnvironment', () => { await countersLoader.getHarness(SubComponentHarness); fail('Expected to throw'); } catch (e) { - expect(e.message) - .toBe('Expected to find element matching selector: "test-sub", but none was found'); + expect(e.message).toBe( + 'Expected to find element for SubComponentHarness matching selector:' + + ' "test-sub", but none was found'); } }); @@ -114,7 +115,8 @@ describe('ProtractorHarnessEnvironment', () => { fail('Expected to throw'); } catch (e) { expect(e.message).toBe( - 'Expected to find element matching selector: "wrong-selector", but none was found'); + 'Expected to find element for WrongComponentHarness matching selector:' + + ' "wrong-selector", but none was found'); } }); @@ -135,10 +137,11 @@ describe('ProtractorHarnessEnvironment', () => { 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(items2.length).toBe(4); expect(await items2[0].text()).toBe('Unit Test'); expect(await items2[1].text()).toBe('Integration Test'); expect(await items2[2].text()).toBe('Performance Test'); + expect(await items2[3].text()).toBe('Mutation Test'); }); it('should wait for async operation to complete', async () => { @@ -228,4 +231,48 @@ describe('ProtractorHarnessEnvironment', () => { .not.toBe(await button.text()); }); }); + + describe('HarnessPredicate', () => { + let harness: MainComponentHarness; + + beforeEach(async () => { + harness = await ProtractorHarnessEnvironment.loader().getHarness(MainComponentHarness); + }); + + it('should find subcomponents with specific item count', async () => { + const fourItemLists = await harness.fourItemLists(); + expect(fourItemLists.length).toBe(1); + expect(await (await fourItemLists[0].title()).text()).toBe('List of test methods'); + }); + + it('should find subcomponents with specific title', async () => { + const toolsLists = await harness.toolsLists(); + expect(toolsLists.length).toBe(1); + expect(await (await toolsLists[0].title()).text()).toBe('List of test tools'); + }); + + it('should find no subcomponents if predicate does not match', async () => { + const fourItemToolsLists = await harness.fourItemToolsLists(); + expect(fourItemToolsLists.length).toBe(0); + }); + + it('should find subcomponents with title regex', async () => { + const testLists = await harness.testLists(); + expect(testLists.length).toBe(2); + expect(await (await testLists[0].title()).text()).toBe('List of test tools'); + expect(await (await testLists[1].title()).text()).toBe('List of test methods'); + }); + + it('should error if predicate does not match but a harness is required', async () => { + try { + await harness.requiredFourIteamToolsLists(); + fail('Expected to throw'); + } catch (e) { + expect(e.message).toBe( + 'Expected to find element for SubComponentHarness matching selector: "test-sub"' + + ' (with restrictions: title = "List of test tools", item count = 4),' + + ' but none was found'); + } + }); + }); }); diff --git a/src/cdk-experimental/testing/tests/test-main-component.ts b/src/cdk-experimental/testing/tests/test-main-component.ts index 355caf5e08b5..e9ea9407c4b1 100644 --- a/src/cdk-experimental/testing/tests/test-main-component.ts +++ b/src/cdk-experimental/testing/tests/test-main-component.ts @@ -50,7 +50,7 @@ export class TestMainComponent { this.asyncCounter = 0; this.memo = ''; this.testTools = ['Protractor', 'TestBed', 'Other']; - this.testMethods = ['Unit Test', 'Integration Test', 'Performance Test']; + this.testMethods = ['Unit Test', 'Integration Test', 'Performance Test', 'Mutation Test']; setTimeout(() => { this.asyncCounter = 5; this._cdr.markForCheck(); diff --git a/src/cdk-experimental/testing/tests/testbed.spec.ts b/src/cdk-experimental/testing/tests/testbed.spec.ts index 638b4df9cd7a..dfe7d44ad745 100644 --- a/src/cdk-experimental/testing/tests/testbed.spec.ts +++ b/src/cdk-experimental/testing/tests/testbed.spec.ts @@ -67,8 +67,9 @@ describe('TestbedHarnessEnvironment', () => { await countersLoader.getHarness(SubComponentHarness); fail('Expected to throw'); } catch (e) { - expect(e.message) - .toBe('Expected to find element matching selector: "test-sub", but none was found'); + expect(e.message).toBe( + 'Expected to find element for SubComponentHarness matching selector:' + + ' "test-sub", but none was found'); } }); @@ -130,7 +131,8 @@ describe('TestbedHarnessEnvironment', () => { fail('Expected to throw'); } catch (e) { expect(e.message).toBe( - 'Expected to find element matching selector: "wrong-selector", but none was found'); + 'Expected to find element for WrongComponentHarness matching selector:' + + ' "wrong-selector", but none was found'); } }); @@ -151,10 +153,11 @@ describe('TestbedHarnessEnvironment', () => { 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(items2.length).toBe(4); expect(await items2[0].text()).toBe('Unit Test'); expect(await items2[1].text()).toBe('Integration Test'); expect(await items2[2].text()).toBe('Performance Test'); + expect(await items2[3].text()).toBe('Mutation Test'); }); it('should wait for async operation to complete', async () => { @@ -245,4 +248,49 @@ describe('TestbedHarnessEnvironment', () => { expect(activeElementText()).not.toBe(await button.text()); }); }); + + describe('HarnessPredicate', () => { + let harness: MainComponentHarness; + + beforeEach(async () => { + harness = + await TestbedHarnessEnvironment.harnessForFixture(fixture, MainComponentHarness); + }); + + it('should find subcomponents with specific item count', async () => { + const fourItemLists = await harness.fourItemLists(); + expect(fourItemLists.length).toBe(1); + expect(await (await fourItemLists[0].title()).text()).toBe('List of test methods'); + }); + + it('should find subcomponents with specific title', async () => { + const toolsLists = await harness.toolsLists(); + expect(toolsLists.length).toBe(1); + expect(await (await toolsLists[0].title()).text()).toBe('List of test tools'); + }); + + it('should find no subcomponents if predicate does not match', async () => { + const fourItemToolsLists = await harness.fourItemToolsLists(); + expect(fourItemToolsLists.length).toBe(0); + }); + + it('should find subcomponents with title regex', async () => { + const testLists = await harness.testLists(); + expect(testLists.length).toBe(2); + expect(await (await testLists[0].title()).text()).toBe('List of test tools'); + expect(await (await testLists[1].title()).text()).toBe('List of test methods'); + }); + + it('should error if predicate does not match but a harness is required', async () => { + try { + await harness.requiredFourIteamToolsLists(); + fail('Expected to throw'); + } catch (e) { + expect(e.message).toBe( + 'Expected to find element for SubComponentHarness matching selector: "test-sub"' + + ' (with restrictions: title = "List of test tools", item count = 4),' + + ' but none was found'); + } + }); + }); });