Skip to content

Commit 5dc4b5c

Browse files
hi-ogawacodex
andauthored
feat(ui): move trace attempts selector to viewer header (#11189)
Co-authored-by: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Co-authored-by: Codex <noreply@openai.com>
1 parent b426c19 commit 5dc4b5c

4 files changed

Lines changed: 72 additions & 56 deletions

File tree

Lines changed: 12 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,24 @@
11
<script setup lang="ts">
22
import type { RunnerTestCase } from 'vitest'
33
import { computed } from 'vue'
4-
import { getTraceAttemptLabel, getTraceAttemptMap, openTrace } from '~/composables/trace-view'
4+
import { getTraceAttemptMap, openTrace } from '~/composables/trace-view'
55
66
const props = defineProps<{
77
test: RunnerTestCase
88
}>()
99
10-
const traces = computed(() => {
11-
const traceMap = getTraceAttemptMap(props.test.artifacts)
12-
return [...traceMap.values()].map(trace => ({
13-
trace,
14-
label: getTraceAttemptLabel(trace),
15-
}))
16-
})
10+
const hasTrace = computed(() => getTraceAttemptMap(props.test.artifacts).size > 0)
1711
</script>
1812

1913
<template>
20-
<template v-if="traces.length">
21-
<h1 m-2>
22-
Trace View
23-
</h1>
24-
<div
25-
v-for="{ trace, label } of traces"
26-
:key="`${trace.repeats}:${trace.retry}`"
27-
bg="yellow-500/10"
28-
text="yellow-500 sm"
29-
p="x3 y2"
30-
m-2
31-
rounded
32-
role="note"
33-
>
34-
<button
35-
data-testid="trace-open-button"
36-
type="button"
37-
class="flex items-center gap-2 rounded px-2 py-1 hover:bg-yellow-500/10"
38-
@click="openTrace(trace, test)"
39-
>
40-
<span class="i-carbon:play-outline block" />
41-
Open trace viewer
42-
<span v-if="label" class="text-xs opacity-70">
43-
{{ label }}
44-
</span>
45-
</button>
46-
</div>
47-
</template>
14+
<button
15+
v-if="hasTrace"
16+
data-testid="trace-open-button"
17+
type="button"
18+
class="m-2 flex items-center gap-2 rounded bg-yellow-500/10 px-3 py-2 text-sm text-yellow-500 hover:bg-yellow-500/20"
19+
@click="openTrace(test)"
20+
>
21+
<span class="i-carbon:play-outline block" />
22+
Open trace viewer
23+
</button>
4824
</template>

‎packages/ui/client/components/trace/TraceViewPane.vue‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import type { TraceSelection } from '~/composables/trace-view'
33
import { computed } from 'vue'
44
import IconButton from '~/components/IconButton.vue'
5-
import { closeTrace, getSelectedTrace, getTraceAttemptLabel, showTraceSelectorHighlight } from '~/composables/trace-view'
5+
import { closeTrace, getSelectedTrace, getTraceAttemptLabel, getTraceAttemptMap, selectActiveTraceAttempt, showTraceSelectorHighlight } from '~/composables/trace-view'
66
import TraceView from './TraceView.vue'
77
88
const props = defineProps<{
@@ -11,16 +11,37 @@ const props = defineProps<{
1111
1212
const trace = computed(() => getSelectedTrace(props.selection))
1313
const attemptLabel = computed(() => trace.value ? getTraceAttemptLabel(trace.value) : '')
14+
const traceAttempts = computed(() => [...getTraceAttemptMap(props.selection.test.artifacts)].map(([key, trace]) => ({
15+
key,
16+
label: getTraceAttemptLabel(trace) || 'Initial run',
17+
})))
18+
const selectedAttemptKey = computed({
19+
get: () => props.selection.attemptKey ?? '0:0',
20+
set: selectActiveTraceAttempt,
21+
})
1422
</script>
1523

1624
<template>
1725
<div data-testid="trace-view" h-full min-h-0 flex="~ col">
1826
<div p="3" h-10 flex="~ gap-2" items-center bg-header border="b base">
1927
<div class="i-carbon:data-vis-4" />
2028
<span pl-1 font-bold text-sm flex-auto ws-nowrap overflow-hidden truncate>Trace Viewer</span>
21-
<!-- TODO: pane should own attempt selector here? -->
29+
<select
30+
v-if="traceAttempts.length > 1"
31+
v-model="selectedAttemptKey"
32+
aria-label="Trace attempt"
33+
class="max-w-40 cursor-pointer border border-base rounded bg-base px-2 py-1 text-xs"
34+
>
35+
<option
36+
v-for="attempt in traceAttempts"
37+
:key="attempt.key"
38+
:value="attempt.key"
39+
>
40+
{{ attempt.label }}
41+
</option>
42+
</select>
2243
<span
23-
v-if="attemptLabel"
44+
v-else-if="attemptLabel"
2445
class="text-xs opacity-70"
2546
>
2647
{{ attemptLabel }}

‎packages/ui/client/composables/trace-view.ts‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,11 @@ export function getTraceEntryClass(entry: BrowserTraceEntry) {
178178
return 'text-gray-400 dark:text-gray-500'
179179
}
180180

181-
export function openTrace(trace: BrowserTraceData, test: RunnerTestCase) {
181+
export function openTrace(test: RunnerTestCase) {
182182
detailsPosition.value = 'bottom'
183-
setActiveTrace({
184-
test,
185-
attemptKey: getTraceAttemptKey(trace),
186-
selectedStepIndex: 0,
187-
})
183+
if (activeTraceView.value?.test !== test) {
184+
setActiveTrace({ test, selectedStepIndex: 0 })
185+
}
188186
}
189187

190188
function setActiveTrace(selection: TraceSelection) {
@@ -207,6 +205,17 @@ export function selectActiveTraceStep(index: number) {
207205
}
208206
}
209207

208+
export function selectActiveTraceAttempt(attemptKey: string) {
209+
const selection = activeTraceView.value
210+
if (selection) {
211+
setActiveTrace({
212+
...selection,
213+
attemptKey,
214+
selectedStepIndex: 0,
215+
})
216+
}
217+
}
218+
210219
// Resolve the URL-selected task only when it can be shown in the trace view.
211220
const selectedTestTask = computed(() => {
212221
const test = selectedTest.value

‎test/ui/test/trace.spec.ts‎

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,12 @@ async function testBasic(page: Page) {
250250
// verify closing trace viewer doesn't immediately auto-open it again
251251
await traceView.getByRole('button', { name: 'Close Trace Viewer' }).click()
252252
await expect(traceView).toBeHidden()
253+
254+
// reopen the trace viewer from the report
255+
await page.getByTestId('btn-report').click()
256+
await page.getByTestId('trace-open-button').click()
257+
await expect(traceView).toBeVisible()
258+
await expect(traceFrame.getByRole('button', { name: 'Switch Target' })).toBeVisible()
253259
}
254260

255261
async function testViewport(page: Page) {
@@ -361,25 +367,28 @@ async function testAttempts(page: Page) {
361367

362368
const traceView = page.getByTestId('trace-view')
363369
const traceFrame = traceView.frameLocator('iframe')
370+
const traceSteps = traceView.getByTestId('trace-step')
364371

365372
await expect(traceView).toBeVisible()
366-
367-
const traceOpenButtons = page.getByTestId('trace-open-button')
368-
await expect(traceOpenButtons).toHaveText([
369-
'Open trace viewer',
370-
'Open trace viewer Retry 1',
371-
'Open trace viewer Retry 2',
373+
const attemptSelect = traceView.getByRole('combobox', { name: 'Trace attempt' })
374+
await expect(attemptSelect.locator('option')).toHaveText([
375+
'Initial run',
376+
'Retry 1',
377+
'Retry 2',
372378
])
373379

374-
await traceOpenButtons.nth(0).click()
380+
await expect(attemptSelect).toHaveValue('0:0')
375381
await expect(traceFrame.getByText('retryCount: 0')).toBeVisible()
376382
await expect(traceFrame.getByText('repeatCount: 0')).toBeVisible()
377383

378-
await traceOpenButtons.nth(1).click()
384+
// trace step is reset to first step when switching attempts
385+
await traceSteps.nth(1).click()
386+
await attemptSelect.selectOption('0:1')
387+
await expect(traceSteps.nth(0)).toHaveAttribute('aria-selected', 'true')
379388
await expect(traceFrame.getByText('retryCount: 1')).toBeVisible()
380389
await expect(traceFrame.getByText('repeatCount: 0')).toBeVisible()
381390

382-
await traceOpenButtons.nth(2).click()
391+
await attemptSelect.selectOption('0:2')
383392
await expect(traceFrame.getByText('retryCount: 2')).toBeVisible()
384393
await expect(traceFrame.getByText('repeatCount: 0')).toBeVisible()
385394
}
@@ -483,8 +492,8 @@ async function testPersistsAttemptInURL(page: Page) {
483492
const traceView = page.getByTestId('trace-view')
484493
const traceFrame = traceView.frameLocator('iframe')
485494

486-
// Opening a retry writes its attempt key to the URL.
487-
await page.getByTestId('trace-open-button').nth(1).click()
495+
// Selecting a retry writes its attempt key to the URL.
496+
await traceView.getByRole('combobox', { name: 'Trace attempt' }).selectOption('0:1')
488497
await expect.poll(() => getHashParams(page)).toMatchObject({
489498
traceAttempt: '0:1',
490499
traceStep: '0',
@@ -499,6 +508,7 @@ async function testPersistsAttemptInURL(page: Page) {
499508
traceStep: '0',
500509
test: testId,
501510
})
511+
await expect(traceView.getByRole('combobox', { name: 'Trace attempt' })).toHaveValue('0:1')
502512
await expect(traceFrame.getByText('retryCount: 1')).toBeVisible()
503513
}
504514

0 commit comments

Comments
 (0)