theo-agent-dashboard

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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 });