Files
site-builder/craft/src/sitesmith/catalog.drift.test.ts
T
shadowdaoandClaude Sonnet 5.5 01a78c9b3d catalog: omit the wall-clock-derived Countdown.targetDate default so the generated catalog (and drift test) no longer flip daily
Add a date-independence test (freezes Date at two different days via a preload).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 16:28:48 -08:00

79 lines
4.3 KiB
TypeScript

// @vitest-environment node
/**
* Drift guard for the Sitesmith catalog. The panel ships a generated file
* (whp: web-files/libs/sitesmith-catalog.generated.json) built from THIS
* editor's real component / section / template definitions. This test
* regenerates the catalog in a child process and compares it byte-for-byte to
* the committed file, so adding or changing a component, prop, enum, section
* family or template without regenerating FAILS here.
*
* Where the committed file lives:
* $SITESMITH_CATALOG_FILE (absolute path), else
* ../../whp/web-files/libs/sitesmith-catalog.generated.json relative to craft/
*
* Regenerate after an editor change:
* node scripts/gen-sitesmith-catalog.mjs --out <path-to-whp>/web-files/libs/sitesmith-catalog.generated.json
* then commit the whp file alongside the site-builder change.
*/
import { describe, test, expect } from 'vitest';
// No @types/node in this package (same trick as ColumnLayout.stack-parity.test.tsx).
const proc = (globalThis as any).process;
const fs = proc.getBuiltinModule('node:fs');
const path = proc.getBuiltinModule('node:path');
const { execFileSync } = proc.getBuiltinModule('node:child_process');
const craftRoot: string = path.resolve(decodeURIComponent(new URL('.', import.meta.url).pathname), '../..');
const committedPath: string = proc.env.SITESMITH_CATALOG_FILE
?? path.resolve(craftRoot, '../../whp/web-files/libs/sitesmith-catalog.generated.json');
function regenerate(fakeNow?: string): string {
const args = ['scripts/gen-sitesmith-catalog.mjs', '--stdout'];
if (fakeNow) args.unshift('-r', path.join(craftRoot, 'scripts/fake-date-preload.cjs'));
return execFileSync(proc.execPath, args, {
env: fakeNow ? { ...proc.env, FAKE_NOW: fakeNow } : proc.env,
cwd: craftRoot, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, timeout: 120_000,
});
}
describe('sitesmith catalog', () => {
test('committed generated catalog matches the editor definitions (no drift)', () => {
if (!fs.existsSync(committedPath)) {
throw new Error(`Committed catalog not found at ${committedPath}. Set SITESMITH_CATALOG_FILE to the whp worktree path, or generate it with: node scripts/gen-sitesmith-catalog.mjs --out ${committedPath}`);
}
const fresh = regenerate();
const committed = fs.readFileSync(committedPath, 'utf8');
if (fresh !== committed) {
const a = JSON.parse(fresh), b = JSON.parse(committed);
const diff = Object.keys(a.components).filter((k) => JSON.stringify(a.components[k]) !== JSON.stringify(b.components?.[k]));
throw new Error(`Sitesmith catalog drift. Regenerate with: node scripts/gen-sitesmith-catalog.mjs --out ${committedPath}\nChanged/new components: ${diff.join(', ') || '(none; sections/templates/tokens/palettes differ)'}`);
}
expect(fresh).toBe(committed);
}, 130_000);
test('catalog is date-independent (Countdown default date is volatile and must not leak in)', () => {
const a = regenerate('2026-12-01T12:00:00Z');
const b = regenerate('2027-03-09T12:00:00Z');
expect(a).toBe(b);
expect(JSON.parse(a).components.Countdown.props.targetDate.default).toBeUndefined();
expect(a).not.toMatch(/"\d{4}-\d{2}-\d{2}"/);
}, 130_000);
test('catalog covers every resolver component, with derived facts', () => {
const cat = JSON.parse(regenerate());
const resolver = fs.readFileSync(path.join(craftRoot, 'src/components/resolver.ts'), 'utf8');
const names = [...resolver.matchAll(/^ ([A-Z]\w+),$/gm)].map((m: RegExpMatchArray) => m[1]);
expect(Object.keys(cat.components).sort()).toEqual(names.sort());
expect(cat.components.Icon.icon).toBe('fontawesome');
expect(cat.components.FeaturesGrid.icon).toBe('text'); // plain-text icon, not Font Awesome
expect(cat.components.ColumnLayout.children).toBe('columns');
expect(cat.components.ColumnLayout.props.split.enum).toContain('50-50');
expect(cat.components.HeroSimple.animation).toBeUndefined();
expect(cat.components.Heading.animation).toBe(true);
expect(cat.components.HeroSimple.props.bgGradientFrom).toMatchObject({ color: true, tokenDefault: true });
expect(cat.common.props.animation).toContain('fade-in');
expect(cat.tokens.colorTokens).toContain('var(--wsb-primary)');
expect(cat.sections.length).toBeGreaterThan(40);
expect(cat.templates.length).toBe(16);
}, 130_000);
});