cb9fea6656
Mocked unit tests for regenerateTreeIds, TemplateModal.addTemplateComponents,
and buildNodeTree each let a real bug ship (DataCloneError on duplicate/paste,
dropped template children, no-op AI section-replace/insert) because their
fake @craftjs/core query/actions never exercised Craft's real node shape
(data.type as a live component reference) or real parseFreshNode validation.
Adds src/test-utils/editorHarness.tsx, which mounts a REAL <Editor>+<Frame>
(no vi.mock('@craftjs/core') anywhere) via react-dom/client + act, plus the
jsdom shims Craft/this component library actually needs (ResizeObserver,
matchMedia, and an HTMLElement.prototype.innerText polyfill -- jsdom has no
native innerText, which Heading/TextBlock rely on to paint their text).
Adds 3 integration suites under src/test-utils/integration/ driving the real
useNodeActions/useKeyboardShortcuts, TemplateModal's real tree-build pipeline,
and the real useApplyAiResponse hook against a live EditorStore, asserting on
both the real rendered DOM and query.serialize()/exportBodyHtml output.
Red-proofed the duplicate/paste suite: temporarily reverted
regenerateTreeIds to structuredClone(oldNode.data) and confirmed both tests
fail with the historical DataCloneError before restoring the fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
162 lines
6.7 KiB
TypeScript
162 lines
6.7 KiB
TypeScript
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 `<Frame>` (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 <Frame> 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);
|
|
});
|
|
});
|