@@ -23,6 +23,11 @@ export const launcher = DevToolsAppLauncher
2323
2424const log = logger ( '@wdio/devtools-service' )
2525
26+ type CommandFrame = {
27+ command : string
28+ callSource ?: string
29+ }
30+
2631/**
2732 * Setup WebdriverIO Devtools hook for standalone instances
2833 */
@@ -87,7 +92,7 @@ export default class DevToolsHookService implements Services.ServiceInstance {
8792 * This is used to capture the command stack to ensure that we only capture
8893 * commands that are top-level user commands.
8994 */
90- #commandStack: string [ ] = [ ]
95+ #commandStack: CommandFrame [ ] = [ ]
9196
9297 // This is used to capture the last command signature to avoid duplicate captures
9398 #lastCommandSig: string | null = null
@@ -101,13 +106,34 @@ export default class DevToolsHookService implements Services.ServiceInstance {
101106 // This is used to track if the injection script is currently being injected
102107 #injecting = false
103108
104- before (
109+ async before (
105110 caps : Capabilities . W3CCapabilities ,
106111 __ : string [ ] ,
107112 browser : WebdriverIO . Browser
108113 ) {
109114 this . #browser = browser
110115
116+ /**
117+ * create a new session capturer instance with the devtools options
118+ */
119+ const wdioCaps = caps as Capabilities . W3CCapabilities & {
120+ 'wdio:devtoolsOptions' ?: any
121+ }
122+ this . #sessionCapturer = new SessionCapturer (
123+ wdioCaps [ 'wdio:devtoolsOptions' ]
124+ )
125+
126+ /**
127+ * Block until injection completes BEFORE any test commands
128+ */
129+ try {
130+ await this . #injectScriptSync( browser )
131+ } catch ( err ) {
132+ log . error (
133+ `Failed to inject script at session start: ${ ( err as Error ) . message } `
134+ )
135+ }
136+
111137 /**
112138 * propagate session metadata at the beginning of the session
113139 */
@@ -121,17 +147,6 @@ export default class DevToolsHookService implements Services.ServiceInstance {
121147 capabilities : browser . capabilities as Capabilities . W3CCapabilities
122148 } )
123149 )
124- this . #ensureInjected( 'session-start' )
125-
126- /**
127- * create a new session capturer instance with the devtools options
128- */
129- const wdioCaps = caps as Capabilities . W3CCapabilities & {
130- 'wdio:devtoolsOptions' ?: any
131- }
132- this . #sessionCapturer = new SessionCapturer (
133- wdioCaps [ 'wdio:devtoolsOptions' ]
134- )
135150 }
136151
137152 // The method signature is corrected to use W3CCapabilities
@@ -216,14 +231,37 @@ export default class DevToolsHookService implements Services.ServiceInstance {
216231 this . #commandStack. length === 0 &&
217232 ! INTERNAL_COMMANDS . includes ( command )
218233 ) {
234+ const rawFile = source . getFileName ( ) ?? undefined
235+ let absPath = rawFile
236+
237+ if ( rawFile ?. startsWith ( 'file://' ) ) {
238+ try {
239+ const url = new URL ( rawFile )
240+ absPath = decodeURIComponent ( url . pathname )
241+ } catch {
242+ absPath = rawFile
243+ }
244+ }
245+
246+ if ( absPath ?. includes ( '?' ) ) {
247+ absPath = absPath . split ( '?' ) [ 0 ]
248+ }
249+
250+ const line = source . getLineNumber ( ) ?? undefined
251+ const column = source . getColumnNumber ( ) ?? undefined
252+ const callSource =
253+ absPath !== undefined
254+ ? `${ absPath } :${ line ?? 0 } :${ column ?? 0 } `
255+ : undefined
256+
219257 const cmdSig = JSON . stringify ( {
220258 command,
221259 args,
222- src : source . getFileName ( ) + ':' + source . getLineNumber ( )
260+ src : callSource
223261 } )
224262
225263 if ( this . #lastCommandSig !== cmdSig ) {
226- this . #commandStack. push ( command )
264+ this . #commandStack. push ( { command, callSource } )
227265 this . #lastCommandSig = cmdSig
228266 }
229267 }
@@ -243,15 +281,17 @@ export default class DevToolsHookService implements Services.ServiceInstance {
243281 /* Ensure that the command is captured only if it matches the last command in the stack.
244282 * This prevents capturing commands that are not top-level user commands.
245283 */
246- if ( this . #commandStack[ this . #commandStack. length - 1 ] === command ) {
284+ const frame = this . #commandStack[ this . #commandStack. length - 1 ]
285+ if ( frame ?. command === command ) {
247286 this . #commandStack. pop ( )
248287 if ( this . #browser) {
249288 return this . #sessionCapturer. afterCommand (
250289 this . #browser,
251290 command ,
252291 args ,
253292 result ,
254- error
293+ error ,
294+ frame . callSource
255295 )
256296 }
257297 }
@@ -295,16 +335,27 @@ export default class DevToolsHookService implements Services.ServiceInstance {
295335 log . info ( `DevTools trace saved to ${ traceFilePath } ` )
296336 }
297337
298- async #ensureInjected( reason : string ) {
299- if ( ! this . #browser) {
300- return
338+ /**
339+ * Synchronous injection that blocks until complete
340+ */
341+ async #injectScriptSync( browser : WebdriverIO . Browser ) {
342+ if ( ! browser . isBidi ) {
343+ throw new SevereServiceError (
344+ `Can not set up devtools for session with id "${ browser . sessionId } " because it doesn't support WebDriver Bidi`
345+ )
301346 }
302- if ( this . #injecting) {
347+
348+ await this . #sessionCapturer. injectScript ( getBrowserObject ( browser ) )
349+ log . info ( '✓ Devtools preload script active' )
350+ }
351+
352+ async #ensureInjected( reason : string ) {
353+ // Keep this for re-injection after context changes
354+ if ( ! this . #browser || this . #injecting) {
303355 return
304356 }
305357 try {
306358 this . #injecting = true
307- // Cheap marker check (no heavy stack work)
308359 const markerPresent = await this . #browser. execute ( ( ) => {
309360 return Boolean ( ( window as any ) . __WDIO_DEVTOOLS_MARK )
310361 } )
0 commit comments