test: added testing via a browser (puppeteer)

This commit is contained in:
2023-12-03 10:42:51 +01:00
parent 2adfbee74b
commit e96c2248ee
27 changed files with 1281 additions and 123 deletions

View File

@@ -2,7 +2,7 @@ import {join, dirname} from "node:path";
import test from "ava";
import { rollup } from "rollup";
import {debugPrintOutput, getCode} from "../util/test.js";
import {debugPrintOutput, getCode} from "../util/index.ts";
import html from "../../src/index.ts";
@@ -25,7 +25,7 @@ test.serial('simple', async (t) => {
}),
]
});
const code = await getCode(bundle, output, true);
const code = await getCode(bundle, output);
debugPrintOutput('simple',code);
t.snapshot(code);
});
@@ -38,7 +38,7 @@ test.serial('inline-script', async (t) => {
}),
]
});
const code = await getCode(bundle, output, true);
const code = await getCode(bundle, output);
debugPrintOutput('inline-script',code);
t.snapshot(code);
});

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<title>HTML5 Logo</title>
<path d="M108.4 0h23v22.8h21.2V0h23v69h-23V46h-21v23h-23.2M206 23h-20.3V0h63.7v23H229v46h-23M259.5 0h24.1l14.8 24.3L313.2 0h24.1v69h-23V34.8l-16.1 24.8l-16.1-24.8v34.2h-22.6M348.7 0h23v46.2h32.6V69h-55.6"/>
<path fill="#e44d26" d="M107.6 471l-33-370.4h362.8l-33 370.2L255.7 512"/>
<path fill="#f16529" d="M256 480.5V131H404.3L376 447"/>
<path fill="#ebebeb" d="M142 176.3h114v45.4h-64.2l4.2 46.5h60v45.3H154.4M156.4 336.3H202l3.2 36.3 50.8 13.6v47.4l-93.2-26"/>
<path fill="#fff" d="M369.6 176.3H255.8v45.4h109.6M361.3 268.2H255.8v45.4h56l-5.3 59-50.7 13.6v47.2l93-25.8"/>
</svg>

After

Width:  |  Height:  |  Size: 693 B

View File

@@ -0,0 +1 @@
export const batman = 'bum badum badum baaaaa dum!';

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>
Test bundle!
</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="./assets/logo-sq.svg">
{{{ head }}}
</head>
<body>
<div id="root">Here the app should load!</div>
<script src="./index.js" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,27 @@
// Dynamically loads libraries and bootstraps the application
(async ()=>{
// Add a loader here if any
const root = document.getElementById('root')
if(root) root.innerHTML= `<div style="align-self: center">My app has loaded!!</div>`;
try {
// Load app
const [
appModule,
] = await Promise.all([
import("./batman.js"),
]);
console.log("Bootstrapped, ready to go!");
// Wait for DOM to be ready
if(document.readyState === 'loading') {
await new Promise((resolve)=>document.addEventListener('DOMContentLoaded', resolve));
}
// Start the app!
root.innerHTML = `<div style="align-self: center"><b>${appModule.batman}</b></div>`;
}catch(err){
console.error(err);
}
})()

View File

