/** * 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(); });