@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,447 @@
1
+ import React, { act } from 'react';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { MAIN_WINDOW_ID } from '@exact/core';
4
+
5
+ import { Text } from '../components.js';
6
+ import { _clearHandlers } from '../host-config.js';
7
+ import { getRootNode } from '../inspector.js';
8
+ import { NodeKind, _resetNodeIdCounter } from '../nodes/index.js';
9
+ import { _getRootContainer, render, reset } from '../reconciler.js';
10
+ import {
11
+ _resetWindowRuntimeForTests,
12
+ getWindowViewportForRoot,
13
+ subscribeToRootWindowState,
14
+ window as exactWindow,
15
+ windows,
16
+ } from '../../../../js/src/window-management.js';
17
+ import {
18
+ emitDesktopHostEvent,
19
+ setDesktopHostBridge,
20
+ } from '../../../../js/src/desktop-platform.js';
21
+ import {
22
+ useKeyboardState,
23
+ useSafeAreaInsets,
24
+ useWindowFocused,
25
+ useWindowSize,
26
+ useWindowState,
27
+ } from '../../../../js/src/window-hooks.js';
28
+
29
+ interface ExactRuntimeMock {
30
+ dispatch: ReturnType<typeof vi.fn>;
31
+ screenWidth: number;
32
+ screenHeight: number;
33
+ screenScale?: number;
34
+ safeAreaInsets?: {
35
+ top?: number;
36
+ right?: number;
37
+ bottom?: number;
38
+ left?: number;
39
+ };
40
+ }
41
+
42
+ interface ExactGlobals {
43
+ exact?: ExactRuntimeMock;
44
+ __exactViewportChanged?: (width?: number, height?: number) => void;
45
+ __exactSafeAreaInsetsChanged?: (
46
+ top?: number,
47
+ right?: number,
48
+ bottom?: number,
49
+ left?: number,
50
+ ) => void;
51
+ __exactKeyboardStateChanged?: (update: {
52
+ occlusion?: {
53
+ x?: number;
54
+ y?: number;
55
+ width?: number;
56
+ height?: number;
57
+ };
58
+ visible?: boolean;
59
+ progress?: number;
60
+ animating?: boolean;
61
+ mode?: 'docked' | 'floating' | 'split' | 'undocked';
62
+ animationCurve?: number;
63
+ animationDurationMs?: number;
64
+ interactive?: boolean;
65
+ accessoryHeight?: number;
66
+ source?: 'software' | 'hardware';
67
+ }) => void;
68
+ IS_REACT_ACT_ENVIRONMENT?: boolean;
69
+ }
70
+
71
+ function getRenderedText(): string | undefined {
72
+ const root = _getRootContainer();
73
+ expect(root).not.toBeNull();
74
+
75
+ const child = root!.children[0];
76
+ expect(child).toBeDefined();
77
+ expect(child?.kind).toBe(NodeKind.Element);
78
+
79
+ if (!child || child.kind !== NodeKind.Element) {
80
+ throw new Error('Expected root child to be a Text element.');
81
+ }
82
+
83
+ return child.props.textContent as string | undefined;
84
+ }
85
+
86
+ function getRenderedTextForRoot(rootId: number): string | undefined {
87
+ const root = getRootNode(rootId);
88
+ expect(root).not.toBeNull();
89
+
90
+ const child = root!.children[0];
91
+ expect(child).toBeDefined();
92
+ expect(child?.kind).toBe(NodeKind.Element);
93
+
94
+ if (!child || child.kind !== NodeKind.Element) {
95
+ throw new Error('Expected root child to be a Text element.');
96
+ }
97
+
98
+ return child.props.textContent as string | undefined;
99
+ }
100
+
101
+ function WindowProbe(): React.ReactElement {
102
+ const size = useWindowSize();
103
+ const state = useWindowState();
104
+ const focused = useWindowFocused();
105
+ const insets = useSafeAreaInsets();
106
+ const keyboard = useKeyboardState();
107
+
108
+ return (
109
+ <Text>
110
+ {`${size.width}x${size.height}|${state}|${focused ? 'focused' : 'blurred'}|${insets.top}/${insets.right}/${insets.bottom}/${insets.left}|${keyboard.visible ? 'keyboard' : 'nokeyboard'}|${keyboard.occlusion.height}`}
111
+ </Text>
112
+ );
113
+ }
114
+
115
+ function SecondaryWindowProbe(): React.ReactElement {
116
+ const size = useWindowSize();
117
+ const insets = useSafeAreaInsets();
118
+ return <Text>{`secondary:${size.width}x${size.height}|${insets.top}/${insets.right}/${insets.bottom}/${insets.left}`}</Text>;
119
+ }
120
+
121
+ describe('window hooks', () => {
122
+ let dispatch: ReturnType<typeof vi.fn>;
123
+
124
+ beforeEach(async () => {
125
+ dispatch = vi.fn();
126
+
127
+ const globals = globalThis as typeof globalThis & ExactGlobals;
128
+ globals.IS_REACT_ACT_ENVIRONMENT = true;
129
+ globals.exact = {
130
+ dispatch,
131
+ screenWidth: 393,
132
+ screenHeight: 852,
133
+ screenScale: 2,
134
+ safeAreaInsets: {
135
+ top: 0,
136
+ right: 0,
137
+ bottom: 0,
138
+ left: 0,
139
+ },
140
+ };
141
+
142
+ _resetNodeIdCounter();
143
+ _clearHandlers();
144
+ await act(async () => {
145
+ reset();
146
+ });
147
+ setDesktopHostBridge(undefined);
148
+ _resetWindowRuntimeForTests();
149
+ });
150
+
151
+ afterEach(async () => {
152
+ await act(async () => {
153
+ reset();
154
+ });
155
+ _clearHandlers();
156
+ _resetNodeIdCounter();
157
+ setDesktopHostBridge(undefined);
158
+ _resetWindowRuntimeForTests();
159
+
160
+ const globals = globalThis as typeof globalThis & ExactGlobals;
161
+ delete globals.exact;
162
+ delete globals.IS_REACT_ACT_ENVIRONMENT;
163
+ });
164
+
165
+ it('re-renders on viewport changes and emits resize events', async () => {
166
+ const resizeSpy = vi.fn();
167
+ const unsubscribe = exactWindow.on('resize', resizeSpy);
168
+
169
+ await act(async () => {
170
+ render(<WindowProbe />);
171
+ });
172
+
173
+ expect(dispatch).toHaveBeenCalledTimes(1);
174
+ expect(getRenderedText()).toBe('393x852|normal|focused|0/0/0/0|nokeyboard|0');
175
+
176
+ const globals = globalThis as typeof globalThis & ExactGlobals;
177
+ globals.exact!.screenWidth = 800;
178
+ globals.exact!.screenHeight = 600;
179
+
180
+ await act(async () => {
181
+ globals.__exactViewportChanged?.(800, 600);
182
+ });
183
+
184
+ expect(dispatch).toHaveBeenCalledTimes(2);
185
+ expect(getRenderedText()).toBe('800x600|normal|focused|0/0/0/0|nokeyboard|0');
186
+ expect(resizeSpy).toHaveBeenCalledWith({
187
+ width: 800,
188
+ height: 600,
189
+ });
190
+
191
+ unsubscribe();
192
+ });
193
+
194
+ it('mirrors viewport updates into the attached desktop main-window snapshot', async () => {
195
+ let mainWindowSnapshot = {
196
+ id: 'main-native',
197
+ rootId: 0,
198
+ title: 'Facet Lab',
199
+ size: { width: 480, height: 960 },
200
+ position: { x: 120, y: 80 },
201
+ state: 'normal' as const,
202
+ isFocused: true,
203
+ isVisible: true,
204
+ display: {
205
+ id: 'display-main',
206
+ size: { width: 1440, height: 900 },
207
+ workArea: { x: 0, y: 0, width: 1440, height: 860 },
208
+ scaleFactor: 2,
209
+ },
210
+ safeAreaInsets: { top: 0, right: 0, bottom: 0, left: 0 },
211
+ devicePixelRatio: 2,
212
+ };
213
+
214
+ setDesktopHostBridge({
215
+ windows: {
216
+ getSnapshot(windowId) {
217
+ return windowId === MAIN_WINDOW_ID || windowId === 'main-native'
218
+ ? mainWindowSnapshot
219
+ : null;
220
+ },
221
+ call: vi.fn(),
222
+ },
223
+ });
224
+
225
+ act(() => {
226
+ emitDesktopHostEvent('windows', 'windowOpened', mainWindowSnapshot);
227
+ });
228
+
229
+ expect(getWindowViewportForRoot(0)).toEqual({
230
+ width: 480,
231
+ height: 960,
232
+ });
233
+
234
+ const rootListener = vi.fn();
235
+ const unsubscribe = subscribeToRootWindowState(0, rootListener);
236
+
237
+ const globals = globalThis as typeof globalThis & ExactGlobals;
238
+ globals.exact!.screenWidth = 676;
239
+ globals.exact!.screenHeight = 960;
240
+ mainWindowSnapshot = {
241
+ ...mainWindowSnapshot,
242
+ size: { width: 676, height: 960 },
243
+ };
244
+
245
+ await act(async () => {
246
+ globals.__exactViewportChanged?.(676, 960);
247
+ });
248
+
249
+ expect(getWindowViewportForRoot(0)).toEqual({
250
+ width: 676,
251
+ height: 960,
252
+ });
253
+ expect(rootListener).toHaveBeenCalled();
254
+
255
+ unsubscribe();
256
+ });
257
+
258
+ it('updates safe area state when host safe area callbacks fire', async () => {
259
+ await act(async () => {
260
+ render(<WindowProbe />);
261
+ });
262
+
263
+ const globals = globalThis as typeof globalThis & ExactGlobals;
264
+
265
+ await act(async () => {
266
+ globals.__exactSafeAreaInsetsChanged?.(12, 4, 8, 2);
267
+ });
268
+
269
+ expect(getRenderedText()).toBe('393x852|normal|focused|12/4/8/2|nokeyboard|0');
270
+ expect(exactWindow.safeAreaInsets).toEqual({
271
+ top: 12,
272
+ right: 4,
273
+ bottom: 8,
274
+ left: 2,
275
+ });
276
+ });
277
+
278
+ it('updates keyboard state when host keyboard callbacks fire', async () => {
279
+ await act(async () => {
280
+ render(<WindowProbe />);
281
+ });
282
+
283
+ const globals = globalThis as typeof globalThis & ExactGlobals;
284
+ await act(async () => {
285
+ globals.__exactKeyboardStateChanged?.({
286
+ visible: true,
287
+ mode: 'docked',
288
+ progress: 1,
289
+ occlusion: {
290
+ x: 0,
291
+ y: 516,
292
+ width: 393,
293
+ height: 336,
294
+ },
295
+ });
296
+ });
297
+
298
+ expect(getRenderedText()).toBe('393x852|normal|focused|0/0/0/0|keyboard|336');
299
+ });
300
+
301
+ it('renders secondary roots against their own window snapshots', async () => {
302
+ let secondarySnapshot = {
303
+ id: 'secondary',
304
+ rootId: 1,
305
+ title: 'Inspector',
306
+ size: { width: 640, height: 480 },
307
+ position: { x: 120, y: 80 },
308
+ state: 'normal' as const,
309
+ isFocused: true,
310
+ isVisible: true,
311
+ display: {
312
+ id: 'display-2',
313
+ size: { width: 1440, height: 900 },
314
+ workArea: { x: 0, y: 0, width: 1440, height: 860 },
315
+ scaleFactor: 2,
316
+ },
317
+ safeAreaInsets: { top: 6, right: 0, bottom: 10, left: 0 },
318
+ devicePixelRatio: 2,
319
+ };
320
+
321
+ setDesktopHostBridge({
322
+ windows: {
323
+ open(config) {
324
+ secondarySnapshot = {
325
+ ...secondarySnapshot,
326
+ rootId: config.rootId,
327
+ title: config.title ?? secondarySnapshot.title,
328
+ };
329
+ return secondarySnapshot;
330
+ },
331
+ getSnapshot(windowId) {
332
+ return windowId === 'secondary'
333
+ ? secondarySnapshot
334
+ : null;
335
+ },
336
+ call: vi.fn(),
337
+ },
338
+ });
339
+
340
+ await act(async () => {
341
+ await windows.open({
342
+ title: 'Inspector',
343
+ render: () => <SecondaryWindowProbe />,
344
+ });
345
+ });
346
+
347
+ expect(windows.count).toBe(2);
348
+ expect(getRenderedTextForRoot(1)).toBe('secondary:640x480|6/0/10/0');
349
+
350
+ secondarySnapshot = {
351
+ ...secondarySnapshot,
352
+ size: { width: 900, height: 700 },
353
+ };
354
+
355
+ await act(async () => {
356
+ emitDesktopHostEvent('windows', 'windowChanged', secondarySnapshot);
357
+ });
358
+
359
+ expect(getRenderedTextForRoot(1)).toBe('secondary:900x700|6/0/10/0');
360
+ });
361
+
362
+ it('tears down secondary roots when host close confirmation observes a missing snapshot', async () => {
363
+ vi.useFakeTimers();
364
+
365
+ let closed = false;
366
+ const bridgeCall = vi.fn((windowId: string, method: string) => {
367
+ if (windowId === 'secondary' && method === 'close') {
368
+ closed = true;
369
+ }
370
+ });
371
+
372
+ setDesktopHostBridge({
373
+ windows: {
374
+ open(config) {
375
+ return {
376
+ id: 'secondary',
377
+ rootId: config.rootId,
378
+ title: 'Inspector',
379
+ size: { width: 640, height: 480 },
380
+ position: { x: 120, y: 80 },
381
+ state: 'normal' as const,
382
+ isFocused: true,
383
+ isVisible: true,
384
+ display: {
385
+ id: 'display-2',
386
+ size: { width: 1440, height: 900 },
387
+ workArea: { x: 0, y: 0, width: 1440, height: 860 },
388
+ scaleFactor: 2,
389
+ },
390
+ safeAreaInsets: { top: 0, right: 0, bottom: 0, left: 0 },
391
+ devicePixelRatio: 2,
392
+ };
393
+ },
394
+ getSnapshot(windowId) {
395
+ if (windowId !== 'secondary' || closed) {
396
+ return null;
397
+ }
398
+
399
+ return {
400
+ id: 'secondary',
401
+ rootId: 1,
402
+ title: 'Inspector',
403
+ size: { width: 640, height: 480 },
404
+ position: { x: 120, y: 80 },
405
+ state: 'normal' as const,
406
+ isFocused: true,
407
+ isVisible: true,
408
+ display: {
409
+ id: 'display-2',
410
+ size: { width: 1440, height: 900 },
411
+ workArea: { x: 0, y: 0, width: 1440, height: 860 },
412
+ scaleFactor: 2,
413
+ },
414
+ safeAreaInsets: { top: 0, right: 0, bottom: 0, left: 0 },
415
+ devicePixelRatio: 2,
416
+ };
417
+ },
418
+ call: bridgeCall,
419
+ },
420
+ });
421
+
422
+ let secondaryWindow = windows.get('secondary');
423
+ expect(secondaryWindow).toBeUndefined();
424
+
425
+ await act(async () => {
426
+ secondaryWindow = await windows.open({
427
+ title: 'Inspector',
428
+ render: () => <SecondaryWindowProbe />,
429
+ });
430
+ });
431
+
432
+ expect(windows.count).toBe(2);
433
+ expect(getRootNode(1)).not.toBeNull();
434
+
435
+ await act(async () => {
436
+ secondaryWindow?.close();
437
+ vi.runAllTimers();
438
+ });
439
+
440
+ expect(bridgeCall).toHaveBeenCalledWith('secondary', 'close');
441
+ expect(windows.count).toBe(1);
442
+ expect(windows.get('secondary')).toBeUndefined();
443
+ expect(getRootNode(1)).toBeNull();
444
+
445
+ vi.useRealTimers();
446
+ });
447
+ });
@@ -0,0 +1,68 @@
1
+ /**
2
+ * RFC 0043 Adapter Contract
3
+ *
4
+ * These types document the shared renderer surface that every Exact adapter or
5
+ * compatibility layer targets. The implementation remains in `host-ops.ts`,
6
+ * but centralizing the contract here gives tests and downstream adapters a
7
+ * stable import path.
8
+ */
9
+
10
+ import type { ElementNode, RootNode, TextNode } from './nodes/node.js';
11
+ import type { TagConfig } from './tags/index.js';
12
+
13
+ /**
14
+ * Shared host operations available to framework adapters.
15
+ */
16
+ export interface ExactAdapterHostOps {
17
+ createInstance(type: string, props: Record<string, unknown>): ElementNode;
18
+ createTextInstance(text: string): TextNode;
19
+ appendChild(parent: ElementNode | RootNode, child: ElementNode | TextNode): void;
20
+ insertBefore(
21
+ parent: ElementNode | RootNode,
22
+ child: ElementNode | TextNode,
23
+ beforeChild: ElementNode | TextNode,
24
+ ): void;
25
+ removeChild(parent: ElementNode | RootNode, child: ElementNode | TextNode): void;
26
+ detachChild(parent: ElementNode | RootNode, child: ElementNode | TextNode): void;
27
+ updateInstanceProps(
28
+ instance: ElementNode,
29
+ oldProps: Record<string, unknown>,
30
+ newProps: Record<string, unknown>,
31
+ ): void;
32
+ updateTextContent(instance: TextNode, text: string): void;
33
+ commitBatch(root: RootNode): void;
34
+ createRoot(rootId?: number, owner?: string): RootNode;
35
+ destroyRoot(rootId: number): void;
36
+ clearContainer(root: RootNode): void;
37
+ registerHandler(fn: Function): number;
38
+ unregisterHandler(
39
+ id: number,
40
+ nodeId?: number,
41
+ eventType?: number,
42
+ rootId?: number,
43
+ ): void;
44
+ getTagConfig(tag: string): TagConfig | null;
45
+ }
46
+
47
+ /**
48
+ * Public root handle every stateful adapter exposes.
49
+ */
50
+ export interface ExactAdapterRoot<TTree = unknown> {
51
+ readonly rootId: number;
52
+ render(tree: TTree): void;
53
+ unmount(): void;
54
+ }
55
+
56
+ /**
57
+ * Public adapter lifecycle surface.
58
+ */
59
+ export interface ExactAdapterOps<
60
+ TTree = unknown,
61
+ TRoot extends ExactAdapterRoot<TTree> = ExactAdapterRoot<TTree>,
62
+ > {
63
+ readonly name: string;
64
+ render(tree: TTree): void | (() => void);
65
+ createExactRoot(rootId: number): TRoot;
66
+ reset(): void;
67
+ unmount?(): void;
68
+ }