From 217e37a2af51ffc24d28f8f42363d27b07c37b0a Mon Sep 17 00:00:00 2001 From: Pablo Cabrera Date: Wed, 24 Jun 2026 12:05:20 +0200 Subject: [PATCH 1/5] test(e2e): add 8 behavioral landing page tests (Phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements Phase 1 of docs/explorer-e2e-test-plan.md. The prerequisite F1 (wire useBootstrapWorkspace) is already done in ShellBootstrap.tsx:38-45. Tests cover the 8 scenarios from the plan: - P1.1 Graph landing renders after workspace bootstrap - P1.2 Landing shows root nodes in cytoscape canvas - P1.3 Landing shows suggested questions strip - P1.4 Landing canvas is interactive (pan/zoom) - P1.5 Click root node → pane-stack opens - P1.6 Landing header: workspace name + symbol count - P1.7 Landing error state when fetch fails - P1.8 Landing loading state during fetch Tests rely on MSW handlers (VITE_USE_MOCKS=true). No real backend needed. Tag: v0.12.10 PATCH Next: F2 (Perspective toggle, 6 tests), F3 (Pane-Stack, 8 tests), F4 (Spotter, 5 tests) per the plan. --- apps/explorer-ui/e2e/landing.spec.ts | 154 +++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 apps/explorer-ui/e2e/landing.spec.ts diff --git a/apps/explorer-ui/e2e/landing.spec.ts b/apps/explorer-ui/e2e/landing.spec.ts new file mode 100644 index 00000000..4d35aaf4 --- /dev/null +++ b/apps/explorer-ui/e2e/landing.spec.ts @@ -0,0 +1,154 @@ +/** + * E2E landing page tests — Phase 1 of the explorer-e2e-test-plan. + * + * Verifies the GraphLanding component renders as the first screen when + * the user opens the Explorer without an explicit workspace selection + * (F1 prerequisite: ShellBootstrap auto-selects the first workspace + * from the MSW-mocked `/api/workspaces` endpoint). + * + * All tests rely on MSW handlers (VITE_USE_MOCKS=true; see + * playwright.config.ts). No real axum backend is needed. + * + * Phase 1 scenarios (8 tests) from docs/explorer-e2e-test-plan.md: + * P1.1 Graph landing renders after workspace bootstrap + * P1.2 Landing shows root nodes in cytoscape canvas + * P1.3 Landing shows suggested questions strip + * P1.4 Landing canvas is interactive (pan/zoom) + * P1.5 Click root node → pane-stack opens + * P1.6 Landing header: workspace name + symbol count + * P1.7 Landing error state when fetch fails + * P1.8 Landing loading state during fetch + */ +import { test, expect } from "@playwright/test"; + +test.describe("Phase 1: Landing page (8 tests)", () => { + test("P1.1 Graph landing renders after workspace bootstrap", async ({ page }) => { + await page.goto("/"); + + // Shell mounts + await expect(page.getByTestId("shell")).toBeVisible({ timeout: 10_000 }); + + // GraphLanding renders (not the InteractiveGraphPanel — that needs a rootId) + await expect(page.getByTestId("graph-landing")).toBeVisible({ timeout: 10_000 }); + + // Loading state clears once data arrives + await expect(page.getByTestId("graph-landing-loading")).toBeHidden({ + timeout: 10_000, + }); + }); + + test("P1.2 Landing shows root nodes in cytoscape canvas", async ({ page }) => { + await page.goto("/"); + + // Wait for the landing canvas to appear (cytoscape mounts lazily) + const canvas = page.getByTestId("graph-landing-canvas"); + await expect(canvas).toBeVisible({ timeout: 15_000 }); + + // Cytoscape renders nodes with data-testid="graph-node-" + // The MSW landing mock returns several root nodes + const nodes = page.locator("[data-testid^='graph-node-']"); + await expect(nodes.first()).toBeVisible({ timeout: 10_000 }); + const count = await nodes.count(); + expect(count).toBeGreaterThan(0); + }); + + test("P1.3 Landing shows suggested questions strip", async ({ page }) => { + await page.goto("/"); + + // The suggestion strip renders below the canvas + const strip = page.getByTestId("landing-suggestion-strip"); + await expect(strip).toBeVisible({ timeout: 10_000 }); + + // At least one suggestion is visible + const suggestions = page.locator("[data-testid^='suggested-question-']"); + await expect(suggestions.first()).toBeVisible({ timeout: 5_000 }); + }); + + test("P1.4 Landing canvas is interactive (pan/zoom)", async ({ page }) => { + await page.goto("/"); + + const canvas = page.getByTestId("graph-landing-canvas"); + await expect(canvas).toBeVisible({ timeout: 15_000 }); + + // Cytoscape container has tabindex=0 for keyboard interaction + const tabIndex = await canvas.getAttribute("tabindex"); + expect(tabIndex).toBe("0"); + + // role=application is set for ARIA compatibility + const role = await canvas.getAttribute("role"); + expect(role).toBe("application"); + }); + + test("P1.5 Click root node → pane-stack opens", async ({ page }) => { + await page.goto("/"); + + // Wait for landing to render + await expect(page.getByTestId("graph-landing")).toBeVisible({ timeout: 10_000 }); + + // Wait for at least one node + const nodes = page.locator("[data-testid^='graph-node-']"); + await expect(nodes.first()).toBeVisible({ timeout: 15_000 }); + + // Click the first node — should select it + await nodes.first().click(); + + // Pane-stack renders the inspector + await expect(page.getByTestId("object-inspector")).toBeVisible({ + timeout: 10_000, + }); + }); + + test("P1.6 Landing header: workspace name + symbol count", async ({ page }) => { + await page.goto("/"); + + // Header is visible + const header = page.getByTestId("landing-header"); + await expect(header).toBeVisible({ timeout: 10_000 }); + + // Workspace name is shown (derived from root_path basename in LandingHeader) + const name = page.getByTestId("landing-workspace-name"); + await expect(name).toBeVisible(); + // workspaceSummaryFixture.root_path = "/var/.../CogniCode" → basename = "CogniCode" + await expect(name).toContainText("CogniCode"); + + // Graph status indicator is present + const status = page.getByTestId("landing-graph-status"); + await expect(status).toBeVisible(); + }); + + test("P1.7 Landing error state when fetch fails", async ({ page }) => { + // Override the landing endpoint to return 500 + await page.route("**/api/workspaces/*/landing**", async (route) => { + await route.fulfill({ + status: 500, + contentType: "application/json", + body: JSON.stringify({ error: "Internal server error" }), + }); + }); + + await page.goto("/"); + + // Error state replaces the canvas + const error = page.getByTestId("graph-landing-error"); + await expect(error).toBeVisible({ timeout: 15_000 }); + }); + + test("P1.8 Landing loading state during fetch", async ({ page }) => { + // Add a deliberate delay to the landing endpoint so the loading + // state is observable. + await page.route("**/api/workspaces/*/landing**", async (route) => { + await new Promise((resolve) => setTimeout(resolve, 2000)); + await route.continue(); + }); + + await page.goto("/"); + + // Loading state is visible immediately (before the 2s delay completes) + const loading = page.getByTestId("graph-landing-loading"); + await expect(loading).toBeVisible({ timeout: 5_000 }); + + // Eventually resolves to the actual landing + await expect(loading).toBeHidden({ timeout: 10_000 }); + await expect(page.getByTestId("graph-landing")).toBeVisible(); + }); +}); From 8d8c9a302a8fc432cca919a614d25ccb5730083a Mon Sep 17 00:00:00 2001 From: Haizea Date: Wed, 24 Jun 2026 12:07:58 +0200 Subject: [PATCH 2/5] test(e2e): add 6 behavioral perspective toggle tests (Phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements Phase 2 of docs/explorer-e2e-test-plan.md. Tests cover the 6 scenarios from the plan: - P2.1 Toggle Graph → C4 perspective - P2.2 C4 shows component architecture nodes - P2.3 C4 shows correct node styles (component/container/system) - P2.4 Toggle back C4 → Graph restores data - P2.5 Repeated toggling doesn't duplicate nodes - P2.6 Toggle keyboard accessible (Tab+Enter/Space) Tests rely on MSW handlers (VITE_USE_MOCKS=true). Architecture data comes from /api/workspaces/:id/architecture endpoint. These complement the existing exploration.spec.ts (which focuses on visual regression) — the new tests assert BEHAVIOR (aria-pressed, node counts, keyboard activation, etc.). --- .../e2e/perspective-toggle.spec.ts | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 apps/explorer-ui/e2e/perspective-toggle.spec.ts diff --git a/apps/explorer-ui/e2e/perspective-toggle.spec.ts b/apps/explorer-ui/e2e/perspective-toggle.spec.ts new file mode 100644 index 00000000..4b3cbeb2 --- /dev/null +++ b/apps/explorer-ui/e2e/perspective-toggle.spec.ts @@ -0,0 +1,174 @@ +/** + * E2E perspective toggle tests — Phase 2 of the explorer-e2e-test-plan. + * + * Verifies the Graph ↔ C4 perspective toggle behavior end-to-end. + * + * All tests rely on MSW handlers (VITE_USE_MOCKS=true). + * The toggle component lives at apps/explorer-ui/src/components/PerspectiveToggle.tsx + * and dispatches SET_PERSPECTIVE which is handled by the perspective slice. + * + * Phase 2 scenarios (6 tests) from docs/explorer-e2e-test-plan.md: + * P2.1 Toggle Graph → C4 perspective + * P2.2 C4 shows component architecture nodes + * P2.3 C4 shows correct node styles (component/container/system) + * P2.4 Toggle back C4 → Graph restores data + * P2.5 Repeated toggling doesn't duplicate nodes + * P2.6 Toggle keyboard accessible (Tab+Enter/Space) + */ +import { test, expect } from "@playwright/test"; + +test.describe("Phase 2: Perspective toggle (6 tests)", () => { + test("P2.1 Toggle Graph → C4 perspective", async ({ page }) => { + await page.goto("/"); + + // Wait for the toggle to appear (it's in the shell header) + const toggle = page.getByTestId("perspective-toggle"); + await expect(toggle).toBeVisible({ timeout: 10_000 }); + + const graphBtn = toggle.getByTestId("perspective-graph"); + const c4Btn = toggle.getByTestId("perspective-c4"); + + // Initially Graph is pressed + await expect(graphBtn).toHaveAttribute("aria-pressed", "true"); + await expect(c4Btn).toHaveAttribute("aria-pressed", "false"); + + // Click C4 + await c4Btn.click(); + + // C4 is now pressed, Graph is not + await expect(c4Btn).toHaveAttribute("aria-pressed", "true"); + await expect(graphBtn).toHaveAttribute("aria-pressed", "false"); + }); + + test("P2.2 C4 shows component architecture nodes", async ({ page }) => { + await page.goto("/"); + + // Wait for landing to render (graph perspective first) + await expect(page.getByTestId("graph-landing")).toBeVisible({ timeout: 10_000 }); + + // Switch to C4 perspective + const c4Btn = page.getByTestId("perspective-toggle").getByTestId("perspective-c4"); + await c4Btn.click(); + + // C4 data arrives via /api/workspaces/:id/architecture + // Architecture nodes render in the cytoscape canvas with the same + // graph-node- testid pattern as the graph perspective + const nodes = page.locator("[data-testid^='graph-node-']"); + await expect(nodes.first()).toBeVisible({ timeout: 15_000 }); + + // The MSW architecture fixture returns at least one node + const count = await nodes.count(); + expect(count).toBeGreaterThan(0); + }); + + test("P2.3 C4 shows correct node styles (component/container/system)", async ({ page }) => { + await page.goto("/"); + + // Switch to C4 perspective + await page.getByTestId("perspective-toggle").getByTestId("perspective-c4").click(); + + // Wait for the C4 canvas + const canvas = page.getByTestId("graph-landing-canvas"); + await expect(canvas).toBeVisible({ timeout: 15_000 }); + + // C4 nodes carry cytoscape style classes for their kind + // The architecture fixture returns nodes with kinds: system, container, component + // These map to style classes: node-system, node-container, node-component + const componentNodes = page.locator(".node-component"); + const containerNodes = page.locator(".node-container"); + const systemNodes = page.locator(".node-system"); + + // At least one C4-styled node should be visible + const componentCount = await componentNodes.count(); + const containerCount = await containerNodes.count(); + const systemCount = await systemNodes.count(); + const total = componentCount + containerCount + systemCount; + + expect(total).toBeGreaterThan(0); + }); + + test("P2.4 Toggle back C4 → Graph restores data", async ({ page }) => { + await page.goto("/"); + + // Wait for graph landing + await expect(page.getByTestId("graph-landing")).toBeVisible({ timeout: 10_000 }); + + // Toggle to C4 + const toggle = page.getByTestId("perspective-toggle"); + const c4Btn = toggle.getByTestId("perspective-c4"); + const graphBtn = toggle.getByTestId("perspective-graph"); + await c4Btn.click(); + await expect(c4Btn).toHaveAttribute("aria-pressed", "true"); + + // Wait for C4 nodes to render + const c4Nodes = page.locator("[data-testid^='graph-node-']"); + await expect(c4Nodes.first()).toBeVisible({ timeout: 15_000 }); + const c4Count = await c4Nodes.count(); + expect(c4Count).toBeGreaterThan(0); + + // Toggle back to Graph + await graphBtn.click(); + await expect(graphBtn).toHaveAttribute("aria-pressed", "true"); + await expect(c4Btn).toHaveAttribute("aria-pressed", "false"); + + // Graph nodes still render (stale-data hold + new fetch) + const graphNodes = page.locator("[data-testid^='graph-node-']"); + await expect(graphNodes.first()).toBeVisible({ timeout: 10_000 }); + }); + + test("P2.5 Repeated toggling doesn't duplicate nodes", async ({ page }) => { + await page.goto("/"); + + // Wait for initial graph landing + await expect(page.getByTestId("graph-landing")).toBeVisible({ timeout: 10_000 }); + + const toggle = page.getByTestId("perspective-toggle"); + const c4Btn = toggle.getByTestId("perspective-c4"); + const graphBtn = toggle.getByTestId("perspective-graph"); + + // Capture initial graph node count + const graphNodes = page.locator("[data-testid^='graph-node-']"); + await expect(graphNodes.first()).toBeVisible({ timeout: 15_000 }); + const initialCount = await graphNodes.count(); + expect(initialCount).toBeGreaterThan(0); + + // Toggle Graph → C4 → Graph → C4 → Graph (3 round trips) + for (let i = 0; i < 3; i++) { + await c4Btn.click(); + await expect(c4Btn).toHaveAttribute("aria-pressed", "true"); + await graphBtn.click(); + await expect(graphBtn).toHaveAttribute("aria-pressed", "true"); + } + + // Final state: Graph perspective, same node count as initial + const finalCount = await graphNodes.count(); + expect(finalCount).toBe(initialCount); + }); + + test("P2.6 Toggle keyboard accessible (Tab+Enter/Space)", async ({ page }) => { + await page.goto("/"); + + // Wait for the shell + await expect(page.getByTestId("shell")).toBeVisible({ timeout: 10_000 }); + + // The toggle buttons are real