Skip to content

Commit d568577

Browse files
committed
server,browsers: Promote bailout to error and fix multi file in Safari
Minor: * browsers: Fix safari() bug when running multiple test files. * browsers: Rename Headless Firefox => Firefox Headless. * client: Set `window.qunit_config_reporters_html = false`. * client: Add basic visual `<pre>` output in debug mode. * server: Change clientId format from "client_1" to "client_S1_C1" to make verbose logs more useful. This makes it obvious which clients are for the same testFile, and thus ControlServer. * qtap: De-duplicate browsers and test files. Major: * qtap,server: Streamline error handling and signal handling to avoid errors going unhandled, bypassing teardown, or causing the process to be stuck waiting for nothing. In particular, "Could not open <file>" is now propagated to a top-level error (e.g. run throws/rejects) rather than a test-level "bail". This means we emit "error" instead of "result" or "finish", and other tests are cancelled as well. With this in place, other uncaught errors are now handled better as well, such as errors from reporters. * qtap: handle uncaught errors from reporters, which otherwise cause emit() to throw. Provide a dedicated EventEmitter proxy to improve attribution in the error message to a given reporter. This is limited to "on" both because its simpler that way, and because it protects our EventEmitter object from reporters tampering with "emit" or "on". * reporter: Flesh out more of the default reporter, and refine the events we emit to ease the work of reporters. In particular, promote all bailouts to first-class errors that bubble up and cancel other clients. This way, reporters don't need to deal with formatting bailout as assert failure or or numbering test counts (i.e. Completed 0 tests, 0 failed.), and thus don't receive a "finish" event.
1 parent a44acc4 commit d568577

File tree

13 files changed

+781
-391
lines changed

13 files changed

+781
-391
lines changed

API.md

Lines changed: 50 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -155,70 +155,78 @@ async function mybrowser (url, signals) {
155155
}
156156
```
157157

158-
## QTap basic events
158+
## QTap summary events
159159

160-
### Event: `'error'`
160+
These are emitted at most once for the run overall.
161161

162-
* `error <Error|string>`
162+
### Event: `'clients'`
163163

164-
### Event: `'finish'`
164+
The `clients` event conveys which browsers are being started, and which tests will be run. It is emitted as soon as QTap has validated the parameters. Each client is a browser process that runs one test suite. For example, if you run 2 test suites in 3 different browsers, there will be 6 clients.
165165

166-
Summary event that is ideal for when you run one test suite in one browser, or if you otherwise don't need a break down of results by client.
166+
* `event.clients {Object<string,Object>}` Keyed by clientId
167+
* `clientId {string}` An identifier unique within the current qtap process (e.g. `client_123`).
168+
* `testFile {string}` Relative file path or URL (e.g. `test/index.html` or `http://localhost/test/`).
169+
* `browserName {string}` Browser name, as specified in config or CLI (e.g. `firefox`).
170+
* `displayName {string}` Browser pretty name (e.g. "Headless Firefox").
167171

168-
* `event.ok <boolean>` Aggregate status of each client's results. If any failed, this is false.
169-
* `event.exitCode <number>` Suggested exit code, 0 for success, 1 for failed.
170-
* `event.total <number>` Aggregated from `result` events.
171-
* `event.passed <number>` Aggregated from `result` events.
172-
* `event.failed <number>` Aggregated from `result` events.
173-
* `event.skips <array>` Carried from the first client that failed, or empty.
174-
* `event.todos <array>` Carried from the first client that failed, or empty.
175-
* `event.failures <array>` Carried from the first client that failed, or empty.
176-
* `event.bailout <false|string>` Carried from the first client that failed, or false.
172+
### Event: `'error'`
177173

178-
## QTap reporter events
174+
* `error {Error|string}`
175+
176+
### Event: `'finish'`
179177

180-
A client will emit each of these events only once, except `consoleerror` which may be emitted any number of times.
178+
Summary event that is ideal for when you run one test suite in one browser, or if you don't need a break down of results by client.
181179

182-
### Event: `'client'`
180+
* `event.ok {boolean}` Aggregate status of each client's results. If any failed, this is false.
181+
* `event.exitCode {number}` Suggested exit code, 0 for success, 1 for failed.
182+
* `event.total {number}` Aggregated from `result` events.
183+
* `event.passed {number}` Aggregated from `result` events.
184+
* `event.failed {number}` Aggregated from `result` events.
185+
* `event.skips {array}` Carried from the first client that failed, or empty.
186+
* `event.todos {array}` Carried from the first client that failed, or empty.
187+
* `event.failures {array}` Carried from the first client that failed, or empty.
183188

