/** * 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 TestFilterOptions{ html?: boolean console?: ('log'|'error'|'warn')[] | true errors?: boolean, // again don't know possible values responses?: boolean, // interesting to see what other values were requested requestsFailed?: boolean, // will probably also be replicated into console errors, but helpful to have if imports werent found } export interface TestOptions { page: string cb: PageTestCallback filterOutput: TestFilterOptions replaceHost: boolean replaceHostWith?: string } const defaultOptions: Partial = { page: 'index.html', cb: async (page: Page)=>{ await page.waitForNetworkIdle({}); }, replaceHost: true, replaceHostWith: `http://localhost`, filterOutput:{ html: true, console: ['log','error','warn'],// TODO: or warning? need to check what possible values are errors: true, // again don't know possible values responses: true, // interesting to see what other values were requested requestsFailed: true, // will probably also be replicated into console errors, but helpful to have if imports werent found } } 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, filterOutput: { ...defaultOptions.filterOutput, ...(opts?.filterOutput), }, }); const { page: path, cb, replaceHost, replaceHostWith, filterOutput } = options; const browser = await puppeteer.launch({ headless: isInDebugMode()? false : 'new', }); const page = await browser.newPage(); let output : TestOutput = { console: [], errors: [], responses: [], requestsFailed: [] }; try{ // Track requests, errors and console page.on('console', message => { let [type, text] = [message.type(), message.text()]; if(replaceHost){ text = text.replaceAll(hostUrl, replaceHostWith!); } if((filterOutput.console)?.includes?.(type) ?? (filterOutput.console === true)){// TODO: add callback option output.console?.push(`[${type}] ${text}`); } }).on('pageerror', ({ message }) => { let text = message; if(replaceHost){ text = text.replaceAll(hostUrl, replaceHostWith!); } if(filterOutput.errors === true) {// TODO add callback option output.errors?.push(text) } }).on('response', response => { let [status, url] = [response.status(), response.url()] if(replaceHost){ url = url.replaceAll(hostUrl, replaceHostWith!); } if(filterOutput.responses === true) {// TODO add callback option 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!); } if(filterOutput.requestsFailed === true) {// TODO add callback option output.requestsFailed?.push(`${failure} ${url}`) } }); const url = new URL(`${hostUrl}/${path??''}`); 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; }finally{ if(isInDebugMode()){ 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(); } return output; }