tape-six 1.7.14 → 1.9.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 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
- It is written in the modern JavaScript for the modern JavaScript and works in [Node](https://nodejs.org/), [Deno](https://deno.land/), [Bun](https://bun.sh/) and browsers. **Zero runtime dependencies.**
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
 
@@ -319,7 +319,7 @@ The following methods are available (all `msg` arguments are optional):
319
319
  - `any` — a symbol that can be used in deep equivalency asserts to match any value.
320
320
  See [deep6's any](https://github.com/uhop/deep6/wiki/any) for details.
321
321
  - `_` — an alias of `any`.
322
- - `plan(n)` — sets the number of tests in the test suite. Rarely used.
322
+ - `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
323
  - `comment(msg)` — sends a comment to the test harness. Rarely used.
324
324
  - `skipTest(...args, msg)` — skips the current test yet sends a message to the test harness.
325
325
  - `bailOut(msg)` — stops the test suite and sends a message to the test harness.
@@ -425,7 +425,9 @@ 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.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
+ - 1.9.0 _New features: `t.plan(n)` emits a TAP-comment diagnostic on mismatch, `registerTesterMethod(name, fn)` registers tester plugins idempotently. New wiki: 3rd-party library catalog and "Writing plugins" page. Removed `chai` from dev dependencies._
429
+ - 1.8.0 _New subpath modules: `tape-six/server` (HTTP server harness) and `tape-six/response` (response reading helpers for `Response` and `IncomingMessage`)._
430
+ - 1.7.14 _Updated vendored `deep6` to 1.2.0._
429
431
  - 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._
430
432
  - 1.7.12 _Added `--help`/`-h` and `--version`/`-v` options to all CLI utilities. Added flag documentation to help output._
431
433
  - 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 | 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, msg)` | `fn()` throws | |
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)` | Promise rejects (**await it**) | `doesNotResolve` |
135
- | `t.resolves(promise, msg)` | Promise resolves (**await it**) | `doesNotReject` |
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)` — 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
+ - `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
 
@@ -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
- server.listen(port, host);
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
  */
@@ -79,8 +95,11 @@ export declare interface Tester {
79
95
  signal: AbortSignal;
80
96
 
81
97
  /**
82
- * Plans the number of assertions that will be run. Unused.
83
- * @param n - The number of assertions
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)
84
103
  */
85
104
  plan(n: number): void;
86
105
 
@@ -501,6 +520,19 @@ export declare interface Tester {
501
520
  * @param message - Optional message to display if the assertion fails
502
521
  */
503
522
  throws(fn: () => void, message?: string): void;
523
+ /**
524
+ * Asserts that `fn` throws an error matching `matcher`.
525
+ * `matcher` may be:
526
+ * - an `Error` subclass (checks `instanceof`)
527
+ * - a `RegExp` (tested against the error's message, or `String(error)` for non-Errors)
528
+ * - a predicate function (called with the thrown value; truthy return = match)
529
+ * - an object pattern (uses deep6's `match`)
530
+ * - `null` or any primitive (strict equality)
531
+ * @param fn - The function to test
532
+ * @param matcher - The matcher to apply to the thrown value
533
+ * @param message - Optional message to display if the assertion fails
534
+ */
535
+ throws(fn: () => void, matcher: AssertionMatcher, message?: string): void;
504
536
 
505
537
  /**
506
538
  * Asserts that `fn` does not throw an error.
@@ -547,6 +579,14 @@ export declare interface Tester {
547
579
  * @param message - Optional message to display if the assertion fails
548
580
  */
549
581
  rejects(promise: Promise<unknown>, message?: string): Promise<void>;
582
+ /**
583
+ * Asserts that `promise` is rejected with a reason matching `matcher`.
584
+ * See `throws` for the `matcher` semantics.
585
+ * @param promise - The promise to test
586
+ * @param matcher - The matcher to apply to the rejection reason
587
+ * @param message - Optional message to display if the assertion fails
588
+ */
589
+ rejects(promise: Promise<unknown>, matcher: AssertionMatcher, message?: string): Promise<void>;
550
590
 
551
591
  /**
552
592
  * Asserts that `promise` is resolved.
@@ -554,6 +594,14 @@ export declare interface Tester {
554
594
  * @param message - Optional message to display if the assertion fails
555
595
  */
556
596
  resolves(promise: Promise<unknown>, message?: string): Promise<void>;
597
+ /**
598
+ * Asserts that `promise` is resolved with a value matching `matcher`.
599
+ * See `throws` for the `matcher` semantics (applied to the resolved value).
600
+ * @param promise - The promise to test
601
+ * @param matcher - The matcher to apply to the resolved value
602
+ * @param message - Optional message to display if the assertion fails
603
+ */
604
+ resolves(promise: Promise<unknown>, matcher: AssertionMatcher, message?: string): Promise<void>;
557
605
 
558
606
  /**
559
607
  * Returns a code as a string for evaluation that checks if the condition is truthy.
@@ -818,6 +866,17 @@ export declare interface Tester {
818
866
  * @param message - Optional message to display if the assertion fails
819
867
  */
820
868
  doesNotResolve(promise: Promise<unknown>, message?: string): Promise<void>;
869
+ /**
870
+ * Asserts that `promise` is rejected with a reason matching `matcher`. Alias of `rejects`.
871
+ * @param promise - The promise to test
872
+ * @param matcher - The matcher to apply to the rejection reason
873
+ * @param message - Optional message to display if the assertion fails
874
+ */
875
+ doesNotResolve(
876
+ promise: Promise<unknown>,
877
+ matcher: AssertionMatcher,
878
+ message?: string
879
+ ): Promise<void>;
821
880
 
822
881
  /**
823
882
  * Asserts that `promise` is resolved. Alias of `resolves`.
@@ -825,6 +884,17 @@ export declare interface Tester {
825
884
  * @param message - Optional message to display if the assertion fails
826
885
  */
827
886
  doesNotReject(promise: Promise<unknown>, message?: string): Promise<void>;
887
+ /**
888
+ * Asserts that `promise` is resolved with a value matching `matcher`. Alias of `resolves`.
889
+ * @param promise - The promise to test
890
+ * @param matcher - The matcher to apply to the resolved value
891
+ * @param message - Optional message to display if the assertion fails
892
+ */
893
+ doesNotReject(
894
+ promise: Promise<unknown>,
895
+ matcher: AssertionMatcher,
896
+ message?: string
897
+ ): Promise<void>;
828
898
 
829
899
  /**
830
900
  * Returns a code as a string for evaluation that checks if the condition is truthy.
@@ -1184,4 +1254,29 @@ export declare const before: typeof test.before;
1184
1254
  */
1185
1255
  export declare const after: typeof test.after;
1186
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
+
1187
1282
  export default test;
package/index.js CHANGED
@@ -254,6 +254,8 @@ if (!getConfiguredFlag()) {
254
254
  registerNotifyCallback(testRunner);
255
255
  }
256
256
 
257
+ export {registerTesterMethod} from './src/Tester.js';
258
+
257
259
  export {
258
260
  test,
259
261
  before,