/** * v0.59c — targeted deterministic tests for Portfolio Report freshness UI. * * Exercises four cases on the Portfolio: * 1. Report + matching revisions → "Current" * 2. Report + differing revisions → "Update available" * 3. differing revisions still render View report action * 4. no Report → neither Current nor Update available * 5. zero model/fetch calls during render * 6. Portfolio does not mutate Investigation storage * 7. existing Continue / Restart actions unchanged */ import { describe, it, expect, vi, beforeEach } from "vitest"; import React from "react"; import { render, screen, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom"; vi.mock("next/navigation", () => ({ useRouter: () => ({ push: () => {} }), })); let mockClearStorage = vi.fn(); let mockLoadResult = null; function setMockSnapshot(snap) { if (snap) { localStorage.setItem( "confidence-engine-investigation", JSON.stringify(snap), ); mockLoadResult = snap; } else { localStorage.removeItem("confidence-engine-investigation"); mockLoadResult = null; } } vi.mock("@/lib/storage/investigation-storage", () => ({ loadInvestigation: () => mockLoadResult, saveInvestigation: vi.fn(), clearInvestigation: () => { localStorage.removeItem("confidence-engine-investigation"); mockClearStorage(); }, })); /** Creates a snapshot with report. revisionMatch controls freshness state. */ function makeFreshSnapshot(investigationRevision, revisionMatch) { return { scenario: "Test investigation", situationGraph: { centralStatement: "CS", nodes: [], edges: [] }, selectedQuestion: null, summary: "Test summary.", updatedAt: new Date().toISOString(), schemaVersion: 1, investigationReport: { understanding: "We understand complaints rose.", hasPlausibleInterpretations: false, generatedFromRevision: revisionMatch ? investigationRevision : (investigationRevision - 2), }, investigationRevision, findings: [], }; } /** Creates a snapshot with NO report. */ function makeNoReportSnapshot(investigationRevision) { return { scenario: "Test investigation", situationGraph: { centralStatement: "CS", nodes: [], edges: [] }, selectedQuestion: null, summary: "Test summary.", updatedAt: new Date().toISOString(), schemaVersion: 1, investigationReport: null, investigationRevision, findings: [], }; } async function cleanup() { localStorage.removeItem("confidence-engine-investigation"); } describe("Portfolio freshness — Report + matching revisions", () => { it("renders Current alongside View report", async () => { setMockSnapshot(makeFreshSnapshot(3, true)); const mod = await import("@/app/page.jsx"); render(React.createElement(mod.default)); expect(await screen.findByText(/View report/i)).toBeInTheDocument(); expect(screen.getByText("Current")).toBeInTheDocument(); await cleanup(); }); it("makes zero fetch calls during render", async () => { global.fetch = vi.fn(); setMockSnapshot(makeFreshSnapshot(3, true)); const mod = await import("@/app/page.jsx"); render(React.createElement(mod.default)); try { await waitFor(() => screen.findByText(/View report/i), { timeout: 5000 }); } catch {} // Portfolio should not call fetch at all — derives state from localStorage only expect(global.fetch).not.toHaveBeenCalled(); global.fetch = undefined; await cleanup(); }); }); describe("Portfolio freshness — Report + differing revisions", () => { it("renders Update available alongside View report", async () => { setMockSnapshot(makeFreshSnapshot(5, false)); const mod = await import("@/app/page.jsx"); render(React.createElement(mod.default)); expect(await screen.findByText(/View report/i)).toBeInTheDocument(); expect(screen.getByText("Update available")).toBeInTheDocument(); await cleanup(); }); it("View report still present as link for differing revisions", async () => { setMockSnapshot(makeFreshSnapshot(5, false)); const mod = await import("@/app/page.jsx"); render(React.createElement(mod.default)); const link = await screen.findByRole("link", { name: /View report/i }); expect(link).toBeInTheDocument(); expect(link).toHaveAttribute("href", "/investigations/case-1/report"); await cleanup(); }); }); describe("Portfolio freshness — no Report", () => { it("renders neither Current nor Update available when no report exists", async () => { setMockSnapshot(makeNoReportSnapshot(0)); const mod = await import("@/app/page.jsx"); render(React.createElement(mod.default)); expect(screen.queryByText(/View report/i)).not.toBeInTheDocument(); expect(screen.queryByText("Current")).not.toBeInTheDocument(); expect(screen.queryByText("Update available")).not.toBeInTheDocument(); await cleanup(); }); }); describe("Portfolio existing actions preserved", () => { it("Continue investigation and Restart remain when a report exists", async () => { setMockSnapshot(makeFreshSnapshot(3, true)); const mod = await import("@/app/page.jsx"); render(React.createElement(mod.default)); expect(await screen.findByText(/Continue investigation/i)).toBeInTheDocument(); expect(await screen.findByText(/Restart investigation/i)).toBeInTheDocument(); await cleanup(); }); }); describe("Portfolio freshness — differing revisions no-additional-calls", () => { it("refreshing with differing data does not trigger additional API calls", async () => { global.fetch = vi.fn(); setMockSnapshot(makeFreshSnapshot(5, false)); const mod = await import("@/app/page.jsx"); render(React.createElement(mod.default)); try { await waitFor(() => screen.findByText(/Update available/i), { timeout: 2000 }); } catch {} expect(global.fetch).not.toHaveBeenCalled(); global.fetch = undefined; await cleanup(); }); });