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 root/atb-56-theme-caching-layer 155 lines 5.6 kB view raw
1import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; 2import { ThemeCache, type CachedPolicy, type CachedTheme } from "../theme-cache.js"; 3 4const TTL_MS = 5 * 60 * 1000; // 5 minutes 5 6const MOCK_POLICY: CachedPolicy = { 7 defaultLightThemeUri: "at://did:plc:forum/space.atbb.forum.theme/3lbllight", 8 defaultDarkThemeUri: "at://did:plc:forum/space.atbb.forum.theme/3lbldark", 9 allowUserChoice: true, 10 availableThemes: [ 11 { uri: "at://did:plc:forum/space.atbb.forum.theme/3lbllight", cid: "bafylight" }, 12 { uri: "at://did:plc:forum/space.atbb.forum.theme/3lbldark", cid: "bafydark" }, 13 ], 14}; 15 16const MOCK_THEME: CachedTheme = { 17 cid: "bafylight", 18 tokens: { "color-bg": "#fff" }, 19 cssOverrides: null, 20 fontUrls: null, 21}; 22 23describe("ThemeCache — policy", () => { 24 let cache: ThemeCache; 25 26 beforeEach(() => { 27 vi.useFakeTimers(); 28 cache = new ThemeCache(TTL_MS); 29 }); 30 31 afterEach(() => { 32 vi.useRealTimers(); 33 }); 34 35 it("returns null when policy has not been set", () => { 36 expect(cache.getPolicy()).toBeNull(); 37 }); 38 39 it("returns policy immediately after setting", () => { 40 cache.setPolicy(MOCK_POLICY); 41 expect(cache.getPolicy()).toEqual(MOCK_POLICY); 42 }); 43 44 it("returns null after TTL expires", () => { 45 cache.setPolicy(MOCK_POLICY); 46 vi.advanceTimersByTime(TTL_MS + 1); 47 expect(cache.getPolicy()).toBeNull(); 48 }); 49 50 it("returns policy just before TTL expires", () => { 51 cache.setPolicy(MOCK_POLICY); 52 vi.advanceTimersByTime(TTL_MS - 1); 53 expect(cache.getPolicy()).toEqual(MOCK_POLICY); 54 }); 55 56 it("can be refreshed before expiry (re-set resets TTL)", () => { 57 cache.setPolicy(MOCK_POLICY); 58 vi.advanceTimersByTime(TTL_MS - 1); 59 cache.setPolicy({ ...MOCK_POLICY, allowUserChoice: false }); 60 vi.advanceTimersByTime(TTL_MS - 1); 61 // Total elapsed: (TTL-1) + (TTL-1) = 2*TTL - 2ms, but the re-set happened at TTL-1 62 // so the new entry expires at (TTL-1) + TTL = 2*TTL - 1ms from start 63 const result = cache.getPolicy(); 64 expect(result).not.toBeNull(); 65 expect(result!.allowUserChoice).toBe(false); 66 }); 67}); 68 69describe("ThemeCache — themes", () => { 70 let cache: ThemeCache; 71 72 beforeEach(() => { 73 vi.useFakeTimers(); 74 cache = new ThemeCache(TTL_MS); 75 }); 76 77 afterEach(() => { 78 vi.useRealTimers(); 79 }); 80 81 it("returns null on cache miss", () => { 82 expect( 83 cache.getTheme("at://did:plc:forum/space.atbb.forum.theme/3lbllight", "light") 84 ).toBeNull(); 85 }); 86 87 it("returns theme immediately after setting", () => { 88 const uri = "at://did:plc:forum/space.atbb.forum.theme/3lbllight"; 89 cache.setTheme(uri, "light", MOCK_THEME); 90 expect(cache.getTheme(uri, "light")).toEqual(MOCK_THEME); 91 }); 92 93 it("returns null after TTL expires", () => { 94 const uri = "at://did:plc:forum/space.atbb.forum.theme/3lbllight"; 95 cache.setTheme(uri, "light", MOCK_THEME); 96 vi.advanceTimersByTime(TTL_MS + 1); 97 expect(cache.getTheme(uri, "light")).toBeNull(); 98 }); 99 100 it("treats light and dark as separate cache entries", () => { 101 const uri = "at://did:plc:forum/space.atbb.forum.theme/shared"; 102 const lightTheme: CachedTheme = { cid: "bafylight", tokens: { "color-bg": "#fff" }, cssOverrides: null, fontUrls: null }; 103 const darkTheme: CachedTheme = { cid: "bafydark", tokens: { "color-bg": "#111" }, cssOverrides: null, fontUrls: null }; 104 105 cache.setTheme(uri, "light", lightTheme); 106 cache.setTheme(uri, "dark", darkTheme); 107 108 expect(cache.getTheme(uri, "light")).toEqual(lightTheme); 109 expect(cache.getTheme(uri, "dark")).toEqual(darkTheme); 110 }); 111 112 it("different URIs are stored independently", () => { 113 const lightUri = "at://did/col/light"; 114 const darkUri = "at://did/col/dark"; 115 const lightTheme: CachedTheme = { cid: "bafylight", tokens: { "color-bg": "#fff" }, cssOverrides: null, fontUrls: null }; 116 const darkTheme: CachedTheme = { cid: "bafydark", tokens: { "color-bg": "#111" }, cssOverrides: null, fontUrls: null }; 117 118 cache.setTheme(lightUri, "light", lightTheme); 119 cache.setTheme(darkUri, "dark", darkTheme); 120 121 expect(cache.getTheme(lightUri, "light")).toEqual(lightTheme); 122 expect(cache.getTheme(darkUri, "dark")).toEqual(darkTheme); 123 expect(cache.getTheme(lightUri, "dark")).toBeNull(); 124 expect(cache.getTheme(darkUri, "light")).toBeNull(); 125 }); 126 127 it("deleteTheme removes a specific entry before TTL", () => { 128 const uri = "at://did/col/theme"; 129 cache.setTheme(uri, "light", MOCK_THEME); 130 cache.deleteTheme(uri, "light"); 131 expect(cache.getTheme(uri, "light")).toBeNull(); 132 }); 133 134 it("deleteTheme only removes the targeted uri:colorScheme entry", () => { 135 const uri = "at://did/col/theme"; 136 const darkTheme: CachedTheme = { cid: "bafydark", tokens: { "color-bg": "#111" }, cssOverrides: null, fontUrls: null }; 137 cache.setTheme(uri, "light", MOCK_THEME); 138 cache.setTheme(uri, "dark", darkTheme); 139 cache.deleteTheme(uri, "light"); 140 expect(cache.getTheme(uri, "light")).toBeNull(); 141 expect(cache.getTheme(uri, "dark")).toEqual(darkTheme); 142 }); 143 144 it("evicts stale entry from the map on expired access", () => { 145 const uri = "at://did/col/theme"; 146 cache.setTheme(uri, "light", MOCK_THEME); 147 vi.advanceTimersByTime(TTL_MS + 1); 148 // Access after expiry 149 expect(cache.getTheme(uri, "light")).toBeNull(); 150 // After eviction, setting a new entry works correctly 151 const newTheme = { ...MOCK_THEME, cid: "bafynew" }; 152 cache.setTheme(uri, "light", newTheme); 153 expect(cache.getTheme(uri, "light")).toEqual(newTheme); 154 }); 155});