tape-six 1.8.0 → 1.10.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 +7 -3
- package/TESTING.md +1 -122
- package/index.d.ts +30 -2
- package/index.js +29 -0
- package/llms-full.txt +38 -103
- package/llms.txt +7 -67
- package/package.json +6 -15
- package/skills/run-tests/SKILL.md +77 -0
- package/skills/write-tests/SKILL.md +5 -11
- package/src/OK.js +3 -2
- package/src/State.js +3 -0
- package/src/Tester.js +26 -4
- package/src/reporters/Reporter.js +23 -0
- package/src/runners/bun/TestWorker.js +26 -1
- package/src/runners/bun/worker.js +14 -1
- package/src/runners/deno/TestWorker.js +26 -1
- package/src/runners/deno/worker.js +14 -1
- package/src/runners/node/TestWorker.js +27 -1
- package/src/runners/node/worker.js +14 -1
- package/src/runners/seq/TestWorker.js +9 -1
- package/src/test.js +13 -0
- package/src/utils/EventServer.js +74 -6
- package/src/utils/config.js +30 -7
- package/src/utils/control-channel.js +109 -0
- package/web-app/TestWorker.js +28 -2
package/README.md
CHANGED
|
@@ -122,6 +122,8 @@ See how it can be used in [tests/](https://github.com/uhop/tape-six/tree/master/
|
|
|
122
122
|
|
|
123
123
|
For AI assistants: see [llms.txt](https://github.com/uhop/tape-six/blob/master/llms.txt) and [llms-full.txt](https://github.com/uhop/tape-six/blob/master/llms-full.txt) for LLM-optimized documentation.
|
|
124
124
|
|
|
125
|
+
When you use `tape-six` as a dependency, point your AI coding agent at the guides shipped inside `node_modules/tape-six/`: [`TESTING.md`](https://github.com/uhop/tape-six/blob/master/TESTING.md) and the `skills/` directory (`write-tests`, `run-tests`). They are written so an agent can write and run `tape-six` tests correctly without fetching anything.
|
|
126
|
+
|
|
125
127
|
The whole API is based on two objects: `test` and `Tester`.
|
|
126
128
|
|
|
127
129
|
### `test`
|
|
@@ -319,7 +321,7 @@ The following methods are available (all `msg` arguments are optional):
|
|
|
319
321
|
- `any` — a symbol that can be used in deep equivalency asserts to match any value.
|
|
320
322
|
See [deep6's any](https://github.com/uhop/deep6/wiki/any) for details.
|
|
321
323
|
- `_` — an alias of `any`.
|
|
322
|
-
- `plan(n)` —
|
|
324
|
+
- `plan(n)` — records the expected number of direct assertions; emits a TAP-comment diagnostic at test end if the count diverges (does not fail the test).
|
|
323
325
|
- `comment(msg)` — sends a comment to the test harness. Rarely used.
|
|
324
326
|
- `skipTest(...args, msg)` — skips the current test yet sends a message to the test harness.
|
|
325
327
|
- `bailOut(msg)` — stops the test suite and sends a message to the test harness.
|
|
@@ -425,8 +427,10 @@ Test output can be controlled by flags. See [Supported flags](https://github.com
|
|
|
425
427
|
|
|
426
428
|
The most recent releases:
|
|
427
429
|
|
|
428
|
-
- 1.
|
|
429
|
-
- 1.
|
|
430
|
+
- 1.10.0 _Worker control channel stops in-flight workers, draining cleanup before a force-kill._
|
|
431
|
+
- 1.9.0 _New features: `t.plan(n)` emits a TAP-comment diagnostic on mismatch, `registerTesterMethod(name, fn)` registers tester plugins idempotently._
|
|
432
|
+
- 1.8.0 _New subpath modules: `tape-six/server` (HTTP server harness) and `tape-six/response` (response reading helpers for `Response` and `IncomingMessage`)._
|
|
433
|
+
- 1.7.14 _Updated vendored `deep6` to 1.2.0._
|
|
430
434
|
- 1.7.13 _Replaced `process.exit()` / `Deno.exit()` with `process.exitCode` in test runners and `index.js` for graceful stdout flushing. Updated dependencies, GitHub Actions._
|
|
431
435
|
- 1.7.12 _Added `--help`/`-h` and `--version`/`-v` options to all CLI utilities. Added flag documentation to help output._
|
|
432
436
|
- 1.7.11 _Documentation consistency: added missing sequential test commands to AI docs, fixed test glob references._
|
package/TESTING.md
CHANGED
|
@@ -221,7 +221,7 @@ By default tests have **no timeout**. Set `timeout` per test, or wrap a fixture
|
|
|
221
221
|
- `t.comment(msg)` — emit a TAP comment line.
|
|
222
222
|
- `t.skipTest(msg)` — skip the **current** test from inside it (e.g. when a precondition isn't met at runtime).
|
|
223
223
|
- `t.bailOut(msg)` — stop the entire run. Catastrophic; use sparingly.
|
|
224
|
-
- `t.plan(n)` —
|
|
224
|
+
- `t.plan(n)` — record the expected number of direct assertions. If the count diverges at test end, a `# plan != count: expected N, ran M` TAP comment is emitted (diagnostic only, doesn't fail the test). Subtest assertions don't count toward the parent's plan.
|
|
225
225
|
- `t.OK(expr, msg)` (aliases `t.TRUE`, `t.ASSERT`) — returns a code string for `eval()` that asserts an expression and dumps the values of the top-level identifiers in the expression on failure. Useful for compact arithmetic/state checks. Not usable in CSP-restricted contexts (uses `eval`).
|
|
226
226
|
|
|
227
227
|
```js
|
|
@@ -335,127 +335,6 @@ test('suite B', dbOpts, async t => {
|
|
|
335
335
|
});
|
|
336
336
|
```
|
|
337
337
|
|
|
338
|
-
## Testing HTTP code
|
|
339
|
-
|
|
340
|
-
Two subpath modules ship for tests that need HTTP-shaped fixtures. Both work on Node, Bun, and Deno via `node:http`. Browsers don't get them — running an HTTP server inside a webpage isn't a use case.
|
|
341
|
-
|
|
342
|
-
> **Placement matters.** Test files that import `tape-six/server`, `tape-six/response`, or any other node-only API (e.g. `node:http`, `node:fs`, `node:child_process`) **must not** match a `tape6.tests` pattern that's universal to all environments — the browser worker would try to load them and fail with `Worker error` on the missing built-in. Put them in a CLI-only location (e.g. `tests/cli/test-*.js`) and add the matching glob to `tape6.cli` so it covers Node/Bun/Deno but not browsers. See [Configuring test discovery](#configuring-test-discovery).
|
|
343
|
-
|
|
344
|
-
### `tape-six/server` — server harness
|
|
345
|
-
|
|
346
|
-
```js
|
|
347
|
-
import {withServer, startServer, setupServer} from 'tape-six/server';
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
Three exports for three lifetimes. `withServer` is the per-test scoped resource (95% case). `startServer` is the procedural primitive. `setupServer` registers `beforeAll`/`afterAll` for suite-shared servers.
|
|
351
|
-
|
|
352
|
-
**Per-test server** — start before the test body, tear down in `finally`:
|
|
353
|
-
|
|
354
|
-
```js
|
|
355
|
-
import test from 'tape-six';
|
|
356
|
-
import {withServer} from 'tape-six/server';
|
|
357
|
-
|
|
358
|
-
test('GET / returns 200', t =>
|
|
359
|
-
withServer(myHandler, async base => {
|
|
360
|
-
const res = await fetch(`${base}/`);
|
|
361
|
-
t.equal(res.status, 200);
|
|
362
|
-
}));
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
`myHandler` is a `(req, res) => void` callback (`http.RequestListener`). `base` is the bound URL, e.g. `"http://127.0.0.1:54321"`. Default host is `'127.0.0.1'` (explicit IPv4); pass `{host, port}` to override.
|
|
366
|
-
|
|
367
|
-
`withServer` handles the symmetric case where the test makes requests directly. It's also the right tool when the SUT is something else (a CLI spawned in a subprocess) and the handler is a mock — the test body just looks different:
|
|
368
|
-
|
|
369
|
-
```js
|
|
370
|
-
test('CLI hits the API correctly', t =>
|
|
371
|
-
withServer(mockApi, async base => {
|
|
372
|
-
const result = await spawnCli({env: {API_URL: base}});
|
|
373
|
-
t.equal(result.exitCode, 0);
|
|
374
|
-
t.equal(recordedRequests.length, 1);
|
|
375
|
-
}));
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
Either side may be the SUT. The names `serverHandler` / `clientHandler` reflect role on the wire, not which side is being tested.
|
|
379
|
-
|
|
380
|
-
**Suite-shared server** — register hooks once, share across many tests:
|
|
381
|
-
|
|
382
|
-
```js
|
|
383
|
-
import test, {beforeEach} from 'tape-six';
|
|
384
|
-
import {setupServer} from 'tape-six/server';
|
|
385
|
-
|
|
386
|
-
let recorded;
|
|
387
|
-
const server = setupServer((req, res) => {
|
|
388
|
-
recorded.push({method: req.method, url: req.url});
|
|
389
|
-
res.writeHead(204).end();
|
|
390
|
-
});
|
|
391
|
-
beforeEach(() => {
|
|
392
|
-
recorded = [];
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
test('records exactly one request', async t => {
|
|
396
|
-
await fetch(`${server.base}/foo`);
|
|
397
|
-
t.equal(recorded.length, 1);
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
test('records the path correctly', async t => {
|
|
401
|
-
await fetch(`${server.base}/bar`);
|
|
402
|
-
t.equal(recorded[0].url, '/bar');
|
|
403
|
-
});
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
The returned handle has live getters — `server.base` reads the current state on each access. **Don't destructure** at module load (`const {base} = setupServer(...)`); `base` is `undefined` until `beforeAll` runs. Per-test state reset (the `recorded = []` above) is user-side via your own `beforeEach`; `setupServer` owns the server lifecycle, you own state.
|
|
407
|
-
|
|
408
|
-
**Procedural primitive** — for multi-phase tests or non-test code:
|
|
409
|
-
|
|
410
|
-
```js
|
|
411
|
-
import http from 'node:http';
|
|
412
|
-
import {startServer} from 'tape-six/server';
|
|
413
|
-
|
|
414
|
-
const lc = await startServer(http.createServer(handler), {host, port});
|
|
415
|
-
try {
|
|
416
|
-
// ... lc.base, lc.port, lc.host ...
|
|
417
|
-
} finally {
|
|
418
|
-
await lc.close();
|
|
419
|
-
}
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
`startServer` accepts a fully-constructed server (so the caller can attach `'clientError'` listeners or configure TLS first). It races `'listening'` against `'error'` so port-busy / `EACCES` rejects with the original error rather than hanging — the failure mode existing helpers across the ecosystem suffer from on busy CI machines.
|
|
423
|
-
|
|
424
|
-
`close()` is idempotent and calls `server.closeAllConnections()` (when available, Node 18.2+) so keep-alive sockets from `fetch` don't delay teardown.
|
|
425
|
-
|
|
426
|
-
### `tape-six/response` — response reading helpers
|
|
427
|
-
|
|
428
|
-
```js
|
|
429
|
-
import {asText, asJson, asBytes, header, headers} from 'tape-six/response';
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
Five reading helpers that work uniformly on both W3C `Response` (from `fetch`) and Node `http.IncomingMessage` (from `http.request`).
|
|
433
|
-
|
|
434
|
-
```js
|
|
435
|
-
import test from 'tape-six';
|
|
436
|
-
import {withServer} from 'tape-six/server';
|
|
437
|
-
import {asJson, header} from 'tape-six/response';
|
|
438
|
-
|
|
439
|
-
test('GET / returns JSON', t =>
|
|
440
|
-
withServer(handler, async base => {
|
|
441
|
-
const res = await fetch(`${base}/`);
|
|
442
|
-
t.equal(res.status, 200);
|
|
443
|
-
t.match(header(res, 'content-type'), /^application\/json/);
|
|
444
|
-
const body = await asJson(res);
|
|
445
|
-
t.deepEqual(body, {ok: true});
|
|
446
|
-
}));
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
| Helper | Returns | Notes |
|
|
450
|
-
| ------------------- | ------------------------ | ----------------------------------- |
|
|
451
|
-
| `asText(res)` | `Promise<string>` | UTF-8 |
|
|
452
|
-
| `asJson(res)` | `Promise<unknown>` | parses JSON |
|
|
453
|
-
| `asBytes(res)` | `Promise<Uint8Array>` | raw bytes |
|
|
454
|
-
| `header(res, name)` | `string \| null` | case-insensitive single-header read |
|
|
455
|
-
| `headers(res)` | `Record<string, string>` | all headers, lowercase keys |
|
|
456
|
-
|
|
457
|
-
For status code, just use `res.status` (W3C `Response`) or `res.statusCode` (Node `IncomingMessage`) directly — no helper needed.
|
|
458
|
-
|
|
459
338
|
## Migrating from other test frameworks
|
|
460
339
|
|
|
461
340
|
`tape-six` supports `describe`/`it` and `before`/`after` aliases. When called inside a test body, all top-level functions automatically delegate to the current tester. This means migration from Mocha, Jest, or `node:test` is nearly mechanical — change the import and swap assertions.
|
package/index.d.ts
CHANGED
|
@@ -95,8 +95,11 @@ export declare interface Tester {
|
|
|
95
95
|
signal: AbortSignal;
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
98
|
+
* Records the expected number of direct assertions. When the test ends, a
|
|
99
|
+
* `# plan != count: expected N, ran M` TAP comment is emitted if the count
|
|
100
|
+
* diverges. Diagnostic only — does not fail the test. Subtest assertions
|
|
101
|
+
* don't count toward the parent's plan.
|
|
102
|
+
* @param n - The expected number of assertions (non-negative integer)
|
|
100
103
|
*/
|
|
101
104
|
plan(n: number): void;
|
|
102
105
|
|
|
@@ -1251,4 +1254,29 @@ export declare const before: typeof test.before;
|
|
|
1251
1254
|
*/
|
|
1252
1255
|
export declare const after: typeof test.after;
|
|
1253
1256
|
|
|
1257
|
+
/**
|
|
1258
|
+
* Idempotently install a method on `Tester.prototype` so it appears as a
|
|
1259
|
+
* method on every tester instance (`t.<name>(...)`). Same name + same function
|
|
1260
|
+
* is a no-op (allows duplicate side-effect imports). Same name + a different
|
|
1261
|
+
* function throws so collisions surface loudly.
|
|
1262
|
+
*
|
|
1263
|
+
* Usage from a plugin module (e.g., `tape-six-spawn`):
|
|
1264
|
+
*
|
|
1265
|
+
* ```ts
|
|
1266
|
+
* import {registerTesterMethod} from 'tape-six/Tester.js';
|
|
1267
|
+
*
|
|
1268
|
+
* declare module 'tape-six' {
|
|
1269
|
+
* interface Tester {
|
|
1270
|
+
* spawnBin(bin: string, args: string[]): Promise<{code: number; stdout: string; stderr: string}>;
|
|
1271
|
+
* }
|
|
1272
|
+
* }
|
|
1273
|
+
*
|
|
1274
|
+
* registerTesterMethod('spawnBin', async function (bin, args) { ... });
|
|
1275
|
+
* ```
|
|
1276
|
+
*
|
|
1277
|
+
* @param name - non-empty method name
|
|
1278
|
+
* @param fn - function to install on `Tester.prototype`
|
|
1279
|
+
*/
|
|
1280
|
+
export declare const registerTesterMethod: (name: string, fn: (...args: any[]) => any) => void;
|
|
1281
|
+
|
|
1254
1282
|
export default test;
|
package/index.js
CHANGED
|
@@ -162,6 +162,33 @@ const init = async () => {
|
|
|
162
162
|
setCurrentReporter?.(reporter);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
if (isBrowser && typeof window.addEventListener == 'function') {
|
|
166
|
+
// Control plane: the parent page posts `tape6-terminate` to drain a running
|
|
167
|
+
// test (failOnce / bail / worker deadline). The reporter arms stopTest +
|
|
168
|
+
// fires the abort signal so the test unwinds. There is no in-page
|
|
169
|
+
// force-kill — see dev-docs/worker-control-channel.md.
|
|
170
|
+
window.addEventListener('message', event => {
|
|
171
|
+
if (event.data?.type === 'tape6-terminate') getReporter()?.terminate();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (isCli) {
|
|
176
|
+
// Control plane (proc transport): a child spawned by tape-six-proc is
|
|
177
|
+
// marked with TAPE6_CONTROL. Open the stdin control channel so the parent
|
|
178
|
+
// can drain / terminate it, and so it stays alive after `end` until told to
|
|
179
|
+
// exit — see dev-docs/worker-control-channel.md.
|
|
180
|
+
const controlled =
|
|
181
|
+
(isDeno && Deno.env.get('TAPE6_CONTROL')) ||
|
|
182
|
+
(isBun && Bun.env.TAPE6_CONTROL) ||
|
|
183
|
+
(isNode && process.env.TAPE6_CONTROL);
|
|
184
|
+
if (controlled) {
|
|
185
|
+
const {listenControlChannel} = await import(
|
|
186
|
+
new URL('./src/utils/control-channel.js', import.meta.url)
|
|
187
|
+
);
|
|
188
|
+
listenControlChannel(getReporter);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
165
192
|
return {options, isBrowser, isBun, isDeno, isNode, isCli};
|
|
166
193
|
};
|
|
167
194
|
|
|
@@ -254,6 +281,8 @@ if (!getConfiguredFlag()) {
|
|
|
254
281
|
registerNotifyCallback(testRunner);
|
|
255
282
|
}
|
|
256
283
|
|
|
284
|
+
export {registerTesterMethod} from './src/Tester.js';
|
|
285
|
+
|
|
257
286
|
export {
|
|
258
287
|
test,
|
|
259
288
|
before,
|
package/llms-full.txt
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
- Before/after hooks: `beforeAll`, `afterAll`, `beforeEach`, `afterEach`
|
|
11
11
|
- `test()` is aliased as `suite()`, `describe()`, and `it()` for easy migration
|
|
12
12
|
- When called inside a test body, top-level functions auto-delegate to the current tester
|
|
13
|
-
-
|
|
13
|
+
- BYO assertion / mocking / property-based libs: `node:assert`, `chai`, `expect`, `node:test` mock, `sinon`, `fast-check`
|
|
14
14
|
|
|
15
15
|
## Quick start
|
|
16
16
|
|
|
@@ -192,7 +192,7 @@ The `Tester` object is passed to test functions. All `msg` arguments are optiona
|
|
|
192
192
|
|
|
193
193
|
### Miscellaneous
|
|
194
194
|
|
|
195
|
-
- `plan(n)` —
|
|
195
|
+
- `plan(n)` — record expected number of direct assertions. On test end, emits a `# plan != count: expected N, ran M` TAP comment if the count diverges (diagnostic only, doesn't fail the test). Subtest assertions don't count toward the parent's plan.
|
|
196
196
|
- `comment(msg)` — send a comment to the reporter.
|
|
197
197
|
- `skipTest(...args, msg)` — skip current test with a message.
|
|
198
198
|
- `bailOut(msg)` — abort the test suite.
|
|
@@ -257,107 +257,27 @@ test('suite', opts, async t => {
|
|
|
257
257
|
|
|
258
258
|
Multiple hooks of the same type run in registration order (before) or reverse order (after).
|
|
259
259
|
|
|
260
|
-
##
|
|
260
|
+
## Plugins
|
|
261
261
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
### tape-six/server — HTTP server harness
|
|
265
|
-
|
|
266
|
-
```js
|
|
267
|
-
import {withServer, startServer, setupServer} from 'tape-six/server';
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
#### `withServer(serverHandler, clientHandler, opts?)` — scoped resource (95% case)
|
|
271
|
-
|
|
272
|
-
Spins up an `http.Server` with `serverHandler`, runs `clientHandler` with the bound base URL, tears down in `finally`. Cleanup runs whether `clientHandler` resolves, rejects, or throws synchronously. Returns whatever `clientHandler` returns.
|
|
273
|
-
|
|
274
|
-
```js
|
|
275
|
-
test('GET / returns 200', t =>
|
|
276
|
-
withServer(handler, async base => {
|
|
277
|
-
const res = await fetch(`${base}/`);
|
|
278
|
-
t.equal(res.status, 200);
|
|
279
|
-
}));
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
`serverHandler` is the per-request callback Node invokes on each incoming request. `clientHandler` is the per-scope test body, called once with `(base, lifecycle)` — naming reflects role on the wire (server side / client side), not which side is the SUT. Either side may be the code under test.
|
|
283
|
-
|
|
284
|
-
`clientHandler` may make HTTP requests itself (`fetch(base)`-style), or set up a separate SUT that does (e.g., a spawned CLI given `${base}` as its endpoint env var), or do neither (multi-phase setup + assertions).
|
|
285
|
-
|
|
286
|
-
#### `startServer(server, opts?)` — procedural primitive
|
|
287
|
-
|
|
288
|
-
For multi-phase tests that span the lifecycle, or non-test code (e.g. `bin/tape6-server`) that wants long-term control. Accepts a fully-constructed `http.Server` (so callers can attach `'clientError'` handlers, configure TLS, etc., before listen).
|
|
289
|
-
|
|
290
|
-
```js
|
|
291
|
-
const lc = await startServer(http.createServer(handler), {host, port});
|
|
292
|
-
// ... use lc.base ...
|
|
293
|
-
await lc.close();
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
Returns a lifecycle handle:
|
|
297
|
-
|
|
298
|
-
- `server` — the underlying `http.Server`.
|
|
299
|
-
- `base` — bound base URL, e.g. `"http://127.0.0.1:54321"`.
|
|
300
|
-
- `port` — actual bound port (OS-assigned when `port: 0`).
|
|
301
|
-
- `host` — bound host.
|
|
302
|
-
- `close()` — idempotent. Calls `server.closeAllConnections()` (when available) so keep-alive sockets don't delay teardown.
|
|
303
|
-
|
|
304
|
-
Races `'listening'` against `'error'`: port-busy / `EACCES` rejects with the original error rather than hanging. Existing helpers across the toolkit ecosystem don't do this and have hung CI machines as a result.
|
|
305
|
-
|
|
306
|
-
#### `setupServer(serverHandler, opts?)` — hook helper
|
|
307
|
-
|
|
308
|
-
Registers `beforeAll` (start) and `afterAll` (close) and returns a live-getter handle for suite-shared servers:
|
|
262
|
+
Tester methods can be added via `registerTesterMethod(name, fn)`. Same name + same fn → no-op (idempotent re-import); same name + different fn → throws (loud collision detection).
|
|
309
263
|
|
|
310
264
|
```js
|
|
311
|
-
|
|
265
|
+
import {registerTesterMethod} from 'tape-six';
|
|
312
266
|
|
|
313
|
-
|
|
314
|
-
|
|
267
|
+
registerTesterMethod('spawnBin', async function (bin, args) {
|
|
268
|
+
// ... implementation, can call this.equal(...), this.reporter.report({...}), etc.
|
|
315
269
|
});
|
|
316
270
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
The returned object is `Object.freeze`d and uses property getters that read the live lifecycle. Don't destructure at module load (`const {base} = setupServer(...)`) — `base` is `undefined` until `beforeAll` runs.
|
|
323
|
-
|
|
324
|
-
State reset stays user-side. For mock-server scenarios that need per-test state clearing, compose your own `beforeEach`:
|
|
325
|
-
|
|
326
|
-
```js
|
|
327
|
-
const server = setupServer((req, res) => {
|
|
328
|
-
recorded.push({method: req.method, url: req.url});
|
|
329
|
-
res.writeHead(204).end();
|
|
271
|
+
// then in tests:
|
|
272
|
+
test('cli', async t => {
|
|
273
|
+
const {code} = await t.spawnBin('node', ['-v']);
|
|
274
|
+
t.equal(code, 0);
|
|
330
275
|
});
|
|
331
|
-
let recorded;
|
|
332
|
-
beforeEach(() => { recorded = []; });
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
`setupServer` owns the suite lifecycle; the caller owns suite state.
|
|
336
|
-
|
|
337
|
-
#### Options
|
|
338
|
-
|
|
339
|
-
```ts
|
|
340
|
-
interface ServerOptions {
|
|
341
|
-
host?: string; // default '127.0.0.1' — explicit IPv4 avoids dual-stack ambiguity
|
|
342
|
-
port?: number; // default 0 — OS-assigned
|
|
343
|
-
}
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
### tape-six/response — HTTP response helpers
|
|
347
|
-
|
|
348
|
-
Reading helpers that work uniformly with both `Response` (fetch results) and Node `http.IncomingMessage`:
|
|
349
|
-
|
|
350
|
-
```js
|
|
351
|
-
import {asText, asJson, asBytes, header, headers} from 'tape-six/response';
|
|
352
276
|
```
|
|
353
277
|
|
|
354
|
-
- `
|
|
355
|
-
- `asJson(res)` → `Promise<unknown>` — body parsed as JSON.
|
|
356
|
-
- `asBytes(res)` → `Promise<Uint8Array>` — body as raw bytes.
|
|
357
|
-
- `header(res, name)` → `string | null` — case-insensitive single-header read. Array-valued `IncomingMessage` headers (e.g. `set-cookie`) are joined with `, `.
|
|
358
|
-
- `headers(res)` → `Record<string, string>` — all headers as a plain object with lowercase keys.
|
|
278
|
+
`Tester` is declared as an `interface` in `index.d.ts` to make TS module augmentation natural — plugins extend the interface, no full class shape needed. The built-in `t.OK()` evaluator (`src/OK.js`) uses this exact pattern.
|
|
359
279
|
|
|
360
|
-
|
|
280
|
+
Plugin installation is **per-file**. `tape6-seq` runs all files in one process; `tape6` uses workers; `tape6-proc` (from the sister package `tape-six-proc`) uses subprocesses; browser tests run in iframes. Each isolation context has its own module graph, so a plugin import in file A is invisible to file B when they're in different contexts. Every test file that uses a plugin must import it directly. `registerTesterMethod`'s idempotency makes repeated imports safe. See [Writing plugins](https://github.com/uhop/tape-six/wiki/Writing-plugins) for the full guide.
|
|
361
281
|
|
|
362
282
|
## Configuring tests
|
|
363
283
|
|
|
@@ -458,6 +378,8 @@ Browser: `http://localhost:3000/?flags=FO`
|
|
|
458
378
|
- `TAPE6_JSONL` — force JSONL reporter (any non-empty value).
|
|
459
379
|
- `TAPE6_MIN` — force minimal reporter (any non-empty value).
|
|
460
380
|
- `TAPE6_TEST_FILE_NAME` — set by runners to identify the current test file.
|
|
381
|
+
- `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).
|
|
382
|
+
- `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`.
|
|
461
383
|
|
|
462
384
|
## Browser testing
|
|
463
385
|
|
|
@@ -473,19 +395,18 @@ Browser: `http://localhost:3000/?flags=FO`
|
|
|
473
395
|
- tape-six-puppeteer (https://www.npmjs.com/package/tape-six-puppeteer) — automates browser testing with Puppeteer.
|
|
474
396
|
- tape-six-playwright (https://www.npmjs.com/package/tape-six-playwright) — automates browser testing with Playwright.
|
|
475
397
|
|
|
476
|
-
## 3rd-party
|
|
398
|
+
## 3rd-party libraries (bring-your-own)
|
|
477
399
|
|
|
478
|
-
`tape-six`
|
|
400
|
+
`tape-six` doesn't bundle assertion / mock / property-based libraries. It catches whatever the test throws — anything throwing a node-compatible `AssertionError` (with `name`, `operator`, `actual`, `expected`) is reported as a regular assertion; other errors are reported as `UNEXPECTED EXCEPTION` (test still fails).
|
|
479
401
|
|
|
480
|
-
|
|
481
|
-
import test from 'tape-six';
|
|
482
|
-
import {assert, expect} from 'chai';
|
|
402
|
+
**Verified candidates** (full per-lib examples and browser importmap snippets in the wiki):
|
|
483
403
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}
|
|
488
|
-
|
|
404
|
+
- `node:assert` — built-in to Node/Bun/Deno, throws `AssertionError`. Cleanest path.
|
|
405
|
+
- `chai` — `expect`/`should`/`assert` styles, throws `AssertionError`. Works in browsers via importmap entry.
|
|
406
|
+
- `expect` (Jest's standalone) — throws plain `Error`, not `AssertionError`. Works but failures render as `UNEXPECTED EXCEPTION` with ANSI-colored messages. Wrap negative cases in `t.throws()`.
|
|
407
|
+
- `node:test` mock — built-in spies/stubs/timers via `import {mock} from 'node:test'`. Doesn't throw; assert on `spy.mock.calls` with `t.*`.
|
|
408
|
+
- `sinon` — same pattern as `node:test` mock. Assert on `spy.callCount` / `spy.firstCall.args`.
|
|
409
|
+
- `fast-check` — property-based testing. Throws plain `Error` on counterexample (with seed + path for repro). Wrap negative cases in `t.throws()`.
|
|
489
410
|
|
|
490
411
|
```js
|
|
491
412
|
import test from 'tape-six';
|
|
@@ -497,4 +418,18 @@ test('with node:assert', t => {
|
|
|
497
418
|
});
|
|
498
419
|
```
|
|
499
420
|
|
|
421
|
+
```js
|
|
422
|
+
import test from 'tape-six';
|
|
423
|
+
import {mock} from 'node:test';
|
|
424
|
+
|
|
425
|
+
test('with node:test mock', t => {
|
|
426
|
+
const spy = mock.fn();
|
|
427
|
+
spy(1, 2);
|
|
428
|
+
t.equal(spy.mock.calls.length, 1);
|
|
429
|
+
t.deepEqual(spy.mock.calls[0].arguments, [1, 2]);
|
|
430
|
+
});
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Wiki: [3rd-party assertion libraries](https://github.com/uhop/tape-six/wiki/3rd%E2%80%90party-assertion-libraries), [mock libraries](https://github.com/uhop/tape-six/wiki/3rd%E2%80%90party-mock-libraries), [property-based testing](https://github.com/uhop/tape-six/wiki/3rd%E2%80%90party-property-based-testing).
|
|
434
|
+
|
|
500
435
|
Assertions that throw `AssertionError` are automatically caught and reported.
|
package/llms.txt
CHANGED
|
@@ -105,11 +105,15 @@ The object passed to test functions. Provides assertions and test control.
|
|
|
105
105
|
|
|
106
106
|
#### Utilities
|
|
107
107
|
|
|
108
|
-
- `t.plan(n)` —
|
|
108
|
+
- `t.plan(n)` — record expected direct-assertion count; emits a `# plan != count: expected N, ran M` TAP comment at test end on mismatch (diagnostic, doesn't fail).
|
|
109
109
|
- `t.comment(msg)` — emit a comment.
|
|
110
110
|
- `t.skipTest(msg)` — skip current test with a message.
|
|
111
111
|
- `t.bailOut(msg)` — abort entire test suite.
|
|
112
112
|
|
|
113
|
+
### Plugins
|
|
114
|
+
|
|
115
|
+
- `registerTesterMethod(name, fn)` — install a method on `Tester.prototype` from a plugin module. Idempotent for same `fn`; throws on collision with a different `fn`. Plugin imports are per-file (workers / subprocesses / iframes don't share module graphs).
|
|
116
|
+
|
|
113
117
|
### Hooks
|
|
114
118
|
|
|
115
119
|
Hooks can be registered as standalone functions or via test options or Tester methods.
|
|
@@ -139,37 +143,6 @@ test('suite', async t => {
|
|
|
139
143
|
test.beforeAll(() => { /* ... */ });
|
|
140
144
|
```
|
|
141
145
|
|
|
142
|
-
## Subpath modules
|
|
143
|
-
|
|
144
|
-
### tape-six/server — HTTP server harness
|
|
145
|
-
|
|
146
|
-
Helpers for tests that need an ephemeral `node:http` server. Cross-runtime (Node, Bun, Deno; not for browser-side tests).
|
|
147
|
-
|
|
148
|
-
```js
|
|
149
|
-
import {withServer, startServer, setupServer} from 'tape-six/server';
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
- `withServer(serverHandler, clientHandler, opts?)` — scoped resource: spin up a server, run the test body with the base URL, tear down in `finally`. The 95% case.
|
|
153
|
-
- `startServer(server, opts?)` — procedural primitive: returns `{server, base, port, host, close}`. For multi-phase tests or non-test code (e.g., `bin/tape6-server`) that wants long-term control. Races `'listening'` against `'error'` so port-busy rejects instead of hanging.
|
|
154
|
-
- `setupServer(serverHandler, opts?)` — registers `beforeAll`/`afterAll` and returns a live-getter context for suite-shared servers. Don't destructure the returned object at module load — properties read live state on each access.
|
|
155
|
-
- Options: `{host = '127.0.0.1', port = 0}`. Default host is explicit IPv4.
|
|
156
|
-
|
|
157
|
-
`serverHandler` is the per-request callback (Node calls it once per incoming request). `clientHandler` is the per-scope test body (called once with the base URL). Either side may be the SUT.
|
|
158
|
-
|
|
159
|
-
### tape-six/response — HTTP response helpers
|
|
160
|
-
|
|
161
|
-
Reading helpers that work uniformly with both `Response` (fetch results) and `http.IncomingMessage` (Node low-level requests).
|
|
162
|
-
|
|
163
|
-
```js
|
|
164
|
-
import {asText, asJson, asBytes, header, headers} from 'tape-six/response';
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
- `asText(res)` — body as UTF-8 string.
|
|
168
|
-
- `asJson(res)` — body parsed as JSON.
|
|
169
|
-
- `asBytes(res)` — body as `Uint8Array`.
|
|
170
|
-
- `header(res, name)` — single header value, case-insensitive. Returns `null` if absent.
|
|
171
|
-
- `headers(res)` — all headers as a plain object with lowercase keys.
|
|
172
|
-
|
|
173
146
|
## Common patterns
|
|
174
147
|
|
|
175
148
|
### Basic test file
|
|
@@ -244,41 +217,6 @@ describe('my module', () => {
|
|
|
244
217
|
});
|
|
245
218
|
```
|
|
246
219
|
|
|
247
|
-
### Testing HTTP code with `withServer`
|
|
248
|
-
|
|
249
|
-
```js
|
|
250
|
-
import test from 'tape-six';
|
|
251
|
-
import {withServer} from 'tape-six/server';
|
|
252
|
-
import {asJson} from 'tape-six/response';
|
|
253
|
-
|
|
254
|
-
test('GET /users returns the list', t =>
|
|
255
|
-
withServer(myHandler, async base => {
|
|
256
|
-
const res = await fetch(`${base}/users`);
|
|
257
|
-
t.equal(res.status, 200);
|
|
258
|
-
const body = await asJson(res);
|
|
259
|
-
t.equal(body.length, 3);
|
|
260
|
-
}));
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
Suite-shared server (multiple tests against one instance):
|
|
264
|
-
|
|
265
|
-
```js
|
|
266
|
-
import test, {beforeEach} from 'tape-six';
|
|
267
|
-
import {setupServer} from 'tape-six/server';
|
|
268
|
-
|
|
269
|
-
const server = setupServer((req, res) => {
|
|
270
|
-
recorded.push({method: req.method, url: req.url});
|
|
271
|
-
res.writeHead(204).end();
|
|
272
|
-
});
|
|
273
|
-
let recorded;
|
|
274
|
-
beforeEach(() => { recorded = []; });
|
|
275
|
-
|
|
276
|
-
test('records one request', async t => {
|
|
277
|
-
await fetch(`${server.base}/foo`);
|
|
278
|
-
t.equal(recorded.length, 1);
|
|
279
|
-
});
|
|
280
|
-
```
|
|
281
|
-
|
|
282
220
|
### Skip and TODO
|
|
283
221
|
|
|
284
222
|
```js
|
|
@@ -303,6 +241,8 @@ npx tape6-server --trace # start browser test server
|
|
|
303
241
|
- `TAPE6_JSONL` — force JSONL reporter (any non-empty value).
|
|
304
242
|
- `TAPE6_MIN` — force minimal reporter (any non-empty value).
|
|
305
243
|
- `TAPE6_TEST_FILE_NAME` — set by runners to identify the current test file.
|
|
244
|
+
- `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).
|
|
245
|
+
- `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`.
|
|
306
246
|
|
|
307
247
|
## Configuration (package.json)
|
|
308
248
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tape-six",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.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",
|
|
@@ -8,15 +8,6 @@
|
|
|
8
8
|
"types": "index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./index.js",
|
|
11
|
-
"./server": {
|
|
12
|
-
"types": "./src/server.d.ts",
|
|
13
|
-
"default": "./src/server.js"
|
|
14
|
-
},
|
|
15
|
-
"./response": {
|
|
16
|
-
"types": "./src/response.d.ts",
|
|
17
|
-
"default": "./src/response.js"
|
|
18
|
-
},
|
|
19
|
-
"./bin/*": "./bin/*",
|
|
20
11
|
"./*": "./src/*"
|
|
21
12
|
},
|
|
22
13
|
"bin": {
|
|
@@ -98,21 +89,21 @@
|
|
|
98
89
|
"/tests/test-*.cjs",
|
|
99
90
|
"/tests/cli/test-*.js"
|
|
100
91
|
],
|
|
92
|
+
"node": [
|
|
93
|
+
"/tests/node/test-*.js"
|
|
94
|
+
],
|
|
101
95
|
"browser": [
|
|
102
96
|
"/tests/browser/test-*.html"
|
|
103
97
|
],
|
|
104
98
|
"importmap": {
|
|
105
99
|
"imports": {
|
|
106
100
|
"tape-six": "../index.js",
|
|
107
|
-
"tape-six/": "../src/"
|
|
108
|
-
"chai": "../node_modules/chai/index.js"
|
|
101
|
+
"tape-six/": "../src/"
|
|
109
102
|
}
|
|
110
103
|
}
|
|
111
104
|
},
|
|
112
105
|
"devDependencies": {
|
|
113
|
-
"@types/
|
|
114
|
-
"@types/node": "^25.6.0",
|
|
115
|
-
"chai": "^6.2.2",
|
|
106
|
+
"@types/node": "^25.9.1",
|
|
116
107
|
"typescript": "^6.0.3"
|
|
117
108
|
}
|
|
118
109
|
}
|