@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,274 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ emptyKeyboardState,
4
+ freezeDisplay,
5
+ freezeInsets,
6
+ freezePosition,
7
+ freezeSafeAreaRegions,
8
+ freezeSize,
9
+ notifySnapshotListeners,
10
+ resetWindowStateStoreForTests,
11
+ setRootWindowId,
12
+ setWindowSnapshot,
13
+ } from '@exact/core';
14
+
15
+ import { getRootNode } from '../inspector.js';
16
+ import { _resetNodeIdCounter, NodeKind, type ElementNode } from '../nodes/index.js';
17
+ import { EventType, dispatchSyntheticEvent } from '../host-ops.js';
18
+ import { _clearHandlers } from '../host-config.js';
19
+ import { _resetHostOpsState } from '../host-ops.js';
20
+ import { _resetInspectorState } from '../inspector.js';
21
+ import { createExactDOM } from '../dom-shim.js';
22
+
23
+ interface ExactRuntimeMock {
24
+ dispatch: ReturnType<typeof vi.fn>;
25
+ screenWidth: number;
26
+ screenHeight: number;
27
+ screenScale?: number;
28
+ safeAreaInsets?: {
29
+ top?: number;
30
+ right?: number;
31
+ bottom?: number;
32
+ left?: number;
33
+ };
34
+ }
35
+
36
+ interface ExactGlobals {
37
+ exact?: ExactRuntimeMock;
38
+ }
39
+
40
+ function flushMicrotasks(): Promise<void> {
41
+ return Promise.resolve().then(() => Promise.resolve());
42
+ }
43
+
44
+ function getFirstElement(rootId: number = 0): ElementNode {
45
+ const root = getRootNode(rootId);
46
+ expect(root).not.toBeNull();
47
+ expect(root?.children[0]?.kind).toBe(NodeKind.Element);
48
+
49
+ const child = root?.children[0];
50
+ if (!child || child.kind !== NodeKind.Element) {
51
+ throw new Error(`Expected root ${rootId} to have an element child.`);
52
+ }
53
+
54
+ return child;
55
+ }
56
+
57
+ function setRootViewport(rootId: number, width: number, height: number): void {
58
+ const windowId = `window-${rootId}`;
59
+ const size = freezeSize({ width, height });
60
+ setRootWindowId(rootId, windowId);
61
+ setWindowSnapshot(windowId, {
62
+ size,
63
+ position: freezePosition({ x: 0, y: 0 }),
64
+ state: 'normal',
65
+ isFocused: true,
66
+ isVisible: true,
67
+ display: freezeDisplay({
68
+ id: windowId,
69
+ size,
70
+ workArea: {
71
+ x: 0,
72
+ y: 0,
73
+ width,
74
+ height,
75
+ },
76
+ scaleFactor: 1,
77
+ }),
78
+ safeAreaInsets: freezeInsets({ top: 0, right: 0, bottom: 0, left: 0 }),
79
+ safeAreaRegions: freezeSafeAreaRegions({
80
+ container: { top: 0, right: 0, bottom: 0, left: 0 },
81
+ displayCutout: { top: 0, right: 0, bottom: 0, left: 0 },
82
+ gestures: { top: 0, right: 0, bottom: 0, left: 0 },
83
+ }),
84
+ keyboardState: emptyKeyboardState(),
85
+ devicePixelRatio: 1,
86
+ });
87
+ notifySnapshotListeners(windowId);
88
+ }
89
+
90
+ beforeEach(() => {
91
+ const globals = globalThis as typeof globalThis & ExactGlobals;
92
+ globals.exact = {
93
+ dispatch: vi.fn(),
94
+ screenWidth: 393,
95
+ screenHeight: 852,
96
+ screenScale: 2,
97
+ safeAreaInsets: {
98
+ top: 0,
99
+ right: 0,
100
+ bottom: 0,
101
+ left: 0,
102
+ },
103
+ };
104
+
105
+ _resetNodeIdCounter();
106
+ _clearHandlers();
107
+ _resetHostOpsState();
108
+ _resetInspectorState();
109
+ resetWindowStateStoreForTests();
110
+ });
111
+
112
+ afterEach(() => {
113
+ const globals = globalThis as typeof globalThis & ExactGlobals;
114
+ delete globals.exact;
115
+
116
+ _clearHandlers();
117
+ _resetNodeIdCounter();
118
+ _resetHostOpsState();
119
+ _resetInspectorState();
120
+ resetWindowStateStoreForTests();
121
+ });
122
+
123
+ describe('Exact DOM shim', () => {
124
+ it('supports DOM-style reparenting and fragment insertion', async () => {
125
+ const dom = createExactDOM();
126
+ const first = dom.document.createElement('div');
127
+ const second = dom.document.createElement('div');
128
+ const label = dom.document.createTextNode('payload');
129
+ const fragment = dom.document.createDocumentFragment();
130
+
131
+ fragment.appendChild(first);
132
+ fragment.appendChild(second);
133
+ dom.document.body.appendChild(fragment);
134
+ second.appendChild(label);
135
+
136
+ expect(fragment.childNodes).toHaveLength(0);
137
+ expect(dom.document.body.childNodes).toHaveLength(2);
138
+ expect(label.parentNode).toBe(second);
139
+
140
+ first.appendChild(label);
141
+
142
+ expect(label.parentNode).toBe(first);
143
+ expect(first.textContent).toBe('payload');
144
+ expect(second.textContent).toBe('');
145
+
146
+ await flushMicrotasks();
147
+ dom.destroy();
148
+ });
149
+
150
+ it('coalesces class, style, and reflected props into the committed host node', async () => {
151
+ const dom = createExactDOM();
152
+ const element = dom.document.createElement('div');
153
+
154
+ dom.document.body.appendChild(element);
155
+ element.classList.add('card', 'primary');
156
+ element.style.cssText = 'padding-top: 12px; background-color: #123456;';
157
+ element.tabIndex = 4;
158
+ element.hidden = true;
159
+
160
+ await flushMicrotasks();
161
+
162
+ const host = getFirstElement();
163
+ expect(host.originalProps.className).toBe('card primary');
164
+ expect(host.originalProps.tabIndex).toBe(4);
165
+ expect(host.originalProps.hidden).toBe(true);
166
+ expect((host.originalProps.style as Record<string, unknown>).paddingTop).toBe('12px');
167
+ expect((host.originalProps.style as Record<string, unknown>).backgroundColor).toBe('#123456');
168
+
169
+ dom.destroy();
170
+ });
171
+
172
+ it('promotes checkbox inputs to toggles and syncs checked state from change payloads', async () => {
173
+ const dom = createExactDOM();
174
+ const input = dom.document.createElement('input');
175
+ const events: Array<boolean> = [];
176
+
177
+ input.setAttribute('type', 'checkbox');
178
+ input.addEventListener('change', (event) => {
179
+ events.push((event.target as typeof input).checked);
180
+ });
181
+
182
+ dom.document.body.appendChild(input);
183
+ await flushMicrotasks();
184
+
185
+ const host = getFirstElement();
186
+ expect(host.tagType).toBe('toggle');
187
+
188
+ dispatchSyntheticEvent(host, EventType.Change, {
189
+ nativeEvent: {
190
+ value: true,
191
+ },
192
+ });
193
+
194
+ expect(input.checked).toBe(true);
195
+ expect(events).toEqual([true]);
196
+
197
+ dom.destroy();
198
+ });
199
+
200
+ it('throws when a node is moved across DOM roots', () => {
201
+ const domA = createExactDOM({ rootId: 1 });
202
+ const domB = createExactDOM({ rootId: 2 });
203
+ const child = domA.document.createElement('div');
204
+
205
+ expect(() => {
206
+ domB.document.body.appendChild(child);
207
+ }).toThrow(/different DOM roots/);
208
+
209
+ domA.destroy();
210
+ domB.destroy();
211
+ });
212
+
213
+ it('fires resize listeners only when the root viewport dimensions change', async () => {
214
+ setRootViewport(3, 320, 640);
215
+ const dom = createExactDOM({ rootId: 3 });
216
+ const listener = vi.fn();
217
+
218
+ dom.window.addEventListener('resize', listener);
219
+
220
+ const windowId = 'window-3';
221
+ const current = freezeSize({ width: 320, height: 640 });
222
+ setWindowSnapshot(windowId, {
223
+ size: current,
224
+ position: freezePosition({ x: 0, y: 0 }),
225
+ state: 'normal',
226
+ isFocused: true,
227
+ isVisible: true,
228
+ display: freezeDisplay({
229
+ id: windowId,
230
+ size: current,
231
+ workArea: {
232
+ x: 0,
233
+ y: 0,
234
+ width: 320,
235
+ height: 640,
236
+ },
237
+ scaleFactor: 1,
238
+ }),
239
+ safeAreaInsets: freezeInsets({ top: 12, right: 0, bottom: 0, left: 0 }),
240
+ safeAreaRegions: freezeSafeAreaRegions({
241
+ container: { top: 12, right: 0, bottom: 0, left: 0 },
242
+ displayCutout: { top: 0, right: 0, bottom: 0, left: 0 },
243
+ gestures: { top: 0, right: 0, bottom: 0, left: 0 },
244
+ }),
245
+ keyboardState: emptyKeyboardState(),
246
+ devicePixelRatio: 1,
247
+ });
248
+ notifySnapshotListeners(windowId);
249
+ await flushMicrotasks();
250
+
251
+ expect(listener).not.toHaveBeenCalled();
252
+
253
+ setRootViewport(3, 400, 700);
254
+ await flushMicrotasks();
255
+
256
+ expect(listener).toHaveBeenCalledTimes(1);
257
+ expect(dom.window.innerWidth).toBe(400);
258
+ expect(dom.window.innerHeight).toBe(700);
259
+
260
+ dom.destroy();
261
+ });
262
+
263
+ it('prevents queued dirty work from flushing after destroy()', async () => {
264
+ const dom = createExactDOM({ rootId: 4 });
265
+ const element = dom.document.createElement('div');
266
+
267
+ dom.document.body.appendChild(element);
268
+ element.setExactProp('hidden', true);
269
+ dom.destroy();
270
+
271
+ await flushMicrotasks();
272
+ expect(getRootNode(4)).toBeNull();
273
+ });
274
+ });
@@ -0,0 +1,7 @@
1
+ <script lang="ts">
2
+ let count = $state(0);
3
+ </script>
4
+
5
+ <button on:click={() => count += 1}>
6
+ Count {count}
7
+ </button>
@@ -0,0 +1,8 @@
1
+ <script lang="ts">
2
+ let value = $state('hi');
3
+ </script>
4
+
5
+ <div>
6
+ <input bind:value />
7
+ <p>{value}</p>
8
+ </div>
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { hostConfig } from '../host-config.js';
4
+ import { createInstance } from '../host-ops.js';
5
+
6
+ describe('hostConfig.shouldSetTextContent', () => {
7
+ it('treats text-only Text children as parent text content', () => {
8
+ expect(hostConfig.shouldSetTextContent('Text', { children: 'Hello' })).toBe(true);
9
+ expect(hostConfig.shouldSetTextContent('Text', { children: ['Build ', 42] })).toBe(true);
10
+ });
11
+
12
+ it('does not flatten non-text children into parent text content', () => {
13
+ expect(
14
+ hostConfig.shouldSetTextContent('Text', {
15
+ children: [{ type: 'View', props: {} }],
16
+ })
17
+ ).toBe(false);
18
+ });
19
+
20
+ it('does not flatten non-text-container elements', () => {
21
+ expect(hostConfig.shouldSetTextContent('View', { children: 'Hello' })).toBe(false);
22
+ });
23
+ });
24
+
25
+ describe('hostConfig.commitUpdate', () => {
26
+ it('has no prepareUpdate (removed in react-reconciler 0.33)', () => {
27
+ expect(hostConfig.prepareUpdate).toBeUndefined();
28
+ });
29
+
30
+ it('applies new props through the 0.33 five-param signature', () => {
31
+ const instance = createInstance('Pressable', {
32
+ onPress: () => 'initial',
33
+ });
34
+ const nextProps = {
35
+ onPress: () => 'updated',
36
+ testId: 'pressable-updated',
37
+ };
38
+
39
+ // react-reconciler 0.33 calls commitUpdate(instance, type, oldProps, newProps, fiber)
40
+ hostConfig.commitUpdate(
41
+ instance,
42
+ 'Pressable',
43
+ instance.originalProps,
44
+ nextProps,
45
+ {} as never
46
+ );
47
+
48
+ expect(instance.originalProps).toBe(nextProps);
49
+ expect(instance.props.testId).toBe('pressable-updated');
50
+ });
51
+ });