import { describe, test, expect, afterEach } from 'vitest'; import { renderEditorHarness, EditorHarness } from '../editorHarness'; import { allTemplates } from '../../templates'; import { buildNodeTree } from '../../utils/craft-tree'; import { templateComponentToTreeNode } from '../../templates/apply-template'; import { exportBodyHtml } from '../../utils/html-export'; /** * Real-`@craftjs/core` integration coverage for loading a template. * * Historical bug: `TemplateModal.addTemplateComponents` used to build each * template component via `React.createElement(Component, comp.props)` with * NO children argument, silently dropping every nested `children` array * authored in `templates/definitions.ts` (e.g. `makeHeader()`'s * `Container > Logo + Menu`, `makeFooterContent()`'s `Container > TextBlock`, * or any page `Section` wrapping a `Heading`/`ColumnLayout`). The resulting * Craft.js node had `nodes: []`, so both the editor's own header/footer zone * preview AND the published HTML (same `exportBodyHtml` path) rendered as an * empty strip instead of the real nav/footer/section content. The mocked * unit test for `templateComponentToTreeNode` (`apply-template.test.ts`) * tests the pure conversion function in isolation and would pass either way * -- it never drove the result through a real Craft.js tree/render, so it * couldn't have caught `TemplateModal` itself dropping children at the * call site. * * This suite drives the EXACT same path `TemplateModal.tsx`'s * `addTemplateComponents` uses against a REAL Craft.js editor mounted with a * REAL `` (via `editorHarness.tsx`) -- so it also exercises real * component rendering (Section/Container SHELL_INNER canvases, ColumnLayout * linked columns), not just tree-shape assertions. */ const tpl = allTemplates.find((t) => t.id === 'saas-landing')!; /** Mirrors TemplateModal.tsx's addTemplateComponents(). */ function addTemplateComponents(harness: EditorHarness, components: any[]) { for (const comp of components) { const tree = buildNodeTree(harness.query, templateComponentToTreeNode(comp)); harness.actions.addNodeTree(tree, 'ROOT'); } } const EMPTY_HEADER_ROOT = JSON.stringify({ ROOT: { type: { resolvedName: 'Container' }, isCanvas: true, props: { style: {}, tag: 'header' }, displayName: 'Container', custom: {}, hidden: false, nodes: [], linkedNodes: {}, }, }); const EMPTY_FOOTER_ROOT = JSON.stringify({ ROOT: { type: { resolvedName: 'Container' }, isCanvas: true, props: { style: {}, tag: 'footer' }, displayName: 'Container', custom: {}, hidden: false, nodes: [], linkedNodes: {}, }, }); let harness: EditorHarness | null = null; afterEach(() => { if (harness) { harness.unmount(); harness = null; } }); describe('template load (real @craftjs/core editor + real render)', () => { test('sanity: the SaaS Landing template actually has nested header/footer/page content to lose', () => { expect(tpl).toBeDefined(); expect(tpl.header.components.length).toBeGreaterThan(0); expect(tpl.footer.components.length).toBeGreaterThan(0); expect(tpl.pages[0].content.components.length).toBeGreaterThan(0); const hasNestedChildren = tpl.header.components.some((c: any) => (c.children?.length ?? 0) > 0); expect(hasNestedChildren).toBe(true); }); test('loading the header keeps its nested nav links + logo -- both in the real DOM and in export', () => { harness = renderEditorHarness({ initialState: EMPTY_HEADER_ROOT, frameTag: 'header' }); expect(() => { harness!.act(() => addTemplateComponents(harness!, tpl.header.components)); }).not.toThrow(); // Real rendered DOM -- not just serialized state. expect(harness.container.textContent).toContain('FlowStack'); expect(harness.container.textContent).toContain('Features'); expect(harness.container.textContent).toContain('Start Free'); const { html } = exportBodyHtml(harness.getSerialized()); expect(html).toContain('FlowStack'); expect(html).toContain('Features'); expect(html).toContain('Start Free'); }); test('loading the footer keeps its nested copyright/tagline content -- both in the real DOM and in export', () => { harness = renderEditorHarness({ initialState: EMPTY_FOOTER_ROOT, frameTag: 'footer' }); expect(() => { harness!.act(() => addTemplateComponents(harness!, tpl.footer.components)); }).not.toThrow(); expect(harness.container.textContent).toContain('FlowStack, Inc.'); expect(harness.container.textContent).toContain('Ship products faster'); const { html } = exportBodyHtml(harness.getSerialized()); expect(html).toContain('FlowStack, Inc.'); expect(html).toContain('Ship products faster'); }); test('loading the home page keeps a Section-wrapped Heading\'s nested content -- both in the real DOM and in export', () => { const homePage = tpl.pages[0]; const sectionComp = homePage.content.components.find((c: any) => c.type === 'Section'); expect(sectionComp).toBeDefined(); expect(sectionComp!.children?.length).toBeGreaterThan(0); harness = renderEditorHarness(); // default empty 'div' root, like a regular page expect(() => { harness!.act(() => addTemplateComponents(harness!, [sectionComp])); }).not.toThrow(); expect(harness.container.textContent).toContain('Trusted by 10,000+ development teams'); const { html } = exportBodyHtml(harness.getSerialized()); expect(html).toContain('Trusted by 10,000+ development teams'); }); test('loading the FULL home page (all components) keeps every top-level node\'s content -- no silent drops', () => { const homePage = tpl.pages[0]; harness = renderEditorHarness(); expect(() => { harness!.act(() => addTemplateComponents(harness!, homePage.content.components)); }).not.toThrow(); // ROOT should have exactly as many direct children as the template // defines top-level page components -- if children got dropped, nested // content wouldn't show up in ROOT's descendant count via serialize, but // an easier, more direct regression signal is: every top-level // component's OWN text ends up rendered somewhere on the canvas. const serialized = JSON.parse(harness.getSerialized()); const totalNodeCount = Object.keys(serialized).length; // A flat (children-dropped) tree would have exactly // homePage.content.components.length + 1 (ROOT) nodes. The real template // has nested children, so a correct load must produce MORE nodes than // that -- this is the shape-level assertion the pre-existing mocked test // could never make (it never touched a real tree). expect(totalNodeCount).toBeGreaterThan(homePage.content.components.length + 1); }); });