@@ -8,6 +8,7 @@ import path from 'node:path';
88import { fnToStr , qtapClientHead , qtapClientBody } from './client.js' ;
99import * as util from './util.js' ;
1010import tapFinished from './tap-finished.js' ;
11+ /** @import events from 'node:events' */
1112/** @import { Logger } from './qtap.js' */
1213
1314const QTAP_DEBUG = process . env . QTAP_DEBUG === '1' ;
@@ -19,12 +20,13 @@ class ControlServer {
1920 /**
2021 * @param {string|undefined } root
2122 * @param {string } testFile File path or URL
23+ * @param {events.EventEmitter } eventbus
2224 * @param {Logger } logger
2325 * @param {Object } options
2426 * @param {number|undefined } options.idleTimeout
2527 * @param {number|undefined } options.connectTimeout
2628 */
27- constructor ( root , testFile , logger , options ) {
29+ constructor ( root , testFile , eventbus , logger , options ) {
2830 if ( ! root ) {
2931 // For `qtap test/index.html`, default root to cwd.
3032 root = process . cwd ( ) ;
@@ -38,6 +40,7 @@ class ControlServer {
3840
3941 this . root = root ;
4042 this . testFile = testFile ;
43+ this . eventbus = eventbus ;
4144 this . logger = logger . channel ( 'qtap_server_' + ControlServer . nextServerId ++ ) ;
4245 this . idleTimeout = options . idleTimeout || 30 ;
4346 this . connectTimeout = options . connectTimeout || 60 ;
@@ -108,9 +111,7 @@ class ControlServer {
108111 async launchBrowser ( browserFn , browserName , globalSignal ) {
109112 const clientId = 'client_' + ControlServer . nextClientId ++ ;
110113 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 ;
114115
115116 const controller = new AbortController ( ) ;
116117 let signal = controller . signal ;
@@ -122,13 +123,6 @@ class ControlServer {
122123 } ) ;
123124 }
124125
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-
132126 // Reasons to stop a browser, whichever comes first:
133127 // 1. tap-finished.
134128 // 2. tap-parser 'bailout' event (client knows it crashed).
@@ -144,19 +138,19 @@ class ControlServer {
144138 controller . abort ( reason ) ;
145139 } ;
146140
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 ) ;
149143 stopBrowser ( 'QTap: browser_tap_finished' ) ;
150144 } ) ;
151145
152146 tapParser . on ( 'bailout' , ( reason ) => {
153147 logger . warning ( 'browser_tap_bailout' , `Test ended unexpectedly, stopping browser. Reason: ${ reason } ` ) ;
154- summary . ok = false ;
155148 stopBrowser ( 'QTap: browser_tap_bailout' ) ;
149+ this . eventbus . emit ( 'clientend' , { clientId, reason : 'browser_tap_bailout' } ) ;
156150 } ) ;
157151 tapParser . once ( 'fail' , ( ) => {
158152 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' } ) ;
160154 } ) ;
161155 // Debugging
162156 // tapParser.on('assert', logger.debug.bind(logger, 'browser_tap_assert'));
@@ -176,32 +170,47 @@ class ControlServer {
176170 // in `handleTap()` or `tapParser.on('line')`. But that adds significant overhead from
177171 // Node.js/V8 natively allocating many timers when processing large batches of test results.
178172 // 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 = ( ) => {
181177 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` ) ;
185180 stopBrowser ( 'QTap: browser_connect_timeout' ) ;
181+ this . eventbus . emit ( 'clientend' , { clientId, reason : 'browser_connect_timeout' } ) ;
186182 return ;
187183 }
188184 } 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` ) ;
192187 stopBrowser ( 'QTap: browser_idle_timeout' ) ;
188+ this . eventbus . emit ( 'clientend' , { clientId, reason : 'browser_idle_timeout' } ) ;
193189 return ;
194190 }
195191 }
196192 clientIdleTimer = setTimeout ( qtapCheckTimeout , TIMEOUT_CHECK_MS ) ;
197- } , TIMEOUT_CHECK_MS ) ;
193+ } ;
194+ clientIdleTimer = setTimeout ( qtapCheckTimeout , TIMEOUT_CHECK_MS ) ;
198195
199196 const url = await this . getProxyBase ( ) + '/?qtap_clientId=' + clientId ;
200197 const signals = { client : signal , global : globalSignal } ;
201198
202199 try {
203200 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+
205214 logger . debug ( 'browser_launch_ended' ) ;
206215 } finally {
207216 // TODO: Report error to TAP. Eg. "No executable found"
@@ -265,6 +274,7 @@ class ControlServer {
265274 if ( browser ) {
266275 browser . clientIdleActive = performance . now ( ) ;
267276 browser . logger . debug ( 'browser_connected' , `${ browser . getDisplayName ( ) } connected! Serving test file.` ) ;
277+ this . eventbus . emit ( 'clientonline' , { clientId } ) ;
268278 } else {
269279 this . logger . debug ( 'respond_static_testfile' , clientId ) ;
270280 }
@@ -309,72 +319,13 @@ class ControlServer {
309319 ) ;
310320
311321 browser . clientIdleActive = performance . now ( ) ;
322+ this . eventbus . emit ( 'clientsample' , { clientId, line : body . trimEnd ( ) . split ( '\n' ) . pop ( ) } ) ;
312323 } else {
313324 this . logger . debug ( 'browser_tap_unhandled' , clientId , JSON . stringify ( body . slice ( 0 , 30 ) + '…' ) ) ;
314325 }
315326 } ) ;
316327 resp . writeHead ( 204 ) ;
317328 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.
378329 }
379330
380331 /**
0 commit comments