feat(site-builder): Layers shows array-prop items, unplaced nodes, and scrolls
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user