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, 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(); // 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(); 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(); 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: '

stranded

', style: {} }, displayName: 'HTML', custom: {}, hidden: false, nodes: [], linkedNodes: {}, parent: 'ghost', }; const harness = renderEditorHarness({ initialState: stateWith({ h1: heading, stray }, ['h1']), }); harness.mountChild(); 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(); }); });