Skip to content

Commit 376dc3b

Browse files
hi-ogawaOpenCode (gpt-5.6-sol)codex
authored
feat(ui): add focused trace view layout mode (#11190)
Co-authored-by: Hiroshi Ogawa <[email protected]> Co-authored-by: OpenCode (gpt-5.6-sol) <[email protected]> Co-authored-by: Codex <[email protected]>
1 parent 41c9f65 commit 376dc3b

5 files changed

Lines changed: 113 additions & 7 deletions

File tree

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

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import type { TraceSelection } from '~/composables/trace-view'
33
import { computed } from 'vue'
44
import IconButton from '~/components/IconButton.vue'
5+
import { layoutMode, params } from '~/composables/params'
56
import { closeTrace, getSelectedTrace, getTraceAttemptLabel, getTraceAttemptMap, selectActiveTraceAttempt, showTraceSelectorHighlight } from '~/composables/trace-view'
7+
import { getNames } from '../../../../vitest/src/utils/tasks.ts'
68
import TraceView from './TraceView.vue'
79
810
const props = defineProps<{
@@ -19,13 +21,34 @@ const selectedAttemptKey = computed({
1921
get: () => props.selection.attemptKey ?? '0:0',
2022
set: selectActiveTraceAttempt,
2123
})
24+
const ancestorNames = computed(() => getNames(props.selection.test).slice(0, -1).join(' > '))
25+
const traceLayoutPageUrl = computed(() => {
26+
const url = new URL(globalThis.location.href)
27+
const focusedParams = new URLSearchParams()
28+
for (const [key, value] of Object.entries(params)) {
29+
if (value != null) {
30+
focusedParams.set(key, String(value))
31+
}
32+
}
33+
focusedParams.set('layout', 'trace')
34+
url.hash = `/?${focusedParams}`
35+
return url.href
36+
})
2237
</script>
2338

2439
<template>
25-
<div data-testid="trace-view" h-full min-h-0 flex="~ col">
26-
<div p="3" h-10 flex="~ gap-2" items-center bg-header border="b base">
40+
<div data-testid="trace-view" class="h-full min-h-0 flex flex-col">
41+
<div class="h-10 flex items-center gap-2 border-b border-base bg-header p-3">
2742
<div class="i-carbon:data-vis-4" />
28-
<span pl-1 font-bold text-sm flex-auto ws-nowrap overflow-hidden truncate>Trace Viewer</span>
43+
<div
44+
v-if="layoutMode === 'trace'"
45+
data-testid="trace-view-title"
46+
class="min-w-0 flex-auto overflow-hidden truncate ws-nowrap pl-1 text-sm"
47+
>
48+
<span class="font-bold">{{ selection.test.name }}</span>
49+
<span v-if="ancestorNames" class="ml-2 op-50">{{ ancestorNames }}</span>
50+
</div>
51+
<span v-else data-testid="trace-view-title" class="flex-auto pl-1 text-sm font-bold">Trace Viewer</span>
2952
<select
3053
v-if="traceAttempts.length > 1"
3154
v-model="selectedAttemptKey"
@@ -53,7 +76,19 @@ const selectedAttemptKey = computed({
5376
>
5477
<span>Show highlight</span>
5578
</label>
79+
<a
80+
v-if="layoutMode !== 'trace'"
81+
v-tooltip.bottom="'Open Trace Viewer in New Tab'"
82+
:href="traceLayoutPageUrl"
83+
target="_blank"
84+
rel="noopener noreferrer"
85+
aria-label="Open Trace Viewer in New Tab"
86+
class="w-1.4em h-1.4em flex op70 rounded hover:bg-active hover:op100"
87+
>
88+
<span class="i-carbon:launch m-auto block" />
89+
</a>
5690
<IconButton
91+
v-if="layoutMode !== 'trace'"
5792
v-tooltip.bottom="'Close Trace Viewer'"
5893
title="Close Trace Viewer"
5994
icon="i-carbon:close"

packages/ui/client/composables/navigation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export function showDashboard(show: boolean) {
116116
}
117117
}
118118

119-
export function navigateTo({ file, line, view, test, column }: Omit<Params, 'traceAttempt' | 'traceStep'>) {
119+
export function navigateTo({ file, line, view, test, column }: Omit<Params, 'layout' | 'traceAttempt' | 'traceStep'>) {
120120
activeFileId.value = file
121121
lineNumber.value = line
122122
columnNumber.value = column

packages/ui/client/composables/params.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { toRef, useUrlSearchParams } from '@vueuse/core'
33
export interface Params {
44
// File.id
55
file: string
6+
// Replaces the standard UI with a full-page trace view
7+
layout: null | 'trace'
68
view: null | 'graph' | 'editor' | 'console'
79
line: null | number
810
// Task.id of either Test or Suite (but not File)
@@ -12,9 +14,10 @@ export interface Params {
1214
traceStep: null | number
1315
}
1416

15-
const params = useUrlSearchParams<Params>('hash', {
17+
export const params = useUrlSearchParams<Params>('hash', {
1618
initialValue: {
1719
file: '',
20+
layout: null,
1821
view: null,
1922
line: null,
2023
test: null,
@@ -25,6 +28,7 @@ const params = useUrlSearchParams<Params>('hash', {
2528
})
2629

2730
export const activeFileId = toRef(params, 'file')
31+
export const layoutMode = toRef(params, 'layout')
2832
export const viewMode = toRef(params, 'view')
2933
export const lineNumber = toRef(params, 'line')
3034
export const columnNumber = toRef(params, 'column')

packages/ui/client/pages/index.vue

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
mainSizes,
2121
panels,
2222
} from '~/composables/navigation'
23+
import { layoutMode } from '~/composables/params'
2324
import { activeTraceView } from '~/composables/trace-view'
2425
2526
const dashboardVisible = initializeNavigation()
@@ -81,8 +82,14 @@ function allowBrowserEvents() {
8182
</script>
8283

8384
<template>
84-
<ProgressBar />
85-
<div h-screen w-screen overflow="hidden">
85+
<ProgressBar v-if="layoutMode !== 'trace'" />
86+
<div v-if="layoutMode === 'trace'" class="h-screen w-screen overflow-hidden">
87+
<TraceViewPane v-if="activeTraceView" :selection="activeTraceView" />
88+
<div v-else class="h-full flex items-center justify-center text-sm op-50">
89+
No trace found
90+
</div>
91+
</div>
92+
<div v-else class="h-screen w-screen overflow-hidden">
8693
<Splitpanes
8794
class="pt-4px"
8895
@resized="onMainResized"

test/ui/test/trace.spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ test.describe('ui', () => {
7373
await testPersistsAttemptInURL(page)
7474
})
7575

76+
test('focused trace mode', async ({ page }) => {
77+
await testFocusedTraceMode(page)
78+
})
79+
7680
test('persists resized trace panes across reloads', async ({ page }) => {
7781
await testPersistsResizedTracePanes(page)
7882
})
@@ -159,6 +163,10 @@ test.describe('html reporter', () => {
159163
await testPersistsAttemptInURL(page)
160164
})
161165

166+
test('focused trace mode', async ({ page }) => {
167+
await testFocusedTraceMode(page)
168+
})
169+
162170
test('persists resized trace panes across reloads', async ({ page }) => {
163171
await testPersistsResizedTracePanes(page)
164172
})
@@ -512,6 +520,58 @@ async function testPersistsAttemptInURL(page: Page) {
512520
await expect(traceFrame.getByText('retryCount: 1')).toBeVisible()
513521
}
514522

523+
async function testFocusedTraceMode(page: Page) {
524+
// Opening the trace layout without a selection shows its empty state.
525+
const standardUrl = page.url()
526+
const emptyTraceUrl = new URL(standardUrl)
527+
emptyTraceUrl.hash = '/?layout=trace'
528+
await page.goto(emptyTraceUrl.href)
529+
await expect(page.getByText('No trace found')).toBeVisible()
530+
531+
// Select a trace step in the standard layout.
532+
await page.goto(standardUrl)
533+
await openExplorerItem(page, 'simple')
534+
const traceView = page.getByTestId('trace-view')
535+
const traceSteps = traceView.getByTestId('trace-step')
536+
const traceFrame = traceView.frameLocator('iframe')
537+
await expect(traceView.getByTestId('trace-view-title')).toHaveText('Trace Viewer')
538+
await traceSteps.nth(1).click()
539+
await expect(traceFrame.getByRole('button', { name: 'Another' })).toBeVisible()
540+
541+
// Follow its new-tab URL in the current page to inspect the focused layout.
542+
const openFocusedTrace = traceView.getByRole('link', { name: 'Open Trace Viewer in New Tab' })
543+
await expect(openFocusedTrace).toHaveAttribute('target', '_blank')
544+
const focusedUrl = await openFocusedTrace.getAttribute('href')
545+
if (!focusedUrl) {
546+
throw new Error('Focused trace URL is unavailable')
547+
}
548+
await page.goto(focusedUrl)
549+
550+
// The focused layout fills the viewport, preserves selection, and hides standard controls.
551+
const viewport = page.viewportSize()
552+
if (!viewport) {
553+
throw new Error('Viewport size is unavailable')
554+
}
555+
await expect(traceView).toBeVisible()
556+
await expect(page.getByAltText('Vitest logo')).toBeHidden()
557+
await expect(traceView.getByTestId('trace-view-title').locator('span')).toHaveText(['simple', 'basic.test.ts'])
558+
await expect(traceView.getByRole('link', { name: 'Open Trace Viewer in New Tab' })).toBeHidden()
559+
await expect(traceView.getByRole('button', { name: 'Close Trace Viewer' })).toBeHidden()
560+
await expect(traceSteps.nth(1)).toHaveAttribute('aria-selected', 'true')
561+
await expect(traceFrame.getByRole('button', { name: 'Another' })).toBeVisible()
562+
await expect.poll(() => traceView.boundingBox()).toEqual({
563+
x: 0,
564+
y: 0,
565+
width: viewport.width,
566+
height: viewport.height,
567+
})
568+
569+
// Reloading restores the selected trace step.
570+
await page.reload()
571+
await expect(traceSteps.nth(1)).toHaveAttribute('aria-selected', 'true')
572+
await expect(traceFrame.getByRole('button', { name: 'Another' })).toBeVisible()
573+
}
574+
515575
function getHashParams(page: Page) {
516576
const hash = new URL(page.url()).hash
517577
return Object.fromEntries(new URLSearchParams(hash.split('?')[1]))

0 commit comments

Comments
 (0)