@@ -8,6 +8,7 @@ import path from 'node:path';
8
8
import { fnToStr , qtapClientHead , qtapClientBody } from './client.js' ;
9
9
import * as util from './util.js' ;
10
10
import tapFinished from './tap-finished.js' ;
11
+ /** @import events from 'node:events' */
11
12
/** @import { Logger } from './qtap.js' */
12
13
13
14
const QTAP_DEBUG = process . env . QTAP_DEBUG === '1' ;
@@ -19,12 +20,13 @@ class ControlServer {
19
20
/**
20
21
* @param {string|undefined } root
21
22
* @param {string } testFile File path or URL
23
+ * @param {events.EventEmitter } eventbus
22
24
* @param {Logger } logger
23
25
* @param {Object } options
24
26
* @param {number|undefined } options.idleTimeout
25
27
* @param {number|undefined } options.connectTimeout
26
28
*/
27
- constructor ( root , testFile , logger , options ) {
29
+ constructor ( root , testFile , eventbus , logger , options ) {
28
30
if ( ! root ) {
29
31
// For `qtap test/index.html`, default root to cwd.
30
32
root = process . cwd ( ) ;
@@ -38,6 +40,7 @@ class ControlServer {
38
40
39
41
this . root = root ;
40
42
this . testFile = testFile ;
43
+ this . eventbus = eventbus ;
41
44
this . logger = logger . channel ( 'qtap_server_' + ControlServer . nextServerId ++ ) ;
42
45
this . idleTimeout = options . idleTimeout || 30 ;
43
46
this . connectTimeout = options . connectTimeout || 60 ;
@@ -108,9 +111,7 @@ class ControlServer {
108
111
async launchBrowser ( browserFn , browserName , globalSignal ) {
109
112
const clientId = 'client_' + ControlServer . nextClientId ++ ;
110
113
const logger = this . logger . channel ( `qtap_browser_${ clientId } _${ browserName } ` ) ;
111
-
112
- // TODO: Remove `summary` in favour of `eventbus`
113
- const summary = { ok : true } ;
114
+ let clientIdleTimer = null ;
114
115
115
116
const controller = new AbortController ( ) ;
116
117
let signal = controller . signal ;
@@ -122,13 +123,6 @@ class ControlServer {
122
123
} ) ;
123
124
}
124
125
125
- // TODO: Write test for --connect-timeout by using a no-op browser.
126
- const TIMEOUT_CONNECT = this . connectTimeout ;
127
- const TIMEOUT_IDLE = this . idleTimeout ;
128
- const TIMEOUT_CHECK_MS = 1000 ;
129
- const launchStart = performance . now ( ) ;
130
- let clientIdleTimer = null ;
131
-
132
126
// Reasons to stop a browser, whichever comes first:
133
127
// 1. tap-finished.
134
128
// 2. tap-parser 'bailout' event (client knows it crashed).
@@ -144,19 +138,19 @@ class ControlServer {
144
138
controller . abort ( reason ) ;
145
139
} ;
146
140
147
- const tapParser = tapFinished ( { wait : 0 } , ( ) => {
148
- logger . debug ( 'browser_tap_finished' , 'Test has finished, stopping browser' ) ;
141
+ const tapParser = tapFinished ( { wait : 0 } , ( result ) => {
142
+ logger . debug ( 'browser_tap_finished' , 'Test has finished, stopping browser' , result . ok ) ;
149
143
stopBrowser ( 'QTap: browser_tap_finished' ) ;
150
144
} ) ;
151
145
152
146
tapParser . on ( 'bailout' , ( reason ) => {
153
147
logger . warning ( 'browser_tap_bailout' , `Test ended unexpectedly, stopping browser. Reason: ${ reason } ` ) ;
154
- summary . ok = false ;
155
148
stopBrowser ( 'QTap: browser_tap_bailout' ) ;
149
+ this . eventbus . emit ( 'clientend' , { clientId, reason : 'browser_tap_bailout' } ) ;
156
150
} ) ;
157
151
tapParser . once ( 'fail' , ( ) => {
158
152
logger . debug ( 'browser_tap_fail' , 'Results indicate at least one test has failed assertions' ) ;
159
- summary . ok = false ;
153
+ this . eventbus . emit ( 'clientend' , { clientId , reason : 'browser_tap_fail' } ) ;
160
154
} ) ;
161
155
// Debugging
162
156
// tapParser.on('assert', logger.debug.bind(logger, 'browser_tap_assert'));
@@ -176,32 +170,47 @@ class ControlServer {
176
170
// in `handleTap()` or `tapParser.on('line')`. But that adds significant overhead from
177
171
// Node.js/V8 natively allocating many timers when processing large batches of test results.
178
172
// Instead, merely store performance.now() and check that periodically.
179
- clientIdleTimer = setTimeout ( function qtapCheckTimeout ( ) {
180
- // TODO: Report timeout failure to reporter/TAP/CLI.
173
+ // TODO: Write test for --connect-timeout by using a no-op browser.
174
+ const TIMEOUT_CHECK_MS = 1000 ;
175
+ const launchStart = performance . now ( ) ;
176
+ const qtapCheckTimeout = ( ) => {
181
177
if ( ! browser . clientIdleActive ) {
182
- if ( ( performance . now ( ) - launchStart ) > ( TIMEOUT_CONNECT * 1000 ) ) {
183
- logger . warning ( 'browser_connect_timeout' , `Browser did not start within ${ TIMEOUT_CONNECT } s, stopping browser` ) ;
184
- summary . ok = false ;
178
+ if ( ( performance . now ( ) - launchStart ) > ( this . connectTimeout * 1000 ) ) {
179
+ logger . warning ( 'browser_connect_timeout' , `Browser did not start within ${ this . connectTimeout } s, stopping browser` ) ;
185
180
stopBrowser ( 'QTap: browser_connect_timeout' ) ;
181
+ this . eventbus . emit ( 'clientend' , { clientId, reason : 'browser_connect_timeout' } ) ;
186
182
return ;
187
183
}
188
184
} else {
189
- if ( ( performance . now ( ) - browser . clientIdleActive ) > ( TIMEOUT_IDLE * 1000 ) ) {
190
- logger . warning ( 'browser_idle_timeout' , `Browser idle for ${ TIMEOUT_IDLE } s, stopping browser` ) ;
191
- summary . ok = false ;
185
+ if ( ( performance . now ( ) - browser . clientIdleActive ) > ( this . idleTimeout * 1000 ) ) {
186
+ logger . warning ( 'browser_idle_timeout' , `Browser idle for ${ this . idleTimeout } s, stopping browser` ) ;
192
187
stopBrowser ( 'QTap: browser_idle_timeout' ) ;
188
+ this . eventbus . emit ( 'clientend' , { clientId, reason : 'browser_idle_timeout' } ) ;
193
189
return ;
194
190
}
195
191
}
196
192
clientIdleTimer = setTimeout ( qtapCheckTimeout , TIMEOUT_CHECK_MS ) ;
197
- } , TIMEOUT_CHECK_MS ) ;
193
+ } ;
194
+ clientIdleTimer = setTimeout ( qtapCheckTimeout , TIMEOUT_CHECK_MS ) ;
198
195
199
196
const url = await this . getProxyBase ( ) + '/?qtap_clientId=' + clientId ;
200
197
const signals = { client : signal , global : globalSignal } ;
201
198
202
199
try {
203
200
logger . debug ( 'browser_launch_call' ) ;
204
- await browserFn ( url , signals , logger ) ;
201
+
202
+ // Separate calling browserFn() from awaiting so that we can emit 'createclient'
203
+ // right after calling it (which may set Browser.displayName). If we await here,
204
+ // the event would be emitted when the client is done instead of when it starts.
205
+ const browerPromise = browserFn ( url , signals , logger ) ;
206
+ this . eventbus . emit ( 'clientcreate' , {
207
+ testFile : this . testFile ,
208
+ clientId,
209
+ browserName,
210
+ displayName : browser . getDisplayName ( )
211
+ } ) ;
212
+ await browerPromise ;
213
+
205
214
logger . debug ( 'browser_launch_ended' ) ;
206
215
} finally {
207
216
// TODO: Report error to TAP. Eg. "No executable found"
@@ -265,6 +274,7 @@ class ControlServer {
265
274
if ( browser ) {
266
275
browser . clientIdleActive = performance . now ( ) ;
267
276
browser . logger . debug ( 'browser_connected' , `${ browser . getDisplayName ( ) } connected! Serving test file.` ) ;
277
+ this . eventbus . emit ( 'clientonline' , { clientId } ) ;
268
278
} else {
269
279
this . logger . debug ( 'respond_static_testfile' , clientId ) ;
270
280
}
@@ -309,72 +319,13 @@ class ControlServer {
309
319
) ;
310
320
311
321
browser . clientIdleActive = performance . now ( ) ;
322
+ this . eventbus . emit ( 'clientsample' , { clientId, line : body . trimEnd ( ) . split ( '\n' ) . pop ( ) } ) ;
312
323
} else {
313
324
this . logger . debug ( 'browser_tap_unhandled' , clientId , JSON . stringify ( body . slice ( 0 , 30 ) + '…' ) ) ;
314
325
}
315
326
} ) ;
316
327
resp . writeHead ( 204 ) ;
317
328
resp . end ( ) ;
318
-
319
- // TODO: Pipe to one of two options, based on --reporter:
320
- // - [tap | default in piped and CI?]: tap-parser + some kind of renumbering or prefixing.
321
- // client_1> ok 40 foo > bar
322
- // out> ok 1 - qtap > Firefox (client_1) connected! Running test/index.html.
323
- // out> ok 42 - foo > bar
324
- // -->
325
- // client_1> ok 40 foo > bar
326
- // client_2> ok 40 foo > bar
327
- // out> ok 1 - qtap > Firefox (client_1) connected! Running test/index.html.
328
- // out> ok 2 - qtap > Chromium (client_2) connected! Running test/index.html.
329
- // out> ok 81 - foo > bar [Firefox client_1]
330
- // out> ok 82 - foo > bar [Chromium client_2]
331
- // - [minimal|default in interactive mode]
332
- // out> Testing /test/index.html
333
- // out>
334
- // out> Firefox : SPINNER [blue]
335
- // out> Running test 40.
336
- // out> [Chromium] : [grey] [star] [grey] Launching...
337
- // out> [Safari] : [grey] [star] [grey] Launching...
338
- // -->
339
- // out> Testing /test/index.html
340
- // out>
341
- // out> [Firefox client_1]: ✔ [green] Completed 123 tests in 42ms.
342
- // out> [Chromium client2]: [blue*spinner] Running test 40.
343
- // out> [Safari client_3] [grey] [star] [grey] Launching...
344
- // -->
345
- // out> Testing /test/index.html
346
- // out>
347
- // out> not ok 40 foo > bar # Chromium client_2
348
- // out> ---
349
- // out> message: failed
350
- // out> actual : false
351
- // out> expected: true
352
- // out> stack: |
353
- // out> @/example.js:46:12
354
- // out> ...
355
- // out>
356
- // out> [Firefox client_1]: ✔ [green] Completed 123 tests in 42ms.
357
- // out> [Chromium client_2]: ✘ [red] 2 failures.
358
- //
359
- // If minimal is selected explicilty in piped/non-interactive/CI mode,
360
- // then it will have no spinners, and also lines won't overwrite each other.
361
- // Test counting will be disabled along with the spinner so instead we'll print:
362
- // out> Firefox client_1: Launching...
363
- // out> Firefox client_1: Running tests... [= instead of spinner/counter]
364
- // out> Firefox client_1: Completed 123 tets in 42ms.
365
-
366
- // "▓", "▒", "░" // noise, 100
367
- // "㊂", "㊀", "㊁" // toggle10, 100
368
- // await new Promise(r=>setTimeout(r,100)); process.stdout.write('\r' + frames[i % frames.length] + ' ');
369
- // writable.isTTY
370
- // !process.env.CI
371
-
372
- // Default: TAP where each browser is 1 virtual test in case of success.
373
- // Verbose: TAP forwarded, test names prepended with [browsername].
374
- // Failures are shown either way, with prepended names.
375
- // TODO: On "runEnd", report runtime
376
- // Default: No-op, as overall TAP line as single test (above) can contain runtime
377
- // Verbose: Output comment indicatinh browser done, and test runtime.
378
329
}
379
330
380
331
/**
0 commit comments