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 +2 -0
- package/TESTING.md +1 -1
- package/bin/tape6-deno-main.js +104 -0
- package/bin/tape6-deno.js +6 -103
- package/bin/tape6-server.js +61 -204
- package/index.d.ts +37 -5
- package/index.js +12 -3
- package/llms-full.txt +92 -2
- package/llms.txt +23 -0
- package/package.json +3 -2
- package/src/State.js +2 -7
- package/src/Tester.js +14 -4
- package/src/reporters/TTYReporter.js +0 -10
- package/src/runners/bun/TestWorker.js +3 -1
- package/src/runners/deno/TestWorker.js +3 -2
- package/src/runners/node/TestWorker.js +3 -1
- package/src/runners/seq/BypassReporter.js +0 -1
- package/src/runners/seq/TestWorker.js +3 -1
- package/src/test-server/adapter.js +121 -0
- package/src/test-server/certs.js +77 -0
- package/src/test-server/control.js +94 -0
- package/src/test-server/plugins/echo.js +20 -0
- package/src/test-server/registry.js +143 -0
- package/src/test-server/statics.js +106 -0
- package/src/test-server/trace.js +31 -0
- package/src/test-server.d.ts +157 -0
- package/src/test-server.js +154 -0
- package/src/test.js +5 -8
- package/src/utils/EventServer.js +3 -6
- package/src/utils/config.js +0 -8
- package/src/utils/defer.js +2 -16
- package/src/utils/sanitize.js +1 -2
- package/src/utils/timer.js +1 -6
- package/src/utils/yamlFormatter.js +1 -3
- package/web-app/donut.js +0 -43
- package/web-app/index.js +0 -1
- package/web-app/tape6-spinner.js +0 -2
- package/src/utils/timeout.js +0 -3
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
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
11
|
-
|
|
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', '
|
|
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
|
|
45
|
-
console.log(' PORT
|
|
46
|
-
console.log(' SERVER_ROOT
|
|
47
|
-
console.log(' WEBAPP_PATH
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
247
|
-
|
|
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
|
-
|
|
110
|
+
const mounted = testServer.plugins();
|
|
111
|
+
if (mounted.length) {
|
|
257
112
|
console.log(
|
|
258
|
-
grey('
|
|
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
|
-
|
|
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
|