From 3b540d0c4878dfa5c75f8bbf394aa3c12720bdc4 Mon Sep 17 00:00:00 2001 From: Miel Truyen Date: Sat, 17 Feb 2024 21:12:54 +0100 Subject: [PATCH] chore: reowrked tests to use a runBrowserTest to allow previewing the results in a browser --- test/evaluated-web-bundle/test.js | 35 ++++---- test/jsx-web-app/test.js | 37 ++++----- test/rewrite-url/test.js | 48 +++++++---- test/util/browser-test.ts | 129 ++++++++++++++++++++++++++++++ test/util/index.ts | 4 +- test/util/puppeteer-run-test.ts | 87 ++++++++------------ test/util/serve-test.ts | 46 ++++++----- 7 files changed, 254 insertions(+), 132 deletions(-) create mode 100644 test/util/browser-test.ts diff --git a/test/evaluated-web-bundle/test.js b/test/evaluated-web-bundle/test.js index fe18652..7292267 100644 --- a/test/evaluated-web-bundle/test.js +++ b/test/evaluated-web-bundle/test.js @@ -5,22 +5,11 @@ import { rollup } from "rollup"; import urlPlugin from "@rollup/plugin-url"; import html from "../../src/index.ts"; -import serveTest from "../util/serve-test.ts"; - -/** - * @type {OutputOptions} - */ -const output= { - dir: 'output', // Output all files - format: 'es', // iifi and cjs should be added to tests - sourcemap: true,// Test if #sourcemapUrl is not accidentally included in the html-output - chunkFileNames: '[name].js', - entryFileNames: '[name].[extname]', - assetFileNames: '[name].[extname]', -}; +import {runBrowserTest} from "../util/browser-test.ts"; import {fileURLToPath} from "node:url"; import handlebars from "handlebars"; + const __dirname = dirname(fileURLToPath(import.meta.url)); process.chdir(join(__dirname, 'fixtures')); @@ -32,7 +21,7 @@ const defaultAssetInclude = [ ]; test.serial('web-bundle', async (t) => { - const bundle = await rollup({ + const out = await runBrowserTest({ input: 'index.hbs', treeshake: 'smallest', plugins: [ @@ -46,13 +35,19 @@ test.serial('web-bundle', async (t) => { urlPlugin({ include: defaultAssetInclude, }), - - serveTest({ - path: 'index.html', - t, - }) ], + }, { + path: 'index.html', + log: t.log, + },{ + dir: 'output', // Output all files + format: 'es', // iifi and cjs should be added to tests + sourcemap: true,// Test if #sourcemapUrl is not accidentally included in the html-output + chunkFileNames: '[name].js', + entryFileNames: '[name].[extname]', + assetFileNames: '[name].[extname]', }); - await bundle.generate(output); + t.snapshot(out); + // await bundle.generate(output); }); diff --git a/test/jsx-web-app/test.js b/test/jsx-web-app/test.js index b0897c5..4ff5123 100644 --- a/test/jsx-web-app/test.js +++ b/test/jsx-web-app/test.js @@ -12,25 +12,11 @@ import typescriptPlugin from "@rollup/plugin-typescript"; import replacePlugin from "@rollup/plugin-replace"; import html from "../../src/index.ts"; -import serveTest from "../util/serve-test.ts"; - - - -/** - * @type {OutputOptions} - */ -const output= { - dir: 'output', // Output all files - format: 'es', // iifi and cjs should be added to tests - sourcemap: true,// Test if #sourcemapUrl is not accidentally included in the html-output - chunkFileNames: '[name].js', - entryFileNames: '[name].[extname]', - assetFileNames: '[name].[extname]', -}; +import {runBrowserTest} from "../util/browser-test.ts"; import {fileURLToPath} from "node:url"; import handlebars from "handlebars"; -import {debugPrintOutput, getCode} from "../util/index.ts"; +// import {debugPrintOutput, getCode, runBrowserTest} from "../util/index.ts"; const __dirname = dirname(fileURLToPath(import.meta.url)); process.chdir(join(__dirname, 'fixtures')); @@ -42,7 +28,7 @@ const defaultAssetInclude = [ ]; test.serial('web-bundle', async (t) => { - const bundle = await rollup({ + const out = await runBrowserTest({ input: 'index.hbs', treeshake: 'smallest', plugins: [ @@ -79,14 +65,19 @@ test.serial('web-bundle', async (t) => { urlPlugin({ include: defaultAssetInclude, }), - - serveTest({ - path: 'index.html', - t, - }) ], + }, { + path: 'index.html', + log: t.log, + },{ + dir: 'output', // Output all files + format: 'es', // iifi and cjs should be added to tests + sourcemap: true,// Test if #sourcemapUrl is not accidentally included in the html-output + chunkFileNames: '[name].js', + entryFileNames: '[name].[extname]', + assetFileNames: '[name].[extname]', }); - const generated = await bundle.generate(output); + t.snapshot(out); // const code = await getCode(bundle, output); // debugPrintOutput('jsx-web-app',code); diff --git a/test/rewrite-url/test.js b/test/rewrite-url/test.js index 236ecd3..5faffc8 100644 --- a/test/rewrite-url/test.js +++ b/test/rewrite-url/test.js @@ -1,24 +1,17 @@ import {resolve, join, dirname} from "node:path"; -import * as path from "node:path"; import test from "ava"; -import { rollup } from "rollup"; -import {debugPrintOutput, getCode} from "../util/index.ts"; +import {runBrowserTest} from "../util/index.ts"; import html from "../../src/index.ts"; -const output = { - dir: 'output', // Output all files - format: 'es', // iifi and cjs should be added to tests - sourcemap: true,// Test if #sourcemapUrl is not accidentally included in the html-output -}; - import {fileURLToPath} from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); process.chdir(join(__dirname, 'fixtures')); test.serial('rewrite-url', async (t) => { - const bundle = await rollup({ + + const out = await runBrowserTest({ input: { ['index']: 'index.html', ['admin/index']: resolve(__dirname,'fixtures','admin/index.html'), @@ -30,11 +23,38 @@ test.serial('rewrite-url', async (t) => { return `/${rootPath}`; } }), - ] + ], + },{ + log: t.log, + filterOutput:{ + // TODO: Currently only need the "await getCode(bundle, output);" as output + }, + path: '/admin' + }, { + dir: 'output', // Output all files + format: 'es', // iifi and cjs should be added to tests + sourcemap: true,// Test if #sourcemapUrl is not accidentally included in the html-output }); - const code = await getCode(bundle, output); - debugPrintOutput('rewrite-url',code); - t.snapshot(code); + + t.snapshot(out.code); // Snapshot the result code + + // const bundle = await rollup({ + // input: { + // ['index']: 'index.html', + // ['admin/index']: resolve(__dirname,'fixtures','admin/index.html'), + // ['admin/app']: resolve(__dirname,'fixtures','admin/app.js'), + // }, + // plugins: [ + // html({ + // rewriteUrl(relative, {rootPath, from}){ + // return `/${rootPath}`; + // } + // }), + // ] + // }); + // const code = await getCode(bundle, output); + // debugPrintOutput('rewrite-url',code); + // t.snapshot(code); }); // TODO various parameters diff --git a/test/util/browser-test.ts b/test/util/browser-test.ts new file mode 100644 index 0000000..20c3a26 --- /dev/null +++ b/test/util/browser-test.ts @@ -0,0 +1,129 @@ +import {Plugin, InputPluginOption, RollupOptions, OutputOptions, RollupOutput} from "rollup"; +import {TestOptions as BrowserTestOptions, TestOutput as PuppeteerTestOutput} from "./puppeteer-run-test.js"; +import { rollup } from "rollup"; +import serveTest, {LogCallback} from "./serve-test.js"; +import type {ExecutionContext} from "ava"; +import {getCode, TestOutput} from "./code-output.ts"; + + +// /** +// * The AVA context used to test (ie t.snapshot(..) ) +// */ +// t: ExecutionContext +// +// +// 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 +// } + + +// 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}`) +// } +// }); + +// testOptions.t?.snapshot?.(testOutput); + +export interface OutputFilterOptions { + 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 BrowserTestInput extends BrowserTestOptions{ + log?: LogCallback; + /** + * Optionally specify what to filter from the output + */ + filterOutput?: OutputFilterOptions; +} + + +export interface BrowserTestOutput extends PuppeteerTestOutput{ + code: TestOutput[]; +} + +export async function runBrowserTest( + build: RollupOptions, + test?: BrowserTestInput | false, + output?: OutputOptions +) : Promise>{ + const resolvedPlugins = await Promise.resolve(build.plugins||null); + let pluginsArray : InputPluginOption[] = []; + if(resolvedPlugins && resolvedPlugins instanceof Array){ + pluginsArray = resolvedPlugins + }else if(resolvedPlugins){ + pluginsArray = [resolvedPlugins]; + } + + let testOutput: Partial = {}; + const bundle = await rollup({ + ...build, + plugins: [ + ...pluginsArray, + // TODO check if browser output is requested (either for snapshot or for testing) + ...(test? [serveTest({ + // TODO: intercept output from the serveTest? (and include as one bit in output options below, for snapshotting) + ...test, + log: test.log ?? console.log, + onResult: (output)=>{ + testOutput = {...testOutput, ...output}; + } + })]: []) + ] + }); + + // TODO make configurable? + const generated = await bundle.generate({ + dir: 'output', // Output all files + format: 'es', // iifi and cjs should be added to tests + sourcemap: true,// Test if #sourcemapUrl is not accidentally included in the html-output + chunkFileNames: '[name].js', + entryFileNames: '[name].mjs', + assetFileNames: '[name].[extname]', + }); + + if(output){ + testOutput.code = await getCode(bundle, output); + } + + return testOutput + +} diff --git a/test/util/index.ts b/test/util/index.ts index f838874..ce9b7f2 100644 --- a/test/util/index.ts +++ b/test/util/index.ts @@ -1,6 +1,8 @@ +// TODO: this should be the main module used, other should be imported manually if exceptions are needed? +export * from "./browser-test.ts"; + export * from "./code-output.ts"; export * from "./print-code-output.ts"; export * from "./serve-test.ts"; - // export * from './misc.js'; diff --git a/test/util/puppeteer-run-test.ts b/test/util/puppeteer-run-test.ts index f1281f7..9ceb917 100644 --- a/test/util/puppeteer-run-test.ts +++ b/test/util/puppeteer-run-test.ts @@ -11,44 +11,28 @@ 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 + path: string cb: PageTestCallback - filterOutput: TestFilterOptions replaceHost: boolean replaceHostWith?: string } const defaultOptions: Partial = { - page: 'index.html', + path: '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[], + 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 @@ -61,17 +45,12 @@ export async function puppeteerRunTest(opts: Partial, hostUrl: stri const options : TestOptions = ({ ...defaultOptions, ...opts, - filterOutput: { - ...defaultOptions.filterOutput, - ...(opts?.filterOutput), - }, }); const { - page: path, + path, cb, replaceHost, replaceHostWith, - filterOutput } = options; const browser = await puppeteer.launch({ @@ -80,64 +59,64 @@ export async function puppeteerRunTest(opts: Partial, hostUrl: stri const page = await browser.newPage(); let output : TestOutput = { + html: '', console: [], errors: [], responses: [], requestsFailed: [] }; - try{ + let errored = false; + + try { // Track requests, errors and console page.on('console', message => { let [type, text] = [message.type(), message.text()]; - if(replaceHost){ + 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 }) => { + output.console?.push(`[${type}] ${text}`); + }).on('pageerror', ({message}) => { let text = message; - if(replaceHost){ + if (replaceHost) { text = text.replaceAll(hostUrl, replaceHostWith!); } - if(filterOutput.errors === true) {// TODO add callback option - output.errors?.push(text) - } + output.errors?.push(text); }).on('response', response => { let [status, url] = [response.status(), response.url()] - if(replaceHost){ + if (replaceHost) { url = url.replaceAll(hostUrl, replaceHostWith!); } - if(filterOutput.responses === true) {// TODO add callback option - output.responses?.push(`${status} ${url}`) - } + output.responses?.push(`${status} ${url}`); }).on('requestfailed', request => { let [failure, url] = [request.failure()?.errorText, request.url()]; - if(replaceHost){ + 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}`) - } + output.requestsFailed?.push(`${failure} ${url}`); }); - const url = new URL(`${hostUrl}/${path??''}`); + const url = new URL(path??'', hostUrl); await page.goto(url.href); - if(!cb) { + if (!cb) { await page.waitForNetworkIdle({}); - }else{ + } else { await cb(page); } const htmlHandle = await page.$('html'); - const html = await page.evaluate(html => html?.outerHTML??html?.innerHTML, htmlHandle); + const html = await page.evaluate(html => html?.outerHTML ?? html?.innerHTML, htmlHandle); // Add the final html - output.html = html; + output.html = html || ''; + + return output; + }catch(err){ + errored = true; + throw err; }finally{ - if(isInDebugMode()){ + 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', ()=>{ @@ -148,9 +127,7 @@ export async function puppeteerRunTest(opts: Partial, hostUrl: stri }else{ await page.close(); } - await browser.close(); } - return output; } diff --git a/test/util/serve-test.ts b/test/util/serve-test.ts index b074263..61c66b4 100644 --- a/test/util/serve-test.ts +++ b/test/util/serve-test.ts @@ -4,7 +4,7 @@ */ -import {puppeteerRunTest, TestFilterOptions, PageTestCallback} from "./puppeteer-run-test.ts"; +import {puppeteerRunTest, PageTestCallback, TestOutput} from "./puppeteer-run-test.ts"; import {isInDebugMode} from "./debug-mode.ts"; import {resolve, posix} from "node:path"; @@ -28,7 +28,7 @@ import type { } from 'http' import type { ServerOptions } from 'https' -import type {ExecutionContext} from "ava"; +import test, {ExecutionContext} from "ava"; import {createReadStream} from "fs"; @@ -37,32 +37,27 @@ type TypeMap = { }; type ErrorCodeException = Error & {code: string}; +export type TestResultCallback = (output: TestOutput)=>void; +export type LogCallback = (...args: string[])=>void; -export interface RollupServeTestOptions { + +export interface ServeTestOptions { /** * Change the path to be opened when the test is started * Remember to start with a slash, e.g. `'/different/page'` */ path?: string - /** - * Optionally specify what to filter from the output - */ - filterOutput?: TestFilterOptions; /** - * Fallback to serving from a specified srcDir + * Fallback to serving from a specified srcDir, this allows setting breakpoints on sourcecode and test the sourcemaps */ srcDir?: string|boolean; /** * A callback to manually take control of the page and simulate user interactions */ - cb?: PageTestCallback - /** - * The AVA context used to test (ie t.snapshot(..) ) - */ - t: ExecutionContext + cb?: PageTestCallback; /** * Set to `true` to return index.html (200) instead of error page (404) @@ -105,6 +100,20 @@ export interface RollupServeTestOptions { * Execute function after server has begun listening */ onListening?: (server: Server) => void + + +} + +export interface RollupServeTestOptions extends ServeTestOptions{ + /** + * A callback to run when a test has been run + */ + onResult?: TestResultCallback; + + /** + * Callback to log messages + */ + log?: LogCallback; } @@ -137,7 +146,7 @@ export default function serveTest (options: RollupServeTestOptions ): Plugin { info: 34, warn: 33, }[mode]; - testOptions.t.log(`\u001b[${modeColor}m${msg}\u001b[0m`); + testOptions.log?.(`\u001b[${modeColor}m${msg}\u001b[0m`); } const requestListener = async (request: IncomingMessage, response: ServerResponse) => { @@ -266,11 +275,10 @@ export default function serveTest (options: RollupServeTestOptions ): Plugin { first = false const testOutput = await puppeteerRunTest({ - page: testOptions.path!, - cb: testOptions.cb, - filterOutput: testOptions.filterOutput, - }, url) - testOptions.t?.snapshot?.(testOutput); + ...testOptions + }, url); + + testOptions.onResult?.(testOutput); } } },