@@ -13,23 +13,41 @@ import type { NightwatchBrowser, CommandStackFrame } from '../types.js'
1313const log = logger ( '@wdio/nightwatch-devtools:browserProxy' )
1414
1515export class BrowserProxy {
16- private browserProxied = false
16+ /** Tracks which browser *instances* have already been proxied to avoid double-wrapping. */
17+ private proxiedBrowsers = new WeakSet < object > ( )
1718 private commandStack : CommandStackFrame [ ] = [ ]
1819 private lastCommandSig : string | null = null
1920 private currentTestFullPath : string | null = null
21+ /**
22+ * Tracks the last captured command so that consecutive retries of the same
23+ * command (e.g. getText inside a waitFor loop) overwrite the previous entry
24+ * rather than appending, showing only the final execution result.
25+ */
26+ private lastCapturedSig : string | null = null
27+ private lastCapturedId : number | null = null
2028
2129 constructor (
2230 private sessionCapturer : SessionCapturer ,
2331 private testManager : TestManager ,
2432 private getCurrentTest : ( ) => any
2533 ) { }
2634
35+ /**
36+ * Update the session capturer reference after a WebDriver session change.
37+ * Does NOT re-wrap browser methods — wrapping is permanent per browser object.
38+ */
39+ updateSessionCapturer ( capturer : SessionCapturer ) : void {
40+ this . sessionCapturer = capturer
41+ }
42+
2743 /**
2844 * Reset command tracking for new test
2945 */
3046 resetCommandTracking ( ) : void {
3147 this . commandStack = [ ]
3248 this . lastCommandSig = null
49+ this . lastCapturedSig = null
50+ this . lastCapturedId = null
3351 }
3452
3553 /**
@@ -82,7 +100,7 @@ export class BrowserProxy {
82100 * Wrap all browser commands to capture them
83101 */
84102 wrapBrowserCommands ( browser : NightwatchBrowser ) : void {
85- if ( this . browserProxied ) {
103+ if ( this . proxiedBrowsers . has ( browser as object ) ) {
86104 return
87105 }
88106
@@ -123,7 +141,7 @@ export class BrowserProxy {
123141 wrappedMethods . push ( methodName )
124142 } )
125143
126- this . browserProxied = true
144+ this . proxiedBrowsers . add ( browser as object )
127145 log . info ( `✓ Wrapped ${ wrappedMethods . length } browser methods` )
128146 }
129147
@@ -243,8 +261,14 @@ export class BrowserProxy {
243261 const effectiveUid = currentTest ?. uid ?? testUid
244262
245263 if ( effectiveUid ) {
246- this . sessionCapturer
247- . captureCommand (
264+ const isRetry =
265+ cmdSig === this . lastCapturedSig && this . lastCapturedId !== null
266+
267+ if ( isRetry ) {
268+ // Same command fired again (internal retry) — replace the previous
269+ // entry so only the final result appears in the UI.
270+ const { entry, oldTimestamp } = this . sessionCapturer . replaceCommand (
271+ this . lastCapturedId ! ,
248272 methodName ,
249273 logArgs ,
250274 serializedResult ,
@@ -253,18 +277,40 @@ export class BrowserProxy {
253277 callSource ,
254278 commandTimestamp
255279 )
256- . then ( ( ) => {
257- const lastCommand =
258- this . sessionCapturer . commandsLog [
259- this . sessionCapturer . commandsLog . length - 1
260- ]
261- if ( lastCommand ) {
262- this . sessionCapturer . sendCommand ( lastCommand )
263- }
264- } )
265- . catch ( ( err : any ) =>
266- log . error ( `Failed to capture ${ methodName } : ${ err . message } ` )
267- )
280+ this . lastCapturedId = entry . _id ?? null
281+ this . sessionCapturer . sendReplaceCommand ( oldTimestamp , entry )
282+ } else {
283+ // New command — capture and track.
284+ // captureCommand() pushes the entry to commandsLog synchronously
285+ // before any async work (navigation perf capture), so we can grab
286+ // the ID immediately after the call — before any microtask fires.
287+ // This avoids the race where a Nightwatch retry callback executes
288+ // before .then() sets lastCapturedId, causing missed dedup.
289+ this . lastCapturedSig = cmdSig
290+ this . lastCapturedId = null
291+ this . sessionCapturer
292+ . captureCommand (
293+ methodName ,
294+ logArgs ,
295+ serializedResult ,
296+ undefined ,
297+ effectiveUid ,
298+ callSource ,
299+ commandTimestamp
300+ )
301+ . catch ( ( err : any ) =>
302+ log . error ( `Failed to capture ${ methodName } : ${ err . message } ` )
303+ )
304+ // Read the entry synchronously — it was already pushed above.
305+ const lastCommand =
306+ this . sessionCapturer . commandsLog [
307+ this . sessionCapturer . commandsLog . length - 1
308+ ]
309+ if ( lastCommand ) {
310+ this . lastCapturedId = ( lastCommand as any ) . _id ?? null
311+ this . sessionCapturer . sendCommand ( lastCommand )
312+ }
313+ }
268314 }
269315
270316 // Forward to the user's original callback (if any)
@@ -330,9 +376,9 @@ export class BrowserProxy {
330376 }
331377
332378 /**
333- * Check if browser is already proxied
379+ * Check if a specific browser instance is already proxied
334380 */
335- isProxied ( ) : boolean {
336- return this . browserProxied
381+ isProxied ( browser : NightwatchBrowser ) : boolean {
382+ return this . proxiedBrowsers . has ( browser as object )
337383 }
338384}
0 commit comments