UNPKG

32.7 kBJavaScriptView Raw
1import { BehaviorSubject } from 'rxjs';
2
3/** Subject used to dispatch and listen for changes to the auto change detection status . */
4const autoChangeDetectionSubject = new BehaviorSubject({
5 isDisabled: false,
6});
7/** The current subscription to `autoChangeDetectionSubject`. */
8let autoChangeDetectionSubscription;
9/**
10 * The default handler for auto change detection status changes. This handler will be used if the
11 * specific environment does not install its own.
12 * @param status The new auto change detection status.
13 */
14function defaultAutoChangeDetectionHandler(status) {
15 status.onDetectChangesNow?.();
16}
17/**
18 * Allows a test `HarnessEnvironment` to install its own handler for auto change detection status
19 * changes.
20 * @param handler The handler for the auto change detection status.
21 */
22function handleAutoChangeDetectionStatus(handler) {
23 stopHandlingAutoChangeDetectionStatus();
24 autoChangeDetectionSubscription = autoChangeDetectionSubject.subscribe(handler);
25}
26/** Allows a `HarnessEnvironment` to stop handling auto change detection status changes. */
27function stopHandlingAutoChangeDetectionStatus() {
28 autoChangeDetectionSubscription?.unsubscribe();
29 autoChangeDetectionSubscription = null;
30}
31/**
32 * Batches together triggering of change detection over the duration of the given function.
33 * @param fn The function to call with batched change detection.
34 * @param triggerBeforeAndAfter Optionally trigger change detection once before and after the batch
35 * operation. If false, change detection will not be triggered.
36 * @return The result of the given function.
37 */
38async function batchChangeDetection(fn, triggerBeforeAndAfter) {
39 // If change detection batching is already in progress, just run the function.
40 if (autoChangeDetectionSubject.getValue().isDisabled) {
41 return await fn();
42 }
43 // If nothing is handling change detection batching, install the default handler.
44 if (!autoChangeDetectionSubscription) {
45 handleAutoChangeDetectionStatus(defaultAutoChangeDetectionHandler);
46 }
47 if (triggerBeforeAndAfter) {
48 await new Promise(resolve => autoChangeDetectionSubject.next({
49 isDisabled: true,
50 onDetectChangesNow: resolve,
51 }));
52 // The function passed in may throw (e.g. if the user wants to make an expectation of an error
53 // being thrown. If this happens, we need to make sure we still re-enable change detection, so
54 // we wrap it in a `finally` block.
55 try {
56 return await fn();
57 }
58 finally {
59 await new Promise(resolve => autoChangeDetectionSubject.next({
60 isDisabled: false,
61 onDetectChangesNow: resolve,
62 }));
63 }
64 }
65 else {
66 autoChangeDetectionSubject.next({ isDisabled: true });
67 // The function passed in may throw (e.g. if the user wants to make an expectation of an error
68 // being thrown. If this happens, we need to make sure we still re-enable change detection, so
69 // we wrap it in a `finally` block.
70 try {
71 return await fn();
72 }
73 finally {
74 autoChangeDetectionSubject.next({ isDisabled: false });
75 }
76 }
77}
78/**
79 * Disables the harness system's auto change detection for the duration of the given function.
80 * @param fn The function to disable auto change detection for.
81 * @return The result of the given function.
82 */
83async function manualChangeDetection(fn) {
84 return batchChangeDetection(fn, false);
85}
86/**
87 * Resolves the given list of async values in parallel (i.e. via Promise.all) while batching change
88 * detection over the entire operation such that change detection occurs exactly once before
89 * resolving the values and once after.
90 * @param values A getter for the async values to resolve in parallel with batched change detection.
91 * @return The resolved values.
92 */
93async function parallel(values) {
94 return batchChangeDetection(() => Promise.all(values()), true);
95}
96
97/**
98 * Base class for component harnesses that all component harness authors should extend. This base
99 * component harness provides the basic ability to locate element and sub-component harness. It
100 * should be inherited when defining user's own harness.
101 */
102class ComponentHarness {
103 constructor(locatorFactory) {
104 this.locatorFactory = locatorFactory;
105 }
106 /** Gets a `Promise` for the `TestElement` representing the host element of the component. */
107 async host() {
108 return this.locatorFactory.rootElement;
109 }
110 /**
111 * Gets a `LocatorFactory` for the document root element. This factory can be used to create
112 * locators for elements that a component creates outside of its own root element. (e.g. by
113 * appending to document.body).
114 */
115 documentRootLocatorFactory() {
116 return this.locatorFactory.documentRootLocatorFactory();
117 }
118 /**
119 * Creates an asynchronous locator function that can be used to find a `ComponentHarness` instance
120 * or element under the host element of this `ComponentHarness`.
121 * @param queries A list of queries specifying which harnesses and elements to search for:
122 * - A `string` searches for elements matching the CSS selector specified by the string.
123 * - A `ComponentHarness` constructor searches for `ComponentHarness` instances matching the
124 * given class.
125 * - A `HarnessPredicate` searches for `ComponentHarness` instances matching the given
126 * predicate.
127 * @return An asynchronous locator function that searches for and returns a `Promise` for the
128 * first element or harness matching the given search criteria. Matches are ordered first by
129 * order in the DOM, and second by order in the queries list. If no matches are found, the
130 * `Promise` rejects. The type that the `Promise` resolves to is a union of all result types for
131 * each query.
132 *
133 * e.g. Given the following DOM: `<div id="d1" /><div id="d2" />`, and assuming
134 * `DivHarness.hostSelector === 'div'`:
135 * - `await ch.locatorFor(DivHarness, 'div')()` gets a `DivHarness` instance for `#d1`
136 * - `await ch.locatorFor('div', DivHarness)()` gets a `TestElement` instance for `#d1`
137 * - `await ch.locatorFor('span')()` throws because the `Promise` rejects.
138 */
139 locatorFor(...queries) {
140 return this.locatorFactory.locatorFor(...queries);
141 }
142 /**
143 * Creates an asynchronous locator function that can be used to find a `ComponentHarness` instance
144 * or element under the host element of this `ComponentHarness`.
145 * @param queries A list of queries specifying which harnesses and elements to search for:
146 * - A `string` searches for elements matching the CSS selector specified by the string.
147 * - A `ComponentHarness` constructor searches for `ComponentHarness` instances matching the
148 * given class.
149 * - A `HarnessPredicate` searches for `ComponentHarness` instances matching the given
150 * predicate.
151 * @return An asynchronous locator function that searches for and returns a `Promise` for the
152 * first element or harness matching the given search criteria. Matches are ordered first by
153 * order in the DOM, and second by order in the queries list. If no matches are found, the
154 * `Promise` is resolved with `null`. The type that the `Promise` resolves to is a union of all
155 * result types for each query or null.
156 *
157 * e.g. Given the following DOM: `<div id="d1" /><div id="d2" />`, and assuming
158 * `DivHarness.hostSelector === 'div'`:
159 * - `await ch.locatorForOptional(DivHarness, 'div')()` gets a `DivHarness` instance for `#d1`
160 * - `await ch.locatorForOptional('div', DivHarness)()` gets a `TestElement` instance for `#d1`
161 * - `await ch.locatorForOptional('span')()` gets `null`.
162 */
163 locatorForOptional(...queries) {
164 return this.locatorFactory.locatorForOptional(...queries);
165 }
166 /**
167 * Creates an asynchronous locator function that can be used to find `ComponentHarness` instances
168 * or elements under the host element of this `ComponentHarness`.
169 * @param queries A list of queries specifying which harnesses and elements to search for:
170 * - A `string` searches for elements matching the CSS selector specified by the string.
171 * - A `ComponentHarness` constructor searches for `ComponentHarness` instances matching the
172 * given class.
173 * - A `HarnessPredicate` searches for `ComponentHarness` instances matching the given
174 * predicate.
175 * @return An asynchronous locator function that searches for and returns a `Promise` for all
176 * elements and harnesses matching the given search criteria. Matches are ordered first by
177 * order in the DOM, and second by order in the queries list. If an element matches more than
178 * one `ComponentHarness` class, the locator gets an instance of each for the same element. If
179 * an element matches multiple `string` selectors, only one `TestElement` instance is returned
180 * for that element. The type that the `Promise` resolves to is an array where each element is
181 * the union of all result types for each query.
182 *
183 * e.g. Given the following DOM: `<div id="d1" /><div id="d2" />`, and assuming
184 * `DivHarness.hostSelector === 'div'` and `IdIsD1Harness.hostSelector === '#d1'`:
185 * - `await ch.locatorForAll(DivHarness, 'div')()` gets `[
186 * DivHarness, // for #d1
187 * TestElement, // for #d1
188 * DivHarness, // for #d2
189 * TestElement // for #d2
190 * ]`
191 * - `await ch.locatorForAll('div', '#d1')()` gets `[
192 * TestElement, // for #d1
193 * TestElement // for #d2
194 * ]`
195 * - `await ch.locatorForAll(DivHarness, IdIsD1Harness)()` gets `[
196 * DivHarness, // for #d1
197 * IdIsD1Harness, // for #d1
198 * DivHarness // for #d2
199 * ]`
200 * - `await ch.locatorForAll('span')()` gets `[]`.
201 */
202 locatorForAll(...queries) {
203 return this.locatorFactory.locatorForAll(...queries);
204 }
205 /**
206 * Flushes change detection and async tasks in the Angular zone.
207 * In most cases it should not be necessary to call this manually. However, there may be some edge
208 * cases where it is needed to fully flush animation events.
209 */
210 async forceStabilize() {
211 return this.locatorFactory.forceStabilize();
212 }
213 /**
214 * Waits for all scheduled or running async tasks to complete. This allows harness
215 * authors to wait for async tasks outside of the Angular zone.
216 */
217 async waitForTasksOutsideAngular() {
218 return this.locatorFactory.waitForTasksOutsideAngular();
219 }
220}
221/**
222 * Base class for component harnesses that authors should extend if they anticipate that consumers
223 * of the harness may want to access other harnesses within the `<ng-content>` of the component.
224 */
225class ContentContainerComponentHarness extends ComponentHarness {
226 async getChildLoader(selector) {
227 return (await this.getRootHarnessLoader()).getChildLoader(selector);
228 }
229 async getAllChildLoaders(selector) {
230 return (await this.getRootHarnessLoader()).getAllChildLoaders(selector);
231 }
232 async getHarness(query) {
233 return (await this.getRootHarnessLoader()).getHarness(query);
234 }
235 async getHarnessOrNull(query) {
236 return (await this.getRootHarnessLoader()).getHarnessOrNull(query);
237 }
238 async getAllHarnesses(query) {
239 return (await this.getRootHarnessLoader()).getAllHarnesses(query);
240 }
241 async hasHarness(query) {
242 return (await this.getRootHarnessLoader()).hasHarness(query);
243 }
244 /**
245 * Gets the root harness loader from which to start
246 * searching for content contained by this harness.
247 */
248 async getRootHarnessLoader() {
249 return this.locatorFactory.rootHarnessLoader();
250 }
251}
252/**
253 * A class used to associate a ComponentHarness class with predicates functions that can be used to
254 * filter instances of the class.
255 */
256class HarnessPredicate {
257 constructor(harnessType, options) {
258 this.harnessType = harnessType;
259 this._predicates = [];
260 this._descriptions = [];
261 this._addBaseOptions(options);
262 }
263 /**
264 * Checks if the specified nullable string value matches the given pattern.
265 * @param value The nullable string value to check, or a Promise resolving to the
266 * nullable string value.
267 * @param pattern The pattern the value is expected to match. If `pattern` is a string,
268 * `value` is expected to match exactly. If `pattern` is a regex, a partial match is
269 * allowed. If `pattern` is `null`, the value is expected to be `null`.
270 * @return Whether the value matches the pattern.
271 */
272 static async stringMatches(value, pattern) {
273 value = await value;
274 if (pattern === null) {
275 return value === null;
276 }
277 else if (value === null) {
278 return false;
279 }
280 return typeof pattern === 'string' ? value === pattern : pattern.test(value);
281 }
282 /**
283 * Adds a predicate function to be run against candidate harnesses.
284 * @param description A description of this predicate that may be used in error messages.
285 * @param predicate An async predicate function.
286 * @return this (for method chaining).
287 */
288 add(description, predicate) {
289 this._descriptions.push(description);
290 this._predicates.push(predicate);
291 return this;
292 }
293 /**
294 * Adds a predicate function that depends on an option value to be run against candidate
295 * harnesses. If the option value is undefined, the predicate will be ignored.
296 * @param name The name of the option (may be used in error messages).
297 * @param option The option value.
298 * @param predicate The predicate function to run if the option value is not undefined.
299 * @return this (for method chaining).
300 */
301 addOption(name, option, predicate) {
302 if (option !== undefined) {
303 this.add(`${name} = ${_valueAsString(option)}`, item => predicate(item, option));
304 }
305 return this;
306 }
307 /**
308 * Filters a list of harnesses on this predicate.
309 * @param harnesses The list of harnesses to filter.
310 * @return A list of harnesses that satisfy this predicate.
311 */
312 async filter(harnesses) {
313 if (harnesses.length === 0) {
314 return [];
315 }
316 const results = await parallel(() => harnesses.map(h => this.evaluate(h)));
317 return harnesses.filter((_, i) => results[i]);
318 }
319 /**
320 * Evaluates whether the given harness satisfies this predicate.
321 * @param harness The harness to check
322 * @return A promise that resolves to true if the harness satisfies this predicate,
323 * and resolves to false otherwise.
324 */
325 async evaluate(harness) {
326 const results = await parallel(() => this._predicates.map(p => p(harness)));
327 return results.reduce((combined, current) => combined && current, true);
328 }
329 /** Gets a description of this predicate for use in error messages. */
330 getDescription() {
331 return this._descriptions.join(', ');
332 }
333 /** Gets the selector used to find candidate elements. */
334 getSelector() {
335 // We don't have to go through the extra trouble if there are no ancestors.
336 if (!this._ancestor) {
337 return (this.harnessType.hostSelector || '').trim();
338 }
339 const [ancestors, ancestorPlaceholders] = _splitAndEscapeSelector(this._ancestor);
340 const [selectors, selectorPlaceholders] = _splitAndEscapeSelector(this.harnessType.hostSelector || '');
341 const result = [];
342 // We have to add the ancestor to each part of the host compound selector, otherwise we can get
343 // incorrect results. E.g. `.ancestor .a, .ancestor .b` vs `.ancestor .a, .b`.
344 ancestors.forEach(escapedAncestor => {
345 const ancestor = _restoreSelector(escapedAncestor, ancestorPlaceholders);
346 return selectors.forEach(escapedSelector => result.push(`${ancestor} ${_restoreSelector(escapedSelector, selectorPlaceholders)}`));
347 });
348 return result.join(', ');
349 }
350 /** Adds base options common to all harness types. */
351 _addBaseOptions(options) {
352 this._ancestor = options.ancestor || '';
353 if (this._ancestor) {
354 this._descriptions.push(`has ancestor matching selector "${this._ancestor}"`);
355 }
356 const selector = options.selector;
357 if (selector !== undefined) {
358 this.add(`host matches selector "${selector}"`, async (item) => {
359 return (await item.host()).matchesSelector(selector);
360 });
361 }
362 }
363}
364/** Represent a value as a string for the purpose of logging. */
365function _valueAsString(value) {
366 if (value === undefined) {
367 return 'undefined';
368 }
369 try {
370 // `JSON.stringify` doesn't handle RegExp properly, so we need a custom replacer.
371 // Use a character that is unlikely to appear in real strings to denote the start and end of
372 // the regex. This allows us to strip out the extra quotes around the value added by
373 // `JSON.stringify`. Also do custom escaping on `"` characters to prevent `JSON.stringify`
374 // from escaping them as if they were part of a string.
375 const stringifiedValue = JSON.stringify(value, (_, v) => v instanceof RegExp
376 ? `◬MAT_RE_ESCAPE◬${v.toString().replace(/"/g, '◬MAT_RE_ESCAPE◬')}◬MAT_RE_ESCAPE◬`
377 : v);
378 // Strip out the extra quotes around regexes and put back the manually escaped `"` characters.
379 return stringifiedValue
380 .replace(/"◬MAT_RE_ESCAPE◬|◬MAT_RE_ESCAPE◬"/g, '')
381 .replace(/◬MAT_RE_ESCAPE◬/g, '"');
382 }
383 catch {
384 // `JSON.stringify` will throw if the object is cyclical,
385 // in this case the best we can do is report the value as `{...}`.
386 return '{...}';
387 }
388}
389/**
390 * Splits up a compound selector into its parts and escapes any quoted content. The quoted content
391 * has to be escaped, because it can contain commas which will throw throw us off when trying to
392 * split it.
393 * @param selector Selector to be split.
394 * @returns The escaped string where any quoted content is replaced with a placeholder. E.g.
395 * `[foo="bar"]` turns into `[foo=__cdkPlaceholder-0__]`. Use `_restoreSelector` to restore
396 * the placeholders.
397 */
398function _splitAndEscapeSelector(selector) {
399 const placeholders = [];
400 // Note that the regex doesn't account for nested quotes so something like `"ab'cd'e"` will be
401 // considered as two blocks. It's a bit of an edge case, but if we find that it's a problem,
402 // we can make it a bit smarter using a loop. Use this for now since it's more readable and
403 // compact. More complete implementation:
404 // https://github.com/angular/angular/blob/bd34bc9e89f18a/packages/compiler/src/shadow_css.ts#L655
405 const result = selector.replace(/(["'][^["']*["'])/g, (_, keep) => {
406 const replaceBy = `__cdkPlaceholder-${placeholders.length}__`;
407 placeholders.push(keep);
408 return replaceBy;
409 });
410 return [result.split(',').map(part => part.trim()), placeholders];
411}
412/** Restores a selector whose content was escaped in `_splitAndEscapeSelector`. */
413function _restoreSelector(selector, placeholders) {
414 return selector.replace(/__cdkPlaceholder-(\d+)__/g, (_, index) => placeholders[+index]);
415}
416
417/**
418 * Base harness environment class that can be extended to allow `ComponentHarness`es to be used in
419 * different test environments (e.g. testbed, protractor, etc.). This class implements the
420 * functionality of both a `HarnessLoader` and `LocatorFactory`. This class is generic on the raw
421 * element type, `E`, used by the particular test environment.
422 */
423class HarnessEnvironment {
424 // Implemented as part of the `LocatorFactory` interface.
425 get rootElement() {
426 this._rootElement = this._rootElement || this.createTestElement(this.rawRootElement);
427 return this._rootElement;
428 }
429 set rootElement(element) {
430 this._rootElement = element;
431 }
432 constructor(rawRootElement) {
433 this.rawRootElement = rawRootElement;
434 }
435 // Implemented as part of the `LocatorFactory` interface.
436 documentRootLocatorFactory() {
437 return this.createEnvironment(this.getDocumentRoot());
438 }
439 // Implemented as part of the `LocatorFactory` interface.
440 locatorFor(...queries) {
441 return () => _assertResultFound(this._getAllHarnessesAndTestElements(queries), _getDescriptionForLocatorForQueries(queries));
442 }
443 // Implemented as part of the `LocatorFactory` interface.
444 locatorForOptional(...queries) {
445 return async () => (await this._getAllHarnessesAndTestElements(queries))[0] || null;
446 }
447 // Implemented as part of the `LocatorFactory` interface.
448 locatorForAll(...queries) {
449 return () => this._getAllHarnessesAndTestElements(queries);
450 }
451 // Implemented as part of the `LocatorFactory` interface.
452 async rootHarnessLoader() {
453 return this;
454 }
455 // Implemented as part of the `LocatorFactory` interface.
456 async harnessLoaderFor(selector) {
457 return this.createEnvironment(await _assertResultFound(this.getAllRawElements(selector), [
458 _getDescriptionForHarnessLoaderQuery(selector),
459 ]));
460 }
461 // Implemented as part of the `LocatorFactory` interface.
462 async harnessLoaderForOptional(selector) {
463 const elements = await this.getAllRawElements(selector);
464 return elements[0] ? this.createEnvironment(elements[0]) : null;
465 }
466 // Implemented as part of the `LocatorFactory` interface.
467 async harnessLoaderForAll(selector) {
468 const elements = await this.getAllRawElements(selector);
469 return elements.map(element => this.createEnvironment(element));
470 }
471 // Implemented as part of the `HarnessLoader` interface.
472 getHarness(query) {
473 return this.locatorFor(query)();
474 }
475 // Implemented as part of the `HarnessLoader` interface.
476 getHarnessOrNull(query) {
477 return this.locatorForOptional(query)();
478 }
479 // Implemented as part of the `HarnessLoader` interface.
480 getAllHarnesses(query) {
481 return this.locatorForAll(query)();
482 }
483 // Implemented as part of the `HarnessLoader` interface.
484 async hasHarness(query) {
485 return (await this.locatorForOptional(query)()) !== null;
486 }
487 // Implemented as part of the `HarnessLoader` interface.
488 async getChildLoader(selector) {
489 return this.createEnvironment(await _assertResultFound(this.getAllRawElements(selector), [
490 _getDescriptionForHarnessLoaderQuery(selector),
491 ]));
492 }
493 // Implemented as part of the `HarnessLoader` interface.
494 async getAllChildLoaders(selector) {
495 return (await this.getAllRawElements(selector)).map(e => this.createEnvironment(e));
496 }
497 /** Creates a `ComponentHarness` for the given harness type with the given raw host element. */
498 createComponentHarness(harnessType, element) {
499 return new harnessType(this.createEnvironment(element));
500 }
501 /**
502 * Matches the given raw elements with the given list of element and harness queries to produce a
503 * list of matched harnesses and test elements.
504 */
505 async _getAllHarnessesAndTestElements(queries) {
506 if (!queries.length) {
507 throw Error('CDK Component harness query must contain at least one element.');
508 }
509 const { allQueries, harnessQueries, elementQueries, harnessTypes } = _parseQueries(queries);
510 // Combine all of the queries into one large comma-delimited selector and use it to get all raw
511 // elements matching any of the individual queries.
512 const rawElements = await this.getAllRawElements([...elementQueries, ...harnessQueries.map(predicate => predicate.getSelector())].join(','));
513 // If every query is searching for the same harness subclass, we know every result corresponds
514 // to an instance of that subclass. Likewise, if every query is for a `TestElement`, we know
515 // every result corresponds to a `TestElement`. Otherwise we need to verify which result was
516 // found by which selector so it can be matched to the appropriate instance.
517 const skipSelectorCheck = (elementQueries.length === 0 && harnessTypes.size === 1) || harnessQueries.length === 0;
518 const perElementMatches = await parallel(() => rawElements.map(async (rawElement) => {
519 const testElement = this.createTestElement(rawElement);
520 const allResultsForElement = await parallel(
521 // For each query, get `null` if it doesn't match, or a `TestElement` or
522 // `ComponentHarness` as appropriate if it does match. This gives us everything that
523 // matches the current raw element, but it may contain duplicate entries (e.g.
524 // multiple `TestElement` or multiple `ComponentHarness` of the same type).
525 () => allQueries.map(query => this._getQueryResultForElement(query, rawElement, testElement, skipSelectorCheck)));
526 return _removeDuplicateQueryResults(allResultsForElement);
527 }));
528 return [].concat(...perElementMatches);
529 }
530 /**
531 * Check whether the given query matches the given element, if it does return the matched
532 * `TestElement` or `ComponentHarness`, if it does not, return null. In cases where the caller
533 * knows for sure that the query matches the element's selector, `skipSelectorCheck` can be used
534 * to skip verification and optimize performance.
535 */
536 async _getQueryResultForElement(query, rawElement, testElement, skipSelectorCheck = false) {
537 if (typeof query === 'string') {
538 return skipSelectorCheck || (await testElement.matchesSelector(query)) ? testElement : null;
539 }
540 if (skipSelectorCheck || (await testElement.matchesSelector(query.getSelector()))) {
541 const harness = this.createComponentHarness(query.harnessType, rawElement);
542 return (await query.evaluate(harness)) ? harness : null;
543 }
544 return null;
545 }
546}
547/**
548 * Parses a list of queries in the format accepted by the `locatorFor*` methods into an easier to
549 * work with format.
550 */
551function _parseQueries(queries) {
552 const allQueries = [];
553 const harnessQueries = [];
554 const elementQueries = [];
555 const harnessTypes = new Set();
556 for (const query of queries) {
557 if (typeof query === 'string') {
558 allQueries.push(query);
559 elementQueries.push(query);
560 }
561 else {
562 const predicate = query instanceof HarnessPredicate ? query : new HarnessPredicate(query, {});
563 allQueries.push(predicate);
564 harnessQueries.push(predicate);
565 harnessTypes.add(predicate.harnessType);
566 }
567 }
568 return { allQueries, harnessQueries, elementQueries, harnessTypes };
569}
570/**
571 * Removes duplicate query results for a particular element. (e.g. multiple `TestElement`
572 * instances or multiple instances of the same `ComponentHarness` class.
573 */
574async function _removeDuplicateQueryResults(results) {
575 let testElementMatched = false;
576 let matchedHarnessTypes = new Set();
577 const dedupedMatches = [];
578 for (const result of results) {
579 if (!result) {
580 continue;
581 }
582 if (result instanceof ComponentHarness) {
583 if (!matchedHarnessTypes.has(result.constructor)) {
584 matchedHarnessTypes.add(result.constructor);
585 dedupedMatches.push(result);
586 }
587 }
588 else if (!testElementMatched) {
589 testElementMatched = true;
590 dedupedMatches.push(result);
591 }
592 }
593 return dedupedMatches;
594}
595/** Verifies that there is at least one result in an array. */
596async function _assertResultFound(results, queryDescriptions) {
597 const result = (await results)[0];
598 if (result == undefined) {
599 throw Error(`Failed to find element matching one of the following queries:\n` +
600 queryDescriptions.map(desc => `(${desc})`).join(',\n'));
601 }
602 return result;
603}
604/** Gets a list of description strings from a list of queries. */
605function _getDescriptionForLocatorForQueries(queries) {
606 return queries.map(query => typeof query === 'string'
607 ? _getDescriptionForTestElementQuery(query)
608 : _getDescriptionForComponentHarnessQuery(query));
609}
610/** Gets a description string for a `ComponentHarness` query. */
611function _getDescriptionForComponentHarnessQuery(query) {
612 const harnessPredicate = query instanceof HarnessPredicate ? query : new HarnessPredicate(query, {});
613 const { name, hostSelector } = harnessPredicate.harnessType;
614 const description = `${name} with host element matching selector: "${hostSelector}"`;
615 const constraints = harnessPredicate.getDescription();
616 return (description +
617 (constraints ? ` satisfying the constraints: ${harnessPredicate.getDescription()}` : ''));
618}
619/** Gets a description string for a `TestElement` query. */
620function _getDescriptionForTestElementQuery(selector) {
621 return `TestElement for element matching selector: "${selector}"`;
622}
623/** Gets a description string for a `HarnessLoader` query. */
624function _getDescriptionForHarnessLoaderQuery(selector) {
625 return `HarnessLoader for element matching selector: "${selector}"`;
626}
627
628/** An enum of non-text keys that can be used with the `sendKeys` method. */
629// NOTE: This is a separate enum from `@angular/cdk/keycodes` because we don't necessarily want to
630// support every possible keyCode. We also can't rely on Protractor's `Key` because we don't want a
631// dependency on any particular testing framework here. Instead we'll just maintain this supported
632// list of keys and let individual concrete `HarnessEnvironment` classes map them to whatever key
633// representation is used in its respective testing framework.
634// tslint:disable-next-line:prefer-const-enum Seems like this causes some issues with System.js
635var TestKey;
636(function (TestKey) {
637 TestKey[TestKey["BACKSPACE"] = 0] = "BACKSPACE";
638 TestKey[TestKey["TAB"] = 1] = "TAB";
639 TestKey[TestKey["ENTER"] = 2] = "ENTER";
640 TestKey[TestKey["SHIFT"] = 3] = "SHIFT";
641 TestKey[TestKey["CONTROL"] = 4] = "CONTROL";
642 TestKey[TestKey["ALT"] = 5] = "ALT";
643 TestKey[TestKey["ESCAPE"] = 6] = "ESCAPE";
644 TestKey[TestKey["PAGE_UP"] = 7] = "PAGE_UP";
645 TestKey[TestKey["PAGE_DOWN"] = 8] = "PAGE_DOWN";
646 TestKey[TestKey["END"] = 9] = "END";
647 TestKey[TestKey["HOME"] = 10] = "HOME";
648 TestKey[TestKey["LEFT_ARROW"] = 11] = "LEFT_ARROW";
649 TestKey[TestKey["UP_ARROW"] = 12] = "UP_ARROW";
650 TestKey[TestKey["RIGHT_ARROW"] = 13] = "RIGHT_ARROW";
651 TestKey[TestKey["DOWN_ARROW"] = 14] = "DOWN_ARROW";
652 TestKey[TestKey["INSERT"] = 15] = "INSERT";
653 TestKey[TestKey["DELETE"] = 16] = "DELETE";
654 TestKey[TestKey["F1"] = 17] = "F1";
655 TestKey[TestKey["F2"] = 18] = "F2";
656 TestKey[TestKey["F3"] = 19] = "F3";
657 TestKey[TestKey["F4"] = 20] = "F4";
658 TestKey[TestKey["F5"] = 21] = "F5";
659 TestKey[TestKey["F6"] = 22] = "F6";
660 TestKey[TestKey["F7"] = 23] = "F7";
661 TestKey[TestKey["F8"] = 24] = "F8";
662 TestKey[TestKey["F9"] = 25] = "F9";
663 TestKey[TestKey["F10"] = 26] = "F10";
664 TestKey[TestKey["F11"] = 27] = "F11";
665 TestKey[TestKey["F12"] = 28] = "F12";
666 TestKey[TestKey["META"] = 29] = "META";
667})(TestKey || (TestKey = {}));
668
669/**
670 * Returns an error which reports that no keys have been specified.
671 * @docs-private
672 */
673function getNoKeysSpecifiedError() {
674 return Error('No keys have been specified.');
675}
676
677/**
678 * Gets text of element excluding certain selectors within the element.
679 * @param element Element to get text from,
680 * @param excludeSelector Selector identifying which elements to exclude,
681 */
682function _getTextWithExcludedElements(element, excludeSelector) {
683 const clone = element.cloneNode(true);
684 const exclusions = clone.querySelectorAll(excludeSelector);
685 for (let i = 0; i < exclusions.length; i++) {
686 exclusions[i].remove();
687 }
688 return (clone.textContent || '').trim();
689}
690
691export { ComponentHarness, ContentContainerComponentHarness, HarnessEnvironment, HarnessPredicate, TestKey, _getTextWithExcludedElements, getNoKeysSpecifiedError, handleAutoChangeDetectionStatus, manualChangeDetection, parallel, stopHandlingAutoChangeDetectionStatus };
692//# sourceMappingURL=testing.mjs.map