diff --git a/craft/src/templates/definitions.tokens-D.test.ts b/craft/src/templates/definitions.tokens-D.test.ts new file mode 100644 index 0000000..c1ab595 --- /dev/null +++ b/craft/src/templates/definitions.tokens-D.test.ts @@ -0,0 +1,124 @@ +import { describe, test, expect, afterEach } from 'vitest'; +import { renderEditorHarness, EditorHarness } from '../test-utils/editorHarness'; +import { allTemplates, } from './index'; +import type { TemplateComponent } from './definitions'; +import { templateComponentToTreeNode } from './apply-template'; +import { buildNodeTree } from '../utils/craft-tree'; +import { exportBodyHtml, exportToHtml } from '../utils/html-export'; +import { DEFAULT_SITE_DESIGN } from '../state/SiteDesignContext'; + +/** + * Group D (coming-soon, church, non-profit, fitness-gym) must carry its + * palette/fonts as `var(--wsb-*)` token refs, not hard-coded copies of its own + * `design` values -- otherwise changing Site Design after applying the + * template would not restyle it. + */ + +const IDS = ['coming-soon', 'church', 'non-profit', 'fitness-gym'] as const; +const COLOR_KEYS = [ + 'primaryColor', 'secondaryColor', 'accentColor', 'backgroundColor', 'textColor', + 'mutedTextColor', 'borderColor', 'linkColor', +] as const; +const FONT_KEYS = ['headingFont', 'bodyFont'] as const; + +function tplOf(id: string) { + return allTemplates.find((t) => t.id === id)!; +} + +/** Every string leaf in a props tree, with its dotted path. */ +function stringLeaves(v: unknown, path: string, out: { path: string; value: string }[]) { + if (typeof v === 'string') out.push({ path, value: v }); + else if (Array.isArray(v)) v.forEach((x, i) => stringLeaves(x, `${path}[${i}]`, out)); + else if (v && typeof v === 'object') { + for (const [k, x] of Object.entries(v)) stringLeaves(x, `${path}.${k}`, out); + } +} + +function collectLeaves(comps: TemplateComponent[], base: string, out: { path: string; value: string }[]) { + comps.forEach((c, i) => { + const p = `${base}/${c.type}[${i}]`; + stringLeaves(c.props, `${p}.props`, out); + if (c.children) collectLeaves(c.children, p, out); + }); +} + +function allLeaves(id: string) { + const t = tplOf(id); + const out: { path: string; value: string }[] = []; + collectLeaves(t.header.components, 'header', out); + collectLeaves(t.footer.components, 'footer', out); + for (const p of t.pages) collectLeaves(p.content.components, `page:${p.slug}`, out); + return out; +} + +let harness: EditorHarness | null = null; +afterEach(() => { + harness?.unmount(); + harness = null; +}); + +describe.each(IDS)('tokens-D: %s', (id) => { + const t = tplOf(id); + const design = t.design as Record; + + test('template exists', () => { + expect(t).toBeDefined(); + }); + + test('no prop string equals one of the template\'s own design colours or fonts', () => { + const own = new Set(); + for (const k of [...COLOR_KEYS, ...FONT_KEYS]) { + if (design[k]) own.add(design[k].toLowerCase()); + } + const offenders = allLeaves(id).filter((l) => own.has(l.value.trim().toLowerCase())); + expect(offenders).toEqual([]); + }); + + test('the primary hex never appears inside any prop string (gradients included)', () => { + const primary = design.primaryColor.toLowerCase(); + const offenders = allLeaves(id).filter((l) => l.value.toLowerCase().includes(primary)); + expect(offenders).toEqual([]); + }); + + test('uses token refs and no colour-mix / relative colour syntax', () => { + const leaves = allLeaves(id); + expect(leaves.some((l) => l.value.includes('var(--wsb-primary)'))).toBe(true); + expect(leaves.some((l) => l.value.includes('var(--wsb-secondary)'))).toBe(true); + expect(leaves.filter((l) => /color-mix\(|\bfrom\s+var\(/.test(l.value))).toEqual([]); + }); + + test('applies through templateComponentToTreeNode + buildNodeTree in a real editor without throwing', () => { + harness = renderEditorHarness(); + for (const page of t.pages) { + for (const comp of page.content.components) { + expect(() => { + harness!.act(() => { + const tree = buildNodeTree(harness!.query, templateComponentToTreeNode(comp)); + harness!.actions.addNodeTree(tree, 'ROOT'); + }); + }).not.toThrow(); + } + } + }); + + test('exported body of the first page carries var(--wsb-primary) and not the primary hex', () => { + harness = renderEditorHarness(); + for (const comp of t.pages[0].content.components) { + harness.act(() => { + const tree = buildNodeTree(harness!.query, templateComponentToTreeNode(comp)); + harness!.actions.addNodeTree(tree, 'ROOT'); + }); + } + const serialized = harness.getSerialized(); + const { html } = exportBodyHtml(serialized); + expect(html).toContain('var(--wsb-primary)'); + expect(html.toLowerCase()).not.toContain(design.primaryColor.toLowerCase()); + + // Full document: :root defines the template primary, body does not repeat it. + const full = exportToHtml(serialized, { design: { ...DEFAULT_SITE_DESIGN, ...t.design } }).html; + const rootDecl = `--wsb-primary:${design.primaryColor}`; + expect(full.toLowerCase()).toContain(rootDecl.toLowerCase()); + const body = full.slice(full.toLowerCase().indexOf('