diff --git a/packages/ui/client/components/trace/TraceArtifacts.vue b/packages/ui/client/components/trace/TraceArtifacts.vue index 38dd0adb4..3902a0601 100644 --- a/packages/ui/client/components/trace/TraceArtifacts.vue +++ b/packages/ui/client/components/trace/TraceArtifacts.vue @@ -1,48 +1,24 @@ - - - Trace View - - - - - Open trace viewer - - {{ label }} - - - - + + + Open trace viewer + diff --git a/packages/ui/client/components/trace/TraceViewPane.vue b/packages/ui/client/components/trace/TraceViewPane.vue index 66b85f346..df6e9d2a1 100644 --- a/packages/ui/client/components/trace/TraceViewPane.vue +++ b/packages/ui/client/components/trace/TraceViewPane.vue @@ -2,7 +2,7 @@ import type { TraceSelection } from '~/composables/trace-view' import { computed } from 'vue' import IconButton from '~/components/IconButton.vue' -import { closeTrace, getSelectedTrace, getTraceAttemptLabel, showTraceSelectorHighlight } from '~/composables/trace-view' +import { closeTrace, getSelectedTrace, getTraceAttemptLabel, getTraceAttemptMap, selectActiveTraceAttempt, showTraceSelectorHighlight } from '~/composables/trace-view' import TraceView from './TraceView.vue' const props = defineProps<{ @@ -11,6 +11,14 @@ const props = defineProps<{ const trace = computed(() => getSelectedTrace(props.selection)) const attemptLabel = computed(() => trace.value ? getTraceAttemptLabel(trace.value) : '') +const traceAttempts = computed(() => [...getTraceAttemptMap(props.selection.test.artifacts)].map(([key, trace]) => ({ + key, + label: getTraceAttemptLabel(trace) || 'Initial run', +}))) +const selectedAttemptKey = computed({ + get: () => props.selection.attemptKey ?? '0:0', + set: selectActiveTraceAttempt, +}) @@ -18,9 +26,22 @@ const attemptLabel = computed(() => trace.value ? getTraceAttemptLabel(trace.val Trace Viewer - + + + {{ attempt.label }} + + {{ attemptLabel }} diff --git a/packages/ui/client/composables/trace-view.ts b/packages/ui/client/composables/trace-view.ts index 53ecd489c..8e1875b09 100644 --- a/packages/ui/client/composables/trace-view.ts +++ b/packages/ui/client/composables/trace-view.ts @@ -178,13 +178,11 @@ export function getTraceEntryClass(entry: BrowserTraceEntry) { return 'text-gray-400 dark:text-gray-500' } -export function openTrace(trace: BrowserTraceData, test: RunnerTestCase) { +export function openTrace(test: RunnerTestCase) { detailsPosition.value = 'bottom' - setActiveTrace({ - test, - attemptKey: getTraceAttemptKey(trace), - selectedStepIndex: 0, - }) + if (activeTraceView.value?.test !== test) { + setActiveTrace({ test, selectedStepIndex: 0 }) + } } function setActiveTrace(selection: TraceSelection) { @@ -207,6 +205,17 @@ export function selectActiveTraceStep(index: number) { } } +export function selectActiveTraceAttempt(attemptKey: string) { + const selection = activeTraceView.value + if (selection) { + setActiveTrace({ + ...selection, + attemptKey, + selectedStepIndex: 0, + }) + } +} + // Resolve the URL-selected task only when it can be shown in the trace view. const selectedTestTask = computed(() => { const test = selectedTest.value diff --git a/test/ui/test/trace.spec.ts b/test/ui/test/trace.spec.ts index 4d2128ce2..e0e456222 100644 --- a/test/ui/test/trace.spec.ts +++ b/test/ui/test/trace.spec.ts @@ -250,6 +250,12 @@ async function testBasic(page: Page) { // verify closing trace viewer doesn't immediately auto-open it again await traceView.getByRole('button', { name: 'Close Trace Viewer' }).click() await expect(traceView).toBeHidden() + + // reopen the trace viewer from the report + await page.getByTestId('btn-report').click() + await page.getByTestId('trace-open-button').click() + await expect(traceView).toBeVisible() + await expect(traceFrame.getByRole('button', { name: 'Switch Target' })).toBeVisible() } async function testViewport(page: Page) { @@ -361,25 +367,28 @@ async function testAttempts(page: Page) { const traceView = page.getByTestId('trace-view') const traceFrame = traceView.frameLocator('iframe') + const traceSteps = traceView.getByTestId('trace-step') await expect(traceView).toBeVisible() - - const traceOpenButtons = page.getByTestId('trace-open-button') - await expect(traceOpenButtons).toHaveText([ - 'Open trace viewer', - 'Open trace viewer Retry 1', - 'Open trace viewer Retry 2', + const attemptSelect = traceView.getByRole('combobox', { name: 'Trace attempt' }) + await expect(attemptSelect.locator('option')).toHaveText([ + 'Initial run', + 'Retry 1', + 'Retry 2', ]) - await traceOpenButtons.nth(0).click() + await expect(attemptSelect).toHaveValue('0:0') await expect(traceFrame.getByText('retryCount: 0')).toBeVisible() await expect(traceFrame.getByText('repeatCount: 0')).toBeVisible() - await traceOpenButtons.nth(1).click() + // trace step is reset to first step when switching attempts + await traceSteps.nth(1).click() + await attemptSelect.selectOption('0:1') + await expect(traceSteps.nth(0)).toHaveAttribute('aria-selected', 'true') await expect(traceFrame.getByText('retryCount: 1')).toBeVisible() await expect(traceFrame.getByText('repeatCount: 0')).toBeVisible() - await traceOpenButtons.nth(2).click() + await attemptSelect.selectOption('0:2') await expect(traceFrame.getByText('retryCount: 2')).toBeVisible() await expect(traceFrame.getByText('repeatCount: 0')).toBeVisible() } @@ -483,8 +492,8 @@ async function testPersistsAttemptInURL(page: Page) { const traceView = page.getByTestId('trace-view') const traceFrame = traceView.frameLocator('iframe') - // Opening a retry writes its attempt key to the URL. - await page.getByTestId('trace-open-button').nth(1).click() + // Selecting a retry writes its attempt key to the URL. + await traceView.getByRole('combobox', { name: 'Trace attempt' }).selectOption('0:1') await expect.poll(() => getHashParams(page)).toMatchObject({ traceAttempt: '0:1', traceStep: '0', @@ -499,6 +508,7 @@ async function testPersistsAttemptInURL(page: Page) { traceStep: '0', test: testId, }) + await expect(traceView.getByRole('combobox', { name: 'Trace attempt' })).toHaveValue('0:1') await expect(traceFrame.getByText('retryCount: 1')).toBeVisible() }