Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
103 lines
4.7 KiB
TypeScript
103 lines
4.7 KiB
TypeScript
import { describe, test, expect } from 'vitest';
|
|
import React from 'react';
|
|
import { renderEditorHarness } from '../../test-utils/editorHarness';
|
|
import { LayersPanel } from './LayersPanel';
|
|
import { LayerFocusProvider } from './LayerFocusContext';
|
|
|
|
function stateWith(extra: Record<string, any>, rootChildren: string[]) {
|
|
return JSON.stringify({
|
|
ROOT: {
|
|
type: { resolvedName: 'Container' }, isCanvas: true,
|
|
props: { style: {}, tag: 'div' }, displayName: 'Container',
|
|
custom: {}, hidden: false, nodes: rootChildren, linkedNodes: {}, parent: null,
|
|
},
|
|
...extra,
|
|
});
|
|
}
|
|
|
|
const featuresNode = {
|
|
type: { resolvedName: 'FeaturesGrid' }, isCanvas: false,
|
|
props: { features: [{ title: 'Fast' }, { title: 'Secure' }] },
|
|
displayName: 'Features Grid',
|
|
custom: {}, hidden: false, nodes: [], linkedNodes: {}, parent: 'ROOT',
|
|
};
|
|
|
|
describe('LayersPanel', () => {
|
|
test('shows virtual rows for a Features Grid array prop', () => {
|
|
const harness = renderEditorHarness({ initialState: stateWith({ f1: featuresNode }, ['f1']) });
|
|
harness.mountChild(<LayerFocusProvider><LayersPanel /></LayerFocusProvider>);
|
|
|
|
// Scoped to `.layer-virtual-row` rather than the whole container:
|
|
// `FeaturesGrid` itself renders `feat.title` on the live canvas (which
|
|
// is also mounted inside `harness.container`, alongside the panel), so
|
|
// asserting on `container.textContent` alone would pass even if
|
|
// LayersPanel never rendered a single virtual row -- the "Fast"/"Secure"
|
|
// text would already be there from the canvas. Scoping to the virtual
|
|
// row elements themselves makes the assertion actually exercise
|
|
// LayersPanel's own rendering.
|
|
const virtualRows = harness.container.querySelectorAll('.layer-virtual-row');
|
|
expect(virtualRows).toHaveLength(2);
|
|
const virtualText = Array.from(virtualRows).map((el) => el.textContent).join(' | ');
|
|
expect(virtualText).toContain('Fast');
|
|
expect(virtualText).toContain('Secure');
|
|
|
|
// The real node row for the parent is still shown too.
|
|
const nodeRows = harness.container.querySelectorAll('.layer-node-row');
|
|
const nodeText = Array.from(nodeRows).map((el) => el.textContent).join(' | ');
|
|
expect(nodeText).toContain('Features Grid');
|
|
|
|
harness.unmount();
|
|
});
|
|
|
|
test('virtual rows are not rendered for a component with no registry entry', () => {
|
|
const heading = {
|
|
type: { resolvedName: 'Heading' }, isCanvas: false,
|
|
props: { text: 'Title', level: 2 }, displayName: 'Heading',
|
|
custom: {}, hidden: false, nodes: [], linkedNodes: {}, parent: 'ROOT',
|
|
};
|
|
const harness = renderEditorHarness({ initialState: stateWith({ h1: heading }, ['h1']) });
|
|
harness.mountChild(<LayerFocusProvider><LayersPanel /></LayerFocusProvider>);
|
|
expect(harness.container.querySelectorAll('.layer-virtual-row')).toHaveLength(0);
|
|
harness.unmount();
|
|
});
|
|
|
|
test('the Unplaced group does not render for a healthy tree', () => {
|
|
const harness = renderEditorHarness({ initialState: stateWith({ f1: featuresNode }, ['f1']) });
|
|
harness.mountChild(<LayerFocusProvider><LayersPanel /></LayerFocusProvider>);
|
|
expect(harness.container.textContent).not.toContain('Unplaced');
|
|
harness.unmount();
|
|
});
|
|
|
|
test('the Unplaced group appears and lists a node unreachable from ROOT', () => {
|
|
// 'stray' is a real node in the deserialized state, but no node's
|
|
// `nodes`/`linkedNodes` list references it -- exactly the "dropped
|
|
// outside the page" scenario findUnreachableNodeIds exists to catch.
|
|
const heading = {
|
|
type: { resolvedName: 'Heading' }, isCanvas: false,
|
|
props: { text: 'Title', level: 2 }, displayName: 'Heading',
|
|
custom: {}, hidden: false, nodes: [], linkedNodes: {}, parent: 'ROOT',
|
|
};
|
|
const stray = {
|
|
type: { resolvedName: 'HtmlBlock' }, isCanvas: false,
|
|
props: { code: '<p>stranded</p>', style: {} }, displayName: 'HTML',
|
|
custom: {}, hidden: false, nodes: [], linkedNodes: {}, parent: 'ghost',
|
|
};
|
|
const harness = renderEditorHarness({
|
|
initialState: stateWith({ h1: heading, stray }, ['h1']),
|
|
});
|
|
harness.mountChild(<LayerFocusProvider><LayersPanel /></LayerFocusProvider>);
|
|
|
|
expect(harness.container.textContent).toContain('Unplaced');
|
|
expect(harness.container.textContent).toContain('Unplaced (1)');
|
|
|
|
// The orphan itself is rendered as a selectable/deletable LayerNode row
|
|
// (displayName 'HTML') underneath the Unplaced heading, not silently
|
|
// dropped from the tree.
|
|
const nodeRows = harness.container.querySelectorAll('.layer-node-row');
|
|
const nodeText = Array.from(nodeRows).map((el) => el.textContent).join(' | ');
|
|
expect(nodeText).toContain('HTML');
|
|
|
|
harness.unmount();
|
|
});
|
|
});
|