Files
site-builder/craft/src/sections/families/a-content.test.tsx
T
shadowdao 74dcbb003b feat(site-builder): add hero, services, about and cta sections to the library
Token-based sections (21 total across four families) with tests covering
insert, export (no stray hex, alt on images, relative links) and tree shape.

Refs #52
2026-10-07 12:15:12 -07:00

95 lines
4.2 KiB
TypeScript

import { describe, test, expect, afterEach } from 'vitest';
import { renderEditorHarness, EditorHarness } from '../../test-utils/editorHarness';
import { exportBodyHtml } from '../../utils/html-export';
import { insertSection, type SectionDef } from '../catalog';
import { HERO_SECTIONS, SERVICES_SECTIONS, ABOUT_SECTIONS, CTA_SECTIONS } from './index';
import type { TemplateComponent } from '../../templates/definitions';
let h: EditorHarness;
afterEach(() => h?.unmount());
const FAMILIES: Array<[string, SectionDef[]]> = [
['hero', HERO_SECTIONS],
['services', SERVICES_SECTIONS],
['about', ABOUT_SECTIONS],
['cta', CTA_SECTIONS],
];
/** FeaturesGrid hardcodes card colours in its own render/toHtml (no colour props), so the
* skeleton's `services-grid` cannot be token-clean; `services-icon-cards` is the token-clean
* equivalent. Only the exported-HTML hex check is skipped (the tree itself is still checked). */
const HEX_EXEMPT_EXPORT = new Set(['services-grid']);
const ALL = FAMILIES.flatMap(([, defs]) => defs);
function walk(node: TemplateComponent, fn: (n: TemplateComponent) => void) {
fn(node);
(node.children || []).forEach((c) => walk(c, fn));
}
/** Strip the places a hex colour is legitimate, then look for any left. */
export function strayHex(html: string): string[] {
const cleaned = html
.replace(/data:image\/svg[^"')\s]*/g, '')
.replace(/linear-gradient\([^;"]*\)/g, '');
return cleaned.match(/#[0-9a-fA-F]{6}\b/g) || [];
}
describe('A-content section families', () => {
test('about 18+ sections overall, 3+ layouts per family', () => {
expect(ALL.length).toBeGreaterThanOrEqual(18);
for (const [fam, defs] of FAMILIES) {
expect(defs.length, fam).toBeGreaterThanOrEqual(3);
for (const d of defs) expect(d.family).toBe(fam);
}
});
test('ids are unique and prefixed by family; keywords >= 3', () => {
expect(new Set(ALL.map((s) => s.id)).size).toBe(ALL.length);
for (const s of ALL) {
expect(s.id.startsWith(`${s.family}-`), s.id).toBe(true);
expect(s.keywords.length, s.id).toBeGreaterThanOrEqual(3);
}
});
test('tree shape: top-level block, anchorId blank, no animation, relative links, alt on images, no hex in props', () => {
const tops = new Set(['Section', 'HeroSimple', 'CTASection', 'CallToAction']);
for (const s of ALL) {
expect(tops.has(s.tree.type), s.id).toBe(true);
expect(s.tree.props.anchorId, s.id).toBe('');
walk(s.tree, (n) => {
expect(n.props.animation, s.id).toBeUndefined();
if (n.type === 'ImageBlock') expect(String(n.props.alt || '').length, `${s.id} alt`).toBeGreaterThan(0);
for (const k of ['href', 'buttonUrl']) {
const v = n.props[k];
if (typeof v === 'string' && v) expect(v === '#' || /^[a-z0-9][a-z0-9\-_/.]*$/i.test(v), `${s.id} ${k}=${v}`).toBe(true);
}
for (const c of (n.props.ctas as Array<{ href: string }> | undefined) || []) {
expect(c.href === '#' || /^[a-z0-9][a-z0-9\-_/.]*$/i.test(c.href), `${s.id} cta ${c.href}`).toBe(true);
}
});
expect(strayHex(JSON.stringify(s.tree)), `${s.id} tree`).toEqual([]);
}
});
for (const def of ALL) {
test(`${def.id}: inserts and exports with no stray hex and alt on every image`, () => {
h = renderEditorHarness();
h.act(() => { insertSection(h.query, h.actions, def); });
expect(h.query.node('ROOT').get().data.nodes.length).toBe(1);
const out = exportBodyHtml(h.getSerialized()).html;
expect(out).not.toContain('Export failed');
expect(out.length).toBeGreaterThan(50);
if (!HEX_EXEMPT_EXPORT.has(def.id)) expect(strayHex(out), 'stray hex').toEqual([]);
for (const img of out.match(/<img\b[^>]*>/g) || []) expect(img, img).toMatch(/\balt="[^"]+"/);
expect(out).toContain('var(--wsb-');
for (const href of [...out.matchAll(/\shref="([^"]*)"/g)].map((m) => m[1])) {
expect(href === '#' || /^[a-z0-9][a-z0-9\-_/.]*$/i.test(href), `href ${href}`).toBe(true);
}
});
}
test('strayHex detector can fail (guards the guard)', () => {
expect(strayHex('<p style="color:#ff0000">x</p>')).toEqual(['#ff0000']);
expect(strayHex('background:linear-gradient(135deg,#112233 0%,#445566 100%)')).toEqual([]);
});
});