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

test: settings preview endpoint and HTMX attribute tests

+139
+139
apps/web/src/routes/__tests__/settings.test.tsx
··· 162 162 expect(html).toContain("Neobrutal Dark"); 163 163 }); 164 164 165 + it("renders selects with hx-get attribute when allowUserChoice is true", async () => { 166 + setupAuthenticatedSessionGet(); 167 + const routes = await loadSettingsRoutes(); 168 + const res = await routes.request("/settings", { 169 + headers: { cookie: "atbb_session=token" }, 170 + }); 171 + expect(res.status).toBe(200); 172 + const html = await res.text(); 173 + expect(html).toContain('hx-get="/settings/preview"'); 174 + }); 175 + 165 176 it("GET /settings with ?saved=1 shows success banner", async () => { 166 177 setupAuthenticatedSessionGet(); 167 178 const routes = await loadSettingsRoutes(); ··· 399 410 expect(res.status).toBe(302); 400 411 const cookies = res.headers.getSetCookie?.() ?? []; 401 412 expect(cookies.length).toBe(0); 413 + }); 414 + 415 + // ── GET /settings/preview ──────────────────────────────────────────────────── 416 + 417 + describe("GET /settings/preview", () => { 418 + it("returns empty fragment when no query params provided", async () => { 419 + const routes = await loadSettingsRoutes(); 420 + const res = await routes.request("/settings/preview"); 421 + expect(res.status).toBe(200); 422 + const html = await res.text(); 423 + expect(html).toBe('<div id="theme-preview"></div>'); 424 + // Verify no fetch was made 425 + expect(mockFetch).not.toHaveBeenCalled(); 426 + }); 427 + 428 + it("returns empty fragment when lightThemeUri is malformed (no slash separators)", async () => { 429 + const routes = await loadSettingsRoutes(); 430 + const res = await routes.request( 431 + "/settings/preview?lightThemeUri=not-a-uri" 432 + ); 433 + expect(res.status).toBe(200); 434 + const html = await res.text(); 435 + expect(html).toBe('<div id="theme-preview"></div>'); 436 + // Verify no fetch was made for malformed URI 437 + expect(mockFetch).not.toHaveBeenCalled(); 438 + }); 439 + 440 + it("returns swatch preview for valid lightThemeUri", async () => { 441 + const themeResponse = { 442 + name: "Clean Light", 443 + colorScheme: "light", 444 + tokens: { 445 + "color-bg": "#f5f0e8", 446 + "color-surface": "#fff", 447 + "color-primary": "#ff5c00", 448 + "color-text": "#1a1a1a", 449 + "color-border": "#000", 450 + }, 451 + }; 452 + mockFetch.mockResolvedValueOnce(mockResponse(themeResponse)); 453 + 454 + const routes = await loadSettingsRoutes(); 455 + const res = await routes.request( 456 + "/settings/preview?lightThemeUri=at://did:plc:forum/space.atbb.forum.theme/3lbllight" 457 + ); 458 + expect(res.status).toBe(200); 459 + const html = await res.text(); 460 + expect(html).toContain('id="theme-preview"'); 461 + expect(html).toContain("Clean Light"); 462 + expect(html).toContain('class="theme-preview__swatch"'); 463 + // Verify the five swatch spans are present 464 + const swatchCount = (html.match(/class="theme-preview__swatch"/g) || []).length; 465 + expect(swatchCount).toBe(5); 466 + }); 467 + 468 + it("returns swatch preview for valid darkThemeUri", async () => { 469 + const themeResponse = { 470 + name: "Neobrutal Dark", 471 + colorScheme: "dark", 472 + tokens: { 473 + "color-bg": "#1a1a1a", 474 + "color-surface": "#2a2a2a", 475 + "color-primary": "#ff5c00", 476 + "color-text": "#f5f0e8", 477 + "color-border": "#3a3a3a", 478 + }, 479 + }; 480 + mockFetch.mockResolvedValueOnce(mockResponse(themeResponse)); 481 + 482 + const routes = await loadSettingsRoutes(); 483 + const res = await routes.request( 484 + "/settings/preview?darkThemeUri=at://did:plc:forum/space.atbb.forum.theme/3lbldark" 485 + ); 486 + expect(res.status).toBe(200); 487 + const html = await res.text(); 488 + expect(html).toContain('id="theme-preview"'); 489 + expect(html).toContain("Neobrutal Dark"); 490 + expect(html).toContain('class="theme-preview__swatch"'); 491 + }); 492 + 493 + it("returns empty fragment when /api/themes/:rkey returns non-ok status", async () => { 494 + mockFetch.mockResolvedValueOnce(mockResponse({}, false, 404)); 495 + 496 + const routes = await loadSettingsRoutes(); 497 + const res = await routes.request( 498 + "/settings/preview?lightThemeUri=at://did:plc:forum/space.atbb.forum.theme/unknown" 499 + ); 500 + expect(res.status).toBe(200); 501 + const html = await res.text(); 502 + expect(html).toBe('<div id="theme-preview"></div>'); 503 + }); 504 + 505 + it("returns empty fragment when fetch throws error (network failure)", async () => { 506 + mockFetch.mockRejectedValueOnce(new Error("Network error")); 507 + 508 + const routes = await loadSettingsRoutes(); 509 + const res = await routes.request( 510 + "/settings/preview?lightThemeUri=at://did:plc:forum/space.atbb.forum.theme/3lbllight" 511 + ); 512 + expect(res.status).toBe(200); 513 + const html = await res.text(); 514 + expect(html).toBe('<div id="theme-preview"></div>'); 515 + }); 516 + 517 + it("includes swatch color values in style attribute", async () => { 518 + const themeResponse = { 519 + name: "Test Theme", 520 + tokens: { 521 + "color-bg": "#ffffff", 522 + "color-surface": "#f0f0f0", 523 + "color-primary": "#0066ff", 524 + "color-text": "#000000", 525 + "color-border": "#cccccc", 526 + }, 527 + }; 528 + mockFetch.mockResolvedValueOnce(mockResponse(themeResponse)); 529 + 530 + const routes = await loadSettingsRoutes(); 531 + const res = await routes.request( 532 + "/settings/preview?lightThemeUri=at://did:plc:forum/space.atbb.forum.theme/test" 533 + ); 534 + expect(res.status).toBe(200); 535 + const html = await res.text(); 536 + expect(html).toContain('style="background:#ffffff"'); 537 + expect(html).toContain('style="background:#0066ff"'); 538 + expect(html).toContain('title="color-bg"'); 539 + expect(html).toContain('title="color-primary"'); 540 + }); 402 541 }); 403 542 });