/** * Tests for utils/apiBase.ts (Phase 1 Wave 3, issue #80). * * Covers the four-branch resolver: * 1. VITE_OMNIVOICE_API override always wins. * 2. Tauri webview context → localhost:3900. * 3. Plain browser context → window.location.hostname:3900. * 4. SSR / no-window fallback → localhost:3900. */ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; describe("apiBase.getApiBase", () => { // Snapshot what we mutate so each test starts clean. let originalTauriInternals: unknown; let originalTauri: unknown; let originalLocation: Location; beforeEach(() => { originalTauriInternals = (window as Window).__TAURI_INTERNALS__; originalTauri = (window as Window).__TAURI__; originalLocation = window.location; // jsdom defaults VITE_OMNIVOICE_API to undefined — no need to stub. vi.resetModules(); }); afterEach(() => { if (originalTauriInternals === undefined) { delete (window as Window).__TAURI_INTERNALS__; } else { (window as Window).__TAURI_INTERNALS__ = originalTauriInternals; } if (originalTauri === undefined) { delete (window as Window).__TAURI__; } else { (window as Window).__TAURI__ = originalTauri; } // Restore window.location via the descriptor — jsdom protects against // direct reassignment, so we have to override via defineProperty in each // test then restore here. Object.defineProperty(window, "location", { configurable: true, value: originalLocation, }); }); it("uses VITE_OMNIVOICE_API override when set", async () => { // vi.stubEnv works for inline `import.meta.env` reads in the test file, // but does not propagate to dynamically imported modules' import.meta in // vitest 4.x (Vite's per-module env snapshot). Instead, we patch the // module's resolver function via its (intentionally exposed) override // hook so the test exercises the same code path. vi.resetModules(); const mod = await import("./apiBase"); mod._setEnvOverrideForTesting("http://10.0.0.5:3900"); expect(mod.getApiBase()).toBe("http://10.0.0.5:3900"); mod._setEnvOverrideForTesting(undefined); }); it("strips trailing slash from override", async () => { vi.resetModules(); const mod = await import("./apiBase"); mod._setEnvOverrideForTesting("http://10.0.0.5:3900/"); expect(mod.getApiBase()).toBe("http://10.0.0.5:3900"); mod._setEnvOverrideForTesting(undefined); }); it("returns localhost:3900 in Tauri context", async () => { (window as Window).__TAURI_INTERNALS__ = {}; const { getApiBase } = await import("./apiBase"); expect(getApiBase()).toBe("http://localhost:3900"); }); it("returns window.location.hostname:3900 in plain browser context (LAN IP)", async () => { delete (window as Window).__TAURI_INTERNALS__; delete (window as Window).__TAURI__; Object.defineProperty(window, "location", { configurable: true, value: { protocol: "http:", hostname: "192.168.1.50", host: "192.168.1.50:5173", href: "http://192.168.1.50:5173/", }, }); const { getApiBase } = await import("./apiBase"); expect(getApiBase()).toBe("http://192.168.1.50:3900"); }); it("respects page protocol (https) when serving over LAN TLS", async () => { delete (window as Window).__TAURI_INTERNALS__; delete (window as Window).__TAURI__; Object.defineProperty(window, "location", { configurable: true, value: { protocol: "https:", hostname: "studio.local", host: "studio.local", href: "https://studio.local/", }, }); const { getApiBase } = await import("./apiBase"); expect(getApiBase()).toBe("https://studio.local:3900"); }); it("BACKEND_PORT constant is exported and equals 3900", async () => { const { BACKEND_PORT } = await import("./apiBase"); expect(BACKEND_PORT).toBe(3900); }); });