tape-six 1.7.13 → 1.8.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 +3 -1
- package/TESTING.md +246 -47
- package/bin/tape6-server.js +19 -19
- package/index.d.ts +67 -0
- package/llms-full.txt +102 -0
- package/llms.txt +66 -0
- package/package.json +13 -4
- package/skills/write-tests/SKILL.md +63 -16
- package/src/State.js +26 -21
- package/src/Tester.js +59 -27
- package/src/deep6/env.d.ts +174 -0
- package/src/deep6/env.js +4 -4
- package/src/deep6/index.d.ts +86 -0
- package/src/deep6/index.js +10 -7
- package/src/deep6/traverse/assemble.d.ts +59 -0
- package/src/deep6/traverse/assemble.js +4 -3
- package/src/deep6/traverse/clone.d.ts +57 -0
- package/src/deep6/traverse/clone.js +4 -2
- package/src/deep6/traverse/deref.d.ts +59 -0
- package/src/deep6/traverse/deref.js +3 -2
- package/src/deep6/traverse/preprocess.d.ts +65 -0
- package/src/deep6/traverse/preprocess.js +2 -1
- package/src/deep6/traverse/walk.d.ts +219 -0
- package/src/deep6/traverse/walk.js +9 -4
- package/src/deep6/unifiers/matchCondition.d.ts +45 -0
- package/src/deep6/unifiers/matchCondition.js +1 -0
- package/src/deep6/unifiers/matchInstanceOf.d.ts +37 -0
- package/src/deep6/unifiers/matchInstanceOf.js +1 -0
- package/src/deep6/unifiers/matchString.d.ts +56 -0
- package/src/deep6/unifiers/matchString.js +1 -0
- package/src/deep6/unifiers/matchTypeOf.d.ts +37 -0
- package/src/deep6/unifiers/matchTypeOf.js +1 -0
- package/src/deep6/unifiers/ref.d.ts +52 -0
- package/src/deep6/unifiers/ref.js +1 -0
- package/src/deep6/unify.d.ts +95 -0
- package/src/deep6/unify.js +130 -66
- package/src/deep6/utils/replaceVars.d.ts +25 -0
- package/src/deep6/utils/replaceVars.js +23 -19
- package/src/response.d.ts +43 -0
- package/src/response.js +57 -0
- package/src/server.d.ts +81 -0
- package/src/server.js +69 -0
- package/src/test.js +26 -53
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[npm-url]: https://npmjs.org/package/tape-six
|
|
5
5
|
|
|
6
6
|
`tape-six` is a [TAP](https://en.wikipedia.org/wiki/Test_Anything_Protocol)-based library for unit tests.
|
|
7
|
-
|
|
7
|
+
Written in modern JavaScript for modern JavaScript runtimes — works in [Node](https://nodejs.org/), [Deno](https://deno.land/), [Bun](https://bun.sh/), and browsers. **Zero runtime dependencies.**
|
|
8
8
|
|
|
9
9
|
It runs ES modules (`import`-based code) natively and supports CommonJS modules transparently using the built-in [ESM](https://nodejs.org/api/esm.html).
|
|
10
10
|
|
|
@@ -425,6 +425,8 @@ Test output can be controlled by flags. See [Supported flags](https://github.com
|
|
|
425
425
|
|
|
426
426
|
The most recent releases:
|
|
427
427
|
|
|
428
|
+
- 1.8.0 _New subpath modules `tape-six/server` (withServer/startServer/setupServer — ephemeral HTTP server harness with port-busy-rejects-not-hangs lifecycle, plus a hook helper for suite-shared servers) and `tape-six/response` (asText/asJson/asBytes/header/headers — reading helpers that work uniformly on W3C Response and Node http.IncomingMessage). Cross-runtime via `node:http` (Node, Bun, Deno). `bin/tape6-server.js` migrated to use `startServer` and `process.exitCode`. `node:` protocol prefix consistently applied to all Node built-in imports; added `types: ["node"]` to tsconfig._
|
|
429
|
+
- 1.7.14 _Updated vendored `deep6` to 1.2.0: added `URL` support (unifier/walker/cloner + processors), named exports alongside default exports, fast path for naked objects, performance optimizations, improved TS typings, removed CommonJS build._
|
|
428
430
|
- 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._
|
|
429
431
|
- 1.7.12 _Added `--help`/`-h` and `--version`/`-v` options to all CLI utilities. Added flag documentation to help output._
|
|
430
432
|
- 1.7.11 _Documentation consistency: added missing sequential test commands to AI docs, fixed test glob references._
|
package/TESTING.md
CHANGED
|
@@ -110,29 +110,55 @@ test.todo('in progress', t => {
|
|
|
110
110
|
|
|
111
111
|
All `msg` arguments are optional. If omitted, a generic message is used.
|
|
112
112
|
|
|
113
|
-
| Method | Checks
|
|
114
|
-
| ------------------------------------ |
|
|
115
|
-
| `t.pass(msg)` | Unconditional pass
|
|
116
|
-
| `t.fail(msg)` | Unconditional fail
|
|
117
|
-
| `t.ok(val, msg)` | `val` is truthy
|
|
118
|
-
| `t.notOk(val, msg)` | `val` is falsy
|
|
119
|
-
| `t.error(err, msg)` | `err` is falsy
|
|
120
|
-
| `t.equal(a, b, msg)` | `a === b`
|
|
121
|
-
| `t.notEqual(a, b, msg)` | `a !== b`
|
|
122
|
-
| `t.deepEqual(a, b, msg)` | Deep strict equality
|
|
123
|
-
| `t.notDeepEqual(a, b, msg)` | Not deeply equal
|
|
124
|
-
| `t.looseEqual(a, b, msg)` | `a == b`
|
|
125
|
-
| `t.notLooseEqual(a, b, msg)` | `a != b`
|
|
126
|
-
| `t.deepLooseEqual(a, b, msg)` | Deep loose equality
|
|
127
|
-
| `t.notDeepLooseEqual(a, b, msg)` | Not deeply loosely equal
|
|
128
|
-
| `t.throws(fn, msg)`
|
|
129
|
-
| `t.doesNotThrow(fn, msg)` | `fn()` does not throw
|
|
130
|
-
| `t.matchString(str, re, msg)` | `str` matches `re`
|
|
131
|
-
| `t.doesNotMatchString(str, re, msg)` | `str` doesn't match `re`
|
|
132
|
-
| `t.match(a, b, msg)` | Structural pattern match
|
|
133
|
-
| `t.doesNotMatch(a, b, msg)` | No structural match
|
|
134
|
-
| `t.rejects(promise, msg)`
|
|
135
|
-
| `t.resolves(promise, msg)`
|
|
113
|
+
| Method | Checks | Common aliases |
|
|
114
|
+
| ------------------------------------ | --------------------------------- | -------------------------------- |
|
|
115
|
+
| `t.pass(msg)` | Unconditional pass | |
|
|
116
|
+
| `t.fail(msg)` | Unconditional fail | |
|
|
117
|
+
| `t.ok(val, msg)` | `val` is truthy | `true`, `assert` |
|
|
118
|
+
| `t.notOk(val, msg)` | `val` is falsy | `false`, `notok` |
|
|
119
|
+
| `t.error(err, msg)` | `err` is falsy | `ifError`, `ifErr` |
|
|
120
|
+
| `t.equal(a, b, msg)` | `a === b` | `is`, `strictEqual`, `isEqual` |
|
|
121
|
+
| `t.notEqual(a, b, msg)` | `a !== b` | `not`, `notStrictEqual`, `isNot` |
|
|
122
|
+
| `t.deepEqual(a, b, msg)` | Deep strict equality | `same`, `isEquivalent` |
|
|
123
|
+
| `t.notDeepEqual(a, b, msg)` | Not deeply equal | `notSame`, `notEquivalent` |
|
|
124
|
+
| `t.looseEqual(a, b, msg)` | `a == b` | |
|
|
125
|
+
| `t.notLooseEqual(a, b, msg)` | `a != b` | |
|
|
126
|
+
| `t.deepLooseEqual(a, b, msg)` | Deep loose equality | |
|
|
127
|
+
| `t.notDeepLooseEqual(a, b, msg)` | Not deeply loosely equal | |
|
|
128
|
+
| `t.throws(fn, m?, msg)` | `fn()` throws (matches `m?`) | |
|
|
129
|
+
| `t.doesNotThrow(fn, msg)` | `fn()` does not throw | |
|
|
130
|
+
| `t.matchString(str, re, msg)` | `str` matches `re` | |
|
|
131
|
+
| `t.doesNotMatchString(str, re, msg)` | `str` doesn't match `re` | |
|
|
132
|
+
| `t.match(a, b, msg)` | Structural pattern match | |
|
|
133
|
+
| `t.doesNotMatch(a, b, msg)` | No structural match | |
|
|
134
|
+
| `t.rejects(promise, m?, msg)` | Rejects (matches `m?`) — `await` | `doesNotResolve` |
|
|
135
|
+
| `t.resolves(promise, m?, msg)` | Resolves (matches `m?`) — `await` | `doesNotReject` |
|
|
136
|
+
|
|
137
|
+
### Matching errors and resolved values
|
|
138
|
+
|
|
139
|
+
`t.throws`, `t.rejects`, and `t.resolves` accept an optional **matcher** as their second argument. If the second argument is a string, it's still treated as the message (backward compatible).
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
t.throws(() => parse(''), TypeError); // Error subclass — instanceof
|
|
143
|
+
t.throws(() => parse(''), /unexpected end/); // RegExp on error.message
|
|
144
|
+
t.throws(
|
|
145
|
+
() => parse(''),
|
|
146
|
+
e => e.code === 'EPARSE'
|
|
147
|
+
); // predicate
|
|
148
|
+
t.throws(() => parse(''), {code: 'EPARSE'}); // deep6 object pattern
|
|
149
|
+
await t.rejects(fetchData(), TypeError, 'wrong type');
|
|
150
|
+
await t.resolves(fetchData(), {status: 200}); // matches resolved value
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Matcher rules:
|
|
154
|
+
|
|
155
|
+
- **Error subclass** (a function whose prototype is Error) → `instanceof` check.
|
|
156
|
+
- **RegExp** → tested against `error.message` for `Error` instances, otherwise against `String(value)`.
|
|
157
|
+
- **Function** (non-Error-class) → predicate; truthy return = match.
|
|
158
|
+
- **Object** (non-null, non-RegExp) → uses deep6's `match()` for partial structural matching.
|
|
159
|
+
- **`null` or any primitive** → strict equality.
|
|
160
|
+
|
|
161
|
+
Falsy reasons (`null`, `0`, `false`, `''`, `NaN`) are correctly handled: `Promise.reject(null)` is still a rejection, and `throw 0` is still a throw.
|
|
136
162
|
|
|
137
163
|
### Async assertions
|
|
138
164
|
|
|
@@ -145,6 +171,69 @@ test('async asserts', async t => {
|
|
|
145
171
|
});
|
|
146
172
|
```
|
|
147
173
|
|
|
174
|
+
### Wildcards in deep equality
|
|
175
|
+
|
|
176
|
+
Use `t.any` (or its alias `t._`) inside an expected value to accept any value at that position. Useful for non-deterministic fields like timestamps or generated IDs:
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
test('partial match', t => {
|
|
180
|
+
const result = {id: 123, name: 'Alice', createdAt: new Date()};
|
|
181
|
+
t.deepEqual(result, {id: t.any, name: 'Alice', createdAt: t.any});
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Async cancellation with `t.signal`
|
|
186
|
+
|
|
187
|
+
`t.signal` is an `AbortSignal` that fires when the test ends, times out, or is stopped. Pass it to long-running async work so it cancels cleanly:
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
test('aborts on test end', async t => {
|
|
191
|
+
const res = await fetch(url, {signal: t.signal});
|
|
192
|
+
t.equal(res.status, 200);
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Test options
|
|
197
|
+
|
|
198
|
+
`test(name, options, fn)` accepts an options object. Type-recognition makes the form flexible (any of `test(opts, fn)`, `test(name, fn)`, `test(name, opts, fn)` is valid).
|
|
199
|
+
|
|
200
|
+
| Option | Effect |
|
|
201
|
+
| ---------------------- | ---------------------------------------------------------- |
|
|
202
|
+
| `name` | Test name (overrides positional) |
|
|
203
|
+
| `timeout` | Milliseconds; test fails and `t.signal` aborts if exceeded |
|
|
204
|
+
| `skip` | Skip the test (does not run) |
|
|
205
|
+
| `todo` | Run, but failures don't count |
|
|
206
|
+
| `beforeAll` / `before` | Run once before the test's first embedded test |
|
|
207
|
+
| `afterAll` / `after` | Run once after the test's last embedded test |
|
|
208
|
+
| `beforeEach` | Run before each embedded test |
|
|
209
|
+
| `afterEach` | Run after each embedded test |
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
test('with timeout', {timeout: 5000}, async t => {
|
|
213
|
+
await longOperation();
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
By default tests have **no timeout**. Set `timeout` per test, or wrap a fixture suite to apply it broadly.
|
|
218
|
+
|
|
219
|
+
### Other tester methods
|
|
220
|
+
|
|
221
|
+
- `t.comment(msg)` — emit a TAP comment line.
|
|
222
|
+
- `t.skipTest(msg)` — skip the **current** test from inside it (e.g. when a precondition isn't met at runtime).
|
|
223
|
+
- `t.bailOut(msg)` — stop the entire run. Catastrophic; use sparingly.
|
|
224
|
+
- `t.plan(n)` — currently a documented no-op kept for migration compatibility from `tape` and `node:test`.
|
|
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
|
+
|
|
227
|
+
```js
|
|
228
|
+
test('OK evaluator', t => {
|
|
229
|
+
const a = 1,
|
|
230
|
+
b = 2,
|
|
231
|
+
c = 'three';
|
|
232
|
+
eval(t.OK('a + b + c === "3three"'));
|
|
233
|
+
// on failure, the report includes: {a: 1, b: 2, c: "three"}
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
148
237
|
### Nested (embedded) tests
|
|
149
238
|
|
|
150
239
|
Tests can be nested. The preferred way is `t.test()` because it makes the delegation explicit. Top-level `test()`/`it()` are equivalent inside a test body — they auto-delegate to the current tester:
|
|
@@ -246,6 +335,127 @@ test('suite B', dbOpts, async t => {
|
|
|
246
335
|
});
|
|
247
336
|
```
|
|
248
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
|
+
|
|
249
459
|
## Migrating from other test frameworks
|
|
250
460
|
|
|
251
461
|
`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.
|
|
@@ -400,30 +610,6 @@ Note: you can also keep using `node:assert` inside tape-six tests — `Assertion
|
|
|
400
610
|
| `expect(a).to.be.ok` (Chai) | `t.ok(a)` or keep `expect` |
|
|
401
611
|
| `expect(fn).to.throw()` (Chai) | `t.throws(fn)` or keep `expect` |
|
|
402
612
|
|
|
403
|
-
### Wildcard matching with `t.any`
|
|
404
|
-
|
|
405
|
-
Use `t.any` (or `t._`) in deep equality checks to match any value:
|
|
406
|
-
|
|
407
|
-
```js
|
|
408
|
-
test('partial match', t => {
|
|
409
|
-
const result = {id: 123, name: 'Alice', createdAt: new Date()};
|
|
410
|
-
t.deepEqual(result, {id: 123, name: 'Alice', createdAt: t.any});
|
|
411
|
-
});
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
### Testing exceptions
|
|
415
|
-
|
|
416
|
-
```js
|
|
417
|
-
test('errors', async t => {
|
|
418
|
-
t.throws(() => {
|
|
419
|
-
throw new Error('boom');
|
|
420
|
-
}, 'should throw');
|
|
421
|
-
t.doesNotThrow(() => 42, 'should not throw');
|
|
422
|
-
await t.rejects(Promise.reject(new Error('fail')), 'should reject');
|
|
423
|
-
await t.resolves(Promise.resolve(42), 'should resolve');
|
|
424
|
-
});
|
|
425
|
-
```
|
|
426
|
-
|
|
427
613
|
### 3rd-party assertion libraries
|
|
428
614
|
|
|
429
615
|
`tape-six` catches `AssertionError` automatically. You can use `chai` or `node:assert`:
|
|
@@ -514,6 +700,18 @@ Common combinations: `FO` (failures only + stop at first), `FOT` (+ show time).
|
|
|
514
700
|
|
|
515
701
|
When multiple `--flags` are given, the last one wins. To see which files are being run (overriding `--flags FO` from a script), append `--flags fo` — lowercase disables the flags.
|
|
516
702
|
|
|
703
|
+
### Other CLI options
|
|
704
|
+
|
|
705
|
+
| Option | Effect |
|
|
706
|
+
| ----------------- | --------------------------------------------------------------------- |
|
|
707
|
+
| `--par N` | Limit `tape6` to `N` parallel workers (default: number of CPU cores). |
|
|
708
|
+
| `--info` | Print the resolved test config (file lists, env, importmap) and exit. |
|
|
709
|
+
| `--self` | Print the path to the runner's own entry script (used in scripts). |
|
|
710
|
+
| `--help`, `-h` | Show usage. |
|
|
711
|
+
| `--version`, `-v` | Show version. |
|
|
712
|
+
|
|
713
|
+
Options that take a value accept both space-separated (`--par 4`) and `=`-separated (`--par=4`) forms.
|
|
714
|
+
|
|
517
715
|
### Environment variables
|
|
518
716
|
|
|
519
717
|
- `TAPE6_FLAGS` — flags string (alternative to `--flags`).
|
|
@@ -521,6 +719,7 @@ When multiple `--flags` are given, the last one wins. To see which files are bei
|
|
|
521
719
|
- `TAPE6_TAP` — force TAP output format.
|
|
522
720
|
- `TAPE6_JSONL` — force JSONL output format.
|
|
523
721
|
- `TAPE6_MIN` — force minimal output format.
|
|
722
|
+
- `TAPE6_WORKER_START_TIMEOUT` — milliseconds to wait for a worker to register its first test before declaring "no tests found". Default: `5000`. Bump this (e.g., `60000`) for tests with heavy `beforeAll` work like Docker container spawn or large fixture loads.
|
|
524
723
|
|
|
525
724
|
## Configuring test discovery
|
|
526
725
|
|
package/bin/tape6-server.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
printVersion,
|
|
14
14
|
printHelp
|
|
15
15
|
} from '../src/utils/config.js';
|
|
16
|
+
import {startServer} from '../src/server.js';
|
|
16
17
|
|
|
17
18
|
const fsp = fs.promises;
|
|
18
19
|
|
|
@@ -241,24 +242,8 @@ const portToString = port => (typeof port === 'string' ? 'pipe' : 'port') + ' '
|
|
|
241
242
|
const host = process.env.HOST || 'localhost',
|
|
242
243
|
port = normalizePort(process.env.PORT || '3000');
|
|
243
244
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
server.on('error', error => {
|
|
247
|
-
if (error.syscall !== 'listen') throw error;
|
|
248
|
-
const bind = portToString(port);
|
|
249
|
-
switch (error.code) {
|
|
250
|
-
case 'EACCES':
|
|
251
|
-
console.log(red('Error: ') + yellow(bind) + red(' requires elevated privileges') + '\n');
|
|
252
|
-
process.exit(1);
|
|
253
|
-
case 'EADDRINUSE':
|
|
254
|
-
console.log(red('Error: ') + yellow(bind) + red(' is already in use') + '\n');
|
|
255
|
-
process.exit(1);
|
|
256
|
-
}
|
|
257
|
-
throw error;
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
server.on('listening', () => {
|
|
261
|
-
//const addr = server.address();
|
|
245
|
+
try {
|
|
246
|
+
await startServer(server, {host, port});
|
|
262
247
|
const bind = portToString(port);
|
|
263
248
|
console.log(
|
|
264
249
|
grey('Listening on ') +
|
|
@@ -274,4 +259,19 @@ server.on('listening', () => {
|
|
|
274
259
|
);
|
|
275
260
|
}
|
|
276
261
|
console.log();
|
|
277
|
-
})
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (error.syscall === 'listen') {
|
|
264
|
+
const bind = portToString(port);
|
|
265
|
+
if (error.code === 'EACCES') {
|
|
266
|
+
console.log(red('Error: ') + yellow(bind) + red(' requires elevated privileges') + '\n');
|
|
267
|
+
process.exitCode = 1;
|
|
268
|
+
} else if (error.code === 'EADDRINUSE') {
|
|
269
|
+
console.log(red('Error: ') + yellow(bind) + red(' is already in use') + '\n');
|
|
270
|
+
process.exitCode = 1;
|
|
271
|
+
} else {
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matcher accepted by `throws`, `rejects`, and `resolves`.
|
|
3
|
+
*
|
|
4
|
+
* - An `Error` subclass — checks `instanceof`.
|
|
5
|
+
* - A `RegExp` — tested against the error's `message` (or `String(value)` for non-Errors).
|
|
6
|
+
* - A predicate function — called with the value; truthy return = match.
|
|
7
|
+
* - An object pattern — uses deep6's `match` for structural matching.
|
|
8
|
+
* - `null` or any primitive — strict equality.
|
|
9
|
+
*/
|
|
10
|
+
export type AssertionMatcher =
|
|
11
|
+
| (new (...args: any[]) => Error)
|
|
12
|
+
| RegExp
|
|
13
|
+
| ((value: unknown) => boolean)
|
|
14
|
+
| object
|
|
15
|
+
| null;
|
|
16
|
+
|
|
1
17
|
/**
|
|
2
18
|
* Test options
|
|
3
19
|
*/
|
|
@@ -501,6 +517,19 @@ export declare interface Tester {
|
|
|
501
517
|
* @param message - Optional message to display if the assertion fails
|
|
502
518
|
*/
|
|
503
519
|
throws(fn: () => void, message?: string): void;
|
|
520
|
+
/**
|
|
521
|
+
* Asserts that `fn` throws an error matching `matcher`.
|
|
522
|
+
* `matcher` may be:
|
|
523
|
+
* - an `Error` subclass (checks `instanceof`)
|
|
524
|
+
* - a `RegExp` (tested against the error's message, or `String(error)` for non-Errors)
|
|
525
|
+
* - a predicate function (called with the thrown value; truthy return = match)
|
|
526
|
+
* - an object pattern (uses deep6's `match`)
|
|
527
|
+
* - `null` or any primitive (strict equality)
|
|
528
|
+
* @param fn - The function to test
|
|
529
|
+
* @param matcher - The matcher to apply to the thrown value
|
|
530
|
+
* @param message - Optional message to display if the assertion fails
|
|
531
|
+
*/
|
|
532
|
+
throws(fn: () => void, matcher: AssertionMatcher, message?: string): void;
|
|
504
533
|
|
|
505
534
|
/**
|
|
506
535
|
* Asserts that `fn` does not throw an error.
|
|
@@ -547,6 +576,14 @@ export declare interface Tester {
|
|
|
547
576
|
* @param message - Optional message to display if the assertion fails
|
|
548
577
|
*/
|
|
549
578
|
rejects(promise: Promise<unknown>, message?: string): Promise<void>;
|
|
579
|
+
/**
|
|
580
|
+
* Asserts that `promise` is rejected with a reason matching `matcher`.
|
|
581
|
+
* See `throws` for the `matcher` semantics.
|
|
582
|
+
* @param promise - The promise to test
|
|
583
|
+
* @param matcher - The matcher to apply to the rejection reason
|
|
584
|
+
* @param message - Optional message to display if the assertion fails
|
|
585
|
+
*/
|
|
586
|
+
rejects(promise: Promise<unknown>, matcher: AssertionMatcher, message?: string): Promise<void>;
|
|
550
587
|
|
|
551
588
|
/**
|
|
552
589
|
* Asserts that `promise` is resolved.
|
|
@@ -554,6 +591,14 @@ export declare interface Tester {
|
|
|
554
591
|
* @param message - Optional message to display if the assertion fails
|
|
555
592
|
*/
|
|
556
593
|
resolves(promise: Promise<unknown>, message?: string): Promise<void>;
|
|
594
|
+
/**
|
|
595
|
+
* Asserts that `promise` is resolved with a value matching `matcher`.
|
|
596
|
+
* See `throws` for the `matcher` semantics (applied to the resolved value).
|
|
597
|
+
* @param promise - The promise to test
|
|
598
|
+
* @param matcher - The matcher to apply to the resolved value
|
|
599
|
+
* @param message - Optional message to display if the assertion fails
|
|
600
|
+
*/
|
|
601
|
+
resolves(promise: Promise<unknown>, matcher: AssertionMatcher, message?: string): Promise<void>;
|
|
557
602
|
|
|
558
603
|
/**
|
|
559
604
|
* Returns a code as a string for evaluation that checks if the condition is truthy.
|
|
@@ -818,6 +863,17 @@ export declare interface Tester {
|
|
|
818
863
|
* @param message - Optional message to display if the assertion fails
|
|
819
864
|
*/
|
|
820
865
|
doesNotResolve(promise: Promise<unknown>, message?: string): Promise<void>;
|
|
866
|
+
/**
|
|
867
|
+
* Asserts that `promise` is rejected with a reason matching `matcher`. Alias of `rejects`.
|
|
868
|
+
* @param promise - The promise to test
|
|
869
|
+
* @param matcher - The matcher to apply to the rejection reason
|
|
870
|
+
* @param message - Optional message to display if the assertion fails
|
|
871
|
+
*/
|
|
872
|
+
doesNotResolve(
|
|
873
|
+
promise: Promise<unknown>,
|
|
874
|
+
matcher: AssertionMatcher,
|
|
875
|
+
message?: string
|
|
876
|
+
): Promise<void>;
|
|
821
877
|
|
|
822
878
|
/**
|
|
823
879
|
* Asserts that `promise` is resolved. Alias of `resolves`.
|
|
@@ -825,6 +881,17 @@ export declare interface Tester {
|
|
|
825
881
|
* @param message - Optional message to display if the assertion fails
|
|
826
882
|
*/
|
|
827
883
|
doesNotReject(promise: Promise<unknown>, message?: string): Promise<void>;
|
|
884
|
+
/**
|
|
885
|
+
* Asserts that `promise` is resolved with a value matching `matcher`. Alias of `resolves`.
|
|
886
|
+
* @param promise - The promise to test
|
|
887
|
+
* @param matcher - The matcher to apply to the resolved value
|
|
888
|
+
* @param message - Optional message to display if the assertion fails
|
|
889
|
+
*/
|
|
890
|
+
doesNotReject(
|
|
891
|
+
promise: Promise<unknown>,
|
|
892
|
+
matcher: AssertionMatcher,
|
|
893
|
+
message?: string
|
|
894
|
+
): Promise<void>;
|
|
828
895
|
|
|
829
896
|
/**
|
|
830
897
|
* Returns a code as a string for evaluation that checks if the condition is truthy.
|
package/llms-full.txt
CHANGED
|
@@ -257,6 +257,108 @@ 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
|
+
## Subpath modules
|
|
261
|
+
|
|
262
|
+
Two helper modules ship as separate subpath imports for tests that need HTTP-shaped fixtures. Both are cross-runtime (Node, Bun, Deno via `node:http`); browsers don't need them because running an HTTP server inside a webpage isn't a use case.
|
|
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:
|
|
309
|
+
|
|
310
|
+
```js
|
|
311
|
+
const server = setupServer(handler);
|
|
312
|
+
|
|
313
|
+
test('first', async t => {
|
|
314
|
+
const res = await fetch(`${server.base}/foo`);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('second', async t => {
|
|
318
|
+
const res = await fetch(`${server.base}/bar`);
|
|
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();
|
|
330
|
+
});
|
|
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
|
+
```
|
|
353
|
+
|
|
354
|
+
- `asText(res)` → `Promise<string>` — body as UTF-8.
|
|
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.
|
|
359
|
+
|
|
360
|
+
For status code, just use `res.status` — same on both `Response` and `IncomingMessage`.
|
|
361
|
+
|
|
260
362
|
## Configuring tests
|
|
261
363
|
|
|
262
364
|
Configuration is read from `tape6.json` or the `"tape6"` section of `package.json`:
|