| // 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 {AssertionError} from 'chai'; |
| import * as os from 'os'; |
| import * as puppeteer from 'puppeteer'; |
| |
| import {reloadDevTools} from '../conductor/hooks.js'; |
| import {getBrowserAndPages, getHostedModeServerPort} from '../conductor/puppeteer-state.js'; |
| import {AsyncScope} from './mocha-extensions.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+. |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| const globalThis: any = global; |
| |
| /** |
| * Returns an {x, y} position within the element identified by the selector within the root. |
| * By default the position is the center of the bounding box. If the element's bounding box |
| * extends beyond that of a containing element, this position may not correspond to the element. |
| * In this case, specifying maxPixelsFromLeft will constrain the returned point to be close to |
| * the left edge of the bounding box. |
| */ |
| export const getElementPosition = |
| async (selector: string|puppeteer.JSHandle, root?: puppeteer.JSHandle, maxPixelsFromLeft?: number) => { |
| let element; |
| if (typeof selector === 'string') { |
| element = await waitFor(selector, root); |
| } else { |
| element = selector; |
| } |
| |
| const rect = await element.evaluate(element => { |
| if (!element) { |
| return {}; |
| } |
| |
| const {left, top, width, height} = element.getBoundingClientRect(); |
| return {left, top, width, height}; |
| }); |
| |
| if (rect.left === undefined) { |
| throw new Error(`Unable to find element with selector "${selector}"`); |
| } |
| |
| let pixelsFromLeft = rect.width * 0.5; |
| if (maxPixelsFromLeft && pixelsFromLeft > maxPixelsFromLeft) { |
| pixelsFromLeft = maxPixelsFromLeft; |
| } |
| |
| return { |
| x: rect.left + pixelsFromLeft, |
| y: rect.top + rect.height * 0.5, |
| }; |
| }; |
| |
| export const click = async ( |
| selector: string|puppeteer.JSHandle, |
| options?: {root?: puppeteer.JSHandle; clickOptions?: puppeteer.ClickOptions; maxPixelsFromLeft?: number;}) => { |
| const {frontend} = getBrowserAndPages(); |
| const clickableElement = |
| await getElementPosition(selector, options && options.root, options && options.maxPixelsFromLeft); |
| |
| 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(); |
| await frontend.keyboard.type(text); |
| }; |
| |
| export const pressKey = async (key: string, 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, across Shadow DOM boundaries. |
| export const $ = async (selector: string, root?: puppeteer.JSHandle) => { |
| const {frontend} = getBrowserAndPages(); |
| const rootElement = root ? root as puppeteer.ElementHandle : frontend; |
| const element = await rootElement.$('pierceShadow/' + selector); |
| return element; |
| }; |
| |
| // Get multiple element handles, across Shadow DOM boundaries. |
| export const $$ = async (selector: string, root?: puppeteer.JSHandle) => { |
| const {frontend} = getBrowserAndPages(); |
| const rootElement = root ? root.asElement() || frontend : frontend; |
| const elements = await rootElement.$$('pierceShadow/' + 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(); |
| const rootElement = root ? root as puppeteer.ElementHandle : frontend; |
| const element = await rootElement.$('pierceShadowText/' + textContent); |
| return element; |
| }; |
| |
| export const timeout = (duration: number) => new Promise(resolve => setTimeout(resolve, duration)); |
| |
| export const waitFor = async (selector: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope()) => { |
| return await asyncScope.exec(() => waitForFunction(async () => { |
| const element = await $(selector, root); |
| return (element || undefined); |
| }, asyncScope)); |
| }; |
| |
| export const waitForNone = async (selector: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope()) => { |
| return await asyncScope.exec(() => waitForFunction(async () => { |
| const elements = await $$(selector, root); |
| if (elements.length === 0) { |
| return true; |
| } |
| return false; |
| }, asyncScope)); |
| }; |
| |
| export const waitForElementWithTextContent = |
| (textContent: string, root?: puppeteer.JSHandle, asyncScope = new AsyncScope()) => { |
| return asyncScope.exec(() => waitForFunction(async () => { |
| const elem = await $textContent(textContent, root); |
| return elem || undefined; |
| }, asyncScope)); |
| }; |
| |
| export const waitForFunction = async<T>(fn: () => Promise<T|undefined>, asyncScope = new AsyncScope()): Promise<T> => { |
| return await asyncScope.exec(async () => { |
| while (true) { |
| const result = await fn(); |
| if (result) { |
| return result; |
| } |
| await timeout(100); |
| } |
| }); |
| }; |
| |
| 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 enableExperiment = async ( |
| experiment: string, options: {selectedPanel?: {name: string, selector?: string}, canDock?: boolean} = {}) => { |
| const {frontend} = getBrowserAndPages(); |
| await frontend.evaluate(experiment => { |
| // @ts-ignore |
| globalThis.Root.Runtime.experiments.setEnabled(experiment, true); |
| }, experiment); |
| |
| await reloadDevTools(options); |
| }; |
| |
| export const goTo = async (url: string) => { |
| const {target} = getBrowserAndPages(); |
| await target.goto(url); |
| }; |
| |
| export const overridePermissions = async (permissions: puppeteer.Permission[]) => { |
| const {browser} = getBrowserAndPages(); |
| await browser.defaultBrowserContext().overridePermissions( |
| `http://localhost:${getHostedModeServerPort()}`, permissions); |
| }; |
| |
| export const clearPermissionsOverride = async () => { |
| const {browser} = getBrowserAndPages(); |
| await browser.defaultBrowserContext().clearPermissionOverrides(); |
| }; |
| |
| export const goToResource = async (path: string) => { |
| await goTo(`${getResourcesPath()}/${path}`); |
| }; |
| |
| export const getResourcesPath = () => { |
| return `http://localhost:${getHostedModeServerPort()}/test/e2e/resources`; |
| }; |
| |
| export const step = async (description: string, step: Function) => { |
| try { |
| // eslint-disable-next-line no-console |
| console.log(` Running step "${description}"`); |
| 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 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 selectOption = async (select: puppeteer.JSHandle<HTMLSelectElement>, value: string) => { |
| await select.evaluate(async (node, _value) => { |
| node.value = _value; |
| const event = document.createEvent('HTMLEvents'); |
| event.initEvent('change', false, true); |
| node.dispatchEvent(event); |
| }, value); |
| }; |
| |
| export {getBrowserAndPages, getHostedModeServerPort, reloadDevTools}; |