feat: add mock-mode docs and additional e2e tests (long investigation, recovery states)
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* 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("textarea[placeholder*=Describe]");
|
||||
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("textarea[placeholder*=Describe]");
|
||||
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('textarea[placeholder*=Answer]');
|
||||
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("textarea[placeholder*=Describe]");
|
||||
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("textarea[placeholder*=Describe]");
|
||||
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("textarea[placeholder*=Describe]");
|
||||
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('textarea[placeholder*=Answer]');
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user