/** * Recovery-state E2E tests — provider error and malformed response flows. * * What these tests validate: * - Error state renders an error panel with structured message * - Workspace remains visible alongside the recovery card * - ProviderUnavailableCard / MalformedResponseCard content visible * - Session persistence survives a recovery state (recovery key in sessionStorage) * * What these tests do NOT validate: * - Error handling logic correctness (that is unit-level concern) */ import { test, expect } from "@playwright/test"; import { INVESTIGATION_SCENARIOS, SCENARIO_LOOKUP } from "./fixtures/investigation-scenarios.js"; const PROVIDER_ERROR = SCENARIO_LOOKUP["Provider error"]; const MALFORMED_RESPONSE = SCENARIO_LOOKUP["Malformed response"]; /* ── Test helpers ─────────────────────────────────────────────── */ async function waitForLoading(page) { await expect(page.locator('[data-testid="loading-overlay"]')).toBeVisible(); } async function waitForWorkspaceReady(page, options = {}) { const { expectLoadingGone = true } = options; if (expectLoadingGone) { await page.waitForSelector('[data-testid="loading-overlay"]', { state: "hidden", timeout: 30_000 }); } const workspace = page.locator('[data-testid="reasoning-workspace"]'); await expect(workspace).toBeVisible({ timeout: 15_000 }); } /* ═══════════════ Test: provider error after start ══════════════ */ test("provider error: analyse returns failure → error panel visible alongside workspace", async ({ page }) => { await page.goto("/"); // Enable mock mode in error scenario await page.evaluate(() => { window.__MOCK_ENABLED = true; window.__MOCK_DELAY = "instant"; window.__MOCK_SCENARIO = "error"; }); const textarea = page.locator('[data-testid="scenario-textarea"]'); await textarea.fill(PROVIDER_ERROR.centralStatement); await page.getByRole("button", { name: "Analyse" }).click(); // Loading overlay appears briefly then disappears await waitForLoading(page); await expect(page.locator('[data-testid="loading-overlay"]')).toBeHidden({ timeout: 5_000 }); // Workspace should still be visible (even with error result) await expect(page.locator('[data-testid="reasoning-workspace"]')).toBeVisible(); // Error panel with structured error message const errorPanel = page.locator('div[class*="border-red"][class*="bg-red"]'); await expect(errorPanel).toBeVisible(); await expect(errorPanel.locator('div[class*="text-sm"]').first()).toContainText(/error/i); }); /* ═══════════════ Test: provider error during update ════════════ */ test("provider error during update turn: error panel appears after answer submit", async ({ page }) => { await page.goto("/"); // Use a non-error scenario first to get into success state, then switch to error for update await page.evaluate(() => { window.__MOCK_ENABLED = true; window.__MOCK_DELAY = "instant"; window.__MOCK_SCENARIO = ""; // normal default for start }); 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); // Switch to error scenario for the update call await page.evaluate(() => { window.__MOCK_SCENARIO = "error"; }); // Submit an answer to trigger the update call (which will now get error) const answerTextarea = page.locator('[data-testid="response-textarea"]'); if (await answerTextarea.isVisible({ timeout: 3000 })) { await answerTextarea.fill("Some answer that should fail"); await page.getByRole("button", { name: "Update situation" }).click(); // Verify error panel appears after the update call errors const errorPanel = page.locator('div[class*="border-red"][class*="bg-red"]'); await expect(errorPanel).toBeVisible({ timeout: 10_000 }); } }); /* ═══════════════ Test: malformed response during start ═════════ */ test("malformed response during start: error state visible, workspace still rendered", async ({ page }) => { await page.goto("/"); // Malformed responses come through the error mock path (same as provider error) await page.evaluate(() => { window.__MOCK_ENABLED = true; window.__MOCK_DELAY = "instant"; window.__MOCK_SCENARIO = "error"; }); const textarea = page.locator('[data-testid="scenario-textarea"]'); await textarea.fill(MALFORMED_RESPONSE.centralStatement); await page.getByRole("button", { name: "Analyse" }).click(); // Loading then workspace await waitForLoading(page); await expect(page.locator('[data-testid="loading-overlay"]')).toBeHidden({ timeout: 5_000 }); const workspace = page.locator('[data-testid="reasoning-workspace"]'); await expect(workspace).toBeVisible(); // Error text present in the red panel const errorText = page.locator('div[class*="border-red"][class*="text-sm"]').first(); await expect(errorText).toHaveText(/error|failed/i); }); /* ═══════════════ Test: recovery state persists in session ══════ */ test("recovery state persists via sessionStorage after error response", async ({ page }) => { await page.goto("/"); await page.evaluate(() => { window.__MOCK_ENABLED = true; window.__MOCK_DELAY = "instant"; window.__MOCK_SCENARIO = "error"; }); const textarea = page.locator('[data-testid="scenario-textarea"]'); await textarea.fill(PROVIDER_ERROR.centralStatement); await page.getByRole("button", { name: "Analyse" }).click(); // Wait for workspace to render with error state await waitForLoading(page); await expect(page.locator('[data-testid="loading-overlay"]')).toBeHidden({ timeout: 5_000 }); await expect(page.locator('[data-testid="reasoning-workspace"]')).toBeVisible(); // Verify sessionStorage has the session key with situationGraph from error response const sessionRaw = await page.evaluate(() => { const raw = sessionStorage.getItem("confidence-engine-session"); return raw ? JSON.parse(raw) : null; }); // Session should exist (even in error recovery path, the scenario text is persisted) expect(sessionRaw).not.toBeNull(); }); /* ═══════════════ Test: multiple error transitions ══════════════ */ test("multiple successive errors maintain UI state without crash", async ({ page }) => { await page.goto("/"); // Enable mock mode with error scenario await page.evaluate(() => { window.__MOCK_ENABLED = true; window.__MOCK_DELAY = "instant"; window.__MOCK_SCENARIO = "error"; }); const textarea = page.locator('[data-testid="scenario-textarea"]'); await textarea.fill(PROVIDER_ERROR.centralStatement); // First analyse call await page.getByRole("button", { name: "Analyse" }).click(); await waitForLoading(page); await expect(page.locator('[data-testid="loading-overlay"]')).toBeHidden({ timeout: 5_000 }); await expect(page.locator('[data-testid="reasoning-workspace"]')).toBeVisible(); // If there's an answer textarea, try another update (also errors) const answerTextarea = page.locator('[data-testid="response-textarea"]'); if (await answerTextarea.isVisible({ timeout: 2000 })) { await answerTextarea.fill("Second attempt"); await page.getByRole("button", { name: "Update situation" }).click(); // Error panel should still be visible (no crash, no layout shift) const errorPanel = page.locator('div[class*="border-red"]'); await expect(errorPanel).toBeVisible({ timeout: 10_000 }); } // Workspace should remain stable — no errors thrown const workspace = page.locator('[data-testid="reasoning-workspace"]'); await expect(workspace).toBeVisible(); });