diff --git a/tests/playwright/rendering-invariants.spec.ts b/tests/playwright/rendering-invariants.spec.ts new file mode 100644 index 0000000..17cd47b --- /dev/null +++ b/tests/playwright/rendering-invariants.spec.ts @@ -0,0 +1,261 @@ +import { test, expect } from "@playwright/test"; +import { waitForHtmx } from "./helpers"; + +/** + * Rendering-invariant audit coverage. + * + * Each test in this file pins a dashboard rendering assumption that was + * previously NOT exercised by any Playwright test (audit performed against + * route inventory in serve/mod.rs vs goto/request URLs in tests/playwright). + * + * Categories of assumption: + * 1. Routes never navigated end-to-end (just smoke-tested for status): + * /eu-ai-act, /matrix/cell, /help/docs/{slug}, + * /artifacts/{id}/preview, /artifacts/{id}/graph, /embed/... + * 2. Render-shape contracts that aren't pinned: + * mermaid in artifact `description` field; ego-graph svg-viewer + * wrapping; embed-layout vs page-layout structural difference. + * 3. Variant-scoping limitations: graph_view doesn't accept variant — + * this is currently silent. Pin it so a future intent change is gated. + * 4. Status-code conventions for missing items: artifact_detail returns + * 200 (not 404) for unknown IDs; same for results detail. + * + * If any of these tests starts failing, that's not necessarily a bug — it + * may be an intentional architectural change. But it should be a CONSCIOUS + * change, not a silent regression. + */ + +test.describe("Rendering invariants — uncovered routes", () => { + test("/eu-ai-act renders a real dashboard page (not just 200)", async ({ + page, + }) => { + // Route is currently never navigated by any Playwright test. The handler + // takes no params and may render either the schema-loaded dashboard or + // a "schema not loaded" stub. Both are valid; assert one of them rendered. + const resp = await page.goto("/eu-ai-act"); + expect(resp?.status()).toBe(200); + await expect(page.locator("h2")).toContainText("EU AI Act Compliance"); + // Layout must wrap the content — direct browser GETs go through + // the wrap_full_page middleware. + await expect(page.locator("nav[role='navigation']")).toBeVisible(); + }); + + test("/help/docs/{slug} renders a topic with a back-link to /help/docs", async ({ + page, + }) => { + // The slug index (/help/docs) is tested but no test ever opens an actual + // topic page. `cli` is one of the built-in slugs in rivet-cli/src/docs.rs. + const resp = await page.goto("/help/docs/cli"); + expect(resp?.status()).toBe(200); + // Topic page must offer a way back to the topic list. + await expect(page.locator('a[href="/help/docs"]')).toBeVisible(); + // Topic body is wrapped in a .card. + await expect(page.locator(".card")).toBeVisible(); + // Layout middleware wraps the partial — nav should be present. + await expect(page.locator("nav[role='navigation']")).toBeVisible(); + }); + + test("/artifacts/{id}/preview returns a hover-tooltip fragment", async ({ + page, + }) => { + // The preview endpoint is hit by hx-get hover handlers in artifact lists, + // but no Playwright test navigates it directly. Pin its fragment shape: + // it must render INSIDE the layout when accessed directly (because the + // wrap_full_page middleware wraps non-HTMX GETs), and the inner fragment + // must use the .art-preview class hierarchy. + const resp = await page.goto("/artifacts/REQ-001/preview"); + expect(resp?.status()).toBe(200); + // The art-preview wrapper is the contract used by the hover-tooltip CSS. + const preview = page.locator(".art-preview").first(); + await expect(preview).toBeVisible(); + // The header carries a type badge + the artifact ID. + await expect(preview.locator(".art-preview-header")).toContainText( + "REQ-001", + ); + // Title must be present (REQ-001's title in dogfood data is non-empty). + await expect(preview.locator(".art-preview-title")).toBeVisible(); + }); + + test("/artifacts/{id}/graph renders an ego-graph wrapped in svg-viewer", async ({ + page, + }) => { + // Pins that the per-artifact ego-graph view follows the same + // .svg-viewer + toolbar invariant as /graph and /doc-linkage. + // No existing test exercises this route end-to-end (the + // diagram-viewer.spec.ts list omits it). + const resp = await page.goto("/artifacts/REQ-001/graph"); + expect(resp?.status()).toBe(200); + await waitForHtmx(page); + + const viewer = page.locator("#ego-graph-viewer"); + await expect(viewer).toBeVisible({ timeout: 10_000 }); + await expect(viewer).toHaveClass(/svg-viewer/); + + // Same three controls as the main /graph view. + const toolbar = viewer.locator(".svg-viewer-toolbar"); + await expect(toolbar.locator("button[title='Zoom to fit']")).toBeVisible(); + await expect(toolbar.locator("button[title='Fullscreen']")).toBeVisible(); + await expect( + toolbar.locator("button[title='Open in new window']"), + ).toBeVisible(); + + // Hops control round-trips the request. + await expect(page.locator("#hops")).toBeVisible(); + }); + + test("/matrix/cell returns a link list fragment", async ({ page }) => { + // The matrix cell drill-down (HTMX-loaded into the matrix table) has + // never been navigated end-to-end. We force a direct browser GET; the + // wrap_full_page middleware will wrap the partial in the layout, so we + // assert on the inner