@@ -12,10 +12,30 @@ import {
1212 ComponentHarnessConstructor ,
1313 HarnessLoader ,
1414 HarnessPredicate ,
15- LocatorFactory
15+ HarnessQuery ,
16+ LocatorFactory ,
17+ LocatorFnResult ,
1618} from './component-harness' ;
1719import { TestElement } from './test-element' ;
1820
21+ /** Parsed form of the queries passed to the `locatorFor*` methods. */
22+ type ParsedQueries < T extends ComponentHarness > = {
23+ /** The full list of queries, in their original order. */
24+ allQueries : ( string | HarnessPredicate < T > ) [ ] ,
25+ /**
26+ * A filtered view of `allQueries` containing only the queries that are looking for a
27+ * `ComponentHarness`
28+ */
29+ harnessQueries : HarnessPredicate < T > [ ] ,
30+ /**
31+ * A filtered view of `allQueries` containing only the queries that are looking for a
32+ * `TestElement`
33+ */
34+ elementQueries : string [ ] ,
35+ /** The set of all `ComponentHarness` subclasses represented in the original query list. */
36+ harnessTypes : Set < ComponentHarnessConstructor < T > > ,
37+ } ;
38+
1939/**
2040 * Base harness environment class that can be extended to allow `ComponentHarness`es to be used in
2141 * different test environments (e.g. testbed, protractor, etc.). This class implements the
@@ -36,55 +56,29 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac
3656 }
3757
3858 // Implemented as part of the `LocatorFactory` interface.
39- locatorFor ( selector : string ) : AsyncFactoryFn < TestElement > ;
40- locatorFor < T extends ComponentHarness > (
41- harnessType : ComponentHarnessConstructor < T > | HarnessPredicate < T > ) : AsyncFactoryFn < T > ;
42- locatorFor < T extends ComponentHarness > (
43- arg : string | ComponentHarnessConstructor < T > | HarnessPredicate < T > ) {
44- return async ( ) => {
45- if ( typeof arg === 'string' ) {
46- return this . createTestElement ( await this . _assertElementFound ( arg ) ) ;
47- } else {
48- return this . _assertHarnessFound ( arg ) ;
49- }
50- } ;
59+ locatorFor < T extends ( HarnessQuery < any > | string ) [ ] > ( ...queries : T ) :
60+ AsyncFactoryFn < LocatorFnResult < T > > {
61+ return ( ) => _assertResultFound (
62+ this . _getAllHarnessesAndTestElements ( queries ) ,
63+ _getDescriptionForLocatorForQueries ( queries ) ) ;
5164 }
5265
5366 // Implemented as part of the `LocatorFactory` interface.
54- locatorForOptional ( selector : string ) : AsyncFactoryFn < TestElement | null > ;
55- locatorForOptional < T extends ComponentHarness > (
56- harnessType : ComponentHarnessConstructor < T > | HarnessPredicate < T > ) : AsyncFactoryFn < T | null > ;
57- locatorForOptional < T extends ComponentHarness > (
58- arg : string | ComponentHarnessConstructor < T > | HarnessPredicate < T > ) {
59- return async ( ) => {
60- if ( typeof arg === 'string' ) {
61- const element = ( await this . getAllRawElements ( arg ) ) [ 0 ] ;
62- return element ? this . createTestElement ( element ) : null ;
63- } else {
64- const candidates = await this . _getAllHarnesses ( arg ) ;
65- return candidates [ 0 ] || null ;
66- }
67- } ;
67+ locatorForOptional < T extends ( HarnessQuery < any > | string ) [ ] > ( ...queries : T ) :
68+ AsyncFactoryFn < LocatorFnResult < T > | null > {
69+ return async ( ) => ( await this . _getAllHarnessesAndTestElements ( queries ) ) [ 0 ] || null ;
6870 }
6971
7072 // Implemented as part of the `LocatorFactory` interface.
71- locatorForAll ( selector : string ) : AsyncFactoryFn < TestElement [ ] > ;
72- locatorForAll < T extends ComponentHarness > (
73- harnessType : ComponentHarnessConstructor < T > | HarnessPredicate < T > ) : AsyncFactoryFn < T [ ] > ;
74- locatorForAll < T extends ComponentHarness > (
75- arg : string | ComponentHarnessConstructor < T > | HarnessPredicate < T > ) {
76- return async ( ) => {
77- if ( typeof arg === 'string' ) {
78- return ( await this . getAllRawElements ( arg ) ) . map ( e => this . createTestElement ( e ) ) ;
79- } else {
80- return this . _getAllHarnesses ( arg ) ;
81- }
82- } ;
73+ locatorForAll < T extends ( HarnessQuery < any > | string ) [ ] > ( ...queries : T ) :
74+ AsyncFactoryFn < LocatorFnResult < T > [ ] > {
75+ return ( ) => this . _getAllHarnessesAndTestElements ( queries ) ;
8376 }
8477
8578 // Implemented as part of the `LocatorFactory` interface.
8679 async harnessLoaderFor ( selector : string ) : Promise < HarnessLoader > {
87- return this . createEnvironment ( await this . _assertElementFound ( selector ) ) ;
80+ return this . createEnvironment ( await _assertResultFound ( this . getAllRawElements ( selector ) ,
81+ [ _getDescriptionForHarnessLoaderQuery ( selector ) ] ) ) ;
8882 }
8983
9084 // Implemented as part of the `LocatorFactory` interface.
@@ -100,20 +94,19 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac
10094 }
10195
10296 // Implemented as part of the `HarnessLoader` interface.
103- getHarness < T extends ComponentHarness > (
104- harnessType : ComponentHarnessConstructor < T > | HarnessPredicate < T > ) : Promise < T > {
105- return this . locatorFor ( harnessType ) ( ) ;
97+ getHarness < T extends ComponentHarness > ( query : HarnessQuery < T > ) : Promise < T > {
98+ return this . locatorFor ( query ) ( ) ;
10699 }
107100
108101 // Implemented as part of the `HarnessLoader` interface.
109- getAllHarnesses < T extends ComponentHarness > (
110- harnessType : ComponentHarnessConstructor < T > | HarnessPredicate < T > ) : Promise < T [ ] > {
111- return this . locatorForAll ( harnessType ) ( ) ;
102+ getAllHarnesses < T extends ComponentHarness > ( query : HarnessQuery < T > ) : Promise < T [ ] > {
103+ return this . locatorForAll ( query ) ( ) ;
112104 }
113105
114106 // Implemented as part of the `HarnessLoader` interface.
115107 async getChildLoader ( selector : string ) : Promise < HarnessLoader > {
116- return this . createEnvironment ( await this . _assertElementFound ( selector ) ) ;
108+ return this . createEnvironment ( await _assertResultFound ( this . getAllRawElements ( selector ) ,
109+ [ _getDescriptionForHarnessLoaderQuery ( selector ) ] ) ) ;
117110 }
118111
119112 // Implemented as part of the `HarnessLoader` interface.
@@ -147,43 +140,147 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac
147140 */
148141 protected abstract getAllRawElements ( selector : string ) : Promise < E [ ] > ;
149142
150- private async _getAllHarnesses < T extends ComponentHarness > (
151- harnessType : ComponentHarnessConstructor < T > | HarnessPredicate < T > ) : Promise < T [ ] > {
152- const harnessPredicate = harnessType instanceof HarnessPredicate ?
153- harnessType : new HarnessPredicate ( harnessType , { } ) ;
154- const elements = await this . getAllRawElements ( harnessPredicate . getSelector ( ) ) ;
155- return harnessPredicate . filter ( elements . map (
156- element => this . createComponentHarness ( harnessPredicate . harnessType , element ) ) ) ;
143+ /**
144+ * Matches the given raw elements with the given list of element and harness queries to produce a
145+ * list of matched harnesses and test elements.
146+ */
147+ private async _getAllHarnessesAndTestElements < T extends ( HarnessQuery < any > | string ) [ ] > (
148+ queries : T ) : Promise < LocatorFnResult < T > [ ] > {
149+ const { allQueries, harnessQueries, elementQueries, harnessTypes} = _parseQueries ( queries ) ;
150+
151+ // Combine all of the queries into one large comma-delimited selector and use it to get all raw
152+ // elements matching any of the individual queries.
153+ const rawElements = await this . getAllRawElements (
154+ [ ...elementQueries , ...harnessQueries . map ( predicate => predicate . getSelector ( ) ) ] . join ( ',' ) ) ;
155+
156+ // If every query is searching for the same harness subclass, we know every result corresponds
157+ // to an instance of that subclass. Likewise, if every query is for a `TestElement`, we know
158+ // every result corresponds to a `TestElement`. Otherwise we need to verify which result was
159+ // found by which selector so it can be matched to the appropriate instance.
160+ const skipSelectorCheck = ( elementQueries . length === 0 && harnessTypes . size === 1 ) ||
161+ harnessQueries . length === 0 ;
162+
163+ const perElementMatches = await Promise . all ( rawElements . map ( async rawElement => {
164+ const testElement = this . createTestElement ( rawElement ) ;
165+ const allResultsForElement = await Promise . all (
166+ // For each query, get `null` if it doesn't match, or a `TestElement` or
167+ // `ComponentHarness` as appropriate if it does match. This gives us everything that
168+ // matches the current raw element, but it may contain duplicate entries (e.g. multiple
169+ // `TestElement` or multiple `ComponentHarness` of the same type.
170+ allQueries . map ( query =>
171+ this . _getQueryResultForElement ( query , rawElement , testElement , skipSelectorCheck ) ) ) ;
172+ return _removeDuplicateQueryResults ( allResultsForElement ) ;
173+ } ) ) ;
174+ return ( [ ] as any ) . concat ( ...perElementMatches ) ;
157175 }
158176
159- private async _assertElementFound ( selector : string ) : Promise < E > {
160- const element = ( await this . getAllRawElements ( selector ) ) [ 0 ] ;
161- if ( ! element ) {
162- throw Error ( `Expected to find element matching selector: "${ selector } ", but none was found` ) ;
177+ /**
178+ * Check whether the given query matches the given element, if it does return the matched
179+ * `TestElement` or `ComponentHarness`, if it does not, return null. In cases where the caller
180+ * knows for sure that the query matches the element's selector, `skipSelectorCheck` can be used
181+ * to skip verification and optimize performance.
182+ */
183+ private async _getQueryResultForElement < T extends ComponentHarness > (
184+ query : string | HarnessPredicate < T > , rawElement : E , testElement : TestElement ,
185+ skipSelectorCheck : boolean = false ) : Promise < T | TestElement | null > {
186+ if ( typeof query === 'string' ) {
187+ return ( ( skipSelectorCheck || await testElement . matchesSelector ( query ) ) ? testElement : null ) ;
163188 }
164- return element ;
189+ if ( skipSelectorCheck || await testElement . matchesSelector ( query . getSelector ( ) ) ) {
190+ const harness = this . createComponentHarness ( query . harnessType , rawElement ) ;
191+ return ( await query . evaluate ( harness ) ) ? harness : null ;
192+ }
193+ return null ;
165194 }
195+ }
196+
197+ /**
198+ * Parses a list of queries in the format accepted by the `locatorFor*` methods into an easier to
199+ * work with format.
200+ */
201+ function _parseQueries < T extends ( HarnessQuery < any > | string ) [ ] > ( queries : T ) :
202+ ParsedQueries < LocatorFnResult < T > & ComponentHarness > {
203+ const allQueries = [ ] ;
204+ const harnessQueries = [ ] ;
205+ const elementQueries = [ ] ;
206+ const harnessTypes =
207+ new Set < ComponentHarnessConstructor < LocatorFnResult < T > & ComponentHarness > > ( ) ;
166208
167- private async _assertHarnessFound < T extends ComponentHarness > (
168- harnessType : ComponentHarnessConstructor < T > | HarnessPredicate < T > ) : Promise < T > {
169- const harness = ( await this . _getAllHarnesses ( harnessType ) ) [ 0 ] ;
170- if ( ! harness ) {
171- throw _getErrorForMissingHarness ( harnessType ) ;
209+ for ( const query of queries ) {
210+ if ( typeof query === 'string' ) {
211+ allQueries . push ( query ) ;
212+ elementQueries . push ( query ) ;
213+ } else {
214+ const predicate = query instanceof HarnessPredicate ? query : new HarnessPredicate ( query , { } ) ;
215+ allQueries . push ( predicate ) ;
216+ harnessQueries . push ( predicate ) ;
217+ harnessTypes . add ( predicate . harnessType ) ;
172218 }
173- return harness ;
174219 }
220+
221+ return { allQueries, harnessQueries, elementQueries, harnessTypes} ;
222+ }
223+
224+ /**
225+ * Removes duplicate query results for a particular element. (e.g. multiple `TestElement`
226+ * instances or multiple instances of the same `ComponentHarness` class.
227+ */
228+ async function _removeDuplicateQueryResults < T extends ( ComponentHarness | TestElement | null ) [ ] > (
229+ results : T ) : Promise < T > {
230+ let testElementMatched = false ;
231+ let matchedHarnessTypes = new Set ( ) ;
232+ const dedupedMatches = [ ] ;
233+ for ( const result of results ) {
234+ if ( ! result ) {
235+ continue ;
236+ }
237+ if ( result instanceof ComponentHarness ) {
238+ if ( ! matchedHarnessTypes . has ( result . constructor ) ) {
239+ matchedHarnessTypes . add ( result . constructor ) ;
240+ dedupedMatches . push ( result ) ;
241+ }
242+ } else if ( ! testElementMatched ) {
243+ testElementMatched = true ;
244+ dedupedMatches . push ( result ) ;
245+ }
246+ }
247+ return dedupedMatches as T ;
175248}
176249
177- function _getErrorForMissingHarness < T extends ComponentHarness > (
178- harnessType : ComponentHarnessConstructor < T > | HarnessPredicate < T > ) : Error {
250+ /** Verifies that there is at least one result in an array. */
251+ async function _assertResultFound < T > ( results : Promise < T [ ] > , queryDescriptions : string [ ] ) :
252+ Promise < T > {
253+ const result = ( await results ) [ 0 ] ;
254+ if ( result == undefined ) {
255+ throw Error ( `Failed to find element matching one of the following queries:\n` +
256+ queryDescriptions . map ( desc => `(${ desc } )` ) . join ( ',\n' ) ) ;
257+ }
258+ return result ;
259+ }
260+
261+ /** Gets a list of description strings from a list of queries. */
262+ function _getDescriptionForLocatorForQueries ( queries : ( string | HarnessQuery < any > ) [ ] ) {
263+ return queries . map ( query => typeof query === 'string' ?
264+ _getDescriptionForTestElementQuery ( query ) : _getDescriptionForComponentHarnessQuery ( query ) ) ;
265+ }
266+
267+ /** Gets a description string for a `ComponentHarness` query. */
268+ function _getDescriptionForComponentHarnessQuery ( query : HarnessQuery < any > ) {
179269 const harnessPredicate =
180- harnessType instanceof HarnessPredicate ? harnessType : new HarnessPredicate ( harnessType , { } ) ;
270+ query instanceof HarnessPredicate ? query : new HarnessPredicate ( query , { } ) ;
181271 const { name, hostSelector} = harnessPredicate . harnessType ;
182- let restrictions = harnessPredicate . getDescription ( ) ;
183- let message = `Expected to find element for ${ name } matching selector: "${ hostSelector } "` ;
184- if ( restrictions ) {
185- message += ` (with restrictions: ${ restrictions } )` ;
186- }
187- message += ', but none was found' ;
188- return Error ( message ) ;
272+ const description = `${ name } with host element matching selector: "${ hostSelector } "` ;
273+ const constraints = harnessPredicate . getDescription ( ) ;
274+ return description + ( constraints ?
275+ ` satisfying the constraints: ${ harnessPredicate . getDescription ( ) } ` : '' ) ;
276+ }
277+
278+ /** Gets a description string for a `TestElement` query. */
279+ function _getDescriptionForTestElementQuery ( selector : string ) {
280+ return `TestElement for element matching selector: "${ selector } "` ;
281+ }
282+
283+ /** Gets a description string for a `HarnessLoader` query. */
284+ function _getDescriptionForHarnessLoaderQuery ( selector : string ) {
285+ return `HarnessLoader for element matching selector: "${ selector } "` ;
189286}
0 commit comments