test: refactoring tests
This commit is contained in:
281
test/util/serve-test.ts
Normal file
281
test/util/serve-test.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* 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 {puppeteerRunTest, TestFilterOptions, PageTestCallback} from "./puppeteer-run-test.ts";
|
||||
import {isInDebugMode} from "./debug-mode.ts";
|
||||
|
||||
import {resolve, posix} from "node:path";
|
||||
import fs from "node:fs/promises";
|
||||
import type {Stats} from "node: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'
|
||||
|
||||
import type {ExecutionContext} from "ava";
|
||||
import {createReadStream} from "fs";
|
||||
|
||||
|
||||
type TypeMap = {
|
||||
[key: string]: string[];
|
||||
};
|
||||
|
||||
type ErrorCodeException = Error & {code: string};
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Fallback to serving from a specified srcDir
|
||||
*/
|
||||
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
|
||||
|
||||
/**
|
||||
* 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: RollupServeTestOptions ): Plugin {
|
||||
const mime = new Mime(standardTypes, otherTypes)
|
||||
const testOptions = {
|
||||
port: 0,
|
||||
headers: {},
|
||||
historyApiFallback: true,
|
||||
srcDir: '', // Serve source dir as fallback (for sourcemaps / debugging)
|
||||
onListening: function noop (){},
|
||||
...options||{},
|
||||
https: options.https??false,
|
||||
mimeTypes: options.mimeTypes? mime.define(options.mimeTypes, true): false
|
||||
}
|
||||
|
||||
let server : Server;
|
||||
let bundle : OutputBundle = {};
|
||||
|
||||
const logTest = (msg: string, mode: 'info'|'warn' = 'info')=>{
|
||||
if(isInDebugMode()){
|
||||
console.log(msg);
|
||||
}
|
||||
const modeColor = {
|
||||
green: 32,
|
||||
info: 34,
|
||||
warn: 33,
|
||||
}[mode];
|
||||
testOptions.t.log(`\u001b[${modeColor}m${msg}\u001b[0m`);
|
||||
}
|
||||
|
||||
const requestListener = async (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);
|
||||
let absPath: string | undefined = undefined;
|
||||
let stats: Stats | undefined = undefined;
|
||||
|
||||
if(!bundle[filePath]){
|
||||
if(testOptions.srcDir || testOptions.srcDir===''){
|
||||
try{
|
||||
absPath = resolve(<string>testOptions.srcDir||'',filePath);
|
||||
stats = await fs.stat(absPath);
|
||||
}catch(err){
|
||||
// File not found
|
||||
}
|
||||
}
|
||||
if(!(stats?.isFile()) && testOptions.historyApiFallback) {
|
||||
const fallbackPath = typeof testOptions.historyApiFallback === 'string'
|
||||
? testOptions.historyApiFallback
|
||||
: '/index.html';
|
||||
if(bundle[urlToFilePath(fallbackPath)]){
|
||||
filePath = urlToFilePath(fallbackPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mimeType = mime.getType(filePath!);
|
||||
if(bundle[filePath]) {
|
||||
let file: OutputChunk | OutputAsset = bundle[filePath];
|
||||
const content = (<OutputChunk>file).code || (<OutputAsset>file).source; // Todo might need to read a source file;
|
||||
response.writeHead(200, {'Content-Type': mimeType || 'text/plain'});
|
||||
response.end(content, 'utf-8');
|
||||
logTest(`[200] ${request.url}`);
|
||||
return;
|
||||
}else if(stats?.isFile()){
|
||||
response.writeHead(200, {
|
||||
'Content-Type': mimeType || 'text/plain',
|
||||
'Content-Length': stats.size,
|
||||
'Last-Modified': stats.mtime.toUTCString()
|
||||
});
|
||||
const content = await fs.readFile(absPath!);
|
||||
response.end(content);
|
||||
response.end();
|
||||
logTest(`[200] ${request.url} (src)`);
|
||||
}else{
|
||||
response.writeHead(404)
|
||||
response.end(
|
||||
'404 Not Found' + '\n\n' + filePath,
|
||||
'utf-8'
|
||||
)
|
||||
|
||||
logTest(`[404] ${request.url}`, "warn");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 puppeteerRunTest({
|
||||
page: testOptions.path!,
|
||||
cb: testOptions.cb,
|
||||
filterOutput: testOptions.filterOutput,
|
||||
}, url)
|
||||
testOptions.t?.snapshot?.(testOutput);
|
||||
}
|
||||
}
|
||||
},
|
||||
closeBundle (){
|
||||
// Done with the bundle
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user