test(layout): split auto-layout coverage
- Move auto-layout tests into focused files by layout behavior - Preserve sizing, wrapping, text measurement, and positioning coverage - Remove the last max-lines test exception
This commit is contained in:
parent
5e7c042352
commit
13a3475026
|
|
@ -212,14 +212,6 @@
|
|||
"complexity": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"tests/engine/layout/auto-layout.test.ts"
|
||||
],
|
||||
"rules": {
|
||||
"max-lines": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["scripts/**/*.ts"],
|
||||
"rules": {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,53 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { getNodeOrThrow } from '#tests/helpers/assert'
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('absolute children in Yoga tree', () => {
|
||||
test('absolute children do not affect auto-layout flow', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 200,
|
||||
primaryAxisSizing: 'HUG',
|
||||
counterAxisSizing: 'HUG'
|
||||
})
|
||||
rect(graph, frame.id, 50, 50)
|
||||
rect(graph, frame.id, 100, 100, { layoutPositioning: 'ABSOLUTE', x: 300, y: 150 })
|
||||
rect(graph, frame.id, 50, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.width).toBe(100)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].x).toBe(0)
|
||||
expect(children[2].x).toBe(50)
|
||||
|
||||
expect(children[1].x).toBe(300)
|
||||
expect(children[1].y).toBe(150)
|
||||
})
|
||||
|
||||
test('absolute children preserve position when parent resizes', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 200,
|
||||
itemSpacing: 10
|
||||
})
|
||||
rect(graph, frame.id, 50, 50)
|
||||
rect(graph, frame.id, 80, 40, { layoutPositioning: 'ABSOLUTE', x: 200, y: 100 })
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].x).toBe(0)
|
||||
expect(children[1].x).toBe(200)
|
||||
expect(children[1].y).toBe(100)
|
||||
expect(children[1].width).toBe(80)
|
||||
expect(children[1].height).toBe(40)
|
||||
})
|
||||
})
|
||||
34
tests/engine/layout/auto-layout/absolute-positioning.test.ts
Normal file
34
tests/engine/layout/auto-layout/absolute-positioning.test.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('absolute positioning', () => {
|
||||
test('absolute children are skipped in layout', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 200,
|
||||
itemSpacing: 10
|
||||
})
|
||||
rect(graph, frame.id, 50, 50)
|
||||
rect(graph, frame.id, 50, 50, {
|
||||
layoutPositioning: 'ABSOLUTE',
|
||||
x: 200,
|
||||
y: 100
|
||||
})
|
||||
rect(graph, frame.id, 50, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
// First auto child at 0
|
||||
expect(children[0].x).toBe(0)
|
||||
// Absolute child should keep its position
|
||||
expect(children[1].x).toBe(200)
|
||||
expect(children[1].y).toBe(100)
|
||||
// Third child should be right after first (no gap for absolute)
|
||||
expect(children[2].x).toBe(60)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('alignment - counter axis', () => {
|
||||
test('center cross-axis alignment (horizontal)', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 200,
|
||||
counterAxisAlign: 'CENTER'
|
||||
})
|
||||
rect(graph, frame.id, 50, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const child = graph.getChildren(frame.id)[0]
|
||||
expect(child.y).toBe(75)
|
||||
})
|
||||
|
||||
test('max cross-axis alignment (horizontal)', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 200,
|
||||
counterAxisAlign: 'MAX'
|
||||
})
|
||||
rect(graph, frame.id, 50, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const child = graph.getChildren(frame.id)[0]
|
||||
expect(child.y).toBe(150)
|
||||
})
|
||||
|
||||
test('center cross-axis alignment (vertical)', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
layoutMode: 'VERTICAL',
|
||||
width: 200,
|
||||
height: 400,
|
||||
counterAxisAlign: 'CENTER'
|
||||
})
|
||||
rect(graph, frame.id, 50, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const child = graph.getChildren(frame.id)[0]
|
||||
expect(child.x).toBe(75)
|
||||
})
|
||||
|
||||
test('stretch cross-axis alignment (horizontal)', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 200,
|
||||
counterAxisAlign: 'STRETCH'
|
||||
})
|
||||
rect(graph, frame.id, 50, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const child = graph.getChildren(frame.id)[0]
|
||||
expect(child.height).toBe(200)
|
||||
})
|
||||
|
||||
test('stretch cross-axis alignment (vertical)', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
layoutMode: 'VERTICAL',
|
||||
width: 200,
|
||||
height: 400,
|
||||
counterAxisAlign: 'STRETCH'
|
||||
})
|
||||
rect(graph, frame.id, 50, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const child = graph.getChildren(frame.id)[0]
|
||||
expect(child.width).toBe(200)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('alignment - primary axis', () => {
|
||||
test('center alignment (horizontal)', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 100,
|
||||
primaryAxisAlign: 'CENTER'
|
||||
})
|
||||
rect(graph, frame.id, 100, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const child = graph.getChildren(frame.id)[0]
|
||||
expect(child.x).toBe(150)
|
||||
})
|
||||
|
||||
test('max alignment (horizontal)', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 100,
|
||||
primaryAxisAlign: 'MAX'
|
||||
})
|
||||
rect(graph, frame.id, 100, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const child = graph.getChildren(frame.id)[0]
|
||||
expect(child.x).toBe(300)
|
||||
})
|
||||
|
||||
test('space-between alignment (horizontal)', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 100,
|
||||
primaryAxisAlign: 'SPACE_BETWEEN'
|
||||
})
|
||||
rect(graph, frame.id, 50, 50)
|
||||
rect(graph, frame.id, 50, 50)
|
||||
rect(graph, frame.id, 50, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].x).toBe(0)
|
||||
expect(children[2].x).toBe(350)
|
||||
expect(children[1].x).toBeCloseTo(175, 0)
|
||||
})
|
||||
|
||||
test('center alignment (vertical)', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
layoutMode: 'VERTICAL',
|
||||
width: 200,
|
||||
height: 400,
|
||||
primaryAxisAlign: 'CENTER'
|
||||
})
|
||||
rect(graph, frame.id, 50, 100)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const child = graph.getChildren(frame.id)[0]
|
||||
expect(child.y).toBe(150)
|
||||
})
|
||||
|
||||
test('max alignment (vertical)', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
layoutMode: 'VERTICAL',
|
||||
width: 200,
|
||||
height: 400,
|
||||
primaryAxisAlign: 'MAX'
|
||||
})
|
||||
rect(graph, frame.id, 50, 100)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const child = graph.getChildren(frame.id)[0]
|
||||
expect(child.y).toBe(300)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('counterAxisAlignContent', () => {
|
||||
test('SPACE_BETWEEN distributes wrapped rows evenly', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 200,
|
||||
height: 300,
|
||||
layoutWrap: 'WRAP',
|
||||
counterAxisAlignContent: 'SPACE_BETWEEN' as const
|
||||
})
|
||||
rect(graph, frame.id, 120, 40)
|
||||
rect(graph, frame.id, 120, 40)
|
||||
rect(graph, frame.id, 120, 40)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
// 3 rows of 40px each = 120px total, 300 - 120 = 180px free space
|
||||
// SPACE_BETWEEN: first row at 0, last row at 260
|
||||
expect(children[0].y).toBe(0)
|
||||
expect(children[2].y).toBe(260)
|
||||
// Middle row centered: (0 + 260) / 2 = 130
|
||||
expect(children[1].y).toBe(130)
|
||||
})
|
||||
|
||||
test('AUTO (default) packs wrapped rows at start', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 200,
|
||||
height: 300,
|
||||
layoutWrap: 'WRAP'
|
||||
})
|
||||
rect(graph, frame.id, 120, 40)
|
||||
rect(graph, frame.id, 120, 40)
|
||||
rect(graph, frame.id, 120, 40)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].y).toBe(0)
|
||||
expect(children[1].y).toBe(40)
|
||||
expect(children[2].y).toBe(80)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('counter axis spacing (no wrap)', () => {
|
||||
test('counterAxisSpacing is ignored without wrap in horizontal', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 300,
|
||||
height: 100,
|
||||
counterAxisSpacing: 50,
|
||||
layoutWrap: 'NO_WRAP'
|
||||
})
|
||||
rect(graph, frame.id, 50, 50)
|
||||
rect(graph, frame.id, 50, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
// Without wrap, counterAxisSpacing has no effect
|
||||
expect(children[0].x).toBe(0)
|
||||
expect(children[1].x).toBe(50)
|
||||
})
|
||||
})
|
||||
81
tests/engine/layout/auto-layout/edge-cases.test.ts
Normal file
81
tests/engine/layout/auto-layout/edge-cases.test.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { getNodeOrThrow } from '#tests/helpers/assert'
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('edge cases', () => {
|
||||
test('empty auto layout frame', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
primaryAxisSizing: 'HUG',
|
||||
counterAxisSizing: 'HUG',
|
||||
paddingTop: 10,
|
||||
paddingRight: 20,
|
||||
paddingBottom: 30,
|
||||
paddingLeft: 40
|
||||
})
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.width).toBe(60)
|
||||
expect(f.height).toBe(40)
|
||||
})
|
||||
|
||||
test('single child', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 200,
|
||||
height: 100
|
||||
})
|
||||
rect(graph, frame.id, 50, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const child = graph.getChildren(frame.id)[0]
|
||||
expect(child.x).toBe(0)
|
||||
expect(child.y).toBe(0)
|
||||
expect(child.width).toBe(50)
|
||||
expect(child.height).toBe(50)
|
||||
})
|
||||
|
||||
test('all children absolute → frame hugs to padding only', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
primaryAxisSizing: 'HUG',
|
||||
counterAxisSizing: 'HUG',
|
||||
paddingTop: 5,
|
||||
paddingRight: 5,
|
||||
paddingBottom: 5,
|
||||
paddingLeft: 5
|
||||
})
|
||||
rect(graph, frame.id, 100, 100, { layoutPositioning: 'ABSOLUTE' })
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.width).toBe(10)
|
||||
expect(f.height).toBe(10)
|
||||
})
|
||||
|
||||
test('zero-size children', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
primaryAxisSizing: 'HUG',
|
||||
counterAxisSizing: 'HUG'
|
||||
})
|
||||
rect(graph, frame.id, 0, 0)
|
||||
rect(graph, frame.id, 50, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.width).toBe(50)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].x).toBe(0)
|
||||
expect(children[1].x).toBe(0)
|
||||
})
|
||||
})
|
||||
73
tests/engine/layout/auto-layout/fill-flex-basis.test.ts
Normal file
73
tests/engine/layout/auto-layout/fill-flex-basis.test.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('FILL flexBasis', () => {
|
||||
test('FILL children with different content sizes get equal width', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 100
|
||||
})
|
||||
|
||||
const inner1 = autoFrame(graph, frame.id, {
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
width: 100,
|
||||
height: 50,
|
||||
layoutGrow: 1
|
||||
})
|
||||
rect(graph, inner1.id, 100, 50)
|
||||
|
||||
const inner2 = autoFrame(graph, frame.id, {
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
width: 200,
|
||||
height: 50,
|
||||
layoutGrow: 1
|
||||
})
|
||||
rect(graph, inner2.id, 200, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].width).toBe(200)
|
||||
expect(children[1].width).toBe(200)
|
||||
})
|
||||
|
||||
test('nested FILL children distribute from zero basis', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 300,
|
||||
height: 100,
|
||||
itemSpacing: 0
|
||||
})
|
||||
autoFrame(graph, frame.id, {
|
||||
primaryAxisSizing: 'FILL' as const,
|
||||
counterAxisSizing: 'FIXED',
|
||||
width: 50,
|
||||
height: 100
|
||||
})
|
||||
autoFrame(graph, frame.id, {
|
||||
primaryAxisSizing: 'FILL' as const,
|
||||
counterAxisSizing: 'FIXED',
|
||||
width: 50,
|
||||
height: 100
|
||||
})
|
||||
autoFrame(graph, frame.id, {
|
||||
primaryAxisSizing: 'FILL' as const,
|
||||
counterAxisSizing: 'FIXED',
|
||||
width: 50,
|
||||
height: 100
|
||||
})
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].width).toBe(100)
|
||||
expect(children[1].width).toBe(100)
|
||||
expect(children[2].width).toBe(100)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('gap mapping correctness', () => {
|
||||
test('horizontal: itemSpacing is column gap, counterAxisSpacing is row gap', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 200,
|
||||
height: 300,
|
||||
layoutWrap: 'WRAP',
|
||||
itemSpacing: 10,
|
||||
counterAxisSpacing: 20
|
||||
})
|
||||
// Each row: 90 + 10 + 90 = 190 <= 200
|
||||
rect(graph, frame.id, 90, 40)
|
||||
rect(graph, frame.id, 90, 40)
|
||||
// These wrap
|
||||
rect(graph, frame.id, 90, 40)
|
||||
rect(graph, frame.id, 90, 40)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
// Row 1
|
||||
expect(children[0].x).toBe(0)
|
||||
expect(children[1].x).toBe(100)
|
||||
// Row 2: y = 40 (row height) + 20 (counterAxisSpacing) = 60
|
||||
expect(children[2].y).toBe(60)
|
||||
expect(children[3].x).toBe(100)
|
||||
})
|
||||
|
||||
test('vertical: itemSpacing is row gap, counterAxisSpacing is column gap', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
layoutMode: 'VERTICAL',
|
||||
width: 300,
|
||||
height: 200,
|
||||
layoutWrap: 'WRAP',
|
||||
itemSpacing: 10,
|
||||
counterAxisSpacing: 20
|
||||
})
|
||||
// Each column: 80 + 10 + 80 = 170 <= 200
|
||||
rect(graph, frame.id, 40, 80)
|
||||
rect(graph, frame.id, 40, 80)
|
||||
// These wrap to second column
|
||||
rect(graph, frame.id, 40, 80)
|
||||
rect(graph, frame.id, 40, 80)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
// Column 1
|
||||
expect(children[0].y).toBe(0)
|
||||
expect(children[1].y).toBe(90)
|
||||
// Column 2: x = 40 (col width) + 20 (counterAxisSpacing) = 60
|
||||
expect(children[2].x).toBe(60)
|
||||
expect(children[2].y).toBe(0)
|
||||
expect(children[3].x).toBe(60)
|
||||
expect(children[3].y).toBe(90)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { getNodeOrThrow } from '#tests/helpers/assert'
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('hidden children preserve dimensions', () => {
|
||||
test('re-showing a hidden child restores original size', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 300,
|
||||
height: 100
|
||||
})
|
||||
const child = rect(graph, frame.id, 80, 60)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
expect(getNodeOrThrow(graph, child.id).width).toBe(80)
|
||||
|
||||
graph.updateNode(child.id, { visible: false })
|
||||
computeLayout(graph, frame.id)
|
||||
expect(getNodeOrThrow(graph, child.id).width).toBe(80)
|
||||
expect(getNodeOrThrow(graph, child.id).height).toBe(60)
|
||||
|
||||
graph.updateNode(child.id, { visible: true })
|
||||
computeLayout(graph, frame.id)
|
||||
expect(getNodeOrThrow(graph, child.id).width).toBe(80)
|
||||
expect(getNodeOrThrow(graph, child.id).height).toBe(60)
|
||||
})
|
||||
})
|
||||
90
tests/engine/layout/auto-layout/hidden-children.test.ts
Normal file
90
tests/engine/layout/auto-layout/hidden-children.test.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { getNodeOrThrow } from '#tests/helpers/assert'
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('hidden children', () => {
|
||||
test('hidden children preserve original size but collapse in layout', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 300,
|
||||
height: 100,
|
||||
itemSpacing: 10
|
||||
})
|
||||
rect(graph, frame.id, 50, 50, { visible: false })
|
||||
rect(graph, frame.id, 80, 40)
|
||||
rect(graph, frame.id, 50, 50, { visible: false })
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].width).toBe(50)
|
||||
expect(children[0].height).toBe(50)
|
||||
expect(children[1].x).toBe(0)
|
||||
expect(children[1].width).toBe(80)
|
||||
expect(children[2].width).toBe(50)
|
||||
expect(children[2].height).toBe(50)
|
||||
})
|
||||
|
||||
test('hidden children do not consume spacing', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 100,
|
||||
itemSpacing: 20
|
||||
})
|
||||
rect(graph, frame.id, 50, 50, { visible: false })
|
||||
rect(graph, frame.id, 100, 50)
|
||||
rect(graph, frame.id, 100, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[1].x).toBe(0)
|
||||
expect(children[2].x).toBe(120)
|
||||
})
|
||||
|
||||
test('hug frame ignores hidden children for sizing', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
primaryAxisSizing: 'HUG',
|
||||
counterAxisSizing: 'HUG'
|
||||
})
|
||||
rect(graph, frame.id, 200, 200, { visible: false })
|
||||
rect(graph, frame.id, 50, 30)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.width).toBe(50)
|
||||
expect(f.height).toBe(30)
|
||||
})
|
||||
|
||||
test('hidden nested auto-layout children preserve size but collapse in layout', () => {
|
||||
const graph = new SceneGraph()
|
||||
const outer = autoFrame(graph, pageId(graph), {
|
||||
width: 300,
|
||||
height: 100,
|
||||
itemSpacing: 16
|
||||
})
|
||||
const inner = autoFrame(graph, outer.id, {
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
width: 50,
|
||||
height: 50,
|
||||
visible: false
|
||||
})
|
||||
rect(graph, inner.id, 30, 30)
|
||||
rect(graph, outer.id, 80, 40)
|
||||
|
||||
computeLayout(graph, outer.id)
|
||||
|
||||
const children = graph.getChildren(outer.id)
|
||||
const innerNode = getNodeOrThrow(graph, inner.id)
|
||||
expect(innerNode.width).toBe(50)
|
||||
expect(innerNode.height).toBe(50)
|
||||
expect(children[1].x).toBe(0)
|
||||
})
|
||||
})
|
||||
168
tests/engine/layout/auto-layout/horizontal-basic.test.ts
Normal file
168
tests/engine/layout/auto-layout/horizontal-basic.test.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeAllLayouts, computeLayout, createEditor, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('horizontal basic', () => {
|
||||
test('positions children left-to-right', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph))
|
||||
rect(graph, frame.id, 80, 40)
|
||||
rect(graph, frame.id, 60, 40)
|
||||
rect(graph, frame.id, 100, 40)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].x).toBe(0)
|
||||
expect(children[1].x).toBe(80)
|
||||
expect(children[2].x).toBe(140)
|
||||
})
|
||||
|
||||
test('applies item spacing', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), { itemSpacing: 10 })
|
||||
rect(graph, frame.id, 50, 50)
|
||||
rect(graph, frame.id, 50, 50)
|
||||
rect(graph, frame.id, 50, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].x).toBe(0)
|
||||
expect(children[1].x).toBe(60)
|
||||
expect(children[2].x).toBe(120)
|
||||
})
|
||||
|
||||
test('applies padding', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
paddingTop: 10,
|
||||
paddingRight: 20,
|
||||
paddingBottom: 30,
|
||||
paddingLeft: 40
|
||||
})
|
||||
rect(graph, frame.id, 50, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const child = graph.getChildren(frame.id)[0]
|
||||
expect(child.x).toBe(40)
|
||||
expect(child.y).toBe(10)
|
||||
})
|
||||
|
||||
test('applies padding and spacing together', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
paddingLeft: 20,
|
||||
paddingTop: 15,
|
||||
itemSpacing: 10
|
||||
})
|
||||
rect(graph, frame.id, 50, 30)
|
||||
rect(graph, frame.id, 50, 30)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].x).toBe(20)
|
||||
expect(children[0].y).toBe(15)
|
||||
expect(children[1].x).toBe(80)
|
||||
expect(children[1].y).toBe(15)
|
||||
})
|
||||
|
||||
test('positions children right-to-left when layoutDirection is RTL', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 300,
|
||||
height: 80,
|
||||
layoutDirection: 'RTL',
|
||||
paddingLeft: 20,
|
||||
paddingRight: 30,
|
||||
itemSpacing: 10
|
||||
})
|
||||
rect(graph, frame.id, 50, 30)
|
||||
rect(graph, frame.id, 60, 30)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].x).toBe(220)
|
||||
expect(children[1].x).toBe(150)
|
||||
})
|
||||
|
||||
test('inherits RTL flow when layoutDirection is AUTO inside an RTL parent', () => {
|
||||
const graph = new SceneGraph()
|
||||
const outer = autoFrame(graph, pageId(graph), {
|
||||
width: 320,
|
||||
height: 120,
|
||||
layoutDirection: 'RTL'
|
||||
})
|
||||
const inner = autoFrame(graph, outer.id, {
|
||||
width: 240,
|
||||
height: 80,
|
||||
layoutDirection: 'AUTO'
|
||||
})
|
||||
rect(graph, inner.id, 50, 30)
|
||||
rect(graph, inner.id, 60, 30)
|
||||
|
||||
computeAllLayouts(graph, outer.id)
|
||||
|
||||
const children = graph.getChildren(inner.id)
|
||||
expect(children[0].x).toBe(190)
|
||||
expect(children[1].x).toBe(130)
|
||||
})
|
||||
|
||||
test('allows nested frame to override parent flow direction', () => {
|
||||
const graph = new SceneGraph()
|
||||
const outer = autoFrame(graph, pageId(graph), {
|
||||
width: 320,
|
||||
height: 120,
|
||||
layoutDirection: 'LTR'
|
||||
})
|
||||
const inner = autoFrame(graph, outer.id, {
|
||||
width: 240,
|
||||
height: 80,
|
||||
layoutDirection: 'RTL'
|
||||
})
|
||||
rect(graph, inner.id, 50, 30)
|
||||
rect(graph, inner.id, 60, 30)
|
||||
|
||||
computeAllLayouts(graph, outer.id)
|
||||
|
||||
const children = graph.getChildren(inner.id)
|
||||
expect(children[0].x).toBe(190)
|
||||
expect(children[1].x).toBe(130)
|
||||
})
|
||||
|
||||
test('recomputes deep auto descendants when parent flow changes', () => {
|
||||
const graph = new SceneGraph()
|
||||
const outer = graph.createNode('FRAME', pageId(graph), {
|
||||
layoutMode: 'VERTICAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
width: 320,
|
||||
height: 200,
|
||||
layoutDirection: 'LTR'
|
||||
})
|
||||
const wrapper = graph.createNode('FRAME', outer.id, {
|
||||
layoutMode: 'NONE',
|
||||
width: 280,
|
||||
height: 120
|
||||
})
|
||||
const inner = autoFrame(graph, wrapper.id, {
|
||||
width: 240,
|
||||
height: 80,
|
||||
layoutDirection: 'AUTO'
|
||||
})
|
||||
rect(graph, inner.id, 50, 30)
|
||||
rect(graph, inner.id, 60, 30)
|
||||
|
||||
const editor = createEditor({ graph })
|
||||
editor.updateNodeWithUndo(outer.id, { layoutDirection: 'RTL' }, 'Change layout direction')
|
||||
|
||||
const children = graph.getChildren(inner.id)
|
||||
expect(children[0].x).toBe(190)
|
||||
expect(children[1].x).toBe(130)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('layoutAlignSelf extended values', () => {
|
||||
test('layoutAlignSelf CENTER positions child at cross-axis center', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 200,
|
||||
counterAxisAlign: 'MIN'
|
||||
})
|
||||
rect(graph, frame.id, 50, 50, { layoutAlignSelf: 'CENTER' as const })
|
||||
rect(graph, frame.id, 50, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].y).toBe(75)
|
||||
expect(children[1].y).toBe(0)
|
||||
})
|
||||
|
||||
test('layoutAlignSelf MAX positions child at cross-axis end', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 200,
|
||||
counterAxisAlign: 'MIN'
|
||||
})
|
||||
rect(graph, frame.id, 50, 50, { layoutAlignSelf: 'MAX' as const })
|
||||
rect(graph, frame.id, 50, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].y).toBe(150)
|
||||
expect(children[1].y).toBe(0)
|
||||
})
|
||||
|
||||
test('layoutAlignSelf MIN overrides parent STRETCH', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 200,
|
||||
counterAxisAlign: 'STRETCH'
|
||||
})
|
||||
rect(graph, frame.id, 50, 50, { layoutAlignSelf: 'MIN' as const })
|
||||
rect(graph, frame.id, 50, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].y).toBe(0)
|
||||
expect(children[0].height).toBe(50)
|
||||
expect(children[1].height).toBe(200)
|
||||
})
|
||||
|
||||
test('layoutAlignSelf in vertical layout', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
layoutMode: 'VERTICAL',
|
||||
width: 300,
|
||||
height: 400,
|
||||
counterAxisAlign: 'MIN'
|
||||
})
|
||||
rect(graph, frame.id, 50, 50, { layoutAlignSelf: 'CENTER' as const })
|
||||
rect(graph, frame.id, 50, 50, { layoutAlignSelf: 'MAX' as const })
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].x).toBe(125)
|
||||
expect(children[1].x).toBe(250)
|
||||
})
|
||||
})
|
||||
25
tests/engine/layout/auto-layout/layout-mode-none.test.ts
Normal file
25
tests/engine/layout/auto-layout/layout-mode-none.test.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { getNodeOrThrow } from '#tests/helpers/assert'
|
||||
import { pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('layout mode NONE', () => {
|
||||
test('computeLayout does nothing for NONE layout', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = graph.createNode('FRAME', pageId(graph), {
|
||||
name: 'Plain',
|
||||
layoutMode: 'NONE',
|
||||
width: 400,
|
||||
height: 200
|
||||
})
|
||||
const child = rect(graph, frame.id, 50, 50, { x: 100, y: 100 })
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const c = getNodeOrThrow(graph, child.id)
|
||||
expect(c.x).toBe(100)
|
||||
expect(c.y).toBe(100)
|
||||
})
|
||||
})
|
||||
88
tests/engine/layout/auto-layout/min-max-constraints.test.ts
Normal file
88
tests/engine/layout/auto-layout/min-max-constraints.test.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { getNodeOrThrow } from '#tests/helpers/assert'
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('min/max constraints', () => {
|
||||
test('maxWidth clamps child in horizontal layout', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 100
|
||||
})
|
||||
rect(graph, frame.id, 50, 50, { layoutGrow: 1, maxWidth: 200 })
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const child = graph.getChildren(frame.id)[0]
|
||||
expect(child.width).toBe(200)
|
||||
})
|
||||
|
||||
test('minWidth prevents shrinking below minimum', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 100,
|
||||
height: 100
|
||||
})
|
||||
rect(graph, frame.id, 200, 50, { minWidth: 150 })
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const child = graph.getChildren(frame.id)[0]
|
||||
expect(child.width).toBeGreaterThanOrEqual(150)
|
||||
})
|
||||
|
||||
test('maxHeight clamps child in vertical layout', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
layoutMode: 'VERTICAL',
|
||||
width: 200,
|
||||
height: 400
|
||||
})
|
||||
rect(graph, frame.id, 50, 50, { layoutGrow: 1, maxHeight: 150 })
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const child = graph.getChildren(frame.id)[0]
|
||||
expect(child.height).toBe(150)
|
||||
})
|
||||
|
||||
test('minHeight enforces minimum in vertical layout', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
layoutMode: 'VERTICAL',
|
||||
width: 200,
|
||||
height: 400
|
||||
})
|
||||
rect(graph, frame.id, 50, 30, { minHeight: 80 })
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const child = graph.getChildren(frame.id)[0]
|
||||
expect(child.height).toBe(80)
|
||||
})
|
||||
|
||||
test('min/max on nested auto-layout frame', () => {
|
||||
const graph = new SceneGraph()
|
||||
const outer = autoFrame(graph, pageId(graph), {
|
||||
width: 500,
|
||||
height: 200
|
||||
})
|
||||
const inner = autoFrame(graph, outer.id, {
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
width: 50,
|
||||
height: 50,
|
||||
layoutGrow: 1,
|
||||
maxWidth: 250
|
||||
})
|
||||
rect(graph, inner.id, 30, 30)
|
||||
|
||||
computeLayout(graph, outer.id)
|
||||
|
||||
const innerNode = getNodeOrThrow(graph, inner.id)
|
||||
expect(innerNode.width).toBe(250)
|
||||
})
|
||||
})
|
||||
46
tests/engine/layout/auto-layout/mixed-sizing.test.ts
Normal file
46
tests/engine/layout/auto-layout/mixed-sizing.test.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { getNodeOrThrow } from '#tests/helpers/assert'
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('mixed sizing', () => {
|
||||
test('fixed primary, hug counter (horizontal)', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 999,
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'HUG'
|
||||
})
|
||||
rect(graph, frame.id, 50, 80)
|
||||
rect(graph, frame.id, 50, 120)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.width).toBe(400)
|
||||
expect(f.height).toBe(120)
|
||||
})
|
||||
|
||||
test('hug primary, fixed counter (vertical)', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
layoutMode: 'VERTICAL',
|
||||
width: 200,
|
||||
height: 999,
|
||||
primaryAxisSizing: 'HUG',
|
||||
counterAxisSizing: 'FIXED',
|
||||
itemSpacing: 10
|
||||
})
|
||||
rect(graph, frame.id, 50, 40)
|
||||
rect(graph, frame.id, 80, 60)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.height).toBe(110)
|
||||
expect(f.width).toBe(200)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { getNodeOrThrow } from '#tests/helpers/assert'
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('nested auto layout with fill children', () => {
|
||||
test('child frame with FILL sizing expands in parent', () => {
|
||||
const graph = new SceneGraph()
|
||||
const outer = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 200,
|
||||
itemSpacing: 10
|
||||
})
|
||||
rect(graph, outer.id, 100, 50)
|
||||
const inner = autoFrame(graph, outer.id, {
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
width: 50,
|
||||
height: 50,
|
||||
layoutGrow: 1
|
||||
})
|
||||
rect(graph, inner.id, 30, 30)
|
||||
|
||||
computeLayout(graph, outer.id)
|
||||
|
||||
const innerNode = getNodeOrThrow(graph, inner.id)
|
||||
// 400 - 100 - 10 = 290
|
||||
expect(innerNode.width).toBe(290)
|
||||
})
|
||||
})
|
||||
97
tests/engine/layout/auto-layout/nested-auto-layout.test.ts
Normal file
97
tests/engine/layout/auto-layout/nested-auto-layout.test.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeAllLayouts, computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { getNodeOrThrow } from '#tests/helpers/assert'
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('nested auto layout', () => {
|
||||
test('nested horizontal frames', () => {
|
||||
const graph = new SceneGraph()
|
||||
const outer = autoFrame(graph, pageId(graph), {
|
||||
width: 500,
|
||||
height: 200,
|
||||
itemSpacing: 10
|
||||
})
|
||||
const inner = autoFrame(graph, outer.id, {
|
||||
primaryAxisSizing: 'HUG',
|
||||
counterAxisSizing: 'FIXED',
|
||||
width: 999,
|
||||
height: 100,
|
||||
itemSpacing: 5
|
||||
})
|
||||
rect(graph, inner.id, 40, 40)
|
||||
rect(graph, inner.id, 60, 40)
|
||||
rect(graph, outer.id, 80, 80)
|
||||
|
||||
computeLayout(graph, outer.id)
|
||||
|
||||
const innerNode = getNodeOrThrow(graph, inner.id)
|
||||
expect(innerNode.width).toBe(105)
|
||||
expect(innerNode.x).toBe(0)
|
||||
|
||||
const outerChildren = graph.getChildren(outer.id)
|
||||
expect(outerChildren[1].x).toBe(115)
|
||||
})
|
||||
|
||||
test('nested vertical inside horizontal', () => {
|
||||
const graph = new SceneGraph()
|
||||
const outer = autoFrame(graph, pageId(graph), {
|
||||
width: 500,
|
||||
height: 300,
|
||||
itemSpacing: 20
|
||||
})
|
||||
const inner = autoFrame(graph, outer.id, {
|
||||
layoutMode: 'VERTICAL',
|
||||
primaryAxisSizing: 'HUG',
|
||||
counterAxisSizing: 'FIXED',
|
||||
width: 100,
|
||||
height: 999,
|
||||
itemSpacing: 10
|
||||
})
|
||||
rect(graph, inner.id, 80, 50)
|
||||
rect(graph, inner.id, 80, 70)
|
||||
rect(graph, outer.id, 60, 60)
|
||||
|
||||
computeLayout(graph, outer.id)
|
||||
|
||||
const innerNode = getNodeOrThrow(graph, inner.id)
|
||||
expect(innerNode.height).toBe(130)
|
||||
|
||||
const outerChildren = graph.getChildren(outer.id)
|
||||
expect(outerChildren[0].x).toBe(0)
|
||||
expect(outerChildren[1].x).toBe(120)
|
||||
})
|
||||
|
||||
test('computeAllLayouts handles deeply nested frames', () => {
|
||||
const graph = new SceneGraph()
|
||||
const page = pageId(graph)
|
||||
const outer = autoFrame(graph, page, {
|
||||
layoutMode: 'VERTICAL',
|
||||
width: 300,
|
||||
height: 500,
|
||||
itemSpacing: 10
|
||||
})
|
||||
const middle = autoFrame(graph, outer.id, {
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisSizing: 'HUG',
|
||||
counterAxisSizing: 'HUG',
|
||||
width: 999,
|
||||
height: 999,
|
||||
itemSpacing: 5
|
||||
})
|
||||
rect(graph, middle.id, 40, 30)
|
||||
rect(graph, middle.id, 60, 30)
|
||||
rect(graph, outer.id, 100, 50)
|
||||
|
||||
computeAllLayouts(graph)
|
||||
|
||||
const middleNode = getNodeOrThrow(graph, middle.id)
|
||||
expect(middleNode.width).toBe(105)
|
||||
expect(middleNode.height).toBe(30)
|
||||
|
||||
const outerChildren = graph.getChildren(outer.id)
|
||||
expect(outerChildren[0].y).toBe(0)
|
||||
expect(outerChildren[1].y).toBe(40)
|
||||
})
|
||||
})
|
||||
24
tests/engine/layout/auto-layout/self-alignment.test.ts
Normal file
24
tests/engine/layout/auto-layout/self-alignment.test.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('self-alignment', () => {
|
||||
test('layoutAlignSelf STRETCH overrides counter axis', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 200,
|
||||
counterAxisAlign: 'MIN'
|
||||
})
|
||||
rect(graph, frame.id, 50, 50, { layoutAlignSelf: 'STRETCH' })
|
||||
rect(graph, frame.id, 50, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].height).toBe(200)
|
||||
expect(children[1].height).toBe(50)
|
||||
})
|
||||
})
|
||||
137
tests/engine/layout/auto-layout/sizing-modes.test.ts
Normal file
137
tests/engine/layout/auto-layout/sizing-modes.test.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { getNodeOrThrow } from '#tests/helpers/assert'
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('sizing modes', () => {
|
||||
test('hug contents horizontally', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
primaryAxisSizing: 'HUG',
|
||||
counterAxisSizing: 'FIXED',
|
||||
width: 999,
|
||||
height: 100,
|
||||
itemSpacing: 10
|
||||
})
|
||||
rect(graph, frame.id, 50, 50)
|
||||
rect(graph, frame.id, 70, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.width).toBe(130)
|
||||
})
|
||||
|
||||
test('hug contents vertically', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
layoutMode: 'VERTICAL',
|
||||
primaryAxisSizing: 'HUG',
|
||||
counterAxisSizing: 'FIXED',
|
||||
width: 200,
|
||||
height: 999,
|
||||
itemSpacing: 10
|
||||
})
|
||||
rect(graph, frame.id, 50, 40)
|
||||
rect(graph, frame.id, 50, 60)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.height).toBe(110)
|
||||
})
|
||||
|
||||
test('hug includes padding', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
primaryAxisSizing: 'HUG',
|
||||
counterAxisSizing: 'HUG',
|
||||
width: 999,
|
||||
height: 999,
|
||||
paddingTop: 10,
|
||||
paddingRight: 20,
|
||||
paddingBottom: 30,
|
||||
paddingLeft: 40
|
||||
})
|
||||
rect(graph, frame.id, 100, 50)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const f = getNodeOrThrow(graph, frame.id)
|
||||
expect(f.width).toBe(160)
|
||||
expect(f.height).toBe(90)
|
||||
})
|
||||
|
||||
test('child fill in horizontal layout', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 100
|
||||
})
|
||||
rect(graph, frame.id, 100, 50)
|
||||
rect(graph, frame.id, 50, 50, { layoutGrow: 1 })
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].width).toBe(100)
|
||||
expect(children[1].width).toBe(300)
|
||||
})
|
||||
|
||||
test('child fill in vertical layout', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
layoutMode: 'VERTICAL',
|
||||
width: 200,
|
||||
height: 400
|
||||
})
|
||||
rect(graph, frame.id, 50, 100)
|
||||
rect(graph, frame.id, 50, 50, { layoutGrow: 1 })
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].height).toBe(100)
|
||||
expect(children[1].height).toBe(300)
|
||||
})
|
||||
|
||||
test('multiple fill children share space equally', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 300,
|
||||
height: 100
|
||||
})
|
||||
rect(graph, frame.id, 50, 50, { layoutGrow: 1 })
|
||||
rect(graph, frame.id, 50, 50, { layoutGrow: 1 })
|
||||
rect(graph, frame.id, 50, 50, { layoutGrow: 1 })
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].width).toBe(100)
|
||||
expect(children[1].width).toBe(100)
|
||||
expect(children[2].width).toBe(100)
|
||||
})
|
||||
|
||||
test('fill with spacing and padding', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 400,
|
||||
height: 100,
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
itemSpacing: 10
|
||||
})
|
||||
rect(graph, frame.id, 100, 50)
|
||||
rect(graph, frame.id, 50, 50, { layoutGrow: 1 })
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].width).toBe(100)
|
||||
// 400 - 20 - 20 (padding) - 100 (fixed child) - 10 (spacing) = 250
|
||||
expect(children[1].width).toBe(250)
|
||||
})
|
||||
})
|
||||
400
tests/engine/layout/auto-layout/text-measurement.test.ts
Normal file
400
tests/engine/layout/auto-layout/text-measurement.test.ts
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeAllLayouts, SceneGraph, setTextMeasurer } from '@open-pencil/core'
|
||||
|
||||
import { createEditorStore } from '@/app/editor/session'
|
||||
|
||||
import { getNodeOrThrow } from '#tests/helpers/assert'
|
||||
import { autoFrame, loadFixtureGraph, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('text measurement', () => {
|
||||
test('opening imported fig keeps stored text bounds before CanvasKit measurement', async () => {
|
||||
const graph = await loadFixtureGraph('gold-preview.fig')
|
||||
const store = createEditorStore(graph)
|
||||
const title = [...store.graph.getAllNodes()].find(
|
||||
(node) => node.type === 'TEXT' && node.text === "World's largest"
|
||||
)
|
||||
const subtitle = [...store.graph.getAllNodes()].find(
|
||||
(node) =>
|
||||
node.type === 'TEXT' && node.text === 'Preline UI Figma - crafted with Tailwind CSS styles'
|
||||
)
|
||||
const description = [...store.graph.getAllNodes()].find(
|
||||
(node) =>
|
||||
node.type === 'TEXT' &&
|
||||
node.text.startsWith('Preline UI Figma is the largest free design system for Figma')
|
||||
)
|
||||
|
||||
if (!title || !subtitle || !description) {
|
||||
throw new Error('Expected imported text nodes in gold-preview.fig')
|
||||
}
|
||||
|
||||
expect(title.width).toBe(444)
|
||||
expect(title.height).toBe(73)
|
||||
expect(subtitle.width).toBe(439)
|
||||
expect(subtitle.height).toBe(22)
|
||||
expect(description.width).toBe(878)
|
||||
expect(description.height).toBe(60)
|
||||
|
||||
await store.switchPage(store.graph.getPages()[0].id)
|
||||
|
||||
expect(store.graph.getNode(title.id)?.width).toBe(444)
|
||||
expect(store.graph.getNode(title.id)?.height).toBe(73)
|
||||
expect(store.graph.getNode(subtitle.id)?.width).toBe(439)
|
||||
expect(store.graph.getNode(subtitle.id)?.height).toBe(22)
|
||||
expect(store.graph.getNode(description.id)?.width).toBe(878)
|
||||
expect(store.graph.getNode(description.id)?.height).toBe(60)
|
||||
})
|
||||
|
||||
test('imported nested instance layout keeps hidden sibling offsets stable', async () => {
|
||||
const graph = await loadFixtureGraph('gold-preview.fig')
|
||||
const previewRoot = graph.getChildren(graph.getPages()[0].id)[0]
|
||||
const wysiwygEditor = graph
|
||||
.getChildren(previewRoot.id)
|
||||
.find((node) => node.name === '_WYSIWYG-editor')
|
||||
const toolbarVariant = wysiwygEditor
|
||||
? graph.getChildren(wysiwygEditor.id).find((node) => node.name === '_on-text-WYSIWYG-toolbar')
|
||||
: undefined
|
||||
const toolbarRow = toolbarVariant
|
||||
? graph.getChildren(toolbarVariant.id).find((node) => node.name === 'Toolbar')
|
||||
: undefined
|
||||
const hiddenInput = toolbarRow
|
||||
? graph.getChildren(toolbarRow.id).find((node) => node.name === 'Input')
|
||||
: undefined
|
||||
const visibleToolbar = toolbarRow
|
||||
? graph
|
||||
.getChildren(toolbarRow.id)
|
||||
.find((node) => node.name === 'Toolbar' && node.id !== toolbarRow.id)
|
||||
: undefined
|
||||
|
||||
if (!toolbarRow || !hiddenInput || !visibleToolbar) {
|
||||
throw new Error('Expected imported WYSIWYG toolbar nodes in gold-preview.fig')
|
||||
}
|
||||
|
||||
expect(hiddenInput.visible).toBe(false)
|
||||
// x=8 is the Figma-exported fixture value (verified via fixture inspection 2026-05-03).
|
||||
// The old assertion of x=298 was incorrect — it was a post-layout computed value from
|
||||
// an older layout implementation. Post-layout, left-aligned HUG content retains x=8.
|
||||
expect(visibleToolbar.x).toBe(8)
|
||||
|
||||
computeAllLayouts(graph, graph.getPages()[0].id)
|
||||
|
||||
expect(graph.getNode(visibleToolbar.id)?.x).toBe(8)
|
||||
expect(graph.getNode(visibleToolbar.id)?.y).toBe(8)
|
||||
})
|
||||
|
||||
test('WIDTH_AND_HEIGHT text uses measured width in centered layout', () => {
|
||||
const graph = new SceneGraph()
|
||||
const pid = pageId(graph)
|
||||
|
||||
const frame = autoFrame(graph, pid, {
|
||||
width: 300,
|
||||
height: 40,
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
primaryAxisAlign: 'CENTER',
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
itemSpacing: 10
|
||||
})
|
||||
|
||||
const arrow1 = graph.createNode('FRAME', frame.id, { width: 20, height: 20 })
|
||||
const text = graph.createNode('TEXT', frame.id, {
|
||||
width: 200,
|
||||
height: 20,
|
||||
text: 'Test',
|
||||
fontSize: 14,
|
||||
textAutoResize: 'WIDTH_AND_HEIGHT' as const
|
||||
})
|
||||
const arrow2 = graph.createNode('FRAME', frame.id, { width: 20, height: 20 })
|
||||
|
||||
setTextMeasurer((node) => {
|
||||
if (node.type === 'TEXT' && node.textAutoResize === 'WIDTH_AND_HEIGHT') {
|
||||
return { width: 60, height: 20 }
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
computeAllLayouts(graph)
|
||||
|
||||
setTextMeasurer(null)
|
||||
|
||||
const updatedText = getNodeOrThrow(graph, text.id)
|
||||
const updatedArrow1 = getNodeOrThrow(graph, arrow1.id)
|
||||
const updatedArrow2 = getNodeOrThrow(graph, arrow2.id)
|
||||
|
||||
expect(updatedText.width).toBe(60)
|
||||
|
||||
// Total content: 10 + 20 + 10 + 60 + 10 + 20 + 10 = 140
|
||||
// Free space: 300 - 140 = 160, centered offset = 80
|
||||
expect(updatedArrow1.x).toBe(90)
|
||||
expect(updatedText.x).toBe(120)
|
||||
expect(updatedArrow2.x).toBe(190)
|
||||
})
|
||||
|
||||
test('without measurer, text preserves stored dimensions from .fig import', () => {
|
||||
const graph = new SceneGraph()
|
||||
const pid = pageId(graph)
|
||||
|
||||
const frame = autoFrame(graph, pid, {
|
||||
width: 300,
|
||||
height: 40,
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
primaryAxisAlign: 'CENTER'
|
||||
})
|
||||
|
||||
graph.createNode('TEXT', frame.id, {
|
||||
width: 200,
|
||||
height: 20,
|
||||
text: 'Test',
|
||||
fontSize: 14,
|
||||
textAutoResize: 'WIDTH_AND_HEIGHT' as const
|
||||
})
|
||||
|
||||
setTextMeasurer(null)
|
||||
computeAllLayouts(graph)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
const updatedText = children[0]
|
||||
expect(updatedText.width).toBe(200)
|
||||
expect(updatedText.height).toBe(20)
|
||||
})
|
||||
|
||||
test('without measurer, text with 100x100 default falls back to estimate', () => {
|
||||
const graph = new SceneGraph()
|
||||
const pid = pageId(graph)
|
||||
|
||||
const frame = autoFrame(graph, pid, {
|
||||
width: 300,
|
||||
height: 40,
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
primaryAxisAlign: 'CENTER'
|
||||
})
|
||||
|
||||
graph.createNode('TEXT', frame.id, {
|
||||
width: 100,
|
||||
height: 100,
|
||||
text: 'Test',
|
||||
fontSize: 14,
|
||||
textAutoResize: 'WIDTH_AND_HEIGHT' as const
|
||||
})
|
||||
|
||||
setTextMeasurer(null)
|
||||
computeAllLayouts(graph)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
const updatedText = children[0]
|
||||
expect(updatedText.width).toBeLessThan(100)
|
||||
expect(updatedText.width).toBeGreaterThan(0)
|
||||
expect(updatedText.height).toBeLessThan(100)
|
||||
expect(updatedText.height).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('without measurer, HEIGHT text preserves stored height from .fig import', () => {
|
||||
const graph = new SceneGraph()
|
||||
const pid = pageId(graph)
|
||||
|
||||
const frame = autoFrame(graph, pid, {
|
||||
width: 269,
|
||||
height: 400,
|
||||
layoutMode: 'VERTICAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED'
|
||||
})
|
||||
|
||||
graph.createNode('TEXT', frame.id, {
|
||||
width: 269,
|
||||
height: 66,
|
||||
text: 'GDP Growth Exceeds Expectations at 3.1% in Q2 Report That Nobody Expected',
|
||||
fontSize: 15,
|
||||
lineHeight: 22,
|
||||
textAutoResize: 'HEIGHT' as const
|
||||
})
|
||||
|
||||
setTextMeasurer(null)
|
||||
computeAllLayouts(graph)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
const updatedText = children[0]
|
||||
expect(updatedText.height).toBe(66)
|
||||
expect(updatedText.width).toBe(269)
|
||||
})
|
||||
|
||||
test('without measurer, HEIGHT text with 100x100 default falls back to estimate', () => {
|
||||
const graph = new SceneGraph()
|
||||
const pid = pageId(graph)
|
||||
|
||||
const frame = autoFrame(graph, pid, {
|
||||
width: 269,
|
||||
height: 400,
|
||||
layoutMode: 'VERTICAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED'
|
||||
})
|
||||
|
||||
graph.createNode('TEXT', frame.id, {
|
||||
width: 100,
|
||||
height: 100,
|
||||
text: 'GDP Growth Exceeds Expectations at 3.1% in Q2 Report That Nobody Expected',
|
||||
fontSize: 15,
|
||||
lineHeight: 22,
|
||||
textAutoResize: 'HEIGHT' as const
|
||||
})
|
||||
|
||||
setTextMeasurer(null)
|
||||
computeAllLayouts(graph)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
const updatedText = children[0]
|
||||
expect(updatedText.height).toBeGreaterThan(22)
|
||||
})
|
||||
|
||||
test('text with w="fill" in flex="col" stretches to parent width', () => {
|
||||
const graph = new SceneGraph()
|
||||
const pid = pageId(graph)
|
||||
|
||||
const frame = autoFrame(graph, pid, {
|
||||
width: 300,
|
||||
height: 400,
|
||||
layoutMode: 'VERTICAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20
|
||||
})
|
||||
|
||||
const text = graph.createNode('TEXT', frame.id, {
|
||||
width: 100,
|
||||
height: 20,
|
||||
text: 'This text should fill the parent width',
|
||||
fontSize: 14,
|
||||
textAutoResize: 'HEIGHT' as const,
|
||||
layoutAlignSelf: 'STRETCH' as const
|
||||
})
|
||||
|
||||
setTextMeasurer((_node, maxWidth) => {
|
||||
const w = maxWidth ?? 260
|
||||
return { width: w, height: 20 }
|
||||
})
|
||||
|
||||
computeAllLayouts(graph)
|
||||
setTextMeasurer(null)
|
||||
|
||||
const updatedText = getNodeOrThrow(graph, text.id)
|
||||
// Should stretch to 300 - 20 - 20 = 260, NOT stay at 100
|
||||
expect(updatedText.width).toBe(260)
|
||||
})
|
||||
|
||||
test('HEIGHT auto-resize text wraps via MeasureFunc when filling parent', () => {
|
||||
const graph = new SceneGraph()
|
||||
const pid = pageId(graph)
|
||||
|
||||
const frame = autoFrame(graph, pid, {
|
||||
width: 300,
|
||||
height: 200,
|
||||
layoutMode: 'VERTICAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED'
|
||||
})
|
||||
|
||||
const text = graph.createNode('TEXT', frame.id, {
|
||||
width: 300,
|
||||
height: 20,
|
||||
text: 'Long text that should wrap within the available width',
|
||||
fontSize: 14,
|
||||
textAutoResize: 'HEIGHT' as const
|
||||
})
|
||||
|
||||
setTextMeasurer((_node, maxWidth) => {
|
||||
const w = maxWidth ?? 1e6
|
||||
if (w >= 300) return { width: 300, height: 20 }
|
||||
return { width: w, height: 60 }
|
||||
})
|
||||
|
||||
computeAllLayouts(graph)
|
||||
setTextMeasurer(null)
|
||||
|
||||
const updatedText = getNodeOrThrow(graph, text.id)
|
||||
expect(updatedText.width).toBe(300)
|
||||
expect(updatedText.height).toBe(20)
|
||||
})
|
||||
|
||||
test('MeasureFunc receives constraint width from flex layout', () => {
|
||||
const graph = new SceneGraph()
|
||||
const pid = pageId(graph)
|
||||
|
||||
const frame = autoFrame(graph, pid, {
|
||||
width: 400,
|
||||
height: 200,
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED',
|
||||
itemSpacing: 10
|
||||
})
|
||||
|
||||
rect(graph, frame.id, 100, 50)
|
||||
|
||||
const text = graph.createNode('TEXT', frame.id, {
|
||||
width: 500,
|
||||
height: 20,
|
||||
text: 'Wide text',
|
||||
fontSize: 14,
|
||||
textAutoResize: 'WIDTH_AND_HEIGHT' as const,
|
||||
layoutGrow: 1
|
||||
})
|
||||
|
||||
const receivedWidths: number[] = []
|
||||
setTextMeasurer((_node, maxWidth) => {
|
||||
if (maxWidth !== undefined) receivedWidths.push(Math.round(maxWidth))
|
||||
const w = maxWidth ?? 500
|
||||
return { width: Math.min(200, w), height: w < 200 ? 40 : 20 }
|
||||
})
|
||||
|
||||
computeAllLayouts(graph)
|
||||
setTextMeasurer(null)
|
||||
|
||||
// 400 - 100 - 10 = 290 available for the fill text
|
||||
expect(receivedWidths.length).toBeGreaterThan(0)
|
||||
const updatedText = getNodeOrThrow(graph, text.id)
|
||||
expect(updatedText.width).toBe(290)
|
||||
})
|
||||
|
||||
test('textAutoResize NONE skips MeasureFunc', () => {
|
||||
const graph = new SceneGraph()
|
||||
const pid = pageId(graph)
|
||||
|
||||
const frame = autoFrame(graph, pid, {
|
||||
width: 300,
|
||||
height: 100,
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED'
|
||||
})
|
||||
|
||||
const text = graph.createNode('TEXT', frame.id, {
|
||||
width: 150,
|
||||
height: 40,
|
||||
text: 'Fixed text',
|
||||
fontSize: 14,
|
||||
textAutoResize: 'NONE' as const
|
||||
})
|
||||
|
||||
let measureCalled = false
|
||||
setTextMeasurer(() => {
|
||||
measureCalled = true
|
||||
return { width: 80, height: 20 }
|
||||
})
|
||||
|
||||
computeAllLayouts(graph)
|
||||
setTextMeasurer(null)
|
||||
|
||||
expect(measureCalled).toBe(false)
|
||||
const updatedText = getNodeOrThrow(graph, text.id)
|
||||
expect(updatedText.width).toBe(150)
|
||||
expect(updatedText.height).toBe(40)
|
||||
})
|
||||
})
|
||||
46
tests/engine/layout/auto-layout/vertical-basic.test.ts
Normal file
46
tests/engine/layout/auto-layout/vertical-basic.test.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('vertical basic', () => {
|
||||
test('positions children top-to-bottom', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
layoutMode: 'VERTICAL',
|
||||
width: 200,
|
||||
height: 400
|
||||
})
|
||||
rect(graph, frame.id, 50, 80)
|
||||
rect(graph, frame.id, 50, 60)
|
||||
rect(graph, frame.id, 50, 100)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].y).toBe(0)
|
||||
expect(children[1].y).toBe(80)
|
||||
expect(children[2].y).toBe(140)
|
||||
})
|
||||
|
||||
test('applies item spacing vertically', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
layoutMode: 'VERTICAL',
|
||||
width: 200,
|
||||
height: 400,
|
||||
itemSpacing: 16
|
||||
})
|
||||
rect(graph, frame.id, 50, 40)
|
||||
rect(graph, frame.id, 50, 40)
|
||||
rect(graph, frame.id, 50, 40)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].y).toBe(0)
|
||||
expect(children[1].y).toBe(56)
|
||||
expect(children[2].y).toBe(112)
|
||||
})
|
||||
})
|
||||
72
tests/engine/layout/auto-layout/wrap-layout.test.ts
Normal file
72
tests/engine/layout/auto-layout/wrap-layout.test.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeLayout, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
import { autoFrame, pageId, rect } from '#tests/helpers/layout'
|
||||
|
||||
describe('wrap layout', () => {
|
||||
test('wraps children in horizontal layout', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 200,
|
||||
height: 300,
|
||||
layoutWrap: 'WRAP'
|
||||
})
|
||||
rect(graph, frame.id, 80, 40)
|
||||
rect(graph, frame.id, 80, 40)
|
||||
rect(graph, frame.id, 80, 40)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
// First two fit on first row (80 + 80 = 160 <= 200)
|
||||
expect(children[0].x).toBe(0)
|
||||
expect(children[0].y).toBe(0)
|
||||
expect(children[1].x).toBe(80)
|
||||
expect(children[1].y).toBe(0)
|
||||
// Third wraps to second row
|
||||
expect(children[2].x).toBe(0)
|
||||
expect(children[2].y).toBe(40)
|
||||
})
|
||||
|
||||
test('counterAxisSpacing adds gap between wrapped rows', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 200,
|
||||
height: 300,
|
||||
layoutWrap: 'WRAP',
|
||||
counterAxisSpacing: 10
|
||||
})
|
||||
rect(graph, frame.id, 120, 40)
|
||||
rect(graph, frame.id, 120, 40)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].y).toBe(0)
|
||||
expect(children[1].y).toBe(50)
|
||||
})
|
||||
|
||||
test('itemSpacing with wrap', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = autoFrame(graph, pageId(graph), {
|
||||
width: 200,
|
||||
height: 300,
|
||||
layoutWrap: 'WRAP',
|
||||
itemSpacing: 10
|
||||
})
|
||||
// 90 + 10 + 90 = 190, fits in 200
|
||||
rect(graph, frame.id, 90, 40)
|
||||
rect(graph, frame.id, 90, 40)
|
||||
// 90 wraps to next row
|
||||
rect(graph, frame.id, 90, 40)
|
||||
|
||||
computeLayout(graph, frame.id)
|
||||
|
||||
const children = graph.getChildren(frame.id)
|
||||
expect(children[0].x).toBe(0)
|
||||
expect(children[1].x).toBe(100)
|
||||
expect(children[2].x).toBe(0)
|
||||
expect(children[2].y).toBe(40)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue