import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import { ScheduleCard } from "../ScheduleCard"; import type { ScheduledTask, AutomationRunResult } from "@fusion/core"; // Mock lucide-react vi.mock("lucide-react", () => ({ Play: () => , Pause: () => , Pencil: () => , Trash2: () => 🗑, Clock: () => 🕐, CheckCircle: () => , XCircle: () => , ChevronDown: () => , ChevronUp: () => , Layers: () => , })); function makeResult(overrides: Partial = {}): AutomationRunResult { return { success: true, output: "hello world", startedAt: "2026-01-01T00:00:00.000Z", completedAt: "2026-01-01T00:00:05.000Z", ...overrides, }; } function makeSchedule(overrides: Partial = {}): ScheduledTask { return { id: "sched-1", name: "Update Dependencies", description: "Run npm update weekly", scheduleType: "weekly", cronExpression: "0 0 * * 1", command: "npm update", enabled: true, runCount: 5, runHistory: [], createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", ...overrides, }; } describe("ScheduleCard", () => { const onEdit = vi.fn(); const onDelete = vi.fn(); const onRun = vi.fn(); const onToggle = vi.fn(); beforeEach(() => { vi.clearAllMocks(); }); it("displays schedule name", () => { render( ); expect(screen.getByText("Update Dependencies")).toBeDefined(); }); it("displays schedule description", () => { render( ); expect(screen.getByText("Run npm update weekly")).toBeDefined(); }); it("displays schedule type badge", () => { render( ); expect(screen.getByText("weekly")).toBeDefined(); }); it("displays cron expression", () => { render( ); expect(screen.getByText("0 0 * * 1")).toBeDefined(); }); it("displays run count", () => { render( ); expect(screen.getByText("42")).toBeDefined(); }); it("applies disabled class when schedule is disabled", () => { const { container } = render( ); expect(container.querySelector(".schedule-card.disabled")).not.toBeNull(); }); it("does not apply disabled class when schedule is enabled", () => { const { container } = render( ); expect(container.querySelector(".schedule-card.disabled")).toBeNull(); }); describe("last run result", () => { it("shows success badge for successful last run", () => { const schedule = makeSchedule({ lastRunResult: makeResult({ success: true }) }); render( ); expect(screen.getByText("Success")).toBeDefined(); }); it("shows failure badge for failed last run", () => { const schedule = makeSchedule({ lastRunResult: makeResult({ success: false }) }); render( ); expect(screen.getByText("Failed")).toBeDefined(); }); }); describe("action buttons", () => { it("calls onRun when run button is clicked", () => { const schedule = makeSchedule(); render( ); fireEvent.click(screen.getByLabelText(`Run ${schedule.name} now`)); expect(onRun).toHaveBeenCalledWith(schedule); }); it("disables run button when running", () => { const schedule = makeSchedule(); render( ); const btn = screen.getByLabelText("Running…"); expect(btn.hasAttribute("disabled")).toBe(true); }); it("calls onToggle when toggle button is clicked", () => { const schedule = makeSchedule(); render( ); fireEvent.click(screen.getByLabelText(`Disable ${schedule.name}`)); expect(onToggle).toHaveBeenCalledWith(schedule); }); it("calls onEdit when edit button is clicked", () => { const schedule = makeSchedule(); render( ); fireEvent.click(screen.getByLabelText(`Edit ${schedule.name}`)); expect(onEdit).toHaveBeenCalledWith(schedule); }); it("calls onDelete after confirm when delete button is clicked", () => { const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true); const schedule = makeSchedule(); render( ); fireEvent.click(screen.getByLabelText(`Delete ${schedule.name}`)); expect(confirmSpy).toHaveBeenCalledWith(expect.stringContaining("Update Dependencies")); expect(onDelete).toHaveBeenCalledWith(schedule); confirmSpy.mockRestore(); }); it("does not call onDelete when confirm is cancelled", () => { const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(false); const schedule = makeSchedule(); render( ); fireEvent.click(screen.getByLabelText(`Delete ${schedule.name}`)); expect(onDelete).not.toHaveBeenCalled(); confirmSpy.mockRestore(); }); }); describe("run history", () => { it("does not show history toggle when no history", () => { render( ); expect(screen.queryByText(/Run History/)).toBeNull(); }); it("shows history toggle when history exists", () => { const history = [makeResult()]; render( ); expect(screen.getByText("Run History (1)")).toBeDefined(); }); it("expands history on toggle click", () => { const history = [makeResult({ output: "test output" })]; render( ); fireEvent.click(screen.getByText("Run History (1)")); // History items should now be visible expect(screen.getByText(/just now|ago/)).toBeDefined(); }); }); describe("multi-step schedules", () => { it("shows step count badge when schedule has steps", () => { const schedule = makeSchedule({ steps: [ { id: "s1", type: "command", name: "Build", command: "npm run build" }, { id: "s2", type: "ai-prompt", name: "Review", prompt: "Review code" }, ], }); render( ); expect(screen.getByText("2 steps")).toBeDefined(); }); it("shows singular 'step' for single step", () => { const schedule = makeSchedule({ steps: [{ id: "s1", type: "command", name: "Build", command: "npm run build" }], }); render( ); expect(screen.getByText("1 step")).toBeDefined(); }); it("shows command preview for legacy schedules without steps", () => { const schedule = makeSchedule({ command: "npm update" }); const { container } = render( ); expect(container.querySelector(".schedule-command-preview")?.textContent).toBe("npm update"); }); it("does not show command preview when schedule has steps", () => { const schedule = makeSchedule({ steps: [{ id: "s1", type: "command", name: "Build", command: "npm run build" }], }); const { container } = render( ); expect(container.querySelector(".schedule-command-preview")).toBeNull(); }); it("shows step result dots in run history", () => { const history = [ makeResult({ stepResults: [ { stepId: "s1", stepName: "Build", stepIndex: 0, success: true, output: "", startedAt: "2026-01-01T00:00:00Z", completedAt: "2026-01-01T00:00:01Z" }, { stepId: "s2", stepName: "Test", stepIndex: 1, success: false, output: "", error: "Tests failed", startedAt: "2026-01-01T00:00:01Z", completedAt: "2026-01-01T00:00:02Z" }, ], }), ]; const schedule = makeSchedule({ runHistory: history }); const { container } = render( ); // Expand the history fireEvent.click(screen.getByText("Run History (1)")); const dots = container.querySelectorAll(".step-result-dot"); expect(dots).toHaveLength(2); expect(dots[0].classList.contains("success")).toBe(true); expect(dots[1].classList.contains("failure")).toBe(true); }); it("shows per-step results in expanded run history", () => { const history = [ makeResult({ stepResults: [ { stepId: "s1", stepName: "Build", stepIndex: 0, success: true, output: "build ok", startedAt: "2026-01-01T00:00:00Z", completedAt: "2026-01-01T00:00:01Z" }, { stepId: "s2", stepName: "Test", stepIndex: 1, success: false, output: "", error: "Tests failed", startedAt: "2026-01-01T00:00:01Z", completedAt: "2026-01-01T00:00:02Z" }, ], }), ]; const schedule = makeSchedule({ runHistory: history }); render( ); // Expand the history fireEvent.click(screen.getByText("Run History (1)")); // Expand the run item fireEvent.click(screen.getByRole("button", { name: /Run #1/ })); // Check per-step results expect(screen.getByText("Build")).toBeDefined(); expect(screen.getByText("Test")).toBeDefined(); expect(screen.getByText("Tests failed")).toBeDefined(); }); }); });