test: added testing via a browser (puppeteer)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
BIN
test/evaluated-web-bundle/fixtures/assets/assistant.ttf
Normal file
BIN
test/evaluated-web-bundle/fixtures/assets/assistant.ttf
Normal file
Binary file not shown.
8
test/evaluated-web-bundle/fixtures/assets/logo-sq.svg
Normal file
8
test/evaluated-web-bundle/fixtures/assets/logo-sq.svg
Normal 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 |
1
test/evaluated-web-bundle/fixtures/batman.js
Normal file
1
test/evaluated-web-bundle/fixtures/batman.js
Normal file
@@ -0,0 +1 @@
|
||||
export const batman = 'bum badum badum baaaaa dum!';
|
||||
18
test/evaluated-web-bundle/fixtures/index.hbs
Normal file
18
test/evaluated-web-bundle/fixtures/index.hbs
Normal 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>
|
||||
27
test/evaluated-web-bundle/fixtures/index.js
Normal file
27
test/evaluated-web-bundle/fixtures/index.js
Normal 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);
|
||||
}
|
||||
})()
|
||||
73
test/evaluated-web-bundle/snapshots/test.js.md
Normal file
73
test/evaluated-web-bundle/snapshots/test.js.md
Normal 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',
|
||||
],
|
||||
}
|
||||
BIN
test/evaluated-web-bundle/snapshots/test.js.snap
Normal file
BIN
test/evaluated-web-bundle/snapshots/test.js.snap
Normal file
Binary file not shown.
58
test/evaluated-web-bundle/test.js
Normal file
58
test/evaluated-web-bundle/test.js
Normal 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);
|
||||
});
|
||||
|
||||
@@ -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;;;;"}',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
26
test/util/code-output.ts
Normal 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
6
test/util/index.ts
Normal 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';
|
||||
@@ -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
|
||||
32
test/util/print-code-output.ts
Normal file
32
test/util/print-code-output.ts
Normal 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
384
test/util/test-server.ts
Normal 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
45
test/util/test.d.ts
vendored
@@ -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'>;
|
||||
}>;
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user