import { describe, test, expect } from 'vitest'; import React from 'react'; import { createRoot, Root } from 'react-dom/client'; import { act } from 'react-dom/test-utils'; import { Editor, useEditor } from '@craftjs/core'; import { componentResolver } from '../../components/resolver'; import { allTemplates } from '../../templates'; import { buildNodeTree } from '../../utils/craft-tree'; import { templateComponentToTreeNode } from '../../templates/apply-template'; import { exportBodyHtml } from '../../utils/html-export'; import { DEFAULT_HEADER_STATE } from '../../state/PageContext'; /** * Regression test for: loading a template makes the header/footer ZONE * PREVIEW (`ZonePreview` in `editor/Canvas.tsx`, which calls * `exportBodyHtml(craftState)`) render as an empty strip -- no nav links, no * logo, no footer text -- and (a second bug found while fixing the first) * makes a template page's `Section`-wrapped content crash the live editor * entirely. * * This drives the EXACT same path `TemplateModal.tsx`'s `applyZone` / * `addTemplateComponents` uses to load a template's header/footer/page * content onto the live Craft.js canvas (`buildNodeTree` + * `actions.addNodeTree(tree, 'ROOT')`), then serializes it * (`query.serialize()`, exactly what `PageContext`'s `switchPage` / * `saveCurrentState` do right after `TemplateModal` finishes applying a * zone/page) and feeds the result through `exportBodyHtml` -- the same * function `ZonePreview` calls, and the same function `handlePublish` uses * to compose the published header/footer HTML. So this also covers whether * published output is affected (it was). */ let api: any; function Harness() { const { actions, query } = useEditor(); api = { actions, query }; return null; } function mount() { const container = document.createElement('div'); document.body.appendChild(container); let root!: Root; act(() => { root = createRoot(container); root.render( , ); }); return { container, unmount: () => { act(() => { root.unmount(); }); container.remove(); }, }; } /** Mirrors TemplateModal.tsx's addTemplateComponents(). */ function addTemplateComponents(components: any[]) { for (const comp of components) { const tree = buildNodeTree(api.query, templateComponentToTreeNode(comp)); api.actions.addNodeTree(tree, 'ROOT'); } } const EMPTY_HEADER_ROOT = '{"ROOT":{"type":{"resolvedName":"Container"},"isCanvas":true,"props":{"style":{},"tag":"header"},"displayName":"Container","custom":{},"hidden":false,"nodes":[],"linkedNodes":{}}}'; const EMPTY_CANVAS_ROOT = '{"ROOT":{"type":{"resolvedName":"Container"},"isCanvas":true,"props":{"style":{},"tag":"div"},"displayName":"Container","custom":{},"hidden":false,"nodes":[],"linkedNodes":{}}}'; describe('template header/footer zone export (loading a template)', () => { test('sanity check: the DEFAULT header (fresh project, no template) exports its content', () => { const { html } = exportBodyHtml(DEFAULT_HEADER_STATE); expect(html).toContain('MySite'); }); test('a template header (SaaS Landing) exports its real nav/logo content -- not an empty strip', () => { const tpl = allTemplates.find((t) => t.id === 'saas-landing'); expect(tpl).toBeDefined(); expect(tpl!.header.components.length).toBeGreaterThan(0); const { unmount } = mount(); act(() => { api.actions.deserialize(EMPTY_HEADER_ROOT); }); act(() => { addTemplateComponents(tpl!.header.components); }); const serialized = api.query.serialize(); const { html } = exportBodyHtml(serialized); // The template's logo text and a real nav link -- if these are missing, // the header rendered as an empty strip (the reported bug). expect(html).toContain('FlowStack'); expect(html).toContain('Features'); expect(html).toContain('Start Free'); unmount(); }); test('a template footer (SaaS Landing) exports its real copyright/tagline content -- not an empty strip', () => { const tpl = allTemplates.find((t) => t.id === 'saas-landing'); expect(tpl).toBeDefined(); expect(tpl!.footer.components.length).toBeGreaterThan(0); const { unmount } = mount(); act(() => { api.actions.deserialize( '{"ROOT":{"type":{"resolvedName":"Container"},"isCanvas":true,"props":{"style":{},"tag":"footer"},"displayName":"Container","custom":{},"hidden":false,"nodes":[],"linkedNodes":{}}}', ); }); act(() => { addTemplateComponents(tpl!.footer.components); }); const serialized = api.query.serialize(); const { html } = exportBodyHtml(serialized); expect(html).toContain('FlowStack, Inc.'); expect(html).toContain('Ship products faster'); unmount(); }); test('a template page Section with nested children (SaaS Landing Home) builds + exports without crashing', () => { // Regression for the SECOND bug surfaced while fixing the first: naively // rebuilding nested children via React.createElement + parseReactElement // crashes any component with an internal SHELL_INNER linked canvas // (Section/BackgroundSection/FormContainer) or linked columns // (ColumnLayout). The SaaS Landing home page's second component is a // `Section` wrapping a `Heading` ("Trusted by 10,000+ development // teams...") -- exactly this shape. const tpl = allTemplates.find((t) => t.id === 'saas-landing'); const homePage = tpl!.pages[0]; const sectionComp = homePage.content.components.find((c) => c.type === 'Section'); expect(sectionComp).toBeDefined(); expect(sectionComp!.children?.length).toBeGreaterThan(0); const { unmount } = mount(); act(() => { api.actions.deserialize(EMPTY_CANVAS_ROOT); }); expect(() => { act(() => { addTemplateComponents([sectionComp!]); }); }).not.toThrow(); const serialized = api.query.serialize(); const { html } = exportBodyHtml(serialized); expect(html).toContain('Trusted by 10,000+ development teams'); unmount(); }); });