feat(confidence-engine): v0.59b-c — report freshness on Report page + Portfolio
v0.59b — Report page freshness UI: - Shows Current / Update available beside the generated report - Manual Update report action with duplicate prevention guard - Explanation copy about investigation changes since generation - Persists generatedFromRevision during update flow v0.59c — Portfolio Report freshness state: - Surfaces Current / Update available alongside existing View report link - Derives solely from revision provenance (zero model calls) - No Update report action on Portfolio (manual update owned by Report page) - Neither state shown when no Report exists - Updated makeSnapshot with investigationRevision for realistic test data
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user