Files
Portfolio/src/app/core/content/content-completeness.spec.ts
Antonio Ledebuhr 86d6dd085d Add the recruiter Pitch route and rebuild Home for direct-customer work.
Pitch takes the verified profile, metrics and public cases off Home so `/` can speak to small companies in plain language. The Systems Map now sits on the services overview, and both locales stay table-driven.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 12:52:12 +02:00

119 lines
4.0 KiB
TypeScript

import { APP_LOCALES } from '../i18n/locale';
import { ROUTE_IDS } from '../routing/route-ids';
import { type SiteContent } from './content.contracts';
import { SITE_CONTENT_DATA } from './site-content';
const TITLE_SUFFIX = ' | Antonio Ledebuhr';
const FORBIDDEN =
/lorem|todo|tbd|platzhaltertext|placeholder text|wird derzeit aufgebaut|being built|coming soon|xxx/i;
function walkStrings(
value: unknown,
visit: (text: string, path: string) => void,
path = '',
skipKeys: ReadonlySet<string> = new Set(),
): void {
if (typeof value === 'string') {
visit(value, path);
return;
}
if (Array.isArray(value)) {
value.forEach((item, index) => walkStrings(item, visit, `${path}[${index}]`, skipKeys));
return;
}
if (value && typeof value === 'object') {
for (const [key, child] of Object.entries(value)) {
if (skipKeys.has(key)) {
continue;
}
walkStrings(child, visit, path ? `${path}.${key}` : key, skipKeys);
}
}
}
describe('content completeness', () => {
it('covers every route in both locales with real page copy', () => {
for (const locale of APP_LOCALES) {
const site = SITE_CONTENT_DATA[locale];
expect(site.pages.home).toBe(site.home);
expect(site.pages.pitch).toBe(site.pitch);
expect(site.pages.services).toBe(site.servicesOverview);
expect(site.pages.contact).toBe(site.contact);
expect(site.pages.projects).toBe(site.projects);
expect(site.pages.stack).toBe(site.stack);
expect(site.pages.imprint).toBe(site.legal.imprint);
expect(site.pages.privacy).toBe(site.legal.privacy);
expect(site.pages.servicesAi).toBe(site.services.servicesAi);
for (const routeId of ROUTE_IDS) {
const page = site.pages[routeId];
expect(page, `${locale}.${routeId}`).toBeTruthy();
expect(page.title.length).toBeGreaterThan(0);
expect(page.title.endsWith(TITLE_SUFFIX)).toBe(true);
expect(page.description.length).toBeGreaterThanOrEqual(60);
expect(page.hero.headline.length).toBeGreaterThan(0);
expect(page.hero.headline.includes(TITLE_SUFFIX)).toBe(false);
if (routeId !== 'notFound') {
const filledSections = page.sections.filter((section) =>
(section.body ?? []).some((paragraph) => paragraph.trim().length > 0),
);
expect(filledSections.length, `${locale}.${routeId} sections`).toBeGreaterThanOrEqual(2);
}
}
}
});
it('keeps the content tree free of scaffolding, except legal review fields', () => {
const skipped = new Set(['reviewNotice', 'reviewTodos']);
for (const locale of APP_LOCALES) {
const site = SITE_CONTENT_DATA[locale];
const hits: string[] = [];
walkStrings(
site,
(text, path) => {
if (FORBIDDEN.test(text)) {
hits.push(`${path}: ${text}`);
}
},
locale,
skipped,
);
expect(hits).toEqual([]);
for (const legalId of ['imprint', 'privacy'] as const) {
const legal = site.legal[legalId];
expect(legal.reviewNotice.trim().length).toBeGreaterThan(20);
expect(legal.reviewTodos.length).toBeGreaterThan(0);
expect(legal.reviewTodos.every((item) => item.trim().length > 0)).toBe(true);
}
}
});
it('does not treat legal review fields as an implicit skip of the rest of the legal pages', () => {
for (const locale of APP_LOCALES) {
const site: SiteContent = SITE_CONTENT_DATA[locale];
for (const legalId of ['imprint', 'privacy'] as const) {
const legal = site.legal[legalId];
const remainderHits: string[] = [];
walkStrings(
{ hero: legal.hero, sections: legal.sections, legalSections: legal.legalSections },
(text, path) => {
if (FORBIDDEN.test(text)) {
remainderHits.push(`${path}: ${text}`);
}
},
`${locale}.${legalId}`,
);
expect(remainderHits).toEqual([]);
}
}
});
});