'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var domAccessibilityApi = require('dom-accessibility-api');
var ariaQuery = require('aria-query');
const refs = new Set();
function clearTestContext() {
for (const ref of refs) {
for (const key in ref) {
delete ref[key];
}
}
refs.clear();
}
function assertNotEmpty(target, p) {
if (!Reflect.has(target, p)) {
throw new Error(`Cannot access context value "${p}" before it has been set`);
}
}
function createTestContext() {
let context = {};
return new Proxy(context, {
get(target, p) {
assertNotEmpty(target, p);
return Reflect.get(target, p);
},
set(target, p, newValue) {
refs.add(context);
return Reflect.set(target, p, newValue);
}
});
}
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
exports.Flag = void 0;
(function (Flag) {
Flag[Flag["DEFAULT"] = 0] = "DEFAULT";
Flag[Flag["SKIP"] = 1] = "SKIP";
Flag[Flag["ONLY"] = 2] = "ONLY";
Flag[Flag["EXCLUDE"] = 3] = "EXCLUDE";
})(exports.Flag || (exports.Flag = {}));
var _a, _b, _c;
function stripDelimiters(string) {
return string.slice(1, string.length - 1);
}
function getValue(expr) {
switch (expr) {
case "true":
return true;
case "false":
return false;
case "null":
return null;
case "undefined":
return undefined;
default:
return Number(expr);
}
}
function normalize(step) {
return step.replace(/('[^\s]*?'|"[^\s]*?"|<[^\s]*?>|\[[^\s]*?\])/g, "%%var%%");
}
function parseArguments(step) {
// eg. "foo[20]bar" = ["foo", "[20]", "bar"]
const tokens = step.split(/(\[[^\s]*?\]|'[^\s]*?'|"[^\s]*?")|\[[^\s]*?\]/);
const parsedArgs = [];
tokens.forEach((value) => {
const expr = stripDelimiters(value);
switch (value.slice(-1)) {
case "]":
parsedArgs.push(getValue(expr));
break;
case "'":
case '"':
parsedArgs.push(expr);
}
});
return parsedArgs;
}
var Type;
(function (Type) {
Type["ROOT"] = "root";
Type["FEATURE"] = "feature";
Type["SCENARIO"] = "scenario";
Type["EXAMPLE"] = "examples";
})(Type || (Type = {}));
class Scenario {
get path() {
var _a, _b;
return [...((_b = (_a = this.parent) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0 ? _b : []), this.name];
}
addStep(factory) {
this.steps.push(factory);
}
addExamples(name, data, tags, flag) {
if (this.examples.has('') || this.examples.size && name === '') {
throw new Error(`Example description cannot be blank when a scenario has multiple "examples()"`);
}
if (this.examples.has(name)) {
throw new Error(`Multiple examples detected: "${name}". Each "examples()" in a scenario must have a unique description`);
}
this.examples.set(name, { data, tags, flag });
}
addScenario(scenario, factory, flag) {
if (this.scenarios.has(scenario.name)) {
throw new Error(`Multiple scenarios detected: "${scenario.name}". Each "scenario()" in a feature must have a unique description`);
}
this.scenarios.set(scenario.name, { scenario, factory });
}
markAsFailed() {
this.failed = true;
}
hasOwnTag(tag) {
return this.tags.has(tag.name);
}
hasTag(tag) {
return this.getEffectiveTags().has(tag.name);
}
getEffectiveTags() {
const tags = [this.feature.tags, this.parent.tags, this.tags, [...this.examples.values()].flatMap(e => Array.from(e.tags))].flatMap(tags => Array.from(tags));
return new Set(tags);
}
constructor(type, name, tags, feature, parent, data = {}) {
this.type = type;
this.name = name;
this.tags = tags;
this.feature = feature;
this.parent = parent;
this.data = data;
this.failed = false;
this.steps = [];
this.examples = new Map();
this.scenarios = new Map();
scenarios.add(this);
}
}
const scenarios = new Set();
function getAllScenarios() {
return Array.from(scenarios);
}
const rootScenario = new Scenario(Type.ROOT, "root", new Set(), void 0);
rootScenario.addStep = function () {
throw new Error("Steps can only be added inside a scenario, background or examples context");
};
const rootContext = {
scenario: rootScenario,
feature: rootScenario,
examples: Object.freeze([]),
};
let context = rootContext;
function setContext(nextContext) {
const previousContext = context;
context = nextContext;
return previousContext;
}
function runScenario(adapter, combinedSteps, scenario, context) {
const metadata = Object.assign({ steps: combinedSteps.map((step) => step(GET_IMPL)) }, getAllEffectiveHooks(scenario.getEffectiveTags()));
runHooks(metadata.beforeScenario, scenario, context, (impl) => adapter.beforeScenario(impl, metadata));
try {
for (const step of combinedSteps) {
runHooks(metadata.beforeStep, scenario, context, (impl) => adapter.beforeStep(impl, metadata));
step(context, scenario);
runHooks(metadata.afterStep, scenario, context, (impl) => adapter.afterStep(impl, metadata));
}
}
finally {
runHooks(metadata.afterScenario, scenario, context, (impl) => adapter.afterScenario(impl, metadata));
clearTestContext();
}
}
function runScenarioAsync(adapter, combinedSteps, scenario, thisContext) {
return __awaiter(this, void 0, void 0, function* () {
const metadata = Object.assign({ steps: combinedSteps.map((step) => step(GET_IMPL)) }, getAllEffectiveHooks(scenario.getEffectiveTags()));
yield runHooksAsync(metadata.beforeScenario, scenario, thisContext, (impl) => adapter.beforeScenario(impl, metadata));
try {
for (const step of combinedSteps) {
yield runHooksAsync(metadata.beforeStep, scenario, thisContext, (impl) => adapter.beforeStep(impl, metadata));
yield step(thisContext, scenario);
yield runHooksAsync(metadata.afterStep, scenario, thisContext, (impl) => adapter.afterStep(impl, metadata));
}
}
finally {
yield runHooksAsync(metadata.afterScenario, scenario, thisContext, (impl) => adapter.afterScenario(impl, metadata));
}
clearTestContext();
});
}
function getFlag(tags, noDefaultExclude = false) {
let flag = (globalThis.LEFTEST_INCLUDED_TAGS.size) && !noDefaultExclude
? exports.Flag.EXCLUDE
: exports.Flag.DEFAULT;
if ([...tags].some((tag) => globalThis.LEFTEST_INCLUDED_TAGS.has(tag))) {
flag = exports.Flag.DEFAULT;
}
if ([...tags].some((tag) => globalThis.LEFTEST_EXCLUDED_TAGS.has(tag))) {
flag = exports.Flag.EXCLUDE;
}
if (flag !== exports.Flag.EXCLUDE && tags.has(only.name)) {
return exports.Flag.ONLY;
}
if (flag !== exports.Flag.EXCLUDE && (tags.has(skip.name) || tags.has(todo.name))) {
return exports.Flag.SKIP;
}
return flag;
}
function createTagFn(tags) {
return function tag(tagOrFn) {
return typeof tagOrFn === "function"
? tagOrFn(tag)
: tags.has(tagOrFn.name);
};
}
/**
* Creates a filter that returns true when both tags or tag expressions are matched
* @param a
* @param b
*/
function and(a, b) {
assertNoTags(and.name);
return (tag) => tag(a) && tag(b);
}
/**
* Creates a filter that returns true when either tag or tag expression are matched
* @param a
* @param b
*/
function or(a, b) {
assertNoTags(or.name);
return (filter) => filter(a) || filter(b);
}
/**
* Creates a filter that returns true when the given tag is matched.
* @param tag
*/
function eq(tag) {
assertNoTags(eq.name);
return (filter) => filter(tag);
}
/**
* Creates a filter that returns true when the given tag or tag expression is not matched.
* @param tag
*/
function not(tag) {
assertNoTags(not.name);
return (filter) => !filter(tag);
}
function getEffectiveHooks(hooks, tags) {
let effectiveHooks = [];
for (const [mask, fn = mask] of hooks) {
if (mask !== fn) {
const tag = createTagFn(tags);
if (mask(tag)) {
effectiveHooks.push(fn);
}
}
else {
effectiveHooks.push(fn);
}
}
return effectiveHooks;
}
function runHooks(hooks, scenario, context, cb) {
if (!hooks.length)
return;
cb(() => {
for (const hook of hooks) {
hook.call(context, scenario);
}
});
}
function runHooksAsync(hooks, scenario, thisContext, cb) {
return __awaiter(this, void 0, void 0, function* () {
if (!hooks.length)
return;
return cb(() => __awaiter(this, void 0, void 0, function* () {
for (const hook of hooks) {
yield hook.call(thisContext, scenario);
}
}));
});
}
function getAllEffectiveHooks(tags) {
const beforeScenario = getEffectiveHooks(hooks.beforeScenario, tags);
const afterScenario = getEffectiveHooks(hooks.afterScenario, tags);
const beforeStep = getEffectiveHooks(hooks.beforeStep, tags);
const afterStep = getEffectiveHooks(hooks.afterStep, tags);
return { beforeScenario, afterScenario, beforeStep, afterStep };
}
function runExamples(examples, exampleTags, scenario, adapter, steps, flag) {
let count = 0;
const total = examples.length;
for (const data of examples) {
count++;
const exampleScenario = new Scenario(Type.EXAMPLE, `Example ${count} of ${total}`, exampleTags, scenario.feature, scenario, data);
const metadata = Object.assign({ flag, steps: steps.map((step) => step(GET_IMPL)) }, getAllEffectiveHooks(exampleScenario.getEffectiveTags()));
adapter.test(exampleScenario.name, (ctx) => {
return (adapter.isAsync ? runScenarioAsync : runScenario)(adapter, steps, exampleScenario, ctx);
}, metadata);
}
}
function scenario(name, fn) {
assertContextType(Type.FEATURE, Type.SCENARIO);
const adapter = getAdapter();
const tags = flushTags();
const backgroundSteps = context.scenario.steps;
const scenario = new Scenario(Type.SCENARIO, name, tags, context.feature, context.feature);
const feature = context.feature;
const previous = setContext({
feature,
scenario,
examples: [],
});
try {
fn();
const isExcluded = getFlag(scenario.feature.tags) === exports.Flag.EXCLUDE && Array.from(scenario.examples.values()).map(e => getFlag(e.tags)).every(flag => flag === exports.Flag.EXCLUDE);
const flag = getFlag(tags, !isExcluded);
const combinedSteps = [...backgroundSteps, ...scenario.steps];
feature.addScenario(scenario, () => {
if (scenario.examples.size) {
adapter.suite(name, () => {
for (const [exampleName, { data: examples, tags: exampleTags, flag }] of scenario.examples) {
if (exampleName) {
adapter.suite(exampleName, () => {
runExamples(examples, exampleTags, scenario, adapter, combinedSteps, flag);
}, flag);
}
else {
runExamples(examples, exampleTags, scenario, adapter, combinedSteps, flag);
}
}
}, flag);
}
else {
const metadata = Object.assign({ flag, steps: combinedSteps.map((step) => step(GET_IMPL)) }, getAllEffectiveHooks(scenario.getEffectiveTags()));
adapter.test(name, function (ctx) {
return (adapter.isAsync ? runScenarioAsync : runScenario)(adapter, combinedSteps, scenario, ctx);
}, metadata);
}
}, flag);
}
finally {
setContext(previous);
}
}
function getAdapter() {
if (!globalThis.LEFTEST_ADAPTER) {
throw new Error('No adapter found. Did you forget to call "setAdapter()"?');
}
return globalThis.LEFTEST_ADAPTER;
}
function feature(name, fn) {
assertContextType(Type.ROOT, Type.FEATURE);
const adapter = getAdapter();
const tags = flushTags();
const feature = new Scenario(Type.FEATURE, name, tags, rootScenario);
feature.addStep = rootScenario.addStep;
const previous = setContext({
scenario: feature,
feature,
examples: [],
});
try {
fn();
const flag = getFlag(tags, [...feature.scenarios.values()].some(({ scenario }) => getFlag(scenario.getEffectiveTags()) !== exports.Flag.EXCLUDE));
adapter.suite(name, () => {
for (const { factory } of feature.scenarios.values()) {
factory();
}
}, flag);
}
finally {
setContext(previous);
}
}
function background(fn) {
assertNoTags(background.name);
assertContextType(Type.FEATURE, background.name);
context.scenario.addStep = Scenario.prototype.addStep;
try {
fn();
}
finally {
context.scenario.addStep = rootScenario.addStep;
}
}
function examples(...args) {
assertContextType(Type.SCENARIO, Type.EXAMPLE);
const name = args.length > 1 ? args[0] : '';
const data = args[args.length - 1];
const tags = flushTags();
const flag = getFlag(tags, tags.size === 0);
context.scenario.addExamples(name, data, tags, flag);
}
const GET_IMPL = Symbol();
function createStep(steps, name) {
function step(step, ...args) {
assertNoTags(name.toLowerCase());
const adapter = getAdapter();
context.scenario.addStep((ctx, scenario) => {
const impl = steps.getImplementation(step);
if (ctx === GET_IMPL) {
return impl;
}
let parsedArgs = args;
if (!parsedArgs.length) {
parsedArgs = parseArguments(step);
}
if (!parsedArgs.length) {
parsedArgs = steps.getArgsFromObject(step, scenario.data);
}
let testName = steps.getTestName(step, parsedArgs);
const metadata = Object.assign({ steps: scenario.steps.map((step) => step(GET_IMPL)) }, getAllEffectiveHooks(scenario.getEffectiveTags()));
if (testName) {
let step;
if (adapter.isAsync) {
step = function (stepCtx) {
return __awaiter(this, void 0, void 0, function* () {
ctx = stepCtx || ctx;
if (scenario.failed) {
throw new Error("Previous step failed");
}
try {
yield impl.apply(ctx, parsedArgs);
}
catch (e) {
scenario.markAsFailed();
throw e;
}
});
};
}
else {
step = function (stepCtx) {
ctx = stepCtx || ctx;
if (scenario.failed) {
throw new Error("Previous step failed");
}
try {
impl.apply(ctx, parsedArgs);
}
catch (e) {
scenario.markAsFailed();
throw e;
}
};
}
return adapter.step(name, testName, step, metadata);
}
});
}
Object.defineProperty(step, 'name', { value: name.toLowerCase() });
return step;
}
function splitVars(name) {
return (name.split(/(<[^\s]*?>)/g)).map((s) => s.startsWith("<") ? { var: stripDelimiters(s) } : s);
}
class EmptyVar {
constructor(name) {
this.name = name;
}
}
class Steps {
getTestName(name, args) {
const copy = args.slice(0);
const step = this.getStep(name);
const varLen = step.vars.filter((s) => typeof s !== "string").length;
if (copy.length < varLen) {
throw new Error(`Expected at least ${varLen} arguments, received ${copy.length}
${name}
`);
}
return step.vars
.map((s) => {
if (typeof s === "string") {
return s;
}
else {
const val = copy.shift();
return JSON.stringify(val);
}
})
.join("");
}
getImplementation(step) {
return this.getStep(step).impl;
}
getArgsFromObject(step, data = {}) {
const args = splitVars(step)
.filter((s) => typeof s !== "string")
.map((s) => {
if (s.var in data) {
return data[s.var];
}
return new EmptyVar(s.var);
});
const missing = args
.filter((s) => s instanceof EmptyVar)
.map((s) => `"${s.name}"`);
if (missing.length) {
throw new Error(`No value given for ${new Intl.ListFormat("en").format(missing)}
${step}
`);
}
return args;
}
getStep(name) {
const step = this.normalNames[normalize(name)];
if (!step) {
throw new Error(`No step matched:
${name}`);
}
return step;
}
constructor(steps, options) {
this.options = options;
this.normalNames = {};
Object.entries(steps)
.map(([name]) => [normalize(name), name])
.forEach(([normalized, name], index, arr) => {
const matches = arr
.filter(([match], matchIndex) => {
return normalized === match && matchIndex !== index;
})
.map(([_, name]) => name);
if (!matches.length)
return;
throw new Error(`Ambiguous step detected:
${name}
Matches:
${matches.join("\n ")}
`);
});
for (const [name, impl] of Object.entries(steps)) {
this.normalNames[normalize(name)] = {
name,
vars: splitVars(name),
impl: impl,
};
}
}
}
const tags = [];
const usedTags = new Set();
function flushTags() {
const flushed = new Set(tags);
tags.length = 0;
return flushed;
}
function assertNoTags(method) {
const tags = [...flushTags()].map((s) => `"${s}"`);
if (tags.length > 0) {
flushTags();
setContext(rootContext);
throw new Error(`Tag${tags.length > 1 ? "s" : ""} ${new Intl.ListFormat("en").format(tags)} ${tags.length > 1 ? "are" : "is"} not allowed on "${method}"`);
}
}
function assertContextType(expected, name) {
if (context.scenario.type !== expected) {
flushTags();
setContext(rootContext);
throw new Error(`"${name}" can only be used in a ${expected} context`);
}
}
const tagCache = new Map();
/**
* Returns a proxy that generates a {@link Tag} for each object key
* @return TagProxy
*/
// IntelliJ doesn't resolve references for index signatures, so leave this return type as `any` for now.
function getTags() {
assertNoTags(getTags.name);
assertContextType(Type.ROOT, getTags.name);
return new Proxy({}, {
get(_, p) {
if (tagCache.has(p)) {
return tagCache.get(p);
}
const tag = {
[Symbol.toPrimitive]() {
usedTags.add(p);
tags.push(p);
return p;
},
name: p,
};
tagCache.set(p, tag);
return tag;
},
});
}
(_a = globalThis.LEFTEST_ADAPTER) !== null && _a !== void 0 ? _a : (globalThis.LEFTEST_ADAPTER = undefined);
(_b = globalThis.LEFTEST_INCLUDED_TAGS) !== null && _b !== void 0 ? _b : (globalThis.LEFTEST_INCLUDED_TAGS = new Set());
(_c = globalThis.LEFTEST_EXCLUDED_TAGS) !== null && _c !== void 0 ? _c : (globalThis.LEFTEST_EXCLUDED_TAGS = new Set());
function setAdapter(value) {
assertNoTags(setAdapter.name);
globalThis.LEFTEST_ADAPTER = value;
}
function setTags(tags) {
assertNoTags(setTags.name);
globalThis.LEFTEST_INCLUDED_TAGS.clear();
globalThis.LEFTEST_EXCLUDED_TAGS.clear();
const taglist = Array.isArray(tags)
? tags
: tags
.trim()
.split(/[,\s]+/)
.filter(Boolean);
for (const tag of taglist) {
if (tag.startsWith("^")) {
globalThis.LEFTEST_EXCLUDED_TAGS.add(tag.slice(1));
}
else {
globalThis.LEFTEST_INCLUDED_TAGS.add(tag);
}
}
}
function isTagUsed(tag) {
return usedTags.has(tag.name);
}
const hooks = {
beforeScenario: [],
beforeStep: [],
afterScenario: [],
afterStep: [],
};
function beforeScenario(filter, fn) {
assertContextType(Type.ROOT, beforeScenario.name);
assertNoTags(beforeScenario.name);
hooks.beforeScenario.push([filter, fn]);
}
function beforeStep(filter, fn) {
assertContextType(Type.ROOT, beforeStep.name);
assertNoTags(beforeStep.name);
hooks.beforeStep.push([filter, fn]);
}
function afterScenario(filter, fn) {
assertContextType(Type.ROOT, afterScenario.name);
assertNoTags(afterScenario.name);
hooks.afterScenario.push([filter, fn]);
}
function afterStep(filter, fn) {
assertContextType(Type.ROOT, afterStep.name);
assertNoTags(afterStep.name);
hooks.afterStep.push([filter, fn]);
}
const {
// @deprecated Remove before commit. For debugging use only.
only,
// @deprecated Remove before commit. For debugging use only. For test stubs use the "todo" tag instead.
skip, todo } = getTags();
/**
* Creates a typed {@link TestSuite} from a module import.
* @param stepDefs
* @param options
*/
function createTestSuite(stepDefs = {}, options = {}) {
assertNoTags(createTestSuite.name);
assertContextType(Type.ROOT, createTestSuite.name);
const steps = new Steps(stepDefs, options);
return {
given: createStep(steps, "Given"),
when: createStep(steps, "When"),
then: createStep(steps, "Then"),
and: createStep(steps, "And"),
but: createStep(steps, "But"),
examples,
};
}
/**
* Returns true if the tag is included in the current test run.
* @param tag
*/
function isIncluded(tag) {
return globalThis.LEFTEST_INCLUDED_TAGS.has(tag.name);
}
/**
Returns true if the tag is excluded in the current test run.
@param tag
*/
function isExcluded(tag) {
return globalThis.LEFTEST_EXCLUDED_TAGS.has(tag.name);
}
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* Resolves the given list of async values in parallel (i.e. via Promise.all) while batching change
* detection over the entire operation such that change detection occurs exactly once before
* resolving the values and once after.
* @param values A getter for the async values to resolve in parallel with batched change detection.
* @return The resolved values.
*/
function parallel(values) {
return __awaiter(this, void 0, void 0, function* () {
return Promise.all(values());
});
}
/**
* A class used to associate a ComponentHarness class with predicates functions that can be used to
* filter instances of the class.
*/
class HarnessPredicate {
constructor(harnessType, options) {
this.harnessType = harnessType;
this._predicates = [];
this._descriptions = [];
this.cache = new WeakMap();
this._addBaseOptions(options);
}
/**
* Checks if the specified nullable string value matches the given pattern.
* @param value The nullable string value to check, or a Promise resolving to the
* nullable string value.
* @param pattern The pattern the value is expected to match. If `pattern` is a string,
* `value` is expected to match exactly. If `pattern` is a regex, a partial match is
* allowed. If `pattern` is `null`, the value is expected to be `null`.
* @return Whether the value matches the pattern.
*/
static stringMatches(value, pattern) {
return __awaiter(this, void 0, void 0, function* () {
value = yield value;
if (pattern == null) {
return value === null;
}
else if (value === null) {
return false;
}
return typeof pattern === "string"
? value === pattern
: pattern.test(value);
});
}
/**
* 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, predicate) {
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, option, predicate) {
if (option !== undefined) {
this.add(`${name} = ${_valueAsString(option)}`, (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.
*/
filter(harnesses) {
return __awaiter(this, void 0, void 0, function* () {
try {
if (harnesses.length === 0) {
return [];
}
const results = yield parallel(() => harnesses.map((h) => this.evaluate(h)));
return harnesses.filter((_, i) => results[i]);
}
finally {
this.cache = new WeakMap();
}
});
}
/**
* 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.
*/
evaluate(harness) {
return __awaiter(this, void 0, void 0, function* () {
const results = yield parallel(() => 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(", ");
}
/** Gets the selector used to find candidate elements. */
getSelector() {
// We don't have to go through the extra trouble if there are no ancestors.
if (!this._ancestor) {
return (this.harnessType.hostSelector || "").trim();
}
const [ancestors, ancestorPlaceholders] = _splitAndEscapeSelector(this._ancestor);
const [selectors, selectorPlaceholders] = _splitAndEscapeSelector(this.harnessType.hostSelector || "");
const result = [];
// We have to add the ancestor to each part of the host compound selector, otherwise we can get
// incorrect results. E.g. `.ancestor .a, .ancestor .b` vs `.ancestor .a, .b`.
ancestors.forEach((escapedAncestor) => {
const ancestor = _restoreSelector(escapedAncestor, ancestorPlaceholders);
return selectors.forEach((escapedSelector) => result.push(`${ancestor} ${_restoreSelector(escapedSelector, selectorPlaceholders)}`));
});
return result.join(", ");
}
/** Adds base options common to all harness types. */
_addBaseOptions(options) {
this._ancestor = options.ancestor || "";
if (this._ancestor) {
this._descriptions.push(`has ancestor matching selector "${this._ancestor}"`);
}
const selector = options.selector;
if (selector !== undefined) {
this.add(`host matches selector "${selector}"`, (item) => __awaiter(this, void 0, void 0, function* () {
return item.host().matchesSelector(selector);
}));
}
}
}
/** Represent a value as a string for the purpose of logging. */
function _valueAsString(value) {
if (value === undefined) {
return "undefined";
}
try {
// `JSON.stringify` doesn't handle RegExp properly, so we need a custom replacer.
// Use a character that is unlikely to appear in real strings to denote the start and end of
// the regex. This allows us to strip out the extra quotes around the value added by
// `JSON.stringify`. Also do custom escaping on `"` characters to prevent `JSON.stringify`
// from escaping them as if they were part of a string.
const stringifiedValue = JSON.stringify(value, (_, v) => v instanceof RegExp
? `◬MAT_RE_ESCAPE◬${v
.toString()
.replace(/"/g, "◬MAT_RE_ESCAPE◬")}◬MAT_RE_ESCAPE◬`
: v);
// Strip out the extra quotes around regexes and put back the manually escaped `"` characters.
return stringifiedValue
.replace(/"◬MAT_RE_ESCAPE◬|◬MAT_RE_ESCAPE◬"/g, "")
.replace(/◬MAT_RE_ESCAPE◬/g, "\"");
}
catch (_a) {
// `JSON.stringify` will throw if the object is cyclical,
// in this case the best we can do is report the value as `{...}`.
return "{...}";
}
}
/**
* Splits up a compound selector into its parts and escapes any quoted content. The quoted content
* has to be escaped, because it can contain commas which will throw throw us off when trying to
* split it.
* @param selector Selector to be split.
* @returns The escaped string where any quoted content is replaced with a placeholder. E.g.
* `[foo="bar"]` turns into `[foo=__cdkPlaceholder-0__]`. Use `_restoreSelector` to restore
* the placeholders.
*/
function _splitAndEscapeSelector(selector) {
const placeholders = [];
// Note that the regex doesn't account for nested quotes so something like `"ab'cd'e"` will be
// considered as two blocks. It's a bit of an edge case, but if we find that it's a problem,
// we can make it a bit smarter using a loop. Use this for now since it's more readable and
// compact. More complete implementation:
// https://github.com/angular/angular/blob/bd34bc9e89f18a/packages/compiler/src/shadow_css.ts#L655
const result = selector.replace(/(["'][^["']*["'])/g, (_, keep) => {
const replaceBy = `__cdkPlaceholder-${placeholders.length}__`;
placeholders.push(keep);
return replaceBy;
});
return [result.split(",").map((part) => part.trim()), placeholders];
}
/** Restores a selector whose content was escaped in `_splitAndEscapeSelector`. */
function _restoreSelector(selector, placeholders) {
return selector.replace(/__cdkPlaceholder-(\d+)__/g, (_, index) => placeholders[+index]);
}
function predicate(description, filter) {
Object.defineProperty(filter, "name", {
value: description,
});
return filter;
}
function byText(pattern) {
return predicate(`match text\n\twith options:\n\t\tpattern: ${pattern}\n`, function byText(harness) {
return __awaiter(this, void 0, void 0, function* () {
const textContent = yield harness.host().text();
return matchText(textContent, pattern);
});
});
}
function matchText(value, pattern) {
return __awaiter(this, void 0, void 0, function* () {
value = yield value;
if (typeof pattern === "function") {
return value ? pattern(value) : false;
}
return HarnessPredicate.stringMatches(value, pattern);
});
}
function byAltText(pattern) {
return predicate(`match alt text\n\twith options:\n\t\tpattern: ${pattern}\n`, function byTestId(harness) {
return __awaiter(this, void 0, void 0, function* () {
return matchText(harness.host().getAttribute('alt'), pattern);
});
});
}
function byDisplayValue(pattern) {
return predicate(`match display value\n\twith options:\n\t\tpattern: ${pattern}\n`, function byDisplayValue(harness) {
return __awaiter(this, void 0, void 0, function* () {
const host = yield harness.host();
const tagName = yield host.getProperty('tagName');
switch (tagName) {
case "SELECT": {
const options = yield host.getProperty('options');
for (const option of Array.from(options)) {
if (!option.selected) {
continue;
}
if (yield matchText(option.textContent, pattern)) {
return true;
}
}
break;
}
case "TEXTAREA":
case "INPUT": {
const type = yield host.getProperty('type');
switch (type) {
case "checkbox": {
const checked = yield host.getProperty('checked');
const indeterminate = yield host.getProperty('indeterminate');
return matchText(indeterminate ? 'indeterminate' : checked ? 'checked' : 'unchecked', pattern);
}
}
const value = yield host.getProperty('value');
return matchText(value, pattern);
}
}
return false;
});
});
}
function getSvgTitleText(tagName, children) {
var _a;
if (tagName !== "svg") {
return null;
}
const titleNode = Array.from(children).find(node => node.tagName === 'title');
return (_a = titleNode === null || titleNode === void 0 ? void 0 : titleNode.textContent) !== null && _a !== void 0 ? _a : null;
}
function byTitle(pattern) {
return predicate(`match title text\n\twith options:\n\t\tpattern: ${pattern}\n`, function byTestId(harness) {
return __awaiter(this, void 0, void 0, function* () {
const host = harness.host();
const [title, children, tagName] = yield parallel(() => [host.getAttribute('title'), host.getProperty('children'), host.getProperty('tagName')]);
return matchText(title || getSvgTitleText(tagName, children), pattern);
});
});
}
function byPlaceholderText(pattern) {
return predicate(`match placeholder text\n\twith options:\n\t\tpattern: ${pattern}\n`, function byTestId(harness) {
return __awaiter(this, void 0, void 0, function* () {
return matchText(harness.host().getAttribute('placeholder'), pattern);
});
});
}
function byLabelText(pattern) {
return predicate(`match label text\n\twith options:\n\t\tpattern: ${pattern}\n`, function byLabelText(harness) {
return __awaiter(this, void 0, void 0, function* () {
const host = harness.host();
const labels = yield host.getProperty('labels');
if (labels) {
for (const label of Array.from(labels)) {
if (yield matchText(label.textContent, pattern)) {
return true;
}
}
}
return false;
});
});
}
function prettyOptions(obj) {
let pretty = '';
for (const [key, value] of Object.entries(obj)) {
pretty += `\t\t${key}: ${JSON.stringify(value)}\n`;
}
return pretty;
}
function getElement(element) {
return __awaiter(this, void 0, void 0, function* () {
const [firstElementChild, previousElementSibling, nextElementSibling, parentElement] = yield parallel(() => [
element.getProperty('firstElementChild'),
element.getProperty('previousElementSibling'),
element.getProperty('nextElementSibling'),
element.getProperty('parentElement'),
]);
if (firstElementChild === null || firstElementChild === void 0 ? void 0 : firstElementChild.parentElement) {
return firstElementChild.parentElement;
}
if (previousElementSibling === null || previousElementSibling === void 0 ? void 0 : previousElementSibling.nextElementSibling) {
return previousElementSibling.nextElementSibling;
}
if (nextElementSibling === null || nextElementSibling === void 0 ? void 0 : nextElementSibling.previousElementSibling) {
return nextElementSibling.previousElementSibling;
}
if (parentElement === null || parentElement === void 0 ? void 0 : parentElement.firstElementChild) {
return parentElement.firstElementChild;
}
return null;
});
}
function byAria(options = {}) {
const { visible, hidden = false, name, description } = options;
return predicate(`match accessible element${Object.keys(options).length ? `\n\twith options:\n${prettyOptions(options)}` : ''}`, function byAria(harness, cache) {
return __awaiter(this, void 0, void 0, function* () {
const host = harness.host();
const element = yield getElement(host);
if (!element) {
return false;
}
else if (name && !(yield matchText(domAccessibilityApi.computeAccessibleName(element), name))) {
return false;
}
else if (description && !(yield matchText(domAccessibilityApi.computeAccessibleDescription(element), description))) {
return false;
}
if (hidden && visible === true) {
return true;
}
if (!hidden && visible === false) {
return false;
}
function cachedIsSubtreeInaccessible(element) {
if (!cache.has(element)) {
cache.set(element, domAccessibilityApi.isSubtreeInaccessible(element));
}
return cache.get(element);
}
return domAccessibilityApi.isInaccessible(element, { isSubtreeInaccessible: cachedIsSubtreeInaccessible }) === hidden;
});
});
}
function withTestId(id) {
return `[${testIdAttribute}="${id}"]`;
}
function withRole(name, implicit = true) {
var _a;
let selectorsWithAttributes = [];
if (implicit) {
const roleRelations = (_a = ariaQuery.roleElements.get(name)) !== null && _a !== void 0 ? _a : [];
selectorsWithAttributes.push(...Array.from(roleRelations)
.map(relation => {
var _a;
let roleSelector = relation.name;
(_a = relation.attributes) === null || _a === void 0 ? void 0 : _a.forEach(attribute => {
roleSelector += attribute.value === undefined ? `[${attribute.name}]` : `[${attribute.name}="${attribute.value}"]`;
});
return roleSelector;
}));
switch (name) {
case "graphics-document": {
selectorsWithAttributes.push('svg');
}
}
}
selectorsWithAttributes.push(`[role="${name}"]`);
return selectorsWithAttributes.join(', ');
}
function withComponent(name) {
return `[${componentAttribute}="${name}"]`;
}
let componentAttribute = 'data-component-name';
let testIdAttribute = 'data-testid';
function setTestIdAttribute(name) {
testIdAttribute = name;
}
function setComponentAttribute(name) {
componentAttribute = name;
}
function selector(selector, ...attributeSelectors) {
const selectors = selector.split(/,\s*/);
attributeSelectors = attributeSelectors.flatMap((attr) => attr.split(/,\s*/));
return selectors.flatMap((tagOrRole) => attributeSelectors.map(attribute => `${tagOrRole}${attribute}`)).join(', ');
}
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* Base class for component harnesses that all component harness authors should extend. This base
* component harness provides the basic ability to locate element and sub-component harness. It
* should be inherited when defining user's own harness.
*/
class ComponentHarness {
static query(filter, ...predicates) {
filter = typeof filter === "object" ? filter : {};
const query = new HarnessPredicate(this, filter);
if (typeof filter === "function") {
predicates = [filter, ...predicates];
}
for (const predicate of predicates) {
query.addOption(predicate.name, query.cache, predicate);
}
return query;
}
static queryByRole(role, options = {}) {
return this.query({ selector: withRole(role) }, byAria(options));
}
static queryByText(text, filters = {}) {
return this.query(filters, byText(text));
}
static queryByLabelText(text, filters = {}) {
return this.query(filters, byLabelText(text));
}
static queryByPlaceholderText(text, filters = {}) {
return this.query(filters, byPlaceholderText(text));
}
static queryByTestId(testId) {
return this.query({ selector: withTestId(testId) });
}
static queryByTitle(text, filters = {}) {
return this.query(filters, byTitle(text));
}
static queryByDisplayValue(text, filters = {}) {
return this.query(filters, byDisplayValue(text));
}
static queryByAltText(text, filters) {
return this.query(filters, byAltText(text));
}
static queryBy(options) {
return new Query(this, options);
}
constructor(locatorFactory) {
this.locatorFactory = locatorFactory;
}
/** Gets a `Promise` for the `TestElement` representing the host element of the component. */
host() {
return this.locatorFactory.rootElement;
}
/**
* Gets a `LocatorFactory` for the document root element. This factory can be used to create
* locators for elements that a component creates outside of its own root element. (e.g. by
* appending to document.body).
*/
documentRootLocatorFactory() {
return this.locatorFactory.documentRootLocatorFactory();
}
/**
* Creates an asynchronous locator function that can be used to find a `ComponentHarness` instance
* or element under the host element of this `ComponentHarness`.
* @param queries A list of queries specifying which harnesses and elements to search for:
* - A `string` searches for elements matching the CSS selector specified by the string.
* - A `ComponentHarness` constructor searches for `ComponentHarness` instances matching the
* given class.
* - A `HarnessPredicate` searches for `ComponentHarness` instances matching the given
* predicate.
* @return An asynchronous locator function that searches for and returns a `Promise` for the
* first element or harness matching the given search criteria. Matches are ordered first by
* order in the DOM, and second by order in the queries list. If no matches are found, the
* `Promise` rejects. The type that the `Promise` resolves to is a union of all result types for
* each query.
*
* e.g. Given the following DOM: `
`, and assuming
* `DivHarness.hostSelector === 'div'`:
* - `await ch.locatorFor(DivHarness, 'div')()` gets a `DivHarness` instance for `#d1`
* - `await ch.locatorFor('div', DivHarness)()` gets a `TestElement` instance for `#d1`
* - `await ch.locatorFor('span')()` throws because the `Promise` rejects.
*/
locatorFor(...queries) {
return this.locatorFactory.locatorFor(...queries);
}
/**
* Creates an asynchronous locator function that can be used to find a `ComponentHarness` instance
* or element under the host element of this `ComponentHarness`.
* @param queries A list of queries specifying which harnesses and elements to search for:
* - A `string` searches for elements matching the CSS selector specified by the string.
* - A `ComponentHarness` constructor searches for `ComponentHarness` instances matching the
* given class.
* - A `HarnessPredicate` searches for `ComponentHarness` instances matching the given
* predicate.
* @return An asynchronous locator function that searches for and returns a `Promise` for the
* first element or harness matching the given search criteria. Matches are ordered first by
* order in the DOM, and second by order in the queries list. If no matches are found, the
* `Promise` is resolved with `null`. The type that the `Promise` resolves to is a union of all
* result types for each query or null.
*
* e.g. Given the following DOM: ``, and assuming
* `DivHarness.hostSelector === 'div'`:
* - `await ch.locatorForOptional(DivHarness, 'div')()` gets a `DivHarness` instance for `#d1`
* - `await ch.locatorForOptional('div', DivHarness)()` gets a `TestElement` instance for `#d1`
* - `await ch.locatorForOptional('span')()` gets `null`.
*/
locatorForOptional(...queries) {
return this.locatorFactory.locatorForOptional(...queries);
}
/**
* Creates an asynchronous locator function that can be used to find `ComponentHarness` instances
* or elements under the host element of this `ComponentHarness`.
* @param queries A list of queries specifying which harnesses and elements to search for:
* - A `string` searches for elements matching the CSS selector specified by the string.
* - A `ComponentHarness` constructor searches for `ComponentHarness` instances matching the
* given class.
* - A `HarnessPredicate` searches for `ComponentHarness` instances matching the given
* predicate.
* @return An asynchronous locator function that searches for and returns a `Promise` for all
* elements and harnesses matching the given search criteria. Matches are ordered first by
* order in the DOM, and second by order in the queries list. If an element matches more than
* one `ComponentHarness` class, the locator gets an instance of each for the same element. If
* an element matches multiple `string` selectors, only one `TestElement` instance is returned
* for that element. The type that the `Promise` resolves to is an array where each element is
* the union of all result types for each query.
*
* e.g. Given the following DOM: ``, and assuming
* `DivHarness.hostSelector === 'div'` and `IdIsD1Harness.hostSelector === '#d1'`:
* - `await ch.locatorForAll(DivHarness, 'div')()` gets `[
* DivHarness, // for #d1
* TestElement, // for #d1
* DivHarness, // for #d2
* TestElement // for #d2
* ]`
* - `await ch.locatorForAll('div', '#d1')()` gets `[
* TestElement, // for #d1
* TestElement // for #d2
* ]`
* - `await ch.locatorForAll(DivHarness, IdIsD1Harness)()` gets `[
* DivHarness, // for #d1
* IdIsD1Harness, // for #d1
* DivHarness // for #d2
* ]`
* - `await ch.locatorForAll('span')()` gets `[]`.
*/
locatorForAll(...queries) {
return this.locatorFactory.locatorForAll(...queries);
}
/**
* Flushes change detection and async tasks in the Angular zone.
* In most cases it should not be necessary to call this manually. However, there may be some edge
* cases where it is needed to fully flush animation events.
*/
forceStabilize() {
return __awaiter(this, void 0, void 0, function* () {
return this.locatorFactory.forceStabilize();
});
}
/**
* Waits for all scheduled or running async tasks to complete. This allows harness
* authors to wait for async tasks outside of the Angular zone.
*/
waitForTasksOutsideAngular() {
return __awaiter(this, void 0, void 0, function* () {
return this.locatorFactory.waitForTasksOutsideAngular();
});
}
/**
* Repeatedly evaluate and resolve an expression until it returns a truthy result. A value is considered
* stable if it doesn't change for two consecutive frames.
*
* @param expr The expression to be evaluated
* @return The first stable truthy result
*/
waitForStable(expr) {
return __awaiter(this, void 0, void 0, function* () {
return this.locatorFactory.waitForStable(() => expr(this));
});
}
}
/**
* Base class for component harnesses that authors should extend if they anticipate that consumers
* of the harness may want to access other harnesses within the `` of the component.
*/
class ContentContainerComponentHarness extends ComponentHarness {
getChildLoader(selector, options) {
return __awaiter(this, void 0, void 0, function* () {
return (yield this.getRootHarnessLoader()).getChildLoader(selector, options);
});
}
getAllChildLoaders(selector, options) {
return __awaiter(this, void 0, void 0, function* () {
return (yield this.getRootHarnessLoader()).getAllChildLoaders(selector, options);
});
}
getHarness(query, options) {
return __awaiter(this, void 0, void 0, function* () {
return (yield this.getRootHarnessLoader()).getHarness(query, options);
});
}
getHarnessOrNull(query, options) {
return __awaiter(this, void 0, void 0, function* () {
return (yield this.getRootHarnessLoader()).getHarnessOrNull(query, options);
});
}
getAllHarnesses(query, options) {
return __awaiter(this, void 0, void 0, function* () {
return (yield this.getRootHarnessLoader()).getAllHarnesses(query, options);
});
}
hasHarness(query) {
return __awaiter(this, void 0, void 0, function* () {
return (yield this.getRootHarnessLoader()).hasHarness(query);
});
}
/**
* Gets the root harness loader from which to start
* searching for content contained by this harness.
*/
getRootHarnessLoader() {
return __awaiter(this, void 0, void 0, function* () {
return this.locatorFactory.rootHarnessLoader();
});
}
}
class TestElementHarness extends ContentContainerComponentHarness {
blur() {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.blur();
});
}
clear() {
return Promise.resolve(undefined);
}
click(...args) {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.click(...args);
});
}
dispatchEvent(name, data) {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.dispatchEvent(name, data);
});
}
focus() {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.focus();
});
}
getAttribute(name) {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.getAttribute(name);
});
}
getCssValue(property) {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.getCssValue(property);
});
}
getDimensions() {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.getDimensions();
});
}
getProperty(name) {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.getProperty(name);
});
}
hasClass(name) {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.hasClass(name);
});
}
hover() {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.hover();
});
}
isFocused() {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.isFocused();
});
}
matchesSelector(selector) {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.matchesSelector(selector);
});
}
mouseAway() {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.mouseAway();
});
}
rightClick(relativeX, relativeY, modifiers) {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.rightClick(relativeX, relativeY, modifiers);
});
}
selectOptions(...optionIndexes) {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.selectOptions(...optionIndexes);
});
}
sendKeys(...keys) {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.sendKeys(...keys);
});
}
setContenteditableValue(value) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return (_a = host.setContenteditableValue) === null || _a === void 0 ? void 0 : _a.call(host, value);
});
}
setInputValue(value) {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.setInputValue(value);
});
}
text(options) {
return __awaiter(this, void 0, void 0, function* () {
const host = this.host();
return host.text(options);
});
}
}
function getElementHarness(selector) {
class ElementHarness extends TestElementHarness {
}
ElementHarness.hostSelector = selector;
return ElementHarness;
}
function query(filter, ...predicates) {
const selector = typeof filter === "string" ? filter : filter === null || filter === void 0 ? void 0 : filter.selector;
const ancestor = typeof filter === "object" ? filter === null || filter === void 0 ? void 0 : filter.ancestor : undefined;
const query = new HarnessPredicate(getElementHarness(selector !== null && selector !== void 0 ? selector : "*"), { ancestor });
for (const predicate of predicates) {
query.addOption(predicate.name, query.cache, predicate);
}
return query;
}
function addQueries(predicate, options) {
if (options.altText) {
const filter = byAltText(options.altText);
predicate.add(filter.name, filter);
}
if (options.displayValue) {
const filter = byDisplayValue(options.displayValue);
predicate.add(filter.name, filter);
}
if (options.labelText) {
const filter = byDisplayValue(options.labelText);
predicate.add(filter.name, filter);
}
if (options.placeholderText) {
const filter = byDisplayValue(options.placeholderText);
predicate.add(filter.name, filter);
}
if (options.text) {
const filter = byText(options.text);
predicate.add(filter.name, filter);
}
const hasAria = options.name || options.description || options.visible || options.hidden;
if (hasAria) {
const filter = byAria(options);
predicate.add(filter.name, filter);
}
}
class Query extends HarnessPredicate {
constructor(harness, options) {
super(harness, options);
this._addQueryOptions(options);
}
_addQueryOptions(options) {
addQueries(this, options);
}
}
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* 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
* functionality of both a `HarnessLoader` and `LocatorFactory`. This class is generic on the raw
* element type, `E`, used by the particular test environment.
*/
class HarnessEnvironment {
// Implemented as part of the `LocatorFactory` interface.
get rootElement() {
this._rootElement =
this._rootElement || this.createTestElement(this.rawRootElement);
return this._rootElement;
}
set rootElement(element) {
this._rootElement = element;
}
constructor(rawRootElement) {
this.rawRootElement = rawRootElement;
}
// Implemented as part of the `LocatorFactory` interface.
documentRootLocatorFactory() {
return this.createEnvironment(this.getDocumentRoot());
}
// Implemented as part of the `LocatorFactory` interface.
locatorFor(...queries) {
return (options) => __awaiter(this, void 0, void 0, function* () {
var _a;
return (yield _assertResultFound(this._waitForResults(() => this._getAllHarnessesAndTestElements(queries), options), _getDescriptionForLocatorForQueries(queries), (_a = options === null || options === void 0 ? void 0 : options.count) !== null && _a !== void 0 ? _a : 1))[0];
});
}
// Implemented as part of the `LocatorFactory` interface.
locatorForOptional(...queries) {
return (options) => __awaiter(this, void 0, void 0, function* () {
var _a;
return (yield _assertResultFound(this._waitForResults(() => this._getAllHarnessesAndTestElements(queries), options), _getDescriptionForLocatorForQueries(queries), (_a = options === null || options === void 0 ? void 0 : options.count) !== null && _a !== void 0 ? _a : -1))[0] || null;
});
}
// Implemented as part of the `LocatorFactory` interface.
locatorForAll(...queries) {
return (options) => {
var _a;
return _assertResultFound(this._waitForResults(() => this._getAllHarnessesAndTestElements(queries), options), _getDescriptionForLocatorForQueries(queries), (_a = options === null || options === void 0 ? void 0 : options.count) !== null && _a !== void 0 ? _a : -1);
};
}
// Implemented as part of the `LocatorFactory` interface.
rootHarnessLoader() {
return __awaiter(this, void 0, void 0, function* () {
return this;
});
}
// Implemented as part of the `LocatorFactory` interface.
harnessLoaderFor(selector, options) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
return this.createEnvironment((yield _assertResultFound(this._waitForResults(() => this.getAllRawElements(selector), options), [
_getDescriptionForHarnessLoaderQuery(selector),
], (_a = options === null || options === void 0 ? void 0 : options.count) !== null && _a !== void 0 ? _a : 1))[0]);
});
}
// Implemented as part of the `LocatorFactory` interface.
harnessLoaderForOptional(selector, options) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const elements = yield _assertResultFound(this._waitForResults(() => this.getAllRawElements(selector), options), [
_getDescriptionForHarnessLoaderQuery(selector),
], (_a = options === null || options === void 0 ? void 0 : options.count) !== null && _a !== void 0 ? _a : -1);
return elements[0] ? this.createEnvironment(elements[0]) : null;
});
}
// Implemented as part of the `LocatorFactory` interface.
harnessLoaderForAll(selector, options) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const elements = yield _assertResultFound(this._waitForResults(() => this.getAllRawElements(selector), options), [
_getDescriptionForHarnessLoaderQuery(selector),
], (_a = options === null || options === void 0 ? void 0 : options.count) !== null && _a !== void 0 ? _a : 0);
return elements.map((element) => this.createEnvironment(element));
});
}
// Implemented as part of the `HarnessLoader` interface.
getHarness(query, options) {
return this.locatorFor(query)(options);
}
// Implemented as part of the `HarnessLoader` interface.
getHarnessOrNull(query, options) {
return this.locatorForOptional(query)(options);
}
// Implemented as part of the `HarnessLoader` interface.
getAllHarnesses(query, options) {
return this.locatorForAll(query)(options);
}
// Implemented as part of the `HarnessLoader` interface.
hasHarness(query, options) {
return __awaiter(this, void 0, void 0, function* () {
return (yield this.locatorForOptional(query)()) !== null;
});
}
// Implemented as part of the `HarnessLoader` interface.
getChildLoader(selector, options) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
return this.createEnvironment((yield _assertResultFound(this.getAllRawElements(selector), [
_getDescriptionForHarnessLoaderQuery(selector),
], (_a = options === null || options === void 0 ? void 0 : options.count) !== null && _a !== void 0 ? _a : 1))[0]);
});
}
// Implemented as part of the `HarnessLoader` interface.
getAllChildLoaders(selector, options) {
return __awaiter(this, void 0, void 0, function* () {
return (yield this.getAllRawElements(selector)).map((e) => this.createEnvironment(e));
});
}
/** Creates a `ComponentHarness` for the given harness type with the given raw host element. */
createComponentHarness(harnessType, element) {
return new harnessType(this.createEnvironment(element));
}
/**
* Matches the given raw elements with the given list of element and harness queries to produce a
* list of matched harnesses and test elements.
*/
_getAllHarnessesAndTestElements(queries) {
return __awaiter(this, void 0, void 0, function* () {
if (!queries.length) {
throw Error("CDK Component harness query must contain at least one element.");
}
const { allQueries, harnessQueries, elementQueries, harnessTypes } = _parseQueries(queries);
// Combine all of the queries into one large comma-delimited selector and use it to get all raw
// elements matching any of the individual queries.
const rawElements = yield this.getAllRawElements([
...elementQueries,
...harnessQueries.map((predicate) => predicate.getSelector()),
].join(","));
// If every query is searching for the same harness subclass, we know every result corresponds
// to an instance of that subclass. Likewise, if every query is for a `TestElement`, we know
// every result corresponds to a `TestElement`. Otherwise we need to verify which result was
// found by which selector so it can be matched to the appropriate instance.
const skipSelectorCheck = (elementQueries.length === 0 && harnessTypes.size === 1) ||
harnessQueries.length === 0;
const perElementMatches = yield parallel(() => rawElements.map((rawElement) => __awaiter(this, void 0, void 0, function* () {
const testElement = this.createTestElement(rawElement);
const allResultsForElement = yield parallel(
// For each query, get `null` if it doesn't match, or a `TestElement` or
// `ComponentHarness` as appropriate if it does match. This gives us everything that
// matches the current raw element, but it may contain duplicate entries (e.g.
// multiple `TestElement` or multiple `ComponentHarness` of the same type).
() => allQueries.map((query) => this._getQueryResultForElement(query, rawElement, testElement, skipSelectorCheck)));
return _removeDuplicateQueryResults(allResultsForElement);
})));
return [].concat(...perElementMatches);
});
}
_waitForResults(expr, options) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const wait = options === null || options === void 0 ? void 0 : options.wait;
const shouldWait = !!wait;
const count = (_a = options === null || options === void 0 ? void 0 : options.count) !== null && _a !== void 0 ? _a : 1;
const timeout = typeof wait === "number" ? wait : 4000;
const then = Date.now();
let results = yield expr();
if (shouldWait) {
while (true) {
if ((count === 0 && results.length === 0) || (count !== 0 && results.length >= count) || (Date.now() - then >= timeout)) {
break;
}
yield this.forceStabilize();
results = yield expr();
}
}
return results;
});
}
/**
* Check whether the given query matches the given element, if it does return the matched
* `TestElement` or `ComponentHarness`, if it does not, return null. In cases where the caller
* knows for sure that the query matches the element's selector, `skipSelectorCheck` can be used
* to skip verification and optimize performance.
*/
_getQueryResultForElement(query, rawElement, testElement, skipSelectorCheck = false) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof query === "string") {
return skipSelectorCheck || (yield testElement.matchesSelector(query))
? testElement
: null;
}
if (skipSelectorCheck ||
(yield testElement.matchesSelector(query.getSelector()))) {
const harness = this.createComponentHarness(query.harnessType, rawElement);
return (yield query.evaluate(harness)) ? harness : null;
}
return null;
});
}
/**
* Evaluates an expression on each frame until it returns a truthy result. A value is considered
* stable if it doesn't change for two consecutive frames. Values are compared with strict equality for primitives and
* JSON.stringify for objects.
*
* @param expr The expression to be evaluated
* @returns The first stable truthy result
*/
waitForStable(expr) {
return __awaiter(this, void 0, void 0, function* () {
let previous;
while (true) {
let result;
result = yield expr();
if (previous !== undefined && (result === previous || JSON.stringify(result) === JSON.stringify(previous))) {
return previous;
}
previous = result;
yield this.forceStabilize();
}
});
}
}
/**
* Parses a list of queries in the format accepted by the `locatorFor*` methods into an easier to
* work with format.
*/
function _parseQueries(queries) {
const allQueries = [];
const harnessQueries = [];
const elementQueries = [];
const harnessTypes = new Set();
for (const query of queries) {
if (typeof query === "string") {
allQueries.push(query);
elementQueries.push(query);
}
else {
const predicate = query instanceof HarnessPredicate
? query
: new HarnessPredicate(query, {});
allQueries.push(predicate);
harnessQueries.push(predicate);
harnessTypes.add(predicate.harnessType);
}
}
return { allQueries, harnessQueries, elementQueries, harnessTypes };
}
/**
* Removes duplicate query results for a particular element. (e.g. multiple `TestElement`
* instances or multiple instances of the same `ComponentHarness` class.
*/
function _removeDuplicateQueryResults(results) {
return __awaiter(this, void 0, void 0, function* () {
let testElementMatched = false;
const matchedHarnessTypes = new Set();
const dedupedMatches = [];
for (const result of results) {
if (!result) {
continue;
}
if (result instanceof ComponentHarness) {
if (!matchedHarnessTypes.has(result.constructor)) {
matchedHarnessTypes.add(result.constructor);
dedupedMatches.push(result);
}
}
else if (!testElementMatched) {
testElementMatched = true;
dedupedMatches.push(result);
}
}
return dedupedMatches;
});
}
/** Verifies that there is at least one result in an array. */
function _assertResultFound(results, queryDescriptions, count) {
return __awaiter(this, void 0, void 0, function* () {
const result = yield results;
if (count < 0) {
return result;
}
if (count && count > 0 && result.length < count) {
throw Error(`Expected at least ${count} element${count === 1 ? '' : 's'} matching one of the following queries:\n` +
queryDescriptions.map((desc) => `(${desc})`).join(",\n"));
}
else if (count === 0 && result.length > 0) {
throw Error(`Expected zero elements matching one of the following queries:\n` +
queryDescriptions.map((desc) => `(${desc})`).join(",\n"));
}
return result;
});
}
/** Gets a list of description strings from a list of queries. */
function _getDescriptionForLocatorForQueries(queries) {
return queries.map((query) => typeof query === "string"
? _getDescriptionForTestElementQuery(query)
: _getDescriptionForComponentHarnessQuery(query));
}
/** Gets a description string for a `ComponentHarness` query. */
function _getDescriptionForComponentHarnessQuery(query) {
const harnessPredicate = query instanceof HarnessPredicate
? query
: new HarnessPredicate(query, {});
const { name, hostSelector } = harnessPredicate.harnessType;
const description = `${name} with host element matching selector: "${hostSelector}"`;
const constraints = harnessPredicate.getDescription();
return (description +
(constraints
? ` satisfying the constraints: ${harnessPredicate.getDescription()}`
: ""));
}
/** Gets a description string for a `TestElement` query. */
function _getDescriptionForTestElementQuery(selector) {
return `TestElement for element matching selector: "${selector}"`;
}
/** Gets a description string for a `HarnessLoader` query. */
function _getDescriptionForHarnessLoaderQuery(selector) {
return `HarnessLoader for element matching selector: "${selector}"`;
}
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
function getNoKeysSpecifiedError() {
return Error('No keys have been specified.');
}
/** An enum of non-text keys that can be used with the `sendKeys` method. */
// NOTE: This is a separate enum from `@angular/cdk/keycodes` because we don't necessarily want to
// support every possible keyCode. We also can't rely on Protractor's `Key` because we don't want a
// dependency on any particular testing framework here. Instead we'll just maintain this supported
// list of keys and let individual concrete `HarnessEnvironment` classes map them to whatever key
// representation is used in its respective testing framework.
// tslint:disable-next-line:prefer-const-enum Seems like this causes some issues with System.js
exports.TestKey = void 0;
(function (TestKey) {
TestKey[TestKey["BACKSPACE"] = 0] = "BACKSPACE";
TestKey[TestKey["TAB"] = 1] = "TAB";
TestKey[TestKey["ENTER"] = 2] = "ENTER";
TestKey[TestKey["SHIFT"] = 3] = "SHIFT";
TestKey[TestKey["CONTROL"] = 4] = "CONTROL";
TestKey[TestKey["ALT"] = 5] = "ALT";
TestKey[TestKey["ESCAPE"] = 6] = "ESCAPE";
TestKey[TestKey["PAGE_UP"] = 7] = "PAGE_UP";
TestKey[TestKey["PAGE_DOWN"] = 8] = "PAGE_DOWN";
TestKey[TestKey["END"] = 9] = "END";
TestKey[TestKey["HOME"] = 10] = "HOME";
TestKey[TestKey["LEFT_ARROW"] = 11] = "LEFT_ARROW";
TestKey[TestKey["UP_ARROW"] = 12] = "UP_ARROW";
TestKey[TestKey["RIGHT_ARROW"] = 13] = "RIGHT_ARROW";
TestKey[TestKey["DOWN_ARROW"] = 14] = "DOWN_ARROW";
TestKey[TestKey["INSERT"] = 15] = "INSERT";
TestKey[TestKey["DELETE"] = 16] = "DELETE";
TestKey[TestKey["F1"] = 17] = "F1";
TestKey[TestKey["F2"] = 18] = "F2";
TestKey[TestKey["F3"] = 19] = "F3";
TestKey[TestKey["F4"] = 20] = "F4";
TestKey[TestKey["F5"] = 21] = "F5";
TestKey[TestKey["F6"] = 22] = "F6";
TestKey[TestKey["F7"] = 23] = "F7";
TestKey[TestKey["F8"] = 24] = "F8";
TestKey[TestKey["F9"] = 25] = "F9";
TestKey[TestKey["F10"] = 26] = "F10";
TestKey[TestKey["F11"] = 27] = "F11";
TestKey[TestKey["F12"] = 28] = "F12";
TestKey[TestKey["META"] = 29] = "META";
TestKey[TestKey["COMMA"] = 30] = "COMMA";
})(exports.TestKey || (exports.TestKey = {}));
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* Gets text of element excluding certain selectors within the element.
* @param element Element to get text from,
* @param excludeSelector Selector identifying which elements to exclude,
*/
function _getTextWithExcludedElements(element, excludeSelector) {
const clone = element.cloneNode(true);
const exclusions = clone.querySelectorAll(excludeSelector);
for (let i = 0; i < exclusions.length; i++) {
exclusions[i].remove();
}
return (clone.textContent || '').trim();
}
const methodNames = new Set([
"blur",
"clear",
"click",
"rightClick",
"focus",
"getCssValue",
"hover",
"mouseAway",
"sendKeys",
"text",
"setContenteditableValue",
"getAttribute",
"hasClass",
"getDimensions",
"getProperty",
"matchesSelector",
"isFocused",
"setInputValue",
"selectOptions",
"dispatchEvent",
]);
const screen = Array.from(methodNames).reduce((acc, method) => {
acc[method] = (handle, ...args) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
handle = yield handle;
if (handle instanceof ComponentHarness) {
const host = yield handle.host();
return (_a = host[method]) === null || _a === void 0 ? void 0 : _a.call(host, ...args);
}
});
return acc;
}, {});
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
const BACKSPACE = 8;
const TAB = 9;
const ENTER = 13;
const SHIFT = 16;
const CONTROL = 17;
const ALT = 18;
const ESCAPE = 27;
const PAGE_UP = 33;
const PAGE_DOWN = 34;
const END = 35;
const HOME = 36;
const LEFT_ARROW = 37;
const UP_ARROW = 38;
const RIGHT_ARROW = 39;
const DOWN_ARROW = 40;
const INSERT = 45;
const DELETE = 46;
const META = 91; // WIN_KEY_LEFT
const F1 = 112;
const F2 = 113;
const F3 = 114;
const F4 = 115;
const F5 = 116;
const F6 = 117;
const F7 = 118;
const F8 = 119;
const F9 = 120;
const F10 = 121;
const F11 = 122;
const F12 = 123;
const COMMA = 188;
const PERIOD = 190;
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* Creates a browser MouseEvent with the specified options.
* @docs-private
*/
function createMouseEvent(type, clientX = 0, clientY = 0, offsetX = 1, offsetY = 1, button = 0, modifiers = {}) {
// Note: We cannot determine the position of the mouse event based on the screen
// because the dimensions and position of the browser window are not available
// To provide reasonable `screenX` and `screenY` coordinates, we simply use the
// client coordinates as if the browser is opened in fullscreen.
const screenX = clientX;
const screenY = clientY;
const event = new MouseEvent(type, {
bubbles: true,
cancelable: true,
composed: true,
view: window,
detail: 0,
relatedTarget: null,
screenX,
screenY,
clientX,
clientY,
ctrlKey: modifiers.control,
altKey: modifiers.alt,
shiftKey: modifiers.shift,
metaKey: modifiers.meta,
button: button,
buttons: 1,
});
// The `MouseEvent` constructor doesn't allow us to pass these properties into the constructor.
// Override them to `1`, because they're used for fake screen reader event detection.
if (offsetX != null) {
defineReadonlyEventProperty(event, 'offsetX', offsetX);
}
if (offsetY != null) {
defineReadonlyEventProperty(event, 'offsetY', offsetY);
}
return event;
}
/**
* Creates a browser `PointerEvent` with the specified options. Pointer events
* by default will appear as if they are the primary pointer of their type.
* https://www.w3.org/TR/pointerevents2/#dom-pointerevent-isprimary.
*
* For example, if pointer events for a multi-touch interaction are created, the non-primary
* pointer touches would need to be represented by non-primary pointer events.
*
* @docs-private
*/
function createPointerEvent(type, clientX = 0, clientY = 0, offsetX, offsetY, options = { isPrimary: true }) {
const event = new PointerEvent(type, Object.assign({ bubbles: true, cancelable: true, composed: true, view: window, clientX,
clientY }, options));
if (offsetX != null) {
defineReadonlyEventProperty(event, 'offsetX', offsetX);
}
if (offsetY != null) {
defineReadonlyEventProperty(event, 'offsetY', offsetY);
}
return event;
}
/**
* Creates a keyboard event with the specified key and modifiers.
* @docs-private
*/
function createKeyboardEvent(type, keyCode = 0, key = '', modifiers = {}) {
return new KeyboardEvent(type, {
bubbles: true,
cancelable: true,
composed: true,
view: window,
keyCode: keyCode,
key: key,
shiftKey: modifiers.shift,
metaKey: modifiers.meta,
altKey: modifiers.alt,
ctrlKey: modifiers.control,
});
}
/**
* Creates a fake event object with any desired event type.
* @docs-private
*/
function createFakeEvent(type, bubbles = false, cancelable = true, composed = true) {
return new Event(type, { bubbles, cancelable, composed });
}
/**
* Defines a readonly property on the given event object. Readonly properties on an event object
* are always set as configurable as that matches default readonly properties for DOM event objects.
*/
function defineReadonlyEventProperty(event, propertyName, value) {
Object.defineProperty(event, propertyName, { get: () => value, configurable: true });
}
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* Utility to dispatch any event on a Node.
* @docs-private
*/
function dispatchEvent(node, event) {
node.dispatchEvent(event);
return event;
}
/**
* Shorthand to dispatch a fake event on a specified node.
* @docs-private
*/
function dispatchFakeEvent(node, type, bubbles) {
return dispatchEvent(node, createFakeEvent(type, bubbles));
}
/**
* Shorthand to dispatch a keyboard event with a specified key code and
* optional modifiers.
* @docs-private
*/
function dispatchKeyboardEvent(node, type, keyCode, key, modifiers) {
return dispatchEvent(node, createKeyboardEvent(type, keyCode, key, modifiers));
}
/**
* Shorthand to dispatch a mouse event on the specified coordinates.
* @docs-private
*/
function dispatchMouseEvent(node, type, clientX = 0, clientY = 0, offsetX, offsetY, button, modifiers) {
return dispatchEvent(node, createMouseEvent(type, clientX, clientY, offsetX, offsetY, button, modifiers));
}
/**
* Shorthand to dispatch a pointer event on the specified coordinates.
* @docs-private
*/
function dispatchPointerEvent(node, type, clientX = 0, clientY = 0, offsetX, offsetY, options) {
return dispatchEvent(node, createPointerEvent(type, clientX, clientY, offsetX, offsetY, options));
}
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
function triggerFocusChange(element, event) {
let eventFired = false;
const handler = () => (eventFired = true);
element.addEventListener(event, handler);
element[event]();
element.removeEventListener(event, handler);
if (!eventFired) {
dispatchFakeEvent(element, event);
}
}
/** @docs-private */
function triggerFocus(element) {
triggerFocusChange(element, 'focus');
}
/** @docs-private */
function triggerBlur(element) {
triggerFocusChange(element, 'blur');
}
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/** Input types for which the value can be entered incrementally. */
const incrementalInputTypes = new Set([
'text',
'email',
'hidden',
'password',
'search',
'tel',
'url',
]);
/**
* Checks whether the given Element is a text input element.
* @docs-private
*/
function isTextInput(element) {
const nodeName = element.nodeName.toLowerCase();
return nodeName === 'input' || nodeName === 'textarea';
}
function typeInElement(element, ...modifiersAndKeys) {
const first = modifiersAndKeys[0];
let modifiers;
let rest;
if (first !== undefined &&
typeof first !== 'string' &&
first.keyCode === undefined &&
first.key === undefined) {
modifiers = first;
rest = modifiersAndKeys.slice(1);
}
else {
modifiers = {};
rest = modifiersAndKeys;
}
const isInput = isTextInput(element);
const inputType = element.getAttribute('type') || 'text';
const keys = rest
.map(k => typeof k === 'string'
? k.split('').map(c => ({ keyCode: c.toUpperCase().charCodeAt(0), key: c }))
: [k])
.reduce((arr, k) => arr.concat(k), []);
// Throw an error if no keys have been specified. Calling this function with no
// keys should not result in a focus event being dispatched unexpectedly.
if (keys.length === 0) {
throw getNoKeysSpecifiedError();
}
// We simulate the user typing in a value by incrementally assigning the value below. The problem
// is that for some input types, the browser won't allow for an invalid value to be set via the
// `value` property which will always be the case when going character-by-character. If we detect
// such an input, we have to set the value all at once or listeners to the `input` event (e.g.
// the `ReactiveFormsModule` uses such an approach) won't receive the correct value.
const enterValueIncrementally = inputType === 'number'
? // The value can be set character by character in number inputs if it doesn't have any decimals.
keys.every(key => key.key !== '.' && key.key !== '-' && key.keyCode !== PERIOD)
: incrementalInputTypes.has(inputType);
triggerFocus(element);
// When we aren't entering the value incrementally, assign it all at once ahead
// of time so that any listeners to the key events below will have access to it.
if (!enterValueIncrementally) {
element.value = keys.reduce((value, key) => value + (key.key || ''), '');
}
for (const key of keys) {
dispatchKeyboardEvent(element, 'keydown', key.keyCode, key.key, modifiers);
dispatchKeyboardEvent(element, 'keypress', key.keyCode, key.key, modifiers);
if (isInput && key.key && key.key.length === 1) {
if (enterValueIncrementally) {
element.value += key.key;
dispatchFakeEvent(element, 'input');
}
}
dispatchKeyboardEvent(element, 'keyup', key.keyCode, key.key, modifiers);
}
// Since we weren't dispatching `input` events while sending the keys, we have to do it now.
if (!enterValueIncrementally) {
dispatchFakeEvent(element, 'input');
}
}
/**
* Clears the text in an input or textarea element.
* @docs-private
*/
function clearElement(element) {
triggerFocus(element);
element.value = '';
dispatchFakeEvent(element, 'input');
}
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/** Maps `TestKey` constants to the `keyCode` and `key` values used by native browser events. */
const keyMap = {
[exports.TestKey.BACKSPACE]: { keyCode: BACKSPACE, key: 'Backspace' },
[exports.TestKey.TAB]: { keyCode: TAB, key: 'Tab' },
[exports.TestKey.ENTER]: { keyCode: ENTER, key: 'Enter' },
[exports.TestKey.SHIFT]: { keyCode: SHIFT, key: 'Shift' },
[exports.TestKey.CONTROL]: { keyCode: CONTROL, key: 'Control' },
[exports.TestKey.ALT]: { keyCode: ALT, key: 'Alt' },
[exports.TestKey.ESCAPE]: { keyCode: ESCAPE, key: 'Escape' },
[exports.TestKey.PAGE_UP]: { keyCode: PAGE_UP, key: 'PageUp' },
[exports.TestKey.PAGE_DOWN]: { keyCode: PAGE_DOWN, key: 'PageDown' },
[exports.TestKey.END]: { keyCode: END, key: 'End' },
[exports.TestKey.HOME]: { keyCode: HOME, key: 'Home' },
[exports.TestKey.LEFT_ARROW]: { keyCode: LEFT_ARROW, key: 'ArrowLeft' },
[exports.TestKey.UP_ARROW]: { keyCode: UP_ARROW, key: 'ArrowUp' },
[exports.TestKey.RIGHT_ARROW]: { keyCode: RIGHT_ARROW, key: 'ArrowRight' },
[exports.TestKey.DOWN_ARROW]: { keyCode: DOWN_ARROW, key: 'ArrowDown' },
[exports.TestKey.INSERT]: { keyCode: INSERT, key: 'Insert' },
[exports.TestKey.DELETE]: { keyCode: DELETE, key: 'Delete' },
[exports.TestKey.F1]: { keyCode: F1, key: 'F1' },
[exports.TestKey.F2]: { keyCode: F2, key: 'F2' },
[exports.TestKey.F3]: { keyCode: F3, key: 'F3' },
[exports.TestKey.F4]: { keyCode: F4, key: 'F4' },
[exports.TestKey.F5]: { keyCode: F5, key: 'F5' },
[exports.TestKey.F6]: { keyCode: F6, key: 'F6' },
[exports.TestKey.F7]: { keyCode: F7, key: 'F7' },
[exports.TestKey.F8]: { keyCode: F8, key: 'F8' },
[exports.TestKey.F9]: { keyCode: F9, key: 'F9' },
[exports.TestKey.F10]: { keyCode: F10, key: 'F10' },
[exports.TestKey.F11]: { keyCode: F11, key: 'F11' },
[exports.TestKey.F12]: { keyCode: F12, key: 'F12' },
[exports.TestKey.META]: { keyCode: META, key: 'Meta' },
[exports.TestKey.COMMA]: { keyCode: COMMA, key: ',' },
};
/** A `TestElement` implementation for unit tests. */
class UnitTestElement {
constructor(element, _stabilize) {
this.element = element;
this._stabilize = _stabilize;
}
/** Blur the element. */
blur() {
return __awaiter(this, void 0, void 0, function* () {
triggerBlur(this.element);
yield this._stabilize();
});
}
/** Clear the element's input (for input and textarea elements only). */
clear() {
return __awaiter(this, void 0, void 0, function* () {
if (!isTextInput(this.element)) {
throw Error('Attempting to clear an invalid element');
}
clearElement(this.element);
yield this._stabilize();
});
}
click(...args) {
return __awaiter(this, void 0, void 0, function* () {
const isDisabled = this.element.disabled === true;
// If the element is `disabled` and has a `disabled` property, we emit the mouse event
// sequence but not dispatch the `click` event. This is necessary to keep the behavior
// consistent with an actual user interaction. The click event is not necessarily
// automatically prevented by the browser. There is mismatch between Firefox and Chromium:
// https://bugzilla.mozilla.org/show_bug.cgi?id=329509.
// https://bugs.chromium.org/p/chromium/issues/detail?id=1115661.
yield this._dispatchMouseEventSequence(isDisabled ? null : 'click', args, 0);
yield this._stabilize();
});
}
rightClick(...args) {
return __awaiter(this, void 0, void 0, function* () {
yield this._dispatchMouseEventSequence('contextmenu', args, 2);
yield this._stabilize();
});
}
/** Focus the element. */
focus() {
return __awaiter(this, void 0, void 0, function* () {
triggerFocus(this.element);
yield this._stabilize();
});
}
/** Get the computed value of the given CSS property for the element. */
getCssValue(property) {
return __awaiter(this, void 0, void 0, function* () {
yield this._stabilize();
// TODO(mmalerba): Consider adding value normalization if we run into common cases where its
// needed.
return getComputedStyle(this.element).getPropertyValue(property);
});
}
/** Hovers the mouse over the element. */
hover() {
return __awaiter(this, void 0, void 0, function* () {
this._dispatchPointerEventIfSupported('pointerenter');
dispatchMouseEvent(this.element, 'mouseover');
dispatchMouseEvent(this.element, 'mouseenter');
yield this._stabilize();
});
}
/** Moves the mouse away from the element. */
mouseAway() {
return __awaiter(this, void 0, void 0, function* () {
this._dispatchPointerEventIfSupported('pointerleave');
dispatchMouseEvent(this.element, 'mouseout');
dispatchMouseEvent(this.element, 'mouseleave');
yield this._stabilize();
});
}
sendKeys(...modifiersAndKeys) {
return __awaiter(this, void 0, void 0, function* () {
const args = modifiersAndKeys.map(k => (typeof k === 'number' ? keyMap[k] : k));
typeInElement(this.element, ...args);
yield this._stabilize();
});
}
/**
* Gets the text from the element.
* @param options Options that affect what text is included.
*/
text(options) {
return __awaiter(this, void 0, void 0, function* () {
yield this._stabilize();
if (options === null || options === void 0 ? void 0 : options.exclude) {
return _getTextWithExcludedElements(this.element, options.exclude);
}
return (this.element.textContent || '').trim();
});
}
/**
* Sets the value of a `contenteditable` element.
* @param value Value to be set on the element.
*/
setContenteditableValue(value) {
return __awaiter(this, void 0, void 0, function* () {
const contenteditableAttr = yield this.getAttribute('contenteditable');
if (contenteditableAttr !== '' && contenteditableAttr !== 'true') {
throw new Error('setContenteditableValue can only be called on a `contenteditable` element.');
}
yield this._stabilize();
this.element.textContent = value;
});
}
/** Gets the value for the given attribute from the element. */
getAttribute(name) {
return __awaiter(this, void 0, void 0, function* () {
yield this._stabilize();
return this.element.getAttribute(name);
});
}
/** Checks whether the element has the given class. */
hasClass(name) {
return __awaiter(this, void 0, void 0, function* () {
yield this._stabilize();
return this.element.classList.contains(name);
});
}
/** Gets the dimensions of the element. */
getDimensions() {
return __awaiter(this, void 0, void 0, function* () {
yield this._stabilize();
return this.element.getBoundingClientRect();
});
}
/** Gets the value of a property of an element. */
getProperty(name) {
return __awaiter(this, void 0, void 0, function* () {
yield this._stabilize();
return this.element[name];
});
}
/** Sets the value of a property of an input. */
setInputValue(value) {
return __awaiter(this, void 0, void 0, function* () {
this.element.value = value;
yield this._stabilize();
});
}
/** Selects the options at the specified indexes inside of a native `select` element. */
selectOptions(...optionIndexes) {
return __awaiter(this, void 0, void 0, function* () {
let hasChanged = false;
const options = this.element.querySelectorAll('option');
const indexes = new Set(optionIndexes); // Convert to a set to remove duplicates.
for (let i = 0; i < options.length; i++) {
const option = options[i];
const wasSelected = option.selected;
// We have to go through `option.selected`, because `HTMLSelectElement.value` doesn't
// allow for multiple options to be selected, even in `multiple` mode.
option.selected = indexes.has(i);
if (option.selected !== wasSelected) {
hasChanged = true;
dispatchFakeEvent(this.element, 'change');
}
}
if (hasChanged) {
yield this._stabilize();
}
});
}
/** Checks whether this element matches the given selector. */
matchesSelector(selector) {
return __awaiter(this, void 0, void 0, function* () {
yield this._stabilize();
const elementPrototype = Element.prototype;
return (elementPrototype['matches'] || elementPrototype['msMatchesSelector']).call(this.element, selector);
});
}
/** Checks whether the element is focused. */
isFocused() {
return __awaiter(this, void 0, void 0, function* () {
yield this._stabilize();
return document.activeElement === this.element;
});
}
/**
* Dispatches an event with a particular name.
* @param name Name of the event to be dispatched.
*/
dispatchEvent(name, data) {
return __awaiter(this, void 0, void 0, function* () {
const event = createFakeEvent(name);
if (data) {
// tslint:disable-next-line:ban Have to use `Object.assign` to preserve the original object.
Object.assign(event, data);
}
dispatchEvent(this.element, event);
yield this._stabilize();
});
}
/**
* Dispatches a pointer event on the current element if the browser supports it.
* @param name Name of the pointer event to be dispatched.
* @param clientX Coordinate of the user's pointer along the X axis.
* @param clientY Coordinate of the user's pointer along the Y axis.
* @param button Mouse button that should be pressed when dispatching the event.
*/
_dispatchPointerEventIfSupported(name, clientX, clientY, offsetX, offsetY, button) {
// The latest versions of all browsers we support have the new `PointerEvent` API.
// Though since we capture the two most recent versions of these browsers, we also
// need to support Safari 12 at time of writing. Safari 12 does not have support for this,
// so we need to conditionally create and dispatch these events based on feature detection.
if (typeof PointerEvent !== 'undefined' && PointerEvent) {
dispatchPointerEvent(this.element, name, clientX, clientY, offsetX, offsetY, {
isPrimary: true,
button,
});
}
}
/**
* Dispatches all the events that are part of a mouse event sequence
* and then emits a given primary event at the end, if speciifed.
*/
_dispatchMouseEventSequence(primaryEventName, args, button) {
return __awaiter(this, void 0, void 0, function* () {
let clientX = undefined;
let clientY = undefined;
let offsetX = undefined;
let offsetY = undefined;
let modifiers = {};
if (args.length && typeof args[args.length - 1] === 'object') {
modifiers = args.pop();
}
if (args.length) {
const { left, top, width, height } = yield this.getDimensions();
offsetX = args[0] === 'center' ? width / 2 : args[0];
offsetY = args[0] === 'center' ? height / 2 : args[1];
// Round the computed click position as decimal pixels are not
// supported by mouse events and could lead to unexpected results.
clientX = Math.round(left + offsetX);
clientY = Math.round(top + offsetY);
}
this._dispatchPointerEventIfSupported('pointerdown', clientX, clientY, offsetX, offsetY, button);
dispatchMouseEvent(this.element, 'mousedown', clientX, clientY, offsetX, offsetY, button, modifiers);
this._dispatchPointerEventIfSupported('pointerup', clientX, clientY, offsetX, offsetY, button);
dispatchMouseEvent(this.element, 'mouseup', clientX, clientY, offsetX, offsetY, button, modifiers);
// If a primary event name is specified, emit it after the mouse event sequence.
if (primaryEventName !== null) {
dispatchMouseEvent(this.element, primaryEventName, clientX, clientY, offsetX, offsetY, button, modifiers);
}
// This call to _stabilize should not be needed since the callers will already do that them-
// selves. Nevertheless it breaks some tests in g3 without it. It needs to be investigated
// why removing breaks those tests.
// See: https://github.com/angular/components/pull/20758/files#r520886256.
yield this._stabilize();
});
}
}
class UnitTestHarnessEnvironment extends HarnessEnvironment {
static getHarness(harnessType, options) {
return new UnitTestHarnessEnvironment(document.body).getHarness(harnessType, options);
}
static getHarnessOrNull(harnessType, options) {
return new UnitTestHarnessEnvironment(document.body).getHarnessOrNull(harnessType, options);
}
static getAllHarnesses(harnessType, options) {
return new UnitTestHarnessEnvironment(document.body).getAllHarnesses(harnessType, options);
}
static getHarnessForContainer(element, harnessType, options) {
return new UnitTestHarnessEnvironment(element).getHarness(harnessType, options);
}
static getAllHarnessesForContainer(element, harnessType, options) {
return new UnitTestHarnessEnvironment(element).getAllHarnesses(harnessType, options);
}
static getRootHarnessLoader() {
return new UnitTestHarnessEnvironment(document.body).rootHarnessLoader();
}
static getNativeElement(element) {
if (element instanceof UnitTestElement) {
return element.element;
}
if (element instanceof ComponentHarness) {
return this.getNativeElement(element.host());
}
throw new Error("This TestElement was not created by the UnitTestHarnessEnvironment");
}
forceStabilize() {
return __awaiter(this, void 0, void 0, function* () {
yield new Promise(requestAnimationFrame);
});
}
waitForTasksOutsideAngular() {
throw new Error('Method not implemented.');
}
getDocumentRoot() {
return document.body;
}
createTestElement(element) {
return new UnitTestElement(element, () => Promise.resolve());
}
createEnvironment(element) {
return new UnitTestHarnessEnvironment(element);
}
getAllRawElements(selector) {
return __awaiter(this, void 0, void 0, function* () {
return Array.from(this.rawRootElement.querySelectorAll(selector));
});
}
}
function queryByRole(role, options = {}) {
return query(withRole(role), byAria(options));
}
function queryByText(text, selector) {
return query(selector, byText(text));
}
function queryByLabelText(text, selector) {
return query(selector, byLabelText(text));
}
function queryByPlaceholderText(text, selector) {
return query(selector, byPlaceholderText(text));
}
function queryByTestId(testId) {
return query(withTestId(testId));
}
function queryByTitle(text, selector) {
return query(selector, byTitle(text));
}
function queryByDisplayValue(text, selector) {
return query(selector, byDisplayValue(text));
}
function queryByAltText(text, selector) {
return query(selector, byAltText(text));
}
function queryBy(options) {
const predicate = query(options);
addQueries(predicate, options);
return predicate;
}
exports.ComponentHarness = ComponentHarness;
exports.ContentContainerComponentHarness = ContentContainerComponentHarness;
exports.HarnessEnvironment = HarnessEnvironment;
exports.HarnessPredicate = HarnessPredicate;
exports.Query = Query;
exports.TestElementHarness = TestElementHarness;
exports.UnitTestElement = UnitTestElement;
exports.UnitTestHarnessEnvironment = UnitTestHarnessEnvironment;
exports._getTextWithExcludedElements = _getTextWithExcludedElements;
exports.addQueries = addQueries;
exports.afterScenario = afterScenario;
exports.afterStep = afterStep;
exports.and = and;
exports.background = background;
exports.beforeScenario = beforeScenario;
exports.beforeStep = beforeStep;
exports.byAltText = byAltText;
exports.byAria = byAria;
exports.byDisplayValue = byDisplayValue;
exports.byLabelText = byLabelText;
exports.byPlaceholderText = byPlaceholderText;
exports.byText = byText;
exports.byTitle = byTitle;
exports.clearTestContext = clearTestContext;
exports.createTestContext = createTestContext;
exports.createTestSuite = createTestSuite;
exports.eq = eq;
exports.feature = feature;
exports.getAllScenarios = getAllScenarios;
exports.getElementHarness = getElementHarness;
exports.getNoKeysSpecifiedError = getNoKeysSpecifiedError;
exports.getTags = getTags;
exports.isExcluded = isExcluded;
exports.isIncluded = isIncluded;
exports.isTagUsed = isTagUsed;
exports.matchText = matchText;
exports.methodNames = methodNames;
exports.not = not;
exports.only = only;
exports.or = or;
exports.parallel = parallel;
exports.predicate = predicate;
exports.query = query;
exports.queryBy = queryBy;
exports.queryByAltText = queryByAltText;
exports.queryByDisplayValue = queryByDisplayValue;
exports.queryByLabelText = queryByLabelText;
exports.queryByPlaceholderText = queryByPlaceholderText;
exports.queryByRole = queryByRole;
exports.queryByTestId = queryByTestId;
exports.queryByText = queryByText;
exports.queryByTitle = queryByTitle;
exports.scenario = scenario;
exports.screen = screen;
exports.selector = selector;
exports.setAdapter = setAdapter;
exports.setComponentAttribute = setComponentAttribute;
exports.setTags = setTags;
exports.setTestIdAttribute = setTestIdAttribute;
exports.skip = skip;
exports.todo = todo;
exports.withComponent = withComponent;
exports.withRole = withRole;
exports.withTestId = withTestId;