@@ -0,0 +1,73 @@
# Snapshot report for `test/evaluated-web-bundle/test.js`
The actual snapshot is saved in `test.js.snap`.
Generated by [AVA](https://avajs.dev).
## web-bundle
> Snapshot 1
{
console: [
'[log] Bootstrapped, ready to go!',
],
errors: [],
html: `<html lang="en"><head>␊
<meta charset="UTF-8">␊
<title>␊
Test bundle!␊
</title>␊
<meta name="viewport" content="width=device-width, initial-scale=1">␊
<link rel="icon" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%20%20%20%20%3Ctitle%3EHTML5%20Logo%3C%2Ftitle%3E%20%20%20%20%3Cpath%20d%3D%22M108.4%200h23v22.8h21.2V0h23v69h-23V46h-21v23h-23.2M206%2023h-20.3V0h63.7v23H229v46h-23M259.5%200h24.1l14.8%2024.3L313.2%200h24.1v69h-23V34.8l-16.1%2024.8l-16.1-24.8v34.2h-22.6M348.7%200h23v46.2h32.6V69h-55.6%22%2F%3E%20%20%20%20%3Cpath%20fill%3D%22%23e44d26%22%20d%3D%22M107.6%20471l-33-370.4h362.8l-33%20370.2L255.7%20512%22%2F%3E%20%20%20%20%3Cpath%20fill%3D%22%23f16529%22%20d%3D%22M256%20480.5V131H404.3L376%20447%22%2F%3E%20%20%20%20%3Cpath%20fill%3D%22%23ebebeb%22%20d%3D%22M142%20176.3h114v45.4h-64.2l4.2%2046.5h60v45.3H154.4M156.4%20336.3H202l3.2%2036.3%2050.8%2013.6v47.4l-93.2-26%22%2F%3E%20%20%20%20%3Cpath%20fill%3D%22%23fff%22%20d%3D%22M369.6%20176.3H255.8v45.4h109.6M361.3%20268.2H255.8v45.4h56l-5.3%2059-50.7%2013.6v47.2l93-25.8%22%2F%3E%3C%2Fsvg%3E">␊
<title>I'm cool!</title>␊
</head>␊
<body>␊
<div id="root"><div style="align-self: center"><b>bum badum badum baaaaa dum!</b></div></div>␊
<script src="index.js" type="module"></script>␊
</body></html>`,
requestsFailed: [],
responses: [
'200 http://localhost/index.html',
'200 http://localhost/index.js',
'200 http://localhost/batman.js',
],
}
## copied-assets
> Snapshot 1
{
console: [
'[log] Bootstrapped, ready to go!',
],
errors: [],
html: `<html lang="en"><head>␊
<meta charset="UTF-8">␊
<title>␊
Test bundle!␊
</title>␊
<meta name="viewport" content="width=device-width, initial-scale=1">␊
<link rel="icon" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%20%20%20%20%3Ctitle%3EHTML5%20Logo%3C%2Ftitle%3E%20%20%20%20%3Cpath%20d%3D%22M108.4%200h23v22.8h21.2V0h23v69h-23V46h-21v23h-23.2M206%2023h-20.3V0h63.7v23H229v46h-23M259.5%200h24.1l14.8%2024.3L313.2%200h24.1v69h-23V34.8l-16.1%2024.8l-16.1-24.8v34.2h-22.6M348.7%200h23v46.2h32.6V69h-55.6%22%2F%3E%20%20%20%20%3Cpath%20fill%3D%22%23e44d26%22%20d%3D%22M107.6%20471l-33-370.4h362.8l-33%20370.2L255.7%20512%22%2F%3E%20%20%20%20%3Cpath%20fill%3D%22%23f16529%22%20d%3D%22M256%20480.5V131H404.3L376%20447%22%2F%3E%20%20%20%20%3Cpath%20fill%3D%22%23ebebeb%22%20d%3D%22M142%20176.3h114v45.4h-64.2l4.2%2046.5h60v45.3H154.4M156.4%20336.3H202l3.2%2036.3%2050.8%2013.6v47.4l-93.2-26%22%2F%3E%20%20%20%20%3Cpath%20fill%3D%22%23fff%22%20d%3D%22M369.6%20176.3H255.8v45.4h109.6M361.3%20268.2H255.8v45.4h56l-5.3%2059-50.7%2013.6v47.2l93-25.8%22%2F%3E%3C%2Fsvg%3E">␊
<title>I'm cool!</title>␊
</head>␊
<body>␊
<div id="root"><div style="align-self: center"><b>bum badum badum baaaaa dum!</b></div></div>␊
<script src="index.js" type="module"></script>␊
</body></html>`,
requestsFailed: [],
responses: [
'200 http://localhost/index.html',
'200 http://localhost/index.js',
'200 http://localhost/batman.js',
],
}

Binary file not shown.

View File

@@ -0,0 +1,58 @@
import {join, dirname} from "node:path";
import test from "ava";
import { rollup } from "rollup";
import urlPlugin from "@rollup/plugin-url";
import html from "../../src/index.ts";
import serveTest from "../util/test-server.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 {fileURLToPath} from "node:url";
import handlebars from "handlebars";
const __dirname = dirname(fileURLToPath(import.meta.url));
process.chdir(join(__dirname, 'fixtures'));
const defaultAssetInclude = [
'**/*.(png|jpg|jpeg|gif|ico|svg)',// images, svg
'**/*.(woff|woff2|eot|ttf|otf)',// fonts
'**/*.(webm|mp4)',// video
];
test.serial('web-bundle', async (t) => {
const bundle = await rollup({
input: 'index.hbs',
treeshake: 'smallest',
plugins: [
html({
transform(src) {
return handlebars.compile(src)({
head: `<title>I'm cool!</title>`
});
}
}),
urlPlugin({
include: defaultAssetInclude,
}),
serveTest({
path: 'index.html',
t,
})
],
});
await bundle.generate(output);
});

View File

@@ -37,7 +37,7 @@ Generated by [AVA](https://avajs.dev).
sources: [
'../icon.svg',
'../index.html',
'../index.js',
'../index.ts',
],
sourcesContent: [
'export default "data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2032%2032%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%20%20%3Cpath%20style%3D%22fill%3Anone%3Bstroke%3A%2300ff0d%3Bstroke-width%3A5%3Bstroke-linecap%3Asquare%3Bstroke-linejoin%3Amiter%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%20d%3D%22M4.1%2014.72%2016%2026.31%2028.38%205.09%22%2F%3E%3C%2Fsvg%3E"',
@@ -66,6 +66,6 @@ Generated by [AVA](https://avajs.dev).
code: undefined,
fileName: 'index-f75fa1e5.js.map',
map: undefined,
source: '{"version":3,"file":"index-f75fa1e5.js","sources":["../icon.svg","../index.html","../index.js"],"sourcesContent":["export default \\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2032%2032%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%20%20%3Cpath%20style%3D%22fill%3Anone%3Bstroke%3A%2300ff0d%3Bstroke-width%3A5%3Bstroke-linecap%3Asquare%3Bstroke-linejoin%3Amiter%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%20d%3D%22M4.1%2014.72%2016%2026.31%2028.38%205.09%22%2F%3E%3C%2Fsvg%3E\\"","<html>\\n <head>\\n <link rel=\\"icon\\" href=\\"./icon.svg\\">\\n<!-- <link rel=\\"stylesheet\\" href=\\"./joker.css\\">-->\\n </head>\\n <body>\\n <!--<script src=\\"./batman.js\\" type=\\"module\\"></script>-->\\n </body>\\n</html>\\n","import html from \\"./index.html\\"\\n\\nexport function render(){\\n return html;\\n}\\n"],"names":[],"mappings":"AAAA,aAAe;;ACAf,MAAA,IAAA,GAAA,CAAA;AACA,+BAA+B,EAAwD,MAAA,CAAA;AACvF;AACA;AACA;AACA;AACA;AACA;AACA,cAAa,CAAA;;ACNN,SAAS,MAAM,EAAE;AACxB,IAAI,OAAO,IAAI,CAAC;AAChB;;;;"}',
source: '{"version":3,"file":"index-f75fa1e5.js","sources":["../icon.svg","../index.html","../index.ts"],"sourcesContent":["export default \\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2032%2032%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%20%20%3Cpath%20style%3D%22fill%3Anone%3Bstroke%3A%2300ff0d%3Bstroke-width%3A5%3Bstroke-linecap%3Asquare%3Bstroke-linejoin%3Amiter%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%20d%3D%22M4.1%2014.72%2016%2026.31%2028.38%205.09%22%2F%3E%3C%2Fsvg%3E\\"","<html>\\n <head>\\n <link rel=\\"icon\\" href=\\"./icon.svg\\">\\n<!-- <link rel=\\"stylesheet\\" href=\\"./joker.css\\">-->\\n </head>\\n <body>\\n <!--<script src=\\"./batman.js\\" type=\\"module\\"></script>-->\\n </body>\\n</html>\\n","import html from \\"./index.html\\"\\n\\nexport function render(){\\n return html;\\n}\\n"],"names":[],"mappings":"AAAA,aAAe;;ACAf,MAAA,IAAA,GAAA,CAAA;AACA,+BAA+B,EAAwD,MAAA,CAAA;AACvF;AACA;AACA;AACA;AACA;AACA;AACA,cAAa,CAAA;;ACNN,SAAS,MAAM,EAAE;AACxB,IAAI,OAAO,IAAI,CAAC;AAChB;;;;"}',
},
]

View File

@@ -3,7 +3,7 @@ import {join, dirname} from "node:path";
import test from "ava";
import { rollup } from "rollup";
import {debugPrintOutput, getCode} from "../util/test.js";
import {debugPrintOutput, getCode} from "../util/index.ts";
import html from "../../src/index.ts";
import handlebars from "handlebars";
@@ -38,7 +38,7 @@ test.serial('js-import', async (t) => {
}),
]
});
const code = await getCode(bundle, output, true);
const code = await getCode(bundle, output);
debugPrintOutput('js-import',code);
t.snapshot(code);
});

