import { describe, it, expect } from "bun:test";
import app from "../app";
const HOME = "/";
const LAYOUT_PAGE = "/help";
async function fetchHomeHtml(): Promise<string> {
const res = await app.request(HOME);
expect(res.status).toBe(200);
return await res.text();
}
async function fetchLayoutHtml(): Promise<string> {
const res = await app.request(LAYOUT_PAGE);
expect(res.status).toBe(200);
return await res.text();
}
describe("landing hero — primary CTA contract", () => {
it("renders the primary CTAs in the hero row", async () => {
const body = await fetchHomeHtml();
const start = body.indexOf('class="lp-hero-ctas"');
expect(start).toBeGreaterThan(-1);
const tail = body.slice(start, start + 400);
expect(tail).toContain('href="/register"');
expect(tail).toContain('href="/import"');
const anchorMatches = tail.match(/<a[^>]*class="lp-btn[^"]*"/g) || [];
expect(anchorMatches.length).toBe(2);
});
it("renders the hero links row beneath the primary CTAs", async () => {
const body = await fetchHomeHtml();
const ctas = body.indexOf('class="lp-hero-ctas"');
const links = body.indexOf('class="lp-hero-links"');
expect(ctas).toBeGreaterThan(-1);
expect(links).toBeGreaterThan(ctas);
expect(body.slice(links, links + 200)).toContain('href="/demo"');
});
it("places the product-card mock below the CTAs", async () => {
const body = await fetchHomeHtml();
const ctas = body.indexOf('class="lp-hero-ctas"');
const card = body.indexOf('class="lp-hero-card"');
expect(ctas).toBeGreaterThan(-1);
expect(card).toBeGreaterThan(ctas);
});
});
describe("Block U2 — button polish", () => {
it("includes the universal hover-lift transform on .btn", async () => {
const body = await fetchLayoutHtml();
expect(body).toMatch(/\.btn:hover\b[\s\S]*?transform:\s*translateY\(-1px\)/);
});
it("uses a soft drop shadow on .btn:hover (not just border)", async () => {
const body = await fetchLayoutHtml();
expect(body).toMatch(/\.btn:hover\b[\s\S]*?box-shadow:\s*0\s+4px\s+12px/);
});
it("uses box-shadow (not outline) for the .btn focus ring", async () => {
const body = await fetchLayoutHtml();
expect(body).toMatch(
/\.btn:focus-visible\b[\s\S]*?box-shadow:\s*0\s+0\s+0\s+3px\s+rgba\(91,\s*110,\s*232,\s*0\.35\)/
);
expect(body).not.toMatch(/\.btn:focus-visible[^{]*\{\s*outline:\s*2px/);
});
it("disables hover-lift on disabled buttons", async () => {
const body = await fetchLayoutHtml();
expect(body).toMatch(/\.btn:disabled[\s\S]*?transform:\s*none/);
expect(body).toMatch(/\.btn:disabled[\s\S]*?opacity:\s*0\.5/);
});
it("btn-primary has a gradient background and smooth transitions", async () => {
const body = await fetchLayoutHtml();
expect(body).toMatch(/\.btn-primary\b[\s\S]*?background:\s*var\(--accent-gradient\)/);
expect(body).toMatch(/\.btn-primary\b[\s\S]*?transition:[\s\S]*?transform\s+180ms/);
});
});
describe("Block U4 — view transitions", () => {
it("includes the @view-transition opt-in", async () => {
const body = await fetchLayoutHtml();
expect(body).toContain("@view-transition");
expect(body).toMatch(/@view-transition\s*\{\s*navigation:\s*auto;?\s*\}/);
});
it("declares both fade-out and fade-in keyframes for ::view-transition-*", async () => {
const body = await fetchLayoutHtml();
expect(body).toContain("::view-transition-old(root)");
expect(body).toContain("::view-transition-new(root)");
expect(body).toContain("@keyframes vt-fade-out");
expect(body).toContain("@keyframes vt-fade-in");
});
it("disables the transition under prefers-reduced-motion", async () => {
const body = await fetchLayoutHtml();
expect(body).toMatch(
/@media\s*\(prefers-reduced-motion:\s*reduce\)\s*\{[\s\S]*?::view-transition-(old|new)\(root\)[\s\S]*?animation-duration:\s*0s/
);
});
it("attaches view-transition-name: root to body", async () => {
const body = await fetchLayoutHtml();
expect(body).toMatch(/\bbody\b\s*\{[^}]*view-transition-name:\s*root/);
});
});
|