@flighthq/node 0.1.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/dist/boundsRectangle.d.ts +26 -0
- package/dist/boundsRectangle.d.ts.map +1 -0
- package/dist/boundsRectangle.js +160 -0
- package/dist/boundsRectangle.js.map +1 -0
- package/dist/hasAppearance.d.ts +3 -0
- package/dist/hasAppearance.d.ts.map +1 -0
- package/dist/hasAppearance.js +6 -0
- package/dist/hasAppearance.js.map +1 -0
- package/dist/hasBoundsRectangle.d.ts +5 -0
- package/dist/hasBoundsRectangle.d.ts.map +1 -0
- package/dist/hasBoundsRectangle.js +9 -0
- package/dist/hasBoundsRectangle.js.map +1 -0
- package/dist/hasClip.d.ts +3 -0
- package/dist/hasClip.d.ts.map +1 -0
- package/dist/hasClip.js +4 -0
- package/dist/hasClip.js.map +1 -0
- package/dist/hasMaterial.d.ts +3 -0
- package/dist/hasMaterial.d.ts.map +1 -0
- package/dist/hasMaterial.js +5 -0
- package/dist/hasMaterial.js.map +1 -0
- package/dist/hasTransform2d.d.ts +4 -0
- package/dist/hasTransform2d.d.ts.map +1 -0
- package/dist/hasTransform2d.js +19 -0
- package/dist/hasTransform2d.js.map +1 -0
- package/dist/hasTransform3d.d.ts +5 -0
- package/dist/hasTransform3d.d.ts.map +1 -0
- package/dist/hasTransform3d.js +8 -0
- package/dist/hasTransform3d.js.map +1 -0
- package/dist/hierarchy.d.ts +127 -0
- package/dist/hierarchy.d.ts.map +1 -0
- package/dist/hierarchy.js +396 -0
- package/dist/hierarchy.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.ts +21 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +109 -0
- package/dist/node.js.map +1 -0
- package/dist/revision.d.ts +40 -0
- package/dist/revision.d.ts.map +1 -0
- package/dist/revision.js +85 -0
- package/dist/revision.js.map +1 -0
- package/dist/transform2d.d.ts +16 -0
- package/dist/transform2d.d.ts.map +1 -0
- package/dist/transform2d.js +99 -0
- package/dist/transform2d.js.map +1 -0
- package/dist/transform3d.d.ts +6 -0
- package/dist/transform3d.d.ts.map +1 -0
- package/dist/transform3d.js +45 -0
- package/dist/transform3d.js.map +1 -0
- package/dist/traversal.d.ts +51 -0
- package/dist/traversal.d.ts.map +1 -0
- package/dist/traversal.js +130 -0
- package/dist/traversal.js.map +1 -0
- package/dist/viewport.d.ts +8 -0
- package/dist/viewport.d.ts.map +1 -0
- package/dist/viewport.js +78 -0
- package/dist/viewport.js.map +1 -0
- package/package.json +40 -0
- package/src/boundsRectangle.test.ts +497 -0
- package/src/hasAppearance.test.ts +47 -0
- package/src/hasBoundsRectangle.test.ts +57 -0
- package/src/hasClip.test.ts +27 -0
- package/src/hasMaterial.test.ts +30 -0
- package/src/hasTransform2d.test.ts +67 -0
- package/src/hasTransform3d.test.ts +37 -0
- package/src/hierarchy.test.ts +973 -0
- package/src/node.test.ts +265 -0
- package/src/revision.test.ts +211 -0
- package/src/transform2d.test.ts +391 -0
- package/src/transform3d.test.ts +202 -0
- package/src/traversal.test.ts +233 -0
- package/src/viewport.test.ts +228 -0
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
import { getEntityRuntime as getRawEntityRuntime } from '@flighthq/entity';
|
|
2
|
+
import { cloneMatrix } from '@flighthq/geometry';
|
|
3
|
+
import { connectSignal } from '@flighthq/signals';
|
|
4
|
+
import type { HasTransform2D, HasTransform2DRuntime, Node, NodeRuntime, NodeTraits } from '@flighthq/types';
|
|
5
|
+
import { NodeKind } from '@flighthq/types';
|
|
6
|
+
|
|
7
|
+
import { initTransform2DRuntimeTrait, initTransform2DTrait } from './hasTransform2d';
|
|
8
|
+
import {
|
|
9
|
+
addNodeChild,
|
|
10
|
+
addNodeChildAt,
|
|
11
|
+
addNodeChildren,
|
|
12
|
+
containsNodeChild,
|
|
13
|
+
forEachNodeChild,
|
|
14
|
+
getNodeAncestors,
|
|
15
|
+
getNodeChildAt,
|
|
16
|
+
getNodeChildByName,
|
|
17
|
+
getNodeChildCount,
|
|
18
|
+
getNodeChildIndex,
|
|
19
|
+
getNodeCommonAncestor,
|
|
20
|
+
getNodeParent,
|
|
21
|
+
getNodeRoot,
|
|
22
|
+
isNodeAncestorOf,
|
|
23
|
+
removeNodeChild,
|
|
24
|
+
removeNodeChildAt,
|
|
25
|
+
removeNodeChildren,
|
|
26
|
+
reparentNode,
|
|
27
|
+
replaceNodeChild,
|
|
28
|
+
setNodeChildIndex,
|
|
29
|
+
swapNodeChildren,
|
|
30
|
+
swapNodeChildrenAt,
|
|
31
|
+
} from './hierarchy';
|
|
32
|
+
import { createNode, enableNodeSignals, getNodeRuntime } from './node';
|
|
33
|
+
import { invalidateNodeLocalTransform } from './revision';
|
|
34
|
+
import { getNodeWorldTransformMatrix } from './transform2d';
|
|
35
|
+
|
|
36
|
+
let container: Node<NodeTraits>;
|
|
37
|
+
let childA: Node<NodeTraits>;
|
|
38
|
+
let childB: Node<NodeTraits>;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
container = createNode(NodeKind);
|
|
42
|
+
childA = createNode(NodeKind);
|
|
43
|
+
childB = createNode(NodeKind);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function getChildren(source: Node) {
|
|
47
|
+
return getNodeRuntime(source).children as Node[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getEntityRuntime(source: Node) {
|
|
51
|
+
return getNodeRuntime(source) as NodeRuntime;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('addNodeChild', () => {
|
|
55
|
+
it('addNodeChild adds a child to the end of the list', () => {
|
|
56
|
+
addNodeChild(container, childA);
|
|
57
|
+
|
|
58
|
+
expect(getNodeChildCount(container)).toBe(1);
|
|
59
|
+
expect(getNodeParent(childA)).toBe(container);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('throws if child is null', () => {
|
|
63
|
+
expect(() => addNodeChild(container, null as any)).toThrow(TypeError);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('throws if child is the same as target', () => {
|
|
67
|
+
expect(() => addNodeChild(container, container)).toThrow(TypeError);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('removes child from previous parent before adding', () => {
|
|
71
|
+
const other = createNode(NodeKind);
|
|
72
|
+
|
|
73
|
+
addNodeChild(other, childA);
|
|
74
|
+
expect(getNodeParent(childA)).toBe(other);
|
|
75
|
+
|
|
76
|
+
addNodeChild(container, childA);
|
|
77
|
+
|
|
78
|
+
expect(getNodeParent(childA)).toBe(container);
|
|
79
|
+
expect(getNodeChildCount(other)).toBe(0);
|
|
80
|
+
expect(getNodeChildCount(container)).toBe(1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('a child never has more than one parent', () => {
|
|
84
|
+
const other = createNode(NodeKind);
|
|
85
|
+
|
|
86
|
+
addNodeChild(container, childA);
|
|
87
|
+
addNodeChild(other, childA);
|
|
88
|
+
|
|
89
|
+
expect(getNodeParent(childA)).toBe(other);
|
|
90
|
+
expect(getNodeChildCount(container)).toBe(0);
|
|
91
|
+
expect(getNodeChildCount(other)).toBe(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('calls onParentChanged on the child', () => {
|
|
95
|
+
let called = false;
|
|
96
|
+
connectSignal(enableNodeSignals(childA).onParentChanged, () => {
|
|
97
|
+
called = true;
|
|
98
|
+
});
|
|
99
|
+
addNodeChild(container, childA);
|
|
100
|
+
expect(called).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('calls onChildrenChanged on the parent', () => {
|
|
104
|
+
let called = false;
|
|
105
|
+
connectSignal(enableNodeSignals(container).onChildrenChanged, () => {
|
|
106
|
+
called = true;
|
|
107
|
+
});
|
|
108
|
+
addNodeChild(container, childA);
|
|
109
|
+
expect(called).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('calls onChildAdded on the parent with the child', () => {
|
|
113
|
+
let added: unknown;
|
|
114
|
+
connectSignal(enableNodeSignals(container).onChildAdded, (child) => {
|
|
115
|
+
added = child;
|
|
116
|
+
});
|
|
117
|
+
addNodeChild(container, childA);
|
|
118
|
+
expect(added).toBe(childA);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('does not call onChildAdded when reordering within the same parent', () => {
|
|
122
|
+
addNodeChild(container, childA);
|
|
123
|
+
addNodeChild(container, childB);
|
|
124
|
+
let called = false;
|
|
125
|
+
connectSignal(enableNodeSignals(container).onChildAdded, () => {
|
|
126
|
+
called = true;
|
|
127
|
+
});
|
|
128
|
+
addNodeChildAt(container, childA, 1);
|
|
129
|
+
expect(called).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Compile-time law: Node<Traits> is invariant in Traits, so nodes from different trait
|
|
133
|
+
// families cannot be mixed in one graph. The @ts-expect-error is the assertion — if the
|
|
134
|
+
// wall ever collapses (e.g. Traits leaks to `any`), this line stops erroring and the test
|
|
135
|
+
// fails to compile under `npm run typecheck`.
|
|
136
|
+
it('rejects mixing nodes from different trait families', () => {
|
|
137
|
+
interface FooTraits {
|
|
138
|
+
foo: number;
|
|
139
|
+
}
|
|
140
|
+
interface BarTraits {
|
|
141
|
+
bar: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const foo = createNode<FooTraits>('FooNode');
|
|
145
|
+
const foo2 = createNode<FooTraits>('FooNode');
|
|
146
|
+
const added = addNodeChild(foo, foo2);
|
|
147
|
+
expect(added).toBe(foo2);
|
|
148
|
+
|
|
149
|
+
const bar = createNode<BarTraits>('BarNode');
|
|
150
|
+
// @ts-expect-error a node from a different trait family does not unify with foo's family
|
|
151
|
+
addNodeChild(foo, bar);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('addNodeChildAt', () => {
|
|
156
|
+
it('addNodeChildAt inserts a child at the given index', () => {
|
|
157
|
+
addNodeChild(container, childA);
|
|
158
|
+
addNodeChildAt(container, childB, 0);
|
|
159
|
+
|
|
160
|
+
expect(getNodeChildCount(container)).toBe(2);
|
|
161
|
+
expect(getChildren(container)[0]).toBe(childB);
|
|
162
|
+
expect(getChildren(container)[1]).toBe(childA);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('addNodeChildAt allows inserting at the end (index === length)', () => {
|
|
166
|
+
addNodeChild(container, childA);
|
|
167
|
+
addNodeChildAt(container, childB, 1);
|
|
168
|
+
|
|
169
|
+
expect(getNodeChildCount(container)).toBe(2);
|
|
170
|
+
expect(getChildren(container)[1]).toBe(childB);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('addNodeChildAt throws if index is negative', () => {
|
|
174
|
+
expect(() => addNodeChildAt(container, childA, -1)).toThrow();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('throws if index is out of bounds', () => {
|
|
178
|
+
expect(() => addNodeChildAt(container, childA, 1)).toThrow();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('reorders child when added again to the same parent', () => {
|
|
182
|
+
addNodeChild(container, childA);
|
|
183
|
+
addNodeChild(container, childB);
|
|
184
|
+
|
|
185
|
+
// move childA to the front
|
|
186
|
+
addNodeChildAt(container, childA, 1);
|
|
187
|
+
|
|
188
|
+
expect(getChildren(container)[0]).toBe(childB);
|
|
189
|
+
expect(getChildren(container)[1]).toBe(childA);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('calls onParentChanged on the child', () => {
|
|
193
|
+
let called = false;
|
|
194
|
+
connectSignal(enableNodeSignals(childA).onParentChanged, () => {
|
|
195
|
+
called = true;
|
|
196
|
+
});
|
|
197
|
+
addNodeChildAt(container, childA, 0);
|
|
198
|
+
expect(called).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('calls onChildrenChanged on the parent', () => {
|
|
202
|
+
let called = false;
|
|
203
|
+
connectSignal(enableNodeSignals(container).onChildrenChanged, () => {
|
|
204
|
+
called = true;
|
|
205
|
+
});
|
|
206
|
+
addNodeChildAt(container, childA, 0);
|
|
207
|
+
expect(called).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('calls onChildAdded on the parent with the child', () => {
|
|
211
|
+
let added: unknown;
|
|
212
|
+
connectSignal(enableNodeSignals(container).onChildAdded, (child) => {
|
|
213
|
+
added = child;
|
|
214
|
+
});
|
|
215
|
+
addNodeChildAt(container, childA, 0);
|
|
216
|
+
expect(added).toBe(childA);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('addNodeChildren', () => {
|
|
221
|
+
it('adds multiple children in order', () => {
|
|
222
|
+
addNodeChildren(container, childA, childB);
|
|
223
|
+
expect(getNodeChildCount(container)).toBe(2);
|
|
224
|
+
expect(getNodeChildAt(container, 0)).toBe(childA);
|
|
225
|
+
expect(getNodeChildAt(container, 1)).toBe(childB);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('is a no-op with no children', () => {
|
|
229
|
+
addNodeChildren(container);
|
|
230
|
+
expect(getNodeChildCount(container)).toBe(0);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('sets the parent on each child', () => {
|
|
234
|
+
addNodeChildren(container, childA, childB);
|
|
235
|
+
expect(getNodeParent(childA)).toBe(container);
|
|
236
|
+
expect(getNodeParent(childB)).toBe(container);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('containsNodeChild', () => {
|
|
241
|
+
it('returns false if parent does not contain child', () => {
|
|
242
|
+
expect(containsNodeChild(container, childA)).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('returns true if parent does not contain child', () => {
|
|
246
|
+
addNodeChild(container, childA);
|
|
247
|
+
expect(containsNodeChild(container, childA)).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('returns true if the child is located deeper in the heirarchy', () => {
|
|
251
|
+
addNodeChild(container, childA);
|
|
252
|
+
addNodeChild(childA, childB);
|
|
253
|
+
expect(containsNodeChild(container, childB)).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('forEachNodeChild', () => {
|
|
258
|
+
it('does not call callback when there are no children', () => {
|
|
259
|
+
let called = false;
|
|
260
|
+
forEachNodeChild(container, () => {
|
|
261
|
+
called = true;
|
|
262
|
+
});
|
|
263
|
+
expect(called).toBe(false);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('visits each child with its index', () => {
|
|
267
|
+
addNodeChild(container, childA);
|
|
268
|
+
addNodeChild(container, childB);
|
|
269
|
+
const visited: Array<[Node<NodeTraits>, number]> = [];
|
|
270
|
+
forEachNodeChild(container, (child, index) => {
|
|
271
|
+
visited.push([child, index]);
|
|
272
|
+
});
|
|
273
|
+
expect(visited).toEqual([
|
|
274
|
+
[childA, 0],
|
|
275
|
+
[childB, 1],
|
|
276
|
+
]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('stops early when callback returns false', () => {
|
|
280
|
+
addNodeChild(container, childA);
|
|
281
|
+
addNodeChild(container, childB);
|
|
282
|
+
const visited: Node<NodeTraits>[] = [];
|
|
283
|
+
forEachNodeChild(container, (child) => {
|
|
284
|
+
visited.push(child);
|
|
285
|
+
return false;
|
|
286
|
+
});
|
|
287
|
+
expect(visited).toHaveLength(1);
|
|
288
|
+
expect(visited[0]).toBe(childA);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('getNodeAncestors', () => {
|
|
293
|
+
it('returns an empty array for a root node', () => {
|
|
294
|
+
expect(getNodeAncestors(container)).toEqual([]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('returns [parent] for a direct child', () => {
|
|
298
|
+
addNodeChild(container, childA);
|
|
299
|
+
expect(getNodeAncestors(childA)).toEqual([container]);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('returns ancestors from parent toward root', () => {
|
|
303
|
+
addNodeChild(container, childA);
|
|
304
|
+
addNodeChild(childA, childB);
|
|
305
|
+
expect(getNodeAncestors(childB)).toEqual([childA, container]);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('getNodeChildAt', () => {
|
|
310
|
+
it('returns null if there are no children', () => {
|
|
311
|
+
expect(getNodeChildAt(container, 0)).toBeNull();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('returns null if there are no children at the given index', () => {
|
|
315
|
+
addNodeChild(container, childA);
|
|
316
|
+
expect(getNodeChildAt(container, 1)).toBeNull();
|
|
317
|
+
expect(getNodeChildAt(container, -1)).toBeNull();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('returns a matching child at the given index', () => {
|
|
321
|
+
addNodeChild(container, childA);
|
|
322
|
+
expect(getNodeChildAt(container, 0)).toStrictEqual(childA);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('getNodeChildByName', () => {
|
|
327
|
+
it('returns null if there are no children', () => {
|
|
328
|
+
expect(getNodeChildByName(container, 'hello')).toBeNull();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('returns null if there are no children with the given name', () => {
|
|
332
|
+
addNodeChild(container, childA);
|
|
333
|
+
childA.name = 'childA';
|
|
334
|
+
expect(getNodeChildByName(container, 'hello')).toBeNull();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('returns the first child with the given name', () => {
|
|
338
|
+
addNodeChild(container, childA);
|
|
339
|
+
addNodeChild(container, childB);
|
|
340
|
+
childA.name = 'hello';
|
|
341
|
+
childB.name = 'hello';
|
|
342
|
+
expect(getNodeChildByName(container, 'hello')).toStrictEqual(childA);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('does not iterate through descendents', () => {
|
|
346
|
+
addNodeChild(container, childA);
|
|
347
|
+
addNodeChild(childA, childB);
|
|
348
|
+
childB.name = 'hello';
|
|
349
|
+
expect(getNodeChildByName(container, 'hello')).toBeNull();
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe('getNodeChildCount', () => {
|
|
354
|
+
it('returns 0 if children is null', () => {
|
|
355
|
+
const children = getEntityRuntime(container).children;
|
|
356
|
+
expect(children).toBeNull();
|
|
357
|
+
expect(getNodeChildCount(container)).toBe(0);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('returns length of runtime children array', () => {
|
|
361
|
+
addNodeChild(container, childA);
|
|
362
|
+
addNodeChild(container, childB);
|
|
363
|
+
const children = getEntityRuntime(container).children;
|
|
364
|
+
expect(getNodeChildCount(container)).toStrictEqual(children!.length);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
describe('getNodeChildIndex', () => {
|
|
369
|
+
it('returns -1 if object is not a child', () => {
|
|
370
|
+
expect(getNodeChildIndex(container, childA)).toBe(-1);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('returns the index if object is a child', () => {
|
|
374
|
+
addNodeChild(container, childA);
|
|
375
|
+
addNodeChild(container, childB);
|
|
376
|
+
expect(getNodeChildIndex(container, childA)).toBe(0);
|
|
377
|
+
expect(getNodeChildIndex(container, childB)).toBe(1);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('does not iterate through descendents', () => {
|
|
381
|
+
addNodeChild(container, childA);
|
|
382
|
+
addNodeChild(childA, childB);
|
|
383
|
+
expect(getNodeChildIndex(container, childB)).toBe(-1);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe('getNodeCommonAncestor', () => {
|
|
388
|
+
it('returns null for nodes in different trees', () => {
|
|
389
|
+
const other = createNode(NodeKind);
|
|
390
|
+
expect(getNodeCommonAncestor(container, other)).toBeNull();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('returns the parent when both nodes are its children', () => {
|
|
394
|
+
addNodeChild(container, childA);
|
|
395
|
+
addNodeChild(container, childB);
|
|
396
|
+
expect(getNodeCommonAncestor(childA, childB)).toBe(container);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('returns the ancestor when one node is the ancestor of the other', () => {
|
|
400
|
+
addNodeChild(container, childA);
|
|
401
|
+
addNodeChild(childA, childB);
|
|
402
|
+
expect(getNodeCommonAncestor(container, childB)).toBe(container);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('returns the node itself when both nodes are the same', () => {
|
|
406
|
+
addNodeChild(container, childA);
|
|
407
|
+
expect(getNodeCommonAncestor(childA, childA)).toBe(childA);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe('getNodeParent', () => {
|
|
412
|
+
it('returns runtime parent reference', () => {
|
|
413
|
+
addNodeChild(container, childA);
|
|
414
|
+
const parent = getEntityRuntime(childA).parent;
|
|
415
|
+
expect(getNodeParent(childA)).toStrictEqual(parent);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe('getNodeRoot', () => {
|
|
420
|
+
it('returns the node itself when it has no parent', () => {
|
|
421
|
+
expect(getNodeRoot(childA)).toBe(childA);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('returns the topmost ancestor', () => {
|
|
425
|
+
addNodeChild(container, childA);
|
|
426
|
+
addNodeChild(childA, childB);
|
|
427
|
+
expect(getNodeRoot(childB)).toBe(container);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('returns the direct parent when depth is one', () => {
|
|
431
|
+
addNodeChild(container, childA);
|
|
432
|
+
expect(getNodeRoot(childA)).toBe(container);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe('isNodeAncestorOf', () => {
|
|
437
|
+
it('returns true when a node is an ancestor', () => {
|
|
438
|
+
addNodeChild(container, childA);
|
|
439
|
+
addNodeChild(childA, childB);
|
|
440
|
+
expect(isNodeAncestorOf(container, childB)).toBe(true);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('returns true when checking a node against itself', () => {
|
|
444
|
+
expect(isNodeAncestorOf(container, container)).toBe(true);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('returns false when there is no ancestor relationship', () => {
|
|
448
|
+
addNodeChild(container, childA);
|
|
449
|
+
expect(isNodeAncestorOf(childA, container)).toBe(false);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('returns false for unrelated nodes', () => {
|
|
453
|
+
const other = createNode(NodeKind);
|
|
454
|
+
expect(isNodeAncestorOf(container, other)).toBe(false);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
describe('removeNodeChild', () => {
|
|
459
|
+
it('removes the child and clears its parent', () => {
|
|
460
|
+
addNodeChild(container, childA);
|
|
461
|
+
expect(getNodeChildCount(container)).toBe(1);
|
|
462
|
+
|
|
463
|
+
removeNodeChild(container, childA);
|
|
464
|
+
|
|
465
|
+
expect(getNodeChildCount(container)).toBe(0);
|
|
466
|
+
expect(getNodeParent(childA)).toBeNull();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('does nothing if child is not a child of target', () => {
|
|
470
|
+
addNodeChild(container, childA);
|
|
471
|
+
|
|
472
|
+
const other = createNode(NodeKind);
|
|
473
|
+
removeNodeChild(other, childA);
|
|
474
|
+
|
|
475
|
+
expect(getNodeChildCount(container)).toBe(1);
|
|
476
|
+
expect(getNodeParent(childA)).toBe(container);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('is safe when child is null', () => {
|
|
480
|
+
expect(() => removeNodeChild(container, null as any)).not.toThrow();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('always clears the parent reference', () => {
|
|
484
|
+
addNodeChild(container, childA);
|
|
485
|
+
removeNodeChild(container, childA);
|
|
486
|
+
|
|
487
|
+
expect(getNodeParent(childA)).toBeNull();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('calls onParentChanged on the child', () => {
|
|
491
|
+
addNodeChild(container, childA);
|
|
492
|
+
let called = false;
|
|
493
|
+
connectSignal(enableNodeSignals(childA).onParentChanged, () => {
|
|
494
|
+
called = true;
|
|
495
|
+
});
|
|
496
|
+
removeNodeChild(container, childA);
|
|
497
|
+
expect(called).toBe(true);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('calls onChildrenChanged on the parent', () => {
|
|
501
|
+
addNodeChild(container, childA);
|
|
502
|
+
let called = false;
|
|
503
|
+
connectSignal(enableNodeSignals(container).onChildrenChanged, () => {
|
|
504
|
+
called = true;
|
|
505
|
+
});
|
|
506
|
+
removeNodeChild(container, childA);
|
|
507
|
+
expect(called).toBe(true);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('calls onChildRemoved on the parent with the child', () => {
|
|
511
|
+
addNodeChild(container, childA);
|
|
512
|
+
let removed: unknown;
|
|
513
|
+
connectSignal(enableNodeSignals(container).onChildRemoved, (child) => {
|
|
514
|
+
removed = child;
|
|
515
|
+
});
|
|
516
|
+
removeNodeChild(container, childA);
|
|
517
|
+
expect(removed).toBe(childA);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
describe('removeNodeChildAt', () => {
|
|
522
|
+
it('removeNodeChildAt removes and returns the child at index', () => {
|
|
523
|
+
addNodeChild(container, childA);
|
|
524
|
+
addNodeChild(container, childB);
|
|
525
|
+
|
|
526
|
+
const removed = removeNodeChildAt(container, 0);
|
|
527
|
+
|
|
528
|
+
expect(removed).toBe(childA);
|
|
529
|
+
expect(getNodeChildCount(container)).toBe(1);
|
|
530
|
+
expect(getNodeParent(childA)).toBeNull();
|
|
531
|
+
expect(getChildren(container)[0]).toBe(childB);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('removeNodeChildAt returns null for out-of-range index', () => {
|
|
535
|
+
expect(removeNodeChildAt(container, 0)).toBeNull();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('calls onParentChanged on the child', () => {
|
|
539
|
+
addNodeChild(container, childA);
|
|
540
|
+
addNodeChild(container, childB);
|
|
541
|
+
|
|
542
|
+
let called = false;
|
|
543
|
+
connectSignal(enableNodeSignals(childA).onParentChanged, () => {
|
|
544
|
+
called = true;
|
|
545
|
+
});
|
|
546
|
+
removeNodeChildAt(container, 0);
|
|
547
|
+
expect(called).toBe(true);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('calls onChildrenChanged on the parent', () => {
|
|
551
|
+
addNodeChild(container, childA);
|
|
552
|
+
addNodeChild(container, childB);
|
|
553
|
+
|
|
554
|
+
let called = false;
|
|
555
|
+
connectSignal(enableNodeSignals(container).onChildrenChanged, () => {
|
|
556
|
+
called = true;
|
|
557
|
+
});
|
|
558
|
+
removeNodeChildAt(container, 0);
|
|
559
|
+
expect(called).toBe(true);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
describe('removeNodeChildren', () => {
|
|
564
|
+
it('removeChildren removes all children by default', () => {
|
|
565
|
+
addNodeChild(container, childA);
|
|
566
|
+
addNodeChild(container, childB);
|
|
567
|
+
|
|
568
|
+
removeNodeChildren(container);
|
|
569
|
+
|
|
570
|
+
expect(getNodeChildCount(container)).toBe(0);
|
|
571
|
+
expect(getNodeParent(childA)).toBeNull();
|
|
572
|
+
expect(getNodeParent(childB)).toBeNull();
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('removeChildren removes a range of children', () => {
|
|
576
|
+
const childC = createNode(NodeKind);
|
|
577
|
+
|
|
578
|
+
addNodeChild(container, childA);
|
|
579
|
+
addNodeChild(container, childB);
|
|
580
|
+
addNodeChild(container, childC);
|
|
581
|
+
|
|
582
|
+
removeNodeChildren(container, 1, 2);
|
|
583
|
+
|
|
584
|
+
expect(getNodeChildCount(container)).toBe(1);
|
|
585
|
+
expect(getChildren(container)[0]).toBe(childA);
|
|
586
|
+
expect(getNodeParent(childB)).toBeNull();
|
|
587
|
+
expect(getNodeParent(childC)).toBeNull();
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it('removeChildren does nothing if beginIndex is out of range', () => {
|
|
591
|
+
addNodeChild(container, childA);
|
|
592
|
+
|
|
593
|
+
removeNodeChildren(container, 5);
|
|
594
|
+
|
|
595
|
+
expect(getNodeChildCount(container)).toBe(1);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('removeChildren throws if indices are invalid', () => {
|
|
599
|
+
addNodeChild(container, childA);
|
|
600
|
+
|
|
601
|
+
expect(() => removeNodeChildren(container, 0, 10)).toThrow(RangeError);
|
|
602
|
+
expect(() => removeNodeChildren(container, -1, 0)).toThrow(RangeError);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('calls onParentChanged on the child', () => {
|
|
606
|
+
addNodeChild(container, childA);
|
|
607
|
+
|
|
608
|
+
let called = false;
|
|
609
|
+
connectSignal(enableNodeSignals(childA).onParentChanged, () => {
|
|
610
|
+
called = true;
|
|
611
|
+
});
|
|
612
|
+
removeNodeChildren(container);
|
|
613
|
+
expect(called).toBe(true);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('calls onChildrenChanged on the parent', () => {
|
|
617
|
+
addNodeChild(container, childA);
|
|
618
|
+
|
|
619
|
+
let called = false;
|
|
620
|
+
connectSignal(enableNodeSignals(container).onChildrenChanged, () => {
|
|
621
|
+
called = true;
|
|
622
|
+
});
|
|
623
|
+
removeNodeChildren(container);
|
|
624
|
+
expect(called).toBe(true);
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
describe('reparentNode', () => {
|
|
629
|
+
type TransformNode = Node<HasTransform2D> & HasTransform2D;
|
|
630
|
+
const TransformKind = 'TransformTest';
|
|
631
|
+
|
|
632
|
+
function createTransformNode(): TransformNode {
|
|
633
|
+
const node = createNode(TransformKind) as TransformNode;
|
|
634
|
+
const runtime = getRawEntityRuntime(node);
|
|
635
|
+
initTransform2DTrait(node);
|
|
636
|
+
initTransform2DRuntimeTrait(runtime as HasTransform2DRuntime);
|
|
637
|
+
return node;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
it('preserves world position after reparenting', () => {
|
|
641
|
+
const parentA = createTransformNode();
|
|
642
|
+
parentA.x = 100;
|
|
643
|
+
parentA.y = 50;
|
|
644
|
+
parentA.rotation = 30;
|
|
645
|
+
invalidateNodeLocalTransform(parentA);
|
|
646
|
+
|
|
647
|
+
const child = createTransformNode();
|
|
648
|
+
child.x = 20;
|
|
649
|
+
child.y = 10;
|
|
650
|
+
invalidateNodeLocalTransform(child);
|
|
651
|
+
addNodeChild(parentA, child);
|
|
652
|
+
|
|
653
|
+
const before = cloneMatrix(getNodeWorldTransformMatrix(child));
|
|
654
|
+
|
|
655
|
+
const parentB = createTransformNode();
|
|
656
|
+
parentB.x = 200;
|
|
657
|
+
parentB.y = 100;
|
|
658
|
+
invalidateNodeLocalTransform(parentB);
|
|
659
|
+
|
|
660
|
+
reparentNode(child, parentB);
|
|
661
|
+
const after = getNodeWorldTransformMatrix(child);
|
|
662
|
+
|
|
663
|
+
expect(after.a).toBeCloseTo(before.a, 10);
|
|
664
|
+
expect(after.b).toBeCloseTo(before.b, 10);
|
|
665
|
+
expect(after.c).toBeCloseTo(before.c, 10);
|
|
666
|
+
expect(after.d).toBeCloseTo(before.d, 10);
|
|
667
|
+
expect(after.tx).toBeCloseTo(before.tx, 10);
|
|
668
|
+
expect(after.ty).toBeCloseTo(before.ty, 10);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it('preserves world position with scaled parents', () => {
|
|
672
|
+
const parentA = createTransformNode();
|
|
673
|
+
parentA.scaleX = 2;
|
|
674
|
+
parentA.scaleY = 2;
|
|
675
|
+
invalidateNodeLocalTransform(parentA);
|
|
676
|
+
|
|
677
|
+
const child = createTransformNode();
|
|
678
|
+
child.x = 10;
|
|
679
|
+
child.y = 10;
|
|
680
|
+
invalidateNodeLocalTransform(child);
|
|
681
|
+
addNodeChild(parentA, child);
|
|
682
|
+
|
|
683
|
+
const before = cloneMatrix(getNodeWorldTransformMatrix(child));
|
|
684
|
+
|
|
685
|
+
const parentB = createTransformNode();
|
|
686
|
+
invalidateNodeLocalTransform(parentB);
|
|
687
|
+
|
|
688
|
+
reparentNode(child, parentB);
|
|
689
|
+
const after = getNodeWorldTransformMatrix(child);
|
|
690
|
+
|
|
691
|
+
expect(after.a).toBeCloseTo(before.a, 10);
|
|
692
|
+
expect(after.b).toBeCloseTo(before.b, 10);
|
|
693
|
+
expect(after.c).toBeCloseTo(before.c, 10);
|
|
694
|
+
expect(after.d).toBeCloseTo(before.d, 10);
|
|
695
|
+
expect(after.tx).toBeCloseTo(before.tx, 10);
|
|
696
|
+
expect(after.ty).toBeCloseTo(before.ty, 10);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it('preserves skewX and skewY values', () => {
|
|
700
|
+
const parentA = createTransformNode();
|
|
701
|
+
parentA.x = 50;
|
|
702
|
+
parentA.rotation = 45;
|
|
703
|
+
invalidateNodeLocalTransform(parentA);
|
|
704
|
+
|
|
705
|
+
const child = createTransformNode();
|
|
706
|
+
child.x = 10;
|
|
707
|
+
child.skewX = 15;
|
|
708
|
+
child.skewY = 20;
|
|
709
|
+
invalidateNodeLocalTransform(child);
|
|
710
|
+
addNodeChild(parentA, child);
|
|
711
|
+
|
|
712
|
+
const parentB = createTransformNode();
|
|
713
|
+
parentB.x = 100;
|
|
714
|
+
invalidateNodeLocalTransform(parentB);
|
|
715
|
+
|
|
716
|
+
reparentNode(child, parentB);
|
|
717
|
+
|
|
718
|
+
expect(child.skewX).toBe(15);
|
|
719
|
+
expect(child.skewY).toBe(20);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it('preserves world position with pivot', () => {
|
|
723
|
+
const parentA = createTransformNode();
|
|
724
|
+
parentA.x = 100;
|
|
725
|
+
parentA.rotation = 45;
|
|
726
|
+
invalidateNodeLocalTransform(parentA);
|
|
727
|
+
|
|
728
|
+
const child = createTransformNode();
|
|
729
|
+
child.x = 30;
|
|
730
|
+
child.y = 20;
|
|
731
|
+
child.pivotX = 50;
|
|
732
|
+
child.pivotY = 50;
|
|
733
|
+
invalidateNodeLocalTransform(child);
|
|
734
|
+
addNodeChild(parentA, child);
|
|
735
|
+
|
|
736
|
+
const before = cloneMatrix(getNodeWorldTransformMatrix(child));
|
|
737
|
+
|
|
738
|
+
const parentB = createTransformNode();
|
|
739
|
+
parentB.x = 200;
|
|
740
|
+
invalidateNodeLocalTransform(parentB);
|
|
741
|
+
|
|
742
|
+
reparentNode(child, parentB);
|
|
743
|
+
const after = getNodeWorldTransformMatrix(child);
|
|
744
|
+
|
|
745
|
+
expect(after.a).toBeCloseTo(before.a, 10);
|
|
746
|
+
expect(after.b).toBeCloseTo(before.b, 10);
|
|
747
|
+
expect(after.c).toBeCloseTo(before.c, 10);
|
|
748
|
+
expect(after.d).toBeCloseTo(before.d, 10);
|
|
749
|
+
expect(after.tx).toBeCloseTo(before.tx, 10);
|
|
750
|
+
expect(after.ty).toBeCloseTo(before.ty, 10);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it('preserves world position when reparenting a root node', () => {
|
|
754
|
+
const child = createTransformNode();
|
|
755
|
+
child.x = 80;
|
|
756
|
+
child.y = 40;
|
|
757
|
+
child.rotation = 60;
|
|
758
|
+
child.scaleX = 1.5;
|
|
759
|
+
invalidateNodeLocalTransform(child);
|
|
760
|
+
|
|
761
|
+
const before = cloneMatrix(getNodeWorldTransformMatrix(child));
|
|
762
|
+
|
|
763
|
+
const parentB = createTransformNode();
|
|
764
|
+
parentB.x = 50;
|
|
765
|
+
parentB.y = 25;
|
|
766
|
+
parentB.rotation = 10;
|
|
767
|
+
invalidateNodeLocalTransform(parentB);
|
|
768
|
+
|
|
769
|
+
reparentNode(child, parentB);
|
|
770
|
+
const after = getNodeWorldTransformMatrix(child);
|
|
771
|
+
|
|
772
|
+
expect(after.a).toBeCloseTo(before.a, 10);
|
|
773
|
+
expect(after.b).toBeCloseTo(before.b, 10);
|
|
774
|
+
expect(after.c).toBeCloseTo(before.c, 10);
|
|
775
|
+
expect(after.d).toBeCloseTo(before.d, 10);
|
|
776
|
+
expect(after.tx).toBeCloseTo(before.tx, 10);
|
|
777
|
+
expect(after.ty).toBeCloseTo(before.ty, 10);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it('preserves world position with reflected scale', () => {
|
|
781
|
+
const parentA = createTransformNode();
|
|
782
|
+
parentA.scaleX = -1;
|
|
783
|
+
parentA.scaleY = 1;
|
|
784
|
+
invalidateNodeLocalTransform(parentA);
|
|
785
|
+
|
|
786
|
+
const child = createTransformNode();
|
|
787
|
+
child.x = 30;
|
|
788
|
+
child.y = 20;
|
|
789
|
+
invalidateNodeLocalTransform(child);
|
|
790
|
+
addNodeChild(parentA, child);
|
|
791
|
+
|
|
792
|
+
const before = cloneMatrix(getNodeWorldTransformMatrix(child));
|
|
793
|
+
|
|
794
|
+
const parentB = createTransformNode();
|
|
795
|
+
parentB.x = 100;
|
|
796
|
+
invalidateNodeLocalTransform(parentB);
|
|
797
|
+
|
|
798
|
+
reparentNode(child, parentB);
|
|
799
|
+
const after = getNodeWorldTransformMatrix(child);
|
|
800
|
+
|
|
801
|
+
expect(after.a).toBeCloseTo(before.a, 10);
|
|
802
|
+
expect(after.b).toBeCloseTo(before.b, 10);
|
|
803
|
+
expect(after.c).toBeCloseTo(before.c, 10);
|
|
804
|
+
expect(after.d).toBeCloseTo(before.d, 10);
|
|
805
|
+
expect(after.tx).toBeCloseTo(before.tx, 10);
|
|
806
|
+
expect(after.ty).toBeCloseTo(before.ty, 10);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it('produces matching local TRS when reparented to identity parent', () => {
|
|
810
|
+
const parentA = createTransformNode();
|
|
811
|
+
parentA.x = 100;
|
|
812
|
+
parentA.y = 50;
|
|
813
|
+
parentA.rotation = 45;
|
|
814
|
+
parentA.scaleX = 2;
|
|
815
|
+
parentA.scaleY = 3;
|
|
816
|
+
invalidateNodeLocalTransform(parentA);
|
|
817
|
+
|
|
818
|
+
const child = createTransformNode();
|
|
819
|
+
child.x = 10;
|
|
820
|
+
child.y = 5;
|
|
821
|
+
invalidateNodeLocalTransform(child);
|
|
822
|
+
addNodeChild(parentA, child);
|
|
823
|
+
|
|
824
|
+
const before = cloneMatrix(getNodeWorldTransformMatrix(child));
|
|
825
|
+
|
|
826
|
+
const identityParent = createTransformNode();
|
|
827
|
+
invalidateNodeLocalTransform(identityParent);
|
|
828
|
+
|
|
829
|
+
reparentNode(child, identityParent);
|
|
830
|
+
const after = getNodeWorldTransformMatrix(child);
|
|
831
|
+
|
|
832
|
+
expect(after.a).toBeCloseTo(before.a, 10);
|
|
833
|
+
expect(after.b).toBeCloseTo(before.b, 10);
|
|
834
|
+
expect(after.c).toBeCloseTo(before.c, 10);
|
|
835
|
+
expect(after.d).toBeCloseTo(before.d, 10);
|
|
836
|
+
expect(after.tx).toBeCloseTo(before.tx, 10);
|
|
837
|
+
expect(after.ty).toBeCloseTo(before.ty, 10);
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
describe('replaceNodeChild', () => {
|
|
842
|
+
it('replaces oldChild with newChild at the same index', () => {
|
|
843
|
+
addNodeChild(container, childA);
|
|
844
|
+
addNodeChild(container, childB);
|
|
845
|
+
const childC = createNode(NodeKind);
|
|
846
|
+
replaceNodeChild(container, childA, childC);
|
|
847
|
+
expect(getNodeChildAt(container, 0)).toBe(childC);
|
|
848
|
+
expect(getNodeChildAt(container, 1)).toBe(childB);
|
|
849
|
+
expect(getNodeParent(childA)).toBeNull();
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it('is a no-op when oldChild is not in target', () => {
|
|
853
|
+
addNodeChild(container, childA);
|
|
854
|
+
replaceNodeChild(container, childB, childA);
|
|
855
|
+
expect(getNodeChildCount(container)).toBe(1);
|
|
856
|
+
expect(getNodeChildAt(container, 0)).toBe(childA);
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
it('sets the parent of newChild to target', () => {
|
|
860
|
+
addNodeChild(container, childA);
|
|
861
|
+
replaceNodeChild(container, childA, childB);
|
|
862
|
+
expect(getNodeParent(childB)).toBe(container);
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
describe('setNodeChildIndex', () => {
|
|
867
|
+
it('setChildIndex moves an existing child to a new index', () => {
|
|
868
|
+
addNodeChild(container, childA);
|
|
869
|
+
addNodeChild(container, childB);
|
|
870
|
+
|
|
871
|
+
setNodeChildIndex(container, childA, 1);
|
|
872
|
+
|
|
873
|
+
expect(getChildren(container)[0]).toBe(childB);
|
|
874
|
+
expect(getChildren(container)[1]).toBe(childA);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it('setChildIndex does nothing if child is not in container', () => {
|
|
878
|
+
const other = createNode(NodeKind);
|
|
879
|
+
|
|
880
|
+
addNodeChild(other, childA);
|
|
881
|
+
addNodeChild(container, childB);
|
|
882
|
+
|
|
883
|
+
setNodeChildIndex(container, childA, 0);
|
|
884
|
+
|
|
885
|
+
expect(getChildren(container)[0]).toBe(childB);
|
|
886
|
+
expect(getNodeParent(childA)).toBe(other);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('setChildIndex ignores out-of-range indices', () => {
|
|
890
|
+
addNodeChild(container, childA);
|
|
891
|
+
|
|
892
|
+
setNodeChildIndex(container, childA, 5);
|
|
893
|
+
|
|
894
|
+
expect(getChildren(container)[0]).toBe(childA);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
it('calls onChildrenOrderChanged on the parent', () => {
|
|
898
|
+
addNodeChild(container, childA);
|
|
899
|
+
addNodeChild(container, childB);
|
|
900
|
+
|
|
901
|
+
let called = false;
|
|
902
|
+
connectSignal(enableNodeSignals(container).onChildrenOrderChanged, () => {
|
|
903
|
+
called = true;
|
|
904
|
+
});
|
|
905
|
+
setNodeChildIndex(container, childA, 1);
|
|
906
|
+
expect(called).toBe(true);
|
|
907
|
+
});
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
describe('swapNodeChildren', () => {
|
|
911
|
+
it('swapNodeChildren swaps two children', () => {
|
|
912
|
+
addNodeChild(container, childA);
|
|
913
|
+
addNodeChild(container, childB);
|
|
914
|
+
|
|
915
|
+
swapNodeChildren(container, childA, childB);
|
|
916
|
+
|
|
917
|
+
expect(getChildren(container)[0]).toBe(childB);
|
|
918
|
+
expect(getChildren(container)[1]).toBe(childA);
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it('swapNodeChildren does nothing if either child is not in container', () => {
|
|
922
|
+
const other = createNode(NodeKind);
|
|
923
|
+
|
|
924
|
+
addNodeChild(container, childA);
|
|
925
|
+
addNodeChild(other, childB);
|
|
926
|
+
|
|
927
|
+
swapNodeChildren(container, childA, childB);
|
|
928
|
+
|
|
929
|
+
expect(getChildren(container)[0]).toBe(childA);
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
it('calls onChildrenOrderChanged on the parent', () => {
|
|
933
|
+
addNodeChild(container, childA);
|
|
934
|
+
addNodeChild(container, childB);
|
|
935
|
+
|
|
936
|
+
let called = false;
|
|
937
|
+
connectSignal(enableNodeSignals(container).onChildrenOrderChanged, () => {
|
|
938
|
+
called = true;
|
|
939
|
+
});
|
|
940
|
+
swapNodeChildren(container, childA, childB);
|
|
941
|
+
expect(called).toBe(true);
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
describe('swapNodeChildrenAt', () => {
|
|
946
|
+
it('swapNodeChildrenAt swaps children by index', () => {
|
|
947
|
+
addNodeChild(container, childA);
|
|
948
|
+
addNodeChild(container, childB);
|
|
949
|
+
|
|
950
|
+
swapNodeChildrenAt(container, 0, 1);
|
|
951
|
+
|
|
952
|
+
expect(getChildren(container)[0]).toBe(childB);
|
|
953
|
+
expect(getChildren(container)[1]).toBe(childA);
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
it('swapNodeChildrenAt assumes valid indices (throws if invalid)', () => {
|
|
957
|
+
addNodeChild(container, childA);
|
|
958
|
+
|
|
959
|
+
expect(() => swapNodeChildrenAt(container, 0, 1)).toThrow();
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
it('calls onChildrenOrderChanged on the parent', () => {
|
|
963
|
+
addNodeChild(container, childA);
|
|
964
|
+
addNodeChild(container, childB);
|
|
965
|
+
|
|
966
|
+
let called = false;
|
|
967
|
+
connectSignal(enableNodeSignals(container).onChildrenOrderChanged, () => {
|
|
968
|
+
called = true;
|
|
969
|
+
});
|
|
970
|
+
swapNodeChildrenAt(container, 0, 1);
|
|
971
|
+
expect(called).toBe(true);
|
|
972
|
+
});
|
|
973
|
+
});
|