From a9d2d46682096110798cebf6e90f3556f0ee896b Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Sun, 26 Apr 2026 14:17:35 -0700 Subject: [PATCH] test(canvas): unit tests for useTemplateDeploy (#2071) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Molecule-Platform-Evolvement-Manager] Closes the first item from #2071 (Canvas test gaps follow-up): adds behavioural coverage for the shared template-deploy hook that both TemplatePalette (sidebar) and EmptyState (welcome grid) drive. 10 cases across 4 buckets: **Happy path (4):** - preflight ok → POST /workspaces → onDeployed fires with new id - caller-supplied canvasCoords flows into the POST body - default coords fall in [100,500) × [100,400) when canvasCoords omitted - template.runtime is preferred over the resolveRuntime fallback (locks the deduped-fallback table contract added in #2061) **Preflight failures (2):** - network throw sets error AND clears `deploying` (regression test for the "stranded button" bug called out in the SUT's inline comment — drop the try block and you'll fail this test) - not-ok-with-missing-keys opens the modal without firing POST **Modal lifecycle (2):** - 'keys added' click retries POST without re-running preflight (verifies the executeDeploy / deploy split — preflight call count stays at 1, POST count goes to 1) - 'cancel' click closes modal without firing POST **POST failures (2):** - Error rejection surfaces the message - non-Error rejection surfaces the "Deploy failed" fallback Mocks `@/lib/api`, `@/lib/deploy-preflight`, and `@/components/MissingKeysModal` (stand-in component exposes the two callbacks as test-id buttons — the real radix modal is irrelevant to this hook's behavior). Test file follows the `vi.hoisted` + import-after-mocks pattern from `canvas/src/app/__tests__/orgs-page.test.tsx`. ## Test plan - [x] All 10 cases pass locally (`vitest run useTemplateDeploy.test.tsx`) - [x] No changes to the SUT — pure additive coverage - [ ] CI green Follow-ups for the rest of #2071 (separate PRs): - A2AEdge rendering + click-to-select-source - OrgCancelButton cancel flow + optimistic state 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../__tests__/useTemplateDeploy.test.tsx | 316 ++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 canvas/src/hooks/__tests__/useTemplateDeploy.test.tsx diff --git a/canvas/src/hooks/__tests__/useTemplateDeploy.test.tsx b/canvas/src/hooks/__tests__/useTemplateDeploy.test.tsx new file mode 100644 index 00000000..6dac5bbb --- /dev/null +++ b/canvas/src/hooks/__tests__/useTemplateDeploy.test.tsx @@ -0,0 +1,316 @@ +// @vitest-environment jsdom +/** + * Tests for useTemplateDeploy — the shared preflight + POST + modal + * hook used by TemplatePalette (sidebar) and EmptyState (welcome grid). + * + * Behavioural coverage for the three flows the hook owns: + * 1. Happy path: preflight ok → POST /workspaces → onDeployed fires + * 2. Preflight errors: network throw vs not-ok-with-missing-keys + * (different code paths — the throw must NOT strand `deploying`, + * see the inline comment in the SUT for the prior bug) + * 3. Modal lifecycle: keys-added retries POST without re-running + * preflight; cancel closes without POST + * + * Issue: #2071 (Canvas test gaps follow-up). + */ +import { + describe, + it, + expect, + vi, + beforeEach, + afterEach, + type Mock, +} from "vitest"; +import { act, render, cleanup, screen, fireEvent } from "@testing-library/react"; +import { renderHook } from "@testing-library/react"; +import type { Template } from "@/lib/deploy-preflight"; + +// ── Hoisted mocks ──────────────────────────────────────────────────────────── +const { mockApiPost, mockCheckDeploySecrets, mockResolveRuntime } = vi.hoisted( + () => ({ + mockApiPost: vi.fn(), + mockCheckDeploySecrets: vi.fn(), + mockResolveRuntime: vi.fn(), + }), +); + +vi.mock("@/lib/api", () => ({ + api: { post: mockApiPost }, +})); + +vi.mock("@/lib/deploy-preflight", async () => { + // Re-export the real types; only swap the runtime functions. + const actual = await vi.importActual< + typeof import("@/lib/deploy-preflight") + >("@/lib/deploy-preflight"); + return { + ...actual, + checkDeploySecrets: mockCheckDeploySecrets, + resolveRuntime: mockResolveRuntime, + }; +}); + +// MissingKeysModal: render a minimal stand-in that exposes the two +// callbacks the hook wires up. The real modal pulls in radix + the +// secrets store, neither of which is relevant to this hook's behavior. +vi.mock("@/components/MissingKeysModal", () => ({ + MissingKeysModal: (props: { + open: boolean; + onKeysAdded: () => void; + onCancel: () => void; + }) => + props.open ? ( +
+ + +
+ ) : null, +})); + +// Import the hook AFTER the mocks are declared. +import { useTemplateDeploy } from "../useTemplateDeploy"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +function makeTemplate(over: Partial