@@ -13,7 +13,7 @@ import { DevToolsAppLauncher } from './launcher.js'
1313import { getBrowserObject } from './utils.ts'
1414import { parse } from 'stack-trace'
1515import { type TraceLog , TraceType } from './types.ts'
16- import { INTERNAL_COMMANDS , SPEC_FILE_PATTERN } from './constants.ts'
16+ import { INTERNAL_COMMANDS , SPEC_FILE_PATTERN , CONTEXT_CHANGE_COMMANDS } from './constants.ts'
1717
1818export const launcher = DevToolsAppLauncher
1919
@@ -86,6 +86,9 @@ export default class DevToolsHookService implements Services.ServiceInstance {
8686 */
8787 captureType = TraceType . Testrunner
8888
89+ // This is used to track if the injection script is currently being injected
90+ #injecting = false
91+
8992 before ( caps : Capabilities . W3CCapabilities , __ : string [ ] , browser : WebdriverIO . Browser ) {
9093 this . #browser = browser
9194
@@ -99,6 +102,7 @@ export default class DevToolsHookService implements Services.ServiceInstance {
99102 options : browser . options ,
100103 capabilities : browser . capabilities as Capabilities . W3CCapabilities ,
101104 } ) )
105+ this . #ensureInjected( 'session-start' )
102106
103107 /**
104108 * create a new session capturer instance with the devtools options
@@ -156,9 +160,6 @@ export default class DevToolsHookService implements Services.ServiceInstance {
156160 async beforeCommand ( command : string , args : string [ ] ) {
157161 if ( ! this . #browser) { return }
158162
159- // Always inject the script to support iframe detection etc.
160- await this . #sessionCapturer. injectScript ( getBrowserObject ( this . #browser) )
161-
162163 /**
163164 * propagate url change to devtools app
164165 */
@@ -173,9 +174,12 @@ export default class DevToolsHookService implements Services.ServiceInstance {
173174 const stack = parse ( new Error ( '' ) ) . reverse ( )
174175 const source = stack . find ( ( frame ) => {
175176 const file = frame . getFileName ( )
176- // Only consider frames from user spec/test files
177+ // Only consider command frames from user spec/test files
177178 return file && SPEC_FILE_PATTERN . test ( file )
178179 } )
180+ log . debug ( 'Command: ' , command )
181+ log . debug ( 'Source: ' , JSON . stringify ( source ) )
182+ log . debug ( 'Stack: ' , JSON . stringify ( stack ) )
179183
180184 if ( source && this . #commandStack. length === 0 && ! INTERNAL_COMMANDS . includes ( command ) ) {
181185 const cmdSig = JSON . stringify ( {
@@ -192,6 +196,9 @@ export default class DevToolsHookService implements Services.ServiceInstance {
192196 }
193197
194198 afterCommand ( command : keyof WebDriverCommands , args : any [ ] , result : any , error ?: Error ) {
199+ // Skip bookkeeping for internal injection calls
200+ if ( this . #injecting) return
201+
195202 /* Ensure that the command is captured only if it matches the last command in the stack.
196203 * This prevents capturing commands that are not top-level user commands.
197204 */
@@ -201,6 +208,11 @@ export default class DevToolsHookService implements Services.ServiceInstance {
201208 return this . #sessionCapturer. afterCommand ( this . #browser, command , args , result , error )
202209 }
203210 }
211+
212+ // Re-inject AFTER context-changing commands complete so new documents/frames are instrumented
213+ if ( CONTEXT_CHANGE_COMMANDS . includes ( command ) ) {
214+ void this . #ensureInjected( `context-change:${ command } ` )
215+ }
204216 }
205217
206218 /**
@@ -232,4 +244,24 @@ export default class DevToolsHookService implements Services.ServiceInstance {
232244 await fs . writeFile ( traceFilePath , JSON . stringify ( traceLog ) )
233245 log . info ( `DevTools trace saved to ${ traceFilePath } ` )
234246 }
247+
248+ async #ensureInjected( reason : string ) {
249+ if ( ! this . #browser) return
250+ if ( this . #injecting) return
251+ try {
252+ this . #injecting = true
253+ // Cheap marker check (no heavy stack work)
254+ const markerPresent = await this . #browser. execute ( ( ) => {
255+ return Boolean ( ( window as any ) . __WDIO_DEVTOOLS_MARK )
256+ } )
257+ if ( markerPresent ) {
258+ return
259+ }
260+ await this . #sessionCapturer. injectScript ( getBrowserObject ( this . #browser) )
261+ } catch ( err ) {
262+ log . warn ( `[inject] failed (reason=${ reason } ): ${ ( err as Error ) . message } ` )
263+ } finally {
264+ this . #injecting = false
265+ }
266+ }
235267}
0 commit comments