import { describe, it, expect } from "vitest"; import { Hono } from "hono"; import { BaseLayout } from "../base.js"; import type { WebSession } from "../../lib/session.js"; const app = new Hono().get("/", (c) => c.html(Page content) ); describe("BaseLayout", () => { it("injects neobrutal tokens as :root CSS custom properties", async () => { const res = await app.request("/"); const html = await res.text(); expect(html).toContain(":root {"); expect(html).toContain("--color-bg:"); expect(html).toContain("--color-primary:"); }); it("loads reset.css and theme.css stylesheets", async () => { const res = await app.request("/"); const html = await res.text(); expect(html).toContain('href="/static/css/reset.css"'); expect(html).toContain('href="/static/css/theme.css"'); }); it("loads Space Grotesk from Google Fonts", async () => { const res = await app.request("/"); const html = await res.text(); expect(html).toContain("fonts.googleapis.com"); expect(html).toContain("Space+Grotesk"); }); it("renders semantic site-header, content-container, and site-footer", async () => { const res = await app.request("/"); const html = await res.text(); expect(html).toContain('class="site-header"'); expect(html).toContain('class="content-container"'); expect(html).toContain('class="site-footer"'); }); it("renders provided page title", async () => { const res = await app.request("/"); const html = await res.text(); expect(html).toContain("Test Page"); }); it("falls back to default title when none provided", async () => { const defaultApp = new Hono().get("/", (c) => c.html(content) ); const res = await defaultApp.request("/"); const html = await res.text(); expect(html).toContain("atBB Forum"); }); it("renders children inside content-container", async () => { const res = await app.request("/"); const html = await res.text(); expect(html).toContain("Page content"); }); it("renders header title link pointing to /", async () => { const res = await app.request("/"); const html = await res.text(); expect(html).toContain('href="/"'); expect(html).toContain('class="site-header__title"'); }); describe("auth-aware navigation", () => { it("shows Log in link when auth is not provided (default unauthenticated)", async () => { const unauthApp = new Hono().get("/", (c) => c.html(content) ); const res = await unauthApp.request("/"); const html = await res.text(); expect(html).toContain('href="/login"'); expect(html).toContain("Log in"); }); it("shows Log in link when auth is explicitly unauthenticated", async () => { const auth: WebSession = { authenticated: false }; const unauthApp = new Hono().get("/", (c) => c.html(content) ); const res = await unauthApp.request("/"); const html = await res.text(); expect(html).toContain('href="/login"'); expect(html).toContain("Log in"); expect(html).not.toContain("Log out"); }); it("shows handle and Log out button when authenticated", async () => { const auth: WebSession = { authenticated: true, did: "did:plc:abc123", handle: "alice.bsky.social", }; const authApp = new Hono().get("/", (c) => c.html(content) ); const res = await authApp.request("/"); const html = await res.text(); expect(html).toContain("alice.bsky.social"); expect(html).toContain("Log out"); expect(html).not.toContain('href="/login"'); }); it("renders logout as a form POST (not a link)", async () => { const auth: WebSession = { authenticated: true, did: "did:plc:abc123", handle: "alice.bsky.social", }; const authApp = new Hono().get("/", (c) => c.html(content) ); const res = await authApp.request("/"); const html = await res.text(); // Logout must be a form POST for CSRF protection, not a plain link expect(html).toContain('action="/logout"'); expect(html).toContain('method="post"'); expect(html).toContain("Log out"); }); }); describe("accessibility", () => { it("renders skip-to-content link before the site header", async () => { const res = await app.request("/"); const html = await res.text(); expect(html).toContain('class="skip-link"'); expect(html).toContain('href="#main-content"'); expect(html).toContain("Skip to main content"); // Skip link must come before header in DOM order const skipLinkPos = html.indexOf("skip-link"); const headerPos = html.indexOf("site-header"); expect(skipLinkPos).toBeLessThan(headerPos); }); it("renders main element with id for skip link target", async () => { const res = await app.request("/"); const html = await res.text(); expect(html).toContain('id="main-content"'); }); it("desktop nav has aria-label for Main navigation", async () => { const res = await app.request("/"); const html = await res.text(); expect(html).toContain('aria-label="Main navigation"'); }); it("mobile nav has distinct aria-label", async () => { const res = await app.request("/"); const html = await res.text(); expect(html).toContain('aria-label="Mobile navigation"'); }); }); describe("favicon", () => { it("includes favicon link in head", async () => { const res = await app.request("/"); const html = await res.text(); expect(html).toContain('rel="icon"'); expect(html).toContain("favicon.svg"); }); }); describe("mobile navigation", () => { it("renders details/summary hamburger menu for mobile", async () => { const res = await app.request("/"); const html = await res.text(); expect(html).toContain("mobile-nav"); expect(html).toContain("mobile-nav__toggle"); }); it("renders desktop nav separately from mobile nav", async () => { const res = await app.request("/"); const html = await res.text(); expect(html).toContain("desktop-nav"); }); it("hamburger has aria-label for accessibility", async () => { const res = await app.request("/"); const html = await res.text(); expect(html).toContain('aria-label="Menu"'); }); it("mobile nav contains login link when not authenticated", async () => { const res = await app.request("/"); const html = await res.text(); // Both mobile and desktop nav should have "Log in" const loginMatches = html.match(/Log in/g); expect(loginMatches!.length).toBe(2); }); it("mobile nav contains auth state when logged in", async () => { const auth: WebSession = { authenticated: true, did: "did:plc:abc123", handle: "alice.bsky.social", }; const authApp = new Hono().get("/", (c) => c.html(content) ); const res = await authApp.request("/"); const html = await res.text(); // Both mobile and desktop nav should have "Log out" const logoutMatches = html.match(/Log out/g); expect(logoutMatches!.length).toBe(2); }); }); });