qunitx-cli 0.9.0 → 0.9.2
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 +9 -1
- package/deno.lock +14 -14
- package/lib/commands/generate.js +1 -1
- package/lib/commands/init.js +13 -22
- package/lib/commands/run/tests-in-browser.js +23 -8
- package/lib/commands/run.js +75 -22
- package/lib/servers/http.js +5 -3
- package/lib/setup/file-watcher.js +2 -3
- package/lib/setup/fs-tree.js +0 -2
- package/lib/setup/web-server.js +9 -5
- package/lib/tap/display-test-result.js +3 -5
- package/lib/utils/listen-to-keyboard-key.js +1 -0
- package/lib/utils/pre-launch-chrome.js +14 -5
- package/package.json +6 -6
package/cli.js
CHANGED
|
@@ -27,5 +27,13 @@ process.title = 'qunitx';
|
|
|
27
27
|
import('./lib/commands/run.js'),
|
|
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,13 @@
|
|
|
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:ws@*": "8.
|
|
19
|
-
"npm:ws@^8.
|
|
17
|
+
"npm:qunitx@^1.0.4": "1.0.4",
|
|
18
|
+
"npm:ws@*": "8.20.0",
|
|
19
|
+
"npm:ws@^8.20.0": "8.20.0"
|
|
20
20
|
},
|
|
21
21
|
"jsr": {
|
|
22
22
|
"@std/fmt@1.0.9": {
|
|
@@ -473,8 +473,8 @@
|
|
|
473
473
|
"path-to-regexp@8.3.0": {
|
|
474
474
|
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="
|
|
475
475
|
},
|
|
476
|
-
"picomatch@4.0.
|
|
477
|
-
"integrity": "sha512-
|
|
476
|
+
"picomatch@4.0.4": {
|
|
477
|
+
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="
|
|
478
478
|
},
|
|
479
479
|
"playwright-core@1.58.2": {
|
|
480
480
|
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
|
@@ -497,8 +497,8 @@
|
|
|
497
497
|
"side-channel"
|
|
498
498
|
]
|
|
499
499
|
},
|
|
500
|
-
"qunitx@1.0.
|
|
501
|
-
"integrity": "sha512-
|
|
500
|
+
"qunitx@1.0.4": {
|
|
501
|
+
"integrity": "sha512-iu3enOfQo5Ul5As+KFiN1Zm1kd8MWeeNcHFUbZpKYQn+y6f6V4yiDeehVc5LFSUFBOkutjnqHzZiqInCf0MutA=="
|
|
502
502
|
},
|
|
503
503
|
"range-parser@1.2.1": {
|
|
504
504
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
|
|
@@ -615,8 +615,8 @@
|
|
|
615
615
|
"wrappy@1.0.2": {
|
|
616
616
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
|
617
617
|
},
|
|
618
|
-
"ws@8.
|
|
619
|
-
"integrity": "sha512-
|
|
618
|
+
"ws@8.20.0": {
|
|
619
|
+
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="
|
|
620
620
|
}
|
|
621
621
|
},
|
|
622
622
|
"workspace": {
|
|
@@ -631,11 +631,11 @@
|
|
|
631
631
|
"npm:esbuild@~0.27.3",
|
|
632
632
|
"npm:express@^5.2.1",
|
|
633
633
|
"npm:js-yaml@^4.1.1",
|
|
634
|
-
"npm:picomatch@^4.0.
|
|
634
|
+
"npm:picomatch@^4.0.4",
|
|
635
635
|
"npm:playwright-core@^1.58.2",
|
|
636
636
|
"npm:prettier@^3.8.1",
|
|
637
|
-
"npm:qunitx@^1.0.
|
|
638
|
-
"npm:ws@^8.
|
|
637
|
+
"npm:qunitx@^1.0.4",
|
|
638
|
+
"npm:ws@^8.20.0"
|
|
639
639
|
]
|
|
640
640
|
}
|
|
641
641
|
}
|
package/lib/commands/generate.js
CHANGED
|
@@ -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]}`
|
package/lib/commands/init.js
CHANGED
|
@@ -9,34 +9,25 @@ import readBoilerplate from '../utils/read-boilerplate.js';
|
|
|
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(projectRoot,
|
|
26
|
+
async function writeTestsHTML(projectRoot, config, oldPackageJSON) {
|
|
36
27
|
const testHTMLTemplateBuffer = await readBoilerplate('setup/tests.hbs');
|
|
37
28
|
|
|
38
29
|
return await Promise.all(
|
|
39
|
-
|
|
30
|
+
config.htmlPaths.map(async (htmlPath) => {
|
|
40
31
|
const targetPath = `${projectRoot}/${htmlPath}`;
|
|
41
32
|
if (await pathExists(targetPath)) {
|
|
42
33
|
return console.log(`${htmlPath} already exists`);
|
|
@@ -44,7 +35,7 @@ async function writeTestsHTML(projectRoot, newQunitxConfig, oldPackageJSON) {
|
|
|
44
35
|
const targetDirectory = path.dirname(targetPath);
|
|
45
36
|
const _targetOutputPath = path.relative(
|
|
46
37
|
targetDirectory,
|
|
47
|
-
`${projectRoot}/${
|
|
38
|
+
`${projectRoot}/${config.output}/tests.js`,
|
|
48
39
|
);
|
|
49
40
|
const testHTMLTemplate = testHTMLTemplateBuffer.replace(
|
|
50
41
|
'{{applicationName}}',
|
|
@@ -60,8 +51,8 @@ async function writeTestsHTML(projectRoot, newQunitxConfig, oldPackageJSON) {
|
|
|
60
51
|
);
|
|
61
52
|
}
|
|
62
53
|
|
|
63
|
-
async function rewritePackageJSON(projectRoot,
|
|
64
|
-
const newPackageJSON = Object.assign(oldPackageJSON, { qunitx:
|
|
54
|
+
async function rewritePackageJSON(projectRoot, config, oldPackageJSON) {
|
|
55
|
+
const newPackageJSON = Object.assign(oldPackageJSON, { qunitx: config });
|
|
65
56
|
|
|
66
57
|
await fs.writeFile(`${projectRoot}/package.json`, JSON.stringify(newPackageJSON, null, 2));
|
|
67
58
|
}
|
|
@@ -140,28 +140,39 @@ function buildFilteredTests(filteredTests, outputPath, config) {
|
|
|
140
140
|
async function runTestInsideHTMLFile(filePath, { page, server, browser }, config) {
|
|
141
141
|
let QUNIT_RESULT;
|
|
142
142
|
let targetError;
|
|
143
|
+
let timeoutHandle;
|
|
143
144
|
try {
|
|
144
145
|
console.log('#', blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
|
|
145
146
|
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
// Single promise driven by the WS handler:
|
|
148
|
+
// config._testRunDone() → tests finished normally
|
|
149
|
+
// config._resetTestTimeout() → reset idle timer; fires as timeout if silent for config.timeout ms
|
|
150
|
+
// This replaces waitForFunction (CDP polling), which raced against WS testEnd messages
|
|
151
|
+
// under load: CDP could win and trigger cleanup before Node.js processed the pending messages.
|
|
152
|
+
const testRaceResult = new Promise((resolve) => {
|
|
153
|
+
config._testRunDone = () => resolve(false);
|
|
154
|
+
config._resetTestTimeout = () => {
|
|
155
|
+
clearTimeout(timeoutHandle);
|
|
156
|
+
timeoutHandle = setTimeout(() => resolve(true), config.timeout);
|
|
157
|
+
};
|
|
148
158
|
});
|
|
149
159
|
|
|
150
160
|
await page.goto(`http://localhost:${config.port}${filePath}`, {
|
|
151
161
|
timeout: config.timeout + 10000,
|
|
152
162
|
});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}),
|
|
158
|
-
]);
|
|
163
|
+
|
|
164
|
+
config._resetTestTimeout(); // start idle countdown once the page is loaded
|
|
165
|
+
|
|
166
|
+
await testRaceResult;
|
|
159
167
|
|
|
160
168
|
QUNIT_RESULT = await page.evaluate(() => window.QUNIT_RESULT);
|
|
161
169
|
} catch (error) {
|
|
162
170
|
targetError = error;
|
|
163
171
|
console.log(error);
|
|
164
172
|
console.error(error);
|
|
173
|
+
} finally {
|
|
174
|
+
clearTimeout(timeoutHandle);
|
|
175
|
+
config._resetTestTimeout = null;
|
|
165
176
|
}
|
|
166
177
|
|
|
167
178
|
if (!QUNIT_RESULT || QUNIT_RESULT.totalTests === 0) {
|
|
@@ -174,6 +185,10 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
174
185
|
console.log(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
175
186
|
console.error(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
176
187
|
await failOnNonWatchMode(config.watch, { server, browser }, config._groupMode);
|
|
188
|
+
} else if (QUNIT_RESULT.failedTests > config.COUNTER.failCount) {
|
|
189
|
+
// Safety net: browser tracked failures that WebSocket events never delivered to Node.js
|
|
190
|
+
// (e.g. WS connection dropped mid-run). Reconcile so the exit code is always correct.
|
|
191
|
+
config.COUNTER.failCount = QUNIT_RESULT.failedTests;
|
|
177
192
|
}
|
|
178
193
|
}
|
|
179
194
|
|
package/lib/commands/run.js
CHANGED
|
@@ -81,6 +81,8 @@ export default async function run(config) {
|
|
|
81
81
|
}));
|
|
82
82
|
const groupCachedContents = groups.map(() => ({ ...cachedContent }));
|
|
83
83
|
|
|
84
|
+
console.log('TAP version 13');
|
|
85
|
+
|
|
84
86
|
// Build all group bundles and write static files while the browser is starting up.
|
|
85
87
|
const [browser] = await Promise.all([
|
|
86
88
|
launchBrowser(config),
|
|
@@ -93,34 +95,74 @@ export default async function run(config) {
|
|
|
93
95
|
),
|
|
94
96
|
),
|
|
95
97
|
]);
|
|
96
|
-
|
|
97
|
-
console.log('TAP version 13');
|
|
98
98
|
const TIME_COUNTER = timeCounter();
|
|
99
|
-
let hasFatalError = false;
|
|
100
99
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
// 3-minute per-group deadline. Firefox/WebKit can hang indefinitely in any Playwright
|
|
101
|
+
// operation (browser.newPage, page.evaluate, page.close) when overwhelmed by concurrent
|
|
102
|
+
// pages. Without this outer timeout, one stuck group freezes Promise.allSettled forever.
|
|
103
|
+
// After all groups settle, browser.close() (below) terminates the browser and unblocks
|
|
104
|
+
// any still-pending Playwright calls in background async fns.
|
|
105
|
+
const GROUP_TIMEOUT_MS = 3 * 60 * 1000;
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
// Keep the event loop alive during Promise.allSettled. The Chrome child process and its
|
|
108
|
+
// stderr pipe are unref'd (pre-launch-chrome.js). If Chrome crashes during group cleanup,
|
|
109
|
+
// all active handles close and the event loop would drain — exiting silently before
|
|
110
|
+
// allSettled resolves or results are printed. This interval holds the loop open so that
|
|
111
|
+
// unref'd group/page-close timers can still fire normally.
|
|
112
|
+
const keepAlive = setInterval(() => {}, 1000);
|
|
109
113
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
114
|
+
const groupResults = await Promise.allSettled(
|
|
115
|
+
groupConfigs.map((groupConfig, i) => {
|
|
116
|
+
const groupTimeout = new Promise((_, reject) => {
|
|
117
|
+
const t = setTimeout(
|
|
118
|
+
() => reject(new Error(`Group ${i} timed out after ${GROUP_TIMEOUT_MS}ms`)),
|
|
119
|
+
GROUP_TIMEOUT_MS,
|
|
120
|
+
);
|
|
121
|
+
t.unref();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return Promise.race([
|
|
125
|
+
(async () => {
|
|
126
|
+
const connections = await setupBrowser(groupConfig, groupCachedContents[i], browser);
|
|
127
|
+
groupConfig.expressApp = connections.server;
|
|
128
|
+
|
|
129
|
+
if (config.before) {
|
|
130
|
+
await runUserModule(`${process.cwd()}/${config.before}`, groupConfig, 'before');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await runTestsInBrowser(groupConfig, groupCachedContents[i], connections);
|
|
135
|
+
} finally {
|
|
136
|
+
await Promise.all([
|
|
137
|
+
connections.server && connections.server.close(),
|
|
138
|
+
connections.page &&
|
|
139
|
+
// Unref'd: the keepAlive interval above holds the event loop open, so this
|
|
140
|
+
// timer still fires if page.close() hangs, without preventing process exit later.
|
|
141
|
+
Promise.race([
|
|
142
|
+
connections.page.close(),
|
|
143
|
+
new Promise((resolve) => {
|
|
144
|
+
const t = setTimeout(resolve, 10000);
|
|
145
|
+
t.unref();
|
|
146
|
+
}),
|
|
147
|
+
]).catch(() => {}),
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
150
|
+
})(),
|
|
151
|
+
groupTimeout,
|
|
152
|
+
]);
|
|
120
153
|
}),
|
|
121
154
|
);
|
|
122
155
|
|
|
123
|
-
|
|
156
|
+
const exitCode = groupResults.reduce(
|
|
157
|
+
(code, { status, reason }) => {
|
|
158
|
+
if (status !== 'rejected') return code;
|
|
159
|
+
console.error(reason);
|
|
160
|
+
return 1;
|
|
161
|
+
},
|
|
162
|
+
config.COUNTER.failCount > 0 ? 1 : 0,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
process.exitCode = exitCode;
|
|
124
166
|
|
|
125
167
|
TAPDisplayFinalResult(config.COUNTER, TIME_COUNTER.stop());
|
|
126
168
|
|
|
@@ -128,7 +170,18 @@ export default async function run(config) {
|
|
|
128
170
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, 'after');
|
|
129
171
|
}
|
|
130
172
|
|
|
131
|
-
|
|
173
|
+
// Flush stdout then exit. keepAlive holds the event loop open until this callback fires,
|
|
174
|
+
// at which point process.exit() takes over — so clearInterval happens here, not earlier.
|
|
175
|
+
// If the write callback never fires (theoretical), the unref'd exitTimer is the fallback.
|
|
176
|
+
const exitTimer = setTimeout(() => process.exit(exitCode), 5000);
|
|
177
|
+
exitTimer.unref();
|
|
178
|
+
process.stdout.write('\n', () => {
|
|
179
|
+
clearTimeout(exitTimer);
|
|
180
|
+
clearInterval(keepAlive);
|
|
181
|
+
// Close browser after stdout is flushed; fire-and-forget since process.exit follows.
|
|
182
|
+
browser.close().catch(() => {});
|
|
183
|
+
process.exit(exitCode);
|
|
184
|
+
});
|
|
132
185
|
}
|
|
133
186
|
}
|
|
134
187
|
|
package/lib/servers/http.js
CHANGED
|
@@ -77,11 +77,13 @@ export default class HTTPServer {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
* Closes the underlying HTTP server
|
|
81
|
-
*
|
|
80
|
+
* Closes the underlying HTTP server and all active connections, returning a
|
|
81
|
+
* Promise that resolves once the server is fully closed.
|
|
82
|
+
* @returns {Promise<void>}
|
|
82
83
|
*/
|
|
83
84
|
close() {
|
|
84
|
-
|
|
85
|
+
this._server.closeAllConnections?.();
|
|
86
|
+
return new Promise((resolve) => this._server.close(resolve));
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
/** Registers a GET route handler. */
|
|
@@ -86,9 +86,8 @@ export function handleWatchEvent(config, extensions, event, filePath, onEventFun
|
|
|
86
86
|
.then(() => {
|
|
87
87
|
onFinishFunc ? onFinishFunc(event, filePath) : null;
|
|
88
88
|
})
|
|
89
|
-
.catch(() => {
|
|
90
|
-
|
|
91
|
-
// error type has to be derived from the error!
|
|
89
|
+
.catch((error) => {
|
|
90
|
+
console.error('#', red('Build error:'), error.message || error);
|
|
92
91
|
})
|
|
93
92
|
.finally(() => (config._building = false));
|
|
94
93
|
}
|
package/lib/setup/fs-tree.js
CHANGED
|
@@ -22,8 +22,6 @@ export default async function buildFSTree(fileAbsolutePaths, config = {}) {
|
|
|
22
22
|
fileAbsolutePaths.map(async (fileAbsolutePath) => {
|
|
23
23
|
const glob = picomatch.scan(fileAbsolutePath);
|
|
24
24
|
|
|
25
|
-
// TODO: maybe allow absolute path references
|
|
26
|
-
|
|
27
25
|
try {
|
|
28
26
|
if (glob.isGlob) {
|
|
29
27
|
const fileNames = await readDirRecursive(glob.base, (name) => {
|
package/lib/setup/web-server.js
CHANGED
|
@@ -29,11 +29,13 @@ export default function setupWebServer(
|
|
|
29
29
|
|
|
30
30
|
if (event === 'connection') {
|
|
31
31
|
if (!config._groupMode) console.log('TAP version 13');
|
|
32
|
+
config._resetTestTimeout?.();
|
|
32
33
|
} else if (event === 'testEnd' && !abort) {
|
|
33
34
|
if (details.status === 'failed') {
|
|
34
35
|
config.lastFailedTestFiles = config.lastRanTestFiles;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
config._resetTestTimeout?.();
|
|
37
39
|
TAPDisplayTestResult(config.COUNTER, details);
|
|
38
40
|
} else if (event === 'done') {
|
|
39
41
|
// Signal test completion. TCP ordering guarantees all testEnd messages
|
|
@@ -214,7 +216,7 @@ function testRuntimeToInject(port, config) {
|
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
function setupQUnit() {
|
|
217
|
-
window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, currentTest: '' };
|
|
219
|
+
window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: '' };
|
|
218
220
|
|
|
219
221
|
if (!window.QUnit) {
|
|
220
222
|
console.log('QUnit not found after WebSocket connected');
|
|
@@ -239,6 +241,7 @@ function testRuntimeToInject(port, config) {
|
|
|
239
241
|
window.QUnit.on('testEnd', (details) => { // NOTE: https://github.com/qunitjs/qunit/blob/master/src/html-reporter/diff.js
|
|
240
242
|
window.testTimeout = 0;
|
|
241
243
|
window.QUNIT_RESULT.finishedTests++;
|
|
244
|
+
if (details.status === 'failed') window.QUNIT_RESULT.failedTests++;
|
|
242
245
|
window.QUNIT_RESULT.currentTest = null;
|
|
243
246
|
if (window.IS_PLAYWRIGHT) {
|
|
244
247
|
window.socket.send(JSON.stringify({ event: 'testEnd', details: details, abort: window.abortQUnit }, getCircularReplacer()));
|
|
@@ -251,10 +254,11 @@ function testRuntimeToInject(port, config) {
|
|
|
251
254
|
window.QUnit.done((details) => {
|
|
252
255
|
if (window.IS_PLAYWRIGHT) {
|
|
253
256
|
window.socket.send(JSON.stringify({ event: 'done', details: details, abort: window.abortQUnit }, getCircularReplacer()));
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
|
|
257
|
+
// Do NOT set testTimeout here. The WS 'done' event (testsDone promise) is the
|
|
258
|
+
// canonical completion signal for Playwright runs. waitForFunction is reserved
|
|
259
|
+
// for true timeouts (test hangs) where testTimeout increments naturally via setInterval.
|
|
260
|
+
// Setting testTimeout after done caused a race: under CI load, waitForFunction could
|
|
261
|
+
// win before Node.js processed the WS done message, dropping all testEnd events.
|
|
258
262
|
} else {
|
|
259
263
|
window.testTimeout = ${config.timeout};
|
|
260
264
|
}
|
|
@@ -23,7 +23,7 @@ export default function TAPDisplayTestResult(COUNTER, details) {
|
|
|
23
23
|
details.fullName.join(' | '),
|
|
24
24
|
`# (${details.runtime.toFixed(0)} ms)`,
|
|
25
25
|
);
|
|
26
|
-
details.assertions.
|
|
26
|
+
details.assertions.forEach((assertion, index) => {
|
|
27
27
|
if (!assertion.passed && assertion.todo === false) {
|
|
28
28
|
COUNTER.errorCount = (COUNTER.errorCount ?? 0) + 1;
|
|
29
29
|
const stack = assertion.stack?.match(/\(.+\)/g);
|
|
@@ -32,7 +32,7 @@ export default function TAPDisplayTestResult(COUNTER, details) {
|
|
|
32
32
|
console.log(
|
|
33
33
|
indentString(
|
|
34
34
|
dumpYaml({
|
|
35
|
-
name: `Assertion #${index + 1}`,
|
|
35
|
+
name: `Assertion #${index + 1}`,
|
|
36
36
|
actual: assertion.actual
|
|
37
37
|
? JSON.parse(JSON.stringify(assertion.actual, getCircularReplacer()))
|
|
38
38
|
: assertion.actual,
|
|
@@ -48,9 +48,7 @@ export default function TAPDisplayTestResult(COUNTER, details) {
|
|
|
48
48
|
);
|
|
49
49
|
console.log(' ...');
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
return errorCount;
|
|
53
|
-
}, 0);
|
|
51
|
+
});
|
|
54
52
|
} else if (details.status === 'passed') {
|
|
55
53
|
COUNTER.passCount++;
|
|
56
54
|
console.log(
|
|
@@ -20,13 +20,22 @@ export default function preLaunchChrome(chromePath, args) {
|
|
|
20
20
|
proc.stderr.on('data', (chunk) => {
|
|
21
21
|
buffer += chunk.toString();
|
|
22
22
|
const match = buffer.match(CDP_URL_REGEX);
|
|
23
|
-
if (match)
|
|
23
|
+
if (match) {
|
|
24
|
+
// Unref so Chrome's process + stderr pipe don't keep the Node.js event loop alive
|
|
25
|
+
// after all test work is done. Chrome is still killed via process.on('exit').
|
|
26
|
+
proc.unref();
|
|
27
|
+
proc.stderr.unref();
|
|
28
|
+
resolve({ proc, cdpEndpoint: match[1] });
|
|
29
|
+
}
|
|
24
30
|
});
|
|
25
31
|
|
|
26
|
-
// Resolve null on any startup failure so launchBrowser falls back to chromium.launch()
|
|
32
|
+
// Resolve null on any startup failure so launchBrowser falls back to chromium.launch().
|
|
33
|
+
// The close handler resolves unconditionally: if Chrome exits before printing its CDP URL
|
|
34
|
+
// (code=0 for a clean exit, code=null for a signal-killed process such as OOM on CI),
|
|
35
|
+
// the original condition `code !== null && code !== 0` would leave the promise pending
|
|
36
|
+
// forever, causing launchBrowser to hang and the event loop to drain silently (exit 0).
|
|
37
|
+
// If Chrome already printed its URL and the promise is resolved, this is a no-op.
|
|
27
38
|
proc.on('error', () => resolve(null));
|
|
28
|
-
proc.on('close', (
|
|
29
|
-
if (code !== null && code !== 0) resolve(null);
|
|
30
|
-
});
|
|
39
|
+
proc.on('close', () => resolve(null));
|
|
31
40
|
});
|
|
32
41
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qunitx-cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.2",
|
|
5
5
|
"description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
6
6
|
"main": "cli.js",
|
|
7
7
|
"author": "Izel Nakri",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"changelog:preview": "git-cliff",
|
|
26
26
|
"changelog:update": "git-cliff --output CHANGELOG.md",
|
|
27
27
|
"postinstall": "deno install --allow-scripts=npm:playwright-core || true",
|
|
28
|
-
"test": "node test/setup.js && FORCE_COLOR=0 node --test test/**/*-test.js",
|
|
29
|
-
"test:browser": "node test/setup.js && FORCE_COLOR=0 node --test test/flags/*-test.js test/inputs/*-test.js",
|
|
28
|
+
"test": "node test/setup.js && FORCE_COLOR=0 node --test --test-concurrency=$(node -p 'require(\"os\").availableParallelism()') test/**/*-test.js",
|
|
29
|
+
"test:browser": "node test/setup.js && FORCE_COLOR=0 node --test --test-concurrency=$(node -p 'require(\"os\").availableParallelism()') test/flags/*-test.js test/inputs/*-test.js",
|
|
30
30
|
"test:sanity-first": "./cli.js test/helpers/failing-tests.js test/helpers/failing-tests.ts",
|
|
31
31
|
"test:sanity-second": "./cli.js test/helpers/passing-tests.js test/helpers/passing-tests.ts"
|
|
32
32
|
},
|
|
@@ -43,16 +43,16 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"esbuild": "^0.27.3",
|
|
46
|
-
"picomatch": "^4.0.
|
|
46
|
+
"picomatch": "^4.0.4",
|
|
47
47
|
"playwright-core": "^1.58.2",
|
|
48
|
-
"ws": "^8.
|
|
48
|
+
"ws": "^8.20.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"cors": "^2.8.6",
|
|
52
52
|
"express": "^5.2.1",
|
|
53
53
|
"js-yaml": "^4.1.1",
|
|
54
54
|
"prettier": "^3.8.1",
|
|
55
|
-
"qunitx": "^1.0.
|
|
55
|
+
"qunitx": "^1.0.4"
|
|
56
56
|
},
|
|
57
57
|
"volta": {
|
|
58
58
|
"node": "24.14.0"
|