184-
The `client` event is emitted when a client is created. A client is a dedicated browser instance that runs one test suite. For example, if you run 2 test suites in 3 different browsers, there will be 6 clients.
189+
## QTap client events
185190

186-
* `event.clientId <string>` An identifier unique within the current qtap process (e.g. `client_123`).
187-
* `event.testFile <string>` Relative file path or URL (e.g. `test/index.html` or `http://localhost/test/`).
188-
* `event.browserName <string>` Browser name, as specified in config or CLI (e.g. `firefox`).
189-
* `event.displayName <string>` Browser pretty name, (e.g. "Headless Firefox").
191+
These are emitted once per client, except `consoleerror` and `assert` which may be emitted many times by a client during a test run.
190192

191193
### Event: `'online'`
192194

193-
The `online` event is emitted when a browser has successfully started and opened the test file. If a browser fails to connect, a `bail` event is emitted instead.
195+
The `online` event is emitted when a browser has successfully started and opened the test file. If a browser fails to connect or a test run bailed out, then an `error` event is emitted instead.
194196

195-
* `event.clientId <string>`
197+
* `event.clientId {string}`
198+
* `event.testFile {string}`
199+
* `event.browserName {string}`
200+
* `event.displayName {string}`
196201

197202
### Event: `'result'`
198203

199-
The `result` event is emitted when a browser has completed a test run. This is mutually exclusive with the `bail` event.
204+
The `result` event is emitted when a browser has completed a test run.
200205

201-
* `event.clientId <string>`
202-
* `event.ok <boolean>`
203-
* `event.total <number>`
204-
* `event.passed <number>`
205-
* `event.failed <number>`
206-
* `event.skips <array>` Details about skipped tests (count as passed).
207-
* `event.todos <array>` Details about todo tests (count as passed).
208-
* `event.failures <array>` Details about failed tests.
206+
* `event.clientId {string}`
207+
* `event.ok {boolean}`
208+
* `event.total {number}`
209+
* `event.passed {number}`
210+
* `event.failed {number}`
211+
* `event.skips {array}` Details about skipped tests (count as passed).
212+
* `event.todos {array}` Details about todo tests (count as passed).
213+
* `event.failures {array}` Details about failed tests.
209214

210-
### Event: `'bail'`
215+
### Event: `'consoleerror'`
211216

212-
The `bail` event is emitted when a browser was unable to start or complete a test run.
217+
The `consoleerror` event relays any warning or error messages from the browser console. These are for debug purposes only, and do not indicate that a test has failed. A complete and successful test run, may nonetheless print warnings or errors to the console.
213218

214-
* `event.clientId <string>`
215-
* `event.reason <string>`
219+
It is recommended that reporters only display console errors if a test run failed (i.e. there was a failed test result, or an uncaught error).
216220

217-
### Event: `'consoleerror'`
221+
* `event.clientId {string}`
222+
* `event.message {string}`
218223

219-
The `consoleerror` event relays any warning or error messages from the browser console. These are for debug purposes only, and do not indicate that a test has failed. A complete and successful test run, may nonetheless print warnings or errors to the console.
224+
### Event: `'assert'`
220225

221-
It is recommended that reporters only display console errors if a test run failed (i.e. there was a failed test result, or the cilent bailed).
226+
The `assert` event describes a single test result (whether passing or failing). This can be used by reporters to indicate activity, display the name of a test in real-time, or to convey failures early.
222227

