import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { RoutineCard } from "../RoutineCard"; import type { Routine, RoutineExecutionResult, RoutineTriggerType } from "@fusion/core"; // Mock lucide-react vi.mock("lucide-react", () => ({ Plus: () => +, Play: () => β–Ά, Pause: () => ⏸, Pencil: () => ✎, Trash2: () => πŸ—‘, Clock: () => πŸ•, CheckCircle: () => βœ“, XCircle: () => βœ—, ChevronDown: () => β–Ό, ChevronUp: () => β–², Calendar: () => πŸ“…, Webhook: () => πŸ”—, Code: () => πŸ’», Zap: () => ⚑, })); // Mock @fusion/core (no runtime values needed) vi.mock("@fusion/core", () => ({})); function makeRoutine(overrides: Partial = {}): Routine { return { id: "routine-001", name: "Test Routine", description: "A test routine", trigger: { type: "cron", cronExpression: "0 * * * *" }, executionPolicy: "parallel", catchUpPolicy: "skip", enabled: true, runCount: 3, runHistory: [], createdAt: "2026-04-07T00:00:00.000Z", updatedAt: "2026-04-07T00:00:00.000Z", ...overrides, }; } function makeRunResult(overrides: Partial = {}): RoutineExecutionResult { return { routineId: "routine-001", success: true, output: "Done", startedAt: "2026-04-08T00:00:00.000Z", completedAt: "2026-04-08T00:01:00.000Z", ...overrides, }; } describe("RoutineCard", () => { const onEdit = vi.fn(); const onDelete = vi.fn(); const onRun = vi.fn(); const onToggle = vi.fn(); beforeEach(() => { vi.clearAllMocks(); }); // ── Rendering ───────────────────────────────────────────────────────── describe("Rendering", () => { it("renders routine name and trigger type badge (cron)", () => { render(); expect(screen.getByText("Test Routine")).toBeDefined(); expect(screen.getByText("Cron")).toBeDefined(); }); it("renders cron expression for cron triggers", () => { render(); expect(screen.getByText("0 * * * *")).toBeDefined(); }); it("renders Webhook label for webhook triggers (no cron expression shown)", () => { const routine = makeRoutine({ trigger: { type: "webhook", webhookPath: "/trigger/test", secret: "secret123" } }); render(); expect(screen.getByText("Webhook")).toBeDefined(); expect(screen.queryByText("0 * * * *")).toBeNull(); }); it("renders API label for api triggers", () => { const routine = makeRoutine({ trigger: { type: "api", endpoint: "/api/test" } }); render(); expect(screen.getByText("API")).toBeDefined(); }); it("renders Manual label for manual triggers", () => { const routine = makeRoutine({ trigger: { type: "manual" } }); render(); expect(screen.getByText("Manual")).toBeDefined(); }); it("renders execution policy badge (parallel)", () => { render(); expect(screen.getByText("Concurrent")).toBeDefined(); }); it("renders execution policy badge (queue)", () => { render(); expect(screen.getByText("Queued")).toBeDefined(); }); it("renders catch-up policy badge (skip)", () => { render(); expect(screen.getByText("Skip missed")).toBeDefined(); }); it("renders catch-up policy badge (run_one)", () => { render(); expect(screen.getByText("Catch up (latest)")).toBeDefined(); }); it("renders next run time", () => { const futureDate = new Date(Date.now() + 3600000).toISOString(); render(); expect(screen.getByText(/Next:/)).toBeDefined(); }); it("renders last run time", () => { render(); expect(screen.getByText(/Last:/)).toBeDefined(); }); it("renders run count", () => { render(); expect(screen.getByText("Runs:")).toBeDefined(); expect(screen.getByText("5")).toBeDefined(); }); it("shows disabled styling when routine.enabled === false", () => { const { container } = render(); expect(container.querySelector(".routine-card.disabled")).toBeDefined(); }); it("shows last run result badge when lastRunResult is present (success)", () => { const routine = makeRoutine({ lastRunResult: makeRunResult({ success: true }) }); render(); expect(screen.getByText("Success")).toBeDefined(); }); it("shows last run result badge when lastRunResult is present (failure)", () => { const routine = makeRoutine({ lastRunResult: makeRunResult({ success: false, error: "Failed" }) }); render(); expect(screen.getByText("Failed")).toBeDefined(); }); it("shows description when present", () => { render(); expect(screen.getByText("My description")).toBeDefined(); }); it("hides description when not present", () => { render(); expect(screen.queryByText("A test routine")).toBeNull(); }); }); // ── Actions ─────────────────────────────────────────────────────────── describe("Actions", () => { it("clicking run button calls onRun with the routine", () => { const routine = makeRoutine(); render(); fireEvent.click(screen.getByLabelText("Run Test Routine now")); expect(onRun).toHaveBeenCalledWith(routine); }); it("disables run button when running prop is true", () => { render(); expect(screen.getByLabelText("Running…")).toBeDisabled(); }); it("clicking toggle button calls onToggle with the routine", () => { const routine = makeRoutine({ enabled: true }); render(); fireEvent.click(screen.getByLabelText("Disable Test Routine")); expect(onToggle).toHaveBeenCalledWith(routine); }); it("clicking edit button calls onEdit with the routine", () => { const routine = makeRoutine(); render(); fireEvent.click(screen.getByLabelText("Edit Test Routine")); expect(onEdit).toHaveBeenCalledWith(routine); }); it("clicking delete button shows confirm dialog and calls onDelete on confirm", async () => { const routine = makeRoutine({ name: "My Routine" }); const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true); render(); fireEvent.click(screen.getByLabelText("Delete My Routine")); await waitFor(() => { expect(confirmSpy).toHaveBeenCalled(); }); expect(onDelete).toHaveBeenCalledWith(routine); confirmSpy.mockRestore(); }); it("clicking delete button does not call onDelete when confirm is cancelled", async () => { const routine = makeRoutine({ name: "My Routine" }); const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(false); render(); fireEvent.click(screen.getByLabelText("Delete My Routine")); await waitFor(() => { expect(confirmSpy).toHaveBeenCalled(); }); expect(onDelete).not.toHaveBeenCalled(); confirmSpy.mockRestore(); }); }); // ── Run History ─────────────────────────────────────────────────────── describe("Run history", () => { it("shows run history toggle when runHistory.length > 0", () => { const routine = makeRoutine({ runHistory: [ makeRunResult({ startedAt: "2026-04-08T00:00:00.000Z", completedAt: "2026-04-08T00:01:00.000Z" }), ], }); render(); expect(screen.getByText("Run History (1)")).toBeDefined(); }); it("expands/collapses run history on toggle click", async () => { const routine = makeRoutine({ runHistory: [ makeRunResult({ startedAt: "2026-04-08T00:00:00.000Z", completedAt: "2026-04-08T00:01:00.000Z" }), ], }); render(); // Initially collapsed - no run detail expect(screen.queryByText("Done")).toBeNull(); // Click to expand history list fireEvent.click(screen.getByText("Run History (1)")); // History list is visible but items are collapsed - click item header to expand const itemHeader = screen.getByRole("button", { name: /Run #1: succeeded/i }); fireEvent.click(itemHeader); await waitFor(() => { expect(screen.getByText("Done")).toBeDefined(); }); // Click to collapse item fireEvent.click(itemHeader); await waitFor(() => { expect(screen.queryByText("Done")).toBeNull(); }); }); it("shows max 10 history items with overflow indicator", () => { const runHistory = Array.from({ length: 15 }, (_, i) => makeRunResult({ startedAt: `2026-04-0${i + 1}T00:00:00.000Z`, completedAt: `2026-04-0${i + 1}T00:01:00.000Z` }) ); const routine = makeRoutine({ runHistory }); render(); // Click to expand fireEvent.click(screen.getByText("Run History (15)")); // Should show only 10 items expect(screen.getByText("…and 5 more")).toBeDefined(); }); it("does not show history section when runHistory is empty", () => { render(); expect(screen.queryByText("Run History")).toBeNull(); }); }); });