test: added a react application test (in the browser)
This commit is contained in:
5
test/util/debug-mode.ts
Normal file
5
test/util/debug-mode.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import inspector from 'node:inspector';
|
||||
|
||||
export function isInDebugMode() {
|
||||
return (inspector.url() !== undefined) || process.env.DEBUG;
|
||||
}
|
||||
155
test/util/run-browser-test.ts
Normal file
155
test/util/run-browser-test.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 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<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[],
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 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: 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((<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{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -4,21 +4,20 @@
|
||||
*/
|
||||
|
||||
|
||||
import puppeteer, {Page} from "puppeteer";
|
||||
import http from 'http';
|
||||
import {runTest, TestFilterOptions, PageTestCallback} from "./run-browser-test.ts";
|
||||
import {isInDebugMode} from "./debug-mode.ts";
|
||||
|
||||
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 {NormalizedOutputOptions, OutputAsset, OutputBundle, OutputChunk, Plugin} from 'rollup';
|
||||
|
||||
import type {
|
||||
IncomingHttpHeaders, OutgoingHttpHeaders,
|
||||
@@ -26,21 +25,34 @@ import type {
|
||||
Server
|
||||
} from 'http'
|
||||
import type { ServerOptions } from 'https'
|
||||
|
||||
|
||||
type TypeMap = {
|
||||
[key: string]: string[];
|
||||
};
|
||||
|
||||
type ErrorCodeException = Error & {code: string};
|
||||
|
||||
export interface RollupServeOptions {
|
||||
export interface RollupServeTestOptions {
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* A callback to manually take control of the page and simulate user interactions
|
||||
*/
|
||||
cb?: PageTestCallback
|
||||
t?: any
|
||||
/**
|
||||
* The AVA context used to test (ie t.snapshot(..) )
|
||||
*/
|
||||
t: any
|
||||
|
||||
/**
|
||||
* Set to `true` to return index.html (200) instead of error page (404)
|
||||
@@ -89,7 +101,7 @@ export interface RollupServeOptions {
|
||||
* Serve your rolled up bundle like webpack-dev-server
|
||||
* @param {import('..').RollupServeOptions} options
|
||||
*/
|
||||
export default function serveTest (options: RollupServeOptions ): Plugin {
|
||||
export default function serveTest (options: RollupServeTestOptions ): Plugin {
|
||||
const mime = new Mime(standardTypes, otherTypes)
|
||||
const testOptions = {
|
||||
port: 0,
|
||||
@@ -135,36 +147,6 @@ export default function serveTest (options: RollupServeOptions ): Plugin {
|
||||
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)
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
|
||||
@@ -180,7 +162,6 @@ export default function serveTest (options: RollupServeOptions ): Plugin {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// release previous server instance if rollup is reloading configuration in watch mode
|
||||
// @ts-ignore
|
||||
if (server) {
|
||||
@@ -229,6 +210,7 @@ export default function serveTest (options: RollupServeOptions ): Plugin {
|
||||
const testOutput = await runTest({
|
||||
page: testOptions.path!,
|
||||
cb: testOptions.cb,
|
||||
filterOutput: testOptions.filterOutput,
|
||||
}, url)
|
||||
testOptions.t?.snapshot?.(testOutput);
|
||||
}
|
||||
@@ -257,128 +239,3 @@ function found (response: ServerResponse, mimeType: string|null, content: any) {
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user