diff --git a/application/app/pages/test-run-cases/[id].vue b/application/app/pages/test-run-cases/[id].vue index 924b94d4..a74a7e91 100644 --- a/application/app/pages/test-run-cases/[id].vue +++ b/application/app/pages/test-run-cases/[id].vue @@ -108,8 +108,6 @@ const failureCluster = computed(() => { } | null; }); -const wastedTimeMs = computed(() => testCase.value?.wastedTimeMs ?? 0); - // ── Tabs ──────────────────────────────────────────────────────────────────── // The tab set depends on whether this execution has an error: a failing case // leads with Diagnosis; a passing one with its Steps and Artifacts. @@ -180,15 +178,105 @@ const stepCategoryColor: Record[] = [ - { id: 'status', header: '', size: 32 }, - { accessorKey: 'category', header: 'Category' }, - { accessorKey: 'title', header: 'Step' }, - { accessorKey: 'duration', header: 'Duration' }, + { id: 'index', header: '#', meta: { class: { th: 'w-12', td: 'w-12' } } }, + { id: 'status', header: '', meta: { class: { th: 'w-10', td: 'w-10' } } }, + { accessorKey: 'category', header: 'Category', meta: { class: { th: 'w-28', td: 'w-28' } } }, + { accessorKey: 'title', header: 'Step' }, // no width → absorbs remaining width + { accessorKey: 'duration', header: 'Duration', meta: { class: { th: 'w-44', td: 'w-44' } } }, ]; +// ── Steps tab derived data ─────────────────────────────────────────────────── +// Per-category rollup for the summary strip above the table. Durations are summed +// over the flat step list (parents include their children), matching how the +// reporter's StepMetrics already reports navigation/wait totals. +const stepSummary = computed(() => { + const byCat = new Map(); + for (const s of steps.value) { + const entry = byCat.get(s.category) ?? { count: 0, duration: 0 }; + entry.count += 1; + entry.duration += s.duration || 0; + byCat.set(s.category, entry); + } + return Array.from(byCat, ([category, v]) => ({ category, ...v })).sort((a, b) => b.duration - a.duration); +}); + +// Row index of the single slowest step, used to tag that row. Mirrors the header's +// slowestStep (max flat-step duration) but resolved to a stable row. +const slowestStepIndex = computed(() => { + let idx = -1; + let max = -1; + steps.value.forEach((s, i) => { + if ((s.duration || 0) > max) { + max = s.duration || 0; + idx = i; + } + }); + return idx; +}); + +const maxStepDuration = computed(() => steps.value.reduce((m, s) => Math.max(m, s.duration || 0), 0)); + +// A true waterfall needs a startTime on every step (only runs from a recent +// reporter carry one); otherwise the bars fall back to left-aligned magnitude. +const hasStepTimings = computed( + () => steps.value.length > 0 && steps.value.every((s) => typeof s.startTime === 'number'), +); +const timelineStart = computed( + () => + testCase.value?.startedAt ?? + (hasStepTimings.value ? Math.min(...steps.value.map((s) => s.startTime as number)) : 0), +); +const timelineDuration = computed(() => { + const total = testCase.value?.duration ?? 0; + if (total > 0) return total; + if (hasStepTimings.value) { + const end = Math.max(...steps.value.map((s) => (s.startTime as number) + (s.duration || 0))); + return Math.max(1, end - timelineStart.value); + } + return 0; +}); + +/** Bar geometry for a step: a real waterfall when timings exist, else magnitude. */ +function stepBarStyle(step: PerformanceStep): Record { + if (hasStepTimings.value && timelineDuration.value > 0) { + const left = Math.max( + 0, + Math.min(100, (((step.startTime as number) - timelineStart.value) / timelineDuration.value) * 100), + ); + const width = Math.min(100 - left, Math.max(1.5, ((step.duration || 0) / timelineDuration.value) * 100)); + return { left: `${left}%`, width: `${width}%` }; + } + const width = maxStepDuration.value > 0 ? Math.max(2, ((step.duration || 0) / maxStepDuration.value) * 100) : 0; + return { left: '0%', width: `${width}%` }; +} + +/** Step duration as a share of the whole test's wall-clock (e.g. "12%"). */ +function stepPctOfTest(duration: number): string { + const total = testCase.value?.duration ?? 0; + if (total <= 0) return ''; + const pct = (duration / total) * 100; + if (pct > 0 && pct < 1) return '<1%'; + return `${Math.round(pct)}%`; +} + +/** Severity color for a duration value, shared by the number and its bar. */ +function stepDurationTextClass(duration: number): string { + return duration > 2000 ? 'text-red-600 font-medium' : duration > 500 ? 'text-orange-500' : 'text-gray-500'; +} +function stepBarColorClass(duration: number): string { + return duration > 2000 ? 'bg-red-500' : duration > 500 ? 'bg-orange-400' : 'bg-gray-400 dark:bg-gray-500'; +} + const environment = computed(() => testCase.value?.testRun?.environment); // ── Retry command ───────────────────────────────────────────────────────── @@ -567,27 +655,36 @@ provide(clusterSectionLocatorKey, {