@ccheever/exact-renderer 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.
Files changed (80) hide show
  1. package/package.json +118 -0
  2. package/src/__tests__/adapter-window-state.test.tsx +190 -0
  3. package/src/__tests__/attrs.test.ts +157 -0
  4. package/src/__tests__/classname.test.ts +332 -0
  5. package/src/__tests__/color.test.ts +169 -0
  6. package/src/__tests__/dom-mirror.test.ts +682 -0
  7. package/src/__tests__/dom-shim.test.ts +274 -0
  8. package/src/__tests__/fixtures/SvelteCounter.svelte +7 -0
  9. package/src/__tests__/fixtures/SvelteInput.svelte +8 -0
  10. package/src/__tests__/host-config.test.ts +51 -0
  11. package/src/__tests__/host-ops.test.ts +2234 -0
  12. package/src/__tests__/image-source.test.ts +135 -0
  13. package/src/__tests__/liquid-glass.test.ts +72 -0
  14. package/src/__tests__/multi-root.test.ts +118 -0
  15. package/src/__tests__/native-view-events.test.ts +102 -0
  16. package/src/__tests__/nodes.test.ts +399 -0
  17. package/src/__tests__/normalize.test.ts +576 -0
  18. package/src/__tests__/paragraph-lowering.test.tsx +144 -0
  19. package/src/__tests__/props.test.ts +518 -0
  20. package/src/__tests__/protocol-encoder.test.ts +732 -0
  21. package/src/__tests__/protocol-fixture-bytes.test.ts +41 -0
  22. package/src/__tests__/reconciler.test.tsx +241 -0
  23. package/src/__tests__/svelte-adapter.test.ts +166 -0
  24. package/src/__tests__/svg-source.test.ts +71 -0
  25. package/src/__tests__/tags.test.ts +354 -0
  26. package/src/__tests__/toggle.test.ts +441 -0
  27. package/src/__tests__/transitions.test.ts +106 -0
  28. package/src/__tests__/web-primitives.test.tsx +454 -0
  29. package/src/__tests__/window-hooks.test.tsx +447 -0
  30. package/src/adapter-contract.ts +68 -0
  31. package/src/attrs.ts +596 -0
  32. package/src/classname-contract.ts +87 -0
  33. package/src/classname-resolve.ts +553 -0
  34. package/src/classname-runtime.ts +29 -0
  35. package/src/components.ts +214 -0
  36. package/src/css-variable-context.ts +83 -0
  37. package/src/dom-hydration.ts +160 -0
  38. package/src/dom-mirror.ts +1459 -0
  39. package/src/dom-shim.ts +1736 -0
  40. package/src/group-context.ts +69 -0
  41. package/src/host-config.ts +431 -0
  42. package/src/host-ops.ts +3167 -0
  43. package/src/image-source.native.ts +703 -0
  44. package/src/image-source.ts +554 -0
  45. package/src/index.ts +278 -0
  46. package/src/inspector-runtime.ts +244 -0
  47. package/src/inspector.ts +3570 -0
  48. package/src/jsx-augmentations.ts +54 -0
  49. package/src/keyboard-avoidance.ts +217 -0
  50. package/src/native-primitives.ts +43 -0
  51. package/src/native-view-events.ts +322 -0
  52. package/src/native-view.ts +60 -0
  53. package/src/nodes/index.ts +41 -0
  54. package/src/nodes/node.ts +531 -0
  55. package/src/peer-context.ts +100 -0
  56. package/src/primitives.native.ts +8 -0
  57. package/src/primitives.ts +8 -0
  58. package/src/props/index.ts +14 -0
  59. package/src/props/normalize.ts +816 -0
  60. package/src/protocol/encoder.ts +940 -0
  61. package/src/protocol/index.ts +33 -0
  62. package/src/reconciler.ts +581 -0
  63. package/src/runtime.ts +11 -0
  64. package/src/safe-area.ts +543 -0
  65. package/src/solid.ts +490 -0
  66. package/src/style/color.js +1 -0
  67. package/src/style/color.ts +15 -0
  68. package/src/style/index.js +1 -0
  69. package/src/style/index.ts +22 -0
  70. package/src/style/normalize.js +1 -0
  71. package/src/style/normalize.ts +1426 -0
  72. package/src/svelte.ts +349 -0
  73. package/src/svg-source.ts +222 -0
  74. package/src/tags/index.ts +21 -0
  75. package/src/tags/tag-map.ts +289 -0
  76. package/src/text/paragraph-lowering.ts +310 -0
  77. package/src/types.ts +1175 -0
  78. package/src/vue.ts +535 -0
  79. package/src/web-host.ts +19 -0
  80. package/src/web-primitives.ts +1654 -0
