From 4a7172f5ac0d7ede4b96a732013b4ed6cb96195b Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Sat, 5 Sep 2026 07:46:05 -0500 Subject: [PATCH] feat(runner-e2e): improve matrix report browsing (#12889) ## Thinking Path > - Paperclip is the open source app that people use to manage AI agents for work. > - The runner E2E system verifies agent profiles across supported execution environments. > - Its generated report is the main surface for inspecting those results and their visual evidence. > - Expanded matcher content could change the matrix column widths and make comparisons difficult. > - Screenshots also required extra navigation, and the report did not have current-result search and filters. > - This pull request stabilizes the matrix layout and makes visual evidence directly browsable. > - The benefit is faster inspection of retained test evidence without another paid matrix run. ## Linked Issues or Issue Description **What happened?** The runner E2E report changed matrix column widths when a matcher table expanded. The report also made screenshot comparison and current-result discovery slower than necessary. **Expected behavior** The matrix columns must remain stable. Each retained screenshot must appear as a thumbnail. The gallery must support keyboard navigation and show the relevant execution metadata. The report must support client-side search and filters. **Steps to reproduce** 1. Open a runner E2E matrix report that contains retained screenshots. 2. Expand the matcher details in a matrix cell. 3. Observe the matrix column movement in the old report. **Paperclip version or commit** `8430bd897` **Deployment mode** Generated static runner E2E report. ## What Changed - Keep matrix and matcher table widths stable when details expand. - Show retained screenshot thumbnails in each test card. - Add a full-screen evidence gallery with mouse, keyboard, and swipe navigation. - Show agent, environment, runtime, status, duration, token, and matcher data in the gallery header. - Add client-side search and profile, environment, suite, and status filters below the report section tabs. - Keep the filters in normal document flow while the report tabs remain sticky. - Add report generator assertions for the new layout and controls. - Add Playwright coverage for filtering, stable matcher expansion, and filtered gallery navigation. ## Verification - `pnpm test:e2e:runner:typecheck` - `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts tests/runner-e2e/report.test.ts` - `PAPERCLIP_PLAYWRIGHT_CHANNEL=chrome pnpm exec playwright test --config tests/e2e/playwright.config.ts tests/e2e/runner-e2e-dashboard.spec.ts` - `node --test ./scripts/__tests__/e2e-shard.test.mjs` - `pnpm check:token-gates` - `pnpm -r typecheck` - `pnpm test:run` - `pnpm build` - Regenerated the report from GitHub Actions run `33963318820` without rerunning the matrix. - Verified 121 retained thumbnails across 66 results in a local browser. - Verified that expanded matchers keep matrix widths at 260, 486, and 486 pixels in a 1280-pixel viewport. - Verified search, filters, modal metadata, and arrow-key gallery navigation. ## Risks Low risk. This change only modifies the static runner E2E report generator and its tests. It does not change runner execution or retained evidence data. > This work is focused report polish. It does not duplicate planned core work in `ROADMAP.md`. ## Model Used OpenAI Codex with `gpt-5.6-sol`. Codex desktop managed the context window. The model used reasoning, filesystem tools, code execution, GitHub CLI access, and in-app browser verification. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- tests/e2e/runner-e2e-dashboard.spec.ts | 164 +++++++++++++++++++ tests/runner-e2e/dashboard.ts | 215 ++++++++++++++++++++++--- tests/runner-e2e/report.test.ts | 13 ++ 3 files changed, 371 insertions(+), 21 deletions(-) create mode 100644 tests/e2e/runner-e2e-dashboard.spec.ts diff --git a/tests/e2e/runner-e2e-dashboard.spec.ts b/tests/e2e/runner-e2e-dashboard.spec.ts new file mode 100644 index 0000000000..8a82e833c4 --- /dev/null +++ b/tests/e2e/runner-e2e-dashboard.spec.ts @@ -0,0 +1,164 @@ +import { expect, test } from "@playwright/test"; + +import { runnerExecutionById } from "../runner-e2e/catalog.js"; +import { + renderRunnerE2EDashboard, + type RunnerDashboardEntry, +} from "../runner-e2e/dashboard.js"; +import type { MatrixExecution, RunnerE2EResult } from "../runner-e2e/types.js"; + +const executionIds = [ + "core-compatibility.legacy-codex.local.message-marker", + "core-compatibility.legacy-codex.daytona.message-marker", + "core-compatibility.legacy-claude.local.message-marker", + "local-session-integrity.runner-acpx-codex.local.structured-question-restart-resume", +] as const; + +function resultFor( + execution: MatrixExecution, + status: RunnerE2EResult["status"], +): RunnerDashboardEntry { + const result: RunnerE2EResult = { + schema: "paperclip.runner-e2e.result/v1", + executionId: execution.id, + suiteId: execution.suite.id, + attempt: 1, + status, + profileId: execution.profile.id, + environmentId: execution.environment.id, + caseId: execution.task.id, + provider: execution.profile.provider, + model: execution.profile.model, + runtimeMode: execution.profile.expectedRuntimeMode, + startedAt: "2026-09-05T12:00:00.000Z", + finishedAt: "2026-09-05T12:00:12.000Z", + durationMs: 12_000, + runIds: [`run-${execution.id}`], + usage: { + inputTokens: 1_250, + outputTokens: 75, + cachedInputTokens: 500, + }, + matcherResults: [ + { + matcher: { kind: "message_contains", expected: "PAPERCLIP_E2E_OK" }, + passed: status === "passed", + detail: status === "passed" ? "matched" : "marker missing", + }, + ], + screenshots: [ + { + id: "final-state", + label: "Final visible task state", + file: "final-state.png", + }, + ], + cleanup: "passed", + }; + + return { + result, + valid: status === "passed", + errors: status === "passed" ? [] : ["fixture failure"], + evidenceBaseHref: `evidence/${execution.id}/attempt-1`, + evidenceFiles: ["final-state.png"], + }; +} + +test("filters matrix results and pages through the filtered screenshot gallery", async ({ + page, +}) => { + const catalog = executionIds.map(runnerExecutionById); + const entries = catalog.map((execution, index) => + resultFor(execution, index === 1 ? "failed" : "passed"), + ); + await page.setContent( + renderRunnerE2EDashboard({ + title: "Runner E2E interaction fixture", + generatedAt: "2026-09-05T12:01:00.000Z", + expected: executionIds, + catalog, + entries, + }), + ); + + const filters = page + .locator("[data-report-query]") + .locator("..") + .locator(".."); + const tabs = page.locator(".suite-nav"); + await expect(page.locator("[data-report-result]")).toHaveText( + "4 of 4 tests shown", + ); + await expect(page.locator("[data-gallery-item]")).toHaveCount(4); + expect( + await filters.evaluate((element) => getComputedStyle(element).position), + ).toBe("static"); + expect( + await tabs.evaluate((element) => + element.nextElementSibling?.classList.contains("report-filters"), + ), + ).toBe(true); + + const firstTable = page.locator(".matrix").first(); + const widthsBefore = await firstTable + .locator("thead th") + .evaluateAll((cells) => + cells.map((cell) => cell.getBoundingClientRect().width), + ); + await firstTable.locator("details summary").first().click(); + const widthsAfter = await firstTable + .locator("thead th") + .evaluateAll((cells) => + cells.map((cell) => cell.getBoundingClientRect().width), + ); + expect(widthsAfter).toEqual(widthsBefore); + + await page.locator("[data-report-query]").fill("Legacy Claude"); + await expect(page.locator("[data-report-result]")).toHaveText( + "1 of 4 tests shown", + ); + await expect(page.locator("[data-gallery-open]")).toHaveText( + "View gallery · 1", + ); + + await page.locator("[data-report-reset]").click(); + await expect(page.locator("[data-report-query]")).toBeFocused(); + await page + .locator("select[data-report-profile]") + .selectOption("legacy-codex"); + await expect(page.locator("[data-report-result]")).toHaveText( + "2 of 4 tests shown", + ); + await expect(page.locator("[data-gallery-open]")).toHaveText( + "View gallery · 2", + ); + + await page.locator("[data-gallery-open]").click(); + const dialog = page.locator("[data-gallery-dialog]"); + await expect(dialog).toBeVisible(); + await expect(dialog.locator("[data-gallery-profile]")).toHaveText( + "Legacy Codex", + ); + await expect(dialog.locator("[data-gallery-environment]")).toHaveText( + "Isolated local", + ); + await expect(dialog.locator("[data-gallery-position]")).toHaveText( + "Image 1 of 2", + ); + + await page.keyboard.press("ArrowRight"); + await expect(dialog.locator("[data-gallery-environment]")).toHaveText( + "Daytona sandbox", + ); + await expect(dialog.locator("[data-gallery-status]")).toHaveText("failed"); + await expect(dialog.locator("[data-gallery-position]")).toHaveText( + "Image 2 of 2", + ); + + await page.keyboard.press("Escape"); + await expect(dialog).not.toBeVisible(); + await page.locator("[data-report-reset]").click(); + await page.keyboard.press("/"); + await expect(page.locator("[data-report-query]")).toBeFocused(); +}); diff --git a/tests/runner-e2e/dashboard.ts b/tests/runner-e2e/dashboard.ts index 13a75fe547..9a9910fb56 100644 --- a/tests/runner-e2e/dashboard.ts +++ b/tests/runner-e2e/dashboard.ts @@ -154,6 +154,24 @@ function renderCase( (entry?.valid ? "All invariants passed" : "Not selected"); const screenshots = resolveScreenshots(entry); const billing = entry ? summarizeExecutionBilling(entry.result) : null; + const matcherResults = entry?.result.matcherResults ?? []; + const passedMatchers = matcherResults.filter( + (result) => result.passed, + ).length; + const searchText = [ + execution.id, + execution.task.label, + execution.profile.label, + execution.profile.generation, + execution.profile.provider, + execution.profile.model, + execution.environment.label, + execution.environment.provider, + execution.suite.label, + label, + ] + .join(" ") + .toLowerCase(); const availableFiles = entry?.evidenceFiles ? new Set(entry.evidenceFiles) : null; @@ -197,6 +215,10 @@ function renderCase( data-gallery-environment-provider="${html(execution.environment.provider)}" data-gallery-execution-target="${html(execution.environment.expectedExecutionTarget.kind)}" data-gallery-runtime="${html(entry?.result.runtimeMode ?? execution.profile.expectedRuntimeMode)}" + data-gallery-status="${html(label)}" + data-gallery-duration="${html(entry ? durationLabel(entry.result.durationMs) : "Not run")}" + data-gallery-tokens="${html(billing ? `${tokenLabel(billing.llm.inputTokens)} in · ${tokenLabel(billing.llm.outputTokens)} out` : "Unavailable")}" + data-gallery-matchers="${html(matcherResults.length > 0 ? `${passedMatchers}/${matcherResults.length} matchers passed` : "No matchers recorded")}" aria-label="Open ${html(item.label)} for ${html(execution.id)} in gallery" > ${html(item.label)} for ${html(execution.id)} @@ -212,7 +234,16 @@ function renderCase(
Execution${billing.runtime.estimatedListCostUsd === undefined ? html(billing.runtime.costStatus === "not_metered" ? "Local · not metered" : "Cost unavailable") : `${html(usdLabel(billing.runtime.estimatedListCostUsd))} est.`}${html(durationLabel(billing.runtime.agentRunDurationMs))} agent${billing.runtime.leaseDurationMs === null ? "" : ` · ${html(durationLabel(billing.runtime.leaseDurationMs))} lease`}
` : ""; - return `
+ return `
${html(execution.task.label)} ${html(label)} @@ -501,7 +532,7 @@ function renderSuiteMatrix(input: { `; }) .join(""); - return `
+ return `
${html(profile.label)}${html(profile.generation)}${html(profile.provider)} · ${html(profile.model)}
${columns}`; @@ -530,7 +561,7 @@ function renderSuiteMatrix(input: {

Test suite

${html(suite.label)}

${html(suite.description)}

${summaryHtml}
Configuration matrix${profiles.length} profiles · ${environments.length} environments · ${selected} selected
-
${environmentHeaders}${rows}
Agent profile
+
${environments.map(() => '').join("")}${environmentHeaders}${rows}
Agent profile
`; } @@ -578,6 +609,22 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) { const publicSummaryImageHref = safePublicAssetHref( input.publicSummaryImageHref, ); + const filterProfiles = [ + ...new Map( + input.catalog.map((execution) => [ + execution.profile.id, + execution.profile, + ]), + ).values(), + ]; + const filterEnvironments = [ + ...new Map( + input.catalog.map((execution) => [ + execution.environment.id, + execution.environment, + ]), + ).values(), + ]; return ` @@ -617,6 +664,7 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) { --idle-text: #52585d; --idle-border: #a8aeb2; --radius: 8px; + --navigation-sticky-offset: 58px; --font-sans: "Paperclip Inter", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; } @@ -672,23 +720,32 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) { .gallery-launch, .gallery-control, .gallery-close { border: 1px solid var(--border-strong); border-radius: calc(var(--radius) * .8); background: var(--background); color: var(--foreground); cursor: pointer; font-weight: 600; transition: background 150ms ease, color 150ms ease, border-color 150ms ease; } .gallery-launch { min-width: 138px; padding: 10px 15px; } .gallery-launch:hover, .gallery-control:hover, .gallery-close:hover { border-color: var(--foreground); background: var(--primary); color: var(--primary-foreground); } - .gallery-launch:focus-visible, .gallery-control:focus-visible, .gallery-close:focus-visible, .screenshot-trigger:focus-visible, summary:focus-visible, a:focus-visible { outline: 2px solid #2563eb; outline-offset: 3px; } + .gallery-launch:focus-visible, .gallery-control:focus-visible, .gallery-close:focus-visible, .screenshot-trigger:focus-visible, .report-filters input:focus-visible, .report-filters select:focus-visible, .filter-reset:focus-visible, summary:focus-visible, a:focus-visible { outline: 2px solid #2563eb; outline-offset: 3px; } + .report-filters { display: grid; grid-template-columns: minmax(240px, 1.6fr) repeat(4, minmax(130px, .7fr)) auto; align-items: end; gap: 10px; margin: 0 0 40px; padding: 12px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--card); } + .report-filters label { display: grid; gap: 5px; min-width: 0; } + .report-filters label > span { color: var(--muted-foreground); font-size: 10px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; } + .report-filters input, .report-filters select, .filter-reset { width: 100%; min-height: 38px; padding: 7px 9px; border: 1px solid var(--border-strong); border-radius: calc(var(--radius) * .8); background: var(--background); color: var(--foreground); } + .filter-reset { width: auto; cursor: pointer; font-weight: 600; } + .filter-reset:hover { border-color: var(--foreground); } + .filter-result { grid-column: 1 / -1; margin: 0; color: var(--muted-foreground); font: 10px/1.4 var(--font-mono); } .table-kicker { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 0; border-top: 1px solid var(--border); color: var(--muted-foreground); } .table-kicker strong { color: var(--foreground); font-weight: 600; } .table-kicker span { font: 11px/1.4 var(--font-mono); font-variant-numeric: tabular-nums; } .table-wrap { overflow: visible; } - .matrix { width: 100%; min-width: 1120px; border-collapse: separate; border-spacing: 0; } + .matrix { width: 100%; min-width: 1120px; table-layout: fixed; border-collapse: separate; border-spacing: 0; } + .matrix .profile-column { width: 260px; } + .matrix .environment-column { width: auto; } .matrix th, .matrix td { padding: 16px; border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); vertical-align: top; text-align: left; } .matrix tr:last-child th, .matrix tr:last-child td { border-bottom: 0; } .matrix th:last-child, .matrix td:last-child { border-right: 0; } - .matrix thead th { position: sticky; top: 0; z-index: 4; background: var(--raised); } + .matrix thead th { position: sticky; top: var(--navigation-sticky-offset); z-index: 4; background: var(--raised); } .matrix thead th:first-child { left: 0; z-index: 6; width: 260px; } .matrix thead small, .matrix tbody th small { display: block; margin-top: 4px; color: var(--muted-foreground); font-weight: 400; } .environment-label { font-size: 15px; font-weight: 650; } .mobile-environment-header { display: none; } .profile-cell { position: sticky; left: 0; z-index: 3; width: 260px; background: var(--card); } .profile-cell { display: table-cell; } - .profile-sticky { position: sticky; top: 76px; display: flex; align-items: flex-start; min-width: 0; } + .profile-sticky { position: sticky; top: calc(var(--navigation-sticky-offset) + 76px); display: flex; align-items: flex-start; min-width: 0; } .profile-sticky > .agent-capsule { flex: none; margin: 2px 12px 18px 0; } .profile-copy { display: block; min-width: 0; } .profile-copy strong { font-size: 14px; } @@ -706,7 +763,7 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) { .agent-10 { background: linear-gradient(to bottom, #f2d95f, #4fbcba); } .generation { display: inline-block; margin-left: 7px; padding: 2px 7px; border: 1px solid var(--border); border-radius: 999px; color: var(--muted-foreground); font: 500 10px/1.4 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; } .case-stack { display: grid; gap: 12px; } - .case { min-width: 0; padding: 16px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--card); } + .case { min-width: 0; max-width: 100%; padding: 16px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--card); overflow: hidden; } .case-heading { display: flex; align-items: start; justify-content: space-between; gap: 12px; } .case-heading strong { font-size: 14px; font-weight: 650; } .status, .matcher-state { display: inline-block; flex: none; padding: 3px 8px; border: 1px solid; border-radius: 999px; font: 600 10px/1.4 var(--font-sans); letter-spacing: .05em; text-transform: uppercase; } @@ -732,13 +789,18 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) { .screenshot-caption { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 9px; } .screenshot-caption span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted-foreground); font-size: 10px; } .screenshot-caption span:last-child { font-size: 10px; font-weight: 650; } - .matcher-wrap { overflow-x: auto; margin: 12px 0; border: 1px solid var(--border); border-radius: calc(var(--radius) * .8); } - .matchers { width: 100%; min-width: 680px; border-collapse: collapse; font-size: 11px; } + .case-context { display: block; max-width: 100%; } + .matcher-wrap { width: 100%; max-width: 100%; overflow-x: auto; margin: 12px 0; border: 1px solid var(--border); border-radius: calc(var(--radius) * .8); } + .matchers { width: 100%; min-width: 680px; table-layout: fixed; border-collapse: collapse; font-size: 11px; } + .matchers th:nth-child(1) { width: 72px; } + .matchers th:nth-child(2) { width: 144px; } + .matchers th:nth-child(4) { width: 110px; } .matchers th, .matchers td { padding: 8px; border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); vertical-align: top; } .matchers tr:last-child td { border-bottom: 0; } .matchers th:last-child, .matchers td:last-child { border-right: 0; } .matchers th { background: var(--raised); color: var(--muted-foreground); font-weight: 600; } .matchers code { display: inline; color: var(--foreground); } + .matchers td { overflow-wrap: anywhere; } .usage { margin: 12px 0; color: var(--muted-foreground); } .billing-strip { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 12px 0 10px; border: 1px solid var(--border); border-radius: calc(var(--radius) * .8); background: var(--raised); } .billing-strip > div { min-width: 0; padding: 9px 10px; border-right: 1px solid var(--border); } @@ -753,10 +815,10 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) { .billing-metric strong { display: block; overflow-wrap: anywhere; font: 550 17px/1.25 var(--font-mono); font-variant-numeric: tabular-nums; } .billing-metric span { display: block; margin-top: 4px; color: var(--muted-foreground); font-size: 10px; letter-spacing: .06em; text-transform: uppercase; } .billing-note { grid-column: 1 / -1; margin: 0; padding: 11px 16px; border-top: 1px solid var(--border); color: var(--muted-foreground); font-size: 11px; } - .suite-nav { position: sticky; top: 0; z-index: 12; display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 40px; padding: 10px 0; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); background: var(--background); } + .suite-nav { position: sticky; top: 0; z-index: 12; display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 24px; padding: 10px 0; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); background: var(--background); } .suite-nav a { padding: 7px 10px; border-radius: calc(var(--radius) * .8); color: var(--muted-foreground); font-size: 12px; font-weight: 600; text-decoration: none; } .suite-nav a:hover { background: var(--raised); color: var(--foreground); } - .suite-section, .history-section { scroll-margin-top: 72px; margin-top: 64px; } + .suite-section, .history-section { scroll-margin-top: calc(var(--navigation-sticky-offset) + 16px); margin-top: 64px; } .section-heading { display: grid; grid-template-columns: minmax(0, 1fr) minmax(260px, 520px); align-items: end; gap: 32px; margin-bottom: 24px; } .section-heading h2 { margin: 0; font-size: clamp(25px, 3vw, 38px); line-height: 1.05; letter-spacing: -.035em; } .section-heading > p { margin: 0; color: var(--muted-foreground); } @@ -816,6 +878,13 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) { .gallery-context span { display: inline-flex; align-items: baseline; gap: 6px; min-width: 0; } .gallery-context strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; } .gallery-context em { color: #b8b8b5; font: 10px/1.4 var(--font-mono); font-style: normal; white-space: nowrap; } + .gallery-facts { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 18px; margin-top: 8px; } + .gallery-facts > span { display: inline-flex; align-items: baseline; gap: 6px; min-width: 0; } + .gallery-facts em { color: #b8b8b5; font: 10px/1.4 var(--font-mono); font-style: normal; text-transform: uppercase; } + .gallery-facts strong { font: 600 11px/1.4 var(--font-mono); } + .gallery-status { padding: 3px 8px; border: 1px solid rgb(255 255 255 / 36%); border-radius: 999px; font: 650 10px/1.4 var(--font-sans); letter-spacing: .05em; text-transform: uppercase; } + .gallery-status[data-status="passed"] { border-color: var(--pass-border); background: var(--pass-bg); color: #6ee79a; } + .gallery-status[data-status="failed"] { border-color: var(--fail-border); background: var(--fail-bg); color: #ff8181; } .gallery-execution { margin-top: 3px; color: #b8b8b5; font: 10px/1.4 var(--font-mono); } .gallery-close, .gallery-control { min-height: 38px; padding: 8px 12px; border-color: rgb(255 255 255 / 32%); background: transparent; color: #fafafa; } .gallery-stage { position: relative; display: grid; place-items: center; min-height: 0; padding: 24px 88px; overflow: hidden; touch-action: pan-y; } @@ -853,6 +922,10 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) { .section-heading { grid-template-columns: 1fr; gap: 12px; } .trend-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .history-filters { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .report-filters { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .report-search, .filter-result { grid-column: 1 / -1; } + .suite-nav { top: 0; } + .suite-section, .history-section { scroll-margin-top: 72px; } } @media (max-width: 640px) { .brand-bar { min-height: 56px; padding: 0 16px; } @@ -875,6 +948,8 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) { .suite-nav { top: 0; overflow-x: auto; flex-wrap: nowrap; } .suite-nav a { white-space: nowrap; } .history-pointers, .trend-grid, .history-filters { grid-template-columns: 1fr; } + .report-filters { grid-template-columns: 1fr; } + .report-search, .filter-result { grid-column: auto; } .suite-summary { grid-template-columns: 1fr; } .suite-summary > div, .suite-summary > div:nth-child(even) { border-right: 0; border-bottom: 1px solid var(--border); } .suite-summary > div:last-child { border-bottom: 0; } @@ -906,7 +981,7 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) {

Full-stack acceptance campaign

${html(input.title)}

-

A browser-verified matrix of runner profiles, execution environments, and deterministic task contracts. Provider-produced visual evidence remains in the access-controlled workflow artifact; public history contains inert structured evidence and a trusted synthetic campaign summary.

+

A browser-verified matrix of runner profiles, execution environments, and deterministic task contracts. Visual evidence is retained in the access-controlled workflow artifact; public history contains inert structured evidence and a trusted synthetic campaign summary.

@@ -934,6 +1009,15 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) { ${suites.map((suite) => `${html(suite.label)}`).join("")} History +
+ + + + + + +

+
${suiteSections} ${historySection}
Generated ${html(input.generatedAt)}${input.catalog.length} catalog executions · Public history excludes provider-produced visual evidence
@@ -948,6 +1032,12 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) {
+
@@ -976,13 +1066,24 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) { const environmentDetail = dialog.querySelector("[data-gallery-environment-detail]"); const caseLabel = dialog.querySelector("[data-gallery-case]"); const runtime = dialog.querySelector("[data-gallery-runtime]"); + const status = dialog.querySelector("[data-gallery-status]"); + const duration = dialog.querySelector("[data-gallery-duration]"); + const tokens = dialog.querySelector("[data-gallery-tokens]"); + const matchers = dialog.querySelector("[data-gallery-matchers]"); const position = dialog.querySelector("[data-gallery-position]"); const stage = dialog.querySelector("[data-gallery-stage]"); - let activeIndex = 0; + let activeItem = items[0]; let pointerStartX = null; const render = () => { - const item = items[activeIndex]; + const visibleItems = items.filter((item) => !item.closest("[data-report-case]").hidden); + if (visibleItems.length === 0) return; + let activeIndex = visibleItems.indexOf(activeItem); + if (activeIndex < 0) { + activeIndex = 0; + activeItem = visibleItems[0]; + } + const item = activeItem; image.src = item.dataset.galleryHref; image.alt = item.dataset.galleryLabel + " for " + item.dataset.galleryExecution; title.textContent = item.dataset.galleryLabel; @@ -993,14 +1094,23 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) { environmentDetail.textContent = item.dataset.galleryEnvironmentProvider + " · " + item.dataset.galleryExecutionTarget; caseLabel.textContent = item.dataset.galleryCase; runtime.textContent = item.dataset.galleryRuntime + " runtime"; - position.textContent = "Image " + (activeIndex + 1) + " of " + items.length; + status.textContent = item.dataset.galleryStatus; + status.dataset.status = item.dataset.galleryStatus; + duration.textContent = item.dataset.galleryDuration; + tokens.textContent = item.dataset.galleryTokens; + matchers.textContent = item.dataset.galleryMatchers; + position.textContent = "Image " + (activeIndex + 1) + " of " + visibleItems.length; }; const move = (amount) => { - activeIndex = (activeIndex + amount + items.length) % items.length; + const visibleItems = items.filter((item) => !item.closest("[data-report-case]").hidden); + if (visibleItems.length === 0) return; + const activeIndex = Math.max(0, visibleItems.indexOf(activeItem)); + activeItem = visibleItems[(activeIndex + amount + visibleItems.length) % visibleItems.length]; render(); }; - const open = (index) => { - activeIndex = index; + const open = (item) => { + if (!item) return; + activeItem = item; render(); if (typeof dialog.showModal === "function") dialog.showModal(); else dialog.setAttribute("open", ""); @@ -1010,8 +1120,8 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) { else dialog.removeAttribute("open"); }; - items.forEach((item, index) => item.addEventListener("click", () => open(index))); - document.querySelectorAll("[data-gallery-open]").forEach((button) => button.addEventListener("click", () => open(0))); + items.forEach((item) => item.addEventListener("click", () => open(item))); + document.querySelectorAll("[data-gallery-open]").forEach((button) => button.addEventListener("click", () => open(items.find((item) => !item.closest("[data-report-case]").hidden)))); dialog.querySelector("[data-gallery-previous]").addEventListener("click", () => move(-1)); dialog.querySelector("[data-gallery-next]").addEventListener("click", () => move(1)); dialog.querySelector("[data-gallery-close]").addEventListener("click", close); @@ -1031,6 +1141,69 @@ export function renderRunnerE2EDashboard(input: RunnerDashboardInput) { stage.addEventListener("pointercancel", () => { pointerStartX = null; }); })(); +