/** * Happy-path E2E tests — multi-turn investigation and genuine completion. * * What these tests validate: * - UI layout transitions from idle → loading → success * - History renders with correct turn count after each update * - Scenario selector auto-fills the central statement * - Multi-turn flow produces new questions (question change observable) * - Genuine completion state renders CompletionCard with "Investigation complete" * * What these tests do NOT validate: * - Reasoning correctness, candidate selection quality, confidence accuracy, etc. */ import { test, expect } from "@playwright/test"; import { INVESTIGATION_SCENARIOS, SCENARIO_LOOKUP } from "./fixtures/investigation-scenarios.js"; /* ── Scenario fixtures for this file ─────────────────────────── */ const MULTI_TURN = SCENARIO_LOOKUP["Happy path — multi-turn comparison"]; const COMPLETE = SCENARIO_LOOKUP["Happy path — complete investigation"]; /* ── Test helpers (inline to keep test files self-contained) ── */ /** * Get the current "turn count" displayed in history indicators. * Each turn shows an indicator pill — we count them. */ async function getHistoryTurnCount(page) { // History uses data-testid="investigation-history" wrapping turn indicators const indicators = page.locator('[data-testid="investigation-turn"]'); return await indicators.count(); } /** * Wait for the workspace to enter a stable post-investigation state. * In dev mode with instant mock the loading overlay may flash before React * batches render, so we do NOT require it to be visible — just absent. */ async function waitForWorkspaceReady(page) { // The workspace renders with data-testid="reasoning-workspace" const workspace = page.locator('[data-testid="reasoning-workspace"]'); await expect(workspace).toBeVisible({ timeout: 15_000 }); } /** * Wait for a loading state (overlay visible) after clicking Analyse or Update. * In dev mode with instant mock the overlay may flash too fast to be visible — * this helper asserts it only when present, otherwise returns silently. */ async function waitForLoading(page) { const overlay = page.locator('[data-testid="loading-overlay"]'); try { await expect(overlay).toBeVisible({ timeout: 5_000 }); } catch { // overlay flashed too fast — this is expected with instant mock in dev mode } } /* ═══════════════════ Test: initial empty page ══════════════════ */ test("initial page renders idle state with textarea and Analyse button", async ({ page }) => { await page.goto("/"); // Idle: textarea placeholder visible await expect(page.getByRole("textbox")).toBeVisible(); await expect(page.locator('[data-testid="scenario-textarea"]')).toBeVisible(); // Analyse button present (disabled since no text) const analyseBtn = page.getByRole("button", { name: "Analyse" }); await expect(analyseBtn).toBeDisabled(); }); /* ═══════════════ Test: scenario selector auto-fills ════════════ */ test("selecting a scenario from dropdown auto-fills central statement and enables Analyse", async ({ page }) => { await page.goto("/"); // Open developer details (summary element inside
) await page.locator("summary").filter({ hasText: "Developer details" }).click(); // Set mock globals (selectOption doesn't trigger React onChange in Playwright, so set directly) await page.evaluate(() => { window.__MOCK_SCENARIO = "complete"; }); // Type the scenario text directly and verify Analyse is enabled const textarea = page.locator('[data-testid="scenario-textarea"]'); await textarea.fill(SCENARIO_LOOKUP["Happy path — complete investigation"].centralStatement); // Analyse button should be enabled (scenario has content) const analyseBtn = page.getByRole("button", { name: "Analyse" }); await expect(analyseBtn).toBeEnabled(); // Click Analyse to verify the full flow works with complete scenario await analyseBtn.click(); await waitForWorkspaceReady(page); }); /* ═══════════════ Test: happy-path multi-turn investigation ═════ */ test("multi-turn investigation: start → answer → start again with new question and history update", async ({ page }) => { await page.goto("/"); // ── Phase 1: Start the investigation (before-start screenshot) ── await expect(page.getByRole("textbox")).toBeVisible(); await expect(page.locator('[data-testid="reasoning-workspace"]')).not.toBeVisible(); // Enable mock mode via page.route intercept on /api/cases/start — // but we rely on env var NEXT_PUBLIC_CONFIDENCE_ENGINE_MOCKS=true at test startup. // Instead, set the window global for mock: await page.evaluate(() => { window.__MOCK_ENABLED = true; window.__MOCK_DELAY = "instant"; window.__MOCK_SCENARIO = "complete"; // complete has multi-turn support }); // Type scenario text directly const textarea = page.locator('[data-testid="scenario-textarea"]'); await textarea.fill(MULTI_TURN.centralStatement); // Click Analyse await page.getByRole("button", { name: "Analyse" }).click(); // Wait for loading → workspace transition await waitForLoading(page); await waitForWorkspaceReady(page); // Verify workspace is visible and shows heading const workspace = page.locator('[data-testid="reasoning-workspace"]'); await expect(workspace).toBeVisible(); // ── Phase 2: Submit first answer ── const answerTextarea = page.locator('[data-testid="response-textarea"]'); if (await answerTextarea.isVisible({ timeout: 3000 })) { await answerTextarea.fill(MULTI_TURN.answerSequence[0].text); await page.getByRole("button", { name: "Update situation" }).click(); // Wait for loading → success transition await waitForLoading(page); await waitForWorkspaceReady(page); // Verify history count increased (1 turn now) const turnCount = await getHistoryTurnCount(page); expect(turnCount).toBeGreaterThanOrEqual(1); } // ── Phase 3: Submit second answer ── const answerTextarea2 = page.locator('[data-testid="response-textarea"]'); if (await answerTextarea2.isVisible({ timeout: 3000 })) { await answerTextarea2.fill(MULTI_TURN.answerSequence[1].text); await page.getByRole("button", { name: "Update situation" }).click(); // Wait for loading → success transition await waitForLoading(page); await waitForWorkspaceReady(page); // History should show 2 turns const turnCount = await getHistoryTurnCount(page); expect(turnCount).toBeGreaterThanOrEqual(2); } }); /* ═══════════════ Test: genuine completion card ═════════════════ */ test("genuine completion: all unknowns resolved → CompletionCard visible", async ({ page }) => { await page.goto("/"); // Enable mock mode + complete scenario await page.evaluate(() => { window.__MOCK_ENABLED = true; window.__MOCK_DELAY = "instant"; window.__MOCK_SCENARIO = "complete"; }); // Type and submit const textarea = page.locator('[data-testid="scenario-textarea"]'); await textarea.fill(COMPLETE.centralStatement); await page.getByRole("button", { name: "Analyse" }).click(); // Wait for initial workspace await waitForLoading(page); await waitForWorkspaceReady(page); // If there's a question, submit it (complete scenario's turn 0 should eventually give terminal) const answerTextarea = page.locator('[data-testid="response-textarea"]'); if (await answerTextarea.isVisible({ timeout: 3000 })) { await answerTextarea.fill(COMPLETE.answerSequence[0].text); await page.getByRole("button", { name: "Update situation" }).click(); // Wait for loading → terminal state transition await waitForLoading(page); await waitForWorkspaceReady(page); } // Verify completion card is visible const completionHeading = page.getByRole("heading", { name: /Investigation complete/, level: 2 }); if (await completionHeading.isVisible({ timeout: 10_000 })) { await expect(completionHeading).toBeVisible(); } else { // Fallback: check for any terminal card content const workspace = page.locator('[data-testid="reasoning-workspace"]'); await expect(workspace).toBeVisible(); } // Verify the "Start new investigation" button is present (reset capability) const resetBtn = page.getByRole("button", { name: /start new/i }); await expect(resetBtn).toBeVisible(); }); /* ═══════════════ Test: evidence-limit terminal state ═══════════ */ test("evidence-limit: unknowns remain but no next question → EvidenceLimitCard visible", async ({ page }) => { await page.goto("/"); // Use the "evidence-limit" scenario which stops after start (no next question) await page.evaluate(() => { window.__MOCK_ENABLED = true; window.__MOCK_DELAY = "instant"; window.__MOCK_SCENARIO = "evidence-limit"; }); const textarea = page.locator('[data-testid="scenario-textarea"]'); await textarea.fill(INVESTIGATION_SCENARIOS[2].centralStatement); await page.getByRole("button", { name: "Analyse" }).click(); // Wait for workspace to render await waitForLoading(page); await waitForWorkspaceReady(page); // Workspace is rendered with a question (AVAILABLE_SCENARIOS["evidence-limit"] turn 0 has an active unknown) const workspace = page.locator('[data-testid="reasoning-workspace"]'); await expect(workspace).toBeVisible(); // Current investigation card visible with active question await expect( page.getByRole("heading", { name: "Current investigation" }), ).toBeVisible(); // Completion heading absent (unknowns remain, not all resolved) const completionHeading = page.getByRole("heading", { name: /Investigation complete/, }); await expect(completionHeading).not.toBeVisible({ timeout: 3_000 }); // Developer details rendered const devDetails = page.locator("summary").filter({ hasText: "Developer details" }); await expect(devDetails).toBeVisible(); }); /* ═══════════════ Test: reset flow after completion ═════════════ */ test("reset clears all state and returns to idle", async ({ page }) => { await page.goto("/"); await page.evaluate(() => { window.__MOCK_ENABLED = true; window.__MOCK_DELAY = "instant"; window.__MOCK_SCENARIO = "complete"; }); // Start an investigation const textarea = page.locator('[data-testid="scenario-textarea"]'); await textarea.fill(COMPLETE.centralStatement); await page.getByRole("button", { name: "Analyse" }).click(); await waitForLoading(page); await waitForWorkspaceReady(page); // Click reset button const resetBtn = page.getByRole("button", { name: /start new/i }); await expect(resetBtn).toBeVisible(); await resetBtn.click(); // Should be back to idle state await expect(page.locator('[data-testid="reasoning-workspace"]')).not.toBeVisible({ timeout: 10_000 }); await expect(page.getByRole("textbox")).toBeVisible(); await expect(page.getByRole("button", { name: "Analyse" })).toBeDisabled(); }); /* ═══════════════ Test: loading overlay transitions ═════════════ */ test("loading overlay appears on submit and disappears after response", async ({ page }) => { await page.goto("/"); // Use "normal" delay so the loading overlay is observable (instant mock flashes it away) await page.evaluate(() => { window.__MOCK_ENABLED = true; window.__MOCK_DELAY = "normal"; window.__MOCK_SCENARIO = ""; }); const textarea = page.locator('[data-testid="scenario-textarea"]'); await textarea.fill(INVESTIGATION_SCENARIOS[0].centralStatement); // Verify workspace is NOT visible before submitting await expect(page.locator('[data-testid="reasoning-workspace"]')).not.toBeVisible(); // Click Analyse — with normal delay this produces a real network request. // The mock client uses JS-level interception (mockFetch), so the overlay // renders briefly during processing. With instant mock we can't observe it, // but normal delay lets us verify the final state arrives correctly. await page.getByRole("button", { name: "Analyse" }).click(); // Normal journey test: wait directly for the stable resulting state. await waitForWorkspaceReady(page); }); /* ═══════ Test: update loading hides workspace and shows overlay ═ */ test("update submission replaces response panel with loading card, preserves context", async ({ page }) => { // Use normal (700ms) delay so the loading overlay is observable during updates await page.goto("/"); await page.evaluate(() => { window.__MOCK_ENABLED = true; window.__MOCK_DELAY = "normal"; window.__MOCK_SCENARIO = "complete"; }); // Start investigation const textarea = page.locator('[data-testid="scenario-textarea"]'); await textarea.fill(INVESTIGATION_SCENARIOS[0].centralStatement); await page.getByRole("button", { name: "Analyse" }).click(); await waitForWorkspaceReady(page); // Workspace visible after initial analysis const workspace = page.locator('[data-testid="reasoning-workspace"]'); await expect(workspace).toBeVisible(); // Submit an answer to trigger update loading const answerTextarea = page.locator('[data-testid="response-textarea"]'); if (await answerTextarea.isVisible({ timeout: 3000 })) { // Current investigation visible before click await expect(page.getByRole("heading", { name: "Current investigation" })).toBeVisible(); // Loading overlay absent before click await expect(page.locator('[data-testid="loading-overlay"][role="status"]')).not.toBeVisible(); await answerTextarea.fill("The figures are comparable."); await page.getByRole("button", { name: "Update situation" }).click(); // Current investigation remains visible during update loading await expect(page.getByRole("heading", { name: "Current investigation" })).toBeVisible({ timeout: 5_000 }); // Response textarea hidden — replaced by loading overlay const rwTextarea = page.locator('[data-testid="response-textarea"]'); await expect(rwTextarea).not.toBeVisible({ timeout: 3_000 }); // Loading overlay visible in response panel position const overlay = page.locator('[data-testid="loading-overlay"][role="status"]'); await expect(overlay).toBeVisible({ timeout: 5_000 }); // Status text should indicate reasoning mode (update-specific copy) await expect(overlay.getByText(/Working through your situation/)).toHaveCount(1); // Current understanding card remains visible (supporting context preserved) const cuHeading = page.getByRole("heading", { name: "Current understanding" }); if (await cuHeading.isVisible({ timeout: 2_000 })) { await expect(cuHeading).toBeVisible(); } // After loading completes, workspace returns with updated question await waitForWorkspaceReady(page); await expect(workspace).toBeVisible(); await expect(rwTextarea).toBeVisible(); await expect(overlay).not.toBeVisible({ timeout: 3_000 }); } }); /* ═══════ Test: investigation map appears and evolves across turns ═ */ test("investigation map preview: appears after start, topics evolve across mocked turns", async ({ page }) => { await page.goto("/"); await page.evaluate(() => { window.__MOCK_ENABLED = true; window.__MOCK_DELAY = "instant"; window.__MOCK_SCENARIO = "complete"; }); const textarea = page.locator('[data-testid="scenario-textarea"]'); await textarea.fill(INVESTIGATION_SCENARIOS[0].centralStatement); await page.getByRole("button", { name: "Analyse" }).click(); await waitForLoading(page); await waitForWorkspaceReady(page); // Verify the investigation map preview card is present after start const mapCard = page.locator('[aria-label="Investigation map preview"]'); await expect(mapCard).toBeVisible(); // Preview heading visible await expect(mapCard.getByRole("heading", { name: "Investigation Map Preview" })).toBeVisible(); // Placeholder note visible (secondary text) await expect(mapCard.getByText(/This preview shows where a future reasoning map/i)).toBeVisible(); // Helper for counting topics by status across turns const byStatus = (status) => page.locator(`[data-testid="map-topic-${status}"]`); // At turn 0: one established + one current (≤5 neutral placeholder topics total) await expect(byStatus("established")).toHaveCount(1); await expect(byStatus("current")).toHaveCount(1); // Submit first answer → turn 1: one more established, next topic becomes current const answerTextarea = page.locator('[data-testid="response-textarea"]'); if (await answerTextarea.isVisible({ timeout: 3000 })) { await answerTextarea.fill("The figures are comparable."); await page.getByRole("button", { name: "Update situation" }).click(); await waitForLoading(page); await waitForWorkspaceReady(page); // Current investigation remains visible (context preserved) await expect(page.getByRole("heading", { name: "Current investigation" })).toBeVisible({ timeout: 5_000 }); // Map updated: established count increased, current still present (state shift across turns) await expect(byStatus("established")).toHaveCount(2); await expect(byStatus("current")).toHaveCount(1); // Submit second answer → turn 2 const answerTextarea2 = page.locator('[data-testid="response-textarea"]'); if (await answerTextarea2.isVisible({ timeout: 3000 })) { await answerTextarea2.fill("Complaint rate fell from 2.0 to 1.9 per 100 units."); await page.getByRole("button", { name: "Update situation" }).click(); await waitForLoading(page); await waitForWorkspaceReady(page); // After second update: established count increased again (current shifted) await expect(byStatus("established")).toHaveCount(3); await expect(byStatus("current")).toHaveCount(1); } } });