/** * FocusView tests -- the main focus container. * * Tests: * 1. Shows FocusCard when focus is set * 2. Shows empty state when no focus and no items * 3. Shows suggestion when no focus but items exist * 4. Auto-advances to next item after start * 5. Shows celebration on last item complete * 6. Skip/Defer/Complete trigger ReasonPrompt before action * 7. ReasonPrompt can be cancelled with Escape * 8. Confirm with reason logs decision via useActions */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { FocusView } from "@/components/FocusView"; import { useFocusStore } from "@/stores/focus-store"; import { makeFocusItem } from "../helpers/fixtures"; // Mock the shell plugin for URL opening vi.mock("@tauri-apps/plugin-shell", () => ({ open: vi.fn(() => Promise.resolve()), })); // Mock Tauri invoke -- useActions calls invoke for log_decision, update_item, close_bead const mockInvoke = vi.fn(() => Promise.resolve()); vi.mock("@tauri-apps/api/core", () => ({ invoke: (...args: unknown[]) => mockInvoke(...args), })); function renderWithProviders(ui: React.ReactElement) { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, }, }); return render( {ui} ); } describe("FocusView", () => { beforeEach(() => { localStorage.clear(); useFocusStore.setState({ current: null, queue: [], isLoading: false, error: null, }); mockInvoke.mockResolvedValue(undefined); }); afterEach(() => { vi.clearAllMocks(); }); describe("with focus set", () => { it("shows FocusCard when focus is set", () => { const item = makeFocusItem({ id: "1", title: "Test Item" }); useFocusStore.setState({ current: item, queue: [] }); renderWithProviders(); expect(screen.getByText("Test Item")).toBeInTheDocument(); expect(screen.getByRole("button", { name: /start/i })).toBeInTheDocument(); }); it("shows queue summary when items exist in queue", () => { const current = makeFocusItem({ id: "1", title: "Current" }); const queued = makeFocusItem({ id: "2", title: "Queued", type: "issue" }); useFocusStore.setState({ current, queue: [queued] }); renderWithProviders(); expect(screen.getByText(/Queue:/)).toBeInTheDocument(); expect(screen.getByText(/1 issue/)).toBeInTheDocument(); }); }); describe("empty state", () => { it("shows empty state when no focus and no items", () => { useFocusStore.setState({ current: null, queue: [] }); renderWithProviders(); expect(screen.getByText(/all clear/i)).toBeInTheDocument(); expect(screen.getByText(/nothing needs your attention/i)).toBeInTheDocument(); }); it("shows celebration message in empty state", () => { useFocusStore.setState({ current: null, queue: [] }); renderWithProviders(); expect(screen.getByText(/nice work/i)).toBeInTheDocument(); }); }); describe("suggestion state", () => { it("shows suggestion when no focus but items exist in queue", () => { const item = makeFocusItem({ id: "1", title: "Suggested Item" }); useFocusStore.setState({ current: null, queue: [item] }); renderWithProviders(); // Should show the item as a suggestion expect(screen.getByText("Suggested Item")).toBeInTheDocument(); // Should have a "Set as focus" or "Start" button expect( screen.getByRole("button", { name: /set as focus|start/i }) ).toBeInTheDocument(); }); it("promotes suggestion to focus when user clicks set as focus", async () => { const user = userEvent.setup(); const item = makeFocusItem({ id: "1", title: "Suggested Item" }); useFocusStore.setState({ current: null, queue: [item] }); renderWithProviders(); // Click the set as focus button await user.click(screen.getByRole("button", { name: /set as focus|start/i })); // Item should now be the current focus expect(useFocusStore.getState().current?.id).toBe("1"); }); }); describe("auto-advance behavior", () => { it("auto-advances to next item after start", async () => { const user = userEvent.setup(); const item1 = makeFocusItem({ id: "1", title: "First Item" }); const item2 = makeFocusItem({ id: "2", title: "Second Item" }); useFocusStore.setState({ current: item1, queue: [item2] }); renderWithProviders(); // Start does not trigger ReasonPrompt -- it goes straight through await user.click(screen.getByRole("button", { name: /start/i })); // Should log decision via invoke await waitFor(() => { expect(mockInvoke).toHaveBeenCalledWith( "log_decision", expect.objectContaining({ entry: expect.objectContaining({ action: "start", bead_id: "1", }), }) ); }); }); it("shows empty state after last item start", async () => { const user = userEvent.setup(); const item = makeFocusItem({ id: "1", title: "Only Item" }); useFocusStore.setState({ current: item, queue: [] }); renderWithProviders(); // Start the only item (no ReasonPrompt for start) await user.click(screen.getByRole("button", { name: /start/i })); // log_decision is called asynchronously; start doesn't advance the queue // (start opens URL + logs, but doesn't call act to advance) await waitFor(() => { expect(mockInvoke).toHaveBeenCalledWith( "log_decision", expect.anything() ); }); }); }); describe("focus selection", () => { it("allows selecting a specific item as focus via setFocus", () => { const item1 = makeFocusItem({ id: "1", title: "First" }); const item2 = makeFocusItem({ id: "2", title: "Second" }); const item3 = makeFocusItem({ id: "3", title: "Third" }); useFocusStore.setState({ current: item1, queue: [item2, item3] }); // Use setFocus to promote item3 useFocusStore.getState().setFocus("3"); const state = useFocusStore.getState(); expect(state.current?.id).toBe("3"); expect(state.queue.map((i) => i.id)).toContain("1"); expect(state.queue.map((i) => i.id)).toContain("2"); }); }); describe("loading and error states", () => { it("shows loading state", () => { useFocusStore.setState({ isLoading: true }); renderWithProviders(); expect(screen.getByText(/loading/i)).toBeInTheDocument(); }); it("shows error state", () => { useFocusStore.setState({ error: "Something went wrong" }); renderWithProviders(); expect(screen.getByText("Something went wrong")).toBeInTheDocument(); }); }); describe("ReasonPrompt wiring", () => { it("AC-F2.1: Skip action triggers ReasonPrompt with 'Skipping: [title]'", async () => { const user = userEvent.setup(); const item = makeFocusItem({ id: "1", title: "Fix auth bug" }); useFocusStore.setState({ current: item, queue: [] }); renderWithProviders(); // Button accessible name includes shortcut text: "SkipCmd+S" await user.click(screen.getByRole("button", { name: /^Skip/i })); // ReasonPrompt should appear with the action and title expect(screen.getByRole("dialog")).toBeInTheDocument(); expect(screen.getByText(/skipping/i)).toBeInTheDocument(); // Title appears in both FocusCard and ReasonPrompt -- check within dialog const dialog = screen.getByRole("dialog"); expect(dialog).toHaveTextContent("Fix auth bug"); }); it("AC-F2.5: 'Skip reason' proceeds with reason=null", async () => { const user = userEvent.setup(); const item = makeFocusItem({ id: "1", title: "Fix auth bug" }); useFocusStore.setState({ current: item, queue: [] }); renderWithProviders(); // Trigger skip to open ReasonPrompt (button name includes shortcut) await user.click(screen.getByRole("button", { name: /^Skip/ })); // Click "Skip reason" button in the prompt await user.click(screen.getByRole("button", { name: /skip reason/i })); // Should have called update_item and log_decision via useActions await waitFor(() => { expect(mockInvoke).toHaveBeenCalledWith( "log_decision", expect.objectContaining({ entry: expect.objectContaining({ action: "skip", bead_id: "1", reason: null, }), }) ); }); // ReasonPrompt should be gone expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); it("AC-F2.7: Escape cancels prompt", async () => { const user = userEvent.setup(); const item = makeFocusItem({ id: "1", title: "Fix auth bug" }); useFocusStore.setState({ current: item, queue: [] }); renderWithProviders(); // Trigger skip to open ReasonPrompt (button name includes shortcut) await user.click(screen.getByRole("button", { name: /^Skip/ })); expect(screen.getByRole("dialog")).toBeInTheDocument(); // Press Escape await user.keyboard("{Escape}"); // ReasonPrompt should be dismissed expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); // No backend calls should have been made expect(mockInvoke).not.toHaveBeenCalled(); }); it("AC-F2.8: Confirm logs decision with reason and tags", async () => { const user = userEvent.setup(); const item = makeFocusItem({ id: "1", title: "Fix auth bug" }); useFocusStore.setState({ current: item, queue: [] }); renderWithProviders(); // Trigger skip to open ReasonPrompt (button name includes shortcut) await user.click(screen.getByRole("button", { name: /^Skip/ })); // Type a reason const textarea = screen.getByRole("textbox"); await user.type(textarea, "Need more context from Sarah"); // Select a tag await user.click(screen.getByRole("button", { name: /blocking/i })); // Click Confirm await user.click(screen.getByRole("button", { name: /confirm/i })); // Should have logged with reason and tags await waitFor(() => { expect(mockInvoke).toHaveBeenCalledWith( "log_decision", expect.objectContaining({ entry: expect.objectContaining({ action: "skip", bead_id: "1", reason: "Need more context from Sarah", tags: ["blocking"], }), }) ); }); }); it("Defer 1h triggers ReasonPrompt then calls defer action", async () => { const user = userEvent.setup(); const item = makeFocusItem({ id: "1", title: "Review MR" }); useFocusStore.setState({ current: item, queue: [] }); renderWithProviders(); // Trigger defer 1h await user.click(screen.getByRole("button", { name: /1 hour/i })); // ReasonPrompt should appear with defer action expect(screen.getByRole("dialog")).toBeInTheDocument(); expect(screen.getByText(/deferring/i)).toBeInTheDocument(); // Skip reason to confirm quickly await user.click(screen.getByRole("button", { name: /skip reason/i })); // Should have called update_item with snooze time await waitFor(() => { expect(mockInvoke).toHaveBeenCalledWith( "update_item", expect.objectContaining({ id: "1", updates: expect.objectContaining({ snoozed_until: expect.any(String), }), }) ); }); }); it("Start does NOT trigger ReasonPrompt", async () => { const user = userEvent.setup(); const item = makeFocusItem({ id: "1", title: "Test" }); useFocusStore.setState({ current: item, queue: [] }); renderWithProviders(); await user.click(screen.getByRole("button", { name: /start/i })); // No dialog should appear expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); // But log_decision should be called directly await waitFor(() => { expect(mockInvoke).toHaveBeenCalledWith( "log_decision", expect.objectContaining({ entry: expect.objectContaining({ action: "start", }), }) ); }); }); }); });