View File

@@ -3,7 +3,7 @@ import {join, dirname} from "node:path";
import test from "ava";
import {rollup} from "rollup";
import liveReload from "rollup-plugin-livereload";
import {debugPrintOutput, getCode} from "../util/test.js";
import {debugPrintOutput, getCode} from "../util/index.ts";
import html from "../../src/index.ts";
@@ -29,7 +29,7 @@ test.serial('live-reload', async (t) => {
})
]
});
const code = await getCode(bundle, output, true);
const code = await getCode(bundle, output);
await bundle.close();// Make sure live-reload closes itself
debugPrintOutput('live-reload',code);
t.snapshot(code);

View File

@@ -2,7 +2,7 @@ import {resolve, join, dirname} from "node:path";
import test from "ava";
import { rollup } from "rollup";
import {debugPrintOutput, getCode} from "../util/test.js";
import {debugPrintOutput, getCode} from "../util/index.ts";
import html from "../../src/index.ts";
@@ -28,7 +28,7 @@ test.serial('multi-entry', async (t) => {
}),
]
});
const code = await getCode(bundle, output, true);
const code = await getCode(bundle, output);
debugPrintOutput('multi-entry',code);
t.snapshot(code);
});

View File

@@ -2,7 +2,7 @@ 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/test.js";
import {debugPrintOutput, getCode} from "../util/index.ts";
import html from "../../src/index.ts";
@@ -32,7 +32,7 @@ test.serial('rewrite-url', async (t) => {
}),
]
});
const code = await getCode(bundle, output, true);
const code = await getCode(bundle, output);
debugPrintOutput('rewrite-url',code);
t.snapshot(code);
});

