tape-six 1.8.0 → 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
@@ -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,8 +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.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
+ - 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._
430
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._
431
432
  - 1.7.12 _Added `--help`/`-h` and `--version`/`-v` options to all CLI utilities. Added flag documentation to help output._
432
433
  - 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)` — currently a documented no-op kept for migration compatibility from `tape` and `node:test`.
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
package/index.d.ts CHANGED
@@ -95,8 +95,11 @@ export declare interface Tester {
95
95
  signal: AbortSignal;
96
96
 
97
97
  /**
98
- * Plans the number of assertions that will be run. Unused.
99
- * @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)
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
@@ -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,
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
- - Compatible with `AssertionError`-based libraries like `node:assert` and `chai`
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)` — set expected number of assertions.
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,6 +257,28 @@ 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
+ ## Plugins
261
+
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).
263
+
264
+ ```js
265
+ import {registerTesterMethod} from 'tape-six';
266
+
267
+ registerTesterMethod('spawnBin', async function (bin, args) {
268
+ // ... implementation, can call this.equal(...), this.reporter.report({...}), etc.
269
+ });
270
+
271
+ // then in tests:
272
+ test('cli', async t => {
273
+ const {code} = await t.spawnBin('node', ['-v']);
274
+ t.equal(code, 0);
275
+ });
276
+ ```
277
+
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.
279
+
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.
281
+
260
282
  ## Subpath modules
261
283
 
262
284
  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.
@@ -473,19 +495,18 @@ Browser: `http://localhost:3000/?flags=FO`
473
495
  - tape-six-puppeteer (https://www.npmjs.com/package/tape-six-puppeteer) — automates browser testing with Puppeteer.
474
496
  - tape-six-playwright (https://www.npmjs.com/package/tape-six-playwright) — automates browser testing with Playwright.
475
497
 
476
- ## 3rd-party assertion libraries
498
+ ## 3rd-party libraries (bring-your-own)
477
499
 
478
- `tape-six` supports `AssertionError`-based assertions. You can use `node:assert` or `chai` inside test functions:
500
+ `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
501
 
480
- ```js
481
- import test from 'tape-six';
482
- import {assert, expect} from 'chai';
502
+ **Verified candidates** (full per-lib examples and browser importmap snippets in the wiki):
483
503
 
484
- test('with chai', t => {
485
- expect(1).to.be.lessThan(2);
486
- assert.deepEqual([1], [1]);
487
- });
488
- ```
504
+ - `node:assert` — built-in to Node/Bun/Deno, throws `AssertionError`. Cleanest path.
505
+ - `chai` — `expect`/`should`/`assert` styles, throws `AssertionError`. Works in browsers via importmap entry.
506
+ - `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()`.
507
+ - `node:test` mock — built-in spies/stubs/timers via `import {mock} from 'node:test'`. Doesn't throw; assert on `spy.mock.calls` with `t.*`.
508
+ - `sinon` — same pattern as `node:test` mock. Assert on `spy.callCount` / `spy.firstCall.args`.
509
+ - `fast-check` — property-based testing. Throws plain `Error` on counterexample (with seed + path for repro). Wrap negative cases in `t.throws()`.
489
510
 
490
511
  ```js
491
512
  import test from 'tape-six';
@@ -497,4 +518,18 @@ test('with node:assert', t => {
497
518
  });
498
519
  ```
499
520
 
521
+ ```js
522
+ import test from 'tape-six';
523
+ import {mock} from 'node:test';
524
+
525
+ test('with node:test mock', t => {
526
+ const spy = mock.fn();
527
+ spy(1, 2);
528
+ t.equal(spy.mock.calls.length, 1);
529
+ t.deepEqual(spy.mock.calls[0].arguments, [1, 2]);
530
+ });
531
+ ```
532
+
533
+ 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).
534
+
500
535
  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)` — declare expected assertion count (rarely needed).
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tape-six",
3
- "version": "1.8.0",
3
+ "version": "1.9.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",
@@ -98,21 +98,21 @@
98
98
  "/tests/test-*.cjs",
99
99
  "/tests/cli/test-*.js"
100
100
  ],
101
+ "node": [
102
+ "/tests/node/test-*.js"
103
+ ],
101
104
  "browser": [
102
105
  "/tests/browser/test-*.html"
103
106
  ],
104
107
  "importmap": {
105
108
  "imports": {
106
109
  "tape-six": "../index.js",
107
- "tape-six/": "../src/",
108
- "chai": "../node_modules/chai/index.js"
110
+ "tape-six/": "../src/"
109
111
  }
110
112
  }
111
113
  },
112
114
  "devDependencies": {
113
- "@types/chai": "^5.2.3",
114
115
  "@types/node": "^25.6.0",
115
- "chai": "^6.2.2",
116
116
  "typescript": "^6.0.3"
117
117
  }
118
118
  }
