tape-six 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/TESTING.md +1 -1
- package/bin/tape6-server.js +62 -190
- package/index.d.ts +1 -5
- package/index.js +1 -3
- package/llms-full.txt +83 -2
- package/llms.txt +21 -0
- package/package.json +3 -2
- package/src/State.js +2 -3
- package/src/Tester.js +1 -4
- package/src/reporters/Reporter.js +4 -13
- package/src/runners/bun/TestWorker.js +4 -5
- package/src/runners/bun/worker.js +1 -3
- package/src/runners/deno/TestWorker.js +4 -5
- package/src/runners/deno/worker.js +1 -3
- package/src/runners/node/TestWorker.js +5 -6
- package/src/runners/node/worker.js +1 -3
- package/src/runners/seq/TestWorker.js +5 -5
- package/src/test-server/adapter.js +121 -0
- package/src/test-server/certs.js +77 -0
- package/src/test-server/control.js +94 -0
- package/src/test-server/plugins/echo.js +20 -0
- package/src/test-server/registry.js +143 -0
- package/src/test-server/statics.js +106 -0
- package/src/test-server/trace.js +31 -0
- package/src/test-server.d.ts +157 -0
- package/src/test-server.js +154 -0
- package/src/test.js +5 -2
- package/src/utils/EventServer.d.ts +114 -0
- package/src/utils/EventServer.js +9 -16
- package/src/utils/config.js +2 -7
- package/src/utils/control-channel.js +1 -3
- package/src/utils/defer.js +1 -1
- package/src/utils/yamlFormatter.js +1 -1
- package/web-app/TestWorker.js +3 -5
- package/src/utils/timeout.js +0 -3
package/README.md
CHANGED
|
@@ -427,6 +427,8 @@ Test output can be controlled by flags. See [Supported flags](https://github.com
|
|
|
427
427
|
|
|
428
428
|
The most recent releases:
|
|
429
429
|
|
|
430
|
+
- 1.13.0 _TypeScript declarations for the `EventServer`._
|
|
431
|
+
- 1.12.0 _`tape6-server` is now pluggable, embeddable server for hermetic integration tests, runtime plugin registration, and an opt-in HTTP/2 mode._
|
|
430
432
|
- 1.11.0 _Invariant host hooks for the new zero-dependency `tape-six-invariant` companion package. Workaround for a Deno 2.9.0 `node_modules/.bin` symlink regression._
|
|
431
433
|
- 1.10.1 _A workaround for a Bun's bug with streams._
|
|
432
434
|
- 1.10.0 _Worker control channel stops in-flight workers, draining cleanup before a force-kill._
|
package/TESTING.md
CHANGED
|
@@ -649,7 +649,7 @@ In this example, running `tape6` on Node will execute tests matching `/tests/nod
|
|
|
649
649
|
|
|
650
650
|
## Browser testing
|
|
651
651
|
|
|
652
|
-
Browser tests use `tape6-server`, a
|
|
652
|
+
Browser tests use `tape6-server`, a pluggable test server bundled with `tape-six` that serves static files and provides a web UI for running tests. Fixture plugins can be mounted under URL prefixes (`--plugin`, the `tape6.server.plugins` config, or `PUT /--plugins` at runtime), the core is embeddable in tests via `createTestServer()` from `tape-six/test-server.js`, and `--h2` switches to HTTP/2 (Node-only). See the [tape6-server wiki page](https://github.com/uhop/tape-six/wiki/Utility-%E2%80%90-tape6%E2%80%90server) for the plugin contract.
|
|
653
653
|
|
|
654
654
|
### Setup
|
|
655
655
|
|
package/bin/tape6-server.js
CHANGED
|
@@ -1,26 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import http from 'node:http';
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import path from 'node:path';
|
|
6
3
|
import process from 'node:process';
|
|
7
4
|
import {fileURLToPath} from 'node:url';
|
|
8
5
|
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
resolvePatterns,
|
|
13
|
-
printVersion,
|
|
14
|
-
printHelp
|
|
15
|
-
} from '../src/utils/config.js';
|
|
16
|
-
import {startServer} from '../src/server.js';
|
|
17
|
-
|
|
18
|
-
const fsp = fs.promises;
|
|
19
|
-
|
|
20
|
-
const toPosix = files =>
|
|
21
|
-
path.sep === path.win32.sep
|
|
22
|
-
? files.map(f => f.replaceAll(path.win32.sep, path.posix.sep))
|
|
23
|
-
: files;
|
|
6
|
+
import {printVersion, printHelp} from '../src/utils/config.js';
|
|
7
|
+
import {createTestServer} from '../src/test-server.js';
|
|
8
|
+
import {detectColors, makePaint} from '../src/test-server/trace.js';
|
|
24
9
|
|
|
25
10
|
const showSelf = () => {
|
|
26
11
|
const self = new URL(import.meta.url);
|
|
@@ -32,17 +17,23 @@ const showSelf = () => {
|
|
|
32
17
|
process.exit(0);
|
|
33
18
|
};
|
|
34
19
|
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
35
|
-
printHelp('tape6-server', '
|
|
20
|
+
printHelp('tape6-server', 'Pluggable test server for browser testing', 'tape6-server [options]', [
|
|
21
|
+
['--plugin path', 'Register a plugin module (repeatable)'],
|
|
22
|
+
['--h2', 'Serve HTTPS with HTTP/2 (+ HTTP/1.1 via ALPN); Node only'],
|
|
23
|
+
['--no-remote-plugins', 'Disable PUT/DELETE on /--plugins'],
|
|
36
24
|
['--trace', 'Enable request trace logging'],
|
|
37
25
|
['--self', 'Print the path to this script and exit'],
|
|
38
26
|
['--help, -h', 'Show this help message and exit'],
|
|
39
27
|
['--version, -v', 'Show version and exit']
|
|
40
28
|
]);
|
|
41
29
|
console.log('\nEnvironment:');
|
|
42
|
-
console.log(' HOST
|
|
43
|
-
console.log(' PORT
|
|
44
|
-
console.log(' SERVER_ROOT
|
|
45
|
-
console.log(' WEBAPP_PATH
|
|
30
|
+
console.log(' HOST Server hostname (default: localhost)');
|
|
31
|
+
console.log(' PORT Server port (default: 3000)');
|
|
32
|
+
console.log(' SERVER_ROOT Root directory for serving files (default: cwd)');
|
|
33
|
+
console.log(' WEBAPP_PATH Path to the web app directory');
|
|
34
|
+
console.log(' TAPE6_PROTOCOL Protocol: h1 or h2 (default: h1)');
|
|
35
|
+
console.log(' TAPE6_CERT Path to a TLS certificate (h2)');
|
|
36
|
+
console.log(' TAPE6_KEY Path to its private key (h2)');
|
|
46
37
|
process.exit(0);
|
|
47
38
|
}
|
|
48
39
|
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
@@ -51,198 +42,79 @@ if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
|
51
42
|
}
|
|
52
43
|
if (process.argv.includes('--self')) showSelf();
|
|
53
44
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
xml: 'application/xml'
|
|
73
|
-
},
|
|
74
|
-
defaultMime = 'application/octet-stream',
|
|
75
|
-
rootFolder = process.env.SERVER_ROOT || process.cwd(),
|
|
76
|
-
traceCalls = process.argv.includes('--trace'),
|
|
77
|
-
hasColors =
|
|
78
|
-
process.stdout.isTTY &&
|
|
79
|
-
(typeof process.stdout.hasColors == 'function' ? process.stdout.hasColors() : true);
|
|
80
|
-
|
|
81
|
-
let webAppPath = process.env.WEBAPP_PATH;
|
|
82
|
-
if (!webAppPath) {
|
|
83
|
-
const url = import.meta.url;
|
|
84
|
-
if (!/^file:\/\//i.test(url))
|
|
85
|
-
throw Error('Cannot identify the location of the web application. Use WEBAPP_PATH.');
|
|
86
|
-
webAppPath = path.relative(
|
|
87
|
-
rootFolder,
|
|
88
|
-
path.join(path.dirname(fileURLToPath(url)), '../web-app/')
|
|
89
|
-
);
|
|
90
|
-
if (path.sep === path.win32.sep)
|
|
91
|
-
webAppPath = webAppPath.replaceAll(path.win32.sep, path.posix.sep);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const mimeAliases = {mjs: 'js', cjs: 'js', htm: 'html', jpeg: 'jpg'};
|
|
95
|
-
Object.keys(mimeAliases).forEach(name => (mimeTable[name] = mimeTable[mimeAliases[name]]));
|
|
96
|
-
|
|
97
|
-
const join = (...args) => args.map(value => value || '').join(''),
|
|
98
|
-
paint = hasColors
|
|
99
|
-
? (prefix, suffix = '\x1B[39m') =>
|
|
100
|
-
text =>
|
|
101
|
-
join(prefix, text, suffix)
|
|
102
|
-
: () => text => text,
|
|
103
|
-
grey = paint('\x1B[2;37m', '\x1B[22;39m'),
|
|
104
|
-
red = paint('\x1B[41;97m', '\x1B[49;39m'),
|
|
105
|
-
green = paint('\x1B[32m'),
|
|
106
|
-
yellow = paint('\x1B[93m'),
|
|
107
|
-
blue = paint('\x1B[44;97m', '\x1B[49;39m');
|
|
108
|
-
|
|
109
|
-
const link = (url, text = url) => paint('\x1B]8;;' + url + '\x1B\\', '\x1B]8;;\x1B\\')(text);
|
|
110
|
-
|
|
111
|
-
const sendFile = (req, res, fileName, ext, justHeaders) => {
|
|
112
|
-
if (!ext) {
|
|
113
|
-
ext = path.extname(fileName).toLowerCase();
|
|
114
|
-
}
|
|
115
|
-
let mime = ext && mimeTable[ext.substring(1)];
|
|
116
|
-
if (!mime || typeof mime != 'string') {
|
|
117
|
-
mime = defaultMime;
|
|
118
|
-
}
|
|
119
|
-
res.writeHead(200, {'Content-Type': mime});
|
|
120
|
-
if (justHeaders) {
|
|
121
|
-
res.end();
|
|
122
|
-
} else {
|
|
123
|
-
fs.createReadStream(fileName).pipe(res);
|
|
124
|
-
}
|
|
125
|
-
traceCalls && console.log(green('200') + ' ' + grey(req.method) + ' ' + grey(req.url));
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const sendJson = (req, res, json, justHeaders) => {
|
|
129
|
-
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
130
|
-
if (justHeaders) {
|
|
131
|
-
res.end();
|
|
132
|
-
} else {
|
|
133
|
-
res.end(JSON.stringify(json));
|
|
134
|
-
}
|
|
135
|
-
traceCalls && console.log(green('200') + ' ' + grey(req.method) + ' ' + grey(req.url));
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const sendRedirect = (req, res, to, code = 307) => {
|
|
139
|
-
res.writeHead(code, {Location: to});
|
|
140
|
-
res.end();
|
|
141
|
-
traceCalls && console.log(blue(code) + ' ' + grey(req.method) + ' ' + grey(req.url));
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const bailOut = (req, res, code = 404) => {
|
|
145
|
-
res.writeHead(code).end();
|
|
146
|
-
traceCalls && console.log(red(code) + ' ' + grey(req.method) + ' ' + grey(req.url));
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const server = http.createServer(async (req, res) => {
|
|
150
|
-
const method = req.method.toUpperCase();
|
|
151
|
-
if (method !== 'GET' && method !== 'HEAD') return bailOut(req, res, 405);
|
|
152
|
-
|
|
153
|
-
const url = new URL(req.url, 'http://' + req.headers.host);
|
|
154
|
-
if (url.pathname === '/--tests') {
|
|
155
|
-
return sendJson(
|
|
156
|
-
req,
|
|
157
|
-
res,
|
|
158
|
-
toPosix(await resolveTests(rootFolder, 'browser')),
|
|
159
|
-
method === 'HEAD'
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
if (url.pathname === '/--patterns') {
|
|
163
|
-
return sendJson(
|
|
164
|
-
req,
|
|
165
|
-
res,
|
|
166
|
-
toPosix(await resolvePatterns(rootFolder, url.searchParams.getAll('q'))),
|
|
167
|
-
method === 'HEAD'
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
if (url.pathname === '/--importmap') {
|
|
171
|
-
const cfg = await getConfig(rootFolder);
|
|
172
|
-
return sendJson(req, res, cfg.importmap || {imports: {}}, method === 'HEAD');
|
|
173
|
-
}
|
|
174
|
-
if (url.pathname === '/favicon.ico') {
|
|
175
|
-
const faviconFile = path.join(rootFolder, webAppPath, 'favicon.ico');
|
|
176
|
-
const stat = await fsp.stat(faviconFile).catch(() => null);
|
|
177
|
-
if (stat && stat.isFile()) return sendFile(req, res, faviconFile, '.ico', method === 'HEAD');
|
|
178
|
-
return bailOut(req, res);
|
|
179
|
-
}
|
|
180
|
-
if (url.pathname === '/' || url.pathname === '/index' || url.pathname === '/index.html') {
|
|
181
|
-
url.pathname = webAppPath;
|
|
182
|
-
return sendRedirect(req, res, url.href);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (path.normalize(url.pathname).includes('..')) return bailOut(req, res, 403);
|
|
186
|
-
|
|
187
|
-
const fileName = path.join(rootFolder, url.pathname),
|
|
188
|
-
ext = path.extname(fileName).toLowerCase(),
|
|
189
|
-
stat = await fsp.stat(fileName).catch(() => null);
|
|
190
|
-
if (stat && stat.isFile()) return sendFile(req, res, fileName, ext, method === 'HEAD');
|
|
191
|
-
|
|
192
|
-
if (stat && stat.isDirectory()) {
|
|
193
|
-
if (fileName.length && fileName[fileName.length - 1] == path.sep) {
|
|
194
|
-
const altFile = path.join(fileName, 'index.html'),
|
|
195
|
-
stat = await fsp.stat(altFile).catch(() => null);
|
|
196
|
-
if (stat && stat.isFile()) return sendFile(req, res, altFile, '.html', method === 'HEAD');
|
|
197
|
-
} else {
|
|
198
|
-
url.pathname += path.posix.sep;
|
|
199
|
-
return sendRedirect(req, res, url.href);
|
|
45
|
+
const plugins = [];
|
|
46
|
+
let traceCalls = false,
|
|
47
|
+
h2 = false,
|
|
48
|
+
remotePlugins = true;
|
|
49
|
+
{
|
|
50
|
+
const args = process.argv.slice(2);
|
|
51
|
+
for (let i = 0; i < args.length; ++i) {
|
|
52
|
+
const arg = args[i];
|
|
53
|
+
if (arg === '--trace') {
|
|
54
|
+
traceCalls = true;
|
|
55
|
+
} else if (arg === '--h2') {
|
|
56
|
+
h2 = true;
|
|
57
|
+
} else if (arg === '--no-remote-plugins') {
|
|
58
|
+
remotePlugins = false;
|
|
59
|
+
} else if (arg === '--plugin') {
|
|
60
|
+
if (++i < args.length) plugins.push(args[i]);
|
|
61
|
+
} else if (arg.startsWith('--plugin=')) {
|
|
62
|
+
plugins.push(arg.substring('--plugin='.length));
|
|
200
63
|
}
|
|
201
|
-
return bailOut(req, res);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (!ext && fileName.length && fileName[fileName.length - 1] != path.sep) {
|
|
205
|
-
const altFile = fileName + '.html',
|
|
206
|
-
stat = await fsp.stat(altFile).catch(() => null);
|
|
207
|
-
if (stat && stat.isFile()) return sendFile(req, res, altFile, '.html', method === 'HEAD');
|
|
208
64
|
}
|
|
209
|
-
|
|
210
|
-
bailOut(req, res);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
server.on('clientError', (error, socket) => {
|
|
214
|
-
if (error.code === 'ECONNRESET' || !socket.writable) return;
|
|
215
|
-
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
216
|
-
});
|
|
65
|
+
}
|
|
217
66
|
|
|
218
67
|
const normalizePort = val => {
|
|
219
68
|
const port = parseInt(val);
|
|
220
69
|
if (isNaN(port)) return val; // named pipe
|
|
221
|
-
if (port >= 0) return port;
|
|
70
|
+
if (port >= 0) return port;
|
|
222
71
|
return false;
|
|
223
72
|
};
|
|
224
73
|
|
|
225
74
|
const portToString = port => (typeof port === 'string' ? 'pipe' : 'port') + ' ' + port;
|
|
226
75
|
|
|
227
76
|
const host = process.env.HOST || 'localhost',
|
|
228
|
-
port = normalizePort(process.env.PORT || '3000')
|
|
77
|
+
port = normalizePort(process.env.PORT || '3000'),
|
|
78
|
+
rootFolder = process.env.SERVER_ROOT || process.cwd(),
|
|
79
|
+
protocol = h2 ? 'h2' : process.env.TAPE6_PROTOCOL || undefined;
|
|
80
|
+
|
|
81
|
+
const hasColors = detectColors(),
|
|
82
|
+
paint = makePaint(hasColors),
|
|
83
|
+
grey = paint('\x1B[2;37m', '\x1B[22;39m'),
|
|
84
|
+
red = paint('\x1B[41;97m', '\x1B[49;39m'),
|
|
85
|
+
yellow = paint('\x1B[93m');
|
|
86
|
+
|
|
87
|
+
const link = (url, text = url) => paint('\x1B]8;;' + url + '\x1B\\', '\x1B]8;;\x1B\\')(text);
|
|
229
88
|
|
|
230
89
|
try {
|
|
231
|
-
await
|
|
232
|
-
|
|
90
|
+
const testServer = await createTestServer({
|
|
91
|
+
rootFolder,
|
|
92
|
+
webAppPath: process.env.WEBAPP_PATH,
|
|
93
|
+
host,
|
|
94
|
+
port,
|
|
95
|
+
protocol,
|
|
96
|
+
plugins,
|
|
97
|
+
remotePlugins,
|
|
98
|
+
trace: traceCalls
|
|
99
|
+
});
|
|
100
|
+
const bind = portToString(testServer.port);
|
|
233
101
|
console.log(
|
|
234
102
|
grey('Listening on ') +
|
|
235
103
|
yellow(host || 'all network interfaces') +
|
|
104
|
+
yellow(' (' + testServer.protocol + ')') +
|
|
236
105
|
grey(' at ') +
|
|
237
106
|
yellow(bind) +
|
|
238
107
|
grey(', serving static files from ') +
|
|
239
108
|
yellow(rootFolder)
|
|
240
109
|
);
|
|
241
|
-
|
|
110
|
+
const mounted = testServer.plugins();
|
|
111
|
+
if (mounted.length) {
|
|
242
112
|
console.log(
|
|
243
|
-
grey('
|
|
113
|
+
grey('Plugins: ') +
|
|
114
|
+
yellow(mounted.map(p => p.name + (p.prefix ? ' (' + p.prefix + ')' : '')).join(', '))
|
|
244
115
|
);
|
|
245
116
|
}
|
|
117
|
+
console.log(grey('Open ') + link(testServer.base + '/') + grey(' in your browser'));
|
|
246
118
|
console.log();
|
|
247
119
|
} catch (error) {
|
|
248
120
|
if (error.syscall === 'listen') {
|
package/index.d.ts
CHANGED
|
@@ -8,11 +8,7 @@
|
|
|
8
8
|
* - `null` or any primitive — strict equality.
|
|
9
9
|
*/
|
|
10
10
|
export type AssertionMatcher =
|
|
11
|
-
|
|
12
|
-
| RegExp
|
|
13
|
-
| ((value: unknown) => boolean)
|
|
14
|
-
| object
|
|
15
|
-
| null;
|
|
11
|
+
(new (...args: any[]) => Error) | RegExp | ((value: unknown) => boolean) | object | null;
|
|
16
12
|
|
|
17
13
|
/**
|
|
18
14
|
* Test options
|
package/index.js
CHANGED
|
@@ -175,9 +175,7 @@ const init = async () => {
|
|
|
175
175
|
|
|
176
176
|
if (isBrowser && typeof window.addEventListener == 'function') {
|
|
177
177
|
// Control plane: the parent page posts `tape6-terminate` to drain a running
|
|
178
|
-
// test
|
|
179
|
-
// fires the abort signal so the test unwinds. There is no in-page
|
|
180
|
-
// force-kill — see dev-docs/worker-control-channel.md.
|
|
178
|
+
// test; there is no in-page force-kill — see dev-docs/worker-control-channel.md.
|
|
181
179
|
window.addEventListener('message', event => {
|
|
182
180
|
if (event.data?.type === 'tape6-terminate') getReporter()?.terminate();
|
|
183
181
|
});
|
package/llms-full.txt
CHANGED
|
@@ -335,21 +335,100 @@ Sequential in-process runner. Same options as `tape6` but no threads.
|
|
|
335
335
|
|
|
336
336
|
### tape6-server
|
|
337
337
|
|
|
338
|
-
|
|
338
|
+
Pluggable test server for browser testing: static files + web UI + fixture plugins.
|
|
339
339
|
|
|
340
340
|
```bash
|
|
341
341
|
tape6-server [options...]
|
|
342
342
|
```
|
|
343
343
|
|
|
344
|
+
- `--plugin path` — register a plugin module (repeatable; path relative to the server root).
|
|
345
|
+
- `--h2` — serve HTTPS with HTTP/2 + HTTP/1.1 via ALPN (Node-only server mode).
|
|
346
|
+
- `--no-remote-plugins` — disable `PUT`/`DELETE` on `/--plugins`.
|
|
344
347
|
- `--trace` — enable request trace logging.
|
|
345
348
|
- `--self` — print the path to this script and exit.
|
|
346
349
|
- `--help`, `-h` — show help message and exit.
|
|
347
350
|
- `--version`, `-v` — show version and exit.
|
|
348
351
|
|
|
349
352
|
Configured via environment variables: `HOST` (default: localhost), `PORT` (default: 3000),
|
|
350
|
-
`SERVER_ROOT` (default: cwd), `WEBAPP_PATH
|
|
353
|
+
`SERVER_ROOT` (default: cwd), `WEBAPP_PATH`, `TAPE6_PROTOCOL` (`h1`/`h2`),
|
|
354
|
+
`TAPE6_CERT`/`TAPE6_KEY` (TLS cert/key paths for h2; auto-generates a self-signed cert via
|
|
355
|
+
openssl when omitted).
|
|
351
356
|
Navigate to `http://localhost:3000` for the web UI.
|
|
352
357
|
|
|
358
|
+
Request routing: reserved control endpoints (`/--tests`, `/--patterns`, `/--importmap`,
|
|
359
|
+
`/--plugins`) → plugins by longest prefix → static files (GET/HEAD only; plugins see all verbs).
|
|
360
|
+
|
|
361
|
+
## Test server plugins and embedding
|
|
362
|
+
|
|
363
|
+
Fixture plugins let real-wire tests (streaming, SSE, stateful ETag resources, upload sinks)
|
|
364
|
+
run against `tape6-server`. A plugin module default-exports an async factory returning a
|
|
365
|
+
handler record:
|
|
366
|
+
|
|
367
|
+
```js
|
|
368
|
+
export default async function fixtures(api, options) {
|
|
369
|
+
// api: {rootFolder, config, base, protocol, log, trace}
|
|
370
|
+
return {
|
|
371
|
+
name: 'fixtures', // registry key (defaults to the module/function name)
|
|
372
|
+
prefix: '/--fixtures/', // claimed namespace, longest-prefix routed
|
|
373
|
+
fetch(request, api) {
|
|
374
|
+
// WHATWG Request → Response | (async) iterable | undefined (= pass to the next plugin)
|
|
375
|
+
return Response.json({ok: true});
|
|
376
|
+
},
|
|
377
|
+
close() {} // optional teardown: runs on deregistration and server shutdown
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
- Request bodies are Web streams (`duplex: 'half'`); `Response` stream bodies are written
|
|
383
|
+
unbuffered; client disconnect fires `request.signal`.
|
|
384
|
+
- `fetch` may return an (async) iterable instead of a `Response`: `Uint8Array` chunks pass
|
|
385
|
+
through, strings are UTF-8 encoded, other values become JSONL lines. A module may
|
|
386
|
+
default-export a bare (async) generator function:
|
|
387
|
+
|
|
388
|
+
```js
|
|
389
|
+
export default async function* numbers({url}) {
|
|
390
|
+
const n = Number(new URL(url).searchParams.get('n')) || 10;
|
|
391
|
+
for (let i = 0; i < n; ++i) yield {n: i}; // streamed as application/x-ndjson
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
- `raw(req, res, api)` is an escape hatch on raw Node objects; returning exactly `false`
|
|
396
|
+
passes the request on.
|
|
397
|
+
- A tiny reference plugin ships in-tree: `tape-six/test-server/plugins/echo.js` mounts
|
|
398
|
+
`/--echo` and reflects method/query/headers/body as JSON.
|
|
399
|
+
|
|
400
|
+
Registration channels:
|
|
401
|
+
|
|
402
|
+
- **Static:** `package.json` → `"tape6": {"server": {"plugins": ["path", {"module": "path", "options": {}}]}}`
|
|
403
|
+
or the repeatable `--plugin` CLI flag.
|
|
404
|
+
- **Dynamic (wire):** `GET /--plugins` lists `{name, prefix, source}`; `PUT /--plugins` with
|
|
405
|
+
`{"module": "path", "options": {}}` mounts a module (resolved against the server root and
|
|
406
|
+
contained to it; re-PUT replaces by name and closes the old instance); `DELETE
|
|
407
|
+
/--plugins/<name>` deregisters. Disable mutations with `--no-remote-plugins`.
|
|
408
|
+
|
|
409
|
+
Embedding (hermetic Node/Bun/Deno integration tests; kernel-assigned ports make parallel test
|
|
410
|
+
files collision-free):
|
|
411
|
+
|
|
412
|
+
```js
|
|
413
|
+
import {createTestServer, withTestServer} from 'tape-six/test-server.js';
|
|
414
|
+
|
|
415
|
+
const {base, close} = await createTestServer({rootFolder, plugins: [myPlugin], port: 0});
|
|
416
|
+
// ... fetch(base + '/--fixtures/...') ...
|
|
417
|
+
await close();
|
|
418
|
+
|
|
419
|
+
// or scoped:
|
|
420
|
+
await withTestServer({rootFolder, plugins: [myPlugin]}, async base => {
|
|
421
|
+
// server closes when the handler settles
|
|
422
|
+
});
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Inline plugins (factories, records, generator functions) are accepted in the `plugins` array
|
|
426
|
+
and via `server.register(spec)`. Other config keys under `tape6.server`: `protocol`
|
|
427
|
+
(`"h1"`/`"h2"`), `cert`/`key` (TLS paths), `remotePlugins` (boolean), `webAppPath`.
|
|
428
|
+
|
|
429
|
+
Stateful fixtures should key state by an explicit `scope` (query param or `X-Scope` header)
|
|
430
|
+
minted per test, so parallel workers sharing one server don't collide.
|
|
431
|
+
|
|
353
432
|
### Runtime-specific runners
|
|
354
433
|
|
|
355
434
|
- `tape6-node` — Node.js runner.
|
|
@@ -389,6 +468,8 @@ Browser: `http://localhost:3000/?flags=FO`
|
|
|
389
468
|
- `TAPE6_TEST_FILE_NAME` — set by runners to identify the current test file.
|
|
390
469
|
- `TAPE6_GRACE_TIMEOUT` — ms a worker gets to drain (run cleanup, flush) after the runner asks it to stop, before being force-killed (default: 5000).
|
|
391
470
|
- `TAPE6_WORKER_TIMEOUT` — per-file wall-clock budget in ms; on expiry the runner stops that worker (default: 0, disabled). The complement to the per-test `timeout` option: this one can stop a worker that ignores `t.signal`.
|
|
471
|
+
- `TAPE6_PROTOCOL` — test server protocol: `h1` (default) or `h2`.
|
|
472
|
+
- `TAPE6_CERT` / `TAPE6_KEY` — TLS certificate/key paths for the test server's h2 mode.
|
|
392
473
|
|
|
393
474
|
## Browser testing
|
|
394
475
|
|
package/llms.txt
CHANGED
|
@@ -235,6 +235,25 @@ npx tape6-seq # run sequentially (no worker threads)
|
|
|
235
235
|
npx tape6-server --trace # start browser test server
|
|
236
236
|
```
|
|
237
237
|
|
|
238
|
+
### Test server (fixtures + embedding)
|
|
239
|
+
|
|
240
|
+
`tape6-server` is pluggable: fixture plugins mount under URL prefixes (`--plugin path`,
|
|
241
|
+
config `tape6.server.plugins`, or `PUT /--plugins` at runtime) and handle requests with a
|
|
242
|
+
WHATWG `fetch(request) → Response | (async) iterable | undefined` contract; an (async)
|
|
243
|
+
generator function streams JSONL. `--h2` switches to HTTPS with HTTP/2 (Node-only). For
|
|
244
|
+
hermetic integration tests embed it on an OS-assigned port:
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
import {createTestServer, withTestServer} from 'tape-six/test-server.js';
|
|
248
|
+
|
|
249
|
+
await withTestServer({plugins: [myPlugin]}, async base => {
|
|
250
|
+
const res = await fetch(base + '/--fixtures/data?n=3');
|
|
251
|
+
// ...
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Full plugin contract: llms-full.txt § Test server plugins and embedding.
|
|
256
|
+
|
|
238
257
|
## Environment variables
|
|
239
258
|
|
|
240
259
|
- `TAPE6_FLAGS` — flags string.
|
|
@@ -245,6 +264,8 @@ npx tape6-server --trace # start browser test server
|
|
|
245
264
|
- `TAPE6_TEST_FILE_NAME` — set by runners to identify the current test file.
|
|
246
265
|
- `TAPE6_GRACE_TIMEOUT` — ms a worker gets to drain (run cleanup, flush) after the runner asks it to stop, before being force-killed (default: 5000).
|
|
247
266
|
- `TAPE6_WORKER_TIMEOUT` — per-file wall-clock budget in ms; on expiry the runner stops that worker (default: 0, disabled). Complements the per-test `timeout` option: this one can stop a worker that ignores `t.signal`.
|
|
267
|
+
- `TAPE6_PROTOCOL` — test server protocol: `h1` (default) or `h2`.
|
|
268
|
+
- `TAPE6_CERT` / `TAPE6_KEY` — TLS certificate/key paths for the test server's h2 mode.
|
|
248
269
|
|
|
249
270
|
## Configuration (package.json)
|
|
250
271
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tape-six",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "TAP-inspired unit test library for Node, Deno, Bun, and browsers. ES modules, TypeScript, zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -103,7 +103,8 @@
|
|
|
103
103
|
}
|
|
104
104
|
},
|
|
105
105
|
"devDependencies": {
|
|
106
|
-
"@types/node": "^26.0
|
|
106
|
+
"@types/node": "^26.1.0",
|
|
107
|
+
"prettier": "^3.9.4",
|
|
107
108
|
"typescript": "^6.0.3"
|
|
108
109
|
}
|
|
109
110
|
}
|
package/src/State.js
CHANGED
|
@@ -184,9 +184,8 @@ export class State {
|
|
|
184
184
|
typeof event.diffTime !== 'number' && (event.diffTime = event.time - this.startTime);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
// inside `if (event.fail)` blocks, so for passing assertions this is wasted work.
|
|
187
|
+
// reporters read event.at / event.stackList only for failures — stack
|
|
188
|
+
// walking on passing assertions is wasted work
|
|
190
189
|
if (isFailed) {
|
|
191
190
|
if (
|
|
192
191
|
event.type === 'assert' &&
|
package/src/Tester.js
CHANGED
|
@@ -460,10 +460,7 @@ export class Tester {
|
|
|
460
460
|
}
|
|
461
461
|
Tester.prototype.any = Tester.prototype._ = any;
|
|
462
462
|
|
|
463
|
-
//
|
|
464
|
-
// function → no-op; same name + different function → throws. Lets plugins
|
|
465
|
-
// extend the tester (e.g., spawnBin, withTempDir, waitFor) without colliding
|
|
466
|
-
// silently when two of them claim the same name.
|
|
463
|
+
// plugin extension point — a same-name collision must fail loudly, not silently override
|
|
467
464
|
export const registerTesterMethod = (name, fn) => {
|
|
468
465
|
if (typeof name !== 'string' || !name)
|
|
469
466
|
throw new TypeError('registerTesterMethod: name must be a non-empty string');
|
|
@@ -15,9 +15,6 @@ export class Reporter {
|
|
|
15
15
|
this.terminating = false;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
// Emit one record to stdout, mirroring console.log's arguments. On Bun,
|
|
19
|
-
// console.log to a subprocess-piped stdout is lossy / stalls under load, so route
|
|
20
|
-
// through process.stdout.write there; Node/Deno keep the proven console.log path.
|
|
21
18
|
writeOut(...args) {
|
|
22
19
|
if (isBun) {
|
|
23
20
|
process.stdout.write(args.map(a => (typeof a == 'string' ? a : String(a))).join(' ') + '\n');
|
|
@@ -34,14 +31,9 @@ export class Reporter {
|
|
|
34
31
|
this.state?.abort();
|
|
35
32
|
}
|
|
36
33
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// assertion, t.signal rejects signal-aware awaits — and remember it so any
|
|
41
|
-
// test that starts afterwards stops at its first assertion too (closing the
|
|
42
|
-
// race where `terminate` lands while the worker is still starting up). The
|
|
43
|
-
// test's cleanup (finally / afterEach / afterAll) still runs. See
|
|
44
|
-
// dev-docs/worker-control-channel.md.
|
|
34
|
+
// `terminating` is remembered so a test that starts after the terminate stops
|
|
35
|
+
// too — closes the race where `terminate` lands mid-startup. Cleanup
|
|
36
|
+
// (finally / afterEach / afterAll) still runs. See dev-docs/worker-control-channel.md.
|
|
45
37
|
terminate() {
|
|
46
38
|
this.terminating = true;
|
|
47
39
|
for (let state = this.state; state; state = state.parent) {
|
|
@@ -61,8 +53,7 @@ export class Reporter {
|
|
|
61
53
|
timer: this.timer
|
|
62
54
|
});
|
|
63
55
|
++this.depth;
|
|
64
|
-
//
|
|
65
|
-
// assertion rather than letting it run to completion.
|
|
56
|
+
// a terminate landed before this test started
|
|
66
57
|
if (this.terminating) {
|
|
67
58
|
this.state.stopTest = true;
|
|
68
59
|
this.state.abort();
|
|
@@ -5,7 +5,7 @@ import EventServer from '../../utils/EventServer.js';
|
|
|
5
5
|
const srcName = new URL('../../', import.meta.url),
|
|
6
6
|
baseName = Bun.pathToFileURL(Bun.cwd + sep);
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export class TestWorker extends EventServer {
|
|
9
9
|
constructor(reporter, numberOfTasks = globalThis.navigator?.hardwareConcurrency || 1, options) {
|
|
10
10
|
super(reporter, numberOfTasks, options);
|
|
11
11
|
this.counter = 0;
|
|
@@ -74,13 +74,10 @@ export default class TestWorker extends EventServer {
|
|
|
74
74
|
const worker = this.idToWorker[id];
|
|
75
75
|
if (!worker) return;
|
|
76
76
|
if (reason === 'done') {
|
|
77
|
-
// The test already finished; just tear the worker down.
|
|
78
77
|
this.#kill(id);
|
|
79
78
|
return;
|
|
80
79
|
}
|
|
81
|
-
//
|
|
82
|
-
// cleanup hooks, then force-kill as a backstop after graceTimeout in case
|
|
83
|
-
// the test ignores the signal and never settles.
|
|
80
|
+
// force-kill backstop: the test may ignore the abort signal and never settle
|
|
84
81
|
if (this.graceTimers[id]) return; // already draining
|
|
85
82
|
try {
|
|
86
83
|
worker.postMessage({type: 'terminate', reason});
|
|
@@ -102,3 +99,5 @@ export default class TestWorker extends EventServer {
|
|
|
102
99
|
}
|
|
103
100
|
}
|
|
104
101
|
}
|
|
102
|
+
|
|
103
|
+
export default TestWorker;
|
|
@@ -38,9 +38,7 @@ let currentReporter = null,
|
|
|
38
38
|
|
|
39
39
|
addEventListener('message', async event => {
|
|
40
40
|
const msg = event.data;
|
|
41
|
-
//
|
|
42
|
-
// abort signal so a running test unwinds (cleanup runs); a terminate that
|
|
43
|
-
// lands before the reporter exists is remembered and applied at startup.
|
|
41
|
+
// a terminate can land before the reporter exists — remember it, apply at startup
|
|
44
42
|
if (msg?.type === 'terminate') {
|
|
45
43
|
pendingTerminate = true;
|
|
46
44
|
currentReporter?.terminate();
|
|
@@ -6,7 +6,7 @@ import EventServer from '../../utils/EventServer.js';
|
|
|
6
6
|
const srcName = new URL('../../', import.meta.url),
|
|
7
7
|
baseName = pathToFileURL(Deno.cwd() + sep);
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export class TestWorker extends EventServer {
|
|
10
10
|
constructor(reporter, numberOfTasks = globalThis.navigator?.hardwareConcurrency || 1, options) {
|
|
11
11
|
super(reporter, numberOfTasks, options);
|
|
12
12
|
this.counter = 0;
|
|
@@ -57,13 +57,10 @@ export default class TestWorker extends EventServer {
|
|
|
57
57
|
const worker = this.idToWorker[id];
|
|
58
58
|
if (!worker) return;
|
|
59
59
|
if (reason === 'done') {
|
|
60
|
-
// The test already finished; just tear the worker down.
|
|
61
60
|
this.#kill(id);
|
|
62
61
|
return;
|
|
63
62
|
}
|
|
64
|
-
//
|
|
65
|
-
// cleanup hooks, then force-kill as a backstop after graceTimeout in case
|
|
66
|
-
// the test ignores the signal and never settles.
|
|
63
|
+
// force-kill backstop: the test may ignore the abort signal and never settle
|
|
67
64
|
if (this.graceTimers[id]) return; // already draining
|
|
68
65
|
try {
|
|
69
66
|
worker.postMessage({type: 'terminate', reason});
|
|
@@ -85,3 +82,5 @@ export default class TestWorker extends EventServer {
|
|
|
85
82
|
}
|
|
86
83
|
}
|
|
87
84
|
}
|
|
85
|
+
|
|
86
|
+
export default TestWorker;
|