App.test.tsx (8689B)
1 import { describe, it, expect, vi, beforeEach } from "vitest"; 2 import { render, screen, waitFor, fireEvent } from "@testing-library/react"; 3 import { App } from "./App"; 4 import { useAuthStore } from "./store/auth"; 5 import { useThemeStore } from "./store/theme"; 6 7 vi.mock("./components/Login/Login", () => ({ 8 Login: () => <div data-testid="login-page">Login Page</div>, 9 })); 10 vi.mock("./components/Setup/Setup", () => ({ 11 Setup: () => <div data-testid="setup-page">Setup Page</div>, 12 })); 13 vi.mock("./components/Chat/ChatView", () => ({ 14 ChatView: () => <div data-testid="chat-view">Chat View</div>, 15 })); 16 vi.mock("./components/Sidebar/Sidebar", () => ({ 17 Sidebar: () => <div data-testid="sidebar">Sidebar</div>, 18 })); 19 vi.mock("./components/Search/SearchModal", () => ({ 20 SearchModal: ({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) => 21 isOpen ? ( 22 <div data-testid="search-modal"> 23 <button onClick={onClose}>Close Search</button> 24 </div> 25 ) : null, 26 })); 27 vi.mock("./components/Theme/ThemeToggle", () => ({ 28 ThemeToggle: () => <button data-testid="theme-toggle">Theme</button>, 29 })); 30 31 function mockFetch(response: unknown) { 32 vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ 33 ok: true, 34 json: async () => response, 35 })); 36 } 37 38 beforeEach(() => { 39 vi.restoreAllMocks(); 40 useThemeStore.setState({ theme: "dark" }); 41 // Create fresh store to reset all actions 42 useAuthStore.setState({ 43 authenticated: false, 44 configured: false, 45 loading: false, 46 error: null, 47 }); 48 mockFetch({ configured: true, authenticated: false }); 49 }); 50 51 describe("App", () => { 52 it("shows loading state initially then transitions", async () => { 53 vi.stubGlobal("fetch", vi.fn().mockReturnValue(new Promise(() => {}))); 54 useAuthStore.setState({ loading: true }); 55 render(<App />); 56 expect(screen.getByText("Loading...")).toBeInTheDocument(); 57 }); 58 59 it("shows setup page when not configured", async () => { 60 mockFetch({ configured: false, authenticated: false }); 61 render(<App />); 62 await waitFor(() => { 63 expect(screen.getByTestId("setup-page")).toBeInTheDocument(); 64 }); 65 }); 66 67 it("shows login page when configured but not authenticated", async () => { 68 mockFetch({ configured: true, authenticated: false }); 69 render(<App />); 70 await waitFor(() => { 71 expect(screen.getByTestId("login-page")).toBeInTheDocument(); 72 }); 73 }); 74 75 it("shows dashboard when authenticated", async () => { 76 mockFetch({ configured: true, authenticated: true }); 77 render(<App />); 78 await waitFor(() => { 79 expect(screen.getByTestId("chat-view")).toBeInTheDocument(); 80 }); 81 expect(screen.getByTestId("sidebar")).toBeInTheDocument(); 82 }); 83 84 it("shows sidebar with Topics and Sessions", async () => { 85 mockFetch({ configured: true, authenticated: true }); 86 render(<App />); 87 await waitFor(() => { 88 expect(screen.getByTestId("sidebar")).toBeInTheDocument(); 89 }); 90 }); 91 92 it("applies dark theme classes", async () => { 93 mockFetch({ configured: true, authenticated: true }); 94 useThemeStore.setState({ theme: "dark" }); 95 const { container } = render(<App />); 96 await waitFor(() => { 97 expect(screen.getByTestId("chat-view")).toBeInTheDocument(); 98 }); 99 expect(container.querySelector(".bg-gray-950")).toBeInTheDocument(); 100 }); 101 102 it("applies light theme classes", async () => { 103 mockFetch({ configured: true, authenticated: true }); 104 useThemeStore.setState({ theme: "light" }); 105 const { container } = render(<App />); 106 await waitFor(() => { 107 expect(screen.getByTestId("chat-view")).toBeInTheDocument(); 108 }); 109 expect(container.querySelector(".bg-gray-100")).toBeInTheDocument(); 110 }); 111 112 it("opens search modal with Ctrl+K", async () => { 113 mockFetch({ configured: true, authenticated: true }); 114 render(<App />); 115 await waitFor(() => { 116 expect(screen.getByTestId("chat-view")).toBeInTheDocument(); 117 }); 118 expect(screen.queryByTestId("search-modal")).not.toBeInTheDocument(); 119 fireEvent.keyDown(document, { key: "k", ctrlKey: true }); 120 await waitFor(() => { 121 expect(screen.getByTestId("search-modal")).toBeInTheDocument(); 122 }); 123 }); 124 125 it("closes search modal", async () => { 126 mockFetch({ configured: true, authenticated: true }); 127 render(<App />); 128 await waitFor(() => { 129 expect(screen.getByTestId("chat-view")).toBeInTheDocument(); 130 }); 131 fireEvent.keyDown(document, { key: "k", ctrlKey: true }); 132 await waitFor(() => { 133 expect(screen.getByTestId("search-modal")).toBeInTheDocument(); 134 }); 135 fireEvent.click(screen.getByText("Close Search")); 136 await waitFor(() => { 137 expect(screen.queryByTestId("search-modal")).not.toBeInTheDocument(); 138 }); 139 }); 140 141 it("opens sidebar via toggle button", async () => { 142 mockFetch({ configured: true, authenticated: true }); 143 render(<App />); 144 await waitFor(() => { 145 expect(screen.getByTestId("chat-view")).toBeInTheDocument(); 146 }); 147 const toggleButton = screen.getByLabelText("Toggle sidebar"); 148 expect(toggleButton).toBeInTheDocument(); 149 fireEvent.click(toggleButton); 150 }); 151 152 it("calls checkStatus on mount via fetch", async () => { 153 const mockFetchFn = vi.fn().mockResolvedValue({ 154 ok: true, 155 json: async () => ({ configured: true, authenticated: true }), 156 }); 157 vi.stubGlobal("fetch", mockFetchFn); 158 render(<App />); 159 await waitFor(() => { 160 expect(mockFetchFn).toHaveBeenCalled(); 161 }); 162 }); 163 164 it("renders ThemeToggle in authenticated mode", async () => { 165 mockFetch({ configured: true, authenticated: true }); 166 render(<App />); 167 await waitFor(() => { 168 expect(screen.getByTestId("theme-toggle")).toBeInTheDocument(); 169 }); 170 }); 171 172 it("does not render ThemeToggle when loading", () => { 173 vi.stubGlobal("fetch", vi.fn().mockReturnValue(new Promise(() => {}))); 174 useAuthStore.setState({ loading: true }); 175 render(<App />); 176 expect(screen.queryByTestId("theme-toggle")).not.toBeInTheDocument(); 177 }); 178 179 it("does not render ThemeToggle when not configured", async () => { 180 mockFetch({ configured: false, authenticated: false }); 181 render(<App />); 182 await waitFor(() => { 183 expect(screen.getByTestId("setup-page")).toBeInTheDocument(); 184 }); 185 expect(screen.queryByTestId("theme-toggle")).not.toBeInTheDocument(); 186 }); 187 188 it("does not render ThemeToggle when not authenticated", async () => { 189 mockFetch({ configured: true, authenticated: false }); 190 render(<App />); 191 await waitFor(() => { 192 expect(screen.getByTestId("login-page")).toBeInTheDocument(); 193 }); 194 expect(screen.queryByTestId("theme-toggle")).not.toBeInTheDocument(); 195 }); 196 197 it("opens search on Meta+K", async () => { 198 mockFetch({ configured: true, authenticated: true }); 199 render(<App />); 200 await waitFor(() => { 201 expect(screen.getByTestId("chat-view")).toBeInTheDocument(); 202 }); 203 fireEvent.keyDown(document, { key: "k", metaKey: true }); 204 await waitFor(() => { 205 expect(screen.getByTestId("search-modal")).toBeInTheDocument(); 206 }); 207 }); 208 209 it("does not open search on K without modifier", async () => { 210 mockFetch({ configured: true, authenticated: true }); 211 render(<App />); 212 await waitFor(() => { 213 expect(screen.getByTestId("chat-view")).toBeInTheDocument(); 214 }); 215 fireEvent.keyDown(document, { key: "k" }); 216 expect(screen.queryByTestId("search-modal")).not.toBeInTheDocument(); 217 }); 218 219 it("shows loading text with light theme classes", () => { 220 vi.stubGlobal("fetch", vi.fn().mockReturnValue(new Promise(() => {}))); 221 useAuthStore.setState({ loading: true }); 222 useThemeStore.setState({ theme: "light" }); 223 const { container } = render(<App />); 224 const loadingDiv = container.querySelector(".bg-gray-100"); 225 expect(loadingDiv).toBeInTheDocument(); 226 expect(loadingDiv?.textContent).toContain("Loading..."); 227 }); 228 229 it("sidebar is hidden by default", async () => { 230 mockFetch({ configured: true, authenticated: true }); 231 const { container } = render(<App />); 232 await waitFor(() => { 233 expect(screen.getByTestId("chat-view")).toBeInTheDocument(); 234 }); 235 expect(container.querySelector(".-translate-x-full")).toBeInTheDocument(); 236 }); 237 238 it("mobile overlay appears when sidebar is open", async () => { 239 mockFetch({ configured: true, authenticated: true }); 240 render(<App />); 241 await waitFor(() => { 242 expect(screen.getByTestId("chat-view")).toBeInTheDocument(); 243 }); 244 fireEvent.click(screen.getByLabelText("Toggle sidebar")); 245 await waitFor(() => { 246 expect(screen.getByLabelText("Toggle sidebar")).toBeInTheDocument(); 247 }); 248 }); 249 });