blob: 5abadf0af1f1af15a3074bcb9d381f201800f9c0 [file] [log] [blame]
Paul Lewisb8b38012020-01-22 17:18:471// Copyright 2020 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
Alex Rudenkod6594132021-02-04 11:58:065import {assert, AssertionError} from 'chai';
Tim van der Lippe869374b2020-04-20 10:12:316import * as os from 'os';
Randolf Jungbcb3bc82023-06-26 16:30:147import type * as puppeteer from 'puppeteer-core';
Tim van der Lippe869374b2020-04-20 10:12:318
Benedikt Meurer8a7915a2022-10-20 05:43:299import {type DevToolsFrontendReloadOptions} from '../conductor/frontend_tab.js';
Simon Zünde3cdd372022-02-16 09:36:5510import {getDevToolsFrontendHostname, reloadDevTools} from '../conductor/hooks.js';
Jack Franklin3b3a7822020-12-03 10:12:4611import {getBrowserAndPages, getTestServerPort} from '../conductor/puppeteer-state.js';
Philip Pfaffe9401ae12022-08-16 09:17:1612
Takuto Ikutaa36c4e82022-01-17 07:23:5413import {AsyncScope} from './async-scope.js';
Paul Lewis36122ad2020-02-28 12:30:0014
Philip Pfaffe1e8f6082020-10-26 11:55:5715declare global {
Tim van der Lippe75c2c9c2020-12-01 12:50:5316 // eslint-disable-next-line @typescript-eslint/no-unused-vars
Philip Pfaffe1e8f6082020-10-26 11:55:5717 interface Window {
Sigurd Schneider123977a2021-02-15 14:17:4618 // eslint-disable-next-line @typescript-eslint/naming-convention
Philip Pfaffe1e8f6082020-10-26 11:55:5719 __pendingEvents: Map<string, Event[]>;
Jack Franklina3f2d162021-10-01 11:30:2120
21 // eslint-disable-next-line @typescript-eslint/naming-convention
Philip Pfaffe7fb6a832022-11-14 13:39:4722 __eventHandlers: WeakMap<Element, Map<string, Promise<void>>>;
23
24 // eslint-disable-next-line @typescript-eslint/naming-convention
Jack Franklina3f2d162021-10-01 11:30:2125 __getRenderCoordinatorPendingFrames(): number;
Philip Pfaffe1e8f6082020-10-26 11:55:5726 }
27}
28
Alex Rudenko27d12032021-02-18 13:07:3329export type Platform = 'mac'|'win32'|'linux';
30export let platform: Platform;
Paul Lewis36122ad2020-02-28 12:30:0031switch (os.platform()) {
32 case 'darwin':
33 platform = 'mac';
34 break;
35
36 case 'win32':
37 platform = 'win32';
38 break;
39
40 default:
41 platform = 'linux';
42 break;
43}
Paul Lewisb8b38012020-01-22 17:18:4744
Mathias Bynensbb54eb52020-01-28 11:24:2745// TODO: Remove once Chromium updates its version of Node.js to 12+.
Paul Lewis839037f2020-07-21 12:25:1946// eslint-disable-next-line @typescript-eslint/no-explicit-any
Mathias Bynensbb54eb52020-01-28 11:24:2747const globalThis: any = global;
48
Jesper van den Ende01138842021-11-22 23:59:3949export interface ClickOptions {
50 root?: puppeteer.JSHandle;
Randolf18fb49b2023-01-30 08:38:5851 clickOptions?: puppeteer.ClickOptions;
Jesper van den Ende01138842021-11-22 23:59:3952 maxPixelsFromLeft?: number;
53}
54
Randolf18fb49b2023-01-30 08:38:5855const CONTROL_OR_META = platform === 'mac' ? 'Meta' : 'Control';
56export const withControlOrMetaKey = async (action: () => Promise<void>, root = getBrowserAndPages().frontend) => {
57 await waitForFunction(async () => {
58 await root.keyboard.down(CONTROL_OR_META);
59 try {
60 await action();
61 return true;
62 } finally {
63 await root.keyboard.up(CONTROL_OR_META);
64 }
65 });
66};
Sigurd Schneidera7d1bb12021-04-30 06:45:3767
Danil Somsikovacd635c2024-02-09 12:56:5168export const click = async (selector: string, options?: ClickOptions) => {
Alex Rudenkoafbc6c62023-01-31 10:49:0369 return await performActionOnSelector(
70 selector, {root: options?.root}, element => element.click(options?.clickOptions));
71};
72
Danil Somsikovacd635c2024-02-09 12:56:5173export const hover = async (selector: string, options?: {root?: puppeteer.JSHandle}) => {
Alex Rudenkoafbc6c62023-01-31 10:49:0374 return await performActionOnSelector(selector, {root: options?.root}, element => element.hover());
75};
76
77type Action = (element: puppeteer.ElementHandle) => Promise<void>;
78
79async function performActionOnSelector(
80 selector: string, options: {root?: puppeteer.JSHandle}, action: Action): Promise<puppeteer.ElementHandle> {
Alex Rudenkoe92fe9d2023-01-30 13:12:2381 // TODO(crbug.com/1410168): we should refactor waitFor to be compatible with
82 // Puppeteer's syntax for selectors.
83 const queryHandlers = new Set([
84 'pierceShadowText',
85 'pierce',
86 'aria',
87 'xpath',
88 'text',
89 ]);
90 let queryHandler = 'pierce';
91 for (const handler of queryHandlers) {
92 const prefix = handler + '/';
93 if (selector.startsWith(prefix)) {
94 queryHandler = handler;
95 selector = selector.substring(prefix.length);
96 break;
97 }
Tim van der Lippe2ab39562020-02-05 16:47:1198 }
Alex Rudenkoe92fe9d2023-01-30 13:12:2399 return waitForFunction(async () => {
100 const element = await waitFor(selector, options?.root, undefined, queryHandler);
101 try {
Alex Rudenkoafbc6c62023-01-31 10:49:03102 await action(element);
Alex Rudenkoe92fe9d2023-01-30 13:12:23103 return element;
104 } catch (err) {
105 // A bit of delay to not retry too often.
106 await new Promise(resolve => setTimeout(resolve, 50));
107 }
108 return undefined;
109 });
Alex Rudenkoafbc6c62023-01-31 10:49:03110}
Tim van der Lippe2ab39562020-02-05 16:47:11111
Alex Rudenkoe92fe9d2023-01-30 13:12:23112/**
Alex Rudenkoafbc6c62023-01-31 10:49:03113 * @deprecated This method is not able to recover from unstable DOM. Use click(selector) instead.
Alex Rudenkoe92fe9d2023-01-30 13:12:23114 */
115export async function clickElement(element: puppeteer.ElementHandle, options?: ClickOptions): Promise<void> {
Alex Rudenko13564022023-01-31 12:16:44116 // Retries here just in case the element gets connected to DOM / becomes visible.
117 await waitForFunction(async () => {
118 try {
119 await element.click(options?.clickOptions);
120 return true;
121 } catch {
122 return false;
123 }
124 });
Alex Rudenkoe92fe9d2023-01-30 13:12:23125}
126
Alex Rudenkoafbc6c62023-01-31 10:49:03127/**
128 * @deprecated This method is not able to recover from unstable DOM. Use hover(selector) instead.
129 */
130export async function hoverElement(element: puppeteer.ElementHandle): Promise<void> {
131 // Retries here just in case the element gets connected to DOM / becomes visible.
132 await waitForFunction(async () => {
133 try {
134 await element.hover();
135 return true;
136 } catch {
137 return false;
138 }
139 });
140}
141
Jack Franklin60f529d2020-03-26 16:58:09142export const doubleClick =
Sigurd Schneider15761862021-02-04 08:05:36143 async (selector: string, options?: {root?: puppeteer.JSHandle, clickOptions?: puppeteer.ClickOptions}) => {
Johan Bay00fc92a2020-08-14 20:06:20144 const passedClickOptions = (options && options.clickOptions) || {};
Jack Franklin60f529d2020-03-26 16:58:09145 const clickOptionsWithDoubleClick: puppeteer.ClickOptions = {
146 ...passedClickOptions,
147 clickCount: 2,
148 };
149 return click(selector, {
150 ...options,
151 clickOptions: clickOptionsWithDoubleClick,
152 });
153};
154
Peter Marshall31814262020-03-11 13:31:09155export const typeText = async (text: string) => {
Tim van der Lippe869374b2020-04-20 10:12:31156 const {frontend} = getBrowserAndPages();
Peter Marshall31814262020-03-11 13:31:09157 await frontend.keyboard.type(text);
158};
159
Tim van der Lipped79e3c52021-09-01 13:31:24160export const pressKey =
161 async (key: puppeteer.KeyInput, modifiers?: {control?: boolean, alt?: boolean, shift?: boolean}) => {
Eric Leese66342552020-07-06 16:43:35162 const {frontend} = getBrowserAndPages();
163 if (modifiers) {
164 if (modifiers.control) {
165 if (platform === 'mac') {
166 // Use command key on mac
167 await frontend.keyboard.down('Meta');
168 } else {
169 await frontend.keyboard.down('Control');
170 }
171 }
172 if (modifiers.alt) {
173 await frontend.keyboard.down('Alt');
174 }
175 if (modifiers.shift) {
176 await frontend.keyboard.down('Shift');
177 }
178 }
Tim van der Lipped79e3c52021-09-01 13:31:24179 await frontend.keyboard.press(key);
Eric Leese66342552020-07-06 16:43:35180 if (modifiers) {
181 if (modifiers.shift) {
182 await frontend.keyboard.up('Shift');
183 }
184 if (modifiers.alt) {
185 await frontend.keyboard.up('Alt');
186 }
187 if (modifiers.control) {
188 if (platform === 'mac') {
189 // Use command key on mac
190 await frontend.keyboard.up('Meta');
191 } else {
192 await frontend.keyboard.up('Control');
193 }
194 }
195 }
196};
197
Almothana Athamnehf55afd72020-05-28 11:26:46198export const pasteText = async (text: string) => {
199 const {frontend} = getBrowserAndPages();
Almothana Athamnehf55afd72020-05-28 11:26:46200 await frontend.keyboard.sendCharacter(text);
201};
202
Johan Bayb01fc262020-11-02 12:38:46203// Get a single element handle. Uses `pierce` handler per default for piercing Shadow DOM.
Jack Franklinfda44a12021-08-23 11:04:21204export const $ =
205 async<ElementType extends Element = Element>(selector: string, root?: puppeteer.JSHandle, handler = 'pierce') => {
Tim van der Lippe869374b2020-04-20 10:12:31206 const {frontend} = getBrowserAndPages();
Johan Baye8245712020-08-04 13:32:10207 const rootElement = root ? root as puppeteer.ElementHandle : frontend;
Randolfcc892542023-01-27 23:44:07208 const element = await rootElement.$(`${handler}/${selector}`) as puppeteer.ElementHandle<ElementType>;
Simon Zünd9e2c7592023-01-17 07:59:41209 return element;
Mathias Bynensbb54eb52020-01-28 11:24:27210};
Paul Lewisb8b38012020-01-22 17:18:47211
Johan Bayb01fc262020-11-02 12:38:46212// Get multiple element handles. Uses `pierce` handler per default for piercing Shadow DOM.
Jack Franklinfda44a12021-08-23 11:04:21213export const $$ =
214 async<ElementType extends Element = Element>(selector: string, root?: puppeteer.JSHandle, handler = 'pierce') => {
Tim van der Lippe869374b2020-04-20 10:12:31215 const {frontend} = getBrowserAndPages();
Johan Baye8245712020-08-04 13:32:10216 const rootElement = root ? root.asElement() || frontend : frontend;
Randolfcc892542023-01-27 23:44:07217 const elements = await rootElement.$$(`${handler}/${selector}`) as puppeteer.ElementHandle<ElementType>[];
Simon Zünd9e2c7592023-01-17 07:59:41218 return elements;
Tim van der Lippea8485182020-02-06 17:27:39219};
220
Paul Lewisab0c65c2020-03-27 10:25:09221/**
222 * Search for an element based on its textContent.
223 *
224 * @param textContent The text content to search for.
225 * @param root The root of the search.
226 */
227export const $textContent = async (textContent: string, root?: puppeteer.JSHandle) => {
Johan Bayb01fc262020-11-02 12:38:46228 return $(textContent, root, 'pierceShadowText');
Paul Lewisab0c65c2020-03-27 10:25:09229};
230
Jack Franklinf56bc382020-09-11 14:00:36231/**
232 * Search for all elements based on their textContent
233 *
234 * @param textContent The text content to search for.
235 * @param root The root of the search.
236 */
237export const $$textContent = async (textContent: string, root?: puppeteer.JSHandle) => {
Johan Bayb01fc262020-11-02 12:38:46238 return $$(textContent, root, 'pierceShadowText');
Jack Franklinf56bc382020-09-11 14:00:36239};
240
Jack Frankline245d1a2020-02-13 15:25:13241export const timeout = (duration: number) => new Promise(resolve => setTimeout(resolve, duration));
Tim van der Lippe011fa182020-02-12 17:12:27242
Charles Vazacbfbdafa2024-01-18 16:22:50243export const getTextContent =
244 async<ElementType extends Element = Element>(selector: string, root?: puppeteer.JSHandle) => {
245 const text = await (await $<ElementType>(selector, root))?.evaluate(node => node.textContent);
PhistucK257ad692022-08-29 14:36:19246 return text ?? undefined;
247};
248
Kim-Anh Tran429e8b72024-03-20 09:12:02249export const getAllTextContents =
250 async(selector: string, root?: puppeteer.JSHandle, handler = 'pierce'): Promise<Array<string|null>> => {
251 const allElements = await $$(selector, root, handler);
252 return Promise.all(allElements.map(e => e.evaluate(e => e.textContent)));
253};
254
Eric Leese88fd4412023-05-16 11:22:05255/**
256 * Match multiple elements based on a selector and return their textContents, but only for those
257 * elements that are visible.
258 *
259 * @param selector jquery selector to match
260 * @returns array containing text contents from visible elements
261 */
262export const getVisibleTextContents = async (selector: string) => {
263 const allElements = await $$(selector);
264 const texts = await Promise.all(
265 allElements.map(el => el.evaluate(node => node.checkVisibility() ? node.textContent?.trim() : undefined)));
266 return texts.filter(content => typeof (content) === 'string');
267};
268
Jack Franklinfda44a12021-08-23 11:04:21269export const waitFor = async<ElementType extends Element = Element>(
270 selector: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope(), handler?: string) => {
Philip Pfaffe6d143ae2020-07-31 11:32:22271 return await asyncScope.exec(() => waitForFunction(async () => {
Jack Franklinfda44a12021-08-23 11:04:21272 const element = await $<ElementType>(selector, root, handler);
Johan Bay504a6f12020-08-04 13:32:53273 return (element || undefined);
Philip Pfaffebafb48f2023-09-15 09:51:42274 }, asyncScope), `Waiting for element matching selector '${selector}'`);
Tim van der Lippe68efc702020-03-03 17:27:45275};
276
Eric Leese88fd4412023-05-16 11:22:05277export const waitForVisible = async<ElementType extends Element = Element>(
278 selector: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope(), handler?: string) => {
279 return await asyncScope.exec(() => waitForFunction(async () => {
280 const element = await $<ElementType>(selector, root, handler);
281 const visible = await element.evaluate(node => node.checkVisibility());
282 return visible ? element : undefined;
Philip Pfaffebafb48f2023-09-15 09:51:42283 }, asyncScope), `Waiting for element matching selector '${selector}' to be visible`);
Eric Leese88fd4412023-05-16 11:22:05284};
285
Philip Pfaffe7426b002020-11-30 15:59:22286export const waitForMany = async (
287 selector: string, count: number, root?: puppeteer.JSHandle, asyncScope = new AsyncScope(), handler?: string) => {
288 return await asyncScope.exec(() => waitForFunction(async () => {
289 const elements = await $$(selector, root, handler);
290 return elements.length >= count ? elements : undefined;
Philip Pfaffebafb48f2023-09-15 09:51:42291 }, asyncScope), `Waiting for ${count} elements to match selector '${selector}'`);
Philip Pfaffe7426b002020-11-30 15:59:22292};
293
Johan Bayb01fc262020-11-02 12:38:46294export const waitForNone =
295 async (selector: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope(), handler?: string) => {
Philip Pfaffe6d143ae2020-07-31 11:32:22296 return await asyncScope.exec(() => waitForFunction(async () => {
Johan Bayb01fc262020-11-02 12:38:46297 const elements = await $$(selector, root, handler);
Johan Baye8245712020-08-04 13:32:10298 if (elements.length === 0) {
Philip Pfaffe6d143ae2020-07-31 11:32:22299 return true;
300 }
301 return false;
Philip Pfaffebafb48f2023-09-15 09:51:42302 }, asyncScope), `Waiting for no elements to match selector '${selector}'`);
Peter Marshall31814262020-03-11 13:31:09303};
304
Johan Bayf04b99c2020-11-03 15:38:41305export const waitForAria = (selector: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope()) => {
306 return waitFor(selector, root, asyncScope, 'aria');
307};
308
Kateryna Prokopenko227106d2021-10-11 15:38:16309export const waitForAriaNone = (selector: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope()) => {
310 return waitForNone(selector, root, asyncScope, 'aria');
311};
312
Peter Marshallc059be12020-07-10 14:17:17313export const waitForElementWithTextContent =
Philip Pfaffe6d143ae2020-07-31 11:32:22314 (textContent: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope()) => {
Johan Bayb01fc262020-11-02 12:38:46315 return waitFor(textContent, root, asyncScope, 'pierceShadowText');
Peter Marshallc059be12020-07-10 14:17:17316 };
Paul Lewisab0c65c2020-03-27 10:25:09317
Jack Franklinf56bc382020-09-11 14:00:36318export const waitForElementsWithTextContent =
319 (textContent: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope()) => {
320 return asyncScope.exec(() => waitForFunction(async () => {
321 const elems = await $$textContent(textContent, root);
322 if (elems && elems.length) {
323 return elems;
324 }
325
326 return undefined;
Philip Pfaffebafb48f2023-09-15 09:51:42327 }, asyncScope), `Waiting for elements with textContent '${textContent}'`);
Jack Franklinf56bc382020-09-11 14:00:36328 };
329
Jack Lynchd529e882020-11-03 19:17:27330export const waitForNoElementsWithTextContent =
331 (textContent: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope()) => {
332 return asyncScope.exec(() => waitForFunction(async () => {
333 const elems = await $$textContent(textContent, root);
334 if (elems && elems.length === 0) {
335 return true;
336 }
337
338 return false;
Philip Pfaffebafb48f2023-09-15 09:51:42339 }, asyncScope), `Waiting for no elements with textContent '${textContent}'`);
Jack Lynchd529e882020-11-03 19:17:27340 };
341
Andres Olivares9ef8a012024-01-16 11:41:03342export const TIMEOUT_ERROR_MESSAGE = 'Test timed out';
343
Eric Leese23e6a0b2023-08-14 12:26:32344export const waitForFunction =
Danil Somsikovacd635c2024-02-09 12:56:51345 async<T>(fn: () => Promise<T|undefined>, asyncScope = new AsyncScope(), description?: string) => {
Philip Pfaffebafb48f2023-09-15 09:51:42346 const innerFunction = async () => {
Philip Pfaffe6d143ae2020-07-31 11:32:22347 while (true) {
Philip Pfaffed097e512021-06-22 09:56:46348 if (asyncScope.isCanceled()) {
Andres Olivares9ef8a012024-01-16 11:41:03349 throw new Error(TIMEOUT_ERROR_MESSAGE);
Philip Pfaffed097e512021-06-22 09:56:46350 }
Philip Pfaffe6d143ae2020-07-31 11:32:22351 const result = await fn();
352 if (result) {
353 return result;
354 }
Johan Bay00fc92a2020-08-14 20:06:20355 await timeout(100);
Paul Lewis11cc3e82020-02-04 13:42:56356 }
Eric Leese23e6a0b2023-08-14 12:26:32357 };
Philip Pfaffebafb48f2023-09-15 09:51:42358 return await asyncScope.exec(innerFunction, description);
Jack Frankline245d1a2020-02-13 15:25:13359};
Paul Lewis11cc3e82020-02-04 13:42:56360
Sigurd Schneiderc3f5d192021-05-18 11:40:45361export const waitForFunctionWithTries = async<T>(
362 fn: () => Promise<T|undefined>, options: {tries: number} = {
363 tries: Number.MAX_SAFE_INTEGER,
364 },
Danil Somsikovacd635c2024-02-09 12:56:51365 asyncScope = new AsyncScope()) => {
Sigurd Schneiderc3f5d192021-05-18 11:40:45366 return await asyncScope.exec(async () => {
367 let tries = 0;
368 while (tries++ < options.tries) {
369 const result = await fn();
370 if (result) {
371 return result;
372 }
373 await timeout(100);
374 }
375 return undefined;
376 });
377};
378
379export const waitForWithTries = async (
380 selector: string, root?: puppeteer.JSHandle, options: {tries: number} = {
381 tries: Number.MAX_SAFE_INTEGER,
382 },
383 asyncScope = new AsyncScope(), handler?: string) => {
384 return await asyncScope.exec(() => waitForFunctionWithTries(async () => {
385 const element = await $(selector, root, handler);
386 return (element || undefined);
387 }, options, asyncScope));
388};
389
Tim van der Lippe58c5bcf2020-02-04 15:26:08390export const debuggerStatement = (frontend: puppeteer.Page) => {
391 return frontend.evaluate(() => {
Jack Frankline245d1a2020-02-13 15:25:13392 // eslint-disable-next-line no-debugger
Tim van der Lippe58c5bcf2020-02-04 15:26:08393 debugger;
394 });
395};
396
Paul Lewis417c5c02020-03-27 10:49:06397export const logToStdOut = (msg: string) => {
398 if (!process.send) {
399 return;
400 }
401
402 process.send({
403 pid: process.pid,
404 details: msg,
405 });
406};
407
Paul Lewis71085742020-04-01 11:18:31408export const logFailure = () => {
409 if (!process.send) {
410 return;
411 }
412
413 process.send({
414 pid: process.pid,
415 details: 'failure',
416 });
417};
418
Benedikt Meurer8a7915a2022-10-20 05:43:29419async function setExperimentEnabled(experiment: string, enabled: boolean, options?: DevToolsFrontendReloadOptions) {
Songtao Xia67f65da2020-06-11 17:58:39420 const {frontend} = getBrowserAndPages();
Simon Zünd5583b992023-08-30 10:58:10421 await frontend.evaluate(`(async () => {
422 const Root = await import('./core/root/root.js');
423 Root.Runtime.experiments.setEnabled('${experiment}', ${enabled});
424 })()`);
Songtao Xia67f65da2020-06-11 17:58:39425 await reloadDevTools(options);
Benedikt Meurer8a7915a2022-10-20 05:43:29426}
427
428export const enableExperiment = (experiment: string, options?: DevToolsFrontendReloadOptions) =>
429 setExperimentEnabled(experiment, true, options);
430
431export const disableExperiment = (experiment: string, options?: DevToolsFrontendReloadOptions) =>
432 setExperimentEnabled(experiment, false, options);
Songtao Xia67f65da2020-06-11 17:58:39433
Simon Zünd0b582052022-02-16 08:09:40434export const setDevToolsSettings = async (settings: Record<string, string>) => {
435 const {frontend} = getBrowserAndPages();
436 await frontend.evaluate(settings => {
437 for (const name in settings) {
438 globalThis.InspectorFrontendHost.setPreference(name, JSON.stringify(settings[name]));
439 }
440 }, settings);
441 await reloadDevTools();
442};
443
Philip Pfaffefdf153a2024-04-05 06:53:27444export function goToHtml(html: string): Promise<void> {
445 return goTo(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
446}
447
Al Muthanna Athamina61c99382023-04-06 10:17:13448export const goTo = async (url: string, options: puppeteer.WaitForOptions = {}) => {
Patrick Brosset0805f4a2020-06-11 09:34:32449 const {target} = getBrowserAndPages();
Al Muthanna Athamina61c99382023-04-06 10:17:13450 await target.goto(url, options);
Patrick Brosset0805f4a2020-06-11 09:34:32451};
452
Jack Franklin5017c092021-02-12 11:40:43453export const overridePermissions = async (permissions: puppeteer.Permission[]) => {
Maksim Sadym4d2e0952020-08-06 08:34:53454 const {browser} = getBrowserAndPages();
Jack Franklin3b3a7822020-12-03 10:12:46455 await browser.defaultBrowserContext().overridePermissions(`https://localhost:${getTestServerPort()}`, permissions);
Maksim Sadym4d2e0952020-08-06 08:34:53456};
457
458export const clearPermissionsOverride = async () => {
459 const {browser} = getBrowserAndPages();
460 await browser.defaultBrowserContext().clearPermissionOverrides();
461};
462
Al Muthanna Athamina61c99382023-04-06 10:17:13463export const goToResource = async (path: string, options: puppeteer.WaitForOptions = {}) => {
464 await goTo(`${getResourcesPath()}/${path}`, options);
Peter Marshallc16cedd2020-07-07 15:11:17465};
466
Simon Zünd5fd75062021-02-12 12:49:45467export const goToResourceWithCustomHost = async (host: string, path: string) => {
468 assert.isTrue(host.endsWith('.test'), 'Only custom hosts with a .test domain are allowed.');
469 await goTo(`${getResourcesPath(host)}/${path}`);
470};
471
472export const getResourcesPath = (host: string = 'localhost') => {
Philip Pfaffe9d80aef2024-07-09 16:59:30473 return `https://${host}:${getTestServerPort()}/test/e2e/resources`;
Patrick Brosset0805f4a2020-06-11 09:34:32474};
475
Eric Leese23e6a0b2023-08-14 12:26:32476export const step = async (description: string, step: Function) => {
477 try {
Philip Pfaffebafb48f2023-09-15 09:51:42478 return await step();
Al Muthanna Athamina889cecf2020-04-22 13:46:37479 } catch (error) {
480 if (error instanceof AssertionError) {
481 throw new AssertionError(
482 `Unexpected Result in Step "${description}"
483 ${error.message}`,
484 error);
485 } else {
486 error.message += ` in Step "${description}"`;
487 throw error;
488 }
489 }
490};
491
Tim van der Lippee9b8e992020-09-22 13:26:37492export const waitForAnimationFrame = async () => {
Tim van der Lippe72f2d4b2021-07-06 16:02:22493 const {frontend} = getBrowserAndPages();
Tim van der Lippee9b8e992020-09-22 13:26:37494
Tim van der Lippe72f2d4b2021-07-06 16:02:22495 await frontend.waitForFunction(() => {
Tim van der Lippee9b8e992020-09-22 13:26:37496 return new Promise(resolve => {
497 requestAnimationFrame(resolve);
498 });
499 });
500};
501
Randolfcc892542023-01-27 23:44:07502export const activeElement = async () => {
Tim van der Lippe72f2d4b2021-07-06 16:02:22503 const {frontend} = getBrowserAndPages();
Tim van der Lippee9b8e992020-09-22 13:26:37504
505 await waitForAnimationFrame();
506
Tim van der Lippe72f2d4b2021-07-06 16:02:22507 return frontend.evaluateHandle(() => {
Tim van der Lippee9b8e992020-09-22 13:26:37508 let activeElement = document.activeElement;
509
510 while (activeElement && activeElement.shadowRoot) {
511 activeElement = activeElement.shadowRoot.activeElement;
512 }
513
Randolfcc892542023-01-27 23:44:07514 if (!activeElement) {
515 throw new Error('No active element found');
516 }
517
Simon Zünd9e2c7592023-01-17 07:59:41518 return activeElement;
Tim van der Lippee9b8e992020-09-22 13:26:37519 });
520};
521
522export const activeElementTextContent = async () => {
523 const element = await activeElement();
524 return element.evaluate(node => node.textContent);
525};
526
Tim van der Lippe72f2d4b2021-07-06 16:02:22527export const activeElementAccessibleName = async () => {
528 const element = await activeElement();
Kateryna Prokopenko17cba892024-07-26 13:33:56529 return element.evaluate(node => node.getAttribute('aria-label') || node.getAttribute('title'));
Tim van der Lippee9b8e992020-09-22 13:26:37530};
531
Tim van der Lippe72f2d4b2021-07-06 16:02:22532export const tabForward = async (page?: puppeteer.Page) => {
533 let targetPage: puppeteer.Page;
534 if (page) {
535 targetPage = page;
536 } else {
537 const {frontend} = getBrowserAndPages();
538 targetPage = frontend;
539 }
Tim van der Lippee9b8e992020-09-22 13:26:37540
Tim van der Lippe72f2d4b2021-07-06 16:02:22541 await targetPage.keyboard.press('Tab');
542};
543
544export const tabBackward = async (page?: puppeteer.Page) => {
545 let targetPage: puppeteer.Page;
546 if (page) {
547 targetPage = page;
548 } else {
549 const {frontend} = getBrowserAndPages();
550 targetPage = frontend;
551 }
552
553 await targetPage.keyboard.down('Shift');
554 await targetPage.keyboard.press('Tab');
555 await targetPage.keyboard.up('Shift');
Tim van der Lippee9b8e992020-09-22 13:26:37556};
557
Randolfcc892542023-01-27 23:44:07558type Awaitable<T> = T|PromiseLike<T>;
559
Tim van der Lippee9b8e992020-09-22 13:26:37560export const selectTextFromNodeToNode = async (
Randolfcc892542023-01-27 23:44:07561 from: Awaitable<puppeteer.ElementHandle>, to: Awaitable<puppeteer.ElementHandle>, direction: 'up'|'down') => {
Tim van der Lippee9b8e992020-09-22 13:26:37562 const {target} = getBrowserAndPages();
563
564 // The clipboard api does not allow you to copy, unless the tab is focused.
565 await target.bringToFront();
566
567 return target.evaluate(async (from, to, direction) => {
Randolfcc892542023-01-27 23:44:07568 const selection = (from.getRootNode() as Document).getSelection();
Tim van der Lippee9b8e992020-09-22 13:26:37569 const range = document.createRange();
570 if (direction === 'down') {
571 range.setStartBefore(from);
572 range.setEndAfter(to);
573 } else {
574 range.setStartBefore(to);
575 range.setEndAfter(from);
576 }
577
Randolfcc892542023-01-27 23:44:07578 if (selection) {
579 selection.removeAllRanges();
580 selection.addRange(range);
581 }
Tim van der Lippee9b8e992020-09-22 13:26:37582
583 document.execCommand('copy');
584
585 return navigator.clipboard.readText();
586 }, await from, await to, direction);
587};
588
Kateryna Prokopenkoa2016cc2024-02-26 14:45:13589export const clickMoreTabsButton = async (root?: puppeteer.ElementHandle<Element>) => {
590 await click('aria/More tabs', {root});
591};
592
Jose Leal Chapa50e7bb52020-06-19 17:03:27593export const closePanelTab = async (panelTabSelector: string) => {
Jose Leal Chapadd4d8812020-05-21 16:45:00594 // Get close button from tab element
Jose Leal Chapa50e7bb52020-06-19 17:03:27595 const selector = `${panelTabSelector} > .tabbed-pane-close-button`;
Jose Leal Chapadd4d8812020-05-21 16:45:00596 await click(selector);
Jose Leal Chapa50e7bb52020-06-19 17:03:27597 await waitForNone(selector);
Jose Leal Chapadd4d8812020-05-21 16:45:00598};
599
600export const closeAllCloseableTabs = async () => {
601 // get all closeable tools by looking for the available x buttons on tabs
602 const selector = '.tabbed-pane-close-button';
603 const allCloseButtons = await $$(selector);
604
605 // Get all panel ids
Johan Baye8245712020-08-04 13:32:10606 const panelTabIds = await Promise.all(allCloseButtons.map(button => {
607 return button.evaluate(button => button.parentElement ? button.parentElement.id : '');
608 }));
Jose Leal Chapadd4d8812020-05-21 16:45:00609
610 // Close each tab
Jose Leal Chapa50e7bb52020-06-19 17:03:27611 for (const tabId of panelTabIds) {
612 const selector = `#${tabId}`;
613 await closePanelTab(selector);
Jose Leal Chapadd4d8812020-05-21 16:45:00614 }
615};
616
Eric Leese66342552020-07-06 16:43:35617// Noisy! Do not leave this in your test but it may be helpful
618// when debugging.
619export const enableCDPLogging = async () => {
620 const {frontend} = getBrowserAndPages();
621 await frontend.evaluate(() => {
622 globalThis.ProtocolClient.test.dumpProtocol = console.log; // eslint-disable-line no-console
623 });
624};
625
Eric Leesea6533f02022-07-19 15:33:04626export const enableCDPTracking = async () => {
627 const {frontend} = getBrowserAndPages();
628 await frontend.evaluate(() => {
629 globalThis.__messageMapForTest = new Map();
630 globalThis.ProtocolClient.test.onMessageSent = (message: {method: string, id: number}) => {
631 globalThis.__messageMapForTest.set(message.id, message.method);
632 };
633 globalThis.ProtocolClient.test.onMessageReceived = (message: {id?: number}) => {
634 if (message.id) {
635 globalThis.__messageMapForTest.delete(message.id);
636 }
637 };
638 });
639};
640
641export const logOutstandingCDP = async () => {
642 const {frontend} = getBrowserAndPages();
643 await frontend.evaluate(() => {
644 for (const entry of globalThis.__messageMapForTest) {
645 console.error(entry);
646 }
647 });
648};
649
Jack Franklin60cc8ba2021-02-10 12:14:26650export const selectOption = async (select: puppeteer.ElementHandle<HTMLSelectElement>, value: string) => {
651 await select.evaluate(async (node: HTMLSelectElement, _value: string) => {
Maksim Sadym4d2e0952020-08-06 08:34:53652 node.value = _value;
653 const event = document.createEvent('HTMLEvents');
654 event.initEvent('change', false, true);
655 node.dispatchEvent(event);
656 }, value);
657};
658
Jesus David Garcia Gomez9103d0e2020-10-12 20:28:40659export const scrollElementIntoView = async (selector: string, root?: puppeteer.JSHandle) => {
660 const element = await $(selector, root);
661
662 if (!element) {
663 throw new Error(`Unable to find element with selector "${selector}"`);
664 }
665
666 await element.evaluate(el => {
667 el.scrollIntoView();
668 });
669};
670
Philip Pfaffe1e8f6082020-10-26 11:55:57671export const installEventListener = function(frontend: puppeteer.Page, eventType: string) {
672 return frontend.evaluate(eventType => {
Jack Franklin2204d752022-11-21 14:10:10673 window.__pendingEvents = window.__pendingEvents || new Map();
Philip Pfaffe1e8f6082020-10-26 11:55:57674 window.addEventListener(eventType, (e: Event) => {
675 let events = window.__pendingEvents.get(eventType);
676 if (!events) {
677 events = [];
678 window.__pendingEvents.set(eventType, events);
679 }
680 events.push(e);
681 });
682 }, eventType);
683};
684
Kim-Anh Tran159f3de2022-05-18 10:54:28685export const getPendingEvents = function(frontend: puppeteer.Page, eventType: string): Promise<Event[]|undefined> {
Philip Pfaffe1e8f6082020-10-26 11:55:57686 return frontend.evaluate(eventType => {
687 if (!('__pendingEvents' in window)) {
Kim-Anh Tran159f3de2022-05-18 10:54:28688 return undefined;
Philip Pfaffe1e8f6082020-10-26 11:55:57689 }
690 const pendingEvents = window.__pendingEvents.get(eventType);
691 window.__pendingEvents.set(eventType, []);
Kim-Anh Tran159f3de2022-05-18 10:54:28692 return pendingEvents;
Philip Pfaffe1e8f6082020-10-26 11:55:57693 }, eventType);
694};
695
Philip Pfaffe7fb6a832022-11-14 13:39:47696export function prepareWaitForEvent(element: puppeteer.ElementHandle, eventType: string): Promise<void> {
697 return element.evaluate((element: Element, eventType: string) => {
Jack Franklin2204d752022-11-21 14:10:10698 window.__eventHandlers = window.__eventHandlers || new WeakMap();
Philip Pfaffe7fb6a832022-11-14 13:39:47699
700 const eventHandlers = (() => {
701 const eventHandlers = window.__eventHandlers.get(element);
702 if (eventHandlers) {
703 return eventHandlers;
704 }
705 const newMap = new Map<string, Promise<void>>();
706 window.__eventHandlers.set(element, newMap);
707 return newMap;
708 })();
709
710 if (eventHandlers.has(eventType)) {
711 throw new Error(`Event listener for ${eventType}' has already been installed.`);
712 }
713 eventHandlers.set(eventType, new Promise<void>(resolve => {
714 const handler = () => {
715 element.removeEventListener(eventType, handler);
716 resolve();
717 };
718 element.addEventListener(eventType, handler);
719 }));
720 }, eventType);
721}
722
723export function waitForEvent(element: puppeteer.ElementHandle, eventType: string): Promise<void> {
724 return element.evaluate((element: Element, eventType: string) => {
725 if (!('__eventHandlers' in window)) {
726 throw new Error(`Event listener for '${eventType}' has not been installed.`);
727 }
728 const handler = window.__eventHandlers.get(element)?.get(eventType);
729 if (!handler) {
730 throw new Error(`Event listener for '${eventType}' has not been installed.`);
731 }
732 return handler;
733 }, eventType);
734}
735
Danil Somsikovacd635c2024-02-09 12:56:51736export const hasClass = async (element: puppeteer.ElementHandle<Element>, classname: string) => {
Sigurd Schneiderbec4aa42021-04-21 08:28:57737 return await element.evaluate((el, classname) => el.classList.contains(classname), classname);
738};
739
Danil Somsikovacd635c2024-02-09 12:56:51740export const waitForClass = async (element: puppeteer.ElementHandle<Element>, classname: string) => {
Sigurd Schneider286466e2021-01-28 10:09:42741 await waitForFunction(async () => {
Sigurd Schneiderbec4aa42021-04-21 08:28:57742 return hasClass(element, classname);
Sigurd Schneider286466e2021-01-28 10:09:42743 });
744};
745
Wolfgang Beyerbe0727c2021-07-30 07:16:04746/**
747 * This is useful to keep TypeScript happy in a test - if you have a value
748 * that's potentially `null` you can use this function to assert that it isn't,
749 * and satisfy TypeScript that the value is present.
750 */
751export function assertNotNullOrUndefined<T>(val: T): asserts val is NonNullable<T> {
752 if (val === null || val === undefined) {
753 throw new Error(`Expected given value to not be null/undefined but it was: ${val}`);
754 }
Alex Rudenkod6594132021-02-04 11:58:06755}
756
Randolfad06e712023-02-03 06:44:47757export {getBrowserAndPages, getDevToolsFrontendHostname, getTestServerPort, reloadDevTools};
Sigurd Schneider2ca00062021-04-20 10:49:05758
Sigurd Schneider76cb0e92021-06-24 06:13:10759export function matchString(actual: string, expected: string|RegExp): true|string {
760 if (typeof expected === 'string') {
761 if (actual !== expected) {
762 return `Expected item "${actual}" to equal "${expected}"`;
763 }
764 } else if (!expected.test(actual)) {
765 return `Expected item "${actual}" to match "${expected}"`;
766 }
767 return true;
768}
769
770export function matchArray<A, E>(
771 actual: A[], expected: E[], comparator: (actual: A, expected: E) => true | string): true|string {
Sigurd Schneider2ca00062021-04-20 10:49:05772 if (actual.length !== expected.length) {
Sigurd Schneiderc86b89f2021-05-05 08:35:31773 return `Expected [${actual.map(x => `"${x}"`).join(', ')}] to have length ${expected.length}`;
Sigurd Schneider2ca00062021-04-20 10:49:05774 }
775
776 for (let i = 0; i < expected.length; ++i) {
Sigurd Schneider76cb0e92021-06-24 06:13:10777 const result = comparator(actual[i], expected[i]);
778 if (result !== true) {
779 return `Mismatch in row ${i}: ${result}`;
Sigurd Schneider2ca00062021-04-20 10:49:05780 }
781 }
Sigurd Schneiderc86b89f2021-05-05 08:35:31782 return true;
783}
784
Sigurd Schneider76cb0e92021-06-24 06:13:10785export function assertOk<Args extends unknown[]>(check: (...args: Args) => true | string) {
786 return (...args: Args) => {
787 const result = check(...args);
788 if (result !== true) {
789 throw new AssertionError(result);
790 }
791 };
Sigurd Schneider2ca00062021-04-20 10:49:05792}
Paul Lewis462704782021-05-21 13:47:34793
Sigurd Schneider76cb0e92021-06-24 06:13:10794export function matchTable<A, E>(
795 actual: A[][], expected: E[][], comparator: (actual: A, expected: E) => true | string) {
796 return matchArray(actual, expected, (actual, expected) => matchArray<A, E>(actual, expected, comparator));
797}
798
799export const matchStringArray = (actual: string[], expected: (string|RegExp)[]) =>
800 matchArray(actual, expected, matchString);
801
802export const assertMatchArray = assertOk(matchStringArray);
803
804export const matchStringTable = (actual: string[][], expected: (string|RegExp)[][]) =>
805 matchTable(actual, expected, matchString);
806
Paul Lewis462704782021-05-21 13:47:34807export async function renderCoordinatorQueueEmpty(): Promise<void> {
Jack Franklina3f2d162021-10-01 11:30:21808 const {frontend} = getBrowserAndPages();
Paul Lewis462704782021-05-21 13:47:34809 await frontend.evaluate(() => {
Jack Franklina3f2d162021-10-01 11:30:21810 return new Promise<void>(resolve => {
811 const pendingFrames = globalThis.__getRenderCoordinatorPendingFrames();
812 if (pendingFrames < 1) {
813 resolve();
814 return;
815 }
Paul Lewis462704782021-05-21 13:47:34816 globalThis.addEventListener('renderqueueempty', resolve, {once: true});
817 });
818 });
819}
Sigurd Schneider20e17482021-09-03 10:25:53820
821export async function setCheckBox(selector: string, wantChecked: boolean): Promise<void> {
822 const checkbox = await waitFor(selector);
823 const checked = await checkbox.evaluate(box => (box as HTMLInputElement).checked);
824 if (checked !== wantChecked) {
825 await click(`${selector} + label`);
826 }
827 assert.strictEqual(await checkbox.evaluate(box => (box as HTMLInputElement).checked), wantChecked);
828}
PhistucK257ad692022-08-29 14:36:19829
830export const summonSearchBox = async () => {
831 await pressKey('f', {control: true});
832};
Randolf Jungbcb3bc82023-06-26 16:30:14833
Danil Somsikovacd635c2024-02-09 12:56:51834export const replacePuppeteerUrl = (value: string) => {
Randolf Jungbcb3bc82023-06-26 16:30:14835 return value.replace(/pptr:.*:([0-9]+)$/, (_, match) => {
836 return `(index):${match}`;
837 });
838};
Alex Rudenkofacf0142023-09-05 07:31:11839
840export async function raf(page: puppeteer.Page): Promise<void> {
841 await page.evaluate(() => {
842 return new Promise(resolve => window.requestAnimationFrame(resolve));
843 });
844}