@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,454 @@
1
+ import React from 'react';
2
+ import { renderToStaticMarkup } from 'react-dom/server';
3
+ import { afterEach, describe, expect, it } from 'vitest';
4
+
5
+ async function importWebImage() {
6
+ (globalThis as { document?: unknown }).document = {};
7
+ delete (globalThis as { __exactPlatform?: unknown }).__exactPlatform;
8
+ const module = await import('../web-primitives.js?web-test');
9
+ return module.Image;
10
+ }
11
+
12
+ async function importWebPrimitives() {
13
+ (globalThis as { document?: unknown }).document = {};
14
+ delete (globalThis as { __exactPlatform?: unknown }).__exactPlatform;
15
+ return import('../web-primitives.js?web-test');
16
+ }
17
+
18
+ async function importSsrWebPrimitives() {
19
+ delete (globalThis as { document?: unknown }).document;
20
+ delete (globalThis as { __exactPlatform?: unknown }).__exactPlatform;
21
+ (globalThis as { __exactWebSsr?: boolean }).__exactWebSsr = true;
22
+ return import('../web-primitives.js?ssr-test');
23
+ }
24
+
25
+ afterEach(() => {
26
+ delete (globalThis as { document?: unknown }).document;
27
+ delete (globalThis as { __exactPlatform?: unknown }).__exactPlatform;
28
+ delete (globalThis as { __exactWebSsr?: boolean }).__exactWebSsr;
29
+ });
30
+
31
+ describe('WebImage', () => {
32
+ it('renders picture sources for multi-candidate image sources', async () => {
33
+ const Image = await importWebImage();
34
+ const html = renderToStaticMarkup(
35
+ React.createElement(Image, {
36
+ source: [
37
+ { uri: 'https://cdn.example.com/hero.avif', pixelDensity: 1, type: 'image/avif' },
38
+ { uri: 'https://cdn.example.com/hero@2x.avif', pixelDensity: 2, type: 'image/avif' },
39
+ { uri: 'https://cdn.example.com/hero.jpg', pixelDensity: 1 },
40
+ ],
41
+ alt: 'Hero image',
42
+ objectFit: 'contain',
43
+ objectPosition: 'top center',
44
+ loading: 'lazy',
45
+ }),
46
+ );
47
+
48
+ expect(html).toContain('<picture>');
49
+ expect(html).toContain('type="image/avif"');
50
+ expect(html).toContain('srcSet="https://cdn.example.com/hero.avif 1x, https://cdn.example.com/hero@2x.avif 2x"');
51
+ expect(html).toContain('object-fit:contain');
52
+ expect(html).toContain('object-position:top center');
53
+ expect(html).toContain('loading="lazy"');
54
+ expect(html).toContain('alt="Hero image"');
55
+ });
56
+
57
+ it('renders decorative images with placeholder background styling', async () => {
58
+ const Image = await importWebImage();
59
+ const html = renderToStaticMarkup(
60
+ React.createElement(Image, {
61
+ source: 'https://example.com/photo.jpg',
62
+ decorative: true,
63
+ placeholder: { color: '#ff0000' },
64
+ }),
65
+ );
66
+
67
+ expect(html).toContain('alt=""');
68
+ expect(html).toContain('aria-hidden="true"');
69
+ expect(html).toContain('background-color:#ff0000');
70
+ });
71
+
72
+ it('renders theme-aware image metadata for static appearance controllers', async () => {
73
+ const Image = await importWebImage();
74
+ const html = renderToStaticMarkup(
75
+ React.createElement(Image, {
76
+ source: {
77
+ light: '/assets/graph.light.png',
78
+ dark: '/assets/graph.dark.png',
79
+ },
80
+ alt: 'Graph',
81
+ }),
82
+ );
83
+
84
+ expect(html).toContain('src="/assets/graph.light.png"');
85
+ expect(html).toContain('data-exact-theme-image="color-scheme"');
86
+ expect(html).toContain('data-light-src="/assets/graph.light.png"');
87
+ expect(html).toContain('data-dark-src="/assets/graph.dark.png"');
88
+ });
89
+
90
+ it('renders ThumbHash-only sources as an inline data URL', async () => {
91
+ const Image = await importWebImage();
92
+ const html = renderToStaticMarkup(
93
+ React.createElement(Image, {
94
+ source: { thumbhash: '3OcRJYB4d3h/iIeHeEh3eIhw+j3A' },
95
+ alt: 'Thumb preview',
96
+ style: { width: 120, height: 80 },
97
+ }),
98
+ );
99
+
100
+ expect(html).toContain('src="data:image/png;base64,');
101
+ expect(html).toContain('alt="Thumb preview"');
102
+ expect(html).toContain('width="120"');
103
+ expect(html).toContain('height="80"');
104
+ });
105
+
106
+ it('delegates SVG image sources to the SVG web path', async () => {
107
+ const Image = await importWebImage();
108
+ const html = renderToStaticMarkup(
109
+ React.createElement(Image, {
110
+ source: 'https://cdn.example.com/icon.svg',
111
+ alt: 'Vector icon',
112
+ objectPosition: 'top right',
113
+ }),
114
+ );
115
+
116
+ expect(html).toContain('src="https://cdn.example.com/icon.svg"');
117
+ expect(html).toContain('object-fit:contain');
118
+ expect(html).toContain('object-position:top right');
119
+ expect(html).toContain('alt="Vector icon"');
120
+ });
121
+ });
122
+
123
+ describe('WebText', () => {
124
+ it('renders authored heading semantics through shared attrs lowering', async () => {
125
+ const { Text } = await importWebPrimitives();
126
+ const html = renderToStaticMarkup(
127
+ React.createElement(Text, {
128
+ accessibilityRole: 'heading',
129
+ accessibilityHeadingLevel: 2,
130
+ testID: 'title',
131
+ children: 'Stations',
132
+ }),
133
+ );
134
+
135
+ expect(html).toContain('<h2');
136
+ expect(html).toContain('data-testid="title"');
137
+ expect(html).toContain('>Stations</h2>');
138
+ expect(html).not.toContain('role="heading"');
139
+ expect(html).not.toContain('aria-level');
140
+ });
141
+
142
+ it('honors selectable text styling', async () => {
143
+ const { Text } = await importWebPrimitives();
144
+ const html = renderToStaticMarkup(
145
+ React.createElement(Text, {
146
+ selectable: false,
147
+ children: 'No selection',
148
+ }),
149
+ );
150
+
151
+ expect(html).toContain('user-select:none');
152
+ });
153
+
154
+ it('inherits pressable selection opt-out by default', async () => {
155
+ const { Text, Pressable } = await importWebPrimitives();
156
+ const html = renderToStaticMarkup(
157
+ React.createElement(
158
+ Pressable,
159
+ null,
160
+ React.createElement(Text, { children: 'Button label' }),
161
+ ),
162
+ );
163
+
164
+ expect(html).toContain('user-select:none');
165
+ });
166
+
167
+ it('lets text opt back into selection inside a pressable', async () => {
168
+ const { Text, Pressable } = await importWebPrimitives();
169
+ const html = renderToStaticMarkup(
170
+ React.createElement(
171
+ Pressable,
172
+ null,
173
+ React.createElement(Text, {
174
+ selectable: true,
175
+ children: 'Override label',
176
+ }),
177
+ ),
178
+ );
179
+
180
+ expect(html).toContain('user-select:text');
181
+ });
182
+
183
+ it('serializes selectable string modes', async () => {
184
+ const { Text } = await importWebPrimitives();
185
+ const html = renderToStaticMarkup(
186
+ React.createElement(Text, {
187
+ selectable: 'all',
188
+ children: 'Select all',
189
+ }),
190
+ );
191
+
192
+ expect(html).toContain('user-select:all');
193
+ });
194
+
195
+ it('uses CSS ellipsis for single-line clamps', async () => {
196
+ const { Text } = await importWebPrimitives();
197
+ const html = renderToStaticMarkup(
198
+ React.createElement(Text, {
199
+ numberOfLines: 1,
200
+ style: {
201
+ width: 220,
202
+ },
203
+ children: 'Single line clamp',
204
+ }),
205
+ );
206
+
207
+ expect(html).toContain('display:block');
208
+ expect(html).toContain('text-overflow:ellipsis');
209
+ expect(html).toContain('white-space:nowrap');
210
+ });
211
+
212
+ it('serializes numeric lineHeight as an absolute pixel value', async () => {
213
+ const { Text } = await importWebPrimitives();
214
+ const html = renderToStaticMarkup(
215
+ React.createElement(Text, {
216
+ style: {
217
+ fontSize: 13,
218
+ lineHeight: 18,
219
+ },
220
+ children: 'Absolute leading',
221
+ }),
222
+ );
223
+
224
+ expect(html).toContain('line-height:18px');
225
+ });
226
+ });
227
+
228
+ describe('Lowercase web primitives', () => {
229
+ it('renders landmarks and labelled inputs through shared attrs lowering', async () => {
230
+ const { TextInput, View } = await importWebPrimitives();
231
+ const html = renderToStaticMarkup(
232
+ React.createElement(
233
+ View,
234
+ {
235
+ accessibilityRole: 'navigation',
236
+ accessibilityLabel: 'Primary navigation',
237
+ testID: 'nav',
238
+ },
239
+ React.createElement(TextInput, {
240
+ nativeID: 'email',
241
+ accessibilityLabelledBy: 'email-label',
242
+ defaultValue: '',
243
+ testID: 'email',
244
+ }),
245
+ ),
246
+ );
247
+
248
+ expect(html).toContain('<nav');
249
+ expect(html).toContain('aria-label="Primary navigation"');
250
+ expect(html).toContain('data-testid="nav"');
251
+ expect(html).toContain('id="email"');
252
+ expect(html).toContain('aria-labelledby="email-label"');
253
+ expect(html).toContain('data-testid="email"');
254
+ });
255
+
256
+ it('renders DOM wrappers during explicit web SSR without a document', async () => {
257
+ const { Pressable, ScrollView, Text, View } = await importSsrWebPrimitives();
258
+ const html = renderToStaticMarkup(
259
+ React.createElement(
260
+ ScrollView,
261
+ {
262
+ testID: 'ssr-scroll',
263
+ contentContainerStyle: { padding: 12 },
264
+ },
265
+ React.createElement(
266
+ View,
267
+ {
268
+ testID: 'ssr-view',
269
+ },
270
+ React.createElement(Text, { testID: 'ssr-text' }, 'SSR body'),
271
+ React.createElement(Pressable, { testID: 'ssr-pressable' }, 'Press'),
272
+ ),
273
+ ),
274
+ );
275
+
276
+ expect(html).toContain('data-testid="ssr-scroll"');
277
+ expect(html).toContain('data-testid="ssr-view"');
278
+ expect(html).toContain('data-testid="ssr-text"');
279
+ expect(html).toContain('data-testid="ssr-pressable"');
280
+ expect(html).toContain('role="button"');
281
+ expect(html).not.toContain('<View');
282
+ expect(html).not.toContain('<Text');
283
+ expect(html).not.toContain('<ScrollView');
284
+ expect(html).not.toContain('<Pressable');
285
+ expect(html).not.toContain('testID=');
286
+ expect(html).not.toContain('contentContainerStyle=');
287
+ });
288
+
289
+ it('renders lowercase div with row flex defaults', async () => {
290
+ const { div: Div } = await importWebPrimitives();
291
+ const html = renderToStaticMarkup(
292
+ React.createElement(Div, {
293
+ testID: 'lowercase-div',
294
+ style: { gap: 12 },
295
+ children: 'Row container',
296
+ }),
297
+ );
298
+
299
+ expect(html).toContain('<div');
300
+ expect(html).toContain('data-testid="lowercase-div"');
301
+ expect(html).toContain('flex-direction:row');
302
+ expect(html).toContain('gap:12px');
303
+ });
304
+
305
+ it('renders lowercase input through the web text-input wrapper', async () => {
306
+ const { input: Input } = await importWebPrimitives();
307
+ const html = renderToStaticMarkup(
308
+ React.createElement(Input, {
309
+ multiline: true,
310
+ placeholder: 'Notes',
311
+ numberOfLines: 4,
312
+ testID: 'lowercase-input',
313
+ }),
314
+ );
315
+
316
+ expect(html).toContain('<textarea');
317
+ expect(html).toContain('placeholder="Notes"');
318
+ expect(html).toContain('rows="4"');
319
+ expect(html).toContain('data-testid="lowercase-input"');
320
+ });
321
+
322
+ it('renders lowercase img with direct web-style props', async () => {
323
+ const { img: Img } = await importWebPrimitives();
324
+ const html = renderToStaticMarkup(
325
+ React.createElement(Img, {
326
+ src: 'https://cdn.example.com/cover.png',
327
+ alt: 'Cover art',
328
+ objectFit: 'cover',
329
+ objectPosition: 'top center',
330
+ testID: 'lowercase-img',
331
+ }),
332
+ );
333
+
334
+ expect(html).toContain('<img');
335
+ expect(html).toContain('src="https://cdn.example.com/cover.png"');
336
+ expect(html).toContain('alt="Cover art"');
337
+ expect(html).toContain('object-fit:cover');
338
+ expect(html).toContain('object-position:top center');
339
+ expect(html).toContain('data-testid="lowercase-img"');
340
+ });
341
+ });
342
+
343
+ describe('Flex-shrink RN parity (ENG-22206)', () => {
344
+ // CSS flexbox defaults flex-shrink to 1; RN/Yoga and the kernel/Taffy path
345
+ // default it to 0. Without flex-shrink:0, a bare View/row inside an
346
+ // overflowing flex column shrinks below its content (to min-height:0) and
347
+ // collapses to height 0 — the ENG-22199 bug, in the React-tier renderer.
348
+ it('defaults View/row containers to flex-shrink:0', async () => {
349
+ const { View, button: Button, div: Div } = await importWebPrimitives();
350
+ expect(
351
+ renderToStaticMarkup(React.createElement(View, { testID: 'v' })),
352
+ ).toContain('flex-shrink:0');
353
+ expect(
354
+ renderToStaticMarkup(React.createElement(Button, { testID: 'b' }, 'Go')),
355
+ ).toContain('flex-shrink:0');
356
+ expect(
357
+ renderToStaticMarkup(React.createElement(Div, { testID: 'd' })),
358
+ ).toContain('flex-shrink:0');
359
+ });
360
+
361
+ it('keeps ScrollView shrinkable so it bounds and scrolls', async () => {
362
+ const { ScrollView } = await importWebPrimitives();
363
+ const html = renderToStaticMarkup(React.createElement(ScrollView, { testID: 's' }));
364
+ expect(html).toContain('flex-shrink:1');
365
+ expect(html).not.toContain('flex-shrink:0');
366
+ });
367
+
368
+ it('lets an explicit flexShrink in component style win over the default', async () => {
369
+ const { View } = await importWebPrimitives();
370
+ const html = renderToStaticMarkup(
371
+ React.createElement(View, { testID: 'v', style: { flexShrink: 1 } }),
372
+ );
373
+ expect(html).toContain('flex-shrink:1');
374
+ expect(html).not.toContain('flex-shrink:0');
375
+ });
376
+ });
377
+
378
+ describe('WebButton', () => {
379
+ it('renders a semantic button with row defaults and mapped accessibility props', async () => {
380
+ const { button: Button } = await importWebPrimitives();
381
+ const html = renderToStaticMarkup(
382
+ React.createElement(
383
+ Button,
384
+ {
385
+ testID: 'facet-button',
386
+ accessibilityRole: 'button',
387
+ accessibilityLabel: 'Launch',
388
+ disabled: true,
389
+ style: {
390
+ paddingLeft: 12,
391
+ paddingRight: 12,
392
+ },
393
+ },
394
+ 'Launch',
395
+ ),
396
+ );
397
+
398
+ expect(html).toContain('<button');
399
+ expect(html).toContain('type="button"');
400
+ expect(html).toContain('data-testid="facet-button"');
401
+ expect(html).toContain('aria-label="Launch"');
402
+ expect(html).toContain('disabled=""');
403
+ expect(html).toContain('flex-direction:row');
404
+ expect(html).not.toContain('role="button"');
405
+ expect(html).not.toContain('aria-disabled');
406
+ });
407
+
408
+ it('honors explicit focusability for roving-focus widgets', async () => {
409
+ const { button: Button } = await importWebPrimitives();
410
+ const html = renderToStaticMarkup(
411
+ React.createElement(
412
+ Button,
413
+ {
414
+ focusable: false,
415
+ children: 'Inactive option',
416
+ },
417
+ ),
418
+ );
419
+
420
+ expect(html).toContain('tabindex="-1"');
421
+ });
422
+ });
423
+
424
+ describe('WebSvg', () => {
425
+ it('renders URI-backed SVGs as img tags by default', async () => {
426
+ const { Svg } = await importWebPrimitives();
427
+ const html = renderToStaticMarkup(
428
+ React.createElement(Svg, {
429
+ source: 'https://cdn.example.com/diagram.svg',
430
+ alt: 'System diagram',
431
+ objectFit: 'cover',
432
+ }),
433
+ );
434
+
435
+ expect(html).toContain('<img');
436
+ expect(html).toContain('src="https://cdn.example.com/diagram.svg"');
437
+ expect(html).toContain('object-fit:cover');
438
+ expect(html).toContain('alt="System diagram"');
439
+ });
440
+
441
+ it('renders inline SVG sources through the inline host wrapper', async () => {
442
+ const { Svg } = await importWebPrimitives();
443
+ const html = renderToStaticMarkup(
444
+ React.createElement(Svg, {
445
+ source: '<svg viewBox="0 0 24 24"><rect width="24" height="24" /></svg>',
446
+ alt: 'Square',
447
+ testID: 'inline-svg',
448
+ }),
449
+ );
450
+
451
+ expect(html).toContain('<span');
452
+ expect(html).toContain('data-testid="inline-svg"');
453
+ });
454
+ });