2026-07-12 20:15:08 -07:00
|
|
|
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;
|
2026-07-12 20:55:52 -07:00
|
|
|
hovered: boolean;
|
2026-07-12 20:15:08 -07:00
|
|
|
dom: HTMLElement | null;
|
|
|
|
|
displayName: string;
|
|
|
|
|
parent: string | null;
|
2026-07-12 20:55:52 -07:00
|
|
|
isCanvas: boolean;
|
2026-07-12 20:15:08 -07:00
|
|
|
};
|
|
|
|
|
const selectNodeSpy = vi.fn();
|
|
|
|
|
|
|
|
|
|
vi.mock('@craftjs/core', () => ({
|
|
|
|
|
useEditor: () => ({ actions: { selectNode: selectNodeSpy } }),
|
|
|
|
|
useNode: (collect?: (node: any) => any) => {
|
|
|
|
|
const node = {
|
2026-07-12 20:55:52 -07:00
|
|
|
events: { selected: mockNode.selected, hovered: mockNode.hovered },
|
2026-07-12 20:15:08 -07:00
|
|
|
dom: mockNode.dom,
|
2026-07-12 20:55:52 -07:00
|
|
|
data: {
|
|
|
|
|
custom: {},
|
|
|
|
|
displayName: mockNode.displayName,
|
|
|
|
|
parent: mockNode.parent,
|
|
|
|
|
isCanvas: mockNode.isCanvas,
|
|
|
|
|
},
|
2026-07-12 20:15:08 -07:00
|
|
|
};
|
|
|
|
|
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);
|
2026-07-12 20:55:52 -07:00
|
|
|
mockNode = {
|
|
|
|
|
id: 'node-1',
|
|
|
|
|
selected: false,
|
|
|
|
|
hovered: false,
|
|
|
|
|
dom: nodeDom,
|
|
|
|
|
displayName: 'Heading',
|
|
|
|
|
parent: 'ROOT',
|
|
|
|
|
isCanvas: false,
|
|
|
|
|
};
|
2026-07-12 20:15:08 -07:00
|
|
|
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();
|
|
|
|
|
});
|
2026-07-12 20:55:52 -07:00
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
});
|
2026-07-12 20:15:08 -07:00
|
|
|
});
|