@@ -12,10 +12,30 @@ import {
12
12
ComponentHarnessConstructor ,
13
13
HarnessLoader ,
14
14
HarnessPredicate ,
15
- LocatorFactory
15
+ HarnessQuery ,
16
+ LocatorFactory ,
17
+ LocatorFnResult ,
16
18
} from './component-harness' ;
17
19
import { TestElement } from './test-element' ;
18
20
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
+
19
39
/**
20
40
* Base harness environment class that can be extended to allow `ComponentHarness`es to be used in
21
41
* different test environments (e.g. testbed, protractor, etc.). This class implements the
@@ -36,55 +56,29 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac
36
56
}
37
57
38
58
// 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 ) ) ;
51
64
}
52
65
53
66
// 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 ;
68
70
}
69
71
70
72
// 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 ) ;
83
76
}
84
77
85
78
// Implemented as part of the `LocatorFactory` interface.
86
79
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 ) ] ) ) ;
88
82
}
89
83
90
84
// Implemented as part of the `LocatorFactory` interface.
@@ -100,20 +94,19 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac
100
94
}
101
95
102
96
// 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 ) ( ) ;
106
99
}
107
100
108
101
// 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 ) ( ) ;
112
104
}
113
105
114
106
// Implemented as part of the `HarnessLoader` interface.
115
107
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 ) ] ) ) ;
117
110
}
118
111
119
112
// Implemented as part of the `HarnessLoader` interface.
@@ -147,43 +140,147 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac
147
140
*/
148
141
protected abstract getAllRawElements ( selector : string ) : Promise < E [ ] > ;
149
142
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 ) ;
157
175
}
158
176
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 ) ;
163
188
}
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 ;
165
194
}
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 > > ( ) ;
166
208
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 ) ;
172
218
}
173
- return harness ;
174
219
}
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 ;
175
248
}
176
249
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 > ) {
179
269
const harnessPredicate =
180
- harnessType instanceof HarnessPredicate ? harnessType : new HarnessPredicate ( harnessType , { } ) ;
270
+ query instanceof HarnessPredicate ? query : new HarnessPredicate ( query , { } ) ;
181
271
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 } "` ;
189
286
}
0 commit comments