qunitx-cli 0.1.2 → 0.5.1
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 +120 -49
- package/cli.js +1 -1
- package/lib/boilerplates/default-project-config-values.js +2 -2
- package/lib/boilerplates/test.js +5 -4
- package/lib/commands/generate.js +6 -8
- package/lib/commands/help.js +8 -8
- package/lib/commands/init.js +35 -25
- package/lib/commands/run/tests-in-browser.js +97 -67
- package/lib/commands/run.js +165 -55
- package/lib/servers/http.js +59 -44
- package/lib/setup/bind-server-to-port.js +3 -12
- package/lib/setup/browser.js +26 -18
- package/lib/setup/config.js +8 -10
- package/lib/setup/file-watcher.js +23 -6
- package/lib/setup/fs-tree.js +29 -27
- package/lib/setup/keyboard-events.js +7 -4
- package/lib/setup/test-file-paths.js +25 -23
- package/lib/setup/web-server.js +87 -61
- package/lib/setup/write-output-static-files.js +4 -1
- package/lib/tap/display-final-result.js +2 -2
- package/lib/tap/display-test-result.js +32 -14
- package/lib/utils/find-chrome.js +16 -0
- package/lib/utils/find-internal-assets-from-html.js +7 -5
- package/lib/utils/find-project-root.js +1 -2
- package/lib/utils/indent-string.js +6 -6
- package/lib/utils/listen-to-keyboard-key.js +6 -2
- package/lib/utils/parse-cli-flags.js +34 -31
- package/lib/utils/resolve-port-number-for.js +3 -3
- package/lib/utils/run-user-module.js +5 -3
- package/lib/utils/search-in-parent-directories.js +4 -1
- package/lib/utils/time-counter.js +2 -2
- package/package.json +21 -35
- package/vendor/qunit.css +7 -7
- package/vendor/qunit.js +3772 -3324
- package/flake.lock +0 -64
- package/flake.nix +0 -26
package/README.md
CHANGED
|
@@ -1,92 +1,163 @@
|
|
|
1
|
-
#
|
|
1
|
+
# qunitx-cli
|
|
2
2
|
|
|
3
|
-
CI
|
|
3
|
+
[](https://github.com/izelnakri/qunitx-cli/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/qunitx-cli)
|
|
5
|
+
[](https://www.npmjs.com/package/qunitx-cli)
|
|
6
|
+
[](LICENSE)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
Browser-based test runner for [QUnitX](https://github.com/izelnakri/qunitx) — bundles your JS/TS tests
|
|
9
|
+
with esbuild, runs them in headless Chrome, and streams TAP output to the terminal.
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
output in anyway you like. Example:
|
|
11
|
+

|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- Runs `.js` and `.ts` test files in headless Chrome (Puppeteer + esbuild)
|
|
16
|
+
- TypeScript works with zero configuration — esbuild handles transpilation
|
|
17
|
+
- Inline source maps for accurate stack traces pointing to original source files
|
|
18
|
+
- Streams TAP-formatted output to the terminal in real time
|
|
19
|
+
- Concurrent mode (default) splits test files across all CPU cores for fast parallel runs
|
|
20
|
+
- `--watch` mode re-runs affected tests on file change
|
|
21
|
+
- `--failFast` stops the run after the first failing test
|
|
22
|
+
- `--debug` prints the local server URL and pipes browser console to stdout
|
|
23
|
+
- `--before` / `--after` hook scripts for server setup and teardown
|
|
24
|
+
- `--timeout` controls the maximum ms to wait for the full suite to finish
|
|
25
|
+
- Docker image for zero-install CI usage
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Requires Node.js >= 24.
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm install --save-dev qunitx-cli
|
|
13
33
|
```
|
|
14
34
|
|
|
15
|
-
|
|
35
|
+
Or run without installing:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npx qunitx test/**/*.js
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
With Docker — no install needed:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
docker run --rm -v "$(pwd):/code" -w /code ghcr.io/izelnakri/qunitx-cli:latest npx qunitx test/**/*.js
|
|
45
|
+
```
|
|
16
46
|
|
|
17
|
-
|
|
18
|
-
npm install -g qunitx-cli
|
|
47
|
+
With Nix:
|
|
19
48
|
|
|
20
|
-
|
|
49
|
+
```sh
|
|
50
|
+
nix profile install github:izelnakri/qunitx-cli
|
|
21
51
|
```
|
|
22
52
|
|
|
23
|
-
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
# Single file
|
|
57
|
+
qunitx test/my-test.js
|
|
58
|
+
|
|
59
|
+
# Multiple files / globs
|
|
60
|
+
qunitx test/**/*.js test/**/*.ts
|
|
61
|
+
|
|
62
|
+
# TypeScript — no tsconfig required
|
|
63
|
+
qunitx test/my-test.ts
|
|
64
|
+
|
|
65
|
+
# Watch mode: re-run on file changes
|
|
66
|
+
qunitx test/**/*.js --watch
|
|
67
|
+
|
|
68
|
+
# Stop on the first failure
|
|
69
|
+
qunitx test/**/*.js --failFast
|
|
70
|
+
|
|
71
|
+
# Print the server URL and pipe Chrome console to stdout
|
|
72
|
+
qunitx test/**/*.js --debug
|
|
73
|
+
|
|
74
|
+
# Custom timeout (ms)
|
|
75
|
+
qunitx test/**/*.js --timeout=30000
|
|
76
|
+
|
|
77
|
+
# Run a setup script before tests (can be async — awaited automatically)
|
|
78
|
+
qunitx test/**/*.js --before=scripts/start-server.js
|
|
79
|
+
|
|
80
|
+
# Run a teardown script after tests (can be async)
|
|
81
|
+
qunitx test/**/*.js --after=scripts/stop-server.js
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Writing Tests
|
|
85
|
+
|
|
86
|
+
qunitx-cli runs [QUnitX](https://github.com/izelnakri/qunitx) tests — a superset of QUnit with async
|
|
87
|
+
hooks, concurrency control, and test metadata.
|
|
88
|
+
|
|
89
|
+
Migrating from QUnit? Change a single import:
|
|
24
90
|
|
|
25
91
|
```js
|
|
92
|
+
// before
|
|
26
93
|
import { module, test } from 'qunit';
|
|
27
|
-
|
|
28
|
-
// to:
|
|
94
|
+
// after
|
|
29
95
|
import { module, test } from 'qunitx';
|
|
30
96
|
```
|
|
31
97
|
|
|
32
|
-
Example:
|
|
98
|
+
Example test file — ES modules, npm imports, and nested modules all work out of the box:
|
|
33
99
|
|
|
34
100
|
```js
|
|
35
|
-
//
|
|
101
|
+
// some-test.js (TypeScript is also supported)
|
|
36
102
|
import { module, test } from 'qunitx';
|
|
37
103
|
import $ from 'jquery';
|
|
38
104
|
|
|
39
|
-
module('Basic sanity check',
|
|
40
|
-
test('it works',
|
|
105
|
+
module('Basic sanity check', (hooks) => {
|
|
106
|
+
test('it works', (assert) => {
|
|
41
107
|
assert.equal(true, true);
|
|
42
108
|
});
|
|
43
109
|
|
|
44
|
-
module('More advanced cases',
|
|
45
|
-
test('deepEqual works',
|
|
110
|
+
module('More advanced cases', (hooks) => {
|
|
111
|
+
test('deepEqual works', (assert) => {
|
|
46
112
|
assert.deepEqual({ username: 'izelnakri' }, { username: 'izelnakri' });
|
|
47
113
|
});
|
|
48
|
-
|
|
114
|
+
|
|
115
|
+
test('can import ES & npm modules', (assert) => {
|
|
49
116
|
assert.ok(Object.keys($));
|
|
50
117
|
});
|
|
51
118
|
});
|
|
52
119
|
});
|
|
53
120
|
```
|
|
54
121
|
|
|
55
|
-
|
|
56
|
-
# you can run the test in node with ES modules package.json{ "type": "module" }
|
|
57
|
-
$ node --test some-test.js
|
|
122
|
+
Run it:
|
|
58
123
|
|
|
59
|
-
|
|
124
|
+
```sh
|
|
125
|
+
# Headless Chrome (recommended for CI)
|
|
126
|
+
qunitx some-test.js
|
|
60
127
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# with browser output enabled:
|
|
64
|
-
|
|
65
|
-
$ qunitx some-test.js --debug
|
|
66
|
-
|
|
67
|
-
# TypeScript also works, make sure on node.js mode, tsconfig.json exists with compilerOptions.module & compilerOptions.moduleResolution set to "NodeNext":
|
|
68
|
-
|
|
69
|
-
$ node --loader=ts-node/esm/transpile-only --test some-test.ts
|
|
70
|
-
|
|
71
|
-
$ qunitx some-test.ts --debug
|
|
128
|
+
# With browser console output
|
|
129
|
+
qunitx some-test.js --debug
|
|
72
130
|
|
|
131
|
+
# TypeScript — no config needed
|
|
132
|
+
qunitx some-test.ts
|
|
73
133
|
```
|
|
74
134
|
|
|
75
|
-
|
|
135
|
+
## CLI Reference
|
|
76
136
|
|
|
77
|
-
Since QUnitX proxies to default node.js test runner in when executed with node, you can use any code coverage tool you like. When running the tests in `qunit`(the browser mode) code coverage support is limited.
|
|
78
137
|
```
|
|
79
|
-
|
|
138
|
+
Usage: qunitx [files/folders...] [options]
|
|
139
|
+
|
|
140
|
+
Options:
|
|
141
|
+
--watch Re-run tests on file changes
|
|
142
|
+
--failFast Stop after the first failure
|
|
143
|
+
--debug Print the server URL; pipe browser console to stdout
|
|
144
|
+
--timeout=<ms> Max ms to wait for the suite to finish [default: 20000]
|
|
145
|
+
--output=<dir> Directory for compiled test assets [default: ./tmp]
|
|
146
|
+
--before=<file> Script to run (and optionally await) before tests start
|
|
147
|
+
--after=<file> Script to run (and optionally await) after tests finish
|
|
148
|
+
--port=<n> HTTP server port (auto-selects a free port if taken)
|
|
80
149
|
```
|
|
81
150
|
|
|
82
|
-
|
|
151
|
+
## Development
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
npm install
|
|
155
|
+
make check # lint + test (run before every commit)
|
|
156
|
+
make test # run tests only
|
|
157
|
+
make demo # regenerate demo output
|
|
158
|
+
make release LEVEL=patch # bump version, update changelog, tag, push
|
|
159
|
+
```
|
|
83
160
|
|
|
84
|
-
|
|
85
|
-
create a JS bundles for testing in the browser, this could be instrumented with `puppeteer-to-istanbul` however
|
|
86
|
-
instrumentation includes transpiled npm imports of `qunitx` and other potential npm imports developer
|
|
87
|
-
includes in the code, this cannot be filtered since potential filtering can only occur after the `esbuild` bundling.
|
|
88
|
-
When chrome browser and puppeteer fully supports ES asset maps we can remove esbuild from the browser mode, run
|
|
89
|
-
everything in deno and make instrumentation for code coverage possible with the default v8 instrumentation.
|
|
161
|
+
## License
|
|
90
162
|
|
|
91
|
-
|
|
92
|
-
with esbuild in the future, which could allow code coverage for --browser mode.
|
|
163
|
+
MIT
|
package/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
import process from 'node:process';
|
|
3
3
|
import displayHelpOutput from './lib/commands/help.js';
|
|
4
4
|
import initializeProject from './lib/commands/init.js';
|
package/lib/boilerplates/test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { module, test } from 'qunitx';
|
|
2
2
|
|
|
3
|
-
module('{{moduleName}}', function(hooks) {
|
|
3
|
+
module('{{moduleName}}', function (hooks) {
|
|
4
4
|
test('assert true works', function (assert) {
|
|
5
5
|
assert.expect(3);
|
|
6
6
|
assert.ok(true);
|
|
@@ -11,9 +11,10 @@ module('{{moduleName}}', function(hooks) {
|
|
|
11
11
|
test('async test finishes', async function (assert) {
|
|
12
12
|
assert.expect(3);
|
|
13
13
|
|
|
14
|
-
let wait = () =>
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
let wait = () =>
|
|
15
|
+
new Promise((resolve, reject) => {
|
|
16
|
+
setTimeout(() => resolve(true), 50);
|
|
17
|
+
});
|
|
17
18
|
let result = await wait();
|
|
18
19
|
|
|
19
20
|
assert.ok(true);
|
package/lib/commands/generate.js
CHANGED
|
@@ -7,12 +7,13 @@ import pathExists from '../utils/path-exists.js';
|
|
|
7
7
|
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
|
|
10
|
-
export default async function() {
|
|
10
|
+
export default async function () {
|
|
11
11
|
let projectRoot = await findProjectRoot();
|
|
12
12
|
let moduleName = process.argv[3]; // TODO: classify this maybe in future
|
|
13
|
-
let path =
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
let path =
|
|
14
|
+
process.argv[3].endsWith('.js') || process.argv[3].endsWith('.ts')
|
|
15
|
+
? `${projectRoot}/${process.argv[3]}`
|
|
16
|
+
: `${projectRoot}/${process.argv[3]}.js`;
|
|
16
17
|
|
|
17
18
|
if (await pathExists(path)) {
|
|
18
19
|
return console.log(`${path} already exists!`);
|
|
@@ -24,10 +25,7 @@ export default async function() {
|
|
|
24
25
|
targetFolderPaths.pop();
|
|
25
26
|
|
|
26
27
|
await fs.mkdir(targetFolderPaths.join('/'), { recursive: true });
|
|
27
|
-
await fs.writeFile(
|
|
28
|
-
path,
|
|
29
|
-
testJSContent.toString().replace('{{moduleName}}', moduleName)
|
|
30
|
-
);
|
|
28
|
+
await fs.writeFile(path, testJSContent.toString().replace('{{moduleName}}', moduleName));
|
|
31
29
|
|
|
32
30
|
console.log(kleur.green(`${path} written`));
|
|
33
31
|
}
|
package/lib/commands/help.js
CHANGED
|
@@ -7,30 +7,30 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
7
7
|
const highlight = (text) => kleur.magenta().bold(text);
|
|
8
8
|
const color = (text) => kleur.blue(text);
|
|
9
9
|
|
|
10
|
+
export default async function () {
|
|
11
|
+
const config = JSON.parse(await fs.readFile(`${__dirname}/../../package.json`));
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
const config = JSON.parse((await fs.readFile(`${__dirname}/../../package.json`)));
|
|
13
|
+
console.log(`${highlight('[qunitx v' + config.version + '] Usage:')} qunitx ${color('[targets] --$flags')}
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
${highlight("Input options:")}
|
|
15
|
+
${highlight('Input options:')}
|
|
17
16
|
- File: $ ${color('qunitx test/foo.js')}
|
|
18
17
|
- Folder: $ ${color('qunitx test/login')}
|
|
19
18
|
- Globs: $ ${color('qunitx test/**/*-test.js')}
|
|
20
19
|
- Combination: $ ${color('qunitx test/foo.js test/bar.js test/*-test.js test/logout')}
|
|
21
20
|
|
|
22
|
-
${highlight(
|
|
21
|
+
${highlight('Optional flags:')}
|
|
23
22
|
${color('--debug')} : print console output when tests run in browser
|
|
24
23
|
${color('--watch')} : run the target file or folders, watch them for continuous run and expose http server under localhost
|
|
25
24
|
${color('--timeout')} : change default timeout per test case
|
|
26
25
|
${color('--output')} : folder to distribute built qunitx html and js that a webservers can run[default: tmp]
|
|
27
26
|
${color('--failFast')} : run the target file or folders with immediate abort if a single test fails
|
|
27
|
+
${color('--port')} : HTTP server port (auto-selects a free port if the given port is taken)[default: 1234]
|
|
28
28
|
${color('--before')} : run a script before the tests(i.e start a new web server before tests)
|
|
29
29
|
${color('--after')} : run a script after the tests(i.e save test results to a file)
|
|
30
30
|
|
|
31
|
-
${highlight(
|
|
31
|
+
${highlight('Example:')} $ ${color('qunitx test/foo.ts app/e2e --debug --watch --before=scripts/start-new-webserver.js --after=scripts/write-test-results.js')}
|
|
32
32
|
|
|
33
|
-
${highlight(
|
|
33
|
+
${highlight('Commands:')}
|
|
34
34
|
${color('$ qunitx init')} # Bootstraps qunitx base html and add qunitx config to package.json if needed
|
|
35
35
|
${color('$ qunitx new $testFileName')} # Creates a qunitx test file
|
|
36
36
|
`);
|
package/lib/commands/init.js
CHANGED
|
@@ -7,49 +7,57 @@ import defaultProjectConfigValues from '../boilerplates/default-project-config-v
|
|
|
7
7
|
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
|
|
10
|
-
export default async function() {
|
|
10
|
+
export default async function () {
|
|
11
11
|
let projectRoot = await findProjectRoot();
|
|
12
12
|
let oldPackageJSON = JSON.parse(await fs.readFile(`${projectRoot}/package.json`));
|
|
13
|
-
let htmlPaths = process.argv.slice(2).reduce(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
let htmlPaths = process.argv.slice(2).reduce(
|
|
14
|
+
(result, arg) => {
|
|
15
|
+
if (arg.endsWith('.html')) {
|
|
16
|
+
result.push(arg);
|
|
17
|
+
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
return result;
|
|
20
|
+
},
|
|
21
|
+
oldPackageJSON.qunitx && oldPackageJSON.qunitx.htmlPaths ? oldPackageJSON.qunitx.htmlPaths : [],
|
|
22
|
+
);
|
|
20
23
|
let newQunitxConfig = Object.assign(
|
|
21
24
|
defaultProjectConfigValues,
|
|
22
25
|
htmlPaths.length > 0 ? { htmlPaths } : { htmlPaths: ['test/tests.html'] },
|
|
23
|
-
oldPackageJSON.qunitx
|
|
26
|
+
oldPackageJSON.qunitx,
|
|
24
27
|
);
|
|
25
28
|
|
|
26
29
|
await Promise.all([
|
|
27
30
|
writeTestsHTML(projectRoot, newQunitxConfig, oldPackageJSON),
|
|
28
31
|
rewritePackageJSON(projectRoot, newQunitxConfig, oldPackageJSON),
|
|
29
|
-
writeTSConfigIfNeeded(projectRoot)
|
|
32
|
+
writeTSConfigIfNeeded(projectRoot),
|
|
30
33
|
]);
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
async function writeTestsHTML(projectRoot, newQunitxConfig, oldPackageJSON) {
|
|
34
37
|
let testHTMLTemplateBuffer = await fs.readFile(`${__dirname}/../boilerplates/setup/tests.hbs`);
|
|
35
38
|
|
|
36
|
-
return await Promise.all(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
return await Promise.all(
|
|
40
|
+
newQunitxConfig.htmlPaths.map(async (htmlPath) => {
|
|
41
|
+
let targetPath = `${projectRoot}/${htmlPath}`;
|
|
42
|
+
if (await pathExists(targetPath)) {
|
|
43
|
+
return console.log(`${htmlPath} already exists`);
|
|
44
|
+
} else {
|
|
45
|
+
let targetDirectory = path.dirname(targetPath);
|
|
46
|
+
let targetOutputPath = path.relative(
|
|
47
|
+
targetDirectory,
|
|
48
|
+
`${projectRoot}/${newQunitxConfig.output}/tests.js`,
|
|
49
|
+
);
|
|
50
|
+
let testHTMLTemplate = testHTMLTemplateBuffer
|
|
51
|
+
.toString()
|
|
52
|
+
.replace('{{applicationName}}', oldPackageJSON.name);
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
await fs.mkdir(targetDirectory, { recursive: true });
|
|
55
|
+
await fs.writeFile(targetPath, testHTMLTemplate);
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
console.log(`${targetPath} written`);
|
|
58
|
+
}
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
async function rewritePackageJSON(projectRoot, newQunitxConfig, oldPackageJSON) {
|
|
@@ -61,7 +69,9 @@ async function rewritePackageJSON(projectRoot, newQunitxConfig, oldPackageJSON)
|
|
|
61
69
|
async function writeTSConfigIfNeeded(projectRoot) {
|
|
62
70
|
let targetPath = `${projectRoot}/tsconfig.json`;
|
|
63
71
|
if (!(await pathExists(targetPath))) {
|
|
64
|
-
let tsConfigTemplateBuffer = await fs.readFile(
|
|
72
|
+
let tsConfigTemplateBuffer = await fs.readFile(
|
|
73
|
+
`${__dirname}/../boilerplates/setup/tsconfig.json`,
|
|
74
|
+
);
|
|
65
75
|
|
|
66
76
|
await fs.writeFile(targetPath, tsConfigTemplateBuffer);
|
|
67
77
|
|
|
@@ -17,81 +17,99 @@ class BundleError extends Error {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// Exported so run.js can pre-build all group bundles in parallel with Chrome startup.
|
|
21
|
+
export async function buildTestBundle(config, cachedContent) {
|
|
22
|
+
const { projectRoot, output } = config;
|
|
23
|
+
const allTestFilePaths = Object.keys(config.fsTree);
|
|
24
|
+
|
|
25
|
+
await Promise.all([
|
|
26
|
+
esbuild.build({
|
|
27
|
+
stdin: {
|
|
28
|
+
contents: allTestFilePaths.map((f) => `import "${f}";`).join(''),
|
|
29
|
+
resolveDir: process.cwd(),
|
|
30
|
+
},
|
|
31
|
+
bundle: true,
|
|
32
|
+
logLevel: 'error',
|
|
33
|
+
outfile: `${projectRoot}/${output}/tests.js`,
|
|
34
|
+
keepNames: true,
|
|
35
|
+
sourcemap: 'inline',
|
|
36
|
+
}),
|
|
37
|
+
Promise.all(
|
|
38
|
+
cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
|
|
39
|
+
const targetPath = `${config.projectRoot}/${config.output}${htmlPath}`;
|
|
40
|
+
if (htmlPath !== '/') {
|
|
41
|
+
await fs.rm(targetPath, { force: true, recursive: true });
|
|
42
|
+
await fs.mkdir(targetPath.split('/').slice(0, -1).join('/'), { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
cachedContent.allTestCode = await fs.readFile(`${projectRoot}/${output}/tests.js`);
|
|
49
|
+
}
|
|
50
|
+
|
|
20
51
|
export default async function runTestsInBrowser(
|
|
21
52
|
config,
|
|
22
53
|
cachedContent = {},
|
|
23
54
|
connections,
|
|
24
|
-
targetTestFilesToFilter
|
|
55
|
+
targetTestFilesToFilter,
|
|
25
56
|
) {
|
|
26
|
-
const { projectRoot,
|
|
57
|
+
const { projectRoot, output } = config;
|
|
27
58
|
const allTestFilePaths = Object.keys(config.fsTree);
|
|
28
59
|
const runHasFilter = !!targetTestFilesToFilter;
|
|
29
60
|
|
|
30
|
-
|
|
61
|
+
// In group mode the COUNTER is shared across all groups and managed by run.js.
|
|
62
|
+
if (!config._groupMode) {
|
|
63
|
+
config.COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0 };
|
|
64
|
+
}
|
|
31
65
|
config.lastRanTestFiles = targetTestFilesToFilter || allTestFilePaths;
|
|
32
66
|
|
|
33
67
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return result + `import "${fileAbsolutePath}";`
|
|
39
|
-
}, ''),
|
|
40
|
-
resolveDir: process.cwd()
|
|
41
|
-
},
|
|
42
|
-
bundle: true,
|
|
43
|
-
logLevel: 'error',
|
|
44
|
-
outfile: `${projectRoot}/${output}/tests.js`,
|
|
45
|
-
keepNames: true
|
|
46
|
-
}), // NOTE: This prevents file cache most likely
|
|
47
|
-
Promise.all(cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
|
|
48
|
-
let targetPath = `${config.projectRoot}/${config.output}${htmlPath}`;
|
|
49
|
-
|
|
50
|
-
if (htmlPath !== '/') {
|
|
51
|
-
await fs.rm(targetPath, { force: true, recursive: true });
|
|
52
|
-
await fs.mkdir(targetPath.split('/').slice(0, -1).join('/'), { recursive: true }); // NOTE: this can be done earlier
|
|
53
|
-
}
|
|
54
|
-
}))
|
|
55
|
-
]);
|
|
56
|
-
cachedContent.allTestCode = await fs.readFile(`${projectRoot}/${output}/tests.js`);
|
|
68
|
+
// Skip bundle build if run.js already pre-built it (group mode optimization).
|
|
69
|
+
if (!cachedContent.allTestCode) {
|
|
70
|
+
await buildTestBundle(config, cachedContent);
|
|
71
|
+
}
|
|
57
72
|
|
|
58
73
|
if (runHasFilter) {
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
const outputPath = `${projectRoot}/${output}/filtered-tests.js`;
|
|
61
75
|
await buildFilteredTests(targetTestFilesToFilter, outputPath);
|
|
62
76
|
cachedContent.filteredTestCode = (await fs.readFile(outputPath)).toString();
|
|
63
77
|
}
|
|
64
78
|
|
|
65
|
-
|
|
79
|
+
const TIME_COUNTER = timeCounter();
|
|
66
80
|
|
|
67
81
|
if (runHasFilter) {
|
|
68
82
|
await runTestInsideHTMLFile('/qunitx.html', connections, config);
|
|
69
83
|
} else {
|
|
70
|
-
await Promise.all(
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
await Promise.all(
|
|
85
|
+
cachedContent.htmlPathsToRunTests.map((htmlPath) =>
|
|
86
|
+
runTestInsideHTMLFile(htmlPath, connections, config),
|
|
87
|
+
),
|
|
88
|
+
);
|
|
73
89
|
}
|
|
74
90
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
TAPDisplayFinalResult(config.COUNTER, TIME_TAKEN);
|
|
91
|
+
const TIME_TAKEN = TIME_COUNTER.stop();
|
|
78
92
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
// In group mode the parent orchestrator handles the final summary, after hook, and exit.
|
|
94
|
+
if (!config._groupMode) {
|
|
95
|
+
TAPDisplayFinalResult(config.COUNTER, TIME_TAKEN);
|
|
82
96
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
connections.browser && connections.browser.close()
|
|
87
|
-
]);
|
|
97
|
+
if (config.after) {
|
|
98
|
+
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, 'after');
|
|
99
|
+
}
|
|
88
100
|
|
|
89
|
-
|
|
101
|
+
if (!config.watch) {
|
|
102
|
+
await Promise.all([
|
|
103
|
+
connections.server && connections.server.close(),
|
|
104
|
+
connections.browser && connections.browser.close(),
|
|
105
|
+
]);
|
|
106
|
+
return process.exit(config.COUNTER.failCount > 0 ? 1 : 0);
|
|
107
|
+
}
|
|
90
108
|
}
|
|
91
|
-
} catch(error) {
|
|
109
|
+
} catch (error) {
|
|
92
110
|
config.lastFailedTestFiles = config.lastRanTestFiles;
|
|
93
111
|
console.log(error);
|
|
94
|
-
|
|
112
|
+
const exception = new BundleError(error);
|
|
95
113
|
|
|
96
114
|
if (config.watch) {
|
|
97
115
|
console.log(`# ${exception}`);
|
|
@@ -106,31 +124,41 @@ export default async function runTestsInBrowser(
|
|
|
106
124
|
function buildFilteredTests(filteredTests, outputPath) {
|
|
107
125
|
return esbuild.build({
|
|
108
126
|
stdin: {
|
|
109
|
-
contents: filteredTests.
|
|
110
|
-
|
|
111
|
-
}, ''),
|
|
112
|
-
resolveDir: process.cwd()
|
|
127
|
+
contents: filteredTests.map((f) => `import "${f}";`).join(''),
|
|
128
|
+
resolveDir: process.cwd(),
|
|
113
129
|
},
|
|
114
130
|
bundle: true,
|
|
115
131
|
logLevel: 'error',
|
|
116
|
-
outfile: outputPath
|
|
132
|
+
outfile: outputPath,
|
|
133
|
+
sourcemap: 'inline',
|
|
117
134
|
});
|
|
118
135
|
}
|
|
119
136
|
|
|
120
|
-
async function runTestInsideHTMLFile(filePath, { page, server }, config) {
|
|
137
|
+
async function runTestInsideHTMLFile(filePath, { page, server, browser }, config) {
|
|
121
138
|
let QUNIT_RESULT;
|
|
122
139
|
let targetError;
|
|
123
140
|
try {
|
|
124
|
-
await wait(350);
|
|
125
141
|
console.log('#', kleur.blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
|
|
126
|
-
|
|
127
|
-
|
|
142
|
+
|
|
143
|
+
const testsDone = new Promise((resolve) => {
|
|
144
|
+
config._testRunDone = resolve;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await page.evaluateOnNewDocument(() => {
|
|
128
148
|
window.IS_PUPPETEER = true;
|
|
129
149
|
});
|
|
130
|
-
await page.
|
|
150
|
+
await page.goto(`http://localhost:${config.port}${filePath}`, {
|
|
151
|
+
timeout: config.timeout + 10000,
|
|
152
|
+
});
|
|
153
|
+
await Promise.race([
|
|
154
|
+
testsDone,
|
|
155
|
+
page.waitForFunction(`window.testTimeout >= ${config.timeout}`, {
|
|
156
|
+
timeout: config.timeout + 10000,
|
|
157
|
+
}),
|
|
158
|
+
]);
|
|
131
159
|
|
|
132
160
|
QUNIT_RESULT = await page.evaluate(() => window.QUNIT_RESULT);
|
|
133
|
-
} catch(error) {
|
|
161
|
+
} catch (error) {
|
|
134
162
|
targetError = error;
|
|
135
163
|
console.log(error);
|
|
136
164
|
console.error(error);
|
|
@@ -140,23 +168,25 @@ async function runTestInsideHTMLFile(filePath, { page, server }, config) {
|
|
|
140
168
|
console.log(targetError);
|
|
141
169
|
console.log('BROWSER: runtime error thrown during executing tests');
|
|
142
170
|
console.error('BROWSER: runtime error thrown during executing tests');
|
|
143
|
-
|
|
144
|
-
await failOnNonWatchMode(config.watch);
|
|
171
|
+
await failOnNonWatchMode(config.watch, { server, browser }, config._groupMode);
|
|
145
172
|
} else if (QUNIT_RESULT.totalTests > QUNIT_RESULT.finishedTests) {
|
|
146
173
|
console.log(targetError);
|
|
147
174
|
console.log(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
148
175
|
console.error(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
149
|
-
|
|
150
|
-
await failOnNonWatchMode(config.watch);
|
|
176
|
+
await failOnNonWatchMode(config.watch, { server, browser }, config._groupMode);
|
|
151
177
|
}
|
|
152
178
|
}
|
|
153
179
|
|
|
154
|
-
async function failOnNonWatchMode(watchMode = false) {
|
|
180
|
+
async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode = false) {
|
|
155
181
|
if (!watchMode) {
|
|
156
|
-
|
|
182
|
+
if (groupMode) {
|
|
183
|
+
// Parent orchestrator handles cleanup and exit; signal failure via throw.
|
|
184
|
+
throw new Error('Browser test run failed');
|
|
185
|
+
}
|
|
186
|
+
await Promise.all([
|
|
187
|
+
connections.server && connections.server.close(),
|
|
188
|
+
connections.browser && connections.browser.close(),
|
|
189
|
+
]);
|
|
190
|
+
process.exit(1);
|
|
157
191
|
}
|
|
158
192
|
}
|
|
159
|
-
|
|
160
|
-
function wait(duration) {
|
|
161
|
-
return new Promise((resolve) => setTimeout(() => { resolve() }, duration));
|
|
162
|
-
}
|