import { afterEach, describe, expect, test } from 'bun:test' import { computeAllLayouts, SceneGraph, setTextMeasurer } from '@open-pencil/core' function importedText(graph: SceneGraph, text: string, width: number, height: number, id: string) { const page = graph.getPages()[0] const node = graph.createNode('TEXT', page.id, { width, height, text, textAutoResize: 'WIDTH_AND_HEIGHT' }) graph.updateNode(node.id, { source: { ...node.source, format: 'fig', id } }) return node } afterEach(() => setTextMeasurer(null)) describe('effective generated FIG text layout', () => { test('shapes generated text and recomputes its HUG width lineage', () => { const graph = new SceneGraph() const page = graph.getPages()[0] const source = importedText(graph, 'Effective instance text', 342, 20, '1:1') const checkbox = graph.createNode('INSTANCE', page.id, { width: 366, height: 40, layoutMode: 'HORIZONTAL', primaryAxisSizing: 'FIXED', counterAxisSizing: 'HUG', componentId: 'component' }) graph.updateNode(checkbox.id, { source: { ...checkbox.source, format: 'fig', id: '1:5' } }) graph.createNode('INSTANCE', checkbox.id, { width: 16, height: 16 }) const textWrapper = graph.createNode('FRAME', checkbox.id, { width: 350, height: 40, layoutMode: 'VERTICAL', primaryAxisSizing: 'FIXED', counterAxisSizing: 'HUG', paddingLeft: 8, componentId: 'text-wrapper', figmaDerivedLayout: { width: 350, height: 40 } }) const generatedText = graph.createNode('TEXT', textWrapper.id, { width: 342, height: 20, text: source.text, textAutoResize: 'WIDTH_AND_HEIGHT', componentId: source.id, figmaDerivedLayout: { width: 342, height: 20 } }) setTextMeasurer(() => ({ width: 336, height: 20 })) computeAllLayouts(graph) expect(graph.getNode(generatedText.id)).toMatchObject({ width: 336, height: 20, figmaDerivedLayout: { width: 336, height: 20 } }) expect(graph.getNode(textWrapper.id)).toMatchObject({ width: 344, figmaDerivedLayout: { width: 344, height: 40 } }) expect(graph.getNode(checkbox.id)).toMatchObject({ width: 360, height: 40 }) expect(graph.getNode(source.id)).toMatchObject({ width: 342, height: 20 }) }) test('repositions centered generated text after effective shaping', () => { const graph = new SceneGraph() const page = graph.getPages()[0] const source = importedText(graph, 'Centered label', 100, 20, '1:7') const parent = graph.createNode('FRAME', page.id, { width: 200, height: 40, layoutMode: 'HORIZONTAL', primaryAxisSizing: 'FIXED', counterAxisSizing: 'FIXED', primaryAxisAlign: 'CENTER' }) const generatedText = graph.createNode('TEXT', parent.id, { width: 100, height: 20, text: source.text, textAutoResize: 'WIDTH_AND_HEIGHT', componentId: source.id, figmaDerivedLayout: { width: 100, height: 20 } }) setTextMeasurer(() => ({ width: 80, height: 20 })) computeAllLayouts(graph) expect(graph.getNode(generatedText.id)).toMatchObject({ x: 60, width: 80 }) }) test('resizes visible inherited-stretch children but preserves excluded children', () => { const graph = new SceneGraph() const page = graph.getPages()[0] const source = importedText(graph, 'Chart heading', 100, 20, '1:8') const parent = graph.createNode('FRAME', page.id, { width: 120, height: 80, layoutMode: 'VERTICAL', primaryAxisSizing: 'FIXED', counterAxisSizing: 'HUG', paddingLeft: 10, paddingRight: 10, counterAxisAlign: 'STRETCH', componentId: 'parent', figmaDerivedLayout: { width: 120, height: 80 } }) const generatedText = graph.createNode('TEXT', parent.id, { width: 100, height: 20, text: source.text, textAutoResize: 'WIDTH_AND_HEIGHT', componentId: source.id, figmaDerivedLayout: { width: 100, height: 20 } }) const inheritedStretch = graph.createNode('RECTANGLE', parent.id, { width: 100, height: 10, layoutAlignSelf: 'AUTO', figmaDerivedLayout: { width: 100, height: 10 } }) const hiddenStretch = graph.createNode('RECTANGLE', parent.id, { width: 100, height: 10, visible: false, layoutAlignSelf: 'AUTO', figmaDerivedLayout: { width: 100, height: 10 } }) const absoluteStretch = graph.createNode('RECTANGLE', parent.id, { width: 100, height: 10, layoutPositioning: 'ABSOLUTE', layoutAlignSelf: 'AUTO', figmaDerivedLayout: { width: 100, height: 10 } }) setTextMeasurer(() => ({ width: 80, height: 20 })) computeAllLayouts(graph) expect(graph.getNode(parent.id)).toMatchObject({ width: 100 }) expect(graph.getNode(inheritedStretch.id)).toMatchObject({ width: 80 }) expect(graph.getNode(hiddenStretch.id)).toMatchObject({ width: 100 }) expect(graph.getNode(absoluteStretch.id)).toMatchObject({ width: 100 }) }) test('preserves fixed generated ancestors after shaping their text', () => { const graph = new SceneGraph() const page = graph.getPages()[0] const source = importedText(graph, 'Choose file', 76, 20, '1:2') const input = graph.createNode('FRAME', page.id, { width: 108, height: 40, layoutMode: 'HORIZONTAL', primaryAxisSizing: 'FIXED', counterAxisSizing: 'FIXED', paddingLeft: 16, paddingRight: 16, componentId: 'input', figmaDerivedLayout: { width: 108, height: 40 } }) const generatedText = graph.createNode('TEXT', input.id, { width: 76, height: 20, text: source.text, textAutoResize: 'WIDTH_AND_HEIGHT', componentId: source.id, figmaDerivedLayout: { width: 76, height: 20 } }) setTextMeasurer(() => ({ width: 74, height: 20 })) computeAllLayouts(graph) expect(graph.getNode(generatedText.id)).toMatchObject({ width: 74, height: 20 }) expect(graph.getNode(input.id)).toMatchObject({ width: 108, height: 40, figmaDerivedLayout: { width: 108, height: 40 } }) }) test('reshapes stretched generated source text only inside HUG-width parents', () => { const graph = new SceneGraph() const page = graph.getPages()[0] const source = importedText(graph, 'Bar Chart - Interactive', 333, 30, '1:3') graph.updateNode(source.id, { textAutoResize: 'HEIGHT' }) const cardHeader = graph.createNode('FRAME', page.id, { width: 381, height: 102, layoutMode: 'VERTICAL', primaryAxisSizing: 'FIXED', counterAxisSizing: 'HUG', paddingLeft: 24, paddingRight: 24, componentId: 'card-header', figmaDerivedLayout: { width: 381, height: 102 } }) const generatedText = graph.createNode('TEXT', cardHeader.id, { width: 333, height: 30, text: source.text, textAutoResize: 'HEIGHT', layoutAlignSelf: 'STRETCH', componentId: source.id, figmaDerivedLayout: { width: 333, height: 30 } }) setTextMeasurer(() => ({ width: 329, height: 37 })) computeAllLayouts(graph) expect(graph.getNode(generatedText.id)).toMatchObject({ width: 329, height: 30 }) expect(graph.getNode(cardHeader.id)).toMatchObject({ width: 377, height: 102, figmaDerivedLayout: { width: 377, height: 102 } }) }) test('preserves fixed-width text inherited through the component lineage', () => { const graph = new SceneGraph() const page = graph.getPages()[0] const source = importedText(graph, 'Label', 38, 14, '1:6') graph.updateNode(source.id, { textAutoResize: 'WIDTH_AND_HEIGHT' }) const fixedIntermediate = graph.createNode('TEXT', page.id, { width: 280, height: 14, text: 'Email', textAutoResize: 'HEIGHT', componentId: source.id, figmaDerivedLayout: { width: 280, height: 14 } }) const generatedText = graph.createNode('TEXT', page.id, { width: 302, height: 14, text: 'Name', textAutoResize: 'WIDTH_AND_HEIGHT', componentId: fixedIntermediate.id, figmaDerivedLayout: { width: 302, height: 14 } }) setTextMeasurer(() => ({ width: 36, height: 17 })) computeAllLayouts(graph) expect(graph.getNode(generatedText.id)).toMatchObject({ width: 302, height: 14 }) }) test('preserves stretched override text and direct imported text bounds', () => { const graph = new SceneGraph() const page = graph.getPages()[0] const source = importedText(graph, 'Source copy', 333, 20, '1:4') graph.updateNode(source.id, { textAutoResize: 'HEIGHT' }) const parent = graph.createNode('FRAME', page.id, { width: 381, height: 68, layoutMode: 'VERTICAL', primaryAxisSizing: 'FIXED', counterAxisSizing: 'HUG', paddingLeft: 24, paddingRight: 24, componentId: 'parent', figmaDerivedLayout: { width: 381, height: 68 } }) const overrideText = graph.createNode('TEXT', parent.id, { width: 333, height: 20, text: 'A different instance override', textAutoResize: 'HEIGHT', layoutAlignSelf: 'STRETCH', componentId: source.id, figmaDerivedLayout: { width: 333, height: 20 } }) setTextMeasurer(() => ({ width: 329, height: 20 })) computeAllLayouts(graph) expect(graph.getNode(overrideText.id)).toMatchObject({ width: 333, height: 20 }) expect(graph.getNode(source.id)).toMatchObject({ width: 333, height: 20 }) }) })