Files
confidence-engine/tests/e2e/long-investigation.spec.js
T

368 lines
15 KiB
JavaScript

/**
* Long investigation E2E tests — multi-turn, contradiction, diagnosis,
* comparison, and prioritisation fixtures.
*
* What these tests validate:
* - Multi-turn flow works correctly for long sequences (5+ turns)
* - History count increases with each answer submission
* - Question text changes after each turn (observable UI transition)
* - Contradiction fixture shows opposing recommendations in history
* - Diagnosis scenario renders correctly with node/edge summary
* - Comparison fixture produces correct side-by-side structure
* - Prioritisation fixture yields ranked options in result card
* - Loading state between each turn is observable (not skipped)
*
* What these tests do NOT validate:
* - Correctness of reasoning, node selection, or question quality
*/
import { test, expect } from "@playwright/test";
import { INVESTIGATION_SCENARIOS, SCENARIO_LOOKUP } from "./fixtures/investigation-scenarios.js";
/* ── Fixture scenarios (from INVESTIGATION_SCENARIOS, not from AVAILABLE_SCENARIOS in the UI)
Note: INVESTIGATION_SCENARIOS has its own labels independent of the UI dropdown
(AVAILABLE_SCENARIOS). The UI dropdown uses keys/labels from lib/mocks/scenarios.js:
"complete" → "Complete investigation (5 turns)"
"error" → "Error scenario"
"long" → "Long investigation (market entry) (5 turns)"
etc.
To use a specific UI-scenario, set window.__MOCK_SCENARIO in the test.
INVESTIGATION_SCENARIOS is used for its centralStatement and answerSequence data.
For example: mockMode="complete" → set __MOCK_SCENARIO="complete".
────────────────────────────────── */
const LONG_INVESTIGATION = SCENARIO_LOOKUP["Long investigation — European market entry"];
const CONTRADICTION = SCENARIO_LOOKUP["Contradiction fixture"];
const DIAGNOSIS = SCENARIO_LOOKUP["Diagnosis fixture"];
const COMPARISON = SCENARIO_LOOKUP["Comparison fixture"];
const PRIORITISATION = SCENARIO_LOOKUP["Prioritisation fixture (team relocation)"];
/* ── 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 });
}
/**
* Helper to submit an answer and wait for the result.
* Returns { success, hasError } for test assertions.
*/
async function submitAnswer(page, text) {
const textarea = page.locator('[data-testid="response-textarea"]');
if (!(await textarea.isVisible({ timeout: 3000 }).catch(() => false))) {
return { success: false, hasError: false };
}
await textarea.fill(text);
await page.getByRole("button", { name: "Update situation" }).click();
// Wait for the answer to submit (if text is non-empty)
if (text && text.trim()) {
// After submit, loading appears then workspace updates
await waitForLoading(page);
await waitForWorkspaceReady(page);
return { success: true, hasError: false };
}
return { success: false, hasError: true };
}
/* ═══════════════ Test: long investigation multi-turn ═══════════ */
test("long investigation — European market entry with 4 answer turns", async ({ page }) => {
await page.goto("/");
// Enable mock mode for complete scenario (supports multiple turns)
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(LONG_INVESTIGATION.centralStatement);
await page.getByRole("button", { name: "Analyse" }).click();
// Initial workspace renders
await waitForLoading(page);
await expect(page.locator('[data-testid="loading-overlay"]')).toBeHidden({ timeout: 5_000 });
await expect(page.locator('[data-testid="reasoning-workspace"]')).toBeVisible();
// Submit each answer in sequence
const historyCounts = [];
for (let i = 0; i < LONG_INVESTIGATION.answerSequence.length; i++) {
const result = await submitAnswer(page, LONG_INVESTIGATION.answerSequence[i].text);
expect(result.success).toBe(true);
// Record history turn count after each answer
const indicators = page.locator('[data-testid="investigation-turn"]');
const count = await indicators.count();
historyCounts.push(count);
// Each turn's question should be visible (or terminal state)
const workspace = page.locator('[data-testid="reasoning-workspace"]');
await expect(workspace).toBeVisible();
}
// History counts should have monotonically increased
for (let i = 1; i < historyCounts.length; i++) {
expect(historyCounts[i]).toBeGreaterThanOrEqual(historyCounts[i - 1]);
}
});
/* ═══════════════ Test: contradiction fixture renders both sides ═ */
test("contradiction fixture — opposing recommendations in investigation card", async ({ page }) => {
await page.goto("/");
await page.evaluate(() => {
window.__MOCK_ENABLED = true;
window.__MOCK_DELAY = "instant";
window.__MOCK_SCENARIO = ""; // use default
});
const textarea = page.locator('[data-testid="scenario-textarea"]');
await textarea.fill(CONTRADICTION.centralStatement);
await page.getByRole("button", { name: "Analyse" }).click();
// Workspace renders with contradiction scenario
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 (next question in contradiction flow), submit it
const result = await submitAnswer(page, CONTRADICTION.answerSequence[0]?.text || "");
if (result.success) {
// Workspace should show updated investigation with contradiction nodes
const workspace = page.locator('[data-testid="reasoning-workspace"]');
await expect(workspace).toBeVisible();
// Current understanding summary should be present
const understandingHeading = page.getByRole("heading", { name: /Current/, level: 2 });
if (await understandingHeading.isVisible({ timeout: 5000 })) {
await expect(understandingHeading).toBeVisible();
}
}
// Final workspace should still be visible regardless of whether terminal was reached
const workspace = page.locator('[data-testid="reasoning-workspace"]');
await expect(workspace).toBeVisible();
});
/* ═══════════════ Test: diagnosis scenario ══════════════════════ */
test("diagnosis scenario — churn investigation renders correctly", async ({ page }) => {
await page.goto("/");
await page.evaluate(() => {
window.__MOCK_ENABLED = true;
window.__MOCK_DELAY = "instant";
window.__MOCK_SCENARIO = "";
});
const textarea = page.locator('[data-testid="scenario-textarea"]');
await textarea.fill(DIAGNOSIS.centralStatement);
await page.getByRole("button", { name: "Analyse" }).click();
// Workspace renders with diagnosis nodes and edges
await waitForLoading(page);
await expect(page.locator('[data-testid="loading-overlay"]')).toBeHidden({ timeout: 5_000 });
await expect(page.locator('[data-testid="reasoning-workspace"]')).toBeVisible();
// Scenario-specific heading should be present
const workspace = page.locator('[data-testid="reasoning-workspace"]');
await expect(workspace).toBeVisible();
// The central statement from the diagnosis scenario should appear in the card
const cardText = page.locator('[data-testid="reasoning-workspace"]').locator('p').first();
if (await cardText.isVisible()) {
await expect(cardText).toContainText(/churn/i);
}
});
/* ═══════════════ Test: comparison scenario with side-by-side ══ */
test("comparison scenario — product rating nodes render with edges", async ({ page }) => {
await page.goto("/");
await page.evaluate(() => {
window.__MOCK_ENABLED = true;
window.__MOCK_DELAY = "instant";
window.__MOCK_SCENARIO = "";
});
const textarea = page.locator('[data-testid="scenario-textarea"]');
await textarea.fill(COMPARISON.centralStatement);
await page.getByRole("button", { name: "Analyse" }).click();
// Workspace renders
await waitForLoading(page);
await expect(page.locator('[data-testid="loading-overlay"]')).toBeHidden({ timeout: 5_000 });
await expect(page.locator('[data-testid="reasoning-workspace"]')).toBeVisible();
// Submit first answer (comparison fixture has multiple turns)
const result = await submitAnswer(page, COMPARISON.answerSequence[0]?.text || "");
if (result.success) {
// Second turn's answer textarea should appear
const answerTextarea = page.locator('[data-testid="response-textarea"]');
expect(await answerTextarea.isVisible()).toBe(true);
// Submit second answer
await submitAnswer(page, COMPARISON.answerSequence[1]?.text || "");
// Workspace still visible after multi-turn
await expect(page.locator('[data-testid="reasoning-workspace"]')).toBeVisible();
}
});
/* ═══════════════ Test: prioritisation scenario ═════════════════ */
test("prioritisation scenario — team relocation yields ranked options", async ({ page }) => {
await page.goto("/");
await page.evaluate(() => {
window.__MOCK_ENABLED = true;
window.__MOCK_DELAY = "instant";
window.__MOCK_SCENARIO = "";
});
const textarea = page.locator('[data-testid="scenario-textarea"]');
await textarea.fill(PRIORITISATION.centralStatement);
await page.getByRole("button", { name: "Analyse" }).click();
// Workspace renders
await waitForLoading(page);
await expect(page.locator('[data-testid="loading-overlay"]')).toBeHidden({ timeout: 5_000 });
await expect(page.locator('[data-testid="reasoning-workspace"]')).toBeVisible();
// Submit first answer
const result = await submitAnswer(page, PRIORITISATION.answerSequence[0]?.text || "");
if (result.success) {
// Workspace should show updated state with prioritisation nodes
const workspace = page.locator('[data-testid="reasoning-workspace"]');
await expect(workspace).toBeVisible();
}
});
/* ═══════════════ Test: rapid multi-turn loading transitions ─── */
test("rapid successive updates maintain correct loading-state cycling", async ({ page }) => {
await page.goto("/");
await page.evaluate(() => {
window.__MOCK_ENABLED = true;
window.__MOCK_DELAY = "instant";
window.__MOCK_SCENARIO = "";
});
const textarea = page.locator('[data-testid="scenario-textarea"]');
await textarea.fill(LONG_INVESTIGATION.centralStatement); // use long scenario central statement
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();
// Submit multiple answers rapidly (all instant mock = no delay)
for (let i = 0; i < LONG_INVESTIGATION.answerSequence.length; i++) {
const ans = LONG_INVESTIGATION.answerSequence[i]?.text;
if (!ans) break;
await submitAnswer(page, ans);
// After each submission, verify workspace stable and no duplicate loading overlays
const overlays = page.locator('[data-testid="loading-overlay"]');
await expect(overlays).toHaveCount(0);
}
});
/* ═══════════════ Test: answer textarea placeholder changes ───── */
test("answer textarea placeholder updates between turns", async ({ page }) => {
await page.goto("/");
await page.evaluate(() => {
window.__MOCK_ENABLED = true;
window.__MOCK_DELAY = "instant";
window.__MOCK_SCENARIO = "";
});
const textarea = page.locator('[data-testid="scenario-textarea"]');
await textarea.fill(INVESTIGATION_SCENARIOS[0].centralStatement);
await page.getByRole("button", { name: "Analyse" }).click();
// Initial workspace should have an answer textarea with a question-related placeholder
const answerTextarea = page.locator('[data-testid="response-textarea"]');
await expect(answerTextarea).toBeVisible({ timeout: 10_000 });
// Submit first answer
await submitAnswer(page, INVESTIGATION_SCENARIOS[0].answerSequence[0]?.text || "");
// After update, new question textarea should be present with potentially different placeholder
const updatedTextarea = page.locator('[data-testid="response-textarea"]');
await expect(updatedTextarea).toBeVisible();
// The workspace card should still display the current investigation context
const workspace = page.locator('[data-testid="reasoning-workspace"]');
await expect(workspace).toBeVisible();
});
/* ═══════════════ Test: terminal state with no answer available ─ */
test("terminal state renders without answer textarea", async ({ page }) => {
await page.goto("/");
// Use the "complete" scenario which eventually reaches a terminal (no next question)
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(SCENARIO_LOOKUP["Happy path — complete investigation"].centralStatement);
await page.getByRole("button", { name: "Analyse" }).click();
// Workspace renders
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 a question (turn 0 gives a question), submit it to reach terminal
const answerTextarea = page.locator('[data-testid="response-textarea"]');
if (await answerTextarea.isVisible({ timeout: 3000 })) {
await answerTextarea.fill(SCENARIO_LOOKUP["Happy path — complete investigation"].answerSequence[0]?.text || "");
await page.getByRole("button", { name: "Update situation" }).click();
// Wait for terminal 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();
// At terminal state, answer textarea should no longer be visible (or disabled)
const hasAnswer = await answerTextarea.isVisible().catch(() => false);
if (hasAnswer) {
const disabledAttr = await answerTextarea.getAttribute("disabled");
expect(disabledAttr).toBeTruthy();
}
}
// Terminal card should be present
const workspace = page.locator('[data-testid="reasoning-workspace"]');
await expect(workspace).toBeVisible();
});