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);
});
});
});