diff --git a/frontend/src/App.css b/frontend/src/App.css index c72aab078..d2b8becb6 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -327,6 +327,21 @@ font-size: 0.85rem; } +/* Reproducibility digest prefixes read inline in the '·'-separated post-meta line. */ +.analysis-run-digest { + display: inline; +} + +.analysis-run-digest > summary { + display: inline; + cursor: pointer; + list-style: none; +} + +.analysis-run-digest > summary::-webkit-details-marker { + display: none; +} + .visually-hidden { position: absolute; width: 1px; diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 7462abd2c..c5f914b55 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -2658,17 +2658,27 @@ describe("App, authenticated", () => { expect(screen.getByText(/Cutoff 2026-01-12/)).toBeInTheDocument(); expect(screen.getByText(/Requested 2026-01-12/)).toBeInTheDocument(); const digests = screen.getByLabelText("Analysis run reproducibility digests"); - expect(digests).toHaveTextContent("Hover a prefix to read the full digest for verification."); + expect(digests).toHaveTextContent("Open a prefix to read the full digest for verification."); expect(digests).toHaveTextContent("Code abcdef012345"); expect(digests).toHaveTextContent("Config 0123456789ab"); - expect(digests).not.toHaveTextContent("abcdef0123456789deadbeefcafebabe"); - expect(digests).not.toHaveTextContent( + // The disclosures are native
/ -- no tabIndex/role/onClick hack -- + // so they are keyboard- and touch-operable with zero JS, unlike a hover-only title tooltip. + const codeDisclosure = screen.getByText("Code abcdef012345").closest("details"); + const configDisclosure = screen.getByText("Config 0123456789ab").closest("details"); + expect(codeDisclosure).not.toHaveAttribute("open"); + expect(configDisclosure).not.toHaveAttribute("open"); + expect(screen.getByText("Code abcdef012345").tagName).toBe("SUMMARY"); + expect(screen.getByText("Code abcdef012345")).not.toHaveAttribute("tabIndex"); + expect(screen.getByText("Code abcdef012345")).not.toHaveAttribute("role"); + await userEvent.click(screen.getByText("Code abcdef012345")); + expect(codeDisclosure).toHaveAttribute("open"); + expect(codeDisclosure).toHaveTextContent("abcdef0123456789deadbeefcafebabe"); + expect(configDisclosure).not.toHaveAttribute("open"); + await userEvent.click(screen.getByText("Config 0123456789ab")); + expect(configDisclosure).toHaveAttribute("open"); + expect(configDisclosure).toHaveTextContent( "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", ); - expect(screen.getByTitle("abcdef0123456789deadbeefcafebabe")).toHaveTextContent("Code abcdef012345"); - expect( - screen.getByTitle("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"), - ).toHaveTextContent("Config 0123456789ab"); const history = screen.getByRole("list", { name: "Analysis run status history" }); expect(history).toHaveTextContent("Pending 2026-01-12 12:31"); expect(history).toHaveTextContent("Running 2026-01-12 12:32"); @@ -3305,7 +3315,11 @@ describe("App, authenticated", () => { ); const digests = screen.getByLabelText("Analysis run reproducibility digests"); expect(digests).toHaveTextContent("Result aaaaaaaaaaaa"); - expect(screen.getByTitle("aa".repeat(32))).toHaveTextContent("Result aaaaaaaaaaaa"); + const resultDisclosure = screen.getByText("Result aaaaaaaaaaaa").closest("details"); + expect(resultDisclosure).not.toHaveAttribute("open"); + await userEvent.click(screen.getByText("Result aaaaaaaaaaaa")); + expect(resultDisclosure).toHaveAttribute("open"); + expect(resultDisclosure).toHaveTextContent("aa".repeat(32)); const startCall = fetchMock.mock.calls.find((call) => String(call[0]).endsWith("/api/analysis-runs/run-demo-lineage-pending/start"), ); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6fba0dd41..d5b934a4d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2527,7 +2527,7 @@ function analysisRunCorpusHint(run: AnalysisRun): string | null { } } -/** Git-style prefix. The full digest stays on `title` for verification. */ +/** Git-style prefix. The full digest is behind a `
` disclosure for verification. */ const ANALYSIS_RUN_DIGEST_PREFIX_LENGTH = 12; function analysisRunDigestPrefix(digest: string): string { @@ -2606,17 +2606,20 @@ function AnalysisRunReproducibilityDigests({ } return (
-

+

- Hover a prefix to read the full digest for verification.{" "} + Open a prefix to read the full digest for verification.{" "} {parts.map((part, index) => ( {index > 0 ? " · " : null} - {`${part.label} ${analysisRunDigestPrefix(part.digest)}`} +
+ {`${part.label} ${analysisRunDigestPrefix(part.digest)}`} + {part.digest} +
))} -

+
); }