blob: e2a66f02a1334c63e5d71c5a9bbc816fb76f795f [file] [log] [blame]
// Copyright 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.
import * as os from 'os';
import {performance} from 'perf_hooks';
import * as puppeteer from 'puppeteer';
import {reloadDevTools} from '../conductor/hooks.js';
import {getBrowserAndPages} from '../conductor/puppeteer-state.js';
export let platform: string;
switch (os.platform()) {
case 'darwin':
platform = 'mac';
break;
case 'win32':
platform = 'win32';
break;
default:
platform = 'linux';
break;
}
// TODO: Remove once Chromium updates its version of Node.js to 12+.
const globalThis: any = global;
/**
* Because querySelector is unable to go through shadow roots, we take the opportunity
* to collect all elements from everywhere in the page, optionally starting at a given
* root node. This means that when we attempt to locate elements for the purposes of
* interactions, we can use this flattened list rather than attempting querySelector
* dances.
*/
const collectAllElementsFromPage = async (root?: puppeteer.JSHandle) => {
const {frontend} = getBrowserAndPages();
await frontend.evaluate(root => {
const container = (self as any);
container.__elements = [];
const collect = (root: HTMLElement|ShadowRoot) => {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
do {
const currentNode = walker.currentNode as HTMLElement;
if (currentNode.shadowRoot) {
collect(currentNode.shadowRoot);
}
// We're only interested in actual elements that we can later use for selector
// matching, so skip shadow roots.
if (!(currentNode instanceof ShadowRoot)) {
container.__elements.push(currentNode);
}
} while (walker.nextNode());
};
collect(root || document.documentElement);
}, root || '');
};
export const getElementPosition = async (selector: string|puppeteer.JSHandle, root?: puppeteer.JSHandle) => {
let element: puppeteer.JSHandle;
if (typeof selector === 'string') {
element = await $(selector, root);
} else {
element = selector;
}
const position = await element.evaluate(element => {
if (!element) {
return {};
}
// Extract the location values.
const {left, top, width, height} = element.getBoundingClientRect();
return {
x: left + width * 0.5,
y: top + height * 0.5,
};
});
if (position.x === undefined || position.y === undefined) {
throw new Error(`Unable to find element with selector "${selector}"`);
}
return position;
};
export const click = async (
selector: string|puppeteer.JSHandle,
options?: {root?: puppeteer.JSHandle, clickOptions?: puppeteer.ClickOptions}) => {
const {frontend} = getBrowserAndPages();
if (!frontend) {
throw new Error('Unable to locate DevTools frontend page. Was it stored first?');
}
const clickableElement = await getElementPosition(selector, options && options.root);
if (!clickableElement) {
throw new Error(`Unable to locate clickable element "${selector}".`);
}
// Click on the button and wait for the console to load. The reason we use this method
// rather than elementHandle.click() is because the frontend attaches the behavior to
// a 'mousedown' event (not the 'click' event). To avoid attaching the test behavior
// to a specific event we instead locate the button in question and ask Puppeteer to
// click on it instead.
await frontend.mouse.click(clickableElement.x, clickableElement.y, options && options.clickOptions);
};
export const doubleClick =
async (selector: string, options?: {root?: puppeteer.JSHandle, clickOptions?: puppeteer.ClickOptions}) => {
const passedClickOptions = options && options.clickOptions || {};
const clickOptionsWithDoubleClick: puppeteer.ClickOptions = {
...passedClickOptions,
clickCount: 2,
};
return click(selector, {
...options,
clickOptions: clickOptionsWithDoubleClick,
});
};
export const typeText = async (text: string) => {
const {frontend} = getBrowserAndPages();
if (!frontend) {
throw new Error('Unable to locate DevTools frontend page. Was it stored first?');
}
await frontend.keyboard.type(text);
};
// Get a single element handle, across Shadow DOM boundaries.
export const $ = async (selector: string, root?: puppeteer.JSHandle) => {
const {frontend} = getBrowserAndPages();
if (!frontend) {
throw new Error('Unable to locate DevTools frontend page. Was it stored first?');
}
await collectAllElementsFromPage(root);
try {
const element = await frontend.evaluateHandle(selector => {
const elements: Element[] = globalThis.__elements;
return elements.find(element => element.matches(selector));
}, selector);
return element;
} catch (error) {
throw new Error(`Unable to find element for selector "${selector}": ${error.stack}`);
}
};
// Get multiple element handles, across Shadow DOM boundaries.
export const $$ = async (selector: string, root?: puppeteer.JSHandle) => {
const {frontend} = getBrowserAndPages();
if (!frontend) {
throw new Error('Unable to locate DevTools frontend page. Was it stored first?');
}
await collectAllElementsFromPage(root);
const elements = await frontend.evaluateHandle(selector => {
const elements: Element[] = globalThis.__elements;
return elements.filter(element => element.matches(selector));
}, selector);
return elements;
};
/**
* Search for an element based on its textContent.
*
* @param textContent The text content to search for.
* @param root The root of the search.
*/
export const $textContent = async (textContent: string, root?: puppeteer.JSHandle) => {
const {frontend} = getBrowserAndPages();
if (!frontend) {
throw new Error('Unable to locate DevTools frontend page. Was it stored first?');
}
await collectAllElementsFromPage(root);
try {
const element = await frontend.evaluateHandle((textContent: string) => {
const elements: Element[] = globalThis.__elements;
return elements.find(element => ('textContent' in element && element.textContent === textContent));
}, textContent);
return element;
} catch (error) {
throw new Error(`Unable to find element with textContent "${textContent}": ${error.stack}`);
}
};
export const timeout = (duration: number) => new Promise(resolve => setTimeout(resolve, duration));
export const waitFor = async (selector: string, root?: puppeteer.JSHandle, maxTotalTimeout = 0) => {
return waitForFunction(async () => {
const element = await $(selector, root);
if (element.asElement()) {
return element;
}
return undefined;
}, `Unable to find element with selector ${selector}`, maxTotalTimeout);
};
export const waitForNone = async (selector: string, root?: puppeteer.JSHandle, maxTotalTimeout = 0) => {
return waitForFunction(async () => {
const elements = await $$(selector, root);
if (elements.evaluate(list => list.length === 0)) {
return true;
}
return false;
}, `At least one element with selector ${selector} still exists`, maxTotalTimeout);
};
export const waitForElementWithTextContent = (textContent: string, root?: puppeteer.JSHandle, maxTotalTimeout = 0) => {
return waitForFunction(async () => {
const element = await $textContent(textContent, root);
if (element.asElement()) {
return element;
}
return undefined;
}, `No element with content ${textContent} exists`, maxTotalTimeout);
};
export const waitForFunction =
async<T>(fn: () => Promise<T|undefined>, errorMessage: string, maxTotalTimeout = 0): Promise<T> => {
if (maxTotalTimeout === 0) {
maxTotalTimeout = Number.POSITIVE_INFINITY;
}
const start = performance.now();
do {
await timeout(100);
const result = await fn();
if (result) {
return result;
}
} while (performance.now() - start < maxTotalTimeout);
throw new Error(errorMessage);
};
export const debuggerStatement = (frontend: puppeteer.Page) => {
return frontend.evaluate(() => {
// eslint-disable-next-line no-debugger
debugger;
});
};
export const logToStdOut = (msg: string) => {
if (!process.send) {
return;
}
process.send({
pid: process.pid,
details: msg,
});
};
export const logFailure = () => {
if (!process.send) {
return;
}
process.send({
pid: process.pid,
details: 'failure',
});
};
export const resourcesPath = 'http://localhost:8090/test/e2e/resources';
export const enableExperiment = async (experiment: string) => {
const {frontend} = getBrowserAndPages();
await frontend.evaluate(experiment => {
// @ts-ignore
globalThis.Root.Runtime.experiments.setEnabled(experiment, true);
}, experiment);
await reloadDevTools();
};
export {getBrowserAndPages, reloadDevTools};