@@ -0,0 +1,399 @@
1
+ /**
2
+ * Node Model Tests
3
+ *
4
+ * Tests for the host node model and helper functions.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach } from 'vitest';
8
+ import {
9
+ NodeKind,
10
+ DirtyFlags,
11
+ createRootNode,
12
+ createElementNode,
13
+ createTextNode,
14
+ appendChild,
15
+ insertBefore,
16
+ removeChild,
17
+ clearChildren,
18
+ getChildIds,
19
+ markStyleDirty,
20
+ markPropsDirty,
21
+ markEventsDirty,
22
+ clearDirtyFlags,
23
+ isDirty,
24
+ hasDirtyFlag,
25
+ _resetNodeIdCounter,
26
+ } from '../nodes/index.js';
27
+
28
+ // Reset node ID counter before each test
29
+ beforeEach(() => {
30
+ _resetNodeIdCounter();
31
+ });
32
+
33
+ describe('createRootNode', () => {
34
+ it('should create a root node with ID 0', () => {
35
+ const root = createRootNode();
36
+ expect(root.kind).toBe(NodeKind.Root);
37
+ expect(root.id).toBe(0);
38
+ expect(root.parent).toBeNull();
39
+ expect(root.children).toEqual([]);
40
+ });
41
+
42
+ it('should mark root as created', () => {
43
+ const root = createRootNode();
44
+ expect(hasDirtyFlag(root, DirtyFlags.Created)).toBe(true);
45
+ });
46
+ });
47
+
48
+ describe('createElementNode', () => {
49
+ it('should create an element node with incrementing IDs', () => {
50
+ const node1 = createElementNode('view', 'div', false, {});
51
+ const node2 = createElementNode('text', 'text', false, {});
52
+ const node3 = createElementNode('view', 'View', true, {});
53
+
54
+ expect(node1.id).toBe(1);
55
+ expect(node2.id).toBe(2);
56
+ expect(node3.id).toBe(3);
57
+ });
58
+
59
+ it('should set correct properties', () => {
60
+ const node = createElementNode('view', 'div', false, { style: { width: 100 } });
61
+
62
+ expect(node.kind).toBe(NodeKind.Element);
63
+ expect(node.tagType).toBe('view');
64
+ expect(node.originalTag).toBe('div');
65
+ expect(node.isRNStyle).toBe(false);
66
+ expect(node.parent).toBeNull();
67
+ expect(node.children).toEqual([]);
68
+ expect(node.events.size).toBe(0);
69
+ expect(node.originalProps).toEqual({ style: { width: 100 } });
70
+ });
71
+
72
+ it('should mark element as created', () => {
73
+ const node = createElementNode('view', 'div', false, {});
74
+ expect(hasDirtyFlag(node, DirtyFlags.Created)).toBe(true);
75
+ });
76
+ });
77
+
78
+ describe('createTextNode', () => {
79
+ it('should create a text node with correct text', () => {
80
+ const node = createTextNode('Hello, world!');
81
+
82
+ expect(node.kind).toBe(NodeKind.Text);
83
+ expect(node.text).toBe('Hello, world!');
84
+ expect(node.parent).toBeNull();
85
+ });
86
+
87
+ it('should mark text node as created', () => {
88
+ const node = createTextNode('test');
89
+ expect(hasDirtyFlag(node, DirtyFlags.Created)).toBe(true);
90
+ });
91
+ });
92
+
93
+ describe('appendChild', () => {
94
+ it('should add child to parent', () => {
95
+ const parent = createElementNode('view', 'div', false, {});
96
+ const child = createElementNode('text', 'text', false, {});
97
+
98
+ const result = appendChild(parent, child);
99
+
100
+ expect(result).toBe(true);
101
+ expect(parent.children).toContain(child);
102
+ expect(child.parent).toBe(parent);
103
+ });
104
+
105
+ it('should mark parent children as dirty', () => {
106
+ const parent = createElementNode('view', 'div', false, {});
107
+ clearDirtyFlags(parent);
108
+ const child = createElementNode('text', 'text', false, {});
109
+
110
+ appendChild(parent, child);
111
+
112
+ expect(hasDirtyFlag(parent, DirtyFlags.Children)).toBe(true);
113
+ });
114
+
115
+ it('should not add duplicate children', () => {
116
+ const parent = createElementNode('view', 'div', false, {});
117
+ const child = createElementNode('text', 'text', false, {});
118
+
119
+ appendChild(parent, child);
120
+ const result = appendChild(parent, child);
121
+
122
+ expect(result).toBe(false);
123
+ expect(parent.children.length).toBe(1);
124
+ });
125
+
126
+ it('should move an existing child to the end', () => {
127
+ const parent = createElementNode('view', 'div', false, {});
128
+ const child1 = createElementNode('text', 'text', false, {});
129
+ const child2 = createElementNode('text', 'text', false, {});
130
+ const child3 = createElementNode('text', 'text', false, {});
131
+
132
+ appendChild(parent, child1);
133
+ appendChild(parent, child2);
134
+ appendChild(parent, child3);
135
+ clearDirtyFlags(parent);
136
+
137
+ const result = appendChild(parent, child1);
138
+
139
+ expect(result).toBe(true);
140
+ expect(parent.children).toEqual([child2, child3, child1]);
141
+ expect(child1.parent).toBe(parent);
142
+ expect(hasDirtyFlag(parent, DirtyFlags.Children)).toBe(true);
143
+ });
144
+
145
+ it('should work with root node', () => {
146
+ const root = createRootNode();
147
+ const child = createElementNode('view', 'div', false, {});
148
+
149
+ appendChild(root, child);
150
+
151
+ expect(root.children).toContain(child);
152
+ expect(child.parent).toBe(root);
153
+ });
154
+ });
155
+
156
+ describe('insertBefore', () => {
157
+ it('should insert child before another child', () => {
158
+ const parent = createElementNode('view', 'div', false, {});
159
+ const child1 = createElementNode('text', 'text', false, {});
160
+ const child2 = createElementNode('text', 'text', false, {});
161
+ const child3 = createElementNode('text', 'text', false, {});
162
+
163
+ appendChild(parent, child1);
164
+ appendChild(parent, child3);
165
+ insertBefore(parent, child2, child3);
166
+
167
+ expect(parent.children[0]).toBe(child1);
168
+ expect(parent.children[1]).toBe(child2);
169
+ expect(parent.children[2]).toBe(child3);
170
+ });
171
+
172
+ it('should append if beforeChild not found', () => {
173
+ const parent = createElementNode('view', 'div', false, {});
174
+ const child1 = createElementNode('text', 'text', false, {});
175
+ const child2 = createElementNode('text', 'text', false, {});
176
+ const notAChild = createElementNode('text', 'text', false, {});
177
+
178
+ appendChild(parent, child1);
179
+ insertBefore(parent, child2, notAChild);
180
+
181
+ expect(parent.children.length).toBe(2);
182
+ expect(parent.children[1]).toBe(child2);
183
+ });
184
+ });
185
+
186
+ describe('removeChild', () => {
187
+ it('should remove child from parent', () => {
188
+ const parent = createElementNode('view', 'div', false, {});
189
+ const child = createElementNode('text', 'text', false, {});
190
+
191
+ appendChild(parent, child);
192
+ const result = removeChild(parent, child);
193
+
194
+ expect(result).toBe(true);
195
+ expect(parent.children).not.toContain(child);
196
+ expect(child.parent).toBeNull();
197
+ });
198
+
199
+ it('should return false if child not found', () => {
200
+ const parent = createElementNode('view', 'div', false, {});
201
+ const notAChild = createElementNode('text', 'text', false, {});
202
+
203
+ const result = removeChild(parent, notAChild);
204
+
205
+ expect(result).toBe(false);
206
+ });
207
+
208
+ it('should mark parent children as dirty', () => {
209
+ const parent = createElementNode('view', 'div', false, {});
210
+ const child = createElementNode('text', 'text', false, {});
211
+
212
+ appendChild(parent, child);
213
+ clearDirtyFlags(parent);
214
+ removeChild(parent, child);
215
+
216
+ expect(hasDirtyFlag(parent, DirtyFlags.Children)).toBe(true);
217
+ });
218
+ });
219
+
220
+ describe('clearChildren', () => {
221
+ it('should remove all children', () => {
222
+ const parent = createElementNode('view', 'div', false, {});
223
+ const child1 = createElementNode('text', 'text', false, {});
224
+ const child2 = createElementNode('text', 'text', false, {});
225
+
226
+ appendChild(parent, child1);
227
+ appendChild(parent, child2);
228
+ clearChildren(parent);
229
+
230
+ expect(parent.children.length).toBe(0);
231
+ expect(child1.parent).toBeNull();
232
+ expect(child2.parent).toBeNull();
233
+ });
234
+ });
235
+
236
+ describe('getChildIds', () => {
237
+ it('should return array of child IDs', () => {
238
+ const parent = createElementNode('view', 'div', false, {});
239
+ const child1 = createElementNode('text', 'text', false, {});
240
+ const child2 = createElementNode('text', 'text', false, {});
241
+
242
+ appendChild(parent, child1);
243
+ appendChild(parent, child2);
244
+
245
+ const ids = getChildIds(parent);
246
+ expect(ids).toContain(child1.id);
247
+ expect(ids).toContain(child2.id);
248
+ expect(ids.length).toBe(2);
249
+ });
250
+
251
+ it('should return empty array for no children', () => {
252
+ const parent = createElementNode('view', 'div', false, {});
253
+ expect(getChildIds(parent)).toEqual([]);
254
+ });
255
+ });
256
+
257
+ describe('dirty flags', () => {
258
+ describe('markStyleDirty', () => {
259
+ it('should set style dirty flag', () => {
260
+ const node = createElementNode('view', 'div', false, {});
261
+ clearDirtyFlags(node);
262
+
263
+ markStyleDirty(node);
264
+
265
+ expect(hasDirtyFlag(node, DirtyFlags.Style)).toBe(true);
266
+ expect(hasDirtyFlag(node, DirtyFlags.Props)).toBe(false);
267
+ });
268
+ });
269
+
270
+ describe('markPropsDirty', () => {
271
+ it('should set props dirty flag', () => {
272
+ const node = createElementNode('view', 'div', false, {});
273
+ clearDirtyFlags(node);
274
+
275
+ markPropsDirty(node);
276
+
277
+ expect(hasDirtyFlag(node, DirtyFlags.Props)).toBe(true);
278
+ expect(hasDirtyFlag(node, DirtyFlags.Style)).toBe(false);
279
+ });
280
+ });
281
+
282
+ describe('markEventsDirty', () => {
283
+ it('should set events dirty flag', () => {
284
+ const node = createElementNode('view', 'div', false, {});
285
+ clearDirtyFlags(node);
286
+
287
+ markEventsDirty(node);
288
+
289
+ expect(hasDirtyFlag(node, DirtyFlags.Events)).toBe(true);
290
+ });
291
+ });
292
+
293
+ describe('clearDirtyFlags', () => {
294
+ it('should clear all dirty flags', () => {
295
+ const node = createElementNode('view', 'div', false, {});
296
+ // node starts with Created flag
297
+
298
+ clearDirtyFlags(node);
299
+
300
+ expect(isDirty(node)).toBe(false);
301
+ expect(node.dirtyFlags).toBe(DirtyFlags.None);
302
+ });
303
+ });
304
+
305
+ describe('isDirty', () => {
306
+ it('should return true if any flag is set', () => {
307
+ const node = createElementNode('view', 'div', false, {});
308
+ expect(isDirty(node)).toBe(true); // Created flag is set
309
+
310
+ clearDirtyFlags(node);
311
+ expect(isDirty(node)).toBe(false);
312
+
313
+ markStyleDirty(node);
314
+ expect(isDirty(node)).toBe(true);
315
+ });
316
+ });
317
+
318
+ describe('hasDirtyFlag', () => {
319
+ it('should check specific flag', () => {
320
+ const node = createElementNode('view', 'div', false, {});
321
+ clearDirtyFlags(node);
322
+
323
+ markStyleDirty(node);
324
+ markPropsDirty(node);
325
+
326
+ expect(hasDirtyFlag(node, DirtyFlags.Style)).toBe(true);
327
+ expect(hasDirtyFlag(node, DirtyFlags.Props)).toBe(true);
328
+ expect(hasDirtyFlag(node, DirtyFlags.Events)).toBe(false);
329
+ expect(hasDirtyFlag(node, DirtyFlags.Children)).toBe(false);
330
+ });
331
+ });
332
+ });
333
+
334
+ describe('child operations on large parents (LLP 0159 index cache)', () => {
335
+ // Above the child-index cache threshold (64), lookups go through a lazily
336
+ // built per-parent map. These tests exercise the cached paths against the
337
+ // same expectations as the small-array paths.
338
+ const CHILD_COUNT = 200;
339
+
340
+ function buildLargeParent() {
341
+ const parent = createElementNode('view', 'View', false, {});
342
+ const children = Array.from({ length: CHILD_COUNT }, () =>
343
+ createElementNode('view', 'View', false, {})
344
+ );
345
+ for (const child of children) {
346
+ appendChild(parent, child);
347
+ }
348
+ return { parent, children };
349
+ }
350
+
351
+ it('appends, moves, inserts, and removes consistently above the threshold', () => {
352
+ const { parent, children } = buildLargeParent();
353
+
354
+ expect(parent.children).toHaveLength(CHILD_COUNT);
355
+ expect(getChildIds(parent)).toEqual(children.map((child) => child.id));
356
+
357
+ // Re-appending the last child is a no-op.
358
+ expect(appendChild(parent, children[CHILD_COUNT - 1]!)).toBe(false);
359
+
360
+ // Moving an interior child to the end.
361
+ expect(appendChild(parent, children[10]!)).toBe(true);
362
+ expect(parent.children[CHILD_COUNT - 1]).toBe(children[10]);
363
+ expect(parent.children).toHaveLength(CHILD_COUNT);
364
+
365
+ // Moving it back before an interior sibling (cache rebuilt after the move).
366
+ expect(insertBefore(parent, children[10]!, children[11]!)).toBe(true);
367
+ expect(parent.children[10]).toBe(children[10]);
368
+ expect(parent.children[11]).toBe(children[11]);
369
+ expect(parent.children).toHaveLength(CHILD_COUNT);
370
+
371
+ // Removing an interior child.
372
+ expect(removeChild(parent, children[50]!)).toBe(true);
373
+ expect(parent.children).toHaveLength(CHILD_COUNT - 1);
374
+ expect(parent.children.indexOf(children[50]!)).toBe(-1);
375
+ expect(removeChild(parent, children[50]!)).toBe(false);
376
+
377
+ // Appending a brand-new child after mutations still lands at the end.
378
+ const fresh = createElementNode('view', 'View', false, {});
379
+ expect(appendChild(parent, fresh)).toBe(true);
380
+ expect(parent.children[parent.children.length - 1]).toBe(fresh);
381
+
382
+ clearChildren(parent);
383
+ expect(parent.children).toHaveLength(0);
384
+ for (const child of children) {
385
+ if (child !== children[50]) {
386
+ expect(child.parent).toBeNull();
387
+ }
388
+ }
389
+ });
390
+
391
+ it('insertBefore falls back to append when beforeChild is absent', () => {
392
+ const { parent } = buildLargeParent();
393
+ const fresh = createElementNode('view', 'View', false, {});
394
+ const detached = createElementNode('view', 'View', false, {});
395
+
396
+ expect(insertBefore(parent, fresh, detached)).toBe(true);
397
+ expect(parent.children[parent.children.length - 1]).toBe(fresh);
398
+ });
399
+ });