/*
Unit tests for build page covering buildSemanticPageModelWith. It also guards the documented public contract and regression-sensitive edge cases.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-1311: Compass header sweep — declares the build suite's contract for test-file scaffold coverage.
*/
import { describe, expect, it } from "vitest";
import { buildSemanticPageModelWith } from "../build-page.ts";
import type { SemanticContentReader } from "../build-page.ts";
function makeReader(overrides: Partial = {}): SemanticContentReader {
return {
getPageFrontmatter: async () => ({
title: "Test Page",
description: "A test page",
blocks: [],
}),
getProseBody: async () => "",
getHomeLabel: async () => "Home",
getFaqEntries: async () => [],
getDerivedPrices: () => null,
...overrides,
};
}
const baseArgs = {
pageId: "test-page",
semanticType: "content" as const,
lang: "de",
url: "https://example.com/de/page/",
profile: {
organization: { name: "Org", description: "Desc", url: "https://example.com" },
people: [],
initiatives: [],
},
};
describe("buildSemanticPageModelWith", () => {
it("builds a SemanticPageModel from reader frontmatter", async () => {
const reader = makeReader();
const result = await buildSemanticPageModelWith(reader, baseArgs);
expect(result).not.toBeNull();
expect(result!.title).toBe("Test Page");
expect(result!.description).toBe("A test page");
expect(result!.lang).toBe("de");
});
it("returns null when reader returns null frontmatter and no fallback", async () => {
const reader = makeReader({ getPageFrontmatter: async () => null });
const result = await buildSemanticPageModelWith(reader, baseArgs);
expect(result).toBeNull();
});
it("uses fallbackFrontmatter when reader returns null", async () => {
const reader = makeReader({ getPageFrontmatter: async () => null });
const result = await buildSemanticPageModelWith(reader, {
...baseArgs,
fallbackFrontmatter: { title: "Fallback", description: "Fallback desc", blocks: [] },
});
expect(result).not.toBeNull();
expect(result!.title).toBe("Fallback");
});
it("attaches people for about pages", async () => {
const reader = makeReader();
const result = await buildSemanticPageModelWith(reader, {
...baseArgs,
semanticType: "about",
profile: {
organization: { name: "Org", description: "Desc", url: "https://example.com" },
people: [{ name: "Jane", role: "CEO" }],
initiatives: [],
},
});
expect(result!.people).toEqual([{ name: "Jane", role: "CEO" }]);
});
it("attaches initiatives for projects pages", async () => {
const reader = makeReader();
const result = await buildSemanticPageModelWith(reader, {
...baseArgs,
semanticType: "projects",
profile: {
organization: { name: "Org", description: "Desc", url: "https://example.com" },
people: [],
initiatives: [{ id: "i1", name: "Project", summary: "A project" }],
},
});
expect(result!.initiatives).toEqual([{ id: "i1", name: "Project", summary: "A project" }]);
});
it("attaches FAQ entries for donationContact pages", async () => {
const reader = makeReader({
getFaqEntries: async () => [{ id: "q1", question: "Q?", answer: "A." }],
});
const result = await buildSemanticPageModelWith(reader, {
...baseArgs,
semanticType: "donationContact",
});
expect(result!.faqEntries).toEqual([{ id: "q1", question: "Q?", answer: "A." }]);
});
it("RFC-1197: inline-only faq-list block contributes only its items (mirrors the section's skipped collection read)", async () => {
const reader = makeReader({
getPageFrontmatter: async () => ({
title: "Baked",
description: "baked page",
blocks: [
{
id: "city-qa",
type: "faq-list",
props: {
header: { heading: "Fragen" },
items: [
{ question: "Inline Q?", answer: "Inline A." },
{ question: "Inline Q2?", answer: "Inline A2." },
],
},
},
],
}),
getFaqEntries: async () => [
{ id: "faq/c1", question: "C1?", answer: "C1.", tags: ["contact"] },
{ id: "faq/c2", question: "C2?", answer: "C2.", tags: ["pricing"] },
],
});
const result = await buildSemanticPageModelWith(reader, baseArgs);
expect(
result!.faqEntries,
"items[] without tag -> inline items only; collection read is skipped in the visible section",
).toEqual([
{ id: "faq/city-qa-0", question: "Inline Q?", answer: "Inline A." },
{ id: "faq/city-qa-1", question: "Inline Q2?", answer: "Inline A2." },
]);
});
it("RFC-1197: unions a tag-filtered collection block with an inline-items block", async () => {
const reader = makeReader({
getPageFrontmatter: async () => ({
title: "Mixed",
description: "tag block plus inline block",
blocks: [
{ id: "tagged", type: "faq-list", props: { tag: "contact" } },
{
id: "city-qa",
type: "faq-list",
props: { items: [{ question: "Inline Q?", answer: "Inline A." }] },
},
],
}),
getFaqEntries: async () => [
{ id: "faq/c1", question: "C1?", answer: "C1.", tags: ["contact"] },
{ id: "faq/c2", question: "C2?", answer: "C2.", tags: ["pricing"] },
],
});
const result = await buildSemanticPageModelWith(reader, baseArgs);
expect(result!.faqEntries).toEqual([
{ id: "faq/c1", question: "C1?", answer: "C1.", tags: ["contact"] },
{ id: "faq/city-qa-0", question: "Inline Q?", answer: "Inline A." },
]);
});
it("RFC-1197: emits FAQPage from inline items alone when the collection is empty", async () => {
const reader = makeReader({
getPageFrontmatter: async () => ({
title: "Baked",
description: "baked page",
blocks: [
{
id: "city-qa",
type: "faq-list",
props: {
header: { heading: "Fragen" },
items: [{ question: "Inline Q?", answer: "Inline A." }],
},
},
],
}),
getFaqEntries: async () => [],
});
const result = await buildSemanticPageModelWith(reader, baseArgs);
expect(
result!.faqEntries,
"empty collection must not suppress inline Q&A — pSEO pages rely on it",
).toEqual([{ id: "faq/city-qa-0", question: "Inline Q?", answer: "Inline A." }]);
});
it("RFC-1197: empty tag-filter no longer suppresses the FAQPage when inline items exist", async () => {
const reader = makeReader({
getPageFrontmatter: async () => ({
title: "Baked",
description: "baked page",
blocks: [
{
id: "city-qa",
type: "faq-list",
props: {
header: { heading: "Fragen" },
tag: "nothing-matches",
items: [{ question: "Inline Q?", answer: "Inline A." }],
},
},
],
}),
getFaqEntries: async () => [
{ id: "faq/c1", question: "C1?", answer: "C1.", tags: ["contact"] },
],
});
const result = await buildSemanticPageModelWith(reader, baseArgs);
expect(result!.faqEntries).toEqual([
{ id: "faq/city-qa-0", question: "Inline Q?", answer: "Inline A." },
]);
});
it("m000185: inline faq items resolve =(…) expressions via reader.resolveInlineText", async () => {
const reader = makeReader({
getPageFrontmatter: async () => ({
title: "Offer",
description: "offer page",
blocks: [
{
id: "offer-faq",
type: "faq-list",
props: {
items: [
{
question: "Was kostet das Modul?",
answer:
"=(business-profile.offerings/visibility.pricing.charges.monthlySubscription.amount.value | money) im Monat.",
},
],
},
},
],
}),
resolveInlineText: async (text) => text.replace(/=\([^)]*\|[^)]*\)/, "29 €"),
});
const result = await buildSemanticPageModelWith(reader, baseArgs);
expect(result!.faqEntries).toEqual([
{ id: "faq/offer-faq-0", question: "Was kostet das Modul?", answer: "29 € im Monat." },
]);
expect(result!.faqEntries![0]!.answer).not.toContain("=(");
});
it("m000185: without resolveInlineText, raw inline text passes through unchanged", async () => {
const raw = "=(business-profile.x.y | money) im Monat.";
const reader = makeReader({
getPageFrontmatter: async () => ({
title: "Offer",
description: "offer page",
blocks: [
{
id: "offer-faq",
type: "faq-list",
props: { items: [{ question: "Q?", answer: raw }] },
},
],
}),
});
const result = await buildSemanticPageModelWith(reader, baseArgs);
expect(result!.faqEntries![0]!.answer).toBe(raw);
});
it("uses provided breadcrumbs when supplied", async () => {
const reader = makeReader();
const result = await buildSemanticPageModelWith(reader, {
...baseArgs,
breadcrumbs: [
{ name: "Home", url: "https://example.com/de/" },
{ name: "Section", url: "https://example.com/de/section/" },
{ name: "Page", url: "https://example.com/de/page/" },
],
});
expect(result!.breadcrumbs).toHaveLength(3);
});
it("resolves price markers in heading and description", async () => {
const reader = makeReader({
getPageFrontmatter: async () => ({
title: "Pricing",
description: "Cost: {amount:50}",
blocks: [],
}),
});
const result = await buildSemanticPageModelWith(reader, baseArgs);
expect(result!.description).toContain("€");
expect(result!.description).not.toContain("{amount:");
});
});