qunitx-cli 0.9.1 → 0.9.3
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/{cli.js → cli.ts} +16 -8
- package/deno.lock +20 -14
- package/lib/commands/{generate.js → generate.ts} +5 -5
- package/lib/commands/{help.js → help.ts} +1 -1
- package/lib/commands/{init.js → init.ts} +26 -27
- package/lib/commands/run/{tests-in-browser.js → tests-in-browser.ts} +51 -22
- package/lib/commands/{run.js → run.ts} +100 -39
- package/lib/servers/{http.js → http.ts} +88 -35
- package/lib/setup/bind-server-to-port.ts +14 -0
- package/lib/setup/{browser.js → browser.ts} +14 -18
- package/lib/setup/config.ts +55 -0
- package/lib/setup/{file-watcher.js → file-watcher.ts} +21 -8
- package/lib/setup/{fs-tree.js → fs-tree.ts} +6 -4
- package/lib/setup/{keyboard-events.js → keyboard-events.ts} +10 -5
- package/lib/setup/{test-file-paths.js → test-file-paths.ts} +10 -4
- package/lib/setup/{web-server.js → web-server.ts} +22 -21
- package/lib/setup/{write-output-static-files.js → write-output-static-files.ts} +6 -2
- package/lib/tap/{display-final-result.js → display-final-result.ts} +6 -3
- package/lib/tap/{display-test-result.js → display-test-result.ts} +25 -11
- package/lib/tap/{dump-yaml.js → dump-yaml.ts} +19 -5
- package/lib/types.ts +61 -0
- package/lib/utils/{chromium-args.js → chromium-args.ts} +1 -1
- package/lib/utils/{color.js → color.ts} +24 -11
- package/lib/utils/{early-chrome.js → early-chrome.ts} +5 -5
- package/lib/utils/{find-chrome.js → find-chrome.ts} +2 -2
- package/lib/utils/{find-internal-assets-from-html.js → find-internal-assets-from-html.ts} +1 -1
- package/lib/utils/{find-project-root.js → find-project-root.ts} +4 -4
- package/lib/utils/{indent-string.js → indent-string.ts} +6 -2
- package/lib/utils/{listen-to-keyboard-key.js → listen-to-keyboard-key.ts} +12 -7
- package/lib/utils/{parse-cli-flags.js → parse-cli-flags.ts} +21 -8
- package/lib/utils/{path-exists.js → path-exists.ts} +2 -2
- package/lib/utils/{perf-logger.js → perf-logger.ts} +1 -1
- package/lib/utils/pre-launch-chrome.ts +45 -0
- package/lib/utils/{read-boilerplate.js → read-boilerplate.ts} +1 -1
- package/lib/utils/{resolve-port-number-for.js → resolve-port-number-for.ts} +2 -2
- package/lib/utils/{run-user-module.js → run-user-module.ts} +6 -2
- package/lib/utils/{search-in-parent-directories.js → search-in-parent-directories.ts} +5 -2
- package/lib/utils/{time-counter.js → time-counter.ts} +2 -2
- package/package.json +16 -15
- package/lib/setup/bind-server-to-port.js +0 -9
- package/lib/setup/config.js +0 -48
- package/lib/utils/pre-launch-chrome.js +0 -32
- /package/lib/setup/{default-project-config-values.js → default-project-config-values.ts} +0 -0
package/{cli.js → cli.ts}
RENAMED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env -S node --experimental-strip-types
|
|
2
2
|
import process from 'node:process';
|
|
3
|
-
import './lib/utils/early-chrome.
|
|
4
|
-
import displayHelpOutput from './lib/commands/help.
|
|
5
|
-
import initializeProject from './lib/commands/init.
|
|
6
|
-
import generateTestFiles from './lib/commands/generate.
|
|
7
|
-
import setupConfig from './lib/setup/config.
|
|
3
|
+
import './lib/utils/early-chrome.ts';
|
|
4
|
+
import displayHelpOutput from './lib/commands/help.ts';
|
|
5
|
+
import initializeProject from './lib/commands/init.ts';
|
|
6
|
+
import generateTestFiles from './lib/commands/generate.ts';
|
|
7
|
+
import setupConfig from './lib/setup/config.ts';
|
|
8
8
|
|
|
9
9
|
process.title = 'qunitx';
|
|
10
10
|
|
|
@@ -24,8 +24,16 @@ process.title = 'qunitx';
|
|
|
24
24
|
// lets playwright-core start loading while config is being assembled.
|
|
25
25
|
const [config, { default: run }] = await Promise.all([
|
|
26
26
|
setupConfig(),
|
|
27
|
-
import('./lib/commands/run.
|
|
27
|
+
import('./lib/commands/run.ts'),
|
|
28
28
|
]);
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
try {
|
|
31
|
+
return await run(config);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error(error);
|
|
34
|
+
// Flush stdout before exit so any queued console.log writes (e.g. from WS testEnd
|
|
35
|
+
// handlers that fired before the exception) are not lost when process.exit() runs.
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
process.stdout.write('\n', () => process.exit(1));
|
|
38
|
+
}
|
|
31
39
|
})();
|
package/deno.lock
CHANGED
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
"npm:esbuild@~0.27.3": "0.27.4",
|
|
11
11
|
"npm:express@^5.2.1": "5.2.1",
|
|
12
12
|
"npm:js-yaml@^4.1.1": "4.1.1",
|
|
13
|
-
"npm:picomatch@*": "4.0.
|
|
14
|
-
"npm:picomatch@^4.0.
|
|
13
|
+
"npm:picomatch@*": "4.0.4",
|
|
14
|
+
"npm:picomatch@^4.0.4": "4.0.4",
|
|
15
15
|
"npm:playwright-core@^1.58.2": "1.58.2",
|
|
16
16
|
"npm:prettier@^3.8.1": "3.8.1",
|
|
17
|
-
"npm:qunitx@^1.0.
|
|
18
|
-
"npm:
|
|
19
|
-
"npm:ws
|
|
17
|
+
"npm:qunitx@^1.0.4": "1.0.4",
|
|
18
|
+
"npm:typescript@5": "5.9.3",
|
|
19
|
+
"npm:ws@*": "8.20.0",
|
|
20
|
+
"npm:ws@^8.20.0": "8.20.0"
|
|
20
21
|
},
|
|
21
22
|
"jsr": {
|
|
22
23
|
"@std/fmt@1.0.9": {
|
|
@@ -473,8 +474,8 @@
|
|
|
473
474
|
"path-to-regexp@8.3.0": {
|
|
474
475
|
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="
|
|
475
476
|
},
|
|
476
|
-
"picomatch@4.0.
|
|
477
|
-
"integrity": "sha512-
|
|
477
|
+
"picomatch@4.0.4": {
|
|
478
|
+
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="
|
|
478
479
|
},
|
|
479
480
|
"playwright-core@1.58.2": {
|
|
480
481
|
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
|
@@ -497,8 +498,8 @@
|
|
|
497
498
|
"side-channel"
|
|
498
499
|
]
|
|
499
500
|
},
|
|
500
|
-
"qunitx@1.0.
|
|
501
|
-
"integrity": "sha512-
|
|
501
|
+
"qunitx@1.0.4": {
|
|
502
|
+
"integrity": "sha512-iu3enOfQo5Ul5As+KFiN1Zm1kd8MWeeNcHFUbZpKYQn+y6f6V4yiDeehVc5LFSUFBOkutjnqHzZiqInCf0MutA=="
|
|
502
503
|
},
|
|
503
504
|
"range-parser@1.2.1": {
|
|
504
505
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
|
|
@@ -603,6 +604,10 @@
|
|
|
603
604
|
"mime-types"
|
|
604
605
|
]
|
|
605
606
|
},
|
|
607
|
+
"typescript@5.9.3": {
|
|
608
|
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
|
609
|
+
"bin": true
|
|
610
|
+
},
|
|
606
611
|
"undici-types@7.18.2": {
|
|
607
612
|
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="
|
|
608
613
|
},
|
|
@@ -615,8 +620,8 @@
|
|
|
615
620
|
"wrappy@1.0.2": {
|
|
616
621
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
|
617
622
|
},
|
|
618
|
-
"ws@8.
|
|
619
|
-
"integrity": "sha512-
|
|
623
|
+
"ws@8.20.0": {
|
|
624
|
+
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="
|
|
620
625
|
}
|
|
621
626
|
},
|
|
622
627
|
"workspace": {
|
|
@@ -631,11 +636,12 @@
|
|
|
631
636
|
"npm:esbuild@~0.27.3",
|
|
632
637
|
"npm:express@^5.2.1",
|
|
633
638
|
"npm:js-yaml@^4.1.1",
|
|
634
|
-
"npm:picomatch@^4.0.
|
|
639
|
+
"npm:picomatch@^4.0.4",
|
|
635
640
|
"npm:playwright-core@^1.58.2",
|
|
636
641
|
"npm:prettier@^3.8.1",
|
|
637
|
-
"npm:qunitx@^1.0.
|
|
638
|
-
"npm:
|
|
642
|
+
"npm:qunitx@^1.0.4",
|
|
643
|
+
"npm:typescript@5",
|
|
644
|
+
"npm:ws@^8.20.0"
|
|
639
645
|
]
|
|
640
646
|
}
|
|
641
647
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import { green } from '../utils/color.
|
|
3
|
-
import findProjectRoot from '../utils/find-project-root.
|
|
4
|
-
import pathExists from '../utils/path-exists.
|
|
5
|
-
import readBoilerplate from '../utils/read-boilerplate.
|
|
2
|
+
import { green } from '../utils/color.ts';
|
|
3
|
+
import findProjectRoot from '../utils/find-project-root.ts';
|
|
4
|
+
import pathExists from '../utils/path-exists.ts';
|
|
5
|
+
import readBoilerplate from '../utils/read-boilerplate.ts';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Generates a new test file from the boilerplate template.
|
|
@@ -10,7 +10,7 @@ import readBoilerplate from '../utils/read-boilerplate.js';
|
|
|
10
10
|
*/
|
|
11
11
|
export default async function generateTestFiles() {
|
|
12
12
|
const projectRoot = await findProjectRoot();
|
|
13
|
-
const moduleName = process.argv[3];
|
|
13
|
+
const moduleName = process.argv[3];
|
|
14
14
|
const path =
|
|
15
15
|
process.argv[3].endsWith('.js') || process.argv[3].endsWith('.ts')
|
|
16
16
|
? `${projectRoot}/${process.argv[3]}`
|
|
@@ -1,42 +1,37 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import findProjectRoot from '../utils/find-project-root.
|
|
4
|
-
import pathExists from '../utils/path-exists.
|
|
5
|
-
import defaultProjectConfigValues from '../setup/default-project-config-values.
|
|
6
|
-
import readBoilerplate from '../utils/read-boilerplate.
|
|
3
|
+
import findProjectRoot from '../utils/find-project-root.ts';
|
|
4
|
+
import pathExists from '../utils/path-exists.ts';
|
|
5
|
+
import defaultProjectConfigValues from '../setup/default-project-config-values.ts';
|
|
6
|
+
import readBoilerplate from '../utils/read-boilerplate.ts';
|
|
7
7
|
|
|
8
8
|
/** Bootstraps a new qunitx project: writes the test HTML template, updates package.json, and optionally writes tsconfig.json. */
|
|
9
9
|
export default async function initializeProject() {
|
|
10
10
|
const projectRoot = await findProjectRoot();
|
|
11
11
|
const oldPackageJSON = JSON.parse(await fs.readFile(`${projectRoot}/package.json`));
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return result;
|
|
19
|
-
},
|
|
20
|
-
oldPackageJSON.qunitx && oldPackageJSON.qunitx.htmlPaths ? oldPackageJSON.qunitx.htmlPaths : [],
|
|
21
|
-
);
|
|
22
|
-
const newQunitxConfig = Object.assign(
|
|
23
|
-
defaultProjectConfigValues,
|
|
24
|
-
htmlPaths.length > 0 ? { htmlPaths } : { htmlPaths: ['test/tests.html'] },
|
|
25
|
-
oldPackageJSON.qunitx,
|
|
26
|
-
);
|
|
12
|
+
const existingQunitx = oldPackageJSON.qunitx || {};
|
|
13
|
+
const cliHtmlPaths = process.argv.slice(2).filter((arg) => arg.endsWith('.html'));
|
|
14
|
+
const config = Object.assign({}, defaultProjectConfigValues, existingQunitx, {
|
|
15
|
+
htmlPaths:
|
|
16
|
+
cliHtmlPaths.length > 0 ? cliHtmlPaths : existingQunitx.htmlPaths || ['test/tests.html'],
|
|
17
|
+
});
|
|
27
18
|
|
|
28
19
|
await Promise.all([
|
|
29
|
-
writeTestsHTML(projectRoot,
|
|
30
|
-
rewritePackageJSON(projectRoot,
|
|
20
|
+
writeTestsHTML(projectRoot, config, oldPackageJSON),
|
|
21
|
+
rewritePackageJSON(projectRoot, config, oldPackageJSON),
|
|
31
22
|
writeTSConfigIfNeeded(projectRoot),
|
|
32
23
|
]);
|
|
33
24
|
}
|
|
34
25
|
|
|
35
|
-
async function writeTestsHTML(
|
|
26
|
+
async function writeTestsHTML(
|
|
27
|
+
projectRoot: string,
|
|
28
|
+
config: { htmlPaths: string[]; output: string },
|
|
29
|
+
oldPackageJSON: Record<string, unknown>,
|
|
30
|
+
): Promise<unknown[]> {
|
|
36
31
|
const testHTMLTemplateBuffer = await readBoilerplate('setup/tests.hbs');
|
|
37
32
|
|
|
38
33
|
return await Promise.all(
|
|
39
|
-
|
|
34
|
+
config.htmlPaths.map(async (htmlPath) => {
|
|
40
35
|
const targetPath = `${projectRoot}/${htmlPath}`;
|
|
41
36
|
if (await pathExists(targetPath)) {
|
|
42
37
|
return console.log(`${htmlPath} already exists`);
|
|
@@ -44,7 +39,7 @@ async function writeTestsHTML(projectRoot, newQunitxConfig, oldPackageJSON) {
|
|
|
44
39
|
const targetDirectory = path.dirname(targetPath);
|
|
45
40
|
const _targetOutputPath = path.relative(
|
|
46
41
|
targetDirectory,
|
|
47
|
-
`${projectRoot}/${
|
|
42
|
+
`${projectRoot}/${config.output}/tests.js`,
|
|
48
43
|
);
|
|
49
44
|
const testHTMLTemplate = testHTMLTemplateBuffer.replace(
|
|
50
45
|
'{{applicationName}}',
|
|
@@ -60,13 +55,17 @@ async function writeTestsHTML(projectRoot, newQunitxConfig, oldPackageJSON) {
|
|
|
60
55
|
);
|
|
61
56
|
}
|
|
62
57
|
|
|
63
|
-
async function rewritePackageJSON(
|
|
64
|
-
|
|
58
|
+
async function rewritePackageJSON(
|
|
59
|
+
projectRoot: string,
|
|
60
|
+
config: unknown,
|
|
61
|
+
oldPackageJSON: Record<string, unknown>,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
const newPackageJSON = Object.assign(oldPackageJSON, { qunitx: config });
|
|
65
64
|
|
|
66
65
|
await fs.writeFile(`${projectRoot}/package.json`, JSON.stringify(newPackageJSON, null, 2));
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
async function writeTSConfigIfNeeded(projectRoot) {
|
|
68
|
+
async function writeTSConfigIfNeeded(projectRoot: string): Promise<void> {
|
|
70
69
|
const targetPath = `${projectRoot}/tsconfig.json`;
|
|
71
70
|
if (!(await pathExists(targetPath))) {
|
|
72
71
|
const tsConfigTemplate = await readBoilerplate('setup/tsconfig.json');
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import { blue } from '../../utils/color.
|
|
2
|
+
import { blue } from '../../utils/color.ts';
|
|
3
3
|
import esbuild from 'esbuild';
|
|
4
|
-
import timeCounter from '../../utils/time-counter.
|
|
5
|
-
import runUserModule from '../../utils/run-user-module.
|
|
6
|
-
import TAPDisplayFinalResult from '../../tap/display-final-result.
|
|
4
|
+
import timeCounter from '../../utils/time-counter.ts';
|
|
5
|
+
import runUserModule from '../../utils/run-user-module.ts';
|
|
6
|
+
import TAPDisplayFinalResult from '../../tap/display-final-result.ts';
|
|
7
|
+
import type { Config, CachedContent, Connections } from '../../types.ts';
|
|
8
|
+
import type HTTPServer from '../../servers/http.ts';
|
|
7
9
|
|
|
8
10
|
class BundleError extends Error {
|
|
9
|
-
constructor(message) {
|
|
11
|
+
constructor(message: unknown) {
|
|
10
12
|
super(message);
|
|
11
13
|
this.name = 'BundleError';
|
|
12
14
|
this.message = `esbuild Bundle Error: ${message}`.split('\n').join('\n# ');
|
|
@@ -17,7 +19,7 @@ class BundleError extends Error {
|
|
|
17
19
|
* Pre-builds the esbuild bundle for all test files and caches the result in `cachedContent`.
|
|
18
20
|
* @returns {Promise<void>}
|
|
19
21
|
*/
|
|
20
|
-
export async function buildTestBundle(config, cachedContent) {
|
|
22
|
+
export async function buildTestBundle(config: Config, cachedContent: CachedContent): Promise<void> {
|
|
21
23
|
const { projectRoot, output } = config;
|
|
22
24
|
const allTestFilePaths = Object.keys(config.fsTree);
|
|
23
25
|
|
|
@@ -52,11 +54,11 @@ export async function buildTestBundle(config, cachedContent) {
|
|
|
52
54
|
* @returns {Promise<object>}
|
|
53
55
|
*/
|
|
54
56
|
export default async function runTestsInBrowser(
|
|
55
|
-
config,
|
|
56
|
-
cachedContent = {},
|
|
57
|
-
connections,
|
|
58
|
-
targetTestFilesToFilter,
|
|
59
|
-
) {
|
|
57
|
+
config: Config,
|
|
58
|
+
cachedContent: CachedContent = {} as CachedContent,
|
|
59
|
+
connections: Connections,
|
|
60
|
+
targetTestFilesToFilter?: string[],
|
|
61
|
+
): Promise<Connections | undefined> {
|
|
60
62
|
const { projectRoot, output } = config;
|
|
61
63
|
const allTestFilePaths = Object.keys(config.fsTree);
|
|
62
64
|
const runHasFilter = !!targetTestFilesToFilter;
|
|
@@ -124,7 +126,11 @@ export default async function runTestsInBrowser(
|
|
|
124
126
|
return connections;
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
function buildFilteredTests(
|
|
129
|
+
function buildFilteredTests(
|
|
130
|
+
filteredTests: string[],
|
|
131
|
+
outputPath: string,
|
|
132
|
+
config: Config,
|
|
133
|
+
): Promise<esbuild.BuildResult> {
|
|
128
134
|
return esbuild.build({
|
|
129
135
|
stdin: {
|
|
130
136
|
contents: filteredTests.map((f) => `import "${f}";`).join(''),
|
|
@@ -137,31 +143,46 @@ function buildFilteredTests(filteredTests, outputPath, config) {
|
|
|
137
143
|
});
|
|
138
144
|
}
|
|
139
145
|
|
|
140
|
-
async function runTestInsideHTMLFile(
|
|
146
|
+
async function runTestInsideHTMLFile(
|
|
147
|
+
filePath: string,
|
|
148
|
+
{ page, server, browser }: Connections,
|
|
149
|
+
config: Config,
|
|
150
|
+
): Promise<void> {
|
|
141
151
|
let QUNIT_RESULT;
|
|
142
152
|
let targetError;
|
|
153
|
+
let timeoutHandle;
|
|
143
154
|
try {
|
|
144
155
|
console.log('#', blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
|
|
145
156
|
|
|
146
|
-
|
|
147
|
-
|
|
157
|
+
// Single promise driven by the WS handler:
|
|
158
|
+
// config._testRunDone() → tests finished normally
|
|
159
|
+
// config._resetTestTimeout() → reset idle timer; fires as timeout if silent for config.timeout ms
|
|
160
|
+
// This replaces waitForFunction (CDP polling), which raced against WS testEnd messages
|
|
161
|
+
// under load: CDP could win and trigger cleanup before Node.js processed the pending messages.
|
|
162
|
+
const testRaceResult = new Promise((resolve) => {
|
|
163
|
+
config._testRunDone = () => resolve(false);
|
|
164
|
+
config._resetTestTimeout = () => {
|
|
165
|
+
clearTimeout(timeoutHandle);
|
|
166
|
+
timeoutHandle = setTimeout(() => resolve(true), config.timeout);
|
|
167
|
+
};
|
|
148
168
|
});
|
|
149
169
|
|
|
150
170
|
await page.goto(`http://localhost:${config.port}${filePath}`, {
|
|
151
171
|
timeout: config.timeout + 10000,
|
|
152
172
|
});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}),
|
|
158
|
-
]);
|
|
173
|
+
|
|
174
|
+
config._resetTestTimeout(); // start idle countdown once the page is loaded
|
|
175
|
+
|
|
176
|
+
await testRaceResult;
|
|
159
177
|
|
|
160
178
|
QUNIT_RESULT = await page.evaluate(() => window.QUNIT_RESULT);
|
|
161
179
|
} catch (error) {
|
|
162
180
|
targetError = error;
|
|
163
181
|
console.log(error);
|
|
164
182
|
console.error(error);
|
|
183
|
+
} finally {
|
|
184
|
+
clearTimeout(timeoutHandle);
|
|
185
|
+
config._resetTestTimeout = null;
|
|
165
186
|
}
|
|
166
187
|
|
|
167
188
|
if (!QUNIT_RESULT || QUNIT_RESULT.totalTests === 0) {
|
|
@@ -174,10 +195,18 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
174
195
|
console.log(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
175
196
|
console.error(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
176
197
|
await failOnNonWatchMode(config.watch, { server, browser }, config._groupMode);
|
|
198
|
+
} else if (QUNIT_RESULT.failedTests > config.COUNTER.failCount) {
|
|
199
|
+
// Safety net: browser tracked failures that WebSocket events never delivered to Node.js
|
|
200
|
+
// (e.g. WS connection dropped mid-run). Reconcile so the exit code is always correct.
|
|
201
|
+
config.COUNTER.failCount = QUNIT_RESULT.failedTests;
|
|
177
202
|
}
|
|
178
203
|
}
|
|
179
204
|
|
|
180
|
-
async function failOnNonWatchMode(
|
|
205
|
+
async function failOnNonWatchMode(
|
|
206
|
+
watchMode: boolean = false,
|
|
207
|
+
connections: { server?: HTTPServer; browser?: { close(): Promise<void> } } = {},
|
|
208
|
+
groupMode: boolean = false,
|
|
209
|
+
): Promise<void> {
|
|
181
210
|
if (!watchMode) {
|
|
182
211
|
if (groupMode) {
|
|
183
212
|
// Parent orchestrator handles cleanup and exit; signal failure via throw.
|
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import setupBrowser, { launchBrowser } from '../setup/browser.
|
|
1
|
+
import setupBrowser, { launchBrowser } from '../setup/browser.ts';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import { normalize } from 'node:path';
|
|
4
4
|
import { availableParallelism } from 'node:os';
|
|
5
|
-
import { blue, yellow } from '../utils/color.
|
|
6
|
-
import runTestsInBrowser, { buildTestBundle } from './run/tests-in-browser.
|
|
7
|
-
import fileWatcher from '../setup/file-watcher.
|
|
8
|
-
import findInternalAssetsFromHTML from '../utils/find-internal-assets-from-html.
|
|
9
|
-
import runUserModule from '../utils/run-user-module.
|
|
10
|
-
import setupKeyboardEvents from '../setup/keyboard-events.
|
|
11
|
-
import writeOutputStaticFiles from '../setup/write-output-static-files.
|
|
12
|
-
import timeCounter from '../utils/time-counter.
|
|
13
|
-
import TAPDisplayFinalResult from '../tap/display-final-result.
|
|
14
|
-
import readBoilerplate from '../utils/read-boilerplate.
|
|
5
|
+
import { blue, yellow } from '../utils/color.ts';
|
|
6
|
+
import runTestsInBrowser, { buildTestBundle } from './run/tests-in-browser.ts';
|
|
7
|
+
import fileWatcher from '../setup/file-watcher.ts';
|
|
8
|
+
import findInternalAssetsFromHTML from '../utils/find-internal-assets-from-html.ts';
|
|
9
|
+
import runUserModule from '../utils/run-user-module.ts';
|
|
10
|
+
import setupKeyboardEvents from '../setup/keyboard-events.ts';
|
|
11
|
+
import writeOutputStaticFiles from '../setup/write-output-static-files.ts';
|
|
12
|
+
import timeCounter from '../utils/time-counter.ts';
|
|
13
|
+
import TAPDisplayFinalResult from '../tap/display-final-result.ts';
|
|
14
|
+
import readBoilerplate from '../utils/read-boilerplate.ts';
|
|
15
|
+
import type { Config, CachedContent } from '../types.ts';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Runs qunitx tests in headless Chrome, either in watch mode or concurrent batch mode.
|
|
18
19
|
* @returns {Promise<void>}
|
|
19
20
|
*/
|
|
20
|
-
export default async function run(config) {
|
|
21
|
+
export default async function run(config: Config): Promise<void> {
|
|
21
22
|
const cachedContent = await buildCachedContent(config, config.htmlPaths);
|
|
22
23
|
|
|
23
24
|
if (config.watch) {
|
|
@@ -81,6 +82,8 @@ export default async function run(config) {
|
|
|
81
82
|
}));
|
|
82
83
|
const groupCachedContents = groups.map(() => ({ ...cachedContent }));
|
|
83
84
|
|
|
85
|
+
console.log('TAP version 13');
|
|
86
|
+
|
|
84
87
|
// Build all group bundles and write static files while the browser is starting up.
|
|
85
88
|
const [browser] = await Promise.all([
|
|
86
89
|
launchBrowser(config),
|
|
@@ -93,34 +96,74 @@ export default async function run(config) {
|
|
|
93
96
|
),
|
|
94
97
|
),
|
|
95
98
|
]);
|
|
96
|
-
|
|
97
|
-
console.log('TAP version 13');
|
|
98
99
|
const TIME_COUNTER = timeCounter();
|
|
99
|
-
let hasFatalError = false;
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
// 3-minute per-group deadline. Firefox/WebKit can hang indefinitely in any Playwright
|
|
102
|
+
// operation (browser.newPage, page.evaluate, page.close) when overwhelmed by concurrent
|
|
103
|
+
// pages. Without this outer timeout, one stuck group freezes Promise.allSettled forever.
|
|
104
|
+
// After all groups settle, browser.close() (below) terminates the browser and unblocks
|
|
105
|
+
// any still-pending Playwright calls in background async fns.
|
|
106
|
+
const GROUP_TIMEOUT_MS = 3 * 60 * 1000;
|
|
105
107
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
// Keep the event loop alive during Promise.allSettled. The Chrome child process and its
|
|
109
|
+
// stderr pipe are unref'd (pre-launch-chrome.js). If Chrome crashes during group cleanup,
|
|
110
|
+
// all active handles close and the event loop would drain — exiting silently before
|
|
111
|
+
// allSettled resolves or results are printed. This interval holds the loop open so that
|
|
112
|
+
// unref'd group/page-close timers can still fire normally.
|
|
113
|
+
const keepAlive = setInterval(() => {}, 1000);
|
|
109
114
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
115
|
+
const groupResults = await Promise.allSettled(
|
|
116
|
+
groupConfigs.map((groupConfig, i) => {
|
|
117
|
+
const groupTimeout = new Promise((_, reject) => {
|
|
118
|
+
const t = setTimeout(
|
|
119
|
+
() => reject(new Error(`Group ${i} timed out after ${GROUP_TIMEOUT_MS}ms`)),
|
|
120
|
+
GROUP_TIMEOUT_MS,
|
|
121
|
+
);
|
|
122
|
+
t.unref();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return Promise.race([
|
|
126
|
+
(async () => {
|
|
127
|
+
const connections = await setupBrowser(groupConfig, groupCachedContents[i], browser);
|
|
128
|
+
groupConfig.expressApp = connections.server;
|
|
129
|
+
|
|
130
|
+
if (config.before) {
|
|
131
|
+
await runUserModule(`${process.cwd()}/${config.before}`, groupConfig, 'before');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
await runTestsInBrowser(groupConfig, groupCachedContents[i], connections);
|
|
136
|
+
} finally {
|
|
137
|
+
await Promise.all([
|
|
138
|
+
connections.server && connections.server.close(),
|
|
139
|
+
connections.page &&
|
|
140
|
+
// Unref'd: the keepAlive interval above holds the event loop open, so this
|
|
141
|
+
// timer still fires if page.close() hangs, without preventing process exit later.
|
|
142
|
+
Promise.race([
|
|
143
|
+
connections.page.close(),
|
|
144
|
+
new Promise((resolve) => {
|
|
145
|
+
const t = setTimeout(resolve, 10000);
|
|
146
|
+
t.unref();
|
|
147
|
+
}),
|
|
148
|
+
]).catch(() => {}),
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
})(),
|
|
152
|
+
groupTimeout,
|
|
153
|
+
]);
|
|
120
154
|
}),
|
|
121
155
|
);
|
|
122
156
|
|
|
123
|
-
|
|
157
|
+
const exitCode = groupResults.reduce(
|
|
158
|
+
(code, { status, reason }) => {
|
|
159
|
+
if (status !== 'rejected') return code;
|
|
160
|
+
console.error(reason);
|
|
161
|
+
return 1;
|
|
162
|
+
},
|
|
163
|
+
config.COUNTER.failCount > 0 ? 1 : 0,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
process.exitCode = exitCode;
|
|
124
167
|
|
|
125
168
|
TAPDisplayFinalResult(config.COUNTER, TIME_COUNTER.stop());
|
|
126
169
|
|
|
@@ -128,11 +171,22 @@ export default async function run(config) {
|
|
|
128
171
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, 'after');
|
|
129
172
|
}
|
|
130
173
|
|
|
131
|
-
|
|
174
|
+
// Flush stdout then exit. keepAlive holds the event loop open until this callback fires,
|
|
175
|
+
// at which point process.exit() takes over — so clearInterval happens here, not earlier.
|
|
176
|
+
// If the write callback never fires (theoretical), the unref'd exitTimer is the fallback.
|
|
177
|
+
const exitTimer = setTimeout(() => process.exit(exitCode), 5000);
|
|
178
|
+
exitTimer.unref();
|
|
179
|
+
process.stdout.write('\n', () => {
|
|
180
|
+
clearTimeout(exitTimer);
|
|
181
|
+
clearInterval(keepAlive);
|
|
182
|
+
// Close browser after stdout is flushed; fire-and-forget since process.exit follows.
|
|
183
|
+
browser.close().catch(() => {});
|
|
184
|
+
process.exit(exitCode);
|
|
185
|
+
});
|
|
132
186
|
}
|
|
133
187
|
}
|
|
134
188
|
|
|
135
|
-
async function buildCachedContent(config, htmlPaths) {
|
|
189
|
+
async function buildCachedContent(config: Config, htmlPaths: string[]): Promise<CachedContent> {
|
|
136
190
|
const htmlBuffers = await Promise.all(config.htmlPaths.map((htmlPath) => fs.readFile(htmlPath)));
|
|
137
191
|
const cachedContent = htmlPaths.reduce(
|
|
138
192
|
(result, _htmlPath, index) => {
|
|
@@ -175,7 +229,10 @@ async function buildCachedContent(config, htmlPaths) {
|
|
|
175
229
|
return addCachedContentMainHTML(config.projectRoot, cachedContent);
|
|
176
230
|
}
|
|
177
231
|
|
|
178
|
-
async function addCachedContentMainHTML(
|
|
232
|
+
async function addCachedContentMainHTML(
|
|
233
|
+
projectRoot: string,
|
|
234
|
+
cachedContent: CachedContent,
|
|
235
|
+
): Promise<CachedContent> {
|
|
179
236
|
const mainHTMLPath = Object.keys(cachedContent.dynamicContentHTMLs)[0];
|
|
180
237
|
if (mainHTMLPath) {
|
|
181
238
|
cachedContent.mainHTML = {
|
|
@@ -191,13 +248,13 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
|
|
|
191
248
|
return cachedContent;
|
|
192
249
|
}
|
|
193
250
|
|
|
194
|
-
function splitIntoGroups(files, groupCount) {
|
|
251
|
+
function splitIntoGroups(files: string[], groupCount: number): string[][] {
|
|
195
252
|
const groups = Array.from({ length: groupCount }, () => []);
|
|
196
253
|
files.forEach((file, i) => groups[i % groupCount].push(file));
|
|
197
254
|
return groups.filter((g) => g.length > 0);
|
|
198
255
|
}
|
|
199
256
|
|
|
200
|
-
function logWatcherAndKeyboardShortcutInfo(config, _server) {
|
|
257
|
+
function logWatcherAndKeyboardShortcutInfo(config: Config, _server: unknown): void {
|
|
201
258
|
console.log(
|
|
202
259
|
'#',
|
|
203
260
|
blue(`Watching files... You can browse the tests on http://localhost:${config.port} ...`),
|
|
@@ -210,7 +267,11 @@ function logWatcherAndKeyboardShortcutInfo(config, _server) {
|
|
|
210
267
|
);
|
|
211
268
|
}
|
|
212
269
|
|
|
213
|
-
function normalizeInternalAssetPathFromHTML(
|
|
270
|
+
function normalizeInternalAssetPathFromHTML(
|
|
271
|
+
projectRoot: string,
|
|
272
|
+
assetPath: string,
|
|
273
|
+
htmlPath: string,
|
|
274
|
+
): string {
|
|
214
275
|
const currentDirectory = htmlPath ? htmlPath.split('/').slice(0, -1).join('/') : projectRoot;
|
|
215
276
|
return assetPath.startsWith('./')
|
|
216
277
|
? normalize(`${currentDirectory}/${assetPath.slice(2)}`)
|