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

feat(web): add createThemeMiddleware Hono middleware (ATB-53)

+125
+112
apps/web/src/middleware/__tests__/theme.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach } from "vitest"; 2 + import { Hono } from "hono"; 3 + import type { WebAppEnv, ResolvedTheme } from "../../lib/theme-resolution.js"; 4 + import { createThemeMiddleware } from "../theme.js"; 5 + 6 + vi.mock("../../lib/theme-resolution.js", () => ({ 7 + resolveTheme: vi.fn(), 8 + })); 9 + 10 + import { resolveTheme } from "../../lib/theme-resolution.js"; 11 + 12 + const mockResolveTheme = vi.mocked(resolveTheme); 13 + 14 + const MOCK_THEME: ResolvedTheme = { 15 + tokens: { "color-bg": "#ffffff", "color-primary": "#ff5c00" }, 16 + cssOverrides: null, 17 + fontUrls: ["https://fonts.example.com/font.css"], 18 + colorScheme: "light", 19 + }; 20 + 21 + describe("createThemeMiddleware", () => { 22 + beforeEach(() => { 23 + mockResolveTheme.mockReset(); 24 + mockResolveTheme.mockResolvedValue(MOCK_THEME); 25 + }); 26 + 27 + it("stores resolved theme in context for downstream handlers", async () => { 28 + let capturedTheme: ResolvedTheme | undefined; 29 + 30 + const app = new Hono<WebAppEnv>() 31 + .use("*", createThemeMiddleware("http://appview.test")) 32 + .get("/test", (c) => { 33 + capturedTheme = c.get("theme"); 34 + return c.json({ ok: true }); 35 + }); 36 + 37 + const res = await app.request("http://localhost/test"); 38 + expect(res.status).toBe(200); 39 + expect(capturedTheme).toEqual(MOCK_THEME); 40 + }); 41 + 42 + it("passes Cookie header to resolveTheme", async () => { 43 + const app = new Hono<WebAppEnv>() 44 + .use("*", createThemeMiddleware("http://appview.test")) 45 + .get("/test", (c) => c.json({ ok: true })); 46 + 47 + await app.request("http://localhost/test", { 48 + headers: { Cookie: "atbb-color-scheme=dark; session=abc" }, 49 + }); 50 + 51 + expect(mockResolveTheme).toHaveBeenCalledWith( 52 + expect.any(String), 53 + "atbb-color-scheme=dark; session=abc", 54 + undefined 55 + ); 56 + }); 57 + 58 + it("passes Sec-CH-Prefers-Color-Scheme header to resolveTheme", async () => { 59 + const app = new Hono<WebAppEnv>() 60 + .use("*", createThemeMiddleware("http://appview.test")) 61 + .get("/test", (c) => c.json({ ok: true })); 62 + 63 + await app.request("http://localhost/test", { 64 + headers: { "Sec-CH-Prefers-Color-Scheme": "dark" }, 65 + }); 66 + 67 + expect(mockResolveTheme).toHaveBeenCalledWith( 68 + expect.any(String), 69 + undefined, 70 + "dark" 71 + ); 72 + }); 73 + 74 + it("passes appviewUrl to resolveTheme", async () => { 75 + const app = new Hono<WebAppEnv>() 76 + .use("*", createThemeMiddleware("http://custom-appview.example.com")) 77 + .get("/test", (c) => c.json({ ok: true })); 78 + 79 + await app.request("http://localhost/test"); 80 + 81 + expect(mockResolveTheme).toHaveBeenCalledWith( 82 + "http://custom-appview.example.com", 83 + undefined, 84 + undefined 85 + ); 86 + }); 87 + 88 + it("calls resolveTheme with undefined when Cookie and color-scheme headers are absent", async () => { 89 + const app = new Hono<WebAppEnv>() 90 + .use("*", createThemeMiddleware("http://appview.test")) 91 + .get("/test", (c) => c.json({ ok: true })); 92 + 93 + await app.request("http://localhost/test"); 94 + 95 + expect(mockResolveTheme).toHaveBeenCalledWith( 96 + "http://appview.test", 97 + undefined, 98 + undefined 99 + ); 100 + }); 101 + 102 + it("calls next() so the downstream handler executes", async () => { 103 + const app = new Hono<WebAppEnv>() 104 + .use("*", createThemeMiddleware("http://appview.test")) 105 + .get("/test", (c) => c.json({ message: "handler ran" })); 106 + 107 + const res = await app.request("http://localhost/test"); 108 + expect(res.status).toBe(200); 109 + const body = await res.json() as { message: string }; 110 + expect(body.message).toBe("handler ran"); 111 + }); 112 + });
+13
apps/web/src/middleware/theme.ts
··· 1 + import type { MiddlewareHandler } from "hono"; 2 + import type { WebAppEnv } from "../lib/theme-resolution.js"; 3 + import { resolveTheme } from "../lib/theme-resolution.js"; 4 + 5 + export function createThemeMiddleware(appviewUrl: string): MiddlewareHandler<WebAppEnv> { 6 + return async (c, next) => { 7 + const cookieHeader = c.req.header("Cookie"); 8 + const colorSchemeHint = c.req.header("Sec-CH-Prefers-Color-Scheme"); 9 + const theme = await resolveTheme(appviewUrl, cookieHeader, colorSchemeHint); 10 + c.set("theme", theme); 11 + await next(); 12 + }; 13 + }