233 lines
8.7 KiB
React
233 lines
8.7 KiB
React
/**
|
|
* Dedicated UI contract tests for v0.60j — restartInvestigation caller behaviour.
|
|
*
|
|
* Deterministic via static file-level vi.mock — no localStorage, no provider imports,
|
|
* no vi.resetModules, no module-cache manipulation, no timing sleeps.
|
|
*/
|
|
|
|
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.
|
|
// NOTE: All exports must be present because ScenarioForm imports multiple.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
let mockList = [];
|
|
let mockRestartTarget = null;
|
|
let mockLoadResult = null;
|
|
let mockClearTarget = null;
|
|
let mockLastSavedSnapshot = null;
|
|
|
|
vi.mock("@/lib/storage/investigation-storage", () => ({
|
|
listInvestigations: () => [...mockList],
|
|
restartInvestigation: (id) => {
|
|
mockRestartTarget = id;
|
|
},
|
|
clearInvestigation: (id) => {
|
|
mockClearTarget = id;
|
|
},
|
|
loadInvestigation: (id) => mockLoadResult,
|
|
saveInvestigation: (snapshot) => {
|
|
mockLastSavedSnapshot = snapshot;
|
|
},
|
|
}));
|
|
|
|
function setMockSummaries(snapshots) {
|
|
mockList = snapshots ?? [];
|
|
}
|
|
|
|
function setMockLoadResult(snap) {
|
|
mockLoadResult = snap;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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,
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// A — Portfolio restart uses restartInvestigation (not clearInvestigation)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("v0.60j — Portfolio Restart calls restartInvestigation", () => {
|
|
let Portfolio;
|
|
|
|
beforeEach(async () => {
|
|
pushRef.push = vi.fn();
|
|
mockRestartTarget = null;
|
|
mockClearTarget = null;
|
|
setMockSummaries([summaryA(), summaryB()]);
|
|
const mod = await import("@/app/page.jsx");
|
|
Portfolio = mod.default;
|
|
});
|
|
|
|
it("restart confirmation calls restartInvestigation with the correct id (inv-a)", async () => {
|
|
render(React.createElement(Portfolio));
|
|
|
|
// Scope to the first card's Restart button (scenario A)
|
|
const cards = document.querySelectorAll('[class*="border-teal-300"]');
|
|
expect(cards).toHaveLength(2);
|
|
const firstCardRestartBtn = within(cards[0]).getByRole("button", { name: /Restart investigation/i });
|
|
fireEvent.click(firstCardRestartBtn);
|
|
|
|
// Confirmation dialog appears — scoped to the aria-labelledby title for inv-a
|
|
expect(await screen.findByRole("heading", { name: /Restart this investigation\?/i })).toBeInTheDocument();
|
|
|
|
// Click the confirm button INSIDE the dialog (scoped), not the card's restart buttons
|
|
const dialog = document.querySelector('[role="dialog"]');
|
|
const confirmBtn = within(dialog).getByRole("button", { name: "Restart investigation" });
|
|
fireEvent.click(confirmBtn);
|
|
|
|
await waitFor(() => expect(mockRestartTarget).toBe("inv-a"));
|
|
});
|
|
|
|
it("restart does NOT call clearInvestigation", async () => {
|
|
render(React.createElement(Portfolio));
|
|
|
|
const cards = document.querySelectorAll('[class*="border-teal-300"]');
|
|
const firstCardRestartBtn = within(cards[0]).getByRole("button", { name: /Restart investigation/i });
|
|
fireEvent.click(firstCardRestartBtn);
|
|
|
|
await screen.findByRole("heading", { name: /Restart this investigation\?/i });
|
|
const dialog = document.querySelector('[role="dialog"]');
|
|
const confirmBtn = within(dialog).getByRole("button", { name: "Restart investigation" });
|
|
fireEvent.click(confirmBtn);
|
|
|
|
await waitFor(() => expect(mockClearTarget).toBeNull());
|
|
});
|
|
|
|
it("restart of inv-b passes inv-b as id", async () => {
|
|
render(React.createElement(Portfolio));
|
|
|
|
const cards = document.querySelectorAll('[class*="border-teal-300"]');
|
|
// Second card (scenario B) — index 1
|
|
const secondCardRestartBtn = within(cards[1]).getByRole("button", { name: /Restart investigation/i });
|
|
fireEvent.click(secondCardRestartBtn);
|
|
|
|
await screen.findByRole("heading", { name: /Restart this investigation\?/i });
|
|
|
|
const dialog = document.querySelector('[role="dialog"]');
|
|
const confirmBtn = within(dialog).getByRole("button", { name: "Restart investigation" });
|
|
fireEvent.click(confirmBtn);
|
|
|
|
await waitFor(() => expect(mockRestartTarget).toBe("inv-b"));
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// B — ScenarioForm restart path calls restartInvestigation with id
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("v0.60j — ScenarioForm restart calls restartInvestigation(investigationId)", () => {
|
|
let ScenarioForm;
|
|
|
|
beforeEach(async () => {
|
|
pushRef.push = vi.fn();
|
|
mockRestartTarget = null;
|
|
setMockLoadResult(null); // No persisted session on mount
|
|
const mod = await import("@/components/scenario-form.jsx");
|
|
ScenarioForm = mod.default;
|
|
});
|
|
|
|
it("ScenarioForm mounts without error under mocked storage", async () => {
|
|
const testId = "inv-scenarioform-mock-test";
|
|
expect(() => {
|
|
render(React.createElement(ScenarioForm, { investigationId: testId }));
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it("restartInvestigation is importable from storage layer", async () => {
|
|
const m = await import("@/lib/storage/investigation-storage.js");
|
|
expect(typeof m.restartInvestigation).toBe("function");
|
|
});
|
|
|
|
// ── Directly observable: ContinueLaterBanner restart ──────────────────────
|
|
|
|
it("ContinueLaterBanner Restart calls restartInvestigation(\"inv-restart-a\")", async () => {
|
|
mockRestartTarget = null;
|
|
setMockLoadResult({
|
|
id: "inv-restart-a",
|
|
scenario: "Test ContinueLaterBanner restart scenario",
|
|
situationGraph: { nodes: [], edges: [] }, // hasGraph → status promoted to "success"
|
|
updatedAt: "2026-09-03T10:00:00.000Z",
|
|
});
|
|
|
|
const mod = await import("@/components/scenario-form.jsx");
|
|
ScenarioForm = mod.default;
|
|
|
|
render(React.createElement(ScenarioForm, { investigationId: "inv-restart-a" }));
|
|
|
|
// ContinueLaterBanner renders when status === "success" && result?.updatedAt exists
|
|
expect(await screen.findByText("Restart investigation")).toBeInTheDocument();
|
|
fireEvent.click(screen.getByText("Restart investigation"));
|
|
|
|
await waitFor(() => expect(mockRestartTarget).toBe("inv-restart-a"));
|
|
});
|
|
|
|
// ── Directly observable: "Start new investigation" reset control ───────────
|
|
|
|
it("'Start new investigation' button calls restartInvestigation(\"inv-restart-a\")", async () => {
|
|
mockRestartTarget = null;
|
|
setMockLoadResult({
|
|
id: "inv-restart-a",
|
|
scenario: "Test StartNew reset scenario",
|
|
situationGraph: { nodes: [], edges: [] }, // hasGraph → status promoted to "success"
|
|
updatedAt: "2026-09-03T10:00:00.000Z",
|
|
});
|
|
|
|
const mod = await import("@/components/scenario-form.jsx");
|
|
ScenarioForm = mod.default;
|
|
|
|
render(React.createElement(ScenarioForm, { investigationId: "inv-restart-a" }));
|
|
|
|
expect(await screen.findByText("Start new investigation")).toBeInTheDocument();
|
|
fireEvent.click(screen.getByText("Start new investigation"));
|
|
|
|
await waitFor(() => expect(mockRestartTarget).toBe("inv-restart-a"));
|
|
});
|
|
|
|
// ── Source-inspected-only: ReasoningWorkspace onRestart path ──────────────
|
|
// PATH 1 NOT DIRECTLY OBSERVABLE IN BOUNDED APPARATUS
|
|
// ProviderUnavailable / MalformedResponse / UnexpectedState cards require
|
|
// error-state reasoning data — not fabricatable in this bounded apparatus.
|
|
//
|
|
// Source confirmation (components/scenario-form.jsx line ~949):
|
|
// onRestart={() => { restartInvestigation(investigationId); ... }}
|
|
// This passes investigationId as the first argument to restartInvestigation.
|
|
});
|