@zseven-w/pen-core 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -9
- package/package.json +5 -5
- package/src/__tests__/arc-path.test.ts +23 -23
- package/src/__tests__/codegen-utils.test.ts +50 -0
- package/src/__tests__/design-md-parser.test.ts +49 -0
- package/src/__tests__/font-utils.test.ts +15 -15
- package/src/__tests__/layout-engine.test.ts +169 -83
- package/src/__tests__/merge-helpers.test.ts +143 -0
- package/src/__tests__/node-diff.test.ts +139 -0
- package/src/__tests__/node-helpers.test.ts +19 -19
- package/src/__tests__/node-merge.test.ts +425 -0
- package/src/__tests__/normalize-stroke-fill-schema.test.ts +416 -0
- package/src/__tests__/normalize-tree-layout.test.ts +294 -0
- package/src/__tests__/normalize.test.ts +119 -80
- package/src/__tests__/path-anchors.test.ts +98 -0
- package/src/__tests__/resolve-variables-recursive.test.ts +109 -54
- package/src/__tests__/strip-redundant-section-fills.test.ts +278 -0
- package/src/__tests__/text-measure.test.ts +84 -79
- package/src/__tests__/tree-utils.test.ts +133 -102
- package/src/__tests__/unwrap-fake-phone-mockup.test.ts +322 -0
- package/src/__tests__/variables.test.ts +68 -65
- package/src/arc-path.ts +35 -35
- package/src/boolean-ops.ts +131 -142
- package/src/constants.ts +36 -36
- package/src/design-md-parser.ts +363 -0
- package/src/font-utils.ts +30 -15
- package/src/id.ts +1 -1
- package/src/index.ts +47 -13
- package/src/layout/engine.ts +255 -224
- package/src/layout/normalize-tree.ts +140 -0
- package/src/layout/strip-redundant-section-fills.ts +155 -0
- package/src/layout/text-measure.ts +130 -106
- package/src/layout/unwrap-fake-phone-mockup.ts +147 -0
- package/src/merge/index.ts +16 -0
- package/src/merge/merge-helpers.ts +113 -0
- package/src/merge/node-diff.ts +123 -0
- package/src/merge/node-merge.ts +651 -0
- package/src/node-helpers.ts +18 -5
- package/src/normalize/normalize-stroke-fill-schema.ts +297 -0
- package/src/normalize.ts +75 -81
- package/src/path-anchors.ts +331 -0
- package/src/sync-lock.ts +3 -3
- package/src/tree-utils.ts +180 -158
- package/src/variables/replace-refs.ts +63 -60
- package/src/variables/resolve.ts +98 -106
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import type { PenNode } from '@zseven-w/pen-types'
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { PenNode } from '@zseven-w/pen-types';
|
|
3
3
|
import {
|
|
4
4
|
parseSizing,
|
|
5
5
|
defaultLineHeight,
|
|
@@ -11,137 +11,142 @@ import {
|
|
|
11
11
|
resolveTextContent,
|
|
12
12
|
countExplicitTextLines,
|
|
13
13
|
estimateTextHeight,
|
|
14
|
-
} from '../layout/text-measure'
|
|
14
|
+
} from '../layout/text-measure';
|
|
15
15
|
|
|
16
16
|
describe('text-measure', () => {
|
|
17
17
|
describe('parseSizing', () => {
|
|
18
18
|
it('returns number for number input', () => {
|
|
19
|
-
expect(parseSizing(100)).toBe(100)
|
|
20
|
-
})
|
|
19
|
+
expect(parseSizing(100)).toBe(100);
|
|
20
|
+
});
|
|
21
21
|
|
|
22
22
|
it('returns "fill" for fill_container', () => {
|
|
23
|
-
expect(parseSizing('fill_container')).toBe('fill')
|
|
24
|
-
})
|
|
23
|
+
expect(parseSizing('fill_container')).toBe('fill');
|
|
24
|
+
});
|
|
25
25
|
|
|
26
26
|
it('returns "fit" for fit_content', () => {
|
|
27
|
-
expect(parseSizing('fit_content')).toBe('fit')
|
|
28
|
-
})
|
|
27
|
+
expect(parseSizing('fit_content')).toBe('fit');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('uses numeric hints from fit_content(N)', () => {
|
|
31
|
+
expect(parseSizing('fit_content(600)')).toBe(600);
|
|
32
|
+
});
|
|
29
33
|
|
|
30
34
|
it('parses numeric strings', () => {
|
|
31
|
-
expect(parseSizing('200')).toBe(200)
|
|
32
|
-
})
|
|
35
|
+
expect(parseSizing('200')).toBe(200);
|
|
36
|
+
});
|
|
33
37
|
|
|
34
38
|
it('returns 0 for non-parseable', () => {
|
|
35
|
-
expect(parseSizing(undefined)).toBe(0)
|
|
36
|
-
expect(parseSizing('abc')).toBe(0)
|
|
37
|
-
})
|
|
38
|
-
})
|
|
39
|
+
expect(parseSizing(undefined)).toBe(0);
|
|
40
|
+
expect(parseSizing('abc')).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
39
43
|
|
|
40
44
|
describe('defaultLineHeight', () => {
|
|
41
45
|
it('returns tight leading for display text', () => {
|
|
42
|
-
expect(defaultLineHeight(48)).toBe(1.0)
|
|
43
|
-
})
|
|
46
|
+
expect(defaultLineHeight(48)).toBe(1.0);
|
|
47
|
+
});
|
|
44
48
|
|
|
45
49
|
it('returns comfortable leading for body text', () => {
|
|
46
|
-
expect(defaultLineHeight(14)).toBe(1.5)
|
|
47
|
-
})
|
|
48
|
-
})
|
|
50
|
+
expect(defaultLineHeight(14)).toBe(1.5);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
49
53
|
|
|
50
54
|
describe('CJK detection', () => {
|
|
51
55
|
it('detects CJK code points', () => {
|
|
52
|
-
expect(isCjkCodePoint('中'.codePointAt(0)!)).toBe(true)
|
|
53
|
-
expect(isCjkCodePoint('あ'.codePointAt(0)!)).toBe(true)
|
|
54
|
-
expect(isCjkCodePoint('A'.codePointAt(0)!)).toBe(false)
|
|
55
|
-
})
|
|
56
|
+
expect(isCjkCodePoint('中'.codePointAt(0)!)).toBe(true);
|
|
57
|
+
expect(isCjkCodePoint('あ'.codePointAt(0)!)).toBe(true);
|
|
58
|
+
expect(isCjkCodePoint('A'.codePointAt(0)!)).toBe(false);
|
|
59
|
+
});
|
|
56
60
|
|
|
57
61
|
it('hasCjkText detects CJK in strings', () => {
|
|
58
|
-
expect(hasCjkText('Hello 世界')).toBe(true)
|
|
59
|
-
expect(hasCjkText('Hello World')).toBe(false)
|
|
60
|
-
})
|
|
61
|
-
})
|
|
62
|
+
expect(hasCjkText('Hello 世界')).toBe(true);
|
|
63
|
+
expect(hasCjkText('Hello World')).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
62
66
|
|
|
63
67
|
describe('estimateGlyphWidth', () => {
|
|
64
68
|
it('returns 0 for newline', () => {
|
|
65
|
-
expect(estimateGlyphWidth('\n', 16)).toBe(0)
|
|
66
|
-
})
|
|
69
|
+
expect(estimateGlyphWidth('\n', 16)).toBe(0);
|
|
70
|
+
});
|
|
67
71
|
|
|
68
72
|
it('estimates CJK wider than Latin', () => {
|
|
69
|
-
const cjk = estimateGlyphWidth('中', 16)
|
|
70
|
-
const latin = estimateGlyphWidth('a', 16)
|
|
71
|
-
expect(cjk).toBeGreaterThan(latin)
|
|
72
|
-
})
|
|
73
|
+
const cjk = estimateGlyphWidth('中', 16);
|
|
74
|
+
const latin = estimateGlyphWidth('a', 16);
|
|
75
|
+
expect(cjk).toBeGreaterThan(latin);
|
|
76
|
+
});
|
|
73
77
|
|
|
74
78
|
it('estimates uppercase wider than lowercase', () => {
|
|
75
|
-
const upper = estimateGlyphWidth('A', 16)
|
|
76
|
-
const lower = estimateGlyphWidth('a', 16)
|
|
77
|
-
expect(upper).toBeGreaterThan(lower)
|
|
78
|
-
})
|
|
79
|
-
})
|
|
79
|
+
const upper = estimateGlyphWidth('A', 16);
|
|
80
|
+
const lower = estimateGlyphWidth('a', 16);
|
|
81
|
+
expect(upper).toBeGreaterThan(lower);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
80
84
|
|
|
81
85
|
describe('estimateLineWidth', () => {
|
|
82
86
|
it('estimates width of a line', () => {
|
|
83
|
-
const width = estimateLineWidth('Hello', 16)
|
|
84
|
-
expect(width).toBeGreaterThan(0)
|
|
85
|
-
})
|
|
87
|
+
const width = estimateLineWidth('Hello', 16);
|
|
88
|
+
expect(width).toBeGreaterThan(0);
|
|
89
|
+
});
|
|
86
90
|
|
|
87
91
|
it('adds letter spacing', () => {
|
|
88
|
-
const base = estimateLineWidth('AB', 16, 0)
|
|
89
|
-
const spaced = estimateLineWidth('AB', 16, 5)
|
|
90
|
-
expect(spaced).toBeGreaterThan(base)
|
|
91
|
-
})
|
|
92
|
-
})
|
|
92
|
+
const base = estimateLineWidth('AB', 16, 0);
|
|
93
|
+
const spaced = estimateLineWidth('AB', 16, 5);
|
|
94
|
+
expect(spaced).toBeGreaterThan(base);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
93
97
|
|
|
94
98
|
describe('estimateTextWidth', () => {
|
|
95
99
|
it('returns the widest line', () => {
|
|
96
|
-
const width = estimateTextWidth('short\nmuch longer line', 16)
|
|
97
|
-
const singleWidth = estimateTextWidth('much longer line', 16)
|
|
100
|
+
const width = estimateTextWidth('short\nmuch longer line', 16);
|
|
101
|
+
const singleWidth = estimateTextWidth('much longer line', 16);
|
|
98
102
|
// Multi-line should return width of longest line
|
|
99
|
-
expect(width).toBeCloseTo(singleWidth, 0)
|
|
100
|
-
})
|
|
101
|
-
})
|
|
103
|
+
expect(width).toBeCloseTo(singleWidth, 0);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
102
106
|
|
|
103
107
|
describe('resolveTextContent', () => {
|
|
104
108
|
it('resolves string content', () => {
|
|
105
|
-
const node: PenNode = { id: '1', type: 'text', content: 'Hello' }
|
|
106
|
-
expect(resolveTextContent(node)).toBe('Hello')
|
|
107
|
-
})
|
|
109
|
+
const node: PenNode = { id: '1', type: 'text', content: 'Hello' };
|
|
110
|
+
expect(resolveTextContent(node)).toBe('Hello');
|
|
111
|
+
});
|
|
108
112
|
|
|
109
113
|
it('resolves styled segment content', () => {
|
|
110
114
|
const node: PenNode = {
|
|
111
|
-
id: '1',
|
|
115
|
+
id: '1',
|
|
116
|
+
type: 'text',
|
|
112
117
|
content: [{ text: 'Hello ' }, { text: 'World' }],
|
|
113
|
-
}
|
|
114
|
-
expect(resolveTextContent(node)).toBe('Hello World')
|
|
115
|
-
})
|
|
118
|
+
};
|
|
119
|
+
expect(resolveTextContent(node)).toBe('Hello World');
|
|
120
|
+
});
|
|
116
121
|
|
|
117
122
|
it('returns empty for non-text nodes', () => {
|
|
118
|
-
const node: PenNode = { id: '1', type: 'rectangle' }
|
|
119
|
-
expect(resolveTextContent(node)).toBe('')
|
|
120
|
-
})
|
|
121
|
-
})
|
|
123
|
+
const node: PenNode = { id: '1', type: 'rectangle' };
|
|
124
|
+
expect(resolveTextContent(node)).toBe('');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
122
127
|
|
|
123
128
|
describe('countExplicitTextLines', () => {
|
|
124
129
|
it('counts newlines', () => {
|
|
125
|
-
expect(countExplicitTextLines('a\nb\nc')).toBe(3)
|
|
126
|
-
})
|
|
130
|
+
expect(countExplicitTextLines('a\nb\nc')).toBe(3);
|
|
131
|
+
});
|
|
127
132
|
|
|
128
133
|
it('returns 1 for empty string', () => {
|
|
129
|
-
expect(countExplicitTextLines('')).toBe(1)
|
|
130
|
-
})
|
|
131
|
-
})
|
|
134
|
+
expect(countExplicitTextLines('')).toBe(1);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
132
137
|
|
|
133
138
|
describe('estimateTextHeight', () => {
|
|
134
139
|
it('estimates height for single-line text', () => {
|
|
135
|
-
const node: PenNode = { id: '1', type: 'text', content: 'Hello', fontSize: 16 }
|
|
136
|
-
const height = estimateTextHeight(node)
|
|
137
|
-
expect(height).toBeGreaterThan(0)
|
|
138
|
-
expect(height).toBeLessThan(50)
|
|
139
|
-
})
|
|
140
|
+
const node: PenNode = { id: '1', type: 'text', content: 'Hello', fontSize: 16 };
|
|
141
|
+
const height = estimateTextHeight(node);
|
|
142
|
+
expect(height).toBeGreaterThan(0);
|
|
143
|
+
expect(height).toBeLessThan(50);
|
|
144
|
+
});
|
|
140
145
|
|
|
141
146
|
it('estimates taller for multi-line text', () => {
|
|
142
|
-
const single: PenNode = { id: '1', type: 'text', content: 'Hello', fontSize: 16 }
|
|
143
|
-
const multi: PenNode = { id: '2', type: 'text', content: 'Hello\nWorld', fontSize: 16 }
|
|
144
|
-
expect(estimateTextHeight(multi)).toBeGreaterThan(estimateTextHeight(single))
|
|
145
|
-
})
|
|
146
|
-
})
|
|
147
|
-
})
|
|
147
|
+
const single: PenNode = { id: '1', type: 'text', content: 'Hello', fontSize: 16 };
|
|
148
|
+
const multi: PenNode = { id: '2', type: 'text', content: 'Hello\nWorld', fontSize: 16 };
|
|
149
|
+
expect(estimateTextHeight(multi)).toBeGreaterThan(estimateTextHeight(single));
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import type { PenNode, PenDocument } from '@zseven-w/pen-types'
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { PenNode, PenDocument } from '@zseven-w/pen-types';
|
|
3
3
|
import {
|
|
4
4
|
createEmptyDocument,
|
|
5
5
|
findNodeInTree,
|
|
@@ -14,157 +14,188 @@ import {
|
|
|
14
14
|
migrateToPages,
|
|
15
15
|
DEFAULT_FRAME_ID,
|
|
16
16
|
DEFAULT_PAGE_ID,
|
|
17
|
-
} from '../tree-utils'
|
|
17
|
+
} from '../tree-utils';
|
|
18
18
|
|
|
19
19
|
const frame = (id: string, children: PenNode[] = []): PenNode => ({
|
|
20
|
-
id,
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
id,
|
|
21
|
+
type: 'frame',
|
|
22
|
+
name: id,
|
|
23
|
+
x: 0,
|
|
24
|
+
y: 0,
|
|
25
|
+
width: 100,
|
|
26
|
+
height: 100,
|
|
27
|
+
fill: [{ type: 'solid', color: '#fff' }],
|
|
28
|
+
children,
|
|
29
|
+
});
|
|
23
30
|
|
|
24
31
|
const rect = (id: string): PenNode => ({
|
|
25
|
-
id,
|
|
26
|
-
|
|
32
|
+
id,
|
|
33
|
+
type: 'rectangle',
|
|
34
|
+
x: 0,
|
|
35
|
+
y: 0,
|
|
36
|
+
width: 50,
|
|
37
|
+
height: 50,
|
|
38
|
+
});
|
|
27
39
|
|
|
28
40
|
describe('tree-utils', () => {
|
|
29
41
|
describe('createEmptyDocument', () => {
|
|
30
42
|
it('creates a document with a default page and root frame', () => {
|
|
31
|
-
const doc = createEmptyDocument()
|
|
32
|
-
expect(doc.version).toBe('1.0.0')
|
|
33
|
-
expect(doc.pages).toHaveLength(1)
|
|
34
|
-
expect(doc.pages![0].id).toBe(DEFAULT_PAGE_ID)
|
|
35
|
-
expect(doc.pages![0].children).toHaveLength(1)
|
|
36
|
-
expect(doc.pages![0].children[0].id).toBe(DEFAULT_FRAME_ID)
|
|
37
|
-
})
|
|
38
|
-
})
|
|
43
|
+
const doc = createEmptyDocument();
|
|
44
|
+
expect(doc.version).toBe('1.0.0');
|
|
45
|
+
expect(doc.pages).toHaveLength(1);
|
|
46
|
+
expect(doc.pages![0].id).toBe(DEFAULT_PAGE_ID);
|
|
47
|
+
expect(doc.pages![0].children).toHaveLength(1);
|
|
48
|
+
expect(doc.pages![0].children[0].id).toBe(DEFAULT_FRAME_ID);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
39
51
|
|
|
40
52
|
describe('findNodeInTree', () => {
|
|
41
53
|
it('finds a node by id at root level', () => {
|
|
42
|
-
const nodes = [rect('a'), rect('b')]
|
|
43
|
-
expect(findNodeInTree(nodes, 'b')?.id).toBe('b')
|
|
44
|
-
})
|
|
54
|
+
const nodes = [rect('a'), rect('b')];
|
|
55
|
+
expect(findNodeInTree(nodes, 'b')?.id).toBe('b');
|
|
56
|
+
});
|
|
45
57
|
|
|
46
58
|
it('finds a nested node', () => {
|
|
47
|
-
const nodes = [frame('parent', [rect('child')])]
|
|
48
|
-
expect(findNodeInTree(nodes, 'child')?.id).toBe('child')
|
|
49
|
-
})
|
|
59
|
+
const nodes = [frame('parent', [rect('child')])];
|
|
60
|
+
expect(findNodeInTree(nodes, 'child')?.id).toBe('child');
|
|
61
|
+
});
|
|
50
62
|
|
|
51
63
|
it('returns undefined for missing node', () => {
|
|
52
|
-
expect(findNodeInTree([rect('a')], 'missing')).toBeUndefined()
|
|
53
|
-
})
|
|
54
|
-
})
|
|
64
|
+
expect(findNodeInTree([rect('a')], 'missing')).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
55
67
|
|
|
56
68
|
describe('findParentInTree', () => {
|
|
57
69
|
it('finds the parent of a child node', () => {
|
|
58
|
-
const nodes = [frame('parent', [rect('child')])]
|
|
59
|
-
expect(findParentInTree(nodes, 'child')?.id).toBe('parent')
|
|
60
|
-
})
|
|
70
|
+
const nodes = [frame('parent', [rect('child')])];
|
|
71
|
+
expect(findParentInTree(nodes, 'child')?.id).toBe('parent');
|
|
72
|
+
});
|
|
61
73
|
|
|
62
74
|
it('returns undefined for root nodes', () => {
|
|
63
|
-
expect(findParentInTree([rect('root')], 'root')).toBeUndefined()
|
|
64
|
-
})
|
|
65
|
-
})
|
|
75
|
+
expect(findParentInTree([rect('root')], 'root')).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
66
78
|
|
|
67
79
|
describe('removeNodeFromTree', () => {
|
|
68
80
|
it('removes a root node', () => {
|
|
69
|
-
const result = removeNodeFromTree([rect('a'), rect('b')], 'a')
|
|
70
|
-
expect(result).toHaveLength(1)
|
|
71
|
-
expect(result[0].id).toBe('b')
|
|
72
|
-
})
|
|
81
|
+
const result = removeNodeFromTree([rect('a'), rect('b')], 'a');
|
|
82
|
+
expect(result).toHaveLength(1);
|
|
83
|
+
expect(result[0].id).toBe('b');
|
|
84
|
+
});
|
|
73
85
|
|
|
74
86
|
it('removes a nested node', () => {
|
|
75
|
-
const nodes = [frame('parent', [rect('child1'), rect('child2')])]
|
|
76
|
-
const result = removeNodeFromTree(nodes, 'child1')
|
|
77
|
-
const parent = result[0] as PenNode & { children: PenNode[] }
|
|
78
|
-
expect(parent.children).toHaveLength(1)
|
|
79
|
-
expect(parent.children[0].id).toBe('child2')
|
|
80
|
-
})
|
|
81
|
-
})
|
|
87
|
+
const nodes = [frame('parent', [rect('child1'), rect('child2')])];
|
|
88
|
+
const result = removeNodeFromTree(nodes, 'child1');
|
|
89
|
+
const parent = result[0] as PenNode & { children: PenNode[] };
|
|
90
|
+
expect(parent.children).toHaveLength(1);
|
|
91
|
+
expect(parent.children[0].id).toBe('child2');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
82
94
|
|
|
83
95
|
describe('updateNodeInTree', () => {
|
|
84
96
|
it('updates a node by id', () => {
|
|
85
|
-
const nodes = [rect('a')]
|
|
86
|
-
const result = updateNodeInTree(nodes, 'a', { name: 'updated' })
|
|
87
|
-
expect(result[0].name).toBe('updated')
|
|
88
|
-
})
|
|
97
|
+
const nodes = [rect('a')];
|
|
98
|
+
const result = updateNodeInTree(nodes, 'a', { name: 'updated' });
|
|
99
|
+
expect(result[0].name).toBe('updated');
|
|
100
|
+
});
|
|
89
101
|
|
|
90
102
|
it('updates a nested node', () => {
|
|
91
|
-
const nodes = [frame('parent', [rect('child')])]
|
|
92
|
-
const result = updateNodeInTree(nodes, 'child', { name: 'updated' })
|
|
93
|
-
const parent = result[0] as PenNode & { children: PenNode[] }
|
|
94
|
-
expect(parent.children[0].name).toBe('updated')
|
|
95
|
-
})
|
|
96
|
-
|
|
103
|
+
const nodes = [frame('parent', [rect('child')])];
|
|
104
|
+
const result = updateNodeInTree(nodes, 'child', { name: 'updated' });
|
|
105
|
+
const parent = result[0] as PenNode & { children: PenNode[] };
|
|
106
|
+
expect(parent.children[0].name).toBe('updated');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('reuses untouched branches when updating a nested node', () => {
|
|
110
|
+
const sibling = frame('sibling', [rect('sibling-child')]);
|
|
111
|
+
const parent = frame('parent', [rect('child')]);
|
|
112
|
+
const nodes = [parent, sibling];
|
|
113
|
+
|
|
114
|
+
const result = updateNodeInTree(nodes, 'child', { name: 'updated' });
|
|
115
|
+
|
|
116
|
+
expect(result).not.toBe(nodes);
|
|
117
|
+
expect(result[0]).not.toBe(parent);
|
|
118
|
+
expect((result[0] as PenNode & { children: PenNode[] }).children[0].name).toBe('updated');
|
|
119
|
+
expect(result[1]).toBe(sibling);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('returns the original tree for no-op updates', () => {
|
|
123
|
+
const nodes = [rect('a')];
|
|
124
|
+
const result = updateNodeInTree(nodes, 'a', { x: 0 });
|
|
125
|
+
expect(result).toBe(nodes);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
97
128
|
|
|
98
129
|
describe('flattenNodes', () => {
|
|
99
130
|
it('flattens a nested tree', () => {
|
|
100
|
-
const nodes = [frame('a', [rect('b'), frame('c', [rect('d')])])]
|
|
101
|
-
const flat = flattenNodes(nodes)
|
|
102
|
-
expect(flat.map(n => n.id)).toEqual(['a', 'b', 'c', 'd'])
|
|
103
|
-
})
|
|
104
|
-
})
|
|
131
|
+
const nodes = [frame('a', [rect('b'), frame('c', [rect('d')])])];
|
|
132
|
+
const flat = flattenNodes(nodes);
|
|
133
|
+
expect(flat.map((n) => n.id)).toEqual(['a', 'b', 'c', 'd']);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
105
136
|
|
|
106
137
|
describe('insertNodeInTree', () => {
|
|
107
138
|
it('inserts at root level', () => {
|
|
108
|
-
const result = insertNodeInTree([rect('a')], null, rect('b'))
|
|
109
|
-
expect(result).toHaveLength(2)
|
|
110
|
-
expect(result[1].id).toBe('b')
|
|
111
|
-
})
|
|
139
|
+
const result = insertNodeInTree([rect('a')], null, rect('b'));
|
|
140
|
+
expect(result).toHaveLength(2);
|
|
141
|
+
expect(result[1].id).toBe('b');
|
|
142
|
+
});
|
|
112
143
|
|
|
113
144
|
it('inserts into a parent', () => {
|
|
114
|
-
const nodes = [frame('parent', [rect('existing')])]
|
|
115
|
-
const result = insertNodeInTree(nodes, 'parent', rect('new'))
|
|
116
|
-
const parent = result[0] as PenNode & { children: PenNode[] }
|
|
117
|
-
expect(parent.children).toHaveLength(2)
|
|
118
|
-
expect(parent.children[1].id).toBe('new')
|
|
119
|
-
})
|
|
145
|
+
const nodes = [frame('parent', [rect('existing')])];
|
|
146
|
+
const result = insertNodeInTree(nodes, 'parent', rect('new'));
|
|
147
|
+
const parent = result[0] as PenNode & { children: PenNode[] };
|
|
148
|
+
expect(parent.children).toHaveLength(2);
|
|
149
|
+
expect(parent.children[1].id).toBe('new');
|
|
150
|
+
});
|
|
120
151
|
|
|
121
152
|
it('inserts at a specific index', () => {
|
|
122
|
-
const nodes = [frame('parent', [rect('a'), rect('c')])]
|
|
123
|
-
const result = insertNodeInTree(nodes, 'parent', rect('b'), 1)
|
|
124
|
-
const parent = result[0] as PenNode & { children: PenNode[] }
|
|
125
|
-
expect(parent.children.map(n => n.id)).toEqual(['a', 'b', 'c'])
|
|
126
|
-
})
|
|
127
|
-
})
|
|
153
|
+
const nodes = [frame('parent', [rect('a'), rect('c')])];
|
|
154
|
+
const result = insertNodeInTree(nodes, 'parent', rect('b'), 1);
|
|
155
|
+
const parent = result[0] as PenNode & { children: PenNode[] };
|
|
156
|
+
expect(parent.children.map((n) => n.id)).toEqual(['a', 'b', 'c']);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
128
159
|
|
|
129
160
|
describe('isDescendantOf', () => {
|
|
130
161
|
it('returns true for a descendant', () => {
|
|
131
|
-
const nodes = [frame('a', [frame('b', [rect('c')])])]
|
|
132
|
-
expect(isDescendantOf(nodes, 'c', 'a')).toBe(true)
|
|
133
|
-
})
|
|
162
|
+
const nodes = [frame('a', [frame('b', [rect('c')])])];
|
|
163
|
+
expect(isDescendantOf(nodes, 'c', 'a')).toBe(true);
|
|
164
|
+
});
|
|
134
165
|
|
|
135
166
|
it('returns false for non-descendant', () => {
|
|
136
|
-
const nodes = [frame('a', [rect('b')]), rect('c')]
|
|
137
|
-
expect(isDescendantOf(nodes, 'c', 'a')).toBe(false)
|
|
138
|
-
})
|
|
139
|
-
})
|
|
167
|
+
const nodes = [frame('a', [rect('b')]), rect('c')];
|
|
168
|
+
expect(isDescendantOf(nodes, 'c', 'a')).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
140
171
|
|
|
141
172
|
describe('page helpers', () => {
|
|
142
173
|
it('getActivePageChildren returns page children', () => {
|
|
143
|
-
const doc = createEmptyDocument()
|
|
144
|
-
const children = getActivePageChildren(doc, DEFAULT_PAGE_ID)
|
|
145
|
-
expect(children).toHaveLength(1)
|
|
146
|
-
expect(children[0].id).toBe(DEFAULT_FRAME_ID)
|
|
147
|
-
})
|
|
174
|
+
const doc = createEmptyDocument();
|
|
175
|
+
const children = getActivePageChildren(doc, DEFAULT_PAGE_ID);
|
|
176
|
+
expect(children).toHaveLength(1);
|
|
177
|
+
expect(children[0].id).toBe(DEFAULT_FRAME_ID);
|
|
178
|
+
});
|
|
148
179
|
|
|
149
180
|
it('setActivePageChildren replaces page children', () => {
|
|
150
|
-
const doc = createEmptyDocument()
|
|
151
|
-
const newChildren = [rect('new')]
|
|
152
|
-
const updated = setActivePageChildren(doc, DEFAULT_PAGE_ID, newChildren)
|
|
153
|
-
expect(updated.pages![0].children).toHaveLength(1)
|
|
154
|
-
expect(updated.pages![0].children[0].id).toBe('new')
|
|
155
|
-
})
|
|
181
|
+
const doc = createEmptyDocument();
|
|
182
|
+
const newChildren = [rect('new')];
|
|
183
|
+
const updated = setActivePageChildren(doc, DEFAULT_PAGE_ID, newChildren);
|
|
184
|
+
expect(updated.pages![0].children).toHaveLength(1);
|
|
185
|
+
expect(updated.pages![0].children[0].id).toBe('new');
|
|
186
|
+
});
|
|
156
187
|
|
|
157
188
|
it('migrateToPages wraps legacy doc', () => {
|
|
158
|
-
const legacy: PenDocument = { version: '1.0.0', children: [rect('a')] }
|
|
159
|
-
const migrated = migrateToPages(legacy)
|
|
160
|
-
expect(migrated.pages).toHaveLength(1)
|
|
161
|
-
expect(migrated.pages![0].children[0].id).toBe('a')
|
|
162
|
-
expect(migrated.children).toEqual([])
|
|
163
|
-
})
|
|
189
|
+
const legacy: PenDocument = { version: '1.0.0', children: [rect('a')] };
|
|
190
|
+
const migrated = migrateToPages(legacy);
|
|
191
|
+
expect(migrated.pages).toHaveLength(1);
|
|
192
|
+
expect(migrated.pages![0].children[0].id).toBe('a');
|
|
193
|
+
expect(migrated.children).toEqual([]);
|
|
194
|
+
});
|
|
164
195
|
|
|
165
196
|
it('migrateToPages preserves existing pages', () => {
|
|
166
|
-
const doc = createEmptyDocument()
|
|
167
|
-
expect(migrateToPages(doc)).toBe(doc)
|
|
168
|
-
})
|
|
169
|
-
})
|
|
170
|
-
})
|
|
197
|
+
const doc = createEmptyDocument();
|
|
198
|
+
expect(migrateToPages(doc)).toBe(doc);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|