package/src/OK.js CHANGED
@@ -1,4 +1,4 @@
1
- import {Tester, setAliases} from './Tester.js';
1
+ import {registerTesterMethod, setAliases} from './Tester.js';
2
2
 
3
3
  // code mostly borrowed from https://github.com/heya/ice/blob/master/assert.js under BSD-3
4
4
 
@@ -22,7 +22,7 @@ const listVariables = (code, self) => {
22
22
  return '{' + result.join(',') + '}';
23
23
  };
24
24
 
25
- Tester.prototype.OK = function OK(condition, msg, options) {
25
+ const OK = function OK(condition, msg, options) {
26
26
  if (typeof condition != 'string') {
27
27
  throw new TypeError('Condition must be a string');
28
28
  }
@@ -44,4 +44,5 @@ Tester.prototype.OK = function OK(condition, msg, options) {
44
44
  }))`;
45
45
  };
46
46
 
47
+ registerTesterMethod('OK', OK);
47
48
  setAliases('OK', 'TRUE, ASSERT');
package/src/State.js CHANGED
@@ -101,6 +101,8 @@ export class State {
101
101
  this.failOnce = failOnce || parent.failOnce;
102
102
  this.offset = parent.asserts || 0;
103
103
  this.asserts = this.skipped = this.failed = 0;
104
+ // direct assertions in this state only — not bumped by updateParent, used by t.plan()
105
+ this.localAsserts = 0;
104
106
  this.stopTest = false;
105
107
  this.timer = timer || parent.timer || getTimer();
106
108
  this.startTime = this.time = time || this.timer.now();
@@ -178,6 +180,7 @@ export class State {
178
180
 
179
181
  if (event.type === 'assert' || event.type === 'assertion-error') {
180
182
  ++this.asserts;
183
+ ++this.localAsserts;
181
184
  event.skip && ++this.skipped;
182
185
  isFailed && ++this.failed;
183
186
  event.id = this.asserts + this.offset;
package/src/Tester.js CHANGED
@@ -48,8 +48,10 @@ export class Tester {
48
48
  return this.reporter.state;
49
49
  }
50
50
 
51
- plan(_n) {
52
- // nothing to do
51
+ plan(n) {
52
+ if (typeof n !== 'number' || !Number.isInteger(n) || n < 0)
53
+ throw new TypeError('plan(n) requires a non-negative integer');
54
+ this.planned = n;
53
55
  }
54
56
 
55
57
  comment(msg) {
@@ -448,8 +450,28 @@ export class Tester {
448
450
  }
449
451
  Tester.prototype.any = Tester.prototype._ = any;
450
452
 
451
- export const setAliases = (source, aliases) =>
452
- aliases.split(', ').forEach(alias => (Tester.prototype[alias] = Tester.prototype[source]));
453
+ // Idempotent registration of a method on Tester.prototype. Same name + same
454
+ // function no-op; same name + different function → throws. Lets plugins
455
+ // extend the tester (e.g., spawnBin, withTempDir, waitFor) without colliding
456
+ // silently when two of them claim the same name.
457
+ export const registerTesterMethod = (name, fn) => {
458
+ if (typeof name !== 'string' || !name)
459
+ throw new TypeError('registerTesterMethod: name must be a non-empty string');
460
+ if (typeof fn !== 'function') throw new TypeError('registerTesterMethod: fn must be a function');
461
+ if (Object.prototype.hasOwnProperty.call(Tester.prototype, name)) {
462
+ if (Tester.prototype[name] !== fn)
463
+ throw new Error(
464
+ `registerTesterMethod: '${name}' is already registered with a different implementation`
465
+ );
466
+ return;
467
+ }
468
+ Tester.prototype[name] = fn;
469
+ };
470
+
471
+ export const setAliases = (source, aliases) => {
472
+ const fn = Tester.prototype[source];
473
+ aliases.split(', ').forEach(alias => registerTesterMethod(alias, fn));
474
+ };
453
475
 
454
476
  setAliases('ok', 'true, assert');
455
477
  setAliases('notOk', 'false, notok');
package/src/test.js CHANGED
@@ -227,6 +227,19 @@ export const runTests = async tests => {
227
227
  await tester.dispose();
228
228
  await tester.state?.runAfterAll();
229
229
  testers.pop();
230
+ if (
231
+ tester.planned !== undefined &&
232
+ !tester.state?.skip &&
233
+ tester.state?.localAsserts !== tester.planned
234
+ ) {
235
+ tester.reporter.report({
236
+ type: 'comment',
237
+ name: `plan != count: expected ${tester.planned}, ran ${tester.state.localAsserts}`,
238
+ test: testNumber,
239
+ marker: new Error(),
240
+ time: tester.timer.now()
241
+ });
242
+ }
230
243
  tester.reporter.report({
231
244
  type: 'end',
232
245
  name: options.name,