import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, describe, expect, it, vi } from "vitest"; import { getSettings } from "./settings"; import SettingsGate from "./SettingsGate"; import { jsonResponse } from "./test/setup"; afterEach(() => { vi.restoreAllMocks(); localStorage.clear(); }); function setup(): { connect: () => Promise } { render( undefined} />); return { connect: async (): Promise => { await userEvent.click(screen.getByRole("button", { name: "Connect" })); }, }; } describe("SettingsGate", () => { it("defaults the server url to the current origin", () => { setup(); const input = screen.getByLabelText("Server URL") as HTMLInputElement; expect(input.value).toBe("http://localhost:3000"); }); it("requires a token", async () => { setup(); await userEvent.click(screen.getByRole("button", { name: "Connect" })); expect(screen.getByRole("alert")).toHaveTextContent("token required"); expect(getSettings()).toBeNull(); }); it("validation failure shows the server error and does not save", async () => { const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(jsonResponse({ error: "unauthorized" }, 401)); setup(); await userEvent.type(screen.getByLabelText("Bearer token"), "bad"); await userEvent.click(screen.getByRole("button", { name: "Connect" })); expect(screen.getByRole("alert")).toHaveTextContent("connection failed (401)"); expect(getSettings()).toBeNull(); expect(fetchMock).toHaveBeenCalledWith( "http://localhost:3000/api/sessions", expect.objectContaining({ headers: { Authorization: "Bearer bad" } }) ); }); it("network rejection surfaces the thrown message", async () => { vi.spyOn(globalThis, "fetch").mockRejectedValue(new TypeError("fetch failed")); setup(); await userEvent.type(screen.getByLabelText("Bearer token"), "tok"); await userEvent.click(screen.getByRole("button", { name: "Connect" })); expect(screen.getByRole("alert")).toHaveTextContent("fetch failed"); }); it("saves normalized url + trimmed token and calls onSaved", async () => { const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(jsonResponse([])); const onSaved = vi.fn(); render(); await userEvent.clear(screen.getByLabelText("Server URL")); await userEvent.type(screen.getByLabelText("Server URL"), "http://daemon:8686///"); await userEvent.type(screen.getByLabelText("Bearer token"), " tok "); await userEvent.click(screen.getByRole("button", { name: "Connect" })); expect(fetchMock).toHaveBeenCalledWith("http://daemon:8686/api/sessions", expect.anything()); expect(getSettings()).toEqual({ serverUrl: "http://daemon:8686", token: "tok" }); expect(onSaved).toHaveBeenCalledTimes(1); }); it("non-Error rejections stringify via String(err)", async () => { vi.spyOn(globalThis, "fetch").mockRejectedValue("plain-string" as never); setup(); await userEvent.type(screen.getByLabelText("Bearer token"), "tok"); await userEvent.click(screen.getByRole("button", { name: "Connect" })); expect(screen.getByRole("alert")).toHaveTextContent("plain-string"); }); it("empty server url falls back to the current origin", async () => { const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(jsonResponse([])); setup(); await userEvent.clear(screen.getByLabelText("Server URL")); await userEvent.type(screen.getByLabelText("Bearer token"), "tok"); await userEvent.click(screen.getByRole("button", { name: "Connect" })); expect(fetchMock).toHaveBeenCalledWith("http://localhost:3000/api/sessions", expect.anything()); expect(getSettings()?.serverUrl).toBe("http://localhost:3000"); }); });