blob: 5abadf0af1f1af15a3074bcb9d381f201800f9c0 [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 {assert, AssertionError} from 'chai';
import * as os from 'os';
import type * as puppeteer from 'puppeteer-core';
import {type DevToolsFrontendReloadOptions} from '../conductor/frontend_tab.js';
import {getDevToolsFrontendHostname, reloadDevTools} from '../conductor/hooks.js';
import {getBrowserAndPages, getTestServerPort} from '../conductor/puppeteer-state.js';
import {AsyncScope} from './async-scope.js';
declare global {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Window {
// eslint-disable-next-line @typescript-eslint/naming-convention
__pendingEvents: Map<string, Event[]>;
// eslint-disable-next-line @typescript-eslint/naming-convention
__eventHandlers: WeakMap<Element, Map<string, Promise<void>>>;
// eslint-disable-next-line @typescript-eslint/naming-convention
__getRenderCoordinatorPendingFrames(): number;
}
}
export type Platform = 'mac'|'win32'|'linux';
export let platform: Platform;
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+.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const globalThis: any = global;
export interface ClickOptions {
root?: puppeteer.JSHandle;
clickOptions?: puppeteer.ClickOptions;
maxPixelsFromLeft?: number;
}
const CONTROL_OR_META = platform === 'mac' ? 'Meta' : 'Control';
export const withControlOrMetaKey = async (action: () => Promise<void>, root = getBrowserAndPages().frontend) => {
await waitForFunction(async () => {
await root.keyboard.down(CONTROL_OR_META);
try {
await action();
return true;
} finally {
await root.keyboard.up(CONTROL_OR_META);
}
});
};
export const click = async (selector: string, options?: ClickOptions) => {
return await performActionOnSelector(
selector, {root: options?.root}, element => element.click(options?.clickOptions));
};
export const hover = async (selector: string, options?: {root?: puppeteer.JSHandle}) => {
return await performActionOnSelector(selector, {root: options?.root}, element => element.hover());
};
type Action = (element: puppeteer.ElementHandle) => Promise<void>;
async function performActionOnSelector(
selector: string, options: {root?: puppeteer.JSHandle}, action: Action): Promise<puppeteer.ElementHandle> {
// TODO(crbug.com/1410168): we should refactor waitFor to be compatible with
// Puppeteer's syntax for selectors.
const queryHandlers = new Set([
'pierceShadowText',
'pierce',
'aria',
'xpath',
'text',
]);
let queryHandler = 'pierce';
for (const handler of queryHandlers) {
const prefix = handler + '/';
if (selector.startsWith(prefix)) {
queryHandler = handler;
selector = selector.substring(prefix.length);
break;
}
}
return waitForFunction(async () => {
const element = await waitFor(selector, options?.root, undefined, queryHandler);
try {
await action(element);
return element;
} catch (err) {
// A bit of delay to not retry too often.
await new Promise(resolve => setTimeout(resolve, 50));
}
return undefined;
});
}
/**
* @deprecated This method is not able to recover from unstable DOM. Use click(selector) instead.
*/
export async function clickElement(element: puppeteer.ElementHandle, options?: ClickOptions): Promise<void> {
// Retries here just in case the element gets connected to DOM / becomes visible.
await waitForFunction(async () => {
try {
await element.click(options?.clickOptions);
return true;
} catch {
return false;
}
});
}
/**
* @deprecated This method is not able to recover from unstable DOM. Use hover(selector) instead.
*/
export async function hoverElement(element: puppeteer.ElementHandle): Promise<void> {
// Retries here just in case the element gets connected to DOM / becomes visible.
await waitForFunction(async () => {
try {
await element.hover();
return true;
} catch {
return false;
}
});
}
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();
await frontend.keyboard.type(text);
};
export const pressKey =
async (key: puppeteer.KeyInput, modifiers?: {control?: boolean, alt?: boolean, shift?: boolean}) => {
const {frontend} = getBrowserAndPages();
if (modifiers) {
if (modifiers.control) {
if (platform === 'mac') {
// Use command key on mac
await frontend.keyboard.down('Meta');
} else {
await frontend.keyboard.down('Control');
}
}
if (modifiers.alt) {
await frontend.keyboard.down('Alt');
}
if (modifiers.shift) {
await frontend.keyboard.down('Shift');
}
}
await frontend.keyboard.press(key);
if (modifiers) {
if (modifiers.shift) {
await frontend.keyboard.up('Shift');
}
if (modifiers.alt) {
await frontend.keyboard.up('Alt');
}
if (modifiers.control) {
if (platform === 'mac') {
// Use command key on mac
await frontend.keyboard.up('Meta');
} else {
await frontend.keyboard.up('Control');
}
}
}
};
export const pasteText = async (text: string) => {
const {frontend} = getBrowserAndPages();
await frontend.keyboard.sendCharacter(text);
};
// Get a single element handle. Uses `pierce` handler per default for piercing Shadow DOM.
export const $ =
async<ElementType extends Element = Element>(selector: string, root?: puppeteer.JSHandle, handler = 'pierce') => {
const {frontend} = getBrowserAndPages();
const rootElement = root ? root as puppeteer.ElementHandle : frontend;
const element = await rootElement.$(`${handler}/${selector}`) as puppeteer.ElementHandle<ElementType>;
return element;
};
// Get multiple element handles. Uses `pierce` handler per default for piercing Shadow DOM.
export const $$ =
async<ElementType extends Element = Element>(selector: string, root?: puppeteer.JSHandle, handler = 'pierce') => {
const {frontend} = getBrowserAndPages();
const rootElement = root ? root.asElement() || frontend : frontend;
const elements = await rootElement.$$(`${handler}/${selector}`) as puppeteer.ElementHandle<ElementType>[];
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) => {
return $(textContent, root, 'pierceShadowText');
};
/**
* Search for all elements based on their 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) => {
return $$(textContent, root, 'pierceShadowText');
};
export const timeout = (duration: number) => new Promise(resolve => setTimeout(resolve, duration));
export const getTextContent =
async<ElementType extends Element = Element>(selector: string, root?: puppeteer.JSHandle) => {
const text = await (await $<ElementType>(selector, root))?.evaluate(node => node.textContent);
return text ?? undefined;
};
export const getAllTextContents =
async(selector: string, root?: puppeteer.JSHandle, handler = 'pierce'): Promise<Array<string|null>> => {
const allElements = await $$(selector, root, handler);
return Promise.all(allElements.map(e => e.evaluate(e => e.textContent)));
};
/**
* Match multiple elements based on a selector and return their textContents, but only for those
* elements that are visible.
*
* @param selector jquery selector to match
* @returns array containing text contents from visible elements
*/
export const getVisibleTextContents = async (selector: string) => {
const allElements = await $$(selector);
const texts = await Promise.all(
allElements.map(el => el.evaluate(node => node.checkVisibility() ? node.textContent?.trim() : undefined)));
return texts.filter(content => typeof (content) === 'string');
};
export const waitFor = async<ElementType extends Element = Element>(
selector: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope(), handler?: string) => {
return await asyncScope.exec(() => waitForFunction(async () => {
const element = await $<ElementType>(selector, root, handler);
return (element || undefined);
}, asyncScope), `Waiting for element matching selector '${selector}'`);
};
export const waitForVisible = async<ElementType extends Element = Element>(
selector: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope(), handler?: string) => {
return await asyncScope.exec(() => waitForFunction(async () => {
const element = await $<ElementType>(selector, root, handler);
const visible = await element.evaluate(node => node.checkVisibility());
return visible ? element : undefined;
}, asyncScope), `Waiting for element matching selector '${selector}' to be visible`);
};
export const waitForMany = async (
selector: string, count: number, root?: puppeteer.JSHandle, asyncScope = new AsyncScope(), handler?: string) => {
return await asyncScope.exec(() => waitForFunction(async () => {
const elements = await $$(selector, root, handler);
return elements.length >= count ? elements : undefined;
}, asyncScope), `Waiting for ${count} elements to match selector '${selector}'`);
};
export const waitForNone =
async (selector: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope(), handler?: string) => {
return await asyncScope.exec(() => waitForFunction(async () => {
const elements = await $$(selector, root, handler);
if (elements.length === 0) {
return true;
}
return false;
}, asyncScope), `Waiting for no elements to match selector '${selector}'`);
};
export const waitForAria = (selector: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope()) => {
return waitFor(selector, root, asyncScope, 'aria');
};
export const waitForAriaNone = (selector: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope()) => {
return waitForNone(selector, root, asyncScope, 'aria');
};
export const waitForElementWithTextContent =
(textContent: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope()) => {
return waitFor(textContent, root, asyncScope, 'pierceShadowText');
};
export const waitForElementsWithTextContent =
(textContent: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope()) => {
return asyncScope.exec(() => waitForFunction(async () => {
const elems = await $$textContent(textContent, root);
if (elems && elems.length) {
return elems;
}
return undefined;
}, asyncScope), `Waiting for elements with textContent '${textContent}'`);
};
export const waitForNoElementsWithTextContent =
(textContent: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope()) => {
return asyncScope.exec(() => waitForFunction(async () => {
const elems = await $$textContent(textContent, root);
if (elems && elems.length === 0) {
return true;
}
return false;
}, asyncScope), `Waiting for no elements with textContent '${textContent}'`);
};
export const TIMEOUT_ERROR_MESSAGE = 'Test timed out';
export const waitForFunction =
async<T>(fn: () => Promise<T|undefined>, asyncScope = new AsyncScope(), description?: string) => {
const innerFunction = async () => {
while (true) {
if (asyncScope.isCanceled()) {
throw new Error(TIMEOUT_ERROR_MESSAGE);
}
const result = await fn();
if (result) {
return result;
}
await timeout(100);
}
};
return await asyncScope.exec(innerFunction, description);
};
export const waitForFunctionWithTries = async<T>(
fn: () => Promise<T|undefined>, options: {tries: number} = {
tries: Number.MAX_SAFE_INTEGER,
},
asyncScope = new AsyncScope()) => {
return await asyncScope.exec(async () => {
let tries = 0;
while (tries++ < options.tries) {
const result = await fn();
if (result) {
return result;
}
await timeout(100);
}
return undefined;
});
};
export const waitForWithTries = async (
selector: string, root?: puppeteer.JSHandle, options: {tries: number} = {
tries: Number.MAX_SAFE_INTEGER,
},
asyncScope = new AsyncScope(), handler?: string) => {
return await asyncScope.exec(() => waitForFunctionWithTries(async () => {
const element = await $(selector, root, handler);
return (element || undefined);
}, options, asyncScope));
};
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',
});
};
async function setExperimentEnabled(experiment: string, enabled: boolean, options?: DevToolsFrontendReloadOptions) {
const {frontend} = getBrowserAndPages();
await frontend.evaluate(`(async () => {
const Root = await import('./core/root/root.js');
Root.Runtime.experiments.setEnabled('${experiment}', ${enabled});
})()`);
await reloadDevTools(options);
}
export const enableExperiment = (experiment: string, options?: DevToolsFrontendReloadOptions) =>
setExperimentEnabled(experiment, true, options);
export const disableExperiment = (experiment: string, options?: DevToolsFrontendReloadOptions) =>
setExperimentEnabled(experiment, false, options);
export const setDevToolsSettings = async (settings: Record<string, string>) => {
const {frontend} = getBrowserAndPages();
await frontend.evaluate(settings => {
for (const name in settings) {
globalThis.InspectorFrontendHost.setPreference(name, JSON.stringify(settings[name]));
}
}, settings);
await reloadDevTools();
};
export function goToHtml(html: string): Promise<void> {
return goTo(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
}
export const goTo = async (url: string, options: puppeteer.WaitForOptions = {}) => {
const {target} = getBrowserAndPages();
await target.goto(url, options);
};
export const overridePermissions = async (permissions: puppeteer.Permission[]) => {
const {browser} = getBrowserAndPages();
await browser.defaultBrowserContext().overridePermissions(`https://localhost:${getTestServerPort()}`, permissions);
};
export const clearPermissionsOverride = async () => {
const {browser} = getBrowserAndPages();
await browser.defaultBrowserContext().clearPermissionOverrides();
};
export const goToResource = async (path: string, options: puppeteer.WaitForOptions = {}) => {
await goTo(`${getResourcesPath()}/${path}`, options);
};
export const goToResourceWithCustomHost = async (host: string, path: string) => {
assert.isTrue(host.endsWith('.test'), 'Only custom hosts with a .test domain are allowed.');
await goTo(`${getResourcesPath(host)}/${path}`);
};
export const getResourcesPath = (host: string = 'localhost') => {
return `https://${host}:${getTestServerPort()}/test/e2e/resources`;
};
export const step = async (description: string, step: Function) => {
try {
return await step();
} catch (error) {
if (error instanceof AssertionError) {
throw new AssertionError(
`Unexpected Result in Step "${description}"
${error.message}`,
error);
} else {
error.message += ` in Step "${description}"`;
throw error;
}
}
};
export const waitForAnimationFrame = async () => {
const {frontend} = getBrowserAndPages();
await frontend.waitForFunction(() => {
return new Promise(resolve => {
requestAnimationFrame(resolve);
});
});
};
export const activeElement = async () => {
const {frontend} = getBrowserAndPages();
await waitForAnimationFrame();
return frontend.evaluateHandle(() => {
let activeElement = document.activeElement;
while (activeElement && activeElement.shadowRoot) {
activeElement = activeElement.shadowRoot.activeElement;
}
if (!activeElement) {
throw new Error('No active element found');
}
return activeElement;
});
};
export const activeElementTextContent = async () => {
const element = await activeElement();
return element.evaluate(node => node.textContent);
};
export const activeElementAccessibleName = async () => {
const element = await activeElement();
return element.evaluate(node => node.getAttribute('aria-label') || node.getAttribute('title'));
};
export const tabForward = async (page?: puppeteer.Page) => {
let targetPage: puppeteer.Page;
if (page) {
targetPage = page;
} else {
const {frontend} = getBrowserAndPages();
targetPage = frontend;
}
await targetPage.keyboard.press('Tab');
};
export const tabBackward = async (page?: puppeteer.Page) => {
let targetPage: puppeteer.Page;
if (page) {
targetPage = page;
} else {
const {frontend} = getBrowserAndPages();
targetPage = frontend;
}
await targetPage.keyboard.down('Shift');
await targetPage.keyboard.press('Tab');
await targetPage.keyboard.up('Shift');
};
type Awaitable<T> = T|PromiseLike<T>;
export const selectTextFromNodeToNode = async (
from: Awaitable<puppeteer.ElementHandle>, to: Awaitable<puppeteer.ElementHandle>, direction: 'up'|'down') => {
const {target} = getBrowserAndPages();
// The clipboard api does not allow you to copy, unless the tab is focused.
await target.bringToFront();
return target.evaluate(async (from, to, direction) => {
const selection = (from.getRootNode() as Document).getSelection();
const range = document.createRange();
if (direction === 'down') {
range.setStartBefore(from);
range.setEndAfter(to);
} else {
range.setStartBefore(to);
range.setEndAfter(from);
}
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
}
document.execCommand('copy');
return navigator.clipboard.readText();
}, await from, await to, direction);
};
export const clickMoreTabsButton = async (root?: puppeteer.ElementHandle<Element>) => {
await click('aria/More tabs', {root});
};
export const closePanelTab = async (panelTabSelector: string) => {
// Get close button from tab element
const selector = `${panelTabSelector} > .tabbed-pane-close-button`;
await click(selector);
await waitForNone(selector);
};
export const closeAllCloseableTabs = async () => {
// get all closeable tools by looking for the available x buttons on tabs
const selector = '.tabbed-pane-close-button';
const allCloseButtons = await $$(selector);
// Get all panel ids
const panelTabIds = await Promise.all(allCloseButtons.map(button => {
return button.evaluate(button => button.parentElement ? button.parentElement.id : '');
}));
// Close each tab
for (const tabId of panelTabIds) {
const selector = `#${tabId}`;
await closePanelTab(selector);
}
};
// Noisy! Do not leave this in your test but it may be helpful
// when debugging.
export const enableCDPLogging = async () => {
const {frontend} = getBrowserAndPages();
await frontend.evaluate(() => {
globalThis.ProtocolClient.test.dumpProtocol = console.log; // eslint-disable-line no-console
});
};
export const enableCDPTracking = async () => {
const {frontend} = getBrowserAndPages();
await frontend.evaluate(() => {
globalThis.__messageMapForTest = new Map();
globalThis.ProtocolClient.test.onMessageSent = (message: {method: string, id: number}) => {
globalThis.__messageMapForTest.set(message.id, message.method);
};
globalThis.ProtocolClient.test.onMessageReceived = (message: {id?: number}) => {
if (message.id) {
globalThis.__messageMapForTest.delete(message.id);
}
};
});
};
export const logOutstandingCDP = async () => {
const {frontend} = getBrowserAndPages();
await frontend.evaluate(() => {
for (const entry of globalThis.__messageMapForTest) {
console.error(entry);
}
});
};
export const selectOption = async (select: puppeteer.ElementHandle<HTMLSelectElement>, value: string) => {
await select.evaluate(async (node: HTMLSelectElement, _value: string) => {
node.value = _value;
const event = document.createEvent('HTMLEvents');
event.initEvent('change', false, true);
node.dispatchEvent(event);
}, value);
};
export const scrollElementIntoView = async (selector: string, root?: puppeteer.JSHandle) => {
const element = await $(selector, root);
if (!element) {
throw new Error(`Unable to find element with selector "${selector}"`);
}
await element.evaluate(el => {
el.scrollIntoView();
});
};
export const installEventListener = function(frontend: puppeteer.Page, eventType: string) {
return frontend.evaluate(eventType => {
window.__pendingEvents = window.__pendingEvents || new Map();
window.addEventListener(eventType, (e: Event) => {
let events = window.__pendingEvents.get(eventType);
if (!events) {
events = [];
window.__pendingEvents.set(eventType, events);
}
events.push(e);
});
}, eventType);
};
export const getPendingEvents = function(frontend: puppeteer.Page, eventType: string): Promise<Event[]|undefined> {
return frontend.evaluate(eventType => {
if (!('__pendingEvents' in window)) {
return undefined;
}
const pendingEvents = window.__pendingEvents.get(eventType);
window.__pendingEvents.set(eventType, []);
return pendingEvents;
}, eventType);
};
export function prepareWaitForEvent(element: puppeteer.ElementHandle, eventType: string): Promise<void> {
return element.evaluate((element: Element, eventType: string) => {
window.__eventHandlers = window.__eventHandlers || new WeakMap();
const eventHandlers = (() => {
const eventHandlers = window.__eventHandlers.get(element);
if (eventHandlers) {
return eventHandlers;
}
const newMap = new Map<string, Promise<void>>();
window.__eventHandlers.set(element, newMap);
return newMap;
})();
if (eventHandlers.has(eventType)) {
throw new Error(`Event listener for ${eventType}' has already been installed.`);
}
eventHandlers.set(eventType, new Promise<void>(resolve => {
const handler = () => {
element.removeEventListener(eventType, handler);
resolve();
};
element.addEventListener(eventType, handler);
}));
}, eventType);
}
export function waitForEvent(element: puppeteer.ElementHandle, eventType: string): Promise<void> {
return element.evaluate((element: Element, eventType: string) => {
if (!('__eventHandlers' in window)) {
throw new Error(`Event listener for '${eventType}' has not been installed.`);
}
const handler = window.__eventHandlers.get(element)?.get(eventType);
if (!handler) {
throw new Error(`Event listener for '${eventType}' has not been installed.`);
}
return handler;
}, eventType);
}
export const hasClass = async (element: puppeteer.ElementHandle<Element>, classname: string) => {
return await element.evaluate((el, classname) => el.classList.contains(classname), classname);
};
export const waitForClass = async (element: puppeteer.ElementHandle<Element>, classname: string) => {
await waitForFunction(async () => {
return hasClass(element, classname);
});
};
/**
* This is useful to keep TypeScript happy in a test - if you have a value
* that's potentially `null` you can use this function to assert that it isn't,
* and satisfy TypeScript that the value is present.
*/
export function assertNotNullOrUndefined<T>(val: T): asserts val is NonNullable<T> {
if (val === null || val === undefined) {
throw new Error(`Expected given value to not be null/undefined but it was: ${val}`);
}
}
export {getBrowserAndPages, getDevToolsFrontendHostname, getTestServerPort, reloadDevTools};
export function matchString(actual: string, expected: string|RegExp): true|string {
if (typeof expected === 'string') {
if (actual !== expected) {
return `Expected item "${actual}" to equal "${expected}"`;
}
} else if (!expected.test(actual)) {
return `Expected item "${actual}" to match "${expected}"`;
}
return true;
}
export function matchArray<A, E>(
actual: A[], expected: E[], comparator: (actual: A, expected: E) => true | string): true|string {
if (actual.length !== expected.length) {
return `Expected [${actual.map(x => `"${x}"`).join(', ')}] to have length ${expected.length}`;
}
for (let i = 0; i < expected.length; ++i) {
const result = comparator(actual[i], expected[i]);
if (result !== true) {
return `Mismatch in row ${i}: ${result}`;
}
}
return true;
}
export function assertOk<Args extends unknown[]>(check: (...args: Args) => true | string) {
return (...args: Args) => {
const result = check(...args);
if (result !== true) {
throw new AssertionError(result);
}
};
}
export function matchTable<A, E>(
actual: A[][], expected: E[][], comparator: (actual: A, expected: E) => true | string) {
return matchArray(actual, expected, (actual, expected) => matchArray<A, E>(actual, expected, comparator));
}
export const matchStringArray = (actual: string[], expected: (string|RegExp)[]) =>
matchArray(actual, expected, matchString);
export const assertMatchArray = assertOk(matchStringArray);
export const matchStringTable = (actual: string[][], expected: (string|RegExp)[][]) =>
matchTable(actual, expected, matchString);
export async function renderCoordinatorQueueEmpty(): Promise<void> {
const {frontend} = getBrowserAndPages();
await frontend.evaluate(() => {
return new Promise<void>(resolve => {
const pendingFrames = globalThis.__getRenderCoordinatorPendingFrames();
if (pendingFrames < 1) {
resolve();
return;
}
globalThis.addEventListener('renderqueueempty', resolve, {once: true});
});
});
}
export async function setCheckBox(selector: string, wantChecked: boolean): Promise<void> {
const checkbox = await waitFor(selector);
const checked = await checkbox.evaluate(box => (box as HTMLInputElement).checked);
if (checked !== wantChecked) {
await click(`${selector} + label`);
}
assert.strictEqual(await checkbox.evaluate(box => (box as HTMLInputElement).checked), wantChecked);
}
export const summonSearchBox = async () => {
await pressKey('f', {control: true});
};
export const replacePuppeteerUrl = (value: string) => {
return value.replace(/pptr:.*:([0-9]+)$/, (_, match) => {
return `(index):${match}`;
});
};
export async function raf(page: puppeteer.Page): Promise<void> {
await page.evaluate(() => {
return new Promise(resolve => window.requestAnimationFrame(resolve));
});
}