chrome-devtools-frontend
Version:
Chrome DevTools UI
1,509 lines (1,494 loc) • 64.1 kB
JavaScript
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
var SelectorType;
(function (SelectorType) {
SelectorType["CSS"] = "css";
SelectorType["ARIA"] = "aria";
SelectorType["Text"] = "text";
SelectorType["XPath"] = "xpath";
SelectorType["Pierce"] = "pierce";
})(SelectorType || (SelectorType = {}));
var StepType;
(function (StepType) {
StepType["Change"] = "change";
StepType["Click"] = "click";
StepType["Close"] = "close";
StepType["CustomStep"] = "customStep";
StepType["DoubleClick"] = "doubleClick";
StepType["EmulateNetworkConditions"] = "emulateNetworkConditions";
StepType["Hover"] = "hover";
StepType["KeyDown"] = "keyDown";
StepType["KeyUp"] = "keyUp";
StepType["Navigate"] = "navigate";
StepType["Scroll"] = "scroll";
StepType["SetViewport"] = "setViewport";
StepType["WaitForElement"] = "waitForElement";
StepType["WaitForExpression"] = "waitForExpression";
})(StepType || (StepType = {}));
var AssertedEventType;
(function (AssertedEventType) {
AssertedEventType["Navigation"] = "navigation";
})(AssertedEventType || (AssertedEventType = {}));
var Schema = /*#__PURE__*/Object.freeze({
__proto__: null,
get AssertedEventType () { return AssertedEventType; },
get SelectorType () { return SelectorType; },
get StepType () { return StepType; }
});
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
function assertAllStepTypesAreHandled(s) {
throw new Error(`Unknown step type: ${s.type}`);
}
const typeableInputTypes = new Set([
'textarea',
'text',
'url',
'tel',
'search',
'password',
'number',
'email',
]);
const pointerDeviceTypes = new Set([
'mouse',
'pen',
'touch',
]);
const mouseButtonMap = new Map([
['primary', 'left'],
['auxiliary', 'middle'],
['secondary', 'right'],
['back', 'back'],
['forward', 'forward'],
]);
function hasProperty(data, prop) {
// TODO: use Object.hasOwn once types are available https://github.com/microsoft/TypeScript/issues/44253
if (!Object.prototype.hasOwnProperty.call(data, prop)) {
return false;
}
const keyedData = data;
return keyedData[prop] !== undefined;
}
function isObject(data) {
return typeof data === 'object' && data !== null;
}
function isString(data) {
return typeof data === 'string';
}
function isNumber(data) {
return typeof data === 'number';
}
function isArray(data) {
return Array.isArray(data);
}
function isBoolean(data) {
return typeof data === 'boolean';
}
function isIntegerArray(data) {
return isArray(data) && data.every((item) => Number.isInteger(item));
}
function isKnownDeviceType(data) {
return typeof data === 'string' && pointerDeviceTypes.has(data);
}
function isKnownMouseButton(data) {
return typeof data === 'string' && mouseButtonMap.has(data);
}
function parseTarget(step) {
if (hasProperty(step, 'target') && isString(step.target)) {
return step.target;
}
return undefined;
}
function parseFrame(step) {
if (hasProperty(step, 'frame')) {
if (isIntegerArray(step.frame)) {
return step.frame;
}
throw new Error('Step `frame` is not an integer array');
}
return undefined;
}
function parseNumber(step, prop) {
if (hasProperty(step, prop)) {
const maybeNumber = step[prop];
if (isNumber(maybeNumber)) {
return maybeNumber;
}
}
throw new Error(`Step.${prop} is not a number`);
}
function parseBoolean(step, prop) {
if (hasProperty(step, prop)) {
const maybeBoolean = step[prop];
if (isBoolean(maybeBoolean)) {
return maybeBoolean;
}
}
throw new Error(`Step.${prop} is not a boolean`);
}
function parseOptionalNumber(step, prop) {
if (hasProperty(step, prop)) {
return parseNumber(step, prop);
}
return undefined;
}
function parseOptionalString(step, prop) {
if (hasProperty(step, prop)) {
return parseString(step, prop);
}
return undefined;
}
function parseOptionalBoolean(step, prop) {
if (hasProperty(step, prop)) {
return parseBoolean(step, prop);
}
return undefined;
}
function parseString(step, prop) {
if (hasProperty(step, prop)) {
const maybeString = step[prop];
if (isString(maybeString)) {
return maybeString;
}
}
throw new Error(`Step.${prop} is not a string`);
}
function parseSelectors(step) {
if (!hasProperty(step, 'selectors')) {
throw new Error('Step does not have required selectors');
}
if (!isArray(step.selectors)) {
throw new Error('Step selectors are not an array');
}
if (step.selectors.length === 0) {
throw new Error('Step does not have required selectors');
}
return step.selectors.map((s) => {
if (!isString(s) && !isArray(s)) {
throw new Error('Selector is not an array or string');
}
if (isArray(s)) {
return s.map((sub) => {
if (!isString(sub)) {
throw new Error('Selector element is not a string');
}
return sub;
});
}
return s;
});
}
function parseOptionalSelectors(step) {
if (!hasProperty(step, 'selectors')) {
return undefined;
}
return parseSelectors(step);
}
function parseAssertedEvent(event) {
if (!isObject(event)) {
throw new Error('Asserted event is not an object');
}
if (!hasProperty(event, 'type')) {
throw new Error('Asserted event is missing type');
}
if (event.type === AssertedEventType.Navigation) {
return {
type: AssertedEventType.Navigation,
url: parseOptionalString(event, 'url'),
title: parseOptionalString(event, 'title'),
};
}
throw new Error('Unknown assertedEvent type');
}
function parseAssertedEvents(events) {
if (!isArray(events)) {
return undefined;
}
return events.map(parseAssertedEvent);
}
function parseBaseStep(type, step) {
if (hasProperty(step, 'timeout') &&
isNumber(step.timeout) &&
!validTimeout(step.timeout)) {
throw new Error(timeoutErrorMessage);
}
return {
type,
assertedEvents: hasProperty(step, 'assertedEvents')
? parseAssertedEvents(step.assertedEvents)
: undefined,
timeout: hasProperty(step, 'timeout') && isNumber(step.timeout)
? step.timeout
: undefined,
};
}
function parseStepWithTarget(type, step) {
return {
...parseBaseStep(type, step),
target: parseTarget(step),
};
}
function parseStepWithFrame(type, step) {
return {
...parseStepWithTarget(type, step),
frame: parseFrame(step),
};
}
function parseStepWithSelectors(type, step) {
return {
...parseStepWithFrame(type, step),
selectors: parseSelectors(step),
};
}
function parseClickAttributes(step) {
const attributes = {
offsetX: parseNumber(step, 'offsetX'),
offsetY: parseNumber(step, 'offsetY'),
duration: parseOptionalNumber(step, 'duration'),
};
const deviceType = parseOptionalString(step, 'deviceType');
if (deviceType) {
if (!isKnownDeviceType(deviceType)) {
throw new Error(`'deviceType' for click steps must be one of the following: ${[
...pointerDeviceTypes,
].join(', ')}`);
}
attributes.deviceType = deviceType;
}
const button = parseOptionalString(step, 'button');
if (button) {
if (!isKnownMouseButton(button)) {
throw new Error(`'button' for click steps must be one of the following: ${[
...mouseButtonMap.keys(),
].join(', ')}`);
}
attributes.button = button;
}
return attributes;
}
function parseClickStep(step) {
return {
...parseStepWithSelectors(StepType.Click, step),
...parseClickAttributes(step),
type: StepType.Click,
};
}
function parseDoubleClickStep(step) {
return {
...parseStepWithSelectors(StepType.DoubleClick, step),
...parseClickAttributes(step),
type: StepType.DoubleClick,
};
}
function parseHoverStep(step) {
return {
...parseStepWithSelectors(StepType.Hover, step),
type: StepType.Hover,
};
}
function parseChangeStep(step) {
return {
...parseStepWithSelectors(StepType.Change, step),
type: StepType.Change,
value: parseString(step, 'value'),
};
}
function parseKeyDownStep(step) {
return {
...parseStepWithTarget(StepType.KeyDown, step),
type: StepType.KeyDown,
// TODO: type-check keys.
key: parseString(step, 'key'),
};
}
function parseKeyUpStep(step) {
return {
...parseStepWithTarget(StepType.KeyUp, step),
type: StepType.KeyUp,
// TODO: type-check keys.
key: parseString(step, 'key'),
};
}
function parseEmulateNetworkConditionsStep(step) {
return {
...parseStepWithTarget(StepType.EmulateNetworkConditions, step),
type: StepType.EmulateNetworkConditions,
download: parseNumber(step, 'download'),
upload: parseNumber(step, 'upload'),
latency: parseNumber(step, 'latency'),
};
}
function parseCloseStep(step) {
return {
...parseStepWithTarget(StepType.Close, step),
type: StepType.Close,
};
}
function parseSetViewportStep(step) {
return {
...parseStepWithTarget(StepType.SetViewport, step),
type: StepType.SetViewport,
width: parseNumber(step, 'width'),
height: parseNumber(step, 'height'),
deviceScaleFactor: parseNumber(step, 'deviceScaleFactor'),
isMobile: parseBoolean(step, 'isMobile'),
hasTouch: parseBoolean(step, 'hasTouch'),
isLandscape: parseBoolean(step, 'isLandscape'),
};
}
function parseScrollStep(step) {
return {
...parseStepWithFrame(StepType.Scroll, step),
type: StepType.Scroll,
x: parseOptionalNumber(step, 'x'),
y: parseOptionalNumber(step, 'y'),
selectors: parseOptionalSelectors(step),
};
}
function parseNavigateStep(step) {
return {
...parseStepWithTarget(StepType.Navigate, step),
type: StepType.Navigate,
target: parseTarget(step),
url: parseString(step, 'url'),
};
}
function parseWaitForElementStep(step) {
const operator = parseOptionalString(step, 'operator');
if (operator && operator !== '>=' && operator !== '==' && operator !== '<=') {
throw new Error("WaitForElement step's operator is not one of '>=','==','<='");
}
if (hasProperty(step, 'attributes')) {
if (!isObject(step.attributes) ||
Object.values(step.attributes).some((attribute) => typeof attribute !== 'string')) {
throw new Error("WaitForElement step's attribute is not a dictionary of strings");
}
}
if (hasProperty(step, 'properties')) {
if (!isObject(step.properties)) {
throw new Error("WaitForElement step's attribute is not an object");
}
}
return {
...parseStepWithSelectors(StepType.WaitForElement, step),
type: StepType.WaitForElement,
operator: operator,
count: parseOptionalNumber(step, 'count'),
visible: parseOptionalBoolean(step, 'visible'),
attributes: hasProperty(step, 'attributes')
? step.attributes
: undefined,
properties: hasProperty(step, 'properties')
? step.properties
: undefined,
};
}
function parseWaitForExpressionStep(step) {
if (!hasProperty(step, 'expression')) {
throw new Error('waitForExpression step is missing `expression`');
}
return {
...parseStepWithFrame(StepType.WaitForExpression, step),
type: StepType.WaitForExpression,
expression: parseString(step, 'expression'),
};
}
function parseCustomStep(step) {
if (!hasProperty(step, 'name')) {
throw new Error('customStep is missing name');
}
if (!isString(step.name)) {
throw new Error("customStep's name is not a string");
}
return {
...parseStepWithFrame(StepType.CustomStep, step),
type: StepType.CustomStep,
name: step.name,
parameters: hasProperty(step, 'parameters') ? step.parameters : undefined,
};
}
function parseStep(step, idx) {
if (!isObject(step)) {
throw new Error(idx ? `Step ${idx} is not an object` : 'Step is not an object');
}
if (!hasProperty(step, 'type')) {
throw new Error(idx ? `Step ${idx} does not have a type` : 'Step does not have a type');
}
if (!isString(step.type)) {
throw new Error(idx
? `Type of the step ${idx} is not a string`
: 'Type of the step is not a string');
}
switch (step.type) {
case StepType.Click:
return parseClickStep(step);
case StepType.DoubleClick:
return parseDoubleClickStep(step);
case StepType.Hover:
return parseHoverStep(step);
case StepType.Change:
return parseChangeStep(step);
case StepType.KeyDown:
return parseKeyDownStep(step);
case StepType.KeyUp:
return parseKeyUpStep(step);
case StepType.EmulateNetworkConditions:
return parseEmulateNetworkConditionsStep(step);
case StepType.Close:
return parseCloseStep(step);
case StepType.SetViewport:
return parseSetViewportStep(step);
case StepType.Scroll:
return parseScrollStep(step);
case StepType.Navigate:
return parseNavigateStep(step);
case StepType.CustomStep:
return parseCustomStep(step);
case StepType.WaitForElement:
return parseWaitForElementStep(step);
case StepType.WaitForExpression:
return parseWaitForExpressionStep(step);
default:
throw new Error(`Step type ${step.type} is not supported`);
}
}
function parseSteps(steps) {
const result = [];
if (!isArray(steps)) {
throw new Error('Recording `steps` is not an array');
}
for (const [idx, step] of steps.entries()) {
result.push(parseStep(step, idx));
}
return result;
}
function cleanUndefined(json) {
return JSON.parse(JSON.stringify(json));
}
const minTimeout = 1;
const maxTimeout = 30000;
const timeoutErrorMessage = `Timeout is not between ${minTimeout} and ${maxTimeout} milliseconds`;
function validTimeout(timeout) {
return timeout >= minTimeout && timeout <= maxTimeout;
}
function parse(data) {
if (!isObject(data)) {
throw new Error('Recording is not an object');
}
if (!hasProperty(data, 'title')) {
throw new Error('Recording is missing `title`');
}
if (!isString(data.title)) {
throw new Error('Recording `title` is not a string');
}
if (hasProperty(data, 'timeout') && !isNumber(data.timeout)) {
throw new Error('Recording `timeout` is not a number');
}
if (!hasProperty(data, 'steps')) {
throw new Error('Recording is missing `steps`');
}
if (hasProperty(data, 'timeout') &&
isNumber(data.timeout) &&
!validTimeout(data.timeout)) {
throw new Error(timeoutErrorMessage);
}
return cleanUndefined({
title: data.title,
timeout: hasProperty(data, 'timeout') && isNumber(data.timeout)
? data.timeout
: undefined,
selectorAttribute: hasProperty(data, 'selectorAttribute') && isString(data.selectorAttribute)
? data.selectorAttribute
: undefined,
steps: parseSteps(data.steps),
});
}
/**
* Detects what type of a selector the string contains. For example,
* `aria/Label` is a SelectorType.ARIA.
*
* Note that CSS selectors are special and usually don't require a prefix,
* therefore, SelectorType.CSS is the default type if other types didn't match.
*/
function getSelectorType(selector) {
for (const value of Object.values(SelectorType)) {
if (selector.startsWith(`${value}/`)) {
return value;
}
}
return SelectorType.CSS;
}
/**
* Converts a selector or an array of selector parts into a Puppeteer selector.
*
* @see https://pptr.dev/guides/query-selectors#p-elements
*/
function selectorToPElementSelector(selector) {
if (!Array.isArray(selector)) {
selector = [selector];
}
function escape(input) {
return input.replace(/['"()]/g, `\\$&`);
}
const result = selector.map((s) => {
switch (getSelectorType(s)) {
case SelectorType.ARIA:
return `::-p-aria(${escape(s.substring(SelectorType.ARIA.length + 1))})`;
case SelectorType.CSS:
return s;
case SelectorType.XPath:
return `::-p-xpath(${escape(s.substring(SelectorType.XPath.length + 1))})`;
case SelectorType.Pierce:
return `:scope >>> ${s.substring(SelectorType.Pierce.length + 1)}`;
case SelectorType.Text:
return `::-p-text(${escape(s.substring(SelectorType.Text.length + 1))})`;
}
});
return result.join(' >>>> ');
}
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
class StringifyExtension {
async beforeAllSteps(out, flow) { }
async afterAllSteps(out, flow) { }
async beforeEachStep(out, step, flow) { }
async stringifyStep(out, step, flow) { }
async afterEachStep(out, step, flow) { }
}
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Stringifies a user flow to JSON with source maps.
*
* You probably want to strip the source map because not all
* parsers support comments in JSON.
*/
class JSONStringifyExtension extends StringifyExtension {
async beforeAllSteps(out, flow) {
const copy = {
...flow,
steps: undefined,
};
// Stringify top-level attributes.
const text = JSON.stringify(copy, null, out.getIndent());
const lines = text.split('\n');
lines.pop();
lines[lines.length - 1] += ',';
lines.push(out.getIndent() + `"steps": [`);
out.appendLine(lines.join('\n')).startBlock().startBlock();
}
async afterAllSteps(out) {
out
.endBlock()
.endBlock()
.appendLine(out.getIndent() + `]`)
.appendLine('}');
}
async stringifyStep(out, step, flow) {
const stepText = JSON.stringify(step, null, out.getIndent());
if (!flow) {
out.appendLine(stepText);
return;
}
const separator = flow.steps.lastIndexOf(step) === flow.steps.length - 1 ? '' : ',';
out.appendLine(stepText + separator);
}
}
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
class InMemoryLineWriter {
#indentation;
#currentIndentation = 0;
#lines = [];
constructor(indentation) {
this.#indentation = indentation;
}
appendLine(line) {
const lines = line.split('\n').map((line) => {
const indentedLine = line
? this.#indentation.repeat(this.#currentIndentation) + line.trimEnd()
: '';
return indentedLine;
});
this.#lines.push(...lines);
return this;
}
startBlock() {
this.#currentIndentation++;
return this;
}
endBlock() {
this.#currentIndentation--;
if (this.#currentIndentation < 0) {
throw new Error('Extra endBlock');
}
return this;
}
toString() {
// Scripts should end with a final blank line.
return this.#lines.join('\n') + '\n';
}
getIndent() {
return this.#indentation;
}
getSize() {
return this.#lines.length;
}
}
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Copyright (c) 2020 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
function formatJSONAsJS(json, indent) {
const buffer = [];
format(json, buffer, 1, indent);
return buffer.join('');
}
function format(json, buffer = [], level = 1, indent = ' ') {
switch (typeof json) {
case 'bigint':
case 'symbol':
case 'function':
case 'undefined':
throw new Error('Invalid JSON');
case 'number':
case 'boolean':
buffer.push(String(json));
break;
case 'string':
buffer.push(formatAsJSLiteral(json));
break;
case 'object': {
if (json === null) {
buffer.push('null');
}
else if (Array.isArray(json)) {
buffer.push('[\n');
for (let i = 0; i < json.length; i++) {
buffer.push(indent.repeat(level));
format(json[i], buffer, level + 1, indent);
if (i !== json.length - 1) {
buffer.push(',');
}
buffer.push('\n');
}
buffer.push(indent.repeat(level - 1) + ']');
}
else {
buffer.push('{\n');
const keys = Object.keys(json);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = json[key];
if (value === undefined) {
continue;
}
buffer.push(indent.repeat(level));
buffer.push(key);
buffer.push(': ');
format(value, buffer, level + 1, indent);
if (i !== keys.length - 1) {
buffer.push(',');
}
buffer.push('\n');
}
buffer.push(indent.repeat(level - 1) + '}');
}
break;
}
default:
throw new Error('Unknown object type');
}
return buffer;
}
// Taken from https://source.chromium.org/chromium/chromium/src/+/main:third_party/devtools-frontend/src/front_end/core/platform/string-utilities.ts;l=29;drc=111134437ee51d74433829bed0088f7239e18867.
const toHexadecimal = (charCode, padToLength) => {
return charCode.toString(16).toUpperCase().padStart(padToLength, '0');
};
// Remember to update the third group in the regexps patternsToEscape and
// patternsToEscapePlusSingleQuote when adding new entries in this map.
const escapedReplacements = new Map([
['\b', '\\b'],
['\f', '\\f'],
['\n', '\\n'],
['\r', '\\r'],
['\t', '\\t'],
['\v', '\\v'],
["'", "\\'"],
['\\', '\\\\'],
['<!--', '\\x3C!--'],
['<script', '\\x3Cscript'],
['</script', '\\x3C/script'],
]);
const formatAsJSLiteral = (content) => {
const patternsToEscape = /(\\|<(?:!--|\/?script))|(\p{Control})|(\p{Surrogate})/gu;
const patternsToEscapePlusSingleQuote = /(\\|'|<(?:!--|\/?script))|(\p{Control})|(\p{Surrogate})/gu;
const escapePattern = (match, pattern, controlChar, loneSurrogate) => {
if (controlChar) {
if (escapedReplacements.has(controlChar)) {
// @ts-ignore https://github.com/microsoft/TypeScript/issues/13086
return escapedReplacements.get(controlChar);
}
const twoDigitHex = toHexadecimal(controlChar.charCodeAt(0), 2);
return '\\x' + twoDigitHex;
}
if (loneSurrogate) {
const fourDigitHex = toHexadecimal(loneSurrogate.charCodeAt(0), 4);
return '\\u' + fourDigitHex;
}
if (pattern) {
return escapedReplacements.get(pattern) || '';
}
return match;
};
let escapedContent = '';
let quote = '';
if (!content.includes("'")) {
quote = "'";
escapedContent = content.replace(patternsToEscape, escapePattern);
}
else if (!content.includes('"')) {
quote = '"';
escapedContent = content.replace(patternsToEscape, escapePattern);
}
else if (!content.includes('`') && !content.includes('${')) {
quote = '`';
escapedContent = content.replace(patternsToEscape, escapePattern);
}
else {
quote = "'";
escapedContent = content.replace(patternsToEscapePlusSingleQuote, escapePattern);
}
return `${quote}${escapedContent}${quote}`;
};
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
class PuppeteerStringifyExtension extends StringifyExtension {
#shouldAppendWaitForElementHelper = false;
#targetBrowser;
constructor(targetBrowser = 'chrome') {
super();
this.#targetBrowser = targetBrowser;
}
async beforeAllSteps(out, flow) {
out.appendLine("import { Locator, launch } from 'puppeteer'; // v25.0.0 or later");
out.appendLine('');
if (this.#targetBrowser === 'firefox') {
out.appendLine(`const browser = await launch({browser: 'firefox'});`);
}
else {
out.appendLine('const browser = await launch();');
}
out.appendLine('const page = await browser.newPage();');
out.appendLine(`const timeout = ${flow.timeout || defaultTimeout};`);
out.appendLine('page.setDefaultTimeout(timeout);');
out.appendLine('');
this.#shouldAppendWaitForElementHelper = false;
}
async afterAllSteps(out, flow) {
out.appendLine('');
out.appendLine('await browser.close();');
out.appendLine('');
if (this.#shouldAppendWaitForElementHelper) {
for (const line of waitForElementHelper.split('\n')) {
out.appendLine(line);
}
}
}
async stringifyStep(out, step, flow) {
out.appendLine('{').startBlock();
if (step.timeout !== undefined) {
out.appendLine(`const timeout = ${step.timeout};`);
}
this.#appendContext(out, step);
const waitForEvents = step.assertedEvents && step.type !== StepType.Navigate;
if (waitForEvents) {
out.appendLine('const promises = [];');
out.appendLine('const startWaitingForEvents = () => {').startBlock();
for (const event of step.assertedEvents) {
switch (event.type) {
case AssertedEventType.Navigation: {
out.appendLine(`promises.push(${'frame' in step && step.frame ? 'frame' : 'targetPage'}.waitForNavigation());`);
break;
}
default:
throw new Error(`Event type ${event.type} is not supported`);
}
}
out.endBlock().appendLine('}');
}
this.#appendStepType(out, step);
if (waitForEvents) {
out.appendLine('await Promise.all(promises);');
}
out.endBlock().appendLine('}');
}
#appendTarget(out, target) {
if (target === 'main') {
out.appendLine('const targetPage = page;');
}
else {
out.appendLine(`const target = await browser.waitForTarget(t => t.url() === ${formatJSONAsJS(target, out.getIndent())}, { timeout });`);
out.appendLine('const targetPage = await target.page();');
out.appendLine('targetPage.setDefaultTimeout(timeout);');
}
}
#appendFrame(out, path) {
out.appendLine('let frame = targetPage.mainFrame();');
for (const index of path) {
out.appendLine(`frame = frame.childFrames()[${index}];`);
}
}
#appendContext(out, step) {
// TODO fix optional target: should it be main?
this.#appendTarget(out, step.target || 'main');
// TODO fix optional frame: should it be required?
if (step.frame) {
this.#appendFrame(out, step.frame);
}
}
#appendLocators(out, step, action) {
out.appendLine('await Locator.race([').startBlock();
out.appendLine(step.selectors
.map((s) => {
return `${step.frame ? 'frame' : 'targetPage'}.locator(${formatJSONAsJS(selectorToPElementSelector(s), out.getIndent())})`;
})
.join(',\n'));
out.endBlock().appendLine('])');
out.startBlock().appendLine('.setTimeout(timeout)');
if (step.assertedEvents?.length) {
out.appendLine(`.on('action', () => startWaitingForEvents())`);
}
action();
out.endBlock();
}
#appendClickStep(out, step) {
this.#appendLocators(out, step, () => {
out.appendLine('.click({');
if (step.duration) {
out.appendLine(` delay: ${step.duration},`);
}
if (step.button) {
out.appendLine(` button: '${mouseButtonMap.get(step.button)}',`);
}
out.appendLine(' offset: {');
out.appendLine(` x: ${step.offsetX},`);
out.appendLine(` y: ${step.offsetY},`);
out.appendLine(' },');
out.appendLine('});');
});
}
#appendDoubleClickStep(out, step) {
this.#appendLocators(out, step, () => {
out.appendLine('.click({');
out.appendLine(` count: 2,`);
if (step.duration) {
out.appendLine(` delay: ${step.duration},`);
}
if (step.button) {
out.appendLine(` button: '${mouseButtonMap.get(step.button)}',`);
}
out.appendLine(' offset: {');
out.appendLine(` x: ${step.offsetX},`);
out.appendLine(` y: ${step.offsetY},`);
out.appendLine(' },');
out.appendLine('});');
});
}
#appendHoverStep(out, step) {
this.#appendLocators(out, step, () => {
out.appendLine('.hover();');
});
}
#appendChangeStep(out, step) {
this.#appendLocators(out, step, () => {
out.appendLine(`.fill(${formatJSONAsJS(step.value, out.getIndent())});`);
});
}
#appendEmulateNetworkConditionsStep(out, step) {
out.appendLine('await targetPage.emulateNetworkConditions({');
out.appendLine(` offline: ${!step.download && !step.upload},`);
out.appendLine(` downloadThroughput: ${step.download},`);
out.appendLine(` uploadThroughput: ${step.upload},`);
out.appendLine(` latency: ${step.latency},`);
out.appendLine('});');
}
#appendKeyDownStep(out, step) {
out.appendLine(`await targetPage.keyboard.down(${formatJSONAsJS(step.key, out.getIndent())});`);
}
#appendKeyUpStep(out, step) {
out.appendLine(`await targetPage.keyboard.up(${formatJSONAsJS(step.key, out.getIndent())});`);
}
#appendCloseStep(out, step) {
out.appendLine('await targetPage.close()');
}
#appendViewportStep(out, step) {
out.appendLine(`await targetPage.setViewport(${formatJSONAsJS({
width: step.width,
height: step.height,
}, out.getIndent())})`);
}
#appendScrollStep(out, step) {
if ('selectors' in step) {
this.#appendLocators(out, step, () => {
out.appendLine(`.scroll({ scrollTop: ${step.y}, scrollLeft: ${step.x}});`);
});
}
else {
out.appendLine(`await targetPage.evaluate((x, y) => { window.scroll(x, y); }, ${step.x}, ${step.y})`);
}
}
#appendStepType(out, step) {
switch (step.type) {
case StepType.Click:
return this.#appendClickStep(out, step);
case StepType.DoubleClick:
return this.#appendDoubleClickStep(out, step);
case StepType.Hover:
return this.#appendHoverStep(out, step);
case StepType.Change:
return this.#appendChangeStep(out, step);
case StepType.EmulateNetworkConditions:
return this.#appendEmulateNetworkConditionsStep(out, step);
case StepType.KeyDown:
return this.#appendKeyDownStep(out, step);
case StepType.KeyUp:
return this.#appendKeyUpStep(out, step);
case StepType.Close:
return this.#appendCloseStep(out, step);
case StepType.SetViewport:
return this.#appendViewportStep(out, step);
case StepType.Scroll:
return this.#appendScrollStep(out, step);
case StepType.Navigate:
return this.#appendNavigationStep(out, step);
case StepType.WaitForElement:
return this.#appendWaitForElementStep(out, step);
case StepType.WaitForExpression:
return this.#appendWaitExpressionStep(out, step);
case StepType.CustomStep:
return; // TODO: implement these
default:
return assertAllStepTypesAreHandled(step);
}
}
#appendNavigationStep(out, step) {
out.appendLine(`await targetPage.goto(${formatJSONAsJS(step.url, out.getIndent())});`);
}
#appendWaitExpressionStep(out, step) {
out.appendLine(`await ${step.frame ? 'frame' : 'targetPage'}.waitForFunction(${formatJSONAsJS(step.expression, out.getIndent())}, { timeout });`);
}
#appendWaitForElementStep(out, step) {
this.#shouldAppendWaitForElementHelper = true;
out.appendLine(`await waitForElement(${formatJSONAsJS(step, out.getIndent())}, ${step.frame ? 'frame' : 'targetPage'}, timeout);`);
}
}
const defaultTimeout = 5000;
const waitForElementHelper = `async function waitForElement(step, frame, timeout) {
const {
count = 1,
operator = '>=',
visible = true,
properties,
attributes,
} = step;
const compFn = {
'==': (a, b) => a === b,
'>=': (a, b) => a >= b,
'<=': (a, b) => a <= b,
}[operator];
await waitForFunction(async () => {
const elements = await querySelectorsAll(step.selectors, frame);
let result = compFn(elements.length, count);
const elementsHandle = await frame.evaluateHandle((...elements) => {
return elements;
}, ...elements);
await Promise.all(elements.map((element) => element.dispose()));
if (result && (properties || attributes)) {
result = await elementsHandle.evaluate(
(elements, properties, attributes) => {
for (const element of elements) {
if (attributes) {
for (const [name, value] of Object.entries(attributes)) {
if (element.getAttribute(name) !== value) {
return false;
}
}
}
if (properties) {
if (!isDeepMatch(properties, element)) {
return false;
}
}
}
return true;
function isDeepMatch(a, b) {
if (a === b) {
return true;
}
if ((a && !b) || (!a && b)) {
return false;
}
if (!(a instanceof Object) || !(b instanceof Object)) {
return false;
}
for (const [key, value] of Object.entries(a)) {
if (!isDeepMatch(value, b[key])) {
return false;
}
}
return true;
}
},
properties,
attributes
);
}
await elementsHandle.dispose();
return result === visible;
}, timeout);
}
async function querySelectorsAll(selectors, frame) {
for (const selector of selectors) {
const result = await querySelectorAll(selector, frame);
if (result.length) {
return result;
}
}
return [];
}
async function querySelectorAll(selector, frame) {
if (!Array.isArray(selector)) {
selector = [selector];
}
if (!selector.length) {
throw new Error('Empty selector provided to querySelectorAll');
}
let elements = [];
for (let i = 0; i < selector.length; i++) {
const part = selector[i];
if (i === 0) {
elements = await frame.$$(part);
} else {
const tmpElements = elements;
elements = [];
for (const el of tmpElements) {
elements.push(...(await el.$$(part)));
}
}
if (elements.length === 0) {
return [];
}
if (i < selector.length - 1) {
const tmpElements = [];
for (const el of elements) {
const newEl = (await el.evaluateHandle(el => el.shadowRoot ? el.shadowRoot : el)).asElement();
if (newEl) {
tmpElements.push(newEl);
}
}
elements = tmpElements;
}
}
return elements;
}
async function waitForFunction(fn, timeout) {
let isActive = true;
const timeoutId = setTimeout(() => {
isActive = false;
}, timeout);
while (isActive) {
const result = await fn();
if (result) {
clearTimeout(timeoutId);
return;
}
await new Promise(resolve => setTimeout(resolve, 100));
}
throw new Error('Timed out');
}`;
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
const alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const charToIdx = alpha.split('').reduce((acc, char, idx) => {
acc.set(char, idx);
return acc;
}, new Map());
const LEAST_5_BIT_MASK = 0b011111;
const CONTINUATION_BIT_MASK = 0b100000;
const MAX_INT = 2147483647;
/**
* Encoding variable length integer into base64 (6-bit):
*
* 1 N N N N N | 0 N N N N N
*
* The first bit indicates if there is more data for the int.
*/
function encodeInt(num) {
if (num < 0) {
throw new Error('Only postive integers and zero are supported');
}
if (num > MAX_INT) {
throw new Error('Only integers between 0 and ' + MAX_INT + ' are supported');
}
const result = [];
do {
let payload = num & LEAST_5_BIT_MASK;
num >>>= 5;
if (num > 0)
payload |= CONTINUATION_BIT_MASK;
result.push(alpha[payload]);
} while (num !== 0);
return result.join('');
}
function encode(nums) {
const parts = [];
for (const num of nums) {
parts.push(encodeInt(num));
}
return parts.join('');
}
function decode(str) {
const results = [];
const chrs = str.split('');
let result = 0;
let shift = 0;
for (const ch of chrs) {
const num = charToIdx.get(ch);
result |= (num & LEAST_5_BIT_MASK) << shift;
shift += 5;
const hasMore = num & CONTINUATION_BIT_MASK;
if (!hasMore) {
results.push(result);
result = 0;
shift = 0;
}
}
return results;
}
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
const SOURCE_MAP_PREFIX = '//# recorderSourceMap=';
/**
* Stringifes an entire recording. The following hooks are invoked with the `flow` parameter containing the entire flow:
* - `beforeAllSteps` (once)
* - `beforeEachStep` (per step)
* - `stringifyStep` (per step)
* - `afterEachStep` (per step)
* - `afterAllSteps` (once)
*/
async function stringify(flow, opts) {
if (!opts) {
opts = {};
}
const ext = opts.extension ?? new PuppeteerStringifyExtension();
const out = opts.writer ?? new InMemoryLineWriter(opts.indentation ?? ' ');
await ext.beforeAllSteps?.(out, flow);
const sourceMap = [1]; // The first int indicates the version.
for (const step of flow.steps) {
const firstLine = out.getSize();
await ext.beforeEachStep?.(out, step, flow);
await ext.stringifyStep(out, step, flow);
await ext.afterEachStep?.(out, step, flow);
const lastLine = out.getSize();
sourceMap.push(...[firstLine, lastLine - firstLine]);
}
await ext.afterAllSteps?.(out, flow);
out.appendLine(SOURCE_MAP_PREFIX + encode(sourceMap));
return out.toString();
}
/**
* Stringifes a single step. Only the following hooks are invoked with the `flow` parameter as undefined:
* - `beforeEachStep`
* - `stringifyStep`
* - `afterEachStep`
*/
async function stringifyStep(step, opts) {
if (!opts) {
opts = {};
}
let ext = opts.extension;
if (!ext) {
ext = new PuppeteerStringifyExtension();
}
if (!opts.indentation) {
opts.indentation = ' ';
}
const out = opts.writer ?? new InMemoryLineWriter(opts.indentation ?? ' ');
await ext.beforeEachStep?.(out, step);
await ext.stringifyStep(out, step);
await ext.afterEachStep?.(out, step);
return out.toString();
}
function isSourceMapLine(line) {
return line.trim().startsWith(SOURCE_MAP_PREFIX);
}
/**
* Extracts a source map from a text.
*/
function parseSourceMap(text) {
const lines = text.split('\n');
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i];
if (isSourceMapLine(line)) {
return decode(line.trim().substring(SOURCE_MAP_PREFIX.length));
}
}
return;
}
function stripSourceMap(text) {
const lines = text.split('\n');
return lines.filter((line) => !isSourceMapLine(line)).join('\n');
}
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
class RunnerExtension {
async beforeAllSteps(flow) { }
async afterAllSteps(flow) { }
async beforeEachStep(step, flow) { }
async runStep(step, flow) { }
async afterEachStep(step, flow) { }
}
const comparators = {
'==': (a, b) => a === b,
'>=': (a, b) => a >= b,
'<=': (a, b) => a <= b,
};
function waitForTimeout(timeout) {
return new Promise((resolve) => {
setTimeout(resolve, timeout);
});
}
class PuppeteerRunnerExtension extends RunnerExtension {
browser;
page;
timeout;
constructor(browser, page, opts) {
super();
this.browser = browser;
this.page = page;
this.timeout = opts?.timeout || 5000;
}
async #ensureAutomationEmulatation(pageOrFrame) {
try {
await pageOrFrame
._client()
.send('Emulation.setAutomationOverride', { enabled: true });
}
catch {
// ignore errors as not all versions support this command.
}
}
#getTimeoutForStep(step, flow) {
return step.timeout || flow?.timeout || this.timeout;
}
async runStep(step, flow) {
const timeout = this.#getTimeoutForStep(step, flow);
const page = this.page;
const browser = this.browser;
const targetPage = await getTargetPageForStep(browser, page, step, timeout);
let targetFrame = null;
if (!targetPage && step.target) {
targetFrame = await page.waitForFrame(step.target, { timeout });
}
const targetPageOrFrame = targetFrame || targetPage;
if (!targetPageOrFrame) {
throw new Error('Target is not found for step: ' + JSON.stringify(step));
}
await this.#ensureAutomationEmulatation(targetPageOrFrame);
const localFrame = await getFrame(targetPageOrFrame, step);
await this.runStepInFrame(step, page, targetPageOrFrame, localFrame, timeout);
}
/**
* @internal
*/
async runStepInFrame(step, mainPage, targetPageOrFrame, localFrame, timeout) {
let assertedEventsPromise = null;
const startWaitingForEvents = () => {
assertedEventsPromise = waitForEvents(localFrame, step, timeout);
};
const locatorRace = this.page.locatorRace;
switch (step.type) {
case StepType.DoubleClick:
await locatorRace(step.selectors.map((selector) => {
return localFrame.locator(selectorToPElementSelector(selector));
}))
.setTimeout(timeout)
.on('action', () => startWaitingForEvents())
.click({
count: 2,
button: step.button && mouseButtonMap.get(step.button),
delay: step.duration,
offset: {
x: step.offsetX,
y: step.offsetY,
},
});
break;
case StepType.Click:
await locatorRace(step.selectors.map((selector) => {
return localFrame.locator(selectorToPElementSelector(selector));
}))
.setTimeout(timeout)
.on('action', () => startWaitingForEvents())
.click({
delay: step.duration,
button: step.button && mouseButtonMap.get(step.button),
offset: {
x: step.offsetX,
y: step.offsetY,
},
});
break;
case StepType.Hover:
await locatorRace(step.selectors.map((selector) => {
return localFrame.locator(selectorToPElementSelector(selector));
}))
.setTimeout(timeout)
.on('action', () => startWaitingForEvents())
.hover();
break;
case StepType.EmulateNetworkConditions:
{
startWaitingForEvents();
const { download, upload, latency } = step;
await mainPage.emulateNetworkConditions({
offline: !download && !upload,
download,
upload,
latency,
});
}
break;
case StepType.KeyDown:
{
startWaitingForEvents();
await mainPage.keyboard.down(step.key);
await waitForTimeout(100);
}
break;
case StepType.KeyUp:
{
startWaitingForEvents();
await mainPage.keyboard.up(step.key);
await waitForTimeout(100);
}
break;
case StepType.Close:
{
if ('close' in targetPageOrFrame) {
startWaitingForEvents();
await targetPageOrFrame.close();
}
}
break;
case StepType.Change:
await locatorRace(step.selectors.map((selector) => {
return localFrame.locator(selectorToPElementSelector(selector));
}))
.on('action', () => startWaitingForEvents())
.setTimeout(timeout)
.fill(step.value);
break;
case StepType.SetViewport: {
if ('setViewport' in targetPageOrFrame) {
startWaitingForEvents();
await targetPageOrFrame.setViewport(step);
}
break;
}
case StepType.Scroll: {
if ('selectors' in step) {
await locatorRace(step.selectors.map((selector) => {
return localFrame.locator(selectorToPElementSelector(selector));
}))
.on('action', () => startWaitingForEvents())
.setTimeout(timeout)
.scroll({
scrollLeft: step.x || 0,
scrollTop: step.y || 0,
});
}
else {
startWaitingForEvents();
await localFrame.evaluate((x, y) => {
/* c8 ignore start */
window.scroll(x, y);
/* c8 ignore stop */
}, step.x || 0, step.y || 0);
}
break;
}
case StepType.Navigate: {
startWaitingForEvents();
await localFrame.goto(step.url);
break;
}
case StepType.WaitForElement: {
try {
startWaitingForEvents();
await waitForElement(step, localFrame, timeout);
}
catch (err) {
if (err.message === 'Timed out') {
throw new Error('waitForElement timed out. The element(s) could not be found.');
}
else {
throw err;
}
}
break;
}
case StepType.WaitForExpression: {
startWaitingForEvents();
await localFrame.waitForFunction(step.expression, {
timeout,
});
break;
}
case StepType.CustomStep: {
// TODO implement these steps
break;
}
default:
assertAllStepTypesAreHandled(step);
}
await assertedEventsPromise;
}
}
class PuppeteerRunnerOwningBrowserExtension extends PuppeteerRunnerExtension {
asyn