1 import http from "node:http";
2 import { readFile, stat } from "node:fs/promises";
3 import { createReadStream } from "node:fs";
4 import mockServer from "../middleware-mockserver.cjs";
5 import getRawBody from "raw-body";
7 export async function createTestServer( report, { quiet } = {} ) {
8 const indexHTML = await readFile( "./test/index.html", "utf8" );
10 // Support connect-style middleware
11 const middlewares = [];
12 function use( middleware ) {
13 middlewares.push( middleware );
16 function run( req, res ) {
19 // Log responses unless quiet is set
21 const originalEnd = res.end;
22 res.end = function( ...args ) {
23 console.log( `${ req.method } ${ req.url } ${ this.statusCode }` );
24 originalEnd.call( this, ...args );
28 // Add a parsed URL object to the request object
29 req.parsedUrl = new URL(
30 `http://${ process.env.HOST ?? "localhost" }${ req.url }`
33 // Add a simplified redirect helper to the response object
34 res.redirect = ( status, location ) => {
40 res.writeHead( status, { Location: location } );
45 const middleware = middlewares[ i++ ];
48 middleware( req, res, next );
50 console.error( error );
51 res.writeHead( 500, { "Content-Type": "application/json" } );
52 res.end( "Internal Server Error" );
63 // Redirect home to test page
64 use( ( req, res, next ) => {
65 if ( req.parsedUrl.pathname === "/" ) {
66 res.redirect( "/test/" );
72 // Redirect to trailing slash
73 use( ( req, res, next ) => {
74 if ( req.parsedUrl.pathname === "/test" ) {
75 res.redirect( 308, `${ req.parsedUrl.pathname }/${ req.parsedUrl.search }` );
81 // Add a script tag to the index.html to load the QUnit listeners
82 use( ( req, res, next ) => {
84 ( req.method === "GET" || req.method === "HEAD" ) &&
85 ( req.parsedUrl.pathname === "/test/" ||
86 req.parsedUrl.pathname === "/test/index.html" )
88 res.writeHead( 200, { "Content-Type": "text/html" } );
92 "<script src=\"/test/runner/listeners.js\"></script></head>"
101 use( async( req, res, next ) => {
102 if ( req.url !== "/api/report" || req.method !== "POST" ) {
107 body = JSON.parse( await getRawBody( req ) );
109 if ( error.code === "ECONNABORTED" ) {
112 console.error( error );
113 res.writeHead( 400, { "Content-Type": "application/json" } );
114 res.end( JSON.stringify( { error: "Invalid JSON" } ) );
117 const response = await report( body );
119 res.writeHead( 200, { "Content-Type": "application/json" } );
120 res.end( JSON.stringify( response ) );
122 res.writeHead( 204 );
127 // Hook up mock server
130 // Serve static files
131 const validMimeTypes = {
133 // No .mjs or .cjs files are used in tests
134 ".js": "application/javascript",
136 ".html": "text/html",
137 ".xml": "application/xml",
138 ".xhtml": "application/xhtml+xml",
139 ".jpg": "image/jpeg",
141 ".svg": "image/svg+xml",
142 ".ico": "image/x-icon",
143 ".map": "application/json",
144 ".txt": "text/plain",
147 use( async( req, res, next ) => {
149 !req.url.startsWith( "/dist/" ) &&
150 !req.url.startsWith( "/src/" ) &&
151 !req.url.startsWith( "/test/" ) &&
152 !req.url.startsWith( "/external/" )
156 const file = req.parsedUrl.pathname.slice( 1 );
157 const ext = file.slice( file.lastIndexOf( "." ) );
159 // Allow POST to .html files in tests
161 req.method !== "GET" &&
162 req.method !== "HEAD" &&
163 ( ext !== ".html" || req.method !== "POST" )
167 const mimeType = validMimeTypes[ ext ];
172 res.writeHead( 404 );
176 res.writeHead( 200, { "Content-Type": mimeType } );
177 createReadStream( file )
179 .on( "error", ( error ) => {
180 console.error( error );
181 res.writeHead( 500 );
185 console.error( `Invalid file extension: ${ ext }` );
186 res.writeHead( 404 );
191 return http.createServer( run );