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 +1 -0
- package/bin/tape6-deno-main.js +104 -0
- package/bin/tape6-deno.js +6 -103
- package/bin/tape6-server.js +0 -15
- package/index.d.ts +36 -0
- package/index.js +12 -3
- package/llms-full.txt +9 -0
- package/llms.txt +2 -0
- package/package.json +2 -2
- package/src/State.js +2 -7
- package/src/Tester.js +14 -4
- package/src/reporters/TTYReporter.js +0 -10
- package/src/runners/deno/TestWorker.js +0 -1
- package/src/runners/seq/BypassReporter.js +0 -1
- package/src/test.js +0 -6
- package/src/utils/EventServer.js +0 -5
- package/src/utils/config.js +0 -8
- package/src/utils/defer.js +1 -15
- package/src/utils/sanitize.js +1 -2
- package/src/utils/timer.js +1 -6
- package/src/utils/yamlFormatter.js +0 -2
- package/web-app/donut.js +0 -43
- package/web-app/index.js +0 -1
- package/web-app/tape6-spinner.js +0 -2
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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);
|
package/bin/tape6-server.js
CHANGED
|
@@ -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.
|
|
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": "^
|
|
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 => {
|
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];
|
package/src/utils/EventServer.js
CHANGED
|
@@ -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) {
|
package/src/utils/config.js
CHANGED
|
@@ -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') {
|
package/src/utils/defer.js
CHANGED
|
@@ -1,27 +1,13 @@
|
|
|
1
1
|
let deferImplementation = globalThis.setTimeout;
|
|
2
2
|
|
|
3
|
-
// initialize the variable
|
|
4
3
|
do {
|
|
5
|
-
//
|
|
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;
|
package/src/utils/sanitize.js
CHANGED
|
@@ -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
|
-
//
|
|
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;
|
package/src/utils/timer.js
CHANGED
|
@@ -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,
|
package/web-app/tape6-spinner.js
CHANGED
|
@@ -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) {
|