WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
at feat/atb-60-theme-import-export 146 lines 4.9 kB view raw
1import { describe, it, expect, vi, beforeEach } from "vitest"; 2import { Hono } from "hono"; 3import type { WebAppEnv, ResolvedTheme } from "../../lib/theme-resolution.js"; 4import { createThemeMiddleware } from "../theme.js"; 5 6vi.mock("../../lib/theme-resolution.js", () => ({ 7 resolveTheme: vi.fn(), 8 FALLBACK_THEME: { 9 tokens: { "color-bg": "#f5f0e8", "color-primary": "#ff5c00" }, 10 cssOverrides: null, 11 fontUrls: ["https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap"], 12 colorScheme: "light", 13 }, 14})); 15 16vi.mock("../../lib/logger.js", () => ({ 17 logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), fatal: vi.fn() }, 18})); 19 20import { resolveTheme, FALLBACK_THEME } from "../../lib/theme-resolution.js"; 21import { logger } from "../../lib/logger.js"; 22 23const mockResolveTheme = vi.mocked(resolveTheme); 24const mockLogger = vi.mocked(logger); 25 26const MOCK_THEME: ResolvedTheme = { 27 tokens: { "color-bg": "#ffffff", "color-primary": "#ff5c00" }, 28 cssOverrides: null, 29 fontUrls: ["https://fonts.example.com/font.css"], 30 colorScheme: "light", 31}; 32 33describe("createThemeMiddleware", () => { 34 beforeEach(() => { 35 mockResolveTheme.mockReset(); 36 mockResolveTheme.mockResolvedValue(MOCK_THEME); 37 mockLogger.error.mockClear(); 38 }); 39 40 it("stores resolved theme in context for downstream handlers", async () => { 41 let capturedTheme: ResolvedTheme | undefined; 42 43 const app = new Hono<WebAppEnv>() 44 .use("*", createThemeMiddleware("http://appview.test")) 45 .get("/test", (c) => { 46 capturedTheme = c.get("theme"); 47 return c.json({ ok: true }); 48 }); 49 50 const res = await app.request("http://localhost/test"); 51 expect(res.status).toBe(200); 52 expect(capturedTheme).toEqual(MOCK_THEME); 53 }); 54 55 it("passes Cookie header to resolveTheme", async () => { 56 const app = new Hono<WebAppEnv>() 57 .use("*", createThemeMiddleware("http://appview.test")) 58 .get("/test", (c) => c.json({ ok: true })); 59 60 await app.request("http://localhost/test", { 61 headers: { Cookie: "atbb-color-scheme=dark; session=abc" }, 62 }); 63 64 expect(mockResolveTheme).toHaveBeenCalledWith( 65 expect.any(String), 66 "atbb-color-scheme=dark; session=abc", 67 undefined 68 ); 69 }); 70 71 it("passes Sec-CH-Prefers-Color-Scheme header to resolveTheme", async () => { 72 const app = new Hono<WebAppEnv>() 73 .use("*", createThemeMiddleware("http://appview.test")) 74 .get("/test", (c) => c.json({ ok: true })); 75 76 await app.request("http://localhost/test", { 77 headers: { "Sec-CH-Prefers-Color-Scheme": "dark" }, 78 }); 79 80 expect(mockResolveTheme).toHaveBeenCalledWith( 81 expect.any(String), 82 undefined, 83 "dark" 84 ); 85 }); 86 87 it("passes appviewUrl to resolveTheme", async () => { 88 const app = new Hono<WebAppEnv>() 89 .use("*", createThemeMiddleware("http://custom-appview.example.com")) 90 .get("/test", (c) => c.json({ ok: true })); 91 92 await app.request("http://localhost/test"); 93 94 expect(mockResolveTheme).toHaveBeenCalledWith( 95 "http://custom-appview.example.com", 96 undefined, 97 undefined 98 ); 99 }); 100 101 it("calls resolveTheme with undefined when Cookie and color-scheme headers are absent", async () => { 102 const app = new Hono<WebAppEnv>() 103 .use("*", createThemeMiddleware("http://appview.test")) 104 .get("/test", (c) => c.json({ ok: true })); 105 106 await app.request("http://localhost/test"); 107 108 expect(mockResolveTheme).toHaveBeenCalledWith( 109 "http://appview.test", 110 undefined, 111 undefined 112 ); 113 }); 114 115 it("calls next() so the downstream handler executes", async () => { 116 const app = new Hono<WebAppEnv>() 117 .use("*", createThemeMiddleware("http://appview.test")) 118 .get("/test", (c) => c.json({ message: "handler ran" })); 119 120 const res = await app.request("http://localhost/test"); 121 expect(res.status).toBe(200); 122 const body = await res.json() as { message: string }; 123 expect(body.message).toBe("handler ran"); 124 }); 125 126 it("catches unexpected throws from resolveTheme, logs the error, and sets FALLBACK_THEME", async () => { 127 mockResolveTheme.mockRejectedValueOnce(new TypeError("Unexpected programming error")); 128 129 let capturedTheme: ResolvedTheme | undefined; 130 const app = new Hono<WebAppEnv>() 131 .use("*", createThemeMiddleware("http://appview.test")) 132 .get("/test", (c) => { 133 capturedTheme = c.get("theme"); 134 return c.json({ ok: true }); 135 }); 136 137 // The request must NOT propagate the error — the middleware should catch it 138 const res = await app.request("http://localhost/test"); 139 expect(res.status).toBe(200); 140 expect(capturedTheme).toEqual(FALLBACK_THEME); 141 expect(mockLogger.error).toHaveBeenCalledWith( 142 expect.stringContaining("resolveTheme threw unexpectedly"), 143 expect.objectContaining({ operation: "createThemeMiddleware" }) 144 ); 145 }); 146});