ModelPicker.test.tsx (7302B)
1 import { describe, it, expect, vi } from "vitest"; 2 import { render, screen, fireEvent } from "@testing-library/react"; 3 import { ModelPicker } from "./ModelPicker"; 4 5 const mockModels = [ 6 { id: "gpt-4", name: "GPT-4" }, 7 { id: "claude-3", name: "Claude 3" }, 8 { id: "llama-3", name: "Llama 3" }, 9 ]; 10 11 describe("ModelPicker", () => { 12 it("renders model selector button", () => { 13 render(<ModelPicker models={mockModels} selectedModel={null} />); 14 expect(screen.getByText(/model/i)).toBeInTheDocument(); 15 }); 16 17 it("shows selected model name", () => { 18 render(<ModelPicker models={mockModels} selectedModel="gpt-4" />); 19 expect(screen.getByText("GPT-4")).toBeInTheDocument(); 20 }); 21 22 it("shows 'Model' when no model is selected", () => { 23 render(<ModelPicker models={mockModels} selectedModel={null} />); 24 expect(screen.getByText("Model")).toBeInTheDocument(); 25 }); 26 27 it("shows model id as fallback when model not found in list", () => { 28 render(<ModelPicker models={mockModels} selectedModel="unknown-model" />); 29 expect(screen.getByText("unknown-model")).toBeInTheDocument(); 30 }); 31 32 it("opens dropdown on click", () => { 33 render(<ModelPicker models={mockModels} selectedModel={null} />); 34 fireEvent.click(screen.getByRole("button")); 35 expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument(); 36 expect(screen.getByText("Claude 3")).toBeInTheDocument(); 37 expect(screen.getByText("Llama 3")).toBeInTheDocument(); 38 }); 39 40 it("calls onSelect when a model is chosen", () => { 41 const onSelect = vi.fn(); 42 render(<ModelPicker models={mockModels} selectedModel={null} onSelect={onSelect} />); 43 fireEvent.click(screen.getByRole("button")); 44 fireEvent.click(screen.getByText("Claude 3")); 45 expect(onSelect).toHaveBeenCalledWith("claude-3"); 46 }); 47 48 it("closes dropdown after selecting a model", () => { 49 render(<ModelPicker models={mockModels} selectedModel={null} onSelect={vi.fn()} />); 50 fireEvent.click(screen.getByRole("button")); 51 fireEvent.click(screen.getByText("Claude 3")); 52 expect(screen.queryByPlaceholderText(/search/i)).not.toBeInTheDocument(); 53 }); 54 55 it("clears search after selecting a model", () => { 56 render(<ModelPicker models={mockModels} selectedModel={null} onSelect={vi.fn()} />); 57 fireEvent.click(screen.getByRole("button")); 58 const input = screen.getByPlaceholderText(/search/i); 59 fireEvent.change(input, { target: { value: "claude" } }); 60 fireEvent.click(screen.getByText("Claude 3")); 61 // Open again - search should be cleared 62 fireEvent.click(screen.getByRole("button")); 63 expect(screen.getByText("Claude 3")).toBeInTheDocument(); 64 expect(screen.getByText("Llama 3")).toBeInTheDocument(); 65 }); 66 67 it("filters models by search input (name)", () => { 68 render(<ModelPicker models={mockModels} selectedModel={null} />); 69 fireEvent.click(screen.getByRole("button")); 70 const input = screen.getByPlaceholderText(/search/i); 71 fireEvent.change(input, { target: { value: "claude" } }); 72 expect(screen.getByText("Claude 3")).toBeInTheDocument(); 73 expect(screen.queryByText("Llama 3")).not.toBeInTheDocument(); 74 }); 75 76 it("filters models by id", () => { 77 render(<ModelPicker models={mockModels} selectedModel={null} />); 78 fireEvent.click(screen.getByRole("button")); 79 const input = screen.getByPlaceholderText(/search/i); 80 fireEvent.change(input, { target: { value: "gpt" } }); 81 // When searching "gpt", GPT-4 button shows in dropdown (not the main button) 82 expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument(); 83 }); 84 85 it("shows 'No models found' when search has no matches", () => { 86 render(<ModelPicker models={mockModels} selectedModel={null} />); 87 fireEvent.click(screen.getByRole("button")); 88 const input = screen.getByPlaceholderText(/search/i); 89 fireEvent.change(input, { target: { value: "nonexistent" } }); 90 expect(screen.getByText("No models found")).toBeInTheDocument(); 91 }); 92 93 it("shows all models when search is cleared", () => { 94 render(<ModelPicker models={mockModels} selectedModel={null} />); 95 fireEvent.click(screen.getByRole("button")); 96 const input = screen.getByPlaceholderText(/search/i); 97 fireEvent.change(input, { target: { value: "claude" } }); 98 expect(screen.queryByText("Llama 3")).not.toBeInTheDocument(); 99 fireEvent.change(input, { target: { value: "" } }); 100 expect(screen.getByText("Claude 3")).toBeInTheDocument(); 101 expect(screen.getByText("Llama 3")).toBeInTheDocument(); 102 }); 103 104 it("closes dropdown on outside click", () => { 105 render( 106 <div data-testid="outside"> 107 <ModelPicker models={mockModels} selectedModel={null} /> 108 </div>, 109 ); 110 fireEvent.click(screen.getByRole("button")); 111 expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument(); 112 fireEvent.mouseDown(screen.getByTestId("outside")); 113 expect(screen.queryByPlaceholderText(/search/i)).not.toBeInTheDocument(); 114 }); 115 116 it("highlights selected model in dropdown", () => { 117 render(<ModelPicker models={mockModels} selectedModel="gpt-4" />); 118 fireEvent.click(screen.getByRole("button")); 119 // Find the dropdown item (not the button) - the dropdown items are in the list 120 const dropdownItems = screen.getAllByText("GPT-4"); 121 // One in button, one in dropdown list 122 const dropdownItem = dropdownItems.find( 123 (el) => el.tagName === "BUTTON" && el.className.includes("block"), 124 ); 125 expect(dropdownItem).toBeDefined(); 126 expect(dropdownItem?.className).toContain("bg-blue"); 127 }); 128 129 it("does not highlight unselected models", () => { 130 render(<ModelPicker models={mockModels} selectedModel="gpt-4" />); 131 fireEvent.click(screen.getByRole("button")); 132 const claudeItems = screen.getAllByText("Claude 3"); 133 const dropdownItem = claudeItems.find( 134 (el) => el.tagName === "BUTTON" && el.className.includes("block"), 135 ); 136 expect(dropdownItem?.className).not.toContain("bg-blue"); 137 }); 138 139 it("search is case-insensitive", () => { 140 render(<ModelPicker models={mockModels} selectedModel={null} />); 141 fireEvent.click(screen.getByRole("button")); 142 const input = screen.getByPlaceholderText(/search/i); 143 fireEvent.change(input, { target: { value: "CLAUDE" } }); 144 expect(screen.getByText("Claude 3")).toBeInTheDocument(); 145 }); 146 147 it("handles empty models list", () => { 148 render(<ModelPicker models={[]} selectedModel={null} />); 149 fireEvent.click(screen.getByRole("button")); 150 expect(screen.getByText("No models found")).toBeInTheDocument(); 151 }); 152 153 it("handles onSelect being undefined", () => { 154 render(<ModelPicker models={mockModels} selectedModel={null} />); 155 fireEvent.click(screen.getByRole("button")); 156 // Should not throw 157 fireEvent.click(screen.getByText("Claude 3")); 158 }); 159 160 it("search by whitespace-only shows all models", () => { 161 render(<ModelPicker models={mockModels} selectedModel={null} />); 162 fireEvent.click(screen.getByRole("button")); 163 const input = screen.getByPlaceholderText(/search/i); 164 fireEvent.change(input, { target: { value: " " } }); 165 // Whitespace-only should show all models (trim returns empty) 166 expect(screen.getByText("Claude 3")).toBeInTheDocument(); 167 expect(screen.getByText("Llama 3")).toBeInTheDocument(); 168 }); 169 });