437 lines
18 KiB
JavaScript
437 lines
18 KiB
JavaScript
/**
|
|
* 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 <details>)
|
|
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);
|
|
}
|
|
}
|
|
}); |