View File

@@ -3,7 +3,7 @@ import {join, dirname} from "node:path";
import test from "ava";
import { rollup } from "rollup";
import {debugPrintOutput, getCode} from "../util/test.js";
import {debugPrintOutput, getCode} from "../util/index.ts";
import html from "../../src/index.ts";
import handlebars from "handlebars";
@@ -30,7 +30,7 @@ test.serial('handlebars', async (t) => {
})
]
});
const code = await getCode(bundle, output, true);
const code = await getCode(bundle, output);
debugPrintOutput('handlebars',code);
t.snapshot(code);
});

View File

@@ -4,7 +4,7 @@ import test from "ava";
import { rollup } from "rollup";
import urlPlugin from "@rollup/plugin-url";
import {debugPrintOutput, getCode} from "../util/test.js";
import {debugPrintOutput, getCode} from "../util/index.ts";
import html from "../../src/index.ts";
@@ -37,7 +37,7 @@ test.serial('copied-assets', async (t) => {
}),
],
});
const code = await getCode(bundle, output, true);
const code = await getCode(bundle, output);
debugPrintOutput('copied-assets',code);
t.snapshot(code);
});
@@ -55,7 +55,7 @@ test.serial('inlined-assets', async (t) => {
}),
]
});
const code = await getCode(bundle, output, true);
const code = await getCode(bundle, output);
debugPrintOutput('inlined-assets',code);
t.snapshot(code);
});

26
test/util/code-output.ts Normal file
View File

