feat(confidence-engine): v0.60g2 render investigation portfolio
This commit is contained in:
+24
-33
@@ -1,30 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { loadInvestigation, clearInvestigation } from "@/lib/storage/investigation-storage";
|
||||
import { listInvestigations, clearInvestigation } from "@/lib/storage/investigation-storage";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const INVESTIGATION_ID = "case-1";
|
||||
|
||||
function Portfolio() {
|
||||
const router = useRouter();
|
||||
const [existing, setExisting] = React.useState(null);
|
||||
const [summaries, setSummaries] = React.useState([]);
|
||||
const [showRestartConfirm, setShowRestartConfirm] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setExisting(loadInvestigation());
|
||||
setSummaries(listInvestigations());
|
||||
}, []);
|
||||
|
||||
const hasReport = Boolean(
|
||||
existing?.investigationReport && existing.investigationReport.understanding
|
||||
);
|
||||
|
||||
const reportIsCurrent =
|
||||
hasReport &&
|
||||
existing.investigationReport.generatedFromRevision ===
|
||||
(existing.investigationRevision ?? 0);
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-[640px] px-6 py-16">
|
||||
<h1 className="mb-2 text-3xl font-bold tracking-tight">Confidence Engine</h1>
|
||||
@@ -32,29 +21,30 @@ function Portfolio() {
|
||||
Investigator's notebook — index of persisted investigations.
|
||||
</p>
|
||||
|
||||
{/* Existing investigation */}
|
||||
{existing && (
|
||||
{/* Investigation collection */}
|
||||
{summaries.length > 0 && (
|
||||
<section className="mb-10">
|
||||
<h2 className="mb-4 text-[13px] font-bold tracking-[.18em] uppercase text-teal-700/80">
|
||||
Investigations
|
||||
</h2>
|
||||
|
||||
<div className="rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 py-6 shadow-sm">
|
||||
{summaries.map((summary) => (
|
||||
<div key={summary.id} className="rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 py-6 shadow-sm">
|
||||
<p className="text-sm text-gray-700">
|
||||
{existing.scenario || "Untitled investigation"}
|
||||
{summary.scenario || "Untitled investigation"}
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex items-start gap-3 text-sm">
|
||||
{hasReport ? (
|
||||
{summary.reportExists ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Link
|
||||
href={`/investigations/${INVESTIGATION_ID}/report`}
|
||||
href={`/investigations/${summary.id}/report`}
|
||||
className="rounded-lg border border-teal-600 bg-white px-4 py-2 font-medium text-teal-700 hover:bg-teal-50 transition"
|
||||
>
|
||||
View report
|
||||
</Link>
|
||||
|
||||
{reportIsCurrent ? (
|
||||
{summary.reportGeneratedFromRevision === summary.investigationRevision ? (
|
||||
<span className="text-[11px] font-semibold tracking-wider uppercase text-teal-700/70">
|
||||
Current
|
||||
</span>
|
||||
@@ -67,32 +57,32 @@ function Portfolio() {
|
||||
) : null}
|
||||
|
||||
<Link
|
||||
href={`/investigations/${INVESTIGATION_ID}`}
|
||||
href={`/investigations/${summary.id}`}
|
||||
className="self-start rounded-lg border border-teal-600 bg-white px-4 py-2 font-medium text-teal-700 hover:bg-teal-50 transition"
|
||||
>
|
||||
Continue investigation
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={() => setShowRestartConfirm(true)}
|
||||
onClick={() => setShowRestartConfirm(summary.id)}
|
||||
className="self-start rounded-lg border border-red-400 bg-white px-4 py-2 font-medium text-red-700 hover:bg-red-50 transition"
|
||||
>
|
||||
Restart investigation
|
||||
</button>
|
||||
|
||||
{showRestartConfirm && (
|
||||
{showRestartConfirm === summary.id && (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="restart-title"
|
||||
aria-labelledby={`restart-title-${summary.id}`}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
|
||||
onClick={() => setShowRestartConfirm(false)}
|
||||
onClick={() => setShowRestartConfirm(null)}
|
||||
>
|
||||
<div
|
||||
className="w-[420px] rounded-xl border border-gray-200 bg-white p-6 shadow-lg"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h2 id="restart-title" className="mb-3 text-lg font-semibold">
|
||||
<h2 id={`restart-title-${summary.id}`} className="mb-3 text-lg font-semibold">
|
||||
Restart this investigation?
|
||||
</h2>
|
||||
<p className="mb-5 text-sm text-gray-600">
|
||||
@@ -100,16 +90,16 @@ function Portfolio() {
|
||||
</p>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setShowRestartConfirm(false)}
|
||||
onClick={() => setShowRestartConfirm(null)}
|
||||
className="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowRestartConfirm(false);
|
||||
try { clearInvestigation(); } catch (_) { /* storage must not crash caller */ }
|
||||
setExisting(null);
|
||||
setShowRestartConfirm(null);
|
||||
try { clearInvestigation(summary.id); } catch (_) { /* storage must not crash caller */ }
|
||||
setSummaries(listInvestigations());
|
||||
}}
|
||||
className="rounded-lg border border-red-400 bg-white px-4 py-2 text-sm font-medium text-red-700 hover:bg-red-50 transition"
|
||||
>
|
||||
@@ -121,11 +111,12 @@ function Portfolio() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* No existing investigation */}
|
||||
{!existing && (
|
||||
{/* No investigations */}
|
||||
{summaries.length === 0 && (
|
||||
<section className="mb-10">
|
||||
<h2 className="mb-4 text-[13px] font-bold tracking-[.18em] uppercase text-teal-700/80">
|
||||
Investigations
|
||||
|
||||
@@ -516,6 +516,39 @@ v0.60g2: Migrate Portfolio to consume `listInvestigations()` for collection rend
|
||||
|
||||
Smallest next increment: implement the four-operation storage contract in `lib/storage/investigation-storage.js` as a re-export of a provider-backed interface whose signatures accept/return domain Investigation objects keyed by durable ID — without committing to any specific localStorage or database representation. This means defining the exported function signatures and the Investigation shape that flows through them, while deferring key scheme, row schema, and collection structure to a later implementation decision.
|
||||
|
||||
## v0.60g2 — Render Investigation Portfolio
|
||||
|
||||
**Purpose:** Migrate Portfolio to consume `listInvestigations()` for collection rendering — replace the hardcoded singleton card with a rendered list of Investigation summaries.
|
||||
|
||||
### What was implemented
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `app/page.jsx` | Portfolio consumes `listInvestigations()` instead of legacy singleton; renders investigation cards from the persisted list; Create New allocates durable ID via `crypto.randomUUID()` + navigates to `/investigations/{id}` |
|
||||
| `tests/ui/v060g2-portfolio-collection.test.jsx` | Dedicated UI test covering: two distinct Investigations persist independently → Portfolio lists both → cards show correct IDs/scenarios/freshness → Create New allocates ID and navigates |
|
||||
|
||||
### Deterministic evidence
|
||||
|
||||
- Test file: `tests/ui/v060g2-portfolio-collection.test.jsx`
|
||||
- First run result: **24/24 PASS** (no reruns)
|
||||
- Build: **PASS**
|
||||
|
||||
### Live verification
|
||||
|
||||
Rob manually verified: created a second genuine durable-ID Investigation → returned to Portfolio → two distinct persisted Investigation cards visible → Create New visible → no case-1 card presentation.
|
||||
|
||||
### Known defect discovered during live verification
|
||||
|
||||
A separate pre-existing Report identity defect was discovered during live verification of v0.60g2. The Report route does not read its `[id]` parameter — it calls `loadInvestigation()` without an ID and therefore reads the legacy singleton path, which returns `null` for durable-ID Investigations. This is classified as:
|
||||
|
||||
**PRIMARY CLASSIFICATION: A — REPORT IDENTITY DEFECT**
|
||||
|
||||
This defect will be addressed in a separate increment (v0.60h). It is NOT caused by empty-Done semantics.
|
||||
|
||||
### Next restart point
|
||||
|
||||
v0.60h: Migrate Report route to use route `[id]` for all identity operations (initial load, generation, update). Preserve existing v0.58/v0.59 Report lifecycle and freshness semantics. This is an identity migration only — no Report redesign.
|
||||
|
||||
## Next restart point
|
||||
|
||||
Consult `docs/design-evolution/README.md` for progressive loading of product reasoning and provenance chronology; load the relevant chapter only when a specific historical question requires it.
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
import { describe, expect, it, beforeEach, vi } from "vitest";
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent, waitFor, within } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock next/navigation — static file-level mock
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let pushRef = { push: () => {} };
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => pushRef,
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock investigation-storage — application-facing contract only
|
||||
// No localStorage. No provider imports. No module-cache manipulation.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let mockList = [];
|
||||
let mockClearTarget = null;
|
||||
|
||||
vi.mock("@/lib/storage/investigation-storage", () => ({
|
||||
listInvestigations: () => [...mockList],
|
||||
clearInvestigation: (id) => {
|
||||
mockClearTarget = id;
|
||||
},
|
||||
}));
|
||||
|
||||
function setMockSummaries(snapshots) {
|
||||
mockList = snapshots ?? [];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test fixture data — plain InvestigationSummary arrays
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function summaryA() {
|
||||
return {
|
||||
id: "inv-a",
|
||||
scenario: "Scenario A",
|
||||
updatedAt: "2026-09-03T10:00:00.000Z",
|
||||
investigationRevision: 2,
|
||||
reportExists: true,
|
||||
reportGeneratedFromRevision: 2,
|
||||
};
|
||||
}
|
||||
|
||||
function summaryB() {
|
||||
return {
|
||||
id: "inv-b",
|
||||
scenario: "Scenario B",
|
||||
updatedAt: "2026-09-02T10:00:00.000Z",
|
||||
investigationRevision: 3,
|
||||
reportExists: true,
|
||||
reportGeneratedFromRevision: 2,
|
||||
};
|
||||
}
|
||||
|
||||
function summaryNoReport() {
|
||||
return {
|
||||
id: "inv-no-report",
|
||||
scenario: "Scenario No Report",
|
||||
updatedAt: "2026-09-01T10:00:00.000Z",
|
||||
investigationRevision: 1,
|
||||
reportExists: false,
|
||||
reportGeneratedFromRevision: null,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// A — zero summaries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("v0.60g2 — zero summaries", () => {
|
||||
let Portfolio;
|
||||
|
||||
beforeEach(async () => {
|
||||
pushRef.push = vi.fn();
|
||||
setMockSummaries([]);
|
||||
const mod = await import("@/app/page.jsx");
|
||||
Portfolio = mod.default;
|
||||
});
|
||||
|
||||
it('shows "No investigations yet."', async () => {
|
||||
render(React.createElement(Portfolio));
|
||||
expect(await screen.findByText(/No investigations yet\./i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show any Continue links", async () => {
|
||||
render(React.createElement(Portfolio));
|
||||
expect(screen.queryByText(/Continue investigation/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// B — one summary
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("v0.60g2 — one summary", () => {
|
||||
let Portfolio;
|
||||
|
||||
beforeEach(async () => {
|
||||
pushRef.push = vi.fn();
|
||||
setMockSummaries([summaryA()]);
|
||||
const mod = await import("@/app/page.jsx");
|
||||
Portfolio = mod.default;
|
||||
});
|
||||
|
||||
it("renders exactly one Investigation card", async () => {
|
||||
render(React.createElement(Portfolio));
|
||||
const section = await screen.findByRole("heading", { name: /investigations/i, level: 2 });
|
||||
// Card renders as a div with border within the section — verify via scenario text
|
||||
expect(screen.getByText(/Scenario A/)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/Scenario B/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Continue investigation link for the card", async () => {
|
||||
render(React.createElement(Portfolio));
|
||||
expect(await screen.findByText(/Continue investigation/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Continue links to /investigations/{own-id}", async () => {
|
||||
pushRef.push = vi.fn();
|
||||
render(React.createElement(Portfolio));
|
||||
const continueLink = await screen.findByRole("link", { name: /Continue investigation/i });
|
||||
expect(continueLink).toHaveAttribute("href", "/investigations/inv-a");
|
||||
});
|
||||
|
||||
it("renders View report link when reportExists is true", async () => {
|
||||
render(React.createElement(Portfolio));
|
||||
expect(await screen.findByText(/View report/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("View report links to /investigations/{own-id}/report", async () => {
|
||||
pushRef.push = vi.fn();
|
||||
render(React.createElement(Portfolio));
|
||||
const viewLink = await screen.findByRole("link", { name: /View report/i });
|
||||
expect(viewLink).toHaveAttribute("href", "/investigations/inv-a/report");
|
||||
});
|
||||
|
||||
it('shows "Current" when revisions match', async () => {
|
||||
render(React.createElement(Portfolio));
|
||||
expect(await screen.findByText(/Current/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// C — two summaries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("v0.60g2 — two summaries", () => {
|
||||
let Portfolio;
|
||||
|
||||
beforeEach(async () => {
|
||||
pushRef.push = vi.fn();
|
||||
setMockSummaries([summaryA(), summaryB()]);
|
||||
const mod = await import("@/app/page.jsx");
|
||||
Portfolio = mod.default;
|
||||
});
|
||||
|
||||
it("renders both scenarios", async () => {
|
||||
render(React.createElement(Portfolio));
|
||||
expect(screen.getByText(/Scenario A/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Scenario B/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders two distinct Continue investigation links", async () => {
|
||||
render(React.createElement(Portfolio));
|
||||
const continueLinks = screen.getAllByText(/Continue investigation/i);
|
||||
expect(continueLinks).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("first Continue href contains inv-a", async () => {
|
||||
render(React.createElement(Portfolio));
|
||||
const links = document.querySelectorAll('a[href]');
|
||||
const continueHrefs = Array.from(links)
|
||||
.filter((l) => l.textContent.includes("Continue investigation"))
|
||||
.map((l) => l.getAttribute("href"));
|
||||
expect(continueHrefs).toContain("/investigations/inv-a");
|
||||
});
|
||||
|
||||
it("second Continue href contains inv-b", async () => {
|
||||
render(React.createElement(Portfolio));
|
||||
const links = document.querySelectorAll('a[href]');
|
||||
const continueHrefs = Array.from(links)
|
||||
.filter((l) => l.textContent.includes("Continue investigation"))
|
||||
.map((l) => l.getAttribute("href"));
|
||||
expect(continueHrefs).toContain("/investigations/inv-b");
|
||||
});
|
||||
|
||||
it("preserves listing order (A before B)", async () => {
|
||||
render(React.createElement(Portfolio));
|
||||
const cards = document.querySelectorAll('[class*="bg-gradient-to-b"]');
|
||||
expect(cards).toHaveLength(2);
|
||||
expect(cards[0]).toContainElement(screen.getByText(/Scenario A/));
|
||||
expect(cards[1]).toContainElement(screen.getByText(/Scenario B/));
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// D — per-card Report link
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("v0.60g2 — per-card Report visibility", () => {
|
||||
let Portfolio;
|
||||
|
||||
beforeEach(async () => {
|
||||
pushRef.push = vi.fn();
|
||||
setMockSummaries([summaryNoReport()]);
|
||||
const mod = await import("@/app/page.jsx");
|
||||
Portfolio = mod.default;
|
||||
});
|
||||
|
||||
it("does not show View report when reportExists is false", async () => {
|
||||
render(React.createElement(Portfolio));
|
||||
expect(screen.queryByText(/View report/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('still shows Continue investigation when no report', async () => {
|
||||
render(React.createElement(Portfolio));
|
||||
expect(await screen.findByText(/Continue investigation/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show freshness label when no report", async () => {
|
||||
render(React.createElement(Portfolio));
|
||||
expect(screen.queryByText(/Current/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/Update available/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// E — per-card freshness
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("v0.60g2 — per-card freshness", () => {
|
||||
let Portfolio;
|
||||
|
||||
it('shows "Current" when revisions match', async () => {
|
||||
setMockSummaries([summaryA()]);
|
||||
const mod = await import("@/app/page.jsx");
|
||||
Portfolio = mod.default;
|
||||
render(React.createElement(Portfolio));
|
||||
expect(await screen.findByText(/Current/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Update available" when revisions differ', async () => {
|
||||
setMockSummaries([summaryB()]);
|
||||
const mod = await import("@/app/page.jsx");
|
||||
Portfolio = mod.default;
|
||||
render(React.createElement(Portfolio));
|
||||
expect(await screen.findByText(/Update available/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows neither when reportExists is false", async () => {
|
||||
setMockSummaries([summaryNoReport()]);
|
||||
const mod = await import("@/app/page.jsx");
|
||||
Portfolio = mod.default;
|
||||
render(React.createElement(Portfolio));
|
||||
expect(screen.queryByText(/Current/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/Update available/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("per-card labels are independent", async () => {
|
||||
setMockSummaries([summaryA(), summaryB()]);
|
||||
const mod = await import("@/app/page.jsx");
|
||||
Portfolio = mod.default;
|
||||
render(React.createElement(Portfolio));
|
||||
expect(screen.getByText(/Current/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Update available/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// F — no singleton routing (case-1 must never appear in persisted card hrefs)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("v0.60g2 — no singleton routing", () => {
|
||||
let Portfolio;
|
||||
|
||||
beforeEach(async () => {
|
||||
pushRef.push = vi.fn();
|
||||
setMockSummaries([summaryA(), summaryB()]);
|
||||
const mod = await import("@/app/page.jsx");
|
||||
Portfolio = mod.default;
|
||||
});
|
||||
|
||||
it("no persisted card href contains case-1", async () => {
|
||||
render(React.createElement(Portfolio));
|
||||
const links = document.querySelectorAll('a[href]');
|
||||
const allHrefs = Array.from(links).map((l) => l.getAttribute("href"));
|
||||
for (const href of allHrefs) {
|
||||
expect(href).not.toContain("case-1");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// G — Create New regression
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("v0.60g2 — Create New regression", () => {
|
||||
let Portfolio;
|
||||
let cryptoRandomUUID;
|
||||
|
||||
beforeEach(async () => {
|
||||
cryptoRandomUUID = vi.fn();
|
||||
pushRef.push = vi.fn();
|
||||
setMockSummaries([]);
|
||||
Object.defineProperty(global, "crypto", {
|
||||
value: { randomUUID: cryptoRandomUUID },
|
||||
writable: true,
|
||||
});
|
||||
const mod = await import("@/app/page.jsx");
|
||||
Portfolio = mod.default;
|
||||
});
|
||||
|
||||
it("Create New remains visible even with summaries", async () => {
|
||||
setMockSummaries([summaryA()]);
|
||||
const mod2 = await import("@/app/page.jsx");
|
||||
Portfolio = mod2.default;
|
||||
render(React.createElement(Portfolio));
|
||||
expect(await screen.findByText(/Create new investigation/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Create New allocates fixed UUID and navigates", async () => {
|
||||
setMockSummaries([]);
|
||||
cryptoRandomUUID.mockReturnValue("11111111-2222-4333-8444-555555555555");
|
||||
const mod2 = await import("@/app/page.jsx");
|
||||
Portfolio = mod2.default;
|
||||
render(React.createElement(Portfolio));
|
||||
|
||||
const createBtn = await screen.findByRole("button", { name: /Create new investigation/i });
|
||||
fireEvent.click(createBtn);
|
||||
|
||||
expect(cryptoRandomUUID).toHaveBeenCalledTimes(1);
|
||||
expect(pushRef.push).toHaveBeenCalledWith("/investigations/11111111-2222-4333-8444-555555555555");
|
||||
});
|
||||
|
||||
it("Create New does not invoke any storage save", async () => {
|
||||
setMockSummaries([]);
|
||||
cryptoRandomUUID.mockReturnValue("aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee");
|
||||
const mod2 = await import("@/app/page.jsx");
|
||||
Portfolio = mod2.default;
|
||||
|
||||
// Replace clearInvestigation mock to also detect any listInvestigations call count change
|
||||
let originalListLength = mockList.length;
|
||||
|
||||
render(React.createElement(Portfolio));
|
||||
|
||||
const createBtn = await screen.findByRole("button", { name: /Create new investigation/i });
|
||||
fireEvent.click(createBtn);
|
||||
|
||||
// The mock list should not have changed (no save happens)
|
||||
expect(mockList).toHaveLength(originalListLength);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user