import { describe, it, expect } from "vitest";
import { Hono } from "hono";
import { BaseLayout } from "../base.js";
import type { WebSession } from "../../lib/session.js";
import { FALLBACK_THEME } from "../../lib/theme-resolution.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();
// css-tree generates compact CSS (no space before brace)
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"');
});
it("includes Accept-CH meta tag for color scheme hint", async () => {
const res = await app.request("/");
const html = await res.text();
expect(html).toContain('http-equiv="Accept-CH"');
expect(html).toContain('content="Sec-CH-Prefers-Color-Scheme"');
});
it("renders cssOverrides in a style tag when non-null", async () => {
const themeWithOverrides = {
...FALLBACK_THEME,
cssOverrides: ".card { border: 2px solid black; }",
};
const overridesApp = new Hono().get("/", (c) =>
c.html(
content
)
);
const res = await overridesApp.request("/");
const html = await res.text();
// css-tree generates compact CSS — check for key selectors and properties
expect(html).toContain(".card{");
expect(html).toContain("border:2px solid black");
});
it("does not render Google Fonts preconnect tags when fontUrls is null", async () => {
const themeNoFonts = { ...FALLBACK_THEME, fontUrls: null };
const noFontsApp = new Hono().get("/", (c) =>
c.html(
content
)
);
const res = await noFontsApp.request("/");
const html = await res.text();
expect(html).not.toContain("fonts.googleapis.com");
});
it("filters out non-https font URLs and does not render them", async () => {
const themeWithUnsafeFontUrl = {
...FALLBACK_THEME,
fontUrls: ["http://evil.com/style.css", "https://fonts.example.com/safe.css"],
};
const unsafeFontApp = new Hono().get("/", (c) =>
c.html(
content
)
);
const res = await unsafeFontApp.request("/");
const html = await res.text();
expect(html).not.toContain("http://evil.com/style.css");
expect(html).toContain("https://fonts.example.com/safe.css");
});
it("does not render cssOverrides style tag when cssOverrides is null", async () => {
const themeNoOverrides = { ...FALLBACK_THEME, cssOverrides: null };
const noOverridesApp = new Hono().get("/", (c) =>
c.html(
content
)
);
const res = await noOverridesApp.request("/");
const html = await res.text();
// The only