import { describe, it, expect, vi, beforeEach } from "vitest"; import { Hono } from "hono"; import type { WebAppEnv, ResolvedTheme } from "../../lib/theme-resolution.js"; import { createThemeMiddleware } from "../theme.js"; vi.mock("../../lib/theme-resolution.js", () => ({ resolveTheme: vi.fn(), FALLBACK_THEME: { tokens: { "color-bg": "#f5f0e8", "color-primary": "#ff5c00" }, cssOverrides: null, fontUrls: ["https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap"], colorScheme: "light", }, })); vi.mock("../../lib/logger.js", () => ({ logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), fatal: vi.fn() }, })); import { resolveTheme, FALLBACK_THEME } from "../../lib/theme-resolution.js"; import { logger } from "../../lib/logger.js"; const mockResolveTheme = vi.mocked(resolveTheme); const mockLogger = vi.mocked(logger); const MOCK_THEME: ResolvedTheme = { tokens: { "color-bg": "#ffffff", "color-primary": "#ff5c00" }, cssOverrides: null, fontUrls: ["https://fonts.example.com/font.css"], colorScheme: "light", }; describe("createThemeMiddleware", () => { beforeEach(() => { mockResolveTheme.mockReset(); mockResolveTheme.mockResolvedValue(MOCK_THEME); mockLogger.error.mockClear(); }); it("stores resolved theme in context for downstream handlers", async () => { let capturedTheme: ResolvedTheme | undefined; const app = new Hono() .use("*", createThemeMiddleware("http://appview.test")) .get("/test", (c) => { capturedTheme = c.get("theme"); return c.json({ ok: true }); }); const res = await app.request("http://localhost/test"); expect(res.status).toBe(200); expect(capturedTheme).toEqual(MOCK_THEME); }); it("passes Cookie header to resolveTheme", async () => { const app = new Hono() .use("*", createThemeMiddleware("http://appview.test")) .get("/test", (c) => c.json({ ok: true })); await app.request("http://localhost/test", { headers: { Cookie: "atbb-color-scheme=dark; session=abc" }, }); expect(mockResolveTheme).toHaveBeenCalledWith( expect.any(String), "atbb-color-scheme=dark; session=abc", undefined ); }); it("passes Sec-CH-Prefers-Color-Scheme header to resolveTheme", async () => { const app = new Hono() .use("*", createThemeMiddleware("http://appview.test")) .get("/test", (c) => c.json({ ok: true })); await app.request("http://localhost/test", { headers: { "Sec-CH-Prefers-Color-Scheme": "dark" }, }); expect(mockResolveTheme).toHaveBeenCalledWith( expect.any(String), undefined, "dark" ); }); it("passes appviewUrl to resolveTheme", async () => { const app = new Hono() .use("*", createThemeMiddleware("http://custom-appview.example.com")) .get("/test", (c) => c.json({ ok: true })); await app.request("http://localhost/test"); expect(mockResolveTheme).toHaveBeenCalledWith( "http://custom-appview.example.com", undefined, undefined ); }); it("calls resolveTheme with undefined when Cookie and color-scheme headers are absent", async () => { const app = new Hono() .use("*", createThemeMiddleware("http://appview.test")) .get("/test", (c) => c.json({ ok: true })); await app.request("http://localhost/test"); expect(mockResolveTheme).toHaveBeenCalledWith( "http://appview.test", undefined, undefined ); }); it("calls next() so the downstream handler executes", async () => { const app = new Hono() .use("*", createThemeMiddleware("http://appview.test")) .get("/test", (c) => c.json({ message: "handler ran" })); const res = await app.request("http://localhost/test"); expect(res.status).toBe(200); const body = await res.json() as { message: string }; expect(body.message).toBe("handler ran"); }); it("catches unexpected throws from resolveTheme, logs the error, and sets FALLBACK_THEME", async () => { mockResolveTheme.mockRejectedValueOnce(new TypeError("Unexpected programming error")); let capturedTheme: ResolvedTheme | undefined; const app = new Hono() .use("*", createThemeMiddleware("http://appview.test")) .get("/test", (c) => { capturedTheme = c.get("theme"); return c.json({ ok: true }); }); // The request must NOT propagate the error — the middleware should catch it const res = await app.request("http://localhost/test"); expect(res.status).toBe(200); expect(capturedTheme).toEqual(FALLBACK_THEME); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining("resolveTheme threw unexpectedly"), expect.objectContaining({ operation: "createThemeMiddleware" }) ); }); });