193 lines
7.9 KiB
JavaScript
193 lines
7.9 KiB
JavaScript
/**
|
|
* 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();
|
|
}); |