/** * Tests for useActions hook. * * TDD: These tests define the expected behavior before implementation. */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { renderHook, act } from "@testing-library/react"; import { useActions } from "@/hooks/useActions"; // Mock Tauri shell plugin const mockOpen = vi.fn(); vi.mock("@tauri-apps/plugin-shell", () => ({ open: (...args: unknown[]) => mockOpen(...args), })); // Mock Tauri invoke const mockInvoke = vi.fn(); vi.mock("@tauri-apps/api/core", () => ({ invoke: (...args: unknown[]) => mockInvoke(...args), })); // Mock the focus store const mockLogDecision = vi.fn(); const mockUpdateItem = vi.fn(); const mockAct = vi.fn(); vi.mock("@/stores/focus-store", () => ({ useFocusStore: () => ({ logDecision: mockLogDecision, updateItem: mockUpdateItem, act: mockAct, }), })); describe("useActions", () => { beforeEach(() => { vi.clearAllMocks(); mockInvoke.mockResolvedValue(undefined); mockOpen.mockResolvedValue(undefined); }); afterEach(() => { vi.useRealTimers(); }); describe("start", () => { it("opens URL in browser", async () => { const { result } = renderHook(() => useActions()); await act(async () => { await result.current.start({ id: "br-x7f", url: "https://gitlab.com/platform/core/-/merge_requests/847", title: "Test MR", }); }); expect(mockOpen).toHaveBeenCalledWith( "https://gitlab.com/platform/core/-/merge_requests/847" ); }); it("does not open URL if none provided", async () => { const { result } = renderHook(() => useActions()); await act(async () => { await result.current.start({ id: "br-x7f", title: "Manual task", }); }); expect(mockOpen).not.toHaveBeenCalled(); }); it("logs decision via Tauri", async () => { const { result } = renderHook(() => useActions()); await act(async () => { await result.current.start({ id: "br-x7f", title: "Test", }); }); expect(mockInvoke).toHaveBeenCalledWith( "log_decision", expect.objectContaining({ entry: expect.objectContaining({ action: "start", bead_id: "br-x7f", }), }) ); }); }); describe("defer", () => { it("calculates correct snooze time for 1h", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-02-25T10:00:00Z")); const { result } = renderHook(() => useActions()); await act(async () => { await result.current.defer( { id: "br-x7f", title: "Test" }, "1h", "Need more time" ); }); expect(mockInvoke).toHaveBeenCalledWith( "update_item", expect.objectContaining({ id: "br-x7f", updates: expect.objectContaining({ snoozed_until: "2026-02-25T11:00:00.000Z", }), }) ); }); it("calculates correct snooze time for 3h", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-02-25T10:00:00Z")); const { result } = renderHook(() => useActions()); await act(async () => { await result.current.defer( { id: "br-x7f", title: "Test" }, "3h", null ); }); expect(mockInvoke).toHaveBeenCalledWith( "update_item", expect.objectContaining({ id: "br-x7f", updates: expect.objectContaining({ snoozed_until: "2026-02-25T13:00:00.000Z", }), }) ); }); it("defer tomorrow uses 9am next day", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-02-25T22:00:00Z")); // 10pm const { result } = renderHook(() => useActions()); await act(async () => { await result.current.defer( { id: "br-x7f", title: "Test" }, "tomorrow", null ); }); // Should be 9am on Feb 26 expect(mockInvoke).toHaveBeenCalledWith( "update_item", expect.objectContaining({ id: "br-x7f", updates: expect.objectContaining({ snoozed_until: expect.stringContaining("2026-02-26T09:00:00"), }), }) ); }); it("logs decision with reason", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-02-25T10:00:00Z")); const { result } = renderHook(() => useActions()); await act(async () => { await result.current.defer( { id: "br-x7f", title: "Test" }, "1h", "In a meeting" ); }); expect(mockInvoke).toHaveBeenCalledWith( "log_decision", expect.objectContaining({ entry: expect.objectContaining({ action: "defer", bead_id: "br-x7f", reason: "In a meeting", }), }) ); }); it("advances to next item in queue", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-02-25T10:00:00Z")); const { result } = renderHook(() => useActions()); await act(async () => { await result.current.defer( { id: "br-x7f", title: "Test" }, "1h", null ); }); expect(mockAct).toHaveBeenCalledWith("defer_1h", undefined); }); }); describe("skip", () => { it("marks item as skipped for today", async () => { const { result } = renderHook(() => useActions()); await act(async () => { await result.current.skip( { id: "br-x7f", title: "Test" }, "Not urgent" ); }); expect(mockInvoke).toHaveBeenCalledWith( "update_item", expect.objectContaining({ id: "br-x7f", updates: expect.objectContaining({ skipped_today: true, }), }) ); }); it("logs decision with reason", async () => { const { result } = renderHook(() => useActions()); await act(async () => { await result.current.skip( { id: "br-x7f", title: "Test" }, "Low priority" ); }); expect(mockInvoke).toHaveBeenCalledWith( "log_decision", expect.objectContaining({ entry: expect.objectContaining({ action: "skip", bead_id: "br-x7f", reason: "Low priority", }), }) ); }); it("advances to next item in queue", async () => { const { result } = renderHook(() => useActions()); await act(async () => { await result.current.skip({ id: "br-x7f", title: "Test" }, null); }); expect(mockAct).toHaveBeenCalledWith("skip", undefined); }); }); describe("complete", () => { it("closes bead via Tauri", async () => { const { result } = renderHook(() => useActions()); await act(async () => { await result.current.complete( { id: "br-x7f", title: "Test" }, "Fixed the bug" ); }); expect(mockInvoke).toHaveBeenCalledWith( "close_bead", expect.objectContaining({ bead_id: "br-x7f", reason: "Fixed the bug", }) ); }); it("logs decision", async () => { const { result } = renderHook(() => useActions()); await act(async () => { await result.current.complete( { id: "br-x7f", title: "Test" }, "Done" ); }); expect(mockInvoke).toHaveBeenCalledWith( "log_decision", expect.objectContaining({ entry: expect.objectContaining({ action: "complete", bead_id: "br-x7f", reason: "Done", }), }) ); }); it("advances to next item in queue", async () => { const { result } = renderHook(() => useActions()); await act(async () => { await result.current.complete( { id: "br-x7f", title: "Test" }, null ); }); expect(mockAct).toHaveBeenCalled(); }); }); });