Files
site-builder/craft/src/editor/RenderNode.test.tsx
T
shadowdao 613a44c4d4 feat(builder): items 10+12+13 — scope canvas guides, Show Guides toggle, Layers panel hover/icons, topbar unification
Item 10: dashed canvas "guide" outlines were applied via blanket tag
selectors (div/section/header/...), so a finished section showed 3-4
nested dashed boxes. RenderNode.tsx (the <Editor onRender> override) now
tags each Craft.js droppable container's real DOM node with a
`data-craft-node` attribute (node.data.isCanvas, excluding ROOT), and
editor.css's guide rules target that attribute instead — a component's own
internal wrapper markup is no longer mistaken for a drop target. Added a
"Show guides" topbar toggle (default ON, persisted to localStorage),
state lifted in EditorShell.tsx (mirrors how `device` is already lifted),
flips `.guides-off` on Canvas.tsx's `.canvas-device-frame`.

Item 12: Layers panel rows get per-type FA icons (keyed off the same
craft.displayName used for the row label, seeded from BlocksPanel's
choices), indent-guide lines connecting nested rows, and row hover
highlights the matching canvas element (`data-layer-hovered`, written
directly to the node's DOM via `query.node(id).get().dom` — NOT via
`actions.setNodeEvent`, which is stripped from useEditor()'s public
`actions` at runtime, not just in its TS type, and threw when called).

Along the way, Craft.js's own connect() was found to already wire mouse
hover to the same `hovered` node event internally (previously invisible
because the matching CSS was dead) — RenderNode now also mirrors that
onto `data-craft-hovered` for a real-mouse-hover canvas highlight, and
both hover attributes plus the guide attribute are suppressed under
`.guides-off` so the toggle stays airtight.

Item 13: unified the topbar to one button radius (`.topbar-btn` 20px pill
-> var(--radius-md)), demoted Code/Preview to icon-only with
`data-tooltip`. Fixed a latent bug the new tooltips exposed: `[data-tooltip]`
tooltips render above their trigger, but the topbar has no room above it
(`body { overflow: hidden }`) — added a `.topbar` override to render
those below instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:55:52 -07:00

170 lines
5.3 KiB
TypeScript

import { describe, test, expect, vi, beforeEach } from 'vitest';
import React from 'react';
import { createRoot, Root } from 'react-dom/client';
import { act } from 'react-dom/test-utils';
/* Same DOM-harness pattern as Footer.editguard.test.tsx: mock @craftjs/core
so RenderNode (the <Editor onRender> override) can be driven without a
real Editor tree. document.body doubles as the portal target, same as
the component itself uses. */
let mockNode: {
id: string;
selected: boolean;
hovered: boolean;
dom: HTMLElement | null;
displayName: string;
parent: string | null;
isCanvas: boolean;
};
const selectNodeSpy = vi.fn();
vi.mock('@craftjs/core', () => ({
useEditor: () => ({ actions: { selectNode: selectNodeSpy } }),
useNode: (collect?: (node: any) => any) => {
const node = {
events: { selected: mockNode.selected, hovered: mockNode.hovered },
dom: mockNode.dom,
data: {
custom: {},
displayName: mockNode.displayName,
parent: mockNode.parent,
isCanvas: mockNode.isCanvas,
},
};
return { id: mockNode.id, ...(collect ? collect(node) : {}) };
},
}));
import { RenderNode } from './RenderNode';
let container: HTMLDivElement;
let root: Root;
let nodeDom: HTMLElement;
function render(ui: React.ReactElement) {
container = document.createElement('div');
document.body.appendChild(container);
act(() => {
root = createRoot(container);
root.render(ui);
});
}
beforeEach(() => {
nodeDom = document.createElement('div');
document.body.appendChild(nodeDom);
mockNode = {
id: 'node-1',
selected: false,
hovered: false,
dom: nodeDom,
displayName: 'Heading',
parent: 'ROOT',
isCanvas: false,
};
selectNodeSpy.mockClear();
});
const rendered = <span data-testid="inner">hello</span>;
describe('RenderNode (Editor onRender override)', () => {
test('passes render through untouched when not selected', () => {
render(<RenderNode render={rendered} />);
expect(container.querySelector('[data-testid="inner"]')).not.toBeNull();
expect(document.querySelector('.component-indicator')).toBeNull();
container.remove();
nodeDom.remove();
});
test('shows the badge with the displayName when selected', () => {
mockNode.selected = true;
render(<RenderNode render={rendered} />);
const badge = document.querySelector('.component-indicator');
expect(badge).not.toBeNull();
expect(badge?.textContent).toContain('Heading');
container.remove();
nodeDom.remove();
document.querySelector('.component-indicator')?.remove();
});
test('never shows a badge for ROOT even if "selected"', () => {
mockNode.selected = true;
mockNode.id = 'ROOT';
render(<RenderNode render={rendered} />);
expect(document.querySelector('.component-indicator')).toBeNull();
container.remove();
nodeDom.remove();
});
test('chevron click selects the parent node', () => {
mockNode.selected = true;
mockNode.parent = 'parent-42';
render(<RenderNode render={rendered} />);
const chevron = document.querySelector('.component-indicator-parent-btn') as HTMLElement;
expect(chevron).not.toBeNull();
act(() => {
chevron.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
});
expect(selectNodeSpy).toHaveBeenCalledWith('parent-42');
container.remove();
nodeDom.remove();
document.querySelector('.component-indicator')?.remove();
});
test('no chevron when there is no parent', () => {
mockNode.selected = true;
mockNode.parent = null;
render(<RenderNode render={rendered} />);
expect(document.querySelector('.component-indicator-parent-btn')).toBeNull();
container.remove();
nodeDom.remove();
document.querySelector('.component-indicator')?.remove();
});
test('tags a droppable (isCanvas) node dom with data-craft-node', () => {
mockNode.isCanvas = true;
render(<RenderNode render={rendered} />);
expect(nodeDom.hasAttribute('data-craft-node')).toBe(true);
container.remove();
nodeDom.remove();
});
test('does not tag a non-canvas (leaf) node dom with data-craft-node', () => {
mockNode.isCanvas = false;
render(<RenderNode render={rendered} />);
expect(nodeDom.hasAttribute('data-craft-node')).toBe(false);
container.remove();
nodeDom.remove();
});
test('never tags ROOT with data-craft-node even though ROOT is a canvas', () => {
mockNode.id = 'ROOT';
mockNode.isCanvas = true;
render(<RenderNode render={rendered} />);
expect(nodeDom.hasAttribute('data-craft-node')).toBe(false);
container.remove();
nodeDom.remove();
});
test('tags the dom with data-craft-hovered when the Craft hovered event is set (Layers panel hover sync)', () => {
mockNode.hovered = true;
render(<RenderNode render={rendered} />);
expect(nodeDom.hasAttribute('data-craft-hovered')).toBe(true);
container.remove();
nodeDom.remove();
});
test('removes data-craft-hovered once the hovered event clears', () => {
mockNode.hovered = true;
render(<RenderNode render={rendered} />);
expect(nodeDom.hasAttribute('data-craft-hovered')).toBe(true);
mockNode.hovered = false;
act(() => {
root.render(<RenderNode render={rendered} />);
});
expect(nodeDom.hasAttribute('data-craft-hovered')).toBe(false);
container.remove();
nodeDom.remove();
});
});