SearchModal.test.tsx (5980B)
1 import { describe, it, expect, vi, beforeEach } from "vitest"; 2 import { render, screen, fireEvent, waitFor } from "@testing-library/react"; 3 import { SearchModal } from "./SearchModal"; 4 import { useSessionStore } from "../../store/session"; 5 6 vi.mock("../../lib/api", () => ({ 7 api: { 8 get: vi.fn(), 9 }, 10 })); 11 12 import { api } from "../../lib/api"; 13 14 beforeEach(() => { 15 vi.clearAllMocks(); 16 useSessionStore.setState({ activeSessionId: null }); 17 }); 18 19 describe("SearchModal", () => { 20 it("renders nothing when not open", () => { 21 const { container } = render( 22 <SearchModal isOpen={false} onClose={vi.fn()} onSelectSession={vi.fn()} />, 23 ); 24 expect(container.innerHTML).toBe(""); 25 }); 26 27 it("renders search bar when open", () => { 28 render( 29 <SearchModal isOpen={true} onClose={vi.fn()} onSelectSession={vi.fn()} />, 30 ); 31 expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument(); 32 }); 33 34 it("closes on Escape key", () => { 35 const onClose = vi.fn(); 36 render( 37 <SearchModal isOpen={true} onClose={onClose} onSelectSession={vi.fn()} />, 38 ); 39 fireEvent.keyDown(document, { key: "Escape" }); 40 expect(onClose).toHaveBeenCalled(); 41 }); 42 43 it("searches via API and displays results", async () => { 44 vi.mocked(api.get).mockResolvedValue({ 45 results: [ 46 { 47 session_id: "s1", 48 session_name: "Test Session", 49 messages: [{ id: "m1", content: "Hello world", role: "user" }], 50 }, 51 ], 52 }); 53 54 render( 55 <SearchModal isOpen={true} onClose={vi.fn()} onSelectSession={vi.fn()} />, 56 ); 57 58 const input = screen.getByPlaceholderText(/search/i); 59 fireEvent.change(input, { target: { value: "hello" } }); 60 61 await waitFor(() => { 62 expect(screen.getByText("Hello world")).toBeInTheDocument(); 63 }); 64 }); 65 66 it("calls onSelectSession when a result is clicked", async () => { 67 const onSelectSession = vi.fn(); 68 vi.mocked(api.get).mockResolvedValue({ 69 results: [ 70 { 71 session_id: "s1", 72 session_name: "Session", 73 messages: [{ id: "m1", content: "test message", role: "user" }], 74 }, 75 ], 76 }); 77 78 render( 79 <SearchModal 80 isOpen={true} 81 onClose={vi.fn()} 82 onSelectSession={onSelectSession} 83 />, 84 ); 85 86 fireEvent.change(screen.getByPlaceholderText(/search/i), { 87 target: { value: "test" }, 88 }); 89 90 await waitFor(() => { 91 fireEvent.click(screen.getByText("test message")); 92 }); 93 94 expect(onSelectSession).toHaveBeenCalledWith("s1"); 95 }); 96 97 it("clears results when search is empty", async () => { 98 vi.mocked(api.get).mockResolvedValue({ 99 results: [ 100 { 101 session_id: "s1", 102 session_name: "Session", 103 messages: [{ id: "m1", content: "found", role: "user" }], 104 }, 105 ], 106 }); 107 108 render( 109 <SearchModal isOpen={true} onClose={vi.fn()} onSelectSession={vi.fn()} />, 110 ); 111 112 const input = screen.getByPlaceholderText(/search/i); 113 fireEvent.change(input, { target: { value: "find" } }); 114 115 await waitFor(() => { 116 expect(screen.getByText("found")).toBeInTheDocument(); 117 }); 118 119 // Clear search 120 fireEvent.change(input, { target: { value: "" } }); 121 122 await waitFor(() => { 123 expect(screen.queryByText("found")).not.toBeInTheDocument(); 124 }); 125 }); 126 127 it("handles API error gracefully", async () => { 128 vi.mocked(api.get).mockRejectedValue(new Error("network error")); 129 130 render( 131 <SearchModal isOpen={true} onClose={vi.fn()} onSelectSession={vi.fn()} />, 132 ); 133 134 fireEvent.change(screen.getByPlaceholderText(/search/i), { 135 target: { value: "test" }, 136 }); 137 138 await waitFor(() => { 139 // Should show "No results found" on error 140 expect(screen.getByText("No results found")).toBeInTheDocument(); 141 }); 142 }); 143 144 it("sets active session in store when selecting result", async () => { 145 vi.mocked(api.get).mockResolvedValue({ 146 results: [ 147 { 148 session_id: "s1", 149 session_name: "Session", 150 messages: [{ id: "m1", content: "click me", role: "user" }], 151 }, 152 ], 153 }); 154 155 render( 156 <SearchModal 157 isOpen={true} 158 onClose={vi.fn()} 159 onSelectSession={vi.fn()} 160 />, 161 ); 162 163 fireEvent.change(screen.getByPlaceholderText(/search/i), { 164 target: { value: "click" }, 165 }); 166 167 await waitFor(() => { 168 fireEvent.click(screen.getByText("click me")); 169 }); 170 171 expect(useSessionStore.getState().activeSessionId).toBe("s1"); 172 }); 173 174 it("does not render search bar content when not open", () => { 175 const { container } = render( 176 <SearchModal isOpen={false} onClose={vi.fn()} onSelectSession={vi.fn()} />, 177 ); 178 expect(container.querySelector("input")).toBeNull(); 179 }); 180 181 it("shows results with session name", async () => { 182 vi.mocked(api.get).mockResolvedValue({ 183 results: [ 184 { 185 session_id: "s1", 186 session_name: "My Session", 187 messages: [{ id: "m1", content: "msg", role: "assistant" }], 188 }, 189 ], 190 }); 191 192 render( 193 <SearchModal isOpen={true} onClose={vi.fn()} onSelectSession={vi.fn()} />, 194 ); 195 196 fireEvent.change(screen.getByPlaceholderText(/search/i), { 197 target: { value: "msg" }, 198 }); 199 200 await waitFor(() => { 201 expect(screen.getByText("My Session")).toBeInTheDocument(); 202 }); 203 }); 204 205 it("shows role for each message", async () => { 206 vi.mocked(api.get).mockResolvedValue({ 207 results: [ 208 { 209 session_id: "s1", 210 session_name: "S", 211 messages: [{ id: "m1", content: "hi", role: "user" }], 212 }, 213 ], 214 }); 215 216 render( 217 <SearchModal isOpen={true} onClose={vi.fn()} onSelectSession={vi.fn()} />, 218 ); 219 220 fireEvent.change(screen.getByPlaceholderText(/search/i), { 221 target: { value: "hi" }, 222 }); 223 224 await waitFor(() => { 225 expect(screen.getByText("user")).toBeInTheDocument(); 226 }); 227 }); 228 });