Skip to content

Commit cd58658

Browse files
authored
feat(ui): decompose EEST test names with global raw/decomposed toggle (#229)
## Summary EEST test names like `test_tx_context.py__test_call_frame_context_ops[fork_Amsterdam-benchmark_test-opcode_ORIGIN-benchmark_90M].txt` (and the newer `benchmark/compute/instruction/test_system.py::test_selfdestruct_created[fork_Osaka-blockchain_test_engine_x-value_bearing_False-benchmark-gas-value_10M]` form) now render as readable chips wherever a test name is shown, with a global toggle to fall back to raw. ## What's new - **Parser** (`utils/eestName.ts`) — pure `parseEESTName` lifts well-known fields (file, fn, fork, opcode, gas, optional path) and keeps everything else as `{key,value}` chips or free-form labels. Handles both `__` and `::` separators, optional path prefix, and the rare `]_<N>M.txt` trailing-gas form. Drops noisy markers (`benchmark_test`, `blockchain_test_engine_x`, standalone `benchmark` / `gas`) and run-id-style numeric paths. - **Toggle** in the page header (next to theme) — `decomposed` (default) ↔ `raw`. Persists via `?names=raw` URL search param + localStorage. - **`<TestName>` component** — chip view (file › fn + gas/opcode/fork chips + params/labels), `compact` variant for tight cells, optional `showRawBelow` to render `Test name:` + raw on a muted second line. - **Wirings** — Tests table, run-detail Performance Heatmap (tooltip + modal), Live heatmap (inherits), suite-detail Performance Heatmap (row labels + tooltip), suite-detail Opcode Heatmap (tooltip), comparison table, comparison detail modal. - **Structured search** (`utils/eestNameFilter.ts`, `testNameMatches`) — every filter input now supports `key:value` terms alongside free text. Recognized keys: `opcode`, `fork`, `gas`/`benchmark`, `file`, `fn`/`function`, `path`, `label`. Anything else (e.g. `mem_size:1024`) hits parsed params. Multiple terms are AND. Each input has the same hint placeholder + tooltip cheat-sheet. Wired into TestsTable, both heatmaps, the opcode heatmap, the comparison page filter, and the sticky filter bar; suite-detail TestHeatmap regex mode is preserved. ## Test plan - [x] Toggle in the header flips chips ↔ raw across every test-name display, persists across navigation, and the `?names=raw` URL is shareable. - [x] Chips wrap onto a new line under `<file› <fn>` in the tests table; long single chips (e.g. `return_data_style:ReturnDataStyle.IDENTITY`) wrap inside their pill instead of overflowing the heatmap tooltip. - [ ] Filter `opcode:ORIGIN gas:90M` narrows tests in the tests table, run-detail heatmap, suite-detail heatmap (text mode), opcode heatmap, and comparison pages. - [ ] Run-detail and compare modals show decomposed name + `TEST NAME:` label + raw + copy icon next to the raw line. - [ ] In raw mode, the name column in the tests table no longer overflows neighbouring columns.
1 parent 66096f3 commit cd58658

15 files changed

Lines changed: 620 additions & 82 deletions

ui/src/components/compare/StickyRunBar.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,10 @@ export function StickyRunBar({ runs, sentinelRef, labelMode, onLabelModeChange,
7676
<div className="flex items-center gap-1.5">
7777
<span>Filter:</span>
7878
<FilterInput
79-
placeholder={testFilterRegex ? 'Regex...' : 'Filter...'}
79+
placeholder={testFilterRegex ? 'Regex...' : 'Filter or e.g. opcode:ORIGIN'}
80+
title={testFilterRegex
81+
? 'Regex against the raw test name.'
82+
: 'Free text matches the raw name. Or filter by extracted fields:\nopcode:ORIGIN gas:90M fork:Amsterdam file:tx_context fn:codecopy path:compute label:LOG1\nUnrecognized keys hit params: mem_size:1024 code_size:0\nMultiple terms are AND.'}
8083
value={testFilter}
8184
onValueChange={onTestFilterChange}
8285
className={clsx(

ui/src/components/compare/TestComparisonTable.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Table } from 'lucide-react'
55
import type { SuiteTest, AggregatedStats, BlockLogs, BlockLogEntry } from '@/api/types'
66
import { type StepTypeOption, getAggregatedStats } from '@/pages/RunDetailPage'
77
import { Pagination } from '@/components/shared/Pagination'
8+
import { TestName } from '@/components/shared/TestName'
89
import { type CompareRun, type LabelMode, RUN_SLOTS, formatRunLabel } from './constants'
910

1011
interface TestComparisonTableProps {
@@ -401,8 +402,8 @@ export function TestComparisonTable({ runs, suiteTests, stepFilter, blockLogsPer
401402
<td className="whitespace-nowrap px-3 py-2 text-center text-xs/5 text-gray-400 dark:text-gray-500">
402403
{test.order || '-'}
403404
</td>
404-
<td className="max-w-sm truncate px-4 py-2 text-sm/6 text-gray-900 dark:text-gray-100" title={test.name}>
405-
{test.name}
405+
<td className="max-w-sm truncate px-4 py-2 text-sm/6 text-gray-900 dark:text-gray-100">
406+
<TestName name={test.name} />
406407
</td>
407408
<td className="whitespace-nowrap px-4 py-2 text-right text-xs/5 text-gray-500 dark:text-gray-400">
408409
{test.gasUsed !== undefined ? `${Math.round(test.gasUsed / 1_000_000)}M` : '-'}

ui/src/components/compare/TestDetailModal.tsx

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { useMemo, useState } from 'react'
22
import { useNavigate } from '@tanstack/react-router'
3-
import { Check, Copy, GitCompareArrows, X } from 'lucide-react'
3+
import { GitCompareArrows, X } from 'lucide-react'
44
import clsx from 'clsx'
55
import type { RunResult } from '@/api/types'
66
import { type StepTypeOption, getAggregatedStats } from '@/pages/RunDetailPage'
7+
import { TestName } from '@/components/shared/TestName'
78
import { formatTimestamp } from '@/utils/date'
89
import { type GroupDef } from './groupUtils'
910
import { MAX_COMPARE_RUNS, MIN_COMPARE_RUNS } from './constants'
@@ -146,11 +147,8 @@ export function TestDetailModal({
146147
<h3 className="text-sm/6 font-medium text-gray-900 dark:text-gray-100">
147148
{testOrder !== undefined ? `Test #${testOrder}` : 'Test Detail'}
148149
</h3>
149-
<div className="mt-0.5 flex items-start gap-1.5">
150-
<p className="break-all font-mono text-xs text-gray-500 dark:text-gray-400">
151-
{testName}
152-
</p>
153-
<CopyButton text={testName} />
150+
<div className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
151+
<TestName name={testName} showRawBelow showCopy />
154152
</div>
155153
</div>
156154
<button onClick={onClose} className="shrink-0 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300">
@@ -376,22 +374,3 @@ function StatCell({ label, value, title }: { label: string; value?: string; titl
376374
)
377375
}
378376

379-
function CopyButton({ text }: { text: string }) {
380-
const [copied, setCopied] = useState(false)
381-
382-
return (
383-
<button
384-
type="button"
385-
onClick={(e) => {
386-
e.stopPropagation()
387-
navigator.clipboard.writeText(text)
388-
setCopied(true)
389-
setTimeout(() => setCopied(false), 1500)
390-
}}
391-
className="shrink-0 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
392-
title="Copy test name"
393-
>
394-
{copied ? <Check className="size-3.5 text-green-500" /> : <Copy className="size-3.5" />}
395-
</button>
396-
)
397-
}

ui/src/components/layout/Header.tsx

Lines changed: 97 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { useState, useEffect } from 'react'
22
import { Link, useMatchRoute, useNavigate } from '@tanstack/react-router'
33
import clsx from 'clsx'
4-
import { Sun, Moon, LogIn, LogOut, Shield, User, Menu, X, FileText, Search } from 'lucide-react'
4+
import { Sun, Moon, LogIn, LogOut, Shield, User, Menu, X, FileText, Search, Tags, Code2, Check, Settings } from 'lucide-react'
55
import { useAuth } from '@/hooks/useAuth'
6+
import { useNameDisplayMode, type NameDisplayMode } from '@/hooks/useNameDisplayMode'
67

78
function NavLink({ to, children, onClick }: { to: string; children: React.ReactNode; onClick?: () => void }) {
89
const matchRoute = useMatchRoute()
@@ -24,30 +25,107 @@ function NavLink({ to, children, onClick }: { to: string; children: React.ReactN
2425
)
2526
}
2627

27-
function ThemeSwitcher() {
28-
const [isDark, setIsDark] = useState(() => {
29-
if (typeof window === 'undefined') return false
30-
return document.documentElement.classList.contains('dark')
28+
type Theme = 'light' | 'dark'
29+
30+
const NAME_MODE_OPTIONS: { value: NameDisplayMode; label: string; icon: typeof Tags }[] = [
31+
{ value: 'decomposed', label: 'Decomposed', icon: Tags },
32+
{ value: 'raw', label: 'Raw', icon: Code2 },
33+
]
34+
35+
const THEME_OPTIONS: { value: Theme; label: string; icon: typeof Sun }[] = [
36+
{ value: 'light', label: 'Light', icon: Sun },
37+
{ value: 'dark', label: 'Dark', icon: Moon },
38+
]
39+
40+
function MenuOption<T extends string>({ active, onClick, icon: Icon, label }: {
41+
active: boolean
42+
onClick: () => void
43+
icon: typeof Tags
44+
label: string
45+
value: T
46+
}) {
47+
return (
48+
<button
49+
onClick={onClick}
50+
className={clsx(
51+
'flex w-full items-center gap-2 px-3 py-2 text-left text-sm',
52+
active
53+
? 'bg-gray-50 text-gray-900 dark:bg-gray-700/50 dark:text-gray-100'
54+
: 'text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700/50',
55+
)}
56+
>
57+
<Icon className="size-3.5 shrink-0" />
58+
<span className="flex-1">{label}</span>
59+
{active && <Check className="size-3.5 text-blue-500" />}
60+
</button>
61+
)
62+
}
63+
64+
function SectionHeader({ children }: { children: React.ReactNode }) {
65+
return (
66+
<div className="px-3 pb-1 pt-0.5 text-[10px]/4 font-medium uppercase tracking-wide text-gray-400 dark:text-gray-500">
67+
{children}
68+
</div>
69+
)
70+
}
71+
72+
function SettingsMenu() {
73+
const { mode: nameMode, setMode: setNameMode } = useNameDisplayMode()
74+
const [open, setOpen] = useState(false)
75+
const [theme, setThemeState] = useState<Theme>(() => {
76+
if (typeof window === 'undefined') return 'light'
77+
return document.documentElement.classList.contains('dark') ? 'dark' : 'light'
3178
})
3279

3380
useEffect(() => {
34-
if (isDark) {
81+
if (theme === 'dark') {
3582
document.documentElement.classList.add('dark')
36-
localStorage.setItem('theme', 'dark')
3783
} else {
3884
document.documentElement.classList.remove('dark')
39-
localStorage.setItem('theme', 'light')
4085
}
41-
}, [isDark])
86+
localStorage.setItem('theme', theme)
87+
}, [theme])
4288

4389
return (
44-
<button
45-
onClick={() => setIsDark(!isDark)}
46-
className="rounded-sm p-2 text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200"
47-
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
48-
>
49-
{isDark ? <Sun className="size-5" /> : <Moon className="size-5" />}
50-
</button>
90+
<div className="relative">
91+
<button
92+
onClick={() => setOpen(!open)}
93+
className="rounded-sm p-2 text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200"
94+
title="Settings"
95+
>
96+
<Settings className="size-5" />
97+
</button>
98+
{open && (
99+
<>
100+
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
101+
<div className="absolute right-0 z-50 mt-1 w-44 rounded-sm border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-800">
102+
<SectionHeader>Test names</SectionHeader>
103+
{NAME_MODE_OPTIONS.map((opt) => (
104+
<MenuOption
105+
key={opt.value}
106+
value={opt.value}
107+
label={opt.label}
108+
icon={opt.icon}
109+
active={nameMode === opt.value}
110+
onClick={() => { setNameMode(opt.value); setOpen(false) }}
111+
/>
112+
))}
113+
<div className="my-1 border-t border-gray-200 dark:border-gray-700" />
114+
<SectionHeader>Theme</SectionHeader>
115+
{THEME_OPTIONS.map((opt) => (
116+
<MenuOption
117+
key={opt.value}
118+
value={opt.value}
119+
label={opt.label}
120+
icon={opt.icon}
121+
active={theme === opt.value}
122+
onClick={() => { setThemeState(opt.value); setOpen(false) }}
123+
/>
124+
))}
125+
</div>
126+
</>
127+
)}
128+
</div>
51129
)
52130
}
53131

@@ -252,7 +330,7 @@ export function Header() {
252330
</nav>
253331
<div className="ml-auto hidden items-center gap-2 md:flex">
254332
<AuthControls />
255-
<ThemeSwitcher />
333+
<SettingsMenu />
256334
</div>
257335

258336
{/* Mobile hamburger */}
@@ -274,8 +352,8 @@ export function Header() {
274352
</nav>
275353
<div className="mt-3 border-t border-gray-200 pt-3 dark:border-gray-700">
276354
<AuthControls onNavigate={closeMobile} variant="mobile" />
277-
<div className="mt-2 flex items-center justify-end">
278-
<ThemeSwitcher />
355+
<div className="mt-2 flex items-center justify-end gap-2">
356+
<SettingsMenu />
279357
</div>
280358
</div>
281359
</div>

ui/src/components/run-detail/TestHeatmap.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { Check, Copy, Download } from 'lucide-react'
55
import type { TestEntry, SuiteTest, AggregatedStats, MethodsAggregated, StepResult, PostTestRPCCallConfig } from '@/api/types'
66
import { fetchHead } from '@/api/client'
77
import { Modal } from '@/components/shared/Modal'
8+
import { TestName } from '@/components/shared/TestName'
9+
import { testNameMatches } from '@/utils/eestNameFilter'
810
import { TimeBreakdown } from './TimeBreakdown'
911
import { MGasBreakdown } from './MGasBreakdown'
1012
import { ExecutionsList } from './ExecutionsList'
@@ -310,7 +312,7 @@ function HeatmapCell({
310312
// ran-and-failed tests cleanly.
311313
(!test.notProcessed && statusFilter === 'passed' && !test.hasFail) ||
312314
(!test.notProcessed && statusFilter === 'failed' && test.hasFail)
313-
const matchesSearchQuery = !searchQuery || test.testKey.toLowerCase().includes(searchQuery.toLowerCase())
315+
const matchesSearchQuery = !searchQuery || testNameMatches(test.testKey, searchQuery)
314316
const matchesFilter = matchesStatusFilter && matchesSearchQuery
315317
let baseStyle: React.CSSProperties
316318
if (test.notProcessed) {
@@ -351,6 +353,14 @@ function HeatmapCell({
351353
)
352354
}
353355

356+
function TooltipFilename({ name }: { name: string }) {
357+
return (
358+
<div className="w-64 max-w-[80vw]">
359+
<TestName name={name} />
360+
</div>
361+
)
362+
}
363+
354364
export function TestHeatmap({
355365
tests,
356366
suiteTests,
@@ -905,7 +915,7 @@ export function TestHeatmap({
905915
</>
906916
)}
907917
<div className="text-gray-500 dark:text-gray-400">Based on steps: {stepFilter.join(', ')}</div>
908-
<div className="w-48 break-all text-gray-500 dark:text-gray-400">{tooltip.test.filename}</div>
918+
<TooltipFilename name={tooltip.test.filename} />
909919
{tooltip.test.notProcessed ? (
910920
<div className="text-gray-500 dark:text-gray-400">Test was not run</div>
911921
) : tooltip.test.noData ? (
@@ -934,10 +944,8 @@ export function TestHeatmap({
934944
<div className="flex flex-col gap-6">
935945
<div className="flex flex-col gap-2">
936946
<div>
937-
<div className="text-xs/5 font-medium text-gray-500 dark:text-gray-400">Test Name</div>
938-
<div className="flex items-center gap-2 text-sm/6 text-gray-900 dark:text-gray-100">
939-
<span className="min-w-0 break-all">{selectedTest}</span>
940-
<CopyButton text={selectedTest} />
947+
<div className="text-sm/6 text-gray-900 dark:text-gray-100">
948+
<TestName name={selectedTest} showRawBelow showCopy className="min-w-0" />
941949
</div>
942950
</div>
943951
{entry.dir && (

ui/src/components/run-detail/TestsTable.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type { TestEntry, SuiteTest, AggregatedStats, StepResult } from '@/api/ty
55
import { Badge } from '@/components/shared/Badge'
66
import { Duration } from '@/components/shared/Duration'
77
import { Pagination } from '@/components/shared/Pagination'
8+
import { TestName } from '@/components/shared/TestName'
9+
import { testNameMatches } from '@/utils/eestNameFilter'
810
import { type StepTypeOption, ALL_STEP_TYPES } from '@/pages/RunDetailPage'
911

1012
export type TestSortColumn = 'order' | 'name' | 'genesis' | 'time' | 'mgas' | 'passed' | 'failed'
@@ -176,8 +178,7 @@ export function TestsTable({
176178
let filtered = Object.entries(tests)
177179

178180
if (searchQuery) {
179-
const query = searchQuery.toLowerCase()
180-
filtered = filtered.filter(([name]) => name.toLowerCase().includes(query))
181+
filtered = filtered.filter(([name]) => testNameMatches(name, searchQuery))
181182
}
182183

183184
// Genesis filter
@@ -308,10 +309,11 @@ export function TestsTable({
308309
)}
309310
<input
310311
type="text"
311-
placeholder="Search tests..."
312+
placeholder="Search… or e.g. opcode:ORIGIN gas:90M"
313+
title={`Free text matches anywhere in the raw name. Or filter by extracted fields:\nopcode:ORIGIN gas:90M fork:Amsterdam file:tx_context fn:codecopy path:compute label:LOG1\nUnrecognized keys hit params: mem_size:1024 code_size:0\nMultiple terms are AND.`}
312314
value={searchQuery}
313315
onChange={(e) => handleSearchInput(e.target.value)}
314-
className="w-64 rounded-sm border border-gray-300 bg-white px-3 py-2 text-sm/6 placeholder:text-gray-400 focus:border-blue-500 focus:outline-hidden focus:ring-1 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:placeholder:text-gray-500"
316+
className="w-72 rounded-sm border border-gray-300 bg-white px-3 py-2 text-sm/6 placeholder:text-gray-400 focus:border-blue-500 focus:outline-hidden focus:ring-1 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:placeholder:text-gray-500"
315317
/>
316318
</div>
317319
</div>
@@ -357,9 +359,7 @@ export function TestsTable({
357359
{executionOrder.get(testName) ?? '-'}
358360
</td>
359361
<td className="max-w-md px-4 py-3">
360-
<div className="truncate text-sm/6 font-medium text-gray-900 dark:text-gray-100" title={testName}>
361-
{testName}
362-
</div>
362+
<TestName name={testName} className="text-sm/6 font-medium text-gray-900 dark:text-gray-100" />
363363
{entry.dir && (
364364
<div className="truncate text-xs/5 text-gray-500 dark:text-gray-400" title={entry.dir}>
365365
{entry.dir}

0 commit comments

Comments
 (0)