test(canvas): cover canvas-actions restart-pending helpers (25% → 100%) (#1815)
[Molecule-Platform-Evolvement-Manager] Continues the #1815 coverage rollup. canvas-actions.ts was at 25% in the baseline run from #2147; this PR brings the file's two helpers to full coverage. 5 cases: **markAllWorkspacesNeedRestart (3):** - calls updateNodeData on every node with `{needsRestart: true}` - no-op when the canvas has zero workspaces - preserves call ordering — matters because the toolbar's Restart Pending pill observes per-node data changes incrementally; a refactor that shuffled iteration order would silently change which workspaces flash first **markWorkspaceNeedsRestart (2):** - targeted call: updateNodeData fires exactly once on the named id - defensive: regardless of how many other workspaces exist in the store, only the target workspace gets updated. Pre-this-test, a refactor that accidentally wired this function through the per-node iteration path of markAll would silently mark every workspace — pinning the cardinality here catches that. ## Mock strategy Standard pattern for canvas store: mock useCanvasStore as both the selector function AND a getState()-bearing object. updateNodeData is a vi.fn() spy so the test asserts on calls + args directly. ## Test plan - [x] All 5 cases pass locally (~132ms) - [x] No SUT changes — pure additive coverage - [ ] CI green ## #1815 progress - [x] Step 1+2: instrumentation + script (#2147) - [x] utils.ts + runtime-names.ts (#2148) - [x] canvas-actions.ts (this PR) - [ ] Remaining low-coverage targets: store/classNames.ts (17%), store/canvas.ts (73% — largest absolute gap by lines) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
34b92c33b7
commit
e5e4eb4d2a
133
canvas/src/lib/__tests__/canvas-actions.test.ts
Normal file
133
canvas/src/lib/__tests__/canvas-actions.test.ts
Normal file
@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Tests for canvas-actions — two helpers that mark workspaces as
|
||||
* "needs restart" after a global / per-workspace config change.
|
||||
*
|
||||
* Used by:
|
||||
* - markAllWorkspacesNeedRestart: triggered after a global secret
|
||||
* change so the user sees a Restart Pending pill on every node
|
||||
* - markWorkspaceNeedsRestart: per-workspace targeted after a single
|
||||
* workspace's config edit
|
||||
*
|
||||
* Both reach into the canvas store via getState(), so the tests
|
||||
* mock the store's selector + getState shape. The bug surface is
|
||||
* tiny but the consequences of regressing markAllWorkspacesNeedRestart
|
||||
* are real — silently miss the pill on global secret changes and the
|
||||
* user can't tell which workspaces need a restart.
|
||||
*
|
||||
* Issue: #1815 follow-up — canvas-actions.ts was at 25% coverage.
|
||||
*/
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
} from "vitest";
|
||||
|
||||
// ── Hoisted mocks ────────────────────────────────────────────────────────────
|
||||
|
||||
const { mockState } = vi.hoisted(() => ({
|
||||
mockState: {
|
||||
nodes: [] as Array<{ id: string }>,
|
||||
updateNodeData: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/store/canvas", () => ({
|
||||
useCanvasStore: Object.assign(
|
||||
(selector: (s: typeof mockState) => unknown) => selector(mockState),
|
||||
{ getState: () => mockState },
|
||||
),
|
||||
}));
|
||||
|
||||
// Import the SUT after the mocks are declared.
|
||||
import {
|
||||
markAllWorkspacesNeedRestart,
|
||||
markWorkspaceNeedsRestart,
|
||||
} from "../canvas-actions";
|
||||
|
||||
beforeEach(() => {
|
||||
mockState.updateNodeData.mockReset();
|
||||
mockState.nodes = [];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
// ── markAllWorkspacesNeedRestart ─────────────────────────────────────────────
|
||||
|
||||
describe("markAllWorkspacesNeedRestart", () => {
|
||||
it("calls updateNodeData on every node with needsRestart: true", () => {
|
||||
mockState.nodes = [
|
||||
{ id: "ws-a" },
|
||||
{ id: "ws-b" },
|
||||
{ id: "ws-c" },
|
||||
];
|
||||
|
||||
markAllWorkspacesNeedRestart();
|
||||
|
||||
expect(mockState.updateNodeData).toHaveBeenCalledTimes(3);
|
||||
expect(mockState.updateNodeData).toHaveBeenCalledWith("ws-a", { needsRestart: true });
|
||||
expect(mockState.updateNodeData).toHaveBeenCalledWith("ws-b", { needsRestart: true });
|
||||
expect(mockState.updateNodeData).toHaveBeenCalledWith("ws-c", { needsRestart: true });
|
||||
});
|
||||
|
||||
it("is a no-op when the canvas has no workspaces", () => {
|
||||
mockState.nodes = [];
|
||||
|
||||
markAllWorkspacesNeedRestart();
|
||||
|
||||
expect(mockState.updateNodeData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves call ordering for deterministic UI updates", () => {
|
||||
// Pinning the iteration order so a future refactor (e.g. switching
|
||||
// to forEach with shuffled keys, or adding async batching) doesn't
|
||||
// silently change the order updates fire — matters when the toolbar
|
||||
// observes per-node data changes incrementally.
|
||||
mockState.nodes = [
|
||||
{ id: "ws-1" },
|
||||
{ id: "ws-2" },
|
||||
{ id: "ws-3" },
|
||||
];
|
||||
|
||||
markAllWorkspacesNeedRestart();
|
||||
|
||||
const callOrder = mockState.updateNodeData.mock.calls.map((c) => c[0]);
|
||||
expect(callOrder).toEqual(["ws-1", "ws-2", "ws-3"]);
|
||||
});
|
||||
});
|
||||
|
||||
// ── markWorkspaceNeedsRestart ────────────────────────────────────────────────
|
||||
|
||||
describe("markWorkspaceNeedsRestart", () => {
|
||||
it("calls updateNodeData on the named workspace only", () => {
|
||||
markWorkspaceNeedsRestart("ws-target");
|
||||
|
||||
expect(mockState.updateNodeData).toHaveBeenCalledTimes(1);
|
||||
expect(mockState.updateNodeData).toHaveBeenCalledWith("ws-target", {
|
||||
needsRestart: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not enumerate the nodes list (purely targeted)", () => {
|
||||
// Defensive: if a future refactor accidentally wired this function
|
||||
// through the per-node iteration path of markAll, every workspace
|
||||
// would be marked. Pin that the function fires exactly ONCE
|
||||
// regardless of how many nodes are in the store.
|
||||
mockState.nodes = [
|
||||
{ id: "ws-other-1" },
|
||||
{ id: "ws-other-2" },
|
||||
{ id: "ws-target" },
|
||||
];
|
||||
|
||||
markWorkspaceNeedsRestart("ws-target");
|
||||
|
||||
expect(mockState.updateNodeData).toHaveBeenCalledTimes(1);
|
||||
expect(mockState.updateNodeData).toHaveBeenCalledWith("ws-target", {
|
||||
needsRestart: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user