/** * Tests for Inbox and InboxView components. * * TDD: These tests define the expected behavior before implementation. */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Inbox } from "@/components/Inbox"; import { InboxView } from "@/components/InboxView"; import { useInboxStore } from "@/stores/inbox-store"; import { makeInboxItem } from "../helpers/fixtures"; import type { InboxItem, TriageAction, DeferDuration } from "@/lib/types"; const mockNewItems: InboxItem[] = [ { id: "1", type: "mention", title: "You were mentioned in #312", triaged: false, createdAt: "2026-02-26T10:00:00Z", snippet: "@user can you look at this?", actor: "alice", }, { id: "2", type: "mr_feedback", title: "Comment on MR !847", triaged: false, createdAt: "2026-02-26T09:00:00Z", snippet: "This needs some refactoring", actor: "bob", }, ]; describe.skip("Inbox", () => { beforeEach(() => { vi.clearAllMocks(); }); it("shows only untriaged items", () => { const items: InboxItem[] = [ ...mockNewItems, { id: "3", type: "mention", title: "Already triaged", triaged: true, createdAt: "2026-02-26T08:00:00Z", }, ]; render(); const inboxItems = screen.getAllByTestId("inbox-item"); expect(inboxItems).toHaveLength(2); }); it("shows inbox zero state when empty", () => { render(); expect(screen.getByText(/Inbox Zero/i)).toBeInTheDocument(); expect(screen.getByText(/All caught up/i)).toBeInTheDocument(); }); it("shows inbox zero when all items are triaged", () => { const items: InboxItem[] = [ { id: "1", type: "mention", title: "Triaged item", triaged: true, createdAt: "2026-02-26T10:00:00Z", }, ]; render(); expect(screen.getByText(/Inbox Zero/i)).toBeInTheDocument(); }); it("accept moves item to queue", async () => { const user = userEvent.setup(); const onTriage = vi.fn(); render(); const acceptButtons = screen.getAllByRole("button", { name: /accept/i }); await user.click(acceptButtons[0]); expect(onTriage).toHaveBeenCalledWith("1", "accept", undefined); }); it("defer shows duration picker", async () => { const user = userEvent.setup(); render(); const deferButtons = screen.getAllByRole("button", { name: /defer/i }); await user.click(deferButtons[0]); // Defer picker should show duration options expect(screen.getByText("1 hour")).toBeInTheDocument(); expect(screen.getByText("Tomorrow")).toBeInTheDocument(); }); it("defer with duration calls onTriage", async () => { const user = userEvent.setup(); const onTriage = vi.fn(); render(); const deferButtons = screen.getAllByRole("button", { name: /defer/i }); await user.click(deferButtons[0]); await user.click(screen.getByText("1 hour")); expect(onTriage).toHaveBeenCalledWith("1", "defer", "1h"); }); it("archive removes item from view", async () => { const user = userEvent.setup(); const onTriage = vi.fn(); render(); const archiveButtons = screen.getAllByRole("button", { name: /archive/i }); await user.click(archiveButtons[0]); expect(onTriage).toHaveBeenCalledWith("1", "archive", undefined); }); it("displays item metadata", () => { render(); expect(screen.getByText("You were mentioned in #312")).toBeInTheDocument(); expect(screen.getByText("@user can you look at this?")).toBeInTheDocument(); expect(screen.getByText("alice")).toBeInTheDocument(); }); it("displays item count in header", () => { render(); expect(screen.getByText(/Inbox \(2\)/)).toBeInTheDocument(); }); describe("keyboard shortcuts", () => { it("pressing 'a' on focused item triggers accept", async () => { const user = userEvent.setup(); const onTriage = vi.fn(); render(); // Focus the first inbox item const firstItem = screen.getAllByTestId("inbox-item")[0]; firstItem.focus(); await user.keyboard("a"); expect(onTriage).toHaveBeenCalledWith("1", "accept", undefined); }); it("pressing 'd' on focused item opens defer picker", async () => { const user = userEvent.setup(); render(); // Focus the first inbox item const firstItem = screen.getAllByTestId("inbox-item")[0]; firstItem.focus(); await user.keyboard("d"); expect(screen.getByText("1 hour")).toBeInTheDocument(); }); it("pressing 'x' on focused item triggers archive", async () => { const user = userEvent.setup(); const onTriage = vi.fn(); render(); // Focus the first inbox item const firstItem = screen.getAllByTestId("inbox-item")[0]; firstItem.focus(); await user.keyboard("x"); expect(onTriage).toHaveBeenCalledWith("1", "archive", undefined); }); }); }); /** * InboxView container tests - integrates with inbox store */ describe("InboxView", () => { beforeEach(() => { vi.clearAllMocks(); // Reset inbox store to initial state useInboxStore.setState({ items: [], isLoading: false, error: null, }); }); it("shows only untriaged items from store", () => { useInboxStore.setState({ items: [ makeInboxItem({ id: "1", triaged: false, title: "Mention in #312" }), makeInboxItem({ id: "2", triaged: false, title: "Comment on MR !847" }), makeInboxItem({ id: "3", triaged: true, title: "Already done" }), ], }); render(); const inboxItems = screen.getAllByTestId("inbox-item"); expect(inboxItems).toHaveLength(2); expect(screen.queryByText("Already done")).not.toBeInTheDocument(); }); it("shows inbox zero celebration when empty", () => { useInboxStore.setState({ items: [] }); render(); expect(screen.getByText(/Inbox Zero/i)).toBeInTheDocument(); expect(screen.getByText(/All caught up/i)).toBeInTheDocument(); }); it("shows inbox zero when all items are triaged", () => { useInboxStore.setState({ items: [ makeInboxItem({ id: "1", triaged: true, title: "Triaged item" }), ], }); render(); expect(screen.getByText(/Inbox Zero/i)).toBeInTheDocument(); }); it("accept triage action updates item in store", async () => { const user = userEvent.setup(); useInboxStore.setState({ items: [ makeInboxItem({ id: "1", triaged: false, title: "Mention in #312" }), ], }); render(); const acceptButton = screen.getByRole("button", { name: /accept/i }); await user.click(acceptButton); // Item should be marked as triaged const { items } = useInboxStore.getState(); expect(items[0].triaged).toBe(true); }); it("archive triage action updates item in store", async () => { const user = userEvent.setup(); useInboxStore.setState({ items: [ makeInboxItem({ id: "1", triaged: false, title: "Mention in #312" }), ], }); render(); const archiveButton = screen.getByRole("button", { name: /archive/i }); await user.click(archiveButton); // Item should be marked as triaged and archived const { items } = useInboxStore.getState(); expect(items[0].triaged).toBe(true); expect(items[0].archived).toBe(true); }); it("updates count in real-time after triage", async () => { const user = userEvent.setup(); useInboxStore.setState({ items: [ makeInboxItem({ id: "1", triaged: false, title: "Item 1" }), makeInboxItem({ id: "2", triaged: false, title: "Item 2" }), ], }); render(); expect(screen.getByText(/Inbox \(2\)/)).toBeInTheDocument(); const acceptButtons = screen.getAllByRole("button", { name: /accept/i }); await user.click(acceptButtons[0]); expect(screen.getByText(/Inbox \(1\)/)).toBeInTheDocument(); }); it("displays keyboard shortcut hints", () => { useInboxStore.setState({ items: [makeInboxItem({ id: "1", triaged: false })], }); render(); // Check for keyboard hints text (using more specific selectors to avoid button text) expect(screen.getByText("j/k")).toBeInTheDocument(); expect(screen.getByText(/navigate/i)).toBeInTheDocument(); // The hint text contains lowercase "a" in a kbd element expect(screen.getAllByText(/accept/i).length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText(/defer/i).length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText(/archive/i).length).toBeGreaterThanOrEqual(1); }); describe("keyboard navigation", () => { it("arrow down moves focus to next item", async () => { const user = userEvent.setup(); useInboxStore.setState({ items: [ makeInboxItem({ id: "1", triaged: false, title: "Item 1" }), makeInboxItem({ id: "2", triaged: false, title: "Item 2" }), ], }); render(); // Focus container and press down arrow const container = screen.getByTestId("inbox-view"); container.focus(); await user.keyboard("{ArrowDown}"); // Second item should be highlighted (focused index = 1) const items = screen.getAllByTestId("inbox-item"); expect(items[1]).toHaveAttribute("data-focused", "true"); }); it("arrow up moves focus to previous item", async () => { const user = userEvent.setup(); useInboxStore.setState({ items: [ makeInboxItem({ id: "1", triaged: false, title: "Item 1" }), makeInboxItem({ id: "2", triaged: false, title: "Item 2" }), ], }); render(); const container = screen.getByTestId("inbox-view"); container.focus(); // Move down then up await user.keyboard("{ArrowDown}"); await user.keyboard("{ArrowUp}"); const items = screen.getAllByTestId("inbox-item"); expect(items[0]).toHaveAttribute("data-focused", "true"); }); it("pressing 'a' on focused item triggers accept", async () => { const user = userEvent.setup(); useInboxStore.setState({ items: [makeInboxItem({ id: "1", triaged: false, title: "Item 1" })], }); render(); const container = screen.getByTestId("inbox-view"); container.focus(); await user.keyboard("a"); const { items } = useInboxStore.getState(); expect(items[0].triaged).toBe(true); }); it("pressing 'x' on focused item triggers archive", async () => { const user = userEvent.setup(); useInboxStore.setState({ items: [makeInboxItem({ id: "1", triaged: false, title: "Item 1" })], }); render(); const container = screen.getByTestId("inbox-view"); container.focus(); await user.keyboard("x"); const { items } = useInboxStore.getState(); expect(items[0].triaged).toBe(true); expect(items[0].archived).toBe(true); }); }); });