UNPKG

@exodus/test

Version:
138 lines (119 loc) 5.29 kB
import assert from 'node:assert/strict' import { spawnSync } from 'node:child_process' import { readFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { findBinary } from './find-binary.js' // See https://playwright.dev/docs/browsers // > Playwright doesn't work with the branded version of Firefox since it relies on patches. // > Playwright doesn't work with the branded version of Safari since it relies on patches. let puppeteer let playwright const puppeteerBrowsers = { brave: 'chrome', msedge: 'chrome' } const playwrightBrowsers = { chrome: 'chromium', msedge: 'chromium' } const launched = Object.create(null) const launchers = { async puppeteer({ binary, devtools }) { if (!puppeteer) puppeteer = await import('puppeteer-core') const browser = Object.hasOwn(puppeteerBrowsers, binary) ? puppeteerBrowsers[binary] : binary assert(['chrome', 'firefox'].includes(browser)) return puppeteer.launch({ executablePath: findBinary(binary), browser, devtools }) }, async playwright({ binary: channel, devtools }) { if (!playwright) playwright = await import('playwright-core') const type = Object.hasOwn(playwrightBrowsers, channel) ? playwrightBrowsers[channel] : channel assert(['chromium', 'firefox', 'webkit'].includes(type) && Object.hasOwn(playwright, type)) return playwright[type].launch({ devtools, channel }) }, } export const close = () => Promise.all(Object.values(launched).map((p) => p.then((b) => b.close()))) async function newPage(runner, browser, { binary, dropNetwork }) { const context = await (browser.newContext ? browser.newContext() : browser.createBrowserContext()) if (dropNetwork && context.setOffline && binary !== 'webkit') await context.setOffline(true) // WebKit crashes if this is done prior to navigation to /dev/null let page try { page = await context.newPage() } catch (err) { // Puppeteer has a bug with Firefox, we expect that and just retry if (runner !== 'puppeteer' || binary !== 'firefox' || err.name !== 'ProtocolError') throw err await context.close() return newPage(runner, browser, { binary, dropNetwork }) } // Need to load a secure origin for e.g. crypto.subtle to be available if (runner === 'playwright' && binary === 'webkit') { // Can attempt to download /dev/null, so we apply a work-around await page.route('https://www.secure-context-top-level-domain-for-tests/*', (route) => route.fulfill({ status: 200, contentType: 'text/html; charset=utf-8', body: '<!doctype html><html><body></body></html>', }) ) await page.goto('https://www.secure-context-top-level-domain-for-tests/') } else { await page.goto('file:///dev/null') } if (dropNetwork && context.setOffline) await context.setOffline(true) if (dropNetwork && page.setOfflineMode) await page.setOfflineMode(true) assert(!dropNetwork || context.setOffline || page.setOfflineMode) return { context, page } } export async function run(runner, args, { binary, devtools, dropNetwork, timeout, throttle }) { assert(args.length === 1, 'Unexpected args to browser runner') const bundle = await readFile(args[0], 'utf8') let code = 0 const [stdout, stderr] = [[], []] assert(Object.hasOwn(launchers, runner), 'Unexpected runner') if (!launched[runner]) launched[runner] = launchers[runner]({ binary, devtools: !!devtools }) const { page, context } = await newPage(runner, await launched[runner], { binary, dropNetwork }) if (throttle) { try { const cdp = await (page.createCDPSession ? page.createCDPSession() : context.newCDPSession(page)) await cdp.send('Emulation.setCPUThrottlingRate', { rate: throttle }) } catch (cause) { throw new Error(`${binary}:${runner} engine does not support --throttle-cpu`, { cause }) } } page.on('console', (message) => { const type = message.type() const target = type === 'error' ? stderr : stdout target.push(message.text()) }) page.on('pageerror', (error) => { if (!code) code = 1 stderr.push(`${error}`) }) let timer const promise = new Promise((resolve) => { timer = setTimeout(() => { stderr.push('timeout reached') resolve(1) // Error code }, timeout) }) const wait = async () => { await page.evaluate(bundle) return page.evaluate('globalThis.EXODUS_TEST_PROMISE') } try { // exitCode might be undefined if we failed before EXODUS_TEST_PROMISE was set, but we will have code then const exitCode = await Promise.race([wait(), promise]) code = code || exitCode if (!Number.isInteger(code)) { stderr.push('Browser test did not indicate completion. Terminating with a failure...') code = 1 } return { code, stdout: stdout.join('\n'), stderr: stderr.join('\n') } } catch (error) { return { code: 1, stdout: '', stderr: `${error}` } } finally { clearTimeout(timer) await context.close() } } export function runPlaywrightCommand(args) { const playwright = dirname(fileURLToPath(import.meta.resolve('playwright-core/package.json'))) const cli = resolve(playwright, 'cli.js') return spawnSync(cli, args, { stdio: 'inherit' }) }