@@ -0,0 +1,26 @@
import type {RollupBuild, OutputOptions, OutputAsset, OutputChunk, SourceMap} from "rollup";
export interface TestOutput{
code: string,
fileName: string,
source: any,
map: any
}
export const getCode = async (bundle: RollupBuild, outputOptions: OutputOptions): Promise<TestOutput[]> => {
const { output } = await bundle.generate(outputOptions || { format: 'cjs', exports: 'auto' });
return output.sort((a,b)=> {
if(a.fileName === b.fileName && (<OutputAsset>a).source !== (<OutputAsset>b).source){ return (<OutputAsset>a).source<(<OutputAsset>b).source?-1:1}
return a.fileName < b.fileName ? -1 : (a.fileName > b.fileName? 1 : 0);
}).map(chunk=> {
const { code, map } = (<OutputChunk>chunk);
const { fileName, source } = (<OutputAsset>chunk);
return {
code,
fileName,
source,
map
};
});
};

6
test/util/index.ts Normal file
View File

@@ -0,0 +1,6 @@
export * from "./code-output.ts";
export * from "./print-code-output.ts";
export * from "./test-server.ts";
export * from './misc.js';

View File

@@ -1,59 +1,5 @@
import path from "node:path";
import process from "node:process";
import chalk from "chalk";
/**
* @param {import('rollup').RollupBuild} bundle
* @param {import('rollup').OutputOptions} [outputOptions]
*/
export const getCode = async (bundle, outputOptions, allFiles = false) => {
const { output } = await bundle.generate(outputOptions || { format: 'cjs', exports: 'auto' });
if (allFiles) {
return output.sort((a,b)=> {
if(a.fileName === b.fileName && a.source !== b.source){ return a.source<b.source?-1:1}
return a.fileName < b.fileName ? -1 : (a.fileName > b.fileName? 1 : 0);
}).map(({ code, fileName, source, map }) => {
return {
code,
fileName,
source,
map
};
});
}
const [{ code }] = output;
return code;
};
export const debugPrintOutput = async (header, files) => {
const out = [];
const headFn = chalk.bgCyan;
const headPadding = header.split('').map(x=>'#').join('');
out.push(...[
headFn(`##${headPadding}##`),
headFn(`# ${header} #`),
headFn(`##${headPadding}##`),
]);
const fileHeadFn = chalk.blue;
const fileContentFn = chalk.blackBright;
out.push(...(files.map(file=>{
return [
fileHeadFn(`${file.fileName}:`),
fileContentFn(`${file.code??file.source}`),
'',
]
}).flat()));
out.push(...[
headFn(`##${headPadding}##`),
]);
process.env.DEBUG? console.log(out.join('\n')) : null;
};
/**
* @param {import('rollup').RollupBuild} bundle

View File

@@ -0,0 +1,32 @@
import process from "node:process";
import chalk from "chalk";
import {TestOutput} from "./code-output.ts";
export const debugPrintOutput = async (header: string, files: TestOutput[]) => {
const out = [];
const headFn = chalk.bgCyan;
const headPadding = header.split('').map(x=>'#').join('');
out.push(...[
headFn(`##${headPadding}##`),
headFn(`# ${header} #`),
headFn(`##${headPadding}##`),
]);
const fileHeadFn = chalk.blue;
const fileContentFn = chalk.blackBright;
out.push(...(files.map(file=>{
return [
fileHeadFn(`${file.fileName}:`),
fileContentFn(`${file.code??file.source}`),
'',
]
}).flat()));
out.push(...[
headFn(`##${headPadding}##`),
]);
process.env.DEBUG? console.log(out.join('\n')) : null;
};

384
test/util/test-server.ts Normal file
View File

@@ -0,0 +1,384 @@
/**
* 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 http from 'http';
import {resolve, posix} from "node:path";
import {URL} from "node:url";
import { readFile } from 'fs'
import { createServer as createHttpsServer } from 'https'
import { createServer} from 'http'
import { Mime } from 'mime/lite'
import standardTypes from 'mime/types/standard.js'
import otherTypes from 'mime/types/other.js'
import type {NormalizedOutputOptions, OutputAsset, OutputBundle, OutputChunk, Plugin} from 'rollup'
import type {
IncomingHttpHeaders, OutgoingHttpHeaders,
IncomingMessage, ServerResponse,
Server
} from 'http'
import type { ServerOptions } from 'https'
type TypeMap = {
[key: string]: string[];
};
type ErrorCodeException = Error & {code: string};
export interface RollupServeOptions {
/**
* Change the path to be opened when the test is started
* Remember to start with a slash, e.g. `'/different/page'`
*/
path?: string
cb?: PageTestCallback
t?: any
/**
* Set to `true` to return index.html (200) instead of error page (404)
* or path to fallback page
*/
historyApiFallback?: boolean | string
/**
* Change the host of the server (default: `'localhost'`)
*/
host?: string
/**
* Change the port that the server will listen on (default: `10001`)
*/
port?: number | string
/**
* By default server will be served over HTTP (https: `false`). It can optionally be served over HTTPS.
*/
https?: ServerOptions | false
/**
* Set custom response headers
*/
headers?:
| IncomingHttpHeaders
| OutgoingHttpHeaders
| {
// i.e. Parameters<OutgoingMessage["setHeader"]>
[name: string]: number | string | ReadonlyArray<string>
}
/**
* Set custom mime types, usage https://github.com/broofa/mime#mimedefinetypemap-force--false
*/
mimeTypes?: TypeMap
/**
* Execute function after server has begun listening
*/
onListening?: (server: Server) => void
}
/**
* Serve your rolled up bundle like webpack-dev-server
* @param {import('..').RollupServeOptions} options
*/
export default function serveTest (options: RollupServeOptions ): Plugin {
const mime = new Mime(standardTypes, otherTypes)
const testOptions = {
port: 0,
headers: {},
historyApiFallback: true,
onListening: function noop (){},
...options||{},
https: options.https??false,
mimeTypes: options.mimeTypes? mime.define(options.mimeTypes, true): false
}
let server : Server;
let bundle : OutputBundle = {};
const requestListener = (request: IncomingMessage, response: ServerResponse) => {
// Remove querystring
const unsafePath = decodeURI(request.url!.split('?')[0])
// Don't allow path traversal
const urlPath = posix.normalize(unsafePath)
for(const [key, value] of Object.entries((<OutgoingHttpHeaders>testOptions.headers))){
response.setHeader(key, value!);
}
function urlToFilePath(url:string){
return url[0]==='/'?url.slice(1):url;
}
let filePath = urlToFilePath(urlPath); // Todo check if we need to strip '/'
let file: OutputChunk|OutputAsset;
if(!bundle[filePath] && testOptions.historyApiFallback) {
const fallbackPath = typeof testOptions.historyApiFallback === 'string'
? testOptions.historyApiFallback
: '/index.html';
if(bundle[urlToFilePath(fallbackPath)]){
filePath = urlToFilePath(fallbackPath);
}
}
file = bundle[filePath];
if(!file){
return notFound(response, filePath);
}else{
const content = (<OutputChunk>file).code || (<OutputAsset>file).source; // Todo might need to read a source file;
return found(response, mime.getType(filePath!), content);
}
//
// if(bundle[urlPath]){
// const fallbackPath = typeof testOptions.historyApiFallback === 'string' ? testOptions.historyApiFallback : '/index.html'
// }
//
// readFileFromContentBase(contentBase, urlPath, function (error, content, filePath) {
// if (!error) {
// return found(response, mime.getType(filePath!), content)
// }
// if ((<ErrorCodeException>error).code !== 'ENOENT') {
// response.writeHead(500)
// response.end('500 Internal Server Error' +
// '\n\n' + filePath +
// '\n\n' + Object.values(error).join('\n') +
// '\n\n(rollup-plugin-serve)', 'utf-8')
// return
// }
// if (testOptions.historyApiFallback) {
// const fallbackPath = typeof testOptions.historyApiFallback === 'string' ? testOptions.historyApiFallback : '/index.html'
// readFileFromContentBase(contentBase, fallbackPath, function (error, content, filePath) {
// if (error) {
// notFound(response, filePath)
// } else {
// found(response, mime.getType(filePath), content)
// }
// })
// } else {
// notFound(response, filePath)
// }
// })
}
function closeServerOnTermination () {
const terminationSignals = ['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP']
terminationSignals.forEach(signal => {
process.on(signal, () => {
if (server) {
server.close()
process.exit()
}
})
})
}
// release previous server instance if rollup is reloading configuration in watch mode
// @ts-ignore
if (server) {
server.close()
} else {
closeServerOnTermination()
}
// If HTTPS options are available, create an HTTPS server
server = testOptions.https
? createHttpsServer(testOptions.https, requestListener)
: createServer(requestListener)
server.listen(
typeof(testOptions.port)==='string'? Number.parseInt(testOptions.port):testOptions.port,
testOptions.host,
undefined,
() => testOptions.onListening?.(server)
)
testOptions.port = (<any>server.address())?.port ?? testOptions.port;
// Assemble url for error and info messages
const url = (testOptions.https ? 'https' : 'http') + '://' + (testOptions.host || 'localhost') + ':' + testOptions.port
// Handle common server errors
server.on('error', e => {
if ((<ErrorCodeException>e).code === 'EADDRINUSE') {
console.error(url + ' is in use, either stop the other server or use a different port.')
process.exit()
} else {
throw e
}
})
let first = true
return {
name: 'serve',
generateBundle: {
order: 'post',
async handler(options, output){
bundle = output;
if (first) {
first = false
const testOutput = await runTest({
page: testOptions.path!,
cb: testOptions.cb,
}, url)
testOptions.t?.snapshot?.(testOutput);
}
}
},
closeBundle (){
// Done with the bundle
}
}
}
function notFound (response: ServerResponse, filePath: string) {
response.writeHead(404)
response.end(
'404 Not Found' + '\n\n' + filePath,
'utf-8'
)
}
function found (response: ServerResponse, mimeType: string|null, content: any) {
response.writeHead(200, { 'Content-Type': mimeType || 'text/plain' })
response.end(content, 'utf-8')
}
function green (text: string) {
return '\u001b[1m\u001b[32m' + text + '\u001b[39m\u001b[22m'
}
export type PageTestCallback = (page: Page)=>Promise<void>;
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<TestOptions> = {
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[],
}
export async function runTest(opts: Partial<TestOptions>, hostUrl: string){
const options : TestOptions = (<TestOptions>{
...defaultOptions,
...opts,
filterOutput: {
...defaultOptions.filterOutput,
...(opts?.filterOutput),
},
});
const {
page: path,
cb,
replaceHost,
replaceHostWith,
filterOutput
} = options;
const browser = await puppeteer.launch({
headless: '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((<any>filterOutput.console)?.includes?.(<any>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{
await page.close();
await browser.close();
}
return output;
}

45
test/util/test.d.ts vendored
View File

@@ -1,45 +0,0 @@
/* eslint-disable import/no-extraneous-dependencies */
import type { RollupBuild, OutputOptions, OutputChunk, OutputAsset } from 'rollup';
import type { Assertions } from 'ava';
interface GetCode {
(bundle: RollupBuild, outputOptions?: OutputOptions | null, allFiles?: false): Promise<string>;
(bundle: RollupBuild, outputOptions: OutputOptions | null | undefined, allFiles: true): Promise<
Array<{
code: OutputChunk['code'] | undefined;
fileName: OutputChunk['fileName'] | OutputAsset['fileName'];
source: OutputAsset['source'] | undefined;
}>
>;
}
export const getCode: GetCode;
export function getFiles(
bundle: RollupBuild,
outputOptions?: OutputOptions
): Promise<
{
fileName: string;
content: any;
}[]
>;
export function evaluateBundle(bundle: RollupBuild): Promise<Pick<NodeModule, 'exports'>>;
export function getImports(bundle: RollupBuild): Promise<string[]>;
export function getResolvedModules(bundle: RollupBuild): Promise<Record<string, string>>;
export function onwarn(warning: string | any): void;
export function testBundle(
t: Assertions,
bundle: RollupBuild,
options: { inject: Record<string, any>; options: Record<string, any> }
): Promise<{
code: string;
error?: any;
result?: any;
module: Pick<NodeModule, 'exports'>;
}>;

View File

@@ -2,7 +2,7 @@ import {join, dirname} from "node:path";
import test from "ava";
import * as rollup from "rollup";
import {debugPrintOutput, getCode} from "../util/test.js";
import {debugPrintOutput, getCode} from "../util/index.ts";
import {resolve} from "node:path";
import {writeFile} from "node:fs/promises";
@@ -49,7 +49,7 @@ test.serial('watch', async (t) => {
// Just wait on the watch mode to pick up on the changes
},
async (bundle)=>{
const code = await getCode(bundle, output, true);
const code = await getCode(bundle, output);
debugPrintOutput('watch',code);
// Reset the source file