223-
* `event.clientId <string>`
224-
* `event.message <string>`
228+
* `event.clientId {string}`
229+
* `event.result {Object}`
230+
* `ok {boolean}`
231+
* `fullname {string}`
232+
* `diag {undefined|Object}`

ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ Safari has long resisted the temptation to offer a reasonable command-line inter
436436
{"value":null}
437437
```
438438

439-
This addresses all previous concerns, and seems to be the best as of 2025. The only downside is that it requires a bit more code to setup (find available port, and perform various HTTP requests).
439+
This addresses all previous concerns, and seems to work best as of 2025. The only downside is that it requires more code to set up (find available port, and perform various HTTP requests).
440440

441441
- https://webkit.org/blog/6900/webdriver-support-in-safari-10/
442442
- https://developer.apple.com/documentation/webkit/macos-webdriver-commands-for-safari-12-and-later

bin/qtap.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#!/usr/bin/env node
22
'use strict';
33

4+
import util from 'node:util';
5+
46
import { program, InvalidArgumentError } from 'commander';
57
import qtap from '../src/qtap.js';
68

@@ -90,7 +92,31 @@ if (opts.version) {
9092
});
9193
process.exit(result.exitCode);
9294
} catch (e) {
93-
console.error(e);
95+
if (e instanceof qtap.QTapError && e.qtapClient) {
96+
console.log(
97+
'\n'
98+
+ util.styleText('bgRedBright',
99+
util.styleText('redBright', '__')
100+
+ util.styleText(['whiteBright', 'bold'], 'BAILED')
101+
+ util.styleText('redBright', '__')
102+
)
103+
+ '\n'
104+
);
105+
console.error(util.styleText('grey',
106+
`Bail out from ${e.qtapClient.testFile} in ${e.qtapClient.browser}:`
107+
));
108+
// Omit internal stack trace, not useful here
109+
console.error(util.styleText('bold',
110+
e.message
111+
));
112+
} else {
113+
if (e instanceof qtap.QTapError) {
114+
console.error(e.toString());
115+
} else {
116+
// Print including full stack trace
117+
console.error(e);
118+
}
119+
}
94120
process.exit(1);
95121
}
96122
}

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default [
2525
}
2626
},
2727
{
28-
files: ['test/*.js'],
28+
files: ['test/**/*.js'],
2929
languageOptions: {
3030
globals: {
3131
QUnit: 'readonly'

src/browsers.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ async function firefox (url, signals, logger, debugMode) {
118118
const profileDir = LocalBrowser.makeTempDir(signals, logger);
119119
const args = [url, '-profile', profileDir, '-no-remote', '-wait-for-browser'];
120120
if (!debugMode) {
121-
firefox.displayName = 'Headless Firefox';
121+
firefox.displayName = 'Firefox Headless';
122122
args.push('-headless');
123123
}
124124

@@ -164,7 +164,7 @@ firefox.displayName = 'Firefox';
164164
function makeChromium (displayName, getPaths) {
165165
/** @type {Browser} - https://github.com/microsoft/TypeScript/issues/22063 */
166166
const chromium = async function (url, signals, logger, debugMode) {
167-
chromium.displayName = debugMode ? displayName : `Headless ${displayName}`;
167+
chromium.displayName = debugMode ? displayName : `${displayName} Headless`;
168168
// https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md
169169
const dataDir = LocalBrowser.makeTempDir(signals, logger);
170170
const args = [

src/client.cjs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
function qtapClientHead () {
55
// Support QUnit 2.24+: Enable TAP reporter, declaratively.
66
window.qunit_config_reporters_tap = true;
7+
window.qunit_config_reporters_html = false;
78

89
// Cache references to original methods, to avoid getting trapped by mocks (e.g. Sinon)
910
var setTimeout = window.setTimeout;
1011
var XMLHttpRequest = window.XMLHttpRequest;
12+
var createTextNode = document.createTextNode && document.createTextNode.bind && document.createTextNode.bind(document);
1113

1214
// Support IE 9: console.log.apply is undefined.
1315
// Don't bother with Function.apply.call. Skip super call instead.
@@ -67,6 +69,7 @@ function qtapClientHead () {
6769
function createBufferedWrite (url) {
6870
var buffer = '';
6971
var isSending = false;
72+
var debugElement = false;
7073
function send () {
7174
var body = buffer;
7275
buffer = '';
@@ -81,8 +84,16 @@ function qtapClientHead () {
8184
};
8285
xhr.open('POST', url, true);
8386
xhr.send(body);
87+
88+
// Optimization: Only check this once, during the first send
89+
if (debugElement === false) {
90+
debugElement = document.getElementById('__qtap_debug_element') || null;
91+
}
92+
if (debugElement) {
93+
debugElement.appendChild(createTextNode(body));
94+
}
8495
}
85-
return function write (str) {
96+
return function writeTap (str) {
8697
buffer += str + '\n';
8798
if (!isSending) {
8899
isSending = true;

0 commit comments

Comments
 (0)