tape-six 1.10.1 → 1.11.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,7 @@ 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.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
431
  - 1.10.1 _A workaround for a Bun's bug with streams._
431
432
  - 1.10.0 _Worker control channel stops in-flight workers, draining cleanup before a force-kill._
432
433
  - 1.9.0 _New features: `t.plan(n)` emits a TAP-comment diagnostic on mismatch, `registerTesterMethod(name, fn)` registers tester plugins idempotently._
@@ -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);
@@ -22,8 +22,6 @@ const toPosix = files =>
22
22
  ? files.map(f => f.replaceAll(path.win32.sep, path.posix.sep))
23
23
  : files;
24
24
 
25
- // simple static server with no dependencies
26
-
27
25
  const showSelf = () => {
28
26
  const self = new URL(import.meta.url);
29
27
  if (self.protocol === 'file:') {
@@ -53,7 +51,6 @@ if (process.argv.includes('--version') || process.argv.includes('-v')) {
53
51
  }
54
52
  if (process.argv.includes('--self')) showSelf();
55
53
 
56
- // MIME source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
57
54
  const mimeTable = {
58
55
  css: 'text/css',
59
56
  csv: 'text/csv',
@@ -94,11 +91,9 @@ if (!webAppPath) {
94
91
  webAppPath = webAppPath.replaceAll(path.win32.sep, path.posix.sep);
95
92
  }
96
93
 
97
- // common aliases
98
94
  const mimeAliases = {mjs: 'js', cjs: 'js', htm: 'html', jpeg: 'jpg'};
99
95
  Object.keys(mimeAliases).forEach(name => (mimeTable[name] = mimeTable[mimeAliases[name]]));
100
96
 
101
- // escape codes to use
102
97
  const join = (...args) => args.map(value => value || '').join(''),
103
98
  paint = hasColors
104
99
  ? (prefix, suffix = '\x1B[39m') =>
@@ -113,8 +108,6 @@ const join = (...args) => args.map(value => value || '').join(''),
113
108
 
114
109
  const link = (url, text = url) => paint('\x1B]8;;' + url + '\x1B\\', '\x1B]8;;\x1B\\')(text);
115
110
 
116
- // sending helpers
117
-
118
111
  const sendFile = (req, res, fileName, ext, justHeaders) => {
119
112
  if (!ext) {
120
113
  ext = path.extname(fileName).toLowerCase();
@@ -153,15 +146,12 @@ const bailOut = (req, res, code = 404) => {
153
146
  traceCalls && console.log(red(code) + ' ' + grey(req.method) + ' ' + grey(req.url));
154
147
  };
155
148
 
156
- // server
157
-
158
149
  const server = http.createServer(async (req, res) => {
159
150
  const method = req.method.toUpperCase();
160
151
  if (method !== 'GET' && method !== 'HEAD') return bailOut(req, res, 405);
161
152
 
162
153
  const url = new URL(req.url, 'http://' + req.headers.host);
163
154
  if (url.pathname === '/--tests') {
164
- // get tests
165
155
  return sendJson(
166
156
  req,
167
157
  res,
@@ -170,7 +160,6 @@ const server = http.createServer(async (req, res) => {
170
160
  );
171
161
  }
172
162
  if (url.pathname === '/--patterns') {
173
- // resolve patterns
174
163
  return sendJson(
175
164
  req,
176
165
  res,
@@ -179,7 +168,6 @@ const server = http.createServer(async (req, res) => {
179
168
  );
180
169
  }
181
170
  if (url.pathname === '/--importmap') {
182
- // get import map contents
183
171
  const cfg = await getConfig(rootFolder);
184
172
  return sendJson(req, res, cfg.importmap || {imports: {}}, method === 'HEAD');
185
173
  }
@@ -190,7 +178,6 @@ const server = http.createServer(async (req, res) => {
190
178
  return bailOut(req, res);
191
179
  }
192
180
  if (url.pathname === '/' || url.pathname === '/index' || url.pathname === '/index.html') {
193
- // redirect to the web app
194
181
  url.pathname = webAppPath;
195
182
  return sendRedirect(req, res, url.href);
196
183
  }
@@ -228,8 +215,6 @@ server.on('clientError', (error, socket) => {
228
215
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
229
216
  });
230
217
 
231
- // general setup
232
-
233
218
  const normalizePort = val => {
234
219
  const port = parseInt(val);
235
220
  if (isNaN(port)) return val; // named pipe
package/index.d.ts CHANGED
@@ -436,6 +436,15 @@ export declare interface Tester {
436
436
  */
437
437
  ok(value: unknown, message?: string): void;
438
438
 
439
+ /**
440
+ * Reports an assertion from an externally captured source `marker`, so the
441
+ * reported location points at the caller rather than this method. Integration
442
+ * primitive for the `tape-six-invariant` host adapter; not a user-facing
443
+ * assertion. With no `operator`/`expected`/`actual` it behaves like `assert`.
444
+ * @param assertion - the assertion descriptor (verdict, message, marker, info)
445
+ */
446
+ reportAssertion(assertion: TesterAssertion): void;
447
+
439
448
  /**
440
449
  * Asserts that `value` is not truthy.
441
450
  * @param value - The value to test
@@ -1279,4 +1288,31 @@ export declare const after: typeof test.after;
1279
1288
  */
1280
1289
  export declare const registerTesterMethod: (name: string, fn: (...args: any[]) => any) => void;
1281
1290
 
1291
+ /**
1292
+ * Descriptor passed to `Tester.reportAssertion`. The verdict comes from `ok`;
1293
+ * `operator`/`expected`/`actual` are optional reporter metadata (omit them for
1294
+ * a plain truthy assertion). Formed by integration code, not end users.
1295
+ */
1296
+ export declare interface TesterAssertion {
1297
+ /** Pass/fail verdict — a falsy value fails the assertion. */
1298
+ ok: unknown;
1299
+ /** Message shown on failure. */
1300
+ message?: string;
1301
+ /** An `Error` whose stack locates the assertion's source. */
1302
+ marker?: Error;
1303
+ /** Operator label for the reporter (defaults to `'ok'`). */
1304
+ operator?: string;
1305
+ /** Expected value, for the reporter's diff. */
1306
+ expected?: unknown;
1307
+ /** Actual value, for the reporter's diff. */
1308
+ actual?: unknown;
1309
+ }
1310
+
1311
+ /**
1312
+ * Returns the innermost currently-running tester (top of the active test
1313
+ * stack), or `null` when no test is executing. Lets ambient code — such as the
1314
+ * `tape-six-invariant` host adapter — route assertions to the current test.
1315
+ */
1316
+ export declare const getTester: () => Tester | null;
1317
+
1282
1318
  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
package/llms-full.txt CHANGED
@@ -279,6 +279,15 @@ test('cli', async t => {
279
279
 
280
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
281
 
282
+ ### Ambient integrations
283
+
284
+ Beyond `registerTesterMethod`, two exports let code that does not receive `t` route assertions into the running test:
285
+
286
+ - `getTester()` returns the innermost running `Tester` (top of the active test stack), or `null` outside a test.
287
+ - `Tester.reportAssertion({ok, message?, marker?, operator?, expected?, actual?})` reports an assertion using an externally captured `marker`, so the reported source location is the caller's, not the adapter's. With no `operator`/`expected`/`actual` it behaves like `assert`/`ok`.
288
+
289
+ `index.js` installs an invariant host at `globalThis[Symbol.for('tape6.invariant.host.v1')]` (`{version, report(assertion)}`) at module load. This is the coordination point for the planned zero-dependency `tape-six-invariant` package, whose `check()` calls materialize as real assertions on the current test under a tape6 run and are inert/configurable otherwise. See `dev-docs/sister-assert-library.md`.
290
+
282
291
  ## Configuring tests
283
292
 
284
293
  Configuration is read from `tape6.json` or the `"tape6"` section of `package.json`:
package/llms.txt CHANGED
@@ -113,6 +113,8 @@ The object passed to test functions. Provides assertions and test control.
113
113
  ### Plugins
114
114
 
115
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
+ - `getTester()` — the innermost running `Tester` (top of the active test stack), or `null` outside a test. Lets ambient code route assertions to the current test.
117
+ - `Tester.reportAssertion({ok, message?, marker?, operator?, expected?, actual?})` — report an assertion with an externally captured `marker` so the source location points at the caller; defaults to an `assert`/`ok`-shaped result. Integration primitive (e.g. the planned `tape-six-invariant` host at `globalThis[Symbol.for('tape6.invariant.host.v1')]`), not a user-facing assertion.
116
118
 
117
119
  ### Hooks
118
120
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tape-six",
3
- "version": "1.10.1",
3
+ "version": "1.11.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",
@@ -103,7 +103,7 @@
103
103
  }
104
104
  },
105
105
  "devDependencies": {
106
- "@types/node": "^25.9.1",
106
+ "@types/node": "^26.0.1",
107
107
  "typescript": "^6.0.3"
108
108
  }
109
109
  }
package/src/State.js CHANGED
@@ -63,7 +63,6 @@ const replacer =
63
63
  if (value instanceof Map)
64
64
  return {type: 'Map', value: Object.fromEntries(value), [signature]: signature};
65
65
 
66
- // break circular references
67
66
  if (seen.has(value)) return {type: 'Circular', [signature]: signature};
68
67
  seen.add(value);
69
68
  }
@@ -75,14 +74,10 @@ const serialize = object => {
75
74
  try {
76
75
  const result = JSON.stringify(object, replacer());
77
76
  if (typeof result == 'string') return result;
78
- } catch (error) {
79
- // squelch
80
- }
77
+ } catch (error) {}
81
78
  try {
82
79
  return JSON.stringify({type: 'String', value: String(object)});
83
- } catch (error) {
84
- // squelch
85
- }
80
+ } catch (error) {}
86
81
  return JSON.stringify({
87
82
  type: 'Problem',
88
83
  value: 'cannot convert value to JSON or string',
package/src/Tester.js CHANGED
@@ -93,8 +93,6 @@ export class Tester {
93
93
  });
94
94
  }
95
95
 
96
- // asserts
97
-
98
96
  pass(msg) {
99
97
  this.reporter.report({
100
98
  name: msg || 'pass',
@@ -133,6 +131,20 @@ export class Tester {
133
131
  });
134
132
  }
135
133
 
134
+ reportAssertion(assertion) {
135
+ const {ok, message, marker, operator, expected, actual} = assertion || {};
136
+ const hasData = expected !== undefined || actual !== undefined;
137
+ this.reporter.report({
138
+ name: message || 'invariant',
139
+ test: this.testNumber,
140
+ marker: marker instanceof Error ? marker : new Error(),
141
+ time: this.timer.now(),
142
+ operator: operator || 'ok',
143
+ fail: !ok,
144
+ data: hasData ? {expected, actual} : {expected: true, actual: ok}
145
+ });
146
+ }
147
+
136
148
  notOk(value, msg) {
137
149
  this.reporter.report({
138
150
  name: msg || 'should be falsy',
@@ -445,8 +457,6 @@ export class Tester {
445
457
  });
446
458
  });
447
459
  }
448
-
449
- // missing: T (eval)
450
460
  }
451
461
  Tester.prototype.any = Tester.prototype._ = any;
452
462
 
@@ -5,8 +5,6 @@ import {signature} from '../State.js';
5
5
  import {normalizeBox, padBox, padBoxLeft, drawBox, stackHorizontally} from '../utils/box.js';
6
6
  import {formatNumber, formatTime} from '../utils/formatters.js';
7
7
 
8
- // colors
9
-
10
8
  const join = (...args) => args.filter(value => value).join('');
11
9
  const to6 = x => Math.min(5, Math.round((Math.max(0, Math.min(255, x)) / 255) * 6));
12
10
  const buildColor = (r, g, b) => 16 + 36 * to6(r) + 6 * to6(g) + to6(b);
@@ -16,8 +14,6 @@ const successStyle = `\x1B[48;5;${buildColor(0, 32, 0)};1;97m`,
16
14
  skippedStyle = `\x1B[48;5;${buildColor(0, 0, 64)};1;97m`,
17
15
  reset = '\x1B[0m';
18
16
 
19
- // misc
20
-
21
17
  const consoleDict = {
22
18
  log: 'log',
23
19
  info: 'inf',
@@ -35,8 +31,6 @@ const getType = value => {
35
31
  return className ? type + '/' + className : type;
36
32
  };
37
33
 
38
- // main
39
-
40
34
  export class TTYReporter extends Reporter {
41
35
  constructor({
42
36
  output = process.stdout,
@@ -80,7 +74,6 @@ export class TTYReporter extends Reporter {
80
74
  this.lines = 0;
81
75
  this.testStack = [];
82
76
 
83
- // colors
84
77
  this.red = this.paint('\x1B[31m');
85
78
  this.green = this.paint('\x1B[92m');
86
79
  this.blue = this.paint('\x1B[94m');
@@ -98,8 +91,6 @@ export class TTYReporter extends Reporter {
98
91
  this.stdoutPaint = this.paint('\x1B[90m');
99
92
  this.stderrPaint = this.paint('\x1B[31m');
100
93
 
101
- // watching for console output
102
-
103
94
  this.consoleWasUsed = false;
104
95
  this.consoleSkipChecks = false;
105
96
 
@@ -416,7 +407,6 @@ export class TTYReporter extends Reporter {
416
407
  );
417
408
 
418
409
  box2[0] = this.brightWhite(box2[0]);
419
- // box2[1] = this.brightYellow(box2[1]);
420
410
  box2[2] = this.green(box2[2]);
421
411
  box2[3] = this.red(box2[3]);
422
412
  box2[4] = this.blue(box2[4]);
@@ -18,7 +18,6 @@ export default class TestWorker extends EventServer {
18
18
  id = String(++this.counter),
19
19
  worker = new Worker(new URL('./worker.js', import.meta.url), {
20
20
  type: 'module'
21
- // deno: {permissions: 'inherit'}
22
21
  });
23
22
  this.idToWorker[id] = worker;
24
23
  worker.addEventListener('message', event => {
@@ -1,4 +1,3 @@
1
- // Faking Reporter
2
1
  export class BypassReporter {
3
2
  constructor(reporter, reportTo) {
4
3
  this.reporter = reporter;
package/src/test.js CHANGED
@@ -38,9 +38,7 @@ export const getTesters = () => testers;
38
38
  export const getTester = () => (testers.length ? testers[testers.length - 1] : null);
39
39
 
40
40
  const processArgs = (name, options, testFn) => {
41
- // normalize arguments
42
41
  if (typeof name == 'string') {
43
- // nothing
44
42
  } else if (typeof options == 'string') {
45
43
  [name, options] = [options, name];
46
44
  } else if (typeof testFn == 'string') {
@@ -49,12 +47,10 @@ const processArgs = (name, options, testFn) => {
49
47
  [name, options, testFn] = [null, name, options];
50
48
  }
51
49
  if (typeof options == 'object') {
52
- // nothing
53
50
  } else {
54
51
  [options, testFn] = [testFn, options];
55
52
  }
56
53
 
57
- // normalize options
58
54
  options = {...options};
59
55
  if (name && typeof name == 'string') {
60
56
  options.name = name;
@@ -330,8 +326,6 @@ Tester.prototype.asPromise = async function asPromise(name, options, testFn) {
330
326
  this.comment('SKIP test: ' + options.name);
331
327
  };
332
328
 
333
- // before/after hooks
334
-
335
329
  const addToHook = name => fn => {
336
330
  if (testers.length) {
337
331
  const tester = testers[testers.length - 1];
@@ -29,7 +29,6 @@ export default class EventServer {
29
29
  this.retained = {};
30
30
  this.readyQueue = [];
31
31
 
32
- // control plane bookkeeping
33
32
  this.liveTasks = new Set();
34
33
  this.deadlineTimers = {};
35
34
  this.stopRequested = false;
@@ -76,7 +75,6 @@ export default class EventServer {
76
75
  }
77
76
  }
78
77
  if (this.passThroughId === id) {
79
- // dump ready events
80
78
  for (const events of this.readyQueue) {
81
79
  for (const event of events) {
82
80
  this.reporter.report(event, true);
@@ -88,12 +86,10 @@ export default class EventServer {
88
86
  const events = this.retained[id];
89
87
  if (events) {
90
88
  if (this.passThroughId === null) {
91
- // dump events
92
89
  for (const event of events) {
93
90
  this.reporter.report(event, true);
94
91
  }
95
92
  } else {
96
- // add to the ready queue
97
93
  this.readyQueue.push(events);
98
94
  }
99
95
  delete this.retained[id];
@@ -149,7 +145,6 @@ export default class EventServer {
149
145
  }
150
146
  }
151
147
  makeTask(fileName) {
152
- // TBD in children
153
148
  // should return a task id as a string
154
149
  }
155
150
  destroyTask(id, reason) {
@@ -83,14 +83,12 @@ export const resolvePatterns = async (rootFolder, patterns) => {
83
83
  export const getConfig = async (rootFolder, traceFn) => {
84
84
  let cfg = null;
85
85
 
86
- // check tape6.json
87
86
  try {
88
87
  cfg = JSON.parse(await fsp.readFile(path.join(rootFolder, 'tape6.json')));
89
88
  } catch (error) {
90
89
  traceFn && traceFn('Cannot read tape6.json');
91
90
  }
92
91
 
93
- // check package.json, "tape6" section
94
92
  if (!cfg) {
95
93
  try {
96
94
  const pkg = JSON.parse(await fsp.readFile(path.join(rootFolder, 'package.json')));
@@ -100,7 +98,6 @@ export const getConfig = async (rootFolder, traceFn) => {
100
98
  }
101
99
  }
102
100
 
103
- // check well-known files
104
101
  if (!cfg) cfg = {tests: ['/tests/test-*.js', '/tests/test-*.mjs'], cli: ['/tests/test-*.cjs']};
105
102
 
106
103
  return cfg;
@@ -109,7 +106,6 @@ export const getConfig = async (rootFolder, traceFn) => {
109
106
  export const resolveTests = async (rootFolder, type, traceFn) => {
110
107
  const cfg = await getConfig(rootFolder, traceFn);
111
108
 
112
- // determine test patterns
113
109
  let patterns = [];
114
110
  if (cfg[type]) {
115
111
  if (Array.isArray(cfg[type])) {
@@ -133,7 +129,6 @@ export const resolveTests = async (rootFolder, type, traceFn) => {
133
129
  patterns.push(cfg.tests);
134
130
  }
135
131
 
136
- // resolve patterns
137
132
  return resolvePatterns(rootFolder, patterns);
138
133
  };
139
134
 
@@ -201,8 +196,6 @@ export const getGraceTimeout = () => readTimeoutEnv('TAPE6_GRACE_TIMEOUT', DEFAU
201
196
  // it — the default — so a worker deadline fires only when explicitly set.
202
197
  export const getWorkerTimeout = () => readTimeoutEnv('TAPE6_WORKER_TIMEOUT', 0);
203
198
 
204
- // parsing options
205
-
206
199
  export const flagNames = Object.fromEntries(
207
200
  Object.entries(flagDefs).map(([k, {name}]) => [k, name])
208
201
  );
@@ -292,7 +285,6 @@ export const getOptions = extraOptions => {
292
285
  const flags = args.flags['--flags'],
293
286
  options = {flags: {flags}, parallel: 1, files: args.files, optionFlags: args.flags};
294
287
 
295
- // set back flags
296
288
  if (runtime.name === 'deno') {
297
289
  Deno.env.set('TAPE6_FLAGS', flags);
298
290
  } else if (runtime.name === 'bun') {
@@ -1,27 +1,13 @@
1
1
  let deferImplementation = globalThis.setTimeout;
2
2
 
3
- // initialize the variable
4
3
  do {
5
- // The selection below doesn't work
6
- // if (typeof queueMicrotask == 'function') {
7
- // deferImplementation = globalThis.queueMicrotask;
8
- // console.log('\nSelected: queueMicrotask()\n');
9
- // break;
10
- // }
11
-
12
- // The selection below doesn't work in Bun
13
- // if (typeof process == 'object' && typeof process?.nextTick == 'function') {
14
- // deferImplementation = process.nextTick;
15
- // break;
16
- // }
17
-
4
+ // queueMicrotask / process.nextTick were tried and rejected (the latter unreliable on Bun).
18
5
  if (typeof setImmediate == 'function') {
19
6
  deferImplementation = setImmediate;
20
7
  break;
21
8
  }
22
9
 
23
10
  if (typeof window != 'object') break;
24
- // below are browser-only implementations
25
11
 
26
12
  if (typeof window.requestIdleCallback == 'function') {
27
13
  deferImplementation = window.requestIdleCallback;
@@ -27,14 +27,13 @@ export const sanitize = (value, processed, seen = new Set()) => {
27
27
  return value.map(v => sanitize(v, false, seen));
28
28
  }
29
29
 
30
- // simple object
31
30
  const result = {};
32
31
  for (let [k, v] of Object.entries(value)) {
33
32
  if (isProhibited(v, seen)) continue;
34
33
  result[k] = sanitize(v, true, seen);
35
34
  }
36
35
 
37
- // was it an error? => copy non-enumerable properties
36
+ // Error's message/stack are non-enumerable, so the loop above skips them.
38
37
  if (value instanceof Error) {
39
38
  if (value.name && !result.name) result.name = value.name;
40
39
  if (value.message && !result.message) result.message = value.message;
@@ -4,21 +4,16 @@ export const getTimer = () => timer;
4
4
  export const setTimer = newTimer => (timer = newTimer);
5
5
 
6
6
  export const selectTimer = async () => {
7
- // set HR timer
8
7
  if (typeof performance == 'object' && performance && typeof performance.now == 'function') {
9
- // browser or Deno
10
8
  setTimer({now: () => performance.now() + performance.timeOrigin});
11
9
  return;
12
10
  }
13
11
  if (typeof process == 'object' && typeof process.exit == 'function') {
14
- // Node
15
12
  try {
16
13
  const {performance} = await import('node:perf_hooks');
17
14
  setTimer({now: () => performance.now() + performance.timeOrigin});
18
15
  return;
19
- } catch (error) {
20
- // squelch
21
- }
16
+ } catch (error) {}
22
17
  }
23
18
  setTimer(Date);
24
19
  };
@@ -71,7 +71,6 @@ const format = (value, options, level, offset, lines) => {
71
71
  return;
72
72
  }
73
73
 
74
- // array
75
74
  if (Array.isArray(value)) {
76
75
  if (level + 1 > options.maxDepth) {
77
76
  lines.push(offset + '[]');
@@ -96,7 +95,6 @@ const format = (value, options, level, offset, lines) => {
96
95
  return;
97
96
  }
98
97
 
99
- // regular object
100
98
  if (level + 1 > options.maxDepth) {
101
99
  lines.push(offset + '{}');
102
100
  return;
package/web-app/donut.js CHANGED
@@ -3,20 +3,6 @@ const TWO_PI = 2 * Math.PI;
3
3
  const tmpl = (template, dict) => template.replace(/\$\{([^\}]*)\}/g, (_, name) => dict[name]);
4
4
 
5
5
  export const makeSegment = (args, options) => {
6
- // args is {startAngle, angle, index, className}
7
- // optional index points to a data point
8
- // optional className is a CSS class
9
- // default startAngle=0 (in radians)
10
- // default angle=2*PI (in radians)
11
-
12
- // options is {center, innerRadius, radius, gap, precision, document}
13
- // default center is {x=0, y=0}
14
- // default innerRadius=0
15
- // default radius=100
16
- // default gap=0 (gap between segments in pixels)
17
- // default precision=6 (digits after decimal point)
18
- // default document is document
19
-
20
6
  const node = (options.document || document).createElementNS('http://www.w3.org/2000/svg', 'path'),
21
7
  center = options.center || {x: 0, y: 0},
22
8
  innerRadius = Math.max(options.innerRadius || 0, 0),
@@ -40,14 +26,11 @@ export const makeSegment = (args, options) => {
40
26
 
41
27
  if (angle >= TWO_PI) {
42
28
  data.tr = (2 * radius).toFixed(precision);
43
- // generate a circle
44
29
  if (innerRadius <= 0) {
45
- // a circle
46
30
  path = tmpl('M${cx} ${cy}m -${r} 0a${r} ${r} 0 1 0 ${tr} 0a${r} ${r} 0 1 0 -${tr} 0z', data);
47
31
  } else {
48
32
  data.r0 = innerRadius.toFixed(precision);
49
33
  data.tr0 = (2 * innerRadius).toFixed(precision);
50
- // a donut
51
34
  path = tmpl(
52
35
  'M${cx} ${cy}m -${r} 0a${r} ${r} 0 1 0 ${tr} 0a${r} ${r} 0 1 0 -${tr} 0zM${cx} ${cy}m -${r0} 0a${r0} ${r0} 0 1 1 ${tr0} 0a${r0} ${r0} 0 1 1 -${tr0} 0z',
53
36
  data
@@ -66,7 +49,6 @@ export const makeSegment = (args, options) => {
66
49
  data.x2 = (radius * Math.cos(finish) + cx).toFixed(precision);
67
50
  data.y2 = (radius * Math.sin(finish) + cy).toFixed(precision);
68
51
  if (innerRadius <= 0) {
69
- // a pie slice
70
52
  path = tmpl('M${cx} ${cy}L${x1} ${y1}A${r} ${r} 0 ${lg} 1 ${x2} ${y2}L${cx} ${cy}z', data);
71
53
  } else {
72
54
  start = startAngle + innerGapAngle;
@@ -79,7 +61,6 @@ export const makeSegment = (args, options) => {
79
61
  data.y3 = (innerRadius * Math.sin(finish) + cy).toFixed(precision);
80
62
  data.x4 = (innerRadius * Math.cos(start) + cx).toFixed(precision);
81
63
  data.y4 = (innerRadius * Math.sin(start) + cy).toFixed(precision);
82
- // a segment
83
64
  path = tmpl(
84
65
  'M${x1} ${y1}A${r} ${r} 0 ${lg} 1 ${x2} ${y2}L${x3} ${y3}A${r0} ${r0} 0 ${lg} 0 ${x4} ${y4}L${x1} ${y1}z',
85
66
  data
@@ -97,24 +78,6 @@ export const makeSegment = (args, options) => {
97
78
  };
98
79
 
99
80
  export const processPieRun = (data, options) => {
100
- // data is [datum, datum...]
101
- // datum is {value, className, skip, hide}
102
- // value is a positive number
103
- // className is an optional CSS class name
104
- // skip is a flag (default: false) to skip this segment completely
105
- // hide is a flag (default: false) to suppress rendering
106
-
107
- // options is {center, innerRadius, radius, startAngle, minSizeInPx, skipIfLessInPx, emptyClass, precision}
108
- // default center is {x=0, y=0}
109
- // default innerRadius=0
110
- // default radius=100
111
- // default startAngle=0 (in radians)
112
- // default gap=0 (gap between segments in pixels)
113
- // default precision=6 (digits after decimal point)
114
- // minSizeInPx is to make non-empty segments at least this big (default: 0).
115
- // skipIfLessInPx is a threshold (default: 0), when to skip too small segments.
116
- // emptyClass is a CSS class name for an empty run
117
-
118
81
  const radius = Math.max(options.radius || 100, options.innerRadius || 0, 0),
119
82
  gap = Math.max(options.gap || 0, 0),
120
83
  minSizeInPx = Math.max(options.minSizeInPx || 0, 0),
@@ -128,7 +91,6 @@ export const processPieRun = (data, options) => {
128
91
  document: options.document
129
92
  };
130
93
 
131
- // sanitize data
132
94
  data.forEach((datum, index) => {
133
95
  if (!datum.skip && (isNaN(datum.value) || datum.value === null || datum.value <= 0)) {
134
96
  datum.skip = true;
@@ -140,7 +102,6 @@ export const processPieRun = (data, options) => {
140
102
 
141
103
  let node;
142
104
  if (total <= 0) {
143
- // empty run
144
105
  node = makeSegment(
145
106
  {
146
107
  index: -1, // to denote that it is not an actionable node
@@ -168,7 +129,6 @@ export const processPieRun = (data, options) => {
168
129
  return [node];
169
130
  }
170
131
 
171
- // find too small segments
172
132
  const sizes = data.map(datum => {
173
133
  let angle = 0;
174
134
  if (!datum.skip) {
@@ -179,7 +139,6 @@ export const processPieRun = (data, options) => {
179
139
 
180
140
  let minAngle, newTotal, changeRatio;
181
141
  if (minSizeInPx > 0) {
182
- // adjust angles
183
142
  minAngle = (minSizeInPx + gap) / radius;
184
143
  sizes.forEach((size, index) => {
185
144
  const datum = data[index];
@@ -200,7 +159,6 @@ export const processPieRun = (data, options) => {
200
159
  }
201
160
  });
202
161
  } else if (skipIfLessInPx > 0) {
203
- // suppress angles
204
162
  minAngle = skipIfLessInPx / radius;
205
163
  sizes.forEach((size, index) => {
206
164
  const datum = data[index];
@@ -217,7 +175,6 @@ export const processPieRun = (data, options) => {
217
175
  });
218
176
  }
219
177
 
220
- // generate shape objects
221
178
  const shapes = [];
222
179
  let startAngle = options.startAngle || 0;
223
180
  data.forEach((datum, index) => {
package/web-app/index.js CHANGED
@@ -106,7 +106,6 @@ window.addEventListener('DOMContentLoaded', () => {
106
106
  const donut = document.querySelector('tape6-donut');
107
107
  donut.show([{value: 0, className: 'nothing'}], {
108
108
  center: {x: 100, y: 100},
109
- // gap: 4,
110
109
  innerRadius: 40,
111
110
  radius: 90,
112
111
  startAngle: Math.PI / 2,
@@ -3,7 +3,6 @@ class Tape6Spinner extends HTMLElement {
3
3
  super();
4
4
  this.nextState = '';
5
5
  this.inTransition = false;
6
- // create squares
7
6
  let child = null;
8
7
  for (let depth = this.getAttribute('depth') || 3; depth > 0; --depth) {
9
8
  const node1 = document.createElement('div');
@@ -18,7 +17,6 @@ class Tape6Spinner extends HTMLElement {
18
17
  node.className = 'square black';
19
18
  child && node.appendChild(child);
20
19
  this.appendChild(node);
21
- // watch for transitions
22
20
  this.addEventListener('transitionend', this);
23
21
  }
24
22
  handleEvent(event) {