tape-six 1.10.1 → 1.12.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
@@ -427,6 +427,8 @@ Test output can be controlled by flags. See [Supported flags](https://github.com
427
427
 
428
428
  The most recent releases:
429
429
 
430
+ - 1.12.0 _`tape6-server` is now pluggable, embeddable server for hermetic integration tests, runtime plugin registration, and an opt-in HTTP/2 mode._
431
+ - 1.11.0 _Invariant host hooks for the new zero-dependency `tape-six-invariant` companion package. Workaround for a Deno 2.9.0 `node_modules/.bin` symlink regression._
430
432
  - 1.10.1 _A workaround for a Bun's bug with streams._
431
433
  - 1.10.0 _Worker control channel stops in-flight workers, draining cleanup before a force-kill._
432
434
  - 1.9.0 _New features: `t.plan(n)` emits a TAP-comment diagnostic on mismatch, `registerTesterMethod(name, fn)` registers tester plugins idempotently._
package/TESTING.md CHANGED
@@ -649,7 +649,7 @@ In this example, running `tape6` on Node will execute tests matching `/tests/nod
649
649
 
650
650
  ## Browser testing
651
651
 
652
- Browser tests use `tape6-server`, a static file server bundled with `tape-six` that provides a web UI for running tests.
652
+ Browser tests use `tape6-server`, a pluggable test server bundled with `tape-six` that serves static files and provides a web UI for running tests. Fixture plugins can be mounted under URL prefixes (`--plugin`, the `tape6.server.plugins` config, or `PUT /--plugins` at runtime), the core is embeddable in tests via `createTestServer()` from `tape-six/test-server.js`, and `--h2` switches to HTTP/2 (Node-only). See the [tape6-server wiki page](https://github.com/uhop/tape-six/wiki/Utility-%E2%80%90-tape6%E2%80%90server) for the plugin contract.
653
653
 
654
654
  ### Setup
655
655
 
@@ -0,0 +1,104 @@
1
+ import process from 'node:process';
2
+ import {fileURLToPath} from 'node:url';
3
+
4
+ import {
5
+ getOptions,
6
+ initFiles,
7
+ initReporter,
8
+ showInfo,
9
+ printVersion,
10
+ printHelp,
11
+ printFlagOptions
12
+ } from '../src/utils/config.js';
13
+
14
+ import {getReporter, setReporter} from '../src/test.js';
15
+ import {selectTimer} from '../src/utils/timer.js';
16
+
17
+ import TestWorker from '../src/runners/deno/TestWorker.js';
18
+
19
+ const rootFolder = Deno.cwd();
20
+
21
+ const showSelf = () => {
22
+ const self = new URL(import.meta.url);
23
+ if (self.protocol === 'file:') {
24
+ console.log(fileURLToPath(self));
25
+ } else {
26
+ console.log(self);
27
+ }
28
+ Deno.exit(0);
29
+ };
30
+
31
+ const showVersion = () => {
32
+ printVersion('tape6-deno');
33
+ Deno.exit(0);
34
+ };
35
+
36
+ const showHelp = () => {
37
+ printHelp('tape6-deno', 'Tape6 test runner for Deno', 'tape6-deno [options] [files...]', [
38
+ ['--flags, -f <flags>', 'Set reporter flags (env: TAPE6_FLAGS)'],
39
+ ['--par, -p <n>', 'Set parallelism level (env: TAPE6_PAR)'],
40
+ ['--info', 'Show configuration info and exit'],
41
+ ['--self', 'Print the path to this script and exit'],
42
+ ['--help, -h', 'Show this help message and exit'],
43
+ ['--version, -v', 'Show version and exit']
44
+ ]);
45
+ printFlagOptions();
46
+ Deno.exit(0);
47
+ };
48
+
49
+ const main = async () => {
50
+ const currentOptions = getOptions({
51
+ '--self': {fn: showSelf, isValueRequired: false},
52
+ '--info': {isValueRequired: false},
53
+ '--help': {aliases: ['-h'], fn: showHelp, isValueRequired: false},
54
+ '--version': {aliases: ['-v'], fn: showVersion, isValueRequired: false}
55
+ });
56
+
57
+ const [files] = await Promise.all([
58
+ initFiles(currentOptions.files, rootFolder),
59
+ initReporter(getReporter, setReporter, currentOptions.flags),
60
+ selectTimer()
61
+ ]);
62
+
63
+ addEventListener('error', event => {
64
+ console.log('UNHANDLED ERROR:', event.message);
65
+ event.preventDefault();
66
+ });
67
+
68
+ if (currentOptions.optionFlags['--info'] === '') {
69
+ showInfo(currentOptions, files);
70
+ await new Promise(r => process.stdout.write('', r));
71
+ process.exitCode = 0;
72
+ return;
73
+ }
74
+
75
+ if (!files.length) {
76
+ console.log('No files found.');
77
+ await new Promise(r => process.stdout.write('', r));
78
+ process.exitCode = 1;
79
+ return;
80
+ }
81
+
82
+ const reporter = getReporter(),
83
+ worker = new TestWorker(reporter, currentOptions.parallel, currentOptions.flags);
84
+
85
+ reporter.report({type: 'test', test: 0});
86
+
87
+ await new Promise(resolve => {
88
+ worker.done = () => resolve();
89
+ worker.execute(files);
90
+ });
91
+
92
+ const hasFailed = reporter.state && reporter.state.failed > 0;
93
+
94
+ reporter.report({
95
+ type: 'end',
96
+ test: 0,
97
+ fail: hasFailed
98
+ });
99
+
100
+ await new Promise(r => process.stdout.write('', r));
101
+ process.exitCode = hasFailed ? 1 : 0;
102
+ };
103
+
104
+ main().catch(error => console.error('ERROR:', error));
package/bin/tape6-deno.js CHANGED
@@ -1,106 +1,9 @@
1
1
  #!/usr/bin/env -S deno run --allow-all --ext=js
2
2
 
3
- import process from 'node:process';
4
- import {fileURLToPath} from 'node:url';
3
+ // Deno 2.9.0 doesn't realpath a symlinked entry (denoland/deno#35551), so the
4
+ // `.bin/tape6-deno` symlink would resolve the runner's ../src imports wrong.
5
+ // Realpath this entry, then load the runner (static imports intact) beside it.
6
+ import {fileURLToPath, pathToFileURL} from 'node:url';
5
7
 
6
- import {
7
- getOptions,
8
- initFiles,
9
- initReporter,
10
- showInfo,
11
- printVersion,
12
- printHelp,
13
- printFlagOptions
14
- } from '../src/utils/config.js';
15
-
16
- import {getReporter, setReporter} from '../src/test.js';
17
- import {selectTimer} from '../src/utils/timer.js';
18
-
19
- import TestWorker from '../src/runners/deno/TestWorker.js';
20
-
21
- const rootFolder = Deno.cwd();
22
-
23
- const showSelf = () => {
24
- const self = new URL(import.meta.url);
25
- if (self.protocol === 'file:') {
26
- console.log(fileURLToPath(self));
27
- } else {
28
- console.log(self);
29
- }
30
- Deno.exit(0);
31
- };
32
-
33
- const showVersion = () => {
34
- printVersion('tape6-deno');
35
- Deno.exit(0);
36
- };
37
-
38
- const showHelp = () => {
39
- printHelp('tape6-deno', 'Tape6 test runner for Deno', 'tape6-deno [options] [files...]', [
40
- ['--flags, -f <flags>', 'Set reporter flags (env: TAPE6_FLAGS)'],
41
- ['--par, -p <n>', 'Set parallelism level (env: TAPE6_PAR)'],
42
- ['--info', 'Show configuration info and exit'],
43
- ['--self', 'Print the path to this script and exit'],
44
- ['--help, -h', 'Show this help message and exit'],
45
- ['--version, -v', 'Show version and exit']
46
- ]);
47
- printFlagOptions();
48
- Deno.exit(0);
49
- };
50
-
51
- const main = async () => {
52
- const currentOptions = getOptions({
53
- '--self': {fn: showSelf, isValueRequired: false},
54
- '--info': {isValueRequired: false},
55
- '--help': {aliases: ['-h'], fn: showHelp, isValueRequired: false},
56
- '--version': {aliases: ['-v'], fn: showVersion, isValueRequired: false}
57
- });
58
-
59
- const [files] = await Promise.all([
60
- initFiles(currentOptions.files, rootFolder),
61
- initReporter(getReporter, setReporter, currentOptions.flags),
62
- selectTimer()
63
- ]);
64
-
65
- addEventListener('error', event => {
66
- console.log('UNHANDLED ERROR:', event.message);
67
- event.preventDefault();
68
- });
69
-
70
- if (currentOptions.optionFlags['--info'] === '') {
71
- showInfo(currentOptions, files);
72
- await new Promise(r => process.stdout.write('', r));
73
- process.exitCode = 0;
74
- return;
75
- }
76
-
77
- if (!files.length) {
78
- console.log('No files found.');
79
- await new Promise(r => process.stdout.write('', r));
80
- process.exitCode = 1;
81
- return;
82
- }
83
-
84
- const reporter = getReporter(),
85
- worker = new TestWorker(reporter, currentOptions.parallel, currentOptions.flags);
86
-
87
- reporter.report({type: 'test', test: 0});
88
-
89
- await new Promise(resolve => {
90
- worker.done = () => resolve();
91
- worker.execute(files);
92
- });
93
-
94
- const hasFailed = reporter.state && reporter.state.failed > 0;
95
-
96
- reporter.report({
97
- type: 'end',
98
- test: 0,
99
- fail: hasFailed
100
- });
101
-
102
- await new Promise(r => process.stdout.write('', r));
103
- process.exitCode = hasFailed ? 1 : 0;
104
- };
105
-
106
- main().catch(error => console.error('ERROR:', error));
8
+ const here = pathToFileURL(Deno.realPathSync(fileURLToPath(import.meta.url)));
9
+ await import(new URL('tape6-deno-main.js', here).href);
@@ -1,28 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import http from 'node:http';
4
- import fs from 'node:fs';
5
- import path from 'node:path';
6
3
  import process from 'node:process';
7
4
  import {fileURLToPath} from 'node:url';
8
5
 
9
- import {
10
- getConfig,
11
- resolveTests,
12
- resolvePatterns,
13
- printVersion,
14
- printHelp
15
- } from '../src/utils/config.js';
16
- import {startServer} from '../src/server.js';
17
-
18
- const fsp = fs.promises;
19
-
20
- const toPosix = files =>
21
- path.sep === path.win32.sep
22
- ? files.map(f => f.replaceAll(path.win32.sep, path.posix.sep))
23
- : files;
24
-
25
- // simple static server with no dependencies
6
+ import {printVersion, printHelp} from '../src/utils/config.js';
7
+ import {createTestServer} from '../src/test-server.js';
8
+ import {detectColors, makePaint} from '../src/test-server/trace.js';
26
9
 
27
10
  const showSelf = () => {
28
11
  const self = new URL(import.meta.url);
@@ -34,17 +17,23 @@ const showSelf = () => {
34
17
  process.exit(0);
35
18
  };
36
19
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
37
- printHelp('tape6-server', 'Static file server for browser testing', 'tape6-server [options]', [
20
+ printHelp('tape6-server', 'Pluggable test server for browser testing', 'tape6-server [options]', [
21
+ ['--plugin path', 'Register a plugin module (repeatable)'],
22
+ ['--h2', 'Serve HTTPS with HTTP/2 (+ HTTP/1.1 via ALPN); Node only'],
23
+ ['--no-remote-plugins', 'Disable PUT/DELETE on /--plugins'],
38
24
  ['--trace', 'Enable request trace logging'],
39
25
  ['--self', 'Print the path to this script and exit'],
40
26
  ['--help, -h', 'Show this help message and exit'],
41
27
  ['--version, -v', 'Show version and exit']
42
28
  ]);
43
29
  console.log('\nEnvironment:');
44
- console.log(' HOST Server hostname (default: localhost)');
45
- console.log(' PORT Server port (default: 3000)');
46
- console.log(' SERVER_ROOT Root directory for serving files (default: cwd)');
47
- console.log(' WEBAPP_PATH Path to the web app directory');
30
+ console.log(' HOST Server hostname (default: localhost)');
31
+ console.log(' PORT Server port (default: 3000)');
32
+ console.log(' SERVER_ROOT Root directory for serving files (default: cwd)');
33
+ console.log(' WEBAPP_PATH Path to the web app directory');
34
+ console.log(' TAPE6_PROTOCOL Protocol: h1 or h2 (default: h1)');
35
+ console.log(' TAPE6_CERT Path to a TLS certificate (h2)');
36
+ console.log(' TAPE6_KEY Path to its private key (h2)');
48
37
  process.exit(0);
49
38
  }
50
39
  if (process.argv.includes('--version') || process.argv.includes('-v')) {
@@ -53,182 +42,27 @@ if (process.argv.includes('--version') || process.argv.includes('-v')) {
53
42
  }
54
43
  if (process.argv.includes('--self')) showSelf();
55
44
 
56
- // MIME source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
57
- const mimeTable = {
58
- css: 'text/css',
59
- csv: 'text/csv',
60
- eot: 'application/vnd.ms-fontobject',
61
- gif: 'image/gif',
62
- html: 'text/html',
63
- ico: 'image/vnd.microsoft.icon',
64
- jpg: 'image/jpeg',
65
- js: 'text/javascript',
66
- json: 'application/json',
67
- otf: 'font/otf',
68
- png: 'image/png',
69
- svg: 'image/svg+xml',
70
- ttf: 'font/ttf',
71
- txt: 'text/plain',
72
- webp: 'image/webp',
73
- woff: 'font/woff',
74
- woff2: 'font/woff2',
75
- xml: 'application/xml'
76
- },
77
- defaultMime = 'application/octet-stream',
78
- rootFolder = process.env.SERVER_ROOT || process.cwd(),
79
- traceCalls = process.argv.includes('--trace'),
80
- hasColors =
81
- process.stdout.isTTY &&
82
- (typeof process.stdout.hasColors == 'function' ? process.stdout.hasColors() : true);
83
-
84
- let webAppPath = process.env.WEBAPP_PATH;
85
- if (!webAppPath) {
86
- const url = import.meta.url;
87
- if (!/^file:\/\//i.test(url))
88
- throw Error('Cannot identify the location of the web application. Use WEBAPP_PATH.');
89
- webAppPath = path.relative(
90
- rootFolder,
91
- path.join(path.dirname(fileURLToPath(url)), '../web-app/')
92
- );
93
- if (path.sep === path.win32.sep)
94
- webAppPath = webAppPath.replaceAll(path.win32.sep, path.posix.sep);
95
- }
96
-
97
- // common aliases
98
- const mimeAliases = {mjs: 'js', cjs: 'js', htm: 'html', jpeg: 'jpg'};
99
- Object.keys(mimeAliases).forEach(name => (mimeTable[name] = mimeTable[mimeAliases[name]]));
100
-
101
- // escape codes to use
102
- const join = (...args) => args.map(value => value || '').join(''),
103
- paint = hasColors
104
- ? (prefix, suffix = '\x1B[39m') =>
105
- text =>
106
- join(prefix, text, suffix)
107
- : () => text => text,
108
- grey = paint('\x1B[2;37m', '\x1B[22;39m'),
109
- red = paint('\x1B[41;97m', '\x1B[49;39m'),
110
- green = paint('\x1B[32m'),
111
- yellow = paint('\x1B[93m'),
112
- blue = paint('\x1B[44;97m', '\x1B[49;39m');
113
-
114
- const link = (url, text = url) => paint('\x1B]8;;' + url + '\x1B\\', '\x1B]8;;\x1B\\')(text);
115
-
116
- // sending helpers
117
-
118
- const sendFile = (req, res, fileName, ext, justHeaders) => {
119
- if (!ext) {
120
- ext = path.extname(fileName).toLowerCase();
121
- }
122
- let mime = ext && mimeTable[ext.substring(1)];
123
- if (!mime || typeof mime != 'string') {
124
- mime = defaultMime;
125
- }
126
- res.writeHead(200, {'Content-Type': mime});
127
- if (justHeaders) {
128
- res.end();
129
- } else {
130
- fs.createReadStream(fileName).pipe(res);
131
- }
132
- traceCalls && console.log(green('200') + ' ' + grey(req.method) + ' ' + grey(req.url));
133
- };
134
-
135
- const sendJson = (req, res, json, justHeaders) => {
136
- res.writeHead(200, {'Content-Type': 'application/json'});
137
- if (justHeaders) {
138
- res.end();
139
- } else {
140
- res.end(JSON.stringify(json));
141
- }
142
- traceCalls && console.log(green('200') + ' ' + grey(req.method) + ' ' + grey(req.url));
143
- };
144
-
145
- const sendRedirect = (req, res, to, code = 307) => {
146
- res.writeHead(code, {Location: to});
147
- res.end();
148
- traceCalls && console.log(blue(code) + ' ' + grey(req.method) + ' ' + grey(req.url));
149
- };
150
-
151
- const bailOut = (req, res, code = 404) => {
152
- res.writeHead(code).end();
153
- traceCalls && console.log(red(code) + ' ' + grey(req.method) + ' ' + grey(req.url));
154
- };
155
-
156
- // server
157
-
158
- const server = http.createServer(async (req, res) => {
159
- const method = req.method.toUpperCase();
160
- if (method !== 'GET' && method !== 'HEAD') return bailOut(req, res, 405);
161
-
162
- const url = new URL(req.url, 'http://' + req.headers.host);
163
- if (url.pathname === '/--tests') {
164
- // get tests
165
- return sendJson(
166
- req,
167
- res,
168
- toPosix(await resolveTests(rootFolder, 'browser')),
169
- method === 'HEAD'
170
- );
171
- }
172
- if (url.pathname === '/--patterns') {
173
- // resolve patterns
174
- return sendJson(
175
- req,
176
- res,
177
- toPosix(await resolvePatterns(rootFolder, url.searchParams.getAll('q'))),
178
- method === 'HEAD'
179
- );
180
- }
181
- if (url.pathname === '/--importmap') {
182
- // get import map contents
183
- const cfg = await getConfig(rootFolder);
184
- return sendJson(req, res, cfg.importmap || {imports: {}}, method === 'HEAD');
185
- }
186
- if (url.pathname === '/favicon.ico') {
187
- const faviconFile = path.join(rootFolder, webAppPath, 'favicon.ico');
188
- const stat = await fsp.stat(faviconFile).catch(() => null);
189
- if (stat && stat.isFile()) return sendFile(req, res, faviconFile, '.ico', method === 'HEAD');
190
- return bailOut(req, res);
191
- }
192
- if (url.pathname === '/' || url.pathname === '/index' || url.pathname === '/index.html') {
193
- // redirect to the web app
194
- url.pathname = webAppPath;
195
- return sendRedirect(req, res, url.href);
196
- }
197
-
198
- if (path.normalize(url.pathname).includes('..')) return bailOut(req, res, 403);
199
-
200
- const fileName = path.join(rootFolder, url.pathname),
201
- ext = path.extname(fileName).toLowerCase(),
202
- stat = await fsp.stat(fileName).catch(() => null);
203
- if (stat && stat.isFile()) return sendFile(req, res, fileName, ext, method === 'HEAD');
204
-
205
- if (stat && stat.isDirectory()) {
206
- if (fileName.length && fileName[fileName.length - 1] == path.sep) {
207
- const altFile = path.join(fileName, 'index.html'),
208
- stat = await fsp.stat(altFile).catch(() => null);
209
- if (stat && stat.isFile()) return sendFile(req, res, altFile, '.html', method === 'HEAD');
210
- } else {
211
- url.pathname += path.posix.sep;
212
- return sendRedirect(req, res, url.href);
45
+ const plugins = [];
46
+ let traceCalls = false,
47
+ h2 = false,
48
+ remotePlugins = true;
49
+ {
50
+ const args = process.argv.slice(2);
51
+ for (let i = 0; i < args.length; ++i) {
52
+ const arg = args[i];
53
+ if (arg === '--trace') {
54
+ traceCalls = true;
55
+ } else if (arg === '--h2') {
56
+ h2 = true;
57
+ } else if (arg === '--no-remote-plugins') {
58
+ remotePlugins = false;
59
+ } else if (arg === '--plugin') {
60
+ if (++i < args.length) plugins.push(args[i]);
61
+ } else if (arg.startsWith('--plugin=')) {
62
+ plugins.push(arg.substring('--plugin='.length));
213
63
  }
214
- return bailOut(req, res);
215
- }
216
-
217
- if (!ext && fileName.length && fileName[fileName.length - 1] != path.sep) {
218
- const altFile = fileName + '.html',
219
- stat = await fsp.stat(altFile).catch(() => null);
220
- if (stat && stat.isFile()) return sendFile(req, res, altFile, '.html', method === 'HEAD');
221
64
  }
222
-
223
- bailOut(req, res);
224
- });
225
-
226
- server.on('clientError', (error, socket) => {
227
- if (error.code === 'ECONNRESET' || !socket.writable) return;
228
- socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
229
- });
230
-
231
- // general setup
65
+ }
232
66
 
233
67
  const normalizePort = val => {
234
68
  const port = parseInt(val);
@@ -240,24 +74,47 @@ const normalizePort = val => {
240
74
  const portToString = port => (typeof port === 'string' ? 'pipe' : 'port') + ' ' + port;
241
75
 
242
76
  const host = process.env.HOST || 'localhost',
243
- port = normalizePort(process.env.PORT || '3000');
77
+ port = normalizePort(process.env.PORT || '3000'),
78
+ rootFolder = process.env.SERVER_ROOT || process.cwd(),
79
+ protocol = h2 ? 'h2' : process.env.TAPE6_PROTOCOL || undefined;
80
+
81
+ const hasColors = detectColors(),
82
+ paint = makePaint(hasColors),
83
+ grey = paint('\x1B[2;37m', '\x1B[22;39m'),
84
+ red = paint('\x1B[41;97m', '\x1B[49;39m'),
85
+ yellow = paint('\x1B[93m');
86
+
87
+ const link = (url, text = url) => paint('\x1B]8;;' + url + '\x1B\\', '\x1B]8;;\x1B\\')(text);
244
88
 
245
89
  try {
246
- await startServer(server, {host, port});
247
- const bind = portToString(port);
90
+ const testServer = await createTestServer({
91
+ rootFolder,
92
+ webAppPath: process.env.WEBAPP_PATH,
93
+ host,
94
+ port,
95
+ protocol,
96
+ plugins,
97
+ remotePlugins,
98
+ trace: traceCalls
99
+ });
100
+ const bind = portToString(testServer.port);
248
101
  console.log(
249
102
  grey('Listening on ') +
250
103
  yellow(host || 'all network interfaces') +
104
+ yellow(' (' + testServer.protocol + ')') +
251
105
  grey(' at ') +
252
106
  yellow(bind) +
253
107
  grey(', serving static files from ') +
254
108
  yellow(rootFolder)
255
109
  );
256
- if (host && bind) {
110
+ const mounted = testServer.plugins();
111
+ if (mounted.length) {
257
112
  console.log(
258
- grey('Open ') + link('http://' + host + ':' + port + '/') + grey(' in your browser')
113
+ grey('Plugins: ') +
114
+ yellow(mounted.map(p => p.name + (p.prefix ? ' (' + p.prefix + ')' : '')).join(', '))
259
115
  );
260
116
  }
117
+ console.log(grey('Open ') + link(testServer.base + '/') + grey(' in your browser'));
261
118
  console.log();
262
119
  } catch (error) {
263
120
  if (error.syscall === 'listen') {
package/index.d.ts CHANGED
@@ -8,11 +8,7 @@
8
8
  * - `null` or any primitive — strict equality.
9
9
  */
10
10
  export type AssertionMatcher =
11
- | (new (...args: any[]) => Error)
12
- | RegExp
13
- | ((value: unknown) => boolean)
14
- | object
15
- | null;
11
+ (new (...args: any[]) => Error) | RegExp | ((value: unknown) => boolean) | object | null;
16
12
 
17
13
  /**
18
14
  * Test options
@@ -436,6 +432,15 @@ export declare interface Tester {
436
432
  */
437
433
  ok(value: unknown, message?: string): void;
438
434
 
435
+ /**
436
+ * Reports an assertion from an externally captured source `marker`, so the
437
+ * reported location points at the caller rather than this method. Integration
438
+ * primitive for the `tape-six-invariant` host adapter; not a user-facing
439
+ * assertion. With no `operator`/`expected`/`actual` it behaves like `assert`.
440
+ * @param assertion - the assertion descriptor (verdict, message, marker, info)
441
+ */
442
+ reportAssertion(assertion: TesterAssertion): void;
443
+
439
444
  /**
440
445
  * Asserts that `value` is not truthy.
441
446
  * @param value - The value to test
@@ -1279,4 +1284,31 @@ export declare const after: typeof test.after;
1279
1284
  */
1280
1285
  export declare const registerTesterMethod: (name: string, fn: (...args: any[]) => any) => void;
1281
1286
 
1287
+ /**
1288
+ * Descriptor passed to `Tester.reportAssertion`. The verdict comes from `ok`;
1289
+ * `operator`/`expected`/`actual` are optional reporter metadata (omit them for
1290
+ * a plain truthy assertion). Formed by integration code, not end users.
1291
+ */
1292
+ export declare interface TesterAssertion {
1293
+ /** Pass/fail verdict — a falsy value fails the assertion. */
1294
+ ok: unknown;
1295
+ /** Message shown on failure. */
1296
+ message?: string;
1297
+ /** An `Error` whose stack locates the assertion's source. */
1298
+ marker?: Error;
1299
+ /** Operator label for the reporter (defaults to `'ok'`). */
1300
+ operator?: string;
1301
+ /** Expected value, for the reporter's diff. */
1302
+ expected?: unknown;
1303
+ /** Actual value, for the reporter's diff. */
1304
+ actual?: unknown;
1305
+ }
1306
+
1307
+ /**
1308
+ * Returns the innermost currently-running tester (top of the active test
1309
+ * stack), or `null` when no test is executing. Lets ambient code — such as the
1310
+ * `tape-six-invariant` host adapter — route assertions to the current test.
1311
+ */
1312
+ export declare const getTester: () => Tester | null;
1313
+
1282
1314
  export default test;
package/index.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  getConfiguredFlag,
11
11
  setTestRunner,
12
12
  registerNotifyCallback,
13
+ getTester,
13
14
  before,
14
15
  after,
15
16
  beforeAll,
@@ -29,6 +30,16 @@ import {selectTimer} from './src/utils/timer.js';
29
30
  import defer from './src/utils/defer.js';
30
31
  import TapReporter from './src/reporters/TapReporter.js';
31
32
 
33
+ // Must be at module load, not init(): tape-six-invariant snapshots hasHost at
34
+ // import. See dev-docs/sister-assert-library.md.
35
+ globalThis[Symbol.for('tape6.invariant.host.v1')] ||= {
36
+ version: 1,
37
+ report(assertion) {
38
+ const tester = getTester();
39
+ if (tester) tester.reportAssertion(assertion);
40
+ }
41
+ };
42
+
32
43
  const optionNames = {
33
44
  f: 'failureOnly',
34
45
  t: 'showTime',
@@ -271,13 +282,10 @@ const testRunner = async () => {
271
282
  if (isBrowser && typeof __tape6_reportResults == 'function') {
272
283
  __tape6_reportResults(runHasFailed ? 'failure' : 'success');
273
284
  }
274
-
275
- // registerNotifyCallback(testRunner); // register self again
276
285
  };
277
286
 
278
287
  setTestRunner(testRunner);
279
288
  if (!getConfiguredFlag()) {
280
- // if nobody is running the show
281
289
  registerNotifyCallback(testRunner);
282
290
  }
283
291
 
@@ -291,6 +299,7 @@ export {
291
299
  afterAll,
292
300
  beforeEach,
293
301
  afterEach,
302
+ getTester,
294
303
  test as suite,
295
304
  test as describe,
296
305
  test as it