368 lines
15 KiB
JavaScript
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('textarea[placeholder*=Answer]');
|
|
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("textarea[placeholder*=Describe]");
|
|
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("textarea[placeholder*=Describe]");
|
|
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("textarea[placeholder*=Describe]");
|
|
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("textarea[placeholder*=Describe]");
|
|
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('textarea[placeholder*=Answer]');
|
|
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("textarea[placeholder*=Describe]");
|
|
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("textarea[placeholder*=Describe]");
|
|
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("textarea[placeholder*=Describe]");
|
|
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('textarea[placeholder*=Answer]');
|
|
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('textarea[placeholder*=Answer]');
|
|
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("textarea[placeholder*=Describe]");
|
|
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('textarea[placeholder*=Answer]');
|
|
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();
|
|
}); |