Utility ‐ tape6‐server - uhop/tape-six GitHub Wiki
To run browser-based tests, a web server is needed. tape6-server is a command-line utility,
which is a generic web server for static files, it provides an API required to run tests,
and it bundles the web application to run tests. It is pluggable: fixture plugins can be
mounted under URL prefixes to serve test endpoints (streaming, SSE, stateful resources), and
the server core can be embedded in tests via createTestServer().
Assuming that the tests are properly configured (see Set-up tests), it can be invoked without arguments:
npx tape6-serverIt serves static files from the current directory as the root. If you navigate to
http://localhost:3000 (using the default settings) in your browser, you should see the tests run.
The utility can be invoked like that:
tape6-server [options...]The options can be one or more of the following:
-
--plugin path— register a plugin module (can be repeated). The path is resolved against the server root. See Plugins below. -
--h2— serve HTTPS with HTTP/2 + HTTP/1.1 negotiated via ALPN. See HTTP/2 mode below. -
--no-remote-plugins— disablePUT/DELETEon the/--pluginsendpoint. -
--trace— enable a simple trace logging of HTTP requests and responses. -
--self— prints its path tostdoutand quits. It is used for scripting to avoid some problems with particular runtime environments. -
--help,-h— shows the help message and exits. -
--version,-v— shows the version and exits.
The following environment variables are supported:
-
HOST— the host to listen on. The default islocalhost. -
PORT— the port to listen on. The default is3000. -
SERVER_ROOT— the root folder to serve. The default is the current directory. -
WEBAPP_PATH— the path to the web application. The default is the web application supplied withtape-six. -
TAPE6_PROTOCOL— the protocol to serve:h1(the default) orh2. -
TAPE6_CERT,TAPE6_KEY— paths to a TLS certificate and its private key for the HTTP/2 mode.
The tape6.server section of package.json (or tape6.json) provides the same knobs:
{
"tape6": {
"server": {
"plugins": [
"tests/fixtures/my-plugin.js",
{"module": "tests/fixtures/other.js", "options": {}}
],
"protocol": "h1",
"remotePlugins": true
}
}
}Requests are routed in three stages:
- Reserved control endpoints —
/--tests,/--patterns,/--importmap, and/--plugins— always first, non-overridable. - Plugins, longest claimed prefix first (registration order breaks ties; a plugin without a
prefix is offered every request). The first response wins; returning
undefinedpasses the request on. - Static files (
GET/HEADonly; plugins see all verbs), exactly as before: favicon, the web application redirect, files, and the.htmlfallback.
A plugin serves test fixtures over a real wire: chunked streaming with controlled timing, server-sent events, stateful ETag resources, upload sinks — anything an in-process mock cannot cover. A plugin module default-exports an async factory; the factory returns a handler record:
export default async function fixtures(api, options) {
// api: {rootFolder, config, base, protocol, log, trace}
return {
name: 'fixtures', // registry key; defaults to the module/function name
prefix: '/--fixtures/', // claimed namespace; convention: /--<name>/
fetch(request, api) {
// WHATWG Request → Response | (async) iterable | undefined
return Response.json({ok: true});
},
close() {} // optional teardown: runs on deregistration and server shutdown
};
}Handlers speak WHATWG Request/Response:
- Request bodies are exposed as web streams (
duplex: 'half'). -
Responsestream bodies are written chunk-by-chunk without buffering. - A client disconnect aborts
request.signal, so long-lived loops (e.g., SSE) terminate.
For trivial data servers, fetch may return an (async) iterable instead of a Response, and
a module may default-export a bare (async) generator function. Chunks are mapped as follows:
Uint8Array passes through, strings are UTF-8 encoded, any other value is serialized as a
JSONL line. The content type is inferred from the first chunk (objects →
application/x-ndjson, otherwise text/plain). A complete fixture:
export default async function* numbers({url}) {
const n = Number(new URL(url).searchParams.get('n')) || 10;
for (let i = 0; i < n; ++i) yield {n: i}; // streamed as JSONL
}An escape hatch for exotics: a record may provide raw(req, res, api) on raw Node objects
instead of fetch. Returning exactly false passes the request on; any other result means
the request was handled.
A tiny reference plugin ships with tape-six: registering
node_modules/tape-six/src/test-server/plugins/echo.js mounts /--echo, which reflects the
request method, query, headers, and body as JSON.
The /--plugins endpoint manages the registry at runtime:
-
GET /--plugins— list mounted plugins as{name, prefix, source}. -
PUT /--pluginswith{"module": "path", "options": {}, "name": "..."}— import and mount a module. The path is resolved against the server root and must stay inside it. Re-PUTwith the same name replaces the plugin (the old instance'sclose()runs first), and the module is re-imported — pleasant for watch-mode iteration. -
DELETE /--plugins/<name>— deregister. New requests stop routing to the plugin immediately; in-flight streams finish unless itsclose()ends them.
Browser tests can mount server-side fixtures before running: a same-origin
fetch('/--plugins', {method: 'PUT', ...}) from the page. Use --no-remote-plugins to
disable mutations (the GET listing stays available).
Parallel test files may share one server. Stateful fixtures should key their state by an
explicit scope (a query parameter or an X-Scope header) minted per test, and sweep scopes
by TTL. This keeps ETag/SSE-resume fixtures safe under parallel workers.
For hermetic integration tests on CLI runtimes, embed the server core instead of spawning the
utility — with port: 0 (the default) every test file gets its own server on an
OS-assigned port, collision-free by construction:
import {createTestServer, withTestServer} from 'tape-six/test-server.js';
const server = await createTestServer({plugins: [myPlugin]});
// server: {base, host, port, protocol, server, register, deregister, plugins, close}
await server.close();
// or scoped:
await withTestServer({plugins: [myPlugin]}, async (base, server) => {
const res = await fetch(base + '/--fixtures/data');
// the server closes when this handler settles
});Inline plugins — factories, handler records, bare handlers, and (async) generator
functions — are accepted in the plugins array and via server.register(). The full
option and plugin types are declared in test-server.d.ts.
--h2 (or TAPE6_PROTOCOL=h2, or "protocol": "h2" in the config) switches the server to
HTTPS with HTTP/2 and HTTP/1.1 negotiated via ALPN on one port. Plugins are unaffected: the
same handlers run under either protocol.
HTTP/1.1 stays the default: h2 in browsers requires TLS, and self-signed certificates tax
manual debugging (security interstitials) and service workers (they refuse to register on
cert-error origins). Use h2 when a test needs it — notably browser fetch() request
body streaming (duplex: 'half'), which Chromium supports over h2/h3 only — or to lift
the ~6-connections-per-origin HTTP/1.1 cap for parallel SSE fixtures.
Certificates are resolved in order:
-
TAPE6_CERT/TAPE6_KEY(or thecert/keyconfig keys) — use mkcert for locally-trusted certificates; - otherwise a self-signed certificate is auto-generated with
openssland cached undernode_modules/.cache/tape6/, reused until expiry; - otherwise the server fails to start with a message naming both options.
The HTTP/2 server mode requires Node (node:http2 support is uneven elsewhere); the server is
a separate process, so this does not restrict which runtime executes the tests.
All tests are executed in a separate newly created IFRAME.
The web application supports the following query parameters:
-
flags— the flags to pass to the test harness. See TAPE6_FLAGS for details. -
par— the parameters to pass to the test harness. See TAPE6_PAR for details. Defaults to1. -
q— a pattern for tests to run. It can be specified multiple times. If not specified, the configured tests will run. See Set-up tests for details.
The web application is served when accessing the root via a redirect.
tape6-server is written using a portable subset of Node API and can be run by Node, Deno,
and Bun in the default HTTP/1.1 mode (the HTTP/2 mode is Node-only). The embeddable core runs
on all three CLI runtimes. See Environment - Browsers for details.