11import type { RunnerTestCase , RunnerTestFile , TestArtifact } from 'vitest'
22import type { BrowserTraceData , BrowserTraceEntry } from '../../../browser/src/client/tester/trace'
3- import { ref , watch , watchEffect } from 'vue'
3+ import { computed , ref , watch , watchEffect } from 'vue'
44import { getProjectConfigByName } from '~/utils/task'
55import { browserState , client , config } from './client'
66import { detailsPosition } from './navigation'
7- import { selectedTest } from './params'
7+ import { selectedTest , selectedTraceAttempt , selectedTraceStep } from './params'
88
99export interface TraceSelection {
1010 test : RunnerTestCase
@@ -82,7 +82,7 @@ function normalizeTraceEntries(entries: BrowserTraceEntry[]): NormalizedBrowserT
8282 return merged
8383}
8484
85- export function getTraceAttemptMap ( artifacts : TestArtifact [ ] ) : Record < string , NormalizedBrowserTraceData > {
85+ export function getTraceAttemptMap ( artifacts : TestArtifact [ ] ) : Map < string , NormalizedBrowserTraceData > {
8686 const grouped : Record < string , BrowserTraceData [ ] > = { }
8787 for ( const artifact of artifacts ) {
8888 if ( artifact . type !== 'internal:browserTrace' ) {
@@ -94,23 +94,23 @@ export function getTraceAttemptMap(artifacts: TestArtifact[]): Record<string, No
9494 grouped [ key ] . push ( trace )
9595 }
9696
97- const merged : Record < string , NormalizedBrowserTraceData > = { }
97+ const merged = new Map < string , NormalizedBrowserTraceData > ( )
9898 for ( const [ key , traces ] of Object . entries ( grouped ) ) {
9999 const trace = traces [ 0 ]
100100 const entries = traces . flatMap ( trace => trace . entries )
101- merged [ key ] = {
101+ merged . set ( key , {
102102 ...trace ,
103103 entries : normalizeTraceEntries ( entries ) ,
104- }
104+ } )
105105 }
106106 return merged
107107}
108108
109109export function getSelectedTrace ( selection : TraceSelection ) : NormalizedBrowserTraceData | undefined {
110110 const attempts = getTraceAttemptMap ( selection . test . artifacts )
111111 return selection . attemptKey
112- ? attempts [ selection . attemptKey ]
113- : Object . values ( attempts ) [ 0 ]
112+ ? attempts . get ( selection . attemptKey )
113+ : [ ... attempts . values ( ) ] [ 0 ]
114114}
115115
116116export function getTraceEditorMarkersForFile (
@@ -174,50 +174,71 @@ export function getTraceEntryClass(entry: BrowserTraceEntry) {
174174
175175export function openTrace ( trace : BrowserTraceData , test : RunnerTestCase ) {
176176 detailsPosition . value = 'bottom'
177- activeTraceView . value = {
177+ setActiveTrace ( {
178178 test,
179179 attemptKey : getTraceAttemptKey ( trace ) ,
180180 selectedStepIndex : 0 ,
181- }
181+ } )
182+ }
183+
184+ function setActiveTrace ( selection : TraceSelection ) {
185+ activeTraceView . value = selection
186+ selectedTraceAttempt . value = selection . attemptKey ?? null
187+ selectedTraceStep . value = selection . selectedStepIndex
182188}
183189
184190export function closeTrace ( ) {
185191 activeTraceView . value = undefined
192+ selectedTraceAttempt . value = null
193+ selectedTraceStep . value = null
186194}
187195
188196export function selectActiveTraceStep ( index : number ) {
189197 const selection = activeTraceView . value
190198 if ( selection ) {
191199 selection . selectedStepIndex = index
200+ selectedTraceStep . value = index
192201 }
193202}
194203
204+ // Resolve the URL-selected task only when it can be shown in the trace view.
205+ const selectedTestTask = computed ( ( ) => {
206+ const test = selectedTest . value
207+ ? client . state . idMap . get ( selectedTest . value )
208+ : undefined
209+ return test ?. type === 'test' && isTraceViewEnabled ( test . file )
210+ ? test
211+ : undefined
212+ } )
213+
195214// Open/close only on selected-test navigation so the close button can clear the
196215// trace view without being auto-opened again for the same selected test.
216+ // Flush synchronously so traceStep is set before the URL is updated.
217+ // Vueuse URL watcher pauses while writing and would otherwise miss the change.
197218watch ( selectedTest , ( testId ) => {
198219 if ( testId ) {
199- const test = client . state . idMap . get ( testId )
200- if ( test ?. type === 'test' && isTraceViewEnabled ( test . file ) ) {
220+ const test = selectedTestTask . value
221+ if ( test ) {
201222 // Auto-open trace view when selecting a trace-enabled test.
202- activeTraceView . value = { test, selectedStepIndex : 0 }
223+ setActiveTrace ( { test, selectedStepIndex : 0 } )
203224 return
204225 }
205226 }
206227
207228 // Close trace view when navigation moves away from a trace-enabled test.
208229 closeTrace ( )
209- } )
230+ } , { flush : 'sync' } )
210231
211232// Keep the pane attached to the latest test object after reruns, and reset the
212233// attempt selection because retries/repeats belong to one run.
213234watchEffect ( ( ) => {
214235 const active = activeTraceView . value
215236 const testId = selectedTest . value
216237 if ( active && testId && active . test . id === testId ) {
217- const test = client . state . idMap . get ( testId )
218- if ( test ?. type === 'test' && active . test !== test ) {
238+ const test = selectedTestTask . value
239+ if ( test && active . test !== test ) {
219240 // Rerun produced a fresh test object; reset attempt selection.
220- activeTraceView . value = { test, selectedStepIndex : 0 }
241+ setActiveTrace ( { test, selectedStepIndex : 0 } )
221242 }
222243 }
223244} )
@@ -241,3 +262,42 @@ export function getTraceAttemptLabel(trace: BrowserTraceData) {
241262 }
242263 return parts . join ( ' / ' )
243264}
265+
266+ // Restore trace URL state once its selected test becomes available.
267+ initializeTraceView ( )
268+
269+ function initializeTraceView ( ) {
270+ const attemptKey = selectedTraceAttempt . value
271+ const step = selectedTraceStep . value
272+ if ( ! selectedTest . value || ( attemptKey == null && step == null ) ) {
273+ return
274+ }
275+
276+ const restoreTrace = ( ) => {
277+ const test = selectedTestTask . value
278+ if ( ! test ) {
279+ return false
280+ }
281+
282+ const attempts = getTraceAttemptMap ( test . artifacts )
283+ const selectedAttemptKey = attemptKey != null && attempts . has ( attemptKey ) ? attemptKey : undefined
284+ const selectedTrace = selectedAttemptKey ? attempts . get ( selectedAttemptKey ) : [ ...attempts . values ( ) ] [ 0 ]
285+ const selectedStepIndex = parseTraceStep ( step , selectedTrace ?. entries . length ?? 0 )
286+ detailsPosition . value = 'bottom'
287+ setActiveTrace ( {
288+ test,
289+ attemptKey : selectedAttemptKey ,
290+ selectedStepIndex,
291+ } )
292+ return true
293+ }
294+
295+ if ( ! restoreTrace ( ) ) {
296+ watch ( selectedTestTask , restoreTrace , { once : true } )
297+ }
298+ }
299+
300+ function parseTraceStep ( value : unknown , entryCount : number ) : number {
301+ const step = typeof value === 'number' ? value : Number ( value )
302+ return Number . isInteger ( step ) && step >= 0 && step < entryCount ? step : 0
303+ }
0 commit comments