UNPKG

jest-environment-puppeteer

Version:
269 lines (259 loc) 8.83 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var NodeEnvironment = require('jest-environment-node'); var cosmiconfig = require('cosmiconfig'); var chalk = require('chalk'); require('node:os'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; } var NodeEnvironment__default = /*#__PURE__*/_interopDefaultCompat(NodeEnvironment); var chalk__default = /*#__PURE__*/_interopDefaultCompat(chalk); const DEFAULT_CONFIG = { browserContext: "default", exitOnPageError: true }; const explorer = cosmiconfig.cosmiconfig("jest-puppeteer"); const getDefaultConfig = ()=>{ if (process.env.CI) { return { ...DEFAULT_CONFIG, launch: { args: [ "--no-sandbox", "--disable-setuid-sandbox", "--disable-background-timer-throttling", "--disable-backgrounding-occluded-windows", "--disable-renderer-backgrounding" ] } }; } return DEFAULT_CONFIG; }; const readConfigFile = async (configFile, searchFrom)=>{ if (!configFile) { const result = await explorer.search(searchFrom); return result ? result.config : null; } const result = await explorer.load(configFile); return result ? result.config : null; }; const readConfig = async (searchFrom)=>{ const defaultConfig = getDefaultConfig(); const config = await readConfigFile(process.env.JEST_PUPPETEER_CONFIG, searchFrom); if (!config) return defaultConfig; return { ...defaultConfig, ...config }; }; const CTRL_C = "\u0003"; const CTRL_D = "\u0004"; const ENTER = "\r"; const blockStdin = ()=>{ console.log(chalk__default.default.blue("\n\n🕵️‍ Code is paused, press enter to resume")); return new Promise((resolve)=>{ const { stdin } = process; const onKeyPress = (key)=>{ if (key === CTRL_C || key === CTRL_D || key === ENTER) { stdin.removeListener("data", onKeyPress); if (!listening) { if (stdin.isTTY) { stdin.setRawMode(false); } stdin.pause(); } resolve(); } }; const listening = stdin.listenerCount("data") > 0; if (!listening) { if (stdin.isTTY) { stdin.setRawMode(true); } stdin.resume(); stdin.setEncoding("utf8"); } stdin.on("data", onKeyPress); }); }; const getPuppeteer = ()=>{ try { // eslint-disable-next-line @typescript-eslint/no-require-imports return require("puppeteer"); } catch { // eslint-disable-next-line @typescript-eslint/no-require-imports return require("puppeteer-core"); } }; const readWorkersCount = ()=>{ if (!process.env.WORKERS_COUNT) { throw new Error("Invariant: WORKERS_COUNT not found"); } return Number(process.env.WORKERS_COUNT); }; const readWsEndpoints = ()=>{ if (!process.env.PUPPETEER_WS_ENDPOINTS) { throw new Error("Invariant: PUPPETEER_WS_ENDPOINTS not found"); } return JSON.parse(process.env.PUPPETEER_WS_ENDPOINTS); }; const getJestWorkerId = ()=>{ if (!process.env.JEST_WORKER_ID) { throw new Error("Invariant: JEST_WORKER_ID not found"); } return Number(process.env.JEST_WORKER_ID); }; const getWorkerIndex = ()=>{ // Jest worker ID starts at 1 return getJestWorkerId() - 1; }; const getWorkerWsEndpointIndex = ()=>{ return Math.min(readWorkersCount() - 1, getWorkerIndex()); }; const getWorkerWsEndpoint = ()=>{ const wsEndpoints = readWsEndpoints(); const index = getWorkerWsEndpointIndex(); const endpoint = wsEndpoints[index]; if (!endpoint) { throw new Error("Invariant: worker WS endpoint not found"); } return endpoint; }; const connectBrowserFromWorker = async (config)=>{ const wsEndpoint = getWorkerWsEndpoint(); const puppeteer = getPuppeteer(); return puppeteer.connect({ ...config.connect, ...config.launch, browserURL: undefined, browserWSEndpoint: wsEndpoint }); }; const testTimeoutSymbol = Symbol.for("TEST_TIMEOUT_SYMBOL"); const handlePageError = (error)=>{ process.emit("uncaughtException", error); }; const getBrowser = (global)=>{ if (!global.browser) { throw new Error("Cannot access browser before launching browser."); } return global.browser; }; const getContext = (global)=>{ if (!global.context) { throw new Error("Cannot access context before launching context."); } return global.context; }; const connectBrowser = async (global)=>{ if (global.browser) { throw new Error("Cannot connect browser before closing previous browser."); } global.browser = await connectBrowserFromWorker(global.puppeteerConfig); }; const disconnectBrowser = async (global)=>{ if (!global.browser) return; await global.browser.disconnect(); global.browser = undefined; }; const getPage = (global)=>{ if (!global.page) { throw new Error("Cannot access page before launching browser."); } return global.page; }; const openPage = async (global)=>{ if (global.page) { throw new Error("Cannot open page before closing previous page."); } const page = await getContext(global).newPage(); if (global.puppeteerConfig.exitOnPageError) { page.on("pageerror", handlePageError); } global.page = page; }; const closePage = async (global)=>{ if (!global.page) return; if (global.puppeteerConfig.exitOnPageError) { global.page.off("pageerror", handlePageError); } await global.page.close({ runBeforeUnload: Boolean(global.puppeteerConfig.runBeforeUnloadOnClose) }); global.page = undefined; }; const createContext = async (global)=>{ if (global.context) { throw new Error("Cannot create context before closing previous context."); } const configBrowserContext = global.puppeteerConfig.browserContext ?? "default"; const browser = getBrowser(global); switch(configBrowserContext){ case "default": global.context = browser.defaultBrowserContext(); break; case "incognito": global.context = "createBrowserContext" in browser ? await browser.createBrowserContext() : await browser.createIncognitoBrowserContext(); break; default: throw new Error(`browserContext should be either 'incognito' or 'default'. Received '${configBrowserContext}'`); } }; const closeContext = async (global)=>{ if (!global.context) return; const browser = getBrowser(global); // If a custom context was created, close it if (global.context !== browser.defaultBrowserContext()) { await global.context.close(); } global.context = undefined; }; const initAll = async (global)=>{ await connectBrowser(global); await createContext(global); await openPage(global); }; const closeAll = async (global)=>{ await closePage(global); await closeContext(global); await disconnectBrowser(global); }; class PuppeteerEnvironment extends NodeEnvironment__default.default { constructor(config, _context){ super(config, _context); this.global.puppeteerConfig = {}; this.global.jestPuppeteer = { debug: async ()=>{ // Set timeout to 4 days this.setTimeout(345600000); // Run a debugger (in case Puppeteer has been launched with `{ devtools: true }`) await getPage(this.global).evaluate(()=>{ debugger; }); return blockStdin(); }, resetPage: async ()=>{ await closePage(this.global); await openPage(this.global); }, resetBrowser: async ()=>{ await closeAll(this.global); await initAll(this.global); } }; } // Jest is not available here, so we have to reverse engineer // the setTimeout function, see https://github.com/facebook/jest/blob/ffe2352c781703b427fab10777043fb76d0d4267/packages/jest-runtime/src/index.ts#L2331 setTimeout(timeout) { this.global[testTimeoutSymbol] = timeout; } async setup() { this.global.puppeteerConfig = await readConfig(); await initAll(this.global); } async teardown() { await closeAll(this.global); } } exports.TestEnvironment = PuppeteerEnvironment; exports.default = PuppeteerEnvironment;