TopicList.test.tsx (4971B)
1 import { describe, it, expect, vi, beforeEach } from "vitest"; 2 import { render, screen, fireEvent } from "@testing-library/react"; 3 import { TopicList } from "./TopicList"; 4 import { useTopicStore } from "../../store/topic"; 5 6 const mockTopics = [ 7 { id: "t1", name: "Work", color: "#6366f1", sort_order: 0, created_at: Date.now(), session_count: 3 }, 8 { id: "t2", name: "Personal", color: "#10b981", sort_order: 1, created_at: Date.now(), session_count: 1 }, 9 ]; 10 11 beforeEach(() => { 12 useTopicStore.setState({ topics: [], selectedTopicId: null }); 13 vi.restoreAllMocks(); 14 }); 15 16 describe("TopicList", () => { 17 it("renders Topics heading", () => { 18 render(<TopicList />); 19 expect(screen.getByText("Topics")).toBeInTheDocument(); 20 }); 21 22 it("renders + New button", () => { 23 render(<TopicList />); 24 expect(screen.getByText("+ New")).toBeInTheDocument(); 25 }); 26 27 it("renders search input", () => { 28 render(<TopicList />); 29 expect(screen.getByPlaceholderText("Search topics...")).toBeInTheDocument(); 30 }); 31 32 it("shows 'No topics yet' when no topics", () => { 33 render(<TopicList />); 34 expect(screen.getByText("No topics yet")).toBeInTheDocument(); 35 }); 36 37 it("renders topics from store", () => { 38 useTopicStore.setState({ topics: mockTopics }); 39 render(<TopicList />); 40 expect(screen.getByText("Work")).toBeInTheDocument(); 41 expect(screen.getByText("Personal")).toBeInTheDocument(); 42 }); 43 44 it("clicking + New shows input", () => { 45 render(<TopicList />); 46 fireEvent.click(screen.getByText("+ New")); 47 expect(screen.getByPlaceholderText("Topic name")).toBeInTheDocument(); 48 }); 49 50 it("clicking Add creates topic", async () => { 51 vi.spyOn(useTopicStore.getState(), "createTopic").mockResolvedValue(undefined); 52 render(<TopicList />); 53 fireEvent.click(screen.getByText("+ New")); 54 const input = screen.getByPlaceholderText("Topic name"); 55 fireEvent.change(input, { target: { value: "New Topic" } }); 56 fireEvent.click(screen.getByText("Add")); 57 expect(useTopicStore.getState().createTopic).toHaveBeenCalledWith("New Topic"); 58 }); 59 60 it("pressing Enter creates topic", async () => { 61 vi.spyOn(useTopicStore.getState(), "createTopic").mockResolvedValue(undefined); 62 render(<TopicList />); 63 fireEvent.click(screen.getByText("+ New")); 64 const input = screen.getByPlaceholderText("Topic name"); 65 fireEvent.change(input, { target: { value: "New Topic" } }); 66 fireEvent.keyDown(input, { key: "Enter" }); 67 expect(useTopicStore.getState().createTopic).toHaveBeenCalledWith("New Topic"); 68 }); 69 70 it("does not create topic with empty name", async () => { 71 const spy = vi.spyOn(useTopicStore.getState(), "createTopic").mockResolvedValue(undefined); 72 render(<TopicList />); 73 fireEvent.click(screen.getByText("+ New")); 74 const input = screen.getByPlaceholderText("Topic name"); 75 fireEvent.change(input, { target: { value: " " } }); 76 fireEvent.click(screen.getByText("Add")); 77 expect(spy).not.toHaveBeenCalled(); 78 }); 79 80 it("filters topics by search", () => { 81 useTopicStore.setState({ topics: mockTopics }); 82 render(<TopicList />); 83 const input = screen.getByPlaceholderText("Search topics..."); 84 fireEvent.change(input, { target: { value: "Work" } }); 85 expect(screen.getByText("Work")).toBeInTheDocument(); 86 expect(screen.queryByText("Personal")).not.toBeInTheDocument(); 87 }); 88 89 it("shows 'No topics yet' when filter has no matches", () => { 90 useTopicStore.setState({ topics: mockTopics }); 91 render(<TopicList />); 92 const input = screen.getByPlaceholderText("Search topics..."); 93 fireEvent.change(input, { target: { value: "xyz" } }); 94 expect(screen.getByText("No topics yet")).toBeInTheDocument(); 95 }); 96 97 it("clicking topic toggles selection", () => { 98 useTopicStore.setState({ topics: mockTopics }); 99 render(<TopicList />); 100 fireEvent.click(screen.getByText("Work")); 101 expect(useTopicStore.getState().selectedTopicId).toBe("t1"); 102 }); 103 104 it("clicking selected topic deselects it", () => { 105 useTopicStore.setState({ topics: mockTopics, selectedTopicId: "t1" }); 106 render(<TopicList />); 107 fireEvent.click(screen.getByText("Work")); 108 expect(useTopicStore.getState().selectedTopicId).toBeNull(); 109 }); 110 111 it("search is case-insensitive", () => { 112 useTopicStore.setState({ topics: mockTopics }); 113 render(<TopicList />); 114 const input = screen.getByPlaceholderText("Search topics..."); 115 fireEvent.change(input, { target: { value: "work" } }); 116 expect(screen.getByText("Work")).toBeInTheDocument(); 117 }); 118 119 it("hides topics that don't match filter", () => { 120 useTopicStore.setState({ topics: mockTopics }); 121 render(<TopicList />); 122 const input = screen.getByPlaceholderText("Search topics..."); 123 fireEvent.change(input, { target: { value: "Personal" } }); 124 expect(screen.queryByText("Work")).not.toBeInTheDocument(); 125 expect(screen.getByText("Personal")).toBeInTheDocument(); 126 }); 127 });