/** * Puppeteer + from-memory devServer rollup plugin to open the result in a webpage en output the result * (after an optional series of commands to the puppeteer Page) */ import puppeteer, {Page} from "puppeteer"; import {fileURLToPath, URL} from "node:url"; import {isInDebugMode} from "./debug-mode.ts"; export type PageTestCallback = (page: Page)=>Promise; export interface TestOptions { path: string cb: PageTestCallback replaceHost: boolean replaceHostWith?: string } const defaultOptions: Partial = { path: 'index.html', cb: async (page: Page)=>{ await page.waitForNetworkIdle({}); }, replaceHost: true, replaceHostWith: `http://localhost`, } export interface TestOutput{ html: string, console: string[], errors: string[], responses: string[], requestsFailed: string[], } /** * Opens a page in a puppeteer browser and return the resulting HTML and logmessages produced. * Optionally a callback can be provided to simulate user interactions on the page before returning the HTML * When DEBUG mode is detected, puppeteer headless mode will be disabled allowing you to inspect the page if you set a breakpoint * * @param opts * @param hostUrl */ export async function puppeteerRunTest(opts: Partial, hostUrl: string){ const options : TestOptions = ({ ...defaultOptions, ...opts, }); const { path, cb, replaceHost, replaceHostWith, } = options; const browser = await puppeteer.launch({ headless: isInDebugMode()? false : 'new', }); const page = await browser.newPage(); let output : TestOutput = { html: '', console: [], errors: [], responses: [], requestsFailed: [] }; let errored = false; try { // Track requests, errors and console page.on('console', message => { let [type, text] = [message.type(), message.text()]; if (replaceHost) { text = text.replaceAll(hostUrl, replaceHostWith!); } output.console?.push(`[${type}] ${text}`); }).on('pageerror', ({message}) => { let text = message; if (replaceHost) { text = text.replaceAll(hostUrl, replaceHostWith!); } output.errors?.push(text); }).on('response', response => { let [status, url] = [response.status(), response.url()] if (replaceHost) { url = url.replaceAll(hostUrl, replaceHostWith!); } output.responses?.push(`${status} ${url}`); }).on('requestfailed', request => { let [failure, url] = [request.failure()?.errorText, request.url()]; if (replaceHost) { failure = failure?.replaceAll(hostUrl, replaceHostWith!); url = url.replaceAll(hostUrl, replaceHostWith!); } output.requestsFailed?.push(`${failure} ${url}`); }); const url = new URL(path??'', hostUrl); await page.goto(url.href); if (!cb) { await page.waitForNetworkIdle({}); } else { await cb(page); } const htmlHandle = await page.$('html'); const html = await page.evaluate(html => html?.outerHTML ?? html?.innerHTML, htmlHandle); // Add the final html output.html = html || ''; return output; }catch(err){ errored = true; throw err; }finally{ if(isInDebugMode() && !errored){ console.log(`DEBUG MODE ENABLED, Close the puppeteer browsertab to continue!\n${import.meta.url}:144`); await new Promise((resolve)=>{ page.on('close', ()=>{ console.log("Page closed"); resolve(null); }) }); }else{ await page.close(); } await browser.close(); } }