@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.
- package/package.json +118 -0
- package/src/__tests__/adapter-window-state.test.tsx +190 -0
- package/src/__tests__/attrs.test.ts +157 -0
- package/src/__tests__/classname.test.ts +332 -0
- package/src/__tests__/color.test.ts +169 -0
- package/src/__tests__/dom-mirror.test.ts +682 -0
- package/src/__tests__/dom-shim.test.ts +274 -0
- package/src/__tests__/fixtures/SvelteCounter.svelte +7 -0
- package/src/__tests__/fixtures/SvelteInput.svelte +8 -0
- package/src/__tests__/host-config.test.ts +51 -0
- package/src/__tests__/host-ops.test.ts +2234 -0
- package/src/__tests__/image-source.test.ts +135 -0
- package/src/__tests__/liquid-glass.test.ts +72 -0
- package/src/__tests__/multi-root.test.ts +118 -0
- package/src/__tests__/native-view-events.test.ts +102 -0
- package/src/__tests__/nodes.test.ts +399 -0
- package/src/__tests__/normalize.test.ts +576 -0
- package/src/__tests__/paragraph-lowering.test.tsx +144 -0
- package/src/__tests__/props.test.ts +518 -0
- package/src/__tests__/protocol-encoder.test.ts +732 -0
- package/src/__tests__/protocol-fixture-bytes.test.ts +41 -0
- package/src/__tests__/reconciler.test.tsx +241 -0
- package/src/__tests__/svelte-adapter.test.ts +166 -0
- package/src/__tests__/svg-source.test.ts +71 -0
- package/src/__tests__/tags.test.ts +354 -0
- package/src/__tests__/toggle.test.ts +441 -0
- package/src/__tests__/transitions.test.ts +106 -0
- package/src/__tests__/web-primitives.test.tsx +454 -0
- package/src/__tests__/window-hooks.test.tsx +447 -0
- package/src/adapter-contract.ts +68 -0
- package/src/attrs.ts +596 -0
- package/src/classname-contract.ts +87 -0
- package/src/classname-resolve.ts +553 -0
- package/src/classname-runtime.ts +29 -0
- package/src/components.ts +214 -0
- package/src/css-variable-context.ts +83 -0
- package/src/dom-hydration.ts +160 -0
- package/src/dom-mirror.ts +1459 -0
- package/src/dom-shim.ts +1736 -0
- package/src/group-context.ts +69 -0
- package/src/host-config.ts +431 -0
- package/src/host-ops.ts +3167 -0
- package/src/image-source.native.ts +703 -0
- package/src/image-source.ts +554 -0
- package/src/index.ts +278 -0
- package/src/inspector-runtime.ts +244 -0
- package/src/inspector.ts +3570 -0
- package/src/jsx-augmentations.ts +54 -0
- package/src/keyboard-avoidance.ts +217 -0
- package/src/native-primitives.ts +43 -0
- package/src/native-view-events.ts +322 -0
- package/src/native-view.ts +60 -0
- package/src/nodes/index.ts +41 -0
- package/src/nodes/node.ts +531 -0
- package/src/peer-context.ts +100 -0
- package/src/primitives.native.ts +8 -0
- package/src/primitives.ts +8 -0
- package/src/props/index.ts +14 -0
- package/src/props/normalize.ts +816 -0
- package/src/protocol/encoder.ts +940 -0
- package/src/protocol/index.ts +33 -0
- package/src/reconciler.ts +581 -0
- package/src/runtime.ts +11 -0
- package/src/safe-area.ts +543 -0
- package/src/solid.ts +490 -0
- package/src/style/color.js +1 -0
- package/src/style/color.ts +15 -0
- package/src/style/index.js +1 -0
- package/src/style/index.ts +22 -0
- package/src/style/normalize.js +1 -0
- package/src/style/normalize.ts +1426 -0
- package/src/svelte.ts +349 -0
- package/src/svg-source.ts +222 -0
- package/src/tags/index.ts +21 -0
- package/src/tags/tag-map.ts +289 -0
- package/src/text/paragraph-lowering.ts +310 -0
- package/src/types.ts +1175 -0
- package/src/vue.ts +535 -0
- package/src/web-host.ts +19 -0
- package/src/web-primitives.ts +1654 -0
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
emptyKeyboardState,
|
|
4
|
+
emptySafeAreaRegions,
|
|
5
|
+
MAIN_WINDOW_ID,
|
|
6
|
+
resetWindowStateStoreForTests,
|
|
7
|
+
setWindowSnapshot,
|
|
8
|
+
zeroInsets,
|
|
9
|
+
} from '@exact/core';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
BUFFER_HEADER_SIZE,
|
|
13
|
+
OP_HEADER_SIZE,
|
|
14
|
+
OpCode,
|
|
15
|
+
PropId,
|
|
16
|
+
StyleMask,
|
|
17
|
+
} from '../../../exact-core/src/protocol/opcodes.js';
|
|
18
|
+
import { appendChild as appendNodeChild, createElementNode } from '../nodes/node.js';
|
|
19
|
+
import { createInstance } from '../host-ops.js';
|
|
20
|
+
import { createNativeViewComponent } from '../native-view.js';
|
|
21
|
+
import { normalizeProps } from '../props/normalize.js';
|
|
22
|
+
import { _clearNativeViewTagsForTesting } from '../tags/tag-map.js';
|
|
23
|
+
import {
|
|
24
|
+
createProtocolEncoder,
|
|
25
|
+
dispatchProtocol,
|
|
26
|
+
encodeProps,
|
|
27
|
+
getScreenDimensions,
|
|
28
|
+
setSvgColors,
|
|
29
|
+
setSvgObjectPosition,
|
|
30
|
+
setSvgPixelDensity,
|
|
31
|
+
setSvgSource,
|
|
32
|
+
} from '../protocol/encoder.js';
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
_clearNativeViewTagsForTesting();
|
|
36
|
+
resetWindowStateStoreForTests();
|
|
37
|
+
delete (
|
|
38
|
+
globalThis as typeof globalThis & {
|
|
39
|
+
exact?: unknown;
|
|
40
|
+
}
|
|
41
|
+
).exact;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function getEncodedStringProp(data: Uint8Array, targetPropId: PropId): string {
|
|
45
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
46
|
+
let offset = BUFFER_HEADER_SIZE;
|
|
47
|
+
|
|
48
|
+
while (offset < data.length) {
|
|
49
|
+
const propId = view.getUint16(offset + OP_HEADER_SIZE, true);
|
|
50
|
+
const payloadLength = view.getUint16(offset + 5, true);
|
|
51
|
+
if (propId === targetPropId) {
|
|
52
|
+
const stringLength = view.getUint16(offset + OP_HEADER_SIZE + 2, true);
|
|
53
|
+
const start = offset + OP_HEADER_SIZE + 4;
|
|
54
|
+
const bytes = Array.from({ length: stringLength }, (_, index) => view.getUint8(start + index));
|
|
55
|
+
return String.fromCharCode(...bytes);
|
|
56
|
+
}
|
|
57
|
+
offset += OP_HEADER_SIZE + payloadLength;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new Error(`Missing prop ${targetPropId}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('protocol encoder', () => {
|
|
64
|
+
it('encodes scroll indicator visibility props for ScrollView nodes', () => {
|
|
65
|
+
const instance = createElementNode('scroll', 'ScrollView', true, {
|
|
66
|
+
showsVerticalScrollIndicator: false,
|
|
67
|
+
});
|
|
68
|
+
instance.props = normalizeProps('scroll', {
|
|
69
|
+
showsVerticalScrollIndicator: false,
|
|
70
|
+
}, true);
|
|
71
|
+
|
|
72
|
+
const encoder = createProtocolEncoder(1024);
|
|
73
|
+
encodeProps(encoder, instance);
|
|
74
|
+
|
|
75
|
+
const data = encoder.getUint8Array();
|
|
76
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
77
|
+
|
|
78
|
+
expect(view.getUint16(4, true)).toBe(1);
|
|
79
|
+
expect(view.getUint8(BUFFER_HEADER_SIZE)).toBe(OpCode.SetProp);
|
|
80
|
+
expect(view.getUint32(BUFFER_HEADER_SIZE + 1, true)).toBe(instance.id);
|
|
81
|
+
expect(view.getUint16(BUFFER_HEADER_SIZE + OP_HEADER_SIZE, true)).toBe(PropId.ShowsScrollIndicator);
|
|
82
|
+
expect(view.getUint16(BUFFER_HEADER_SIZE + OP_HEADER_SIZE + 2, true)).toBe(5);
|
|
83
|
+
|
|
84
|
+
const textBytes = Array.from({ length: 5 }, (_, index) =>
|
|
85
|
+
view.getUint8(BUFFER_HEADER_SIZE + OP_HEADER_SIZE + 4 + index),
|
|
86
|
+
);
|
|
87
|
+
expect(String.fromCharCode(...textBytes)).toBe('false');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('encodes scroll indicator visibility props for List nodes', () => {
|
|
91
|
+
const instance = createElementNode('list', 'List', true, {
|
|
92
|
+
showsVerticalScrollIndicator: false,
|
|
93
|
+
});
|
|
94
|
+
instance.props = normalizeProps('list', {
|
|
95
|
+
showsVerticalScrollIndicator: false,
|
|
96
|
+
}, true);
|
|
97
|
+
|
|
98
|
+
const encoder = createProtocolEncoder(1024);
|
|
99
|
+
encodeProps(encoder, instance);
|
|
100
|
+
|
|
101
|
+
expect(getEncodedStringProp(encoder.getUint8Array(), PropId.ShowsScrollIndicator)).toBe('false');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('encodes SVG prop ids in the expected order', () => {
|
|
105
|
+
const encoder = createProtocolEncoder(1024);
|
|
106
|
+
|
|
107
|
+
setSvgSource(encoder, 7, '<svg viewBox="0 0 24 24"></svg>');
|
|
108
|
+
setSvgColors(encoder, 7, '{"--primary":"#007aff"}');
|
|
109
|
+
setSvgPixelDensity(encoder, 7, '2');
|
|
110
|
+
setSvgObjectPosition(encoder, 7, 'right bottom');
|
|
111
|
+
|
|
112
|
+
const data = encoder.getUint8Array();
|
|
113
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
114
|
+
|
|
115
|
+
const opOffsets: number[] = [];
|
|
116
|
+
let offset = BUFFER_HEADER_SIZE;
|
|
117
|
+
while (offset < data.length) {
|
|
118
|
+
opOffsets.push(offset);
|
|
119
|
+
const payloadLength = view.getUint16(offset + 5, true);
|
|
120
|
+
offset += OP_HEADER_SIZE + payloadLength;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
expect(opOffsets).toHaveLength(4);
|
|
124
|
+
expect(view.getUint16(opOffsets[0] + OP_HEADER_SIZE, true)).toBe(PropId.SvgSource);
|
|
125
|
+
expect(view.getUint16(opOffsets[1] + OP_HEADER_SIZE, true)).toBe(PropId.SvgColors);
|
|
126
|
+
expect(view.getUint16(opOffsets[2] + OP_HEADER_SIZE, true)).toBe(PropId.SvgPixelDensity);
|
|
127
|
+
expect(view.getUint16(opOffsets[3] + OP_HEADER_SIZE, true)).toBe(PropId.SvgObjectPosition);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('encodes normalized Svg props through encodeProps', () => {
|
|
131
|
+
const instance = createElementNode('svg', 'Svg', true, {
|
|
132
|
+
source: '<svg viewBox="0 0 24 24"></svg>',
|
|
133
|
+
objectPosition: 'left top',
|
|
134
|
+
pixelDensity: 3,
|
|
135
|
+
colors: {
|
|
136
|
+
'--primary': '#ff9500',
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
instance.props = normalizeProps('svg', {
|
|
140
|
+
source: '<svg viewBox="0 0 24 24"></svg>',
|
|
141
|
+
objectPosition: 'left top',
|
|
142
|
+
pixelDensity: 3,
|
|
143
|
+
colors: {
|
|
144
|
+
'--primary': '#ff9500',
|
|
145
|
+
},
|
|
146
|
+
}, true);
|
|
147
|
+
|
|
148
|
+
const encoder = createProtocolEncoder(1024);
|
|
149
|
+
encodeProps(encoder, instance);
|
|
150
|
+
|
|
151
|
+
const data = encoder.getUint8Array();
|
|
152
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
153
|
+
|
|
154
|
+
const propIds: number[] = [];
|
|
155
|
+
let offset = BUFFER_HEADER_SIZE;
|
|
156
|
+
while (offset < data.length) {
|
|
157
|
+
propIds.push(view.getUint16(offset + OP_HEADER_SIZE, true));
|
|
158
|
+
const payloadLength = view.getUint16(offset + 5, true);
|
|
159
|
+
offset += OP_HEADER_SIZE + payloadLength;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
expect(propIds).toContain(PropId.SvgSource);
|
|
163
|
+
expect(propIds).toContain(PropId.SvgColors);
|
|
164
|
+
expect(propIds).toContain(PropId.SvgPixelDensity);
|
|
165
|
+
expect(propIds).toContain(PropId.SvgObjectPosition);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('encodes TextInput value as text content for native readback', () => {
|
|
169
|
+
const instance = createElementNode('input', 'TextInput', true, {
|
|
170
|
+
value: 'hello terminal',
|
|
171
|
+
placeholder: 'Type here',
|
|
172
|
+
});
|
|
173
|
+
instance.props = normalizeProps('input', {
|
|
174
|
+
value: 'hello terminal',
|
|
175
|
+
placeholder: 'Type here',
|
|
176
|
+
}, true);
|
|
177
|
+
|
|
178
|
+
const encoder = createProtocolEncoder(1024);
|
|
179
|
+
encodeProps(encoder, instance);
|
|
180
|
+
|
|
181
|
+
const data = encoder.getUint8Array();
|
|
182
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
183
|
+
|
|
184
|
+
const propIds: number[] = [];
|
|
185
|
+
let offset = BUFFER_HEADER_SIZE;
|
|
186
|
+
while (offset < data.length) {
|
|
187
|
+
propIds.push(view.getUint16(offset + OP_HEADER_SIZE, true));
|
|
188
|
+
const payloadLength = view.getUint16(offset + 5, true);
|
|
189
|
+
offset += OP_HEADER_SIZE + payloadLength;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
expect(propIds).toContain(PropId.TextContent);
|
|
193
|
+
expect(propIds).toContain(PropId.Placeholder);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('encodes selectable text nodes as a string-backed prop', () => {
|
|
197
|
+
const instance = createElementNode('text', 'Text', true, {
|
|
198
|
+
selectable: true,
|
|
199
|
+
children: 'copy me',
|
|
200
|
+
});
|
|
201
|
+
instance.props = normalizeProps('text', {
|
|
202
|
+
selectable: true,
|
|
203
|
+
children: 'copy me',
|
|
204
|
+
}, true);
|
|
205
|
+
|
|
206
|
+
const encoder = createProtocolEncoder(1024);
|
|
207
|
+
encodeProps(encoder, instance);
|
|
208
|
+
|
|
209
|
+
const data = encoder.getUint8Array();
|
|
210
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
211
|
+
|
|
212
|
+
const propIds: number[] = [];
|
|
213
|
+
let offset = BUFFER_HEADER_SIZE;
|
|
214
|
+
while (offset < data.length) {
|
|
215
|
+
propIds.push(view.getUint16(offset + OP_HEADER_SIZE, true));
|
|
216
|
+
const payloadLength = view.getUint16(offset + 5, true);
|
|
217
|
+
offset += OP_HEADER_SIZE + payloadLength;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
expect(propIds).toContain(PropId.TextContent);
|
|
221
|
+
expect(propIds).toContain(PropId.Selectable);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('encodes selectable string modes as string-backed props', () => {
|
|
225
|
+
const instance = createElementNode('text', 'Text', true, {
|
|
226
|
+
selectable: 'all',
|
|
227
|
+
children: 'copy me',
|
|
228
|
+
});
|
|
229
|
+
instance.props = normalizeProps('text', {
|
|
230
|
+
selectable: 'all',
|
|
231
|
+
children: 'copy me',
|
|
232
|
+
}, true);
|
|
233
|
+
|
|
234
|
+
const encoder = createProtocolEncoder(1024);
|
|
235
|
+
encodeProps(encoder, instance);
|
|
236
|
+
|
|
237
|
+
expect(getEncodedStringProp(encoder.getUint8Array(), PropId.Selectable)).toBe('all');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('encodes selectionCopyText as a string-backed prop', () => {
|
|
241
|
+
const instance = createElementNode('video', 'VideoView', true, {
|
|
242
|
+
selectionCopyText: '[Intro clip]',
|
|
243
|
+
videoPlayerId: '7',
|
|
244
|
+
videoViewConfig: '{}',
|
|
245
|
+
});
|
|
246
|
+
instance.props = normalizeProps('video', {
|
|
247
|
+
selectionCopyText: '[Intro clip]',
|
|
248
|
+
videoPlayerId: '7',
|
|
249
|
+
videoViewConfig: '{}',
|
|
250
|
+
}, true);
|
|
251
|
+
|
|
252
|
+
const encoder = createProtocolEncoder(1024);
|
|
253
|
+
encodeProps(encoder, instance);
|
|
254
|
+
|
|
255
|
+
expect(getEncodedStringProp(encoder.getUint8Array(), PropId.SelectionCopyText)).toBe('[Intro clip]');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('encodes controlled TextInput selection offsets', () => {
|
|
259
|
+
const instance = createElementNode('input', 'TextInput', true, {
|
|
260
|
+
value: 'Millbrae',
|
|
261
|
+
selection: { start: 2, end: 6 },
|
|
262
|
+
});
|
|
263
|
+
instance.props = normalizeProps('input', {
|
|
264
|
+
value: 'Millbrae',
|
|
265
|
+
selection: { start: 2, end: 6 },
|
|
266
|
+
}, true);
|
|
267
|
+
|
|
268
|
+
const encoder = createProtocolEncoder(1024);
|
|
269
|
+
encodeProps(encoder, instance);
|
|
270
|
+
const data = encoder.getUint8Array();
|
|
271
|
+
|
|
272
|
+
expect(getEncodedStringProp(data, PropId.SelectionStart)).toBe('2');
|
|
273
|
+
expect(getEncodedStringProp(data, PropId.SelectionEnd)).toBe('6');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('encodes generic native-view metadata and serialized props', () => {
|
|
277
|
+
const ChartView = createNativeViewComponent<{
|
|
278
|
+
data: unknown;
|
|
279
|
+
title: string;
|
|
280
|
+
}>({
|
|
281
|
+
moduleName: 'com.exact.chart',
|
|
282
|
+
propKeys: ['data', 'title'],
|
|
283
|
+
selection: {
|
|
284
|
+
tier: 'atomic',
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const element = ChartView({
|
|
289
|
+
data: { points: [1, 2, 3] },
|
|
290
|
+
title: 'Revenue',
|
|
291
|
+
accessibilityLabel: 'Quarterly revenue chart',
|
|
292
|
+
});
|
|
293
|
+
const instance = createInstance(
|
|
294
|
+
String(element.type),
|
|
295
|
+
element.props as Record<string, unknown>,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const encoder = createProtocolEncoder(2048);
|
|
299
|
+
encodeProps(encoder, instance);
|
|
300
|
+
|
|
301
|
+
const data = encoder.getUint8Array();
|
|
302
|
+
expect(getEncodedStringProp(data, PropId.NativeViewModuleName)).toBe('com.exact.chart');
|
|
303
|
+
expect(getEncodedStringProp(data, PropId.NativeViewSelectionTier)).toBe('atomic');
|
|
304
|
+
expect(getEncodedStringProp(data, PropId.NativeViewProps)).toBe(
|
|
305
|
+
'{"data":{"points":[1,2,3]},"title":"Revenue"}',
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('encodes resolved lang as a string-backed prop', () => {
|
|
310
|
+
const instance = createElementNode('text', 'Text', true, {
|
|
311
|
+
children: 'مرحبا',
|
|
312
|
+
lang: 'ar-EG',
|
|
313
|
+
});
|
|
314
|
+
instance.props = normalizeProps('text', {
|
|
315
|
+
children: 'مرحبا',
|
|
316
|
+
lang: 'ar-EG',
|
|
317
|
+
}, true);
|
|
318
|
+
instance.resolvedLang = 'ar-EG';
|
|
319
|
+
|
|
320
|
+
const encoder = createProtocolEncoder(1024);
|
|
321
|
+
encodeProps(encoder, instance);
|
|
322
|
+
|
|
323
|
+
expect(getEncodedStringProp(encoder.getUint8Array(), PropId.Lang)).toBe('ar-EG');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('derives selectable=false for text inside an unselectable container', () => {
|
|
327
|
+
const parent = createElementNode('view', 'View', true, { selectable: false });
|
|
328
|
+
parent.props = normalizeProps('view', { selectable: false }, true);
|
|
329
|
+
|
|
330
|
+
const child = createElementNode('text', 'Text', true, { children: 'copy me' });
|
|
331
|
+
child.props = normalizeProps('text', { children: 'copy me' }, true);
|
|
332
|
+
appendNodeChild(parent, child);
|
|
333
|
+
|
|
334
|
+
const encoder = createProtocolEncoder(1024);
|
|
335
|
+
encodeProps(encoder, child);
|
|
336
|
+
|
|
337
|
+
expect(getEncodedStringProp(encoder.getUint8Array(), PropId.Selectable)).toBe('false');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('derives selectable=false for text inside a pressable', () => {
|
|
341
|
+
const parent = createElementNode('pressable', 'Pressable', true, {});
|
|
342
|
+
parent.props = normalizeProps('pressable', {}, true);
|
|
343
|
+
|
|
344
|
+
const child = createElementNode('text', 'Text', true, { children: 'copy me' });
|
|
345
|
+
child.props = normalizeProps('text', { children: 'copy me' }, true);
|
|
346
|
+
appendNodeChild(parent, child);
|
|
347
|
+
|
|
348
|
+
const encoder = createProtocolEncoder(1024);
|
|
349
|
+
encodeProps(encoder, child);
|
|
350
|
+
|
|
351
|
+
expect(getEncodedStringProp(encoder.getUint8Array(), PropId.Selectable)).toBe('false');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('encodes disabled props as string-backed values for protocol/native renderers', () => {
|
|
355
|
+
const instance = createElementNode('button', 'button', true, {
|
|
356
|
+
disabled: true,
|
|
357
|
+
children: 'Publish',
|
|
358
|
+
});
|
|
359
|
+
instance.props = normalizeProps('button', {
|
|
360
|
+
disabled: true,
|
|
361
|
+
children: 'Publish',
|
|
362
|
+
}, true);
|
|
363
|
+
|
|
364
|
+
const encoder = createProtocolEncoder(1024);
|
|
365
|
+
encodeProps(encoder, instance);
|
|
366
|
+
|
|
367
|
+
expect(getEncodedStringProp(encoder.getUint8Array(), PropId.Disabled)).toBe('true');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('encodes extended accessibility metadata and text scaling props', () => {
|
|
371
|
+
const instance = createElementNode('text', 'Text', true, {
|
|
372
|
+
testID: 'hero-title',
|
|
373
|
+
tabIndex: 3,
|
|
374
|
+
accessibilityActions: [
|
|
375
|
+
{ name: 'share', label: 'Share title' },
|
|
376
|
+
],
|
|
377
|
+
accessibilityOrder: ['price', 'title'],
|
|
378
|
+
allowFontScaling: true,
|
|
379
|
+
maxFontSizeMultiplier: 2,
|
|
380
|
+
minimumFontSize: 14,
|
|
381
|
+
accessibilityHeadingLevel: 1,
|
|
382
|
+
});
|
|
383
|
+
instance.props = normalizeProps('text', {
|
|
384
|
+
testID: 'hero-title',
|
|
385
|
+
tabIndex: 3,
|
|
386
|
+
accessibilityActions: [
|
|
387
|
+
{ name: 'share', label: 'Share title' },
|
|
388
|
+
],
|
|
389
|
+
accessibilityOrder: ['price', 'title'],
|
|
390
|
+
allowFontScaling: true,
|
|
391
|
+
maxFontSizeMultiplier: 2,
|
|
392
|
+
minimumFontSize: 14,
|
|
393
|
+
accessibilityHeadingLevel: 1,
|
|
394
|
+
}, true);
|
|
395
|
+
|
|
396
|
+
const encoder = createProtocolEncoder(2048);
|
|
397
|
+
encodeProps(encoder, instance);
|
|
398
|
+
|
|
399
|
+
const data = encoder.getUint8Array();
|
|
400
|
+
expect(getEncodedStringProp(data, PropId.TestId)).toBe('hero-title');
|
|
401
|
+
expect(getEncodedStringProp(data, PropId.TabIndex)).toBe('3');
|
|
402
|
+
expect(getEncodedStringProp(data, PropId.AccessibilityActions)).toBe(
|
|
403
|
+
JSON.stringify([{ name: 'share', label: 'Share title' }]),
|
|
404
|
+
);
|
|
405
|
+
expect(getEncodedStringProp(data, PropId.AccessibilityOrder)).toBe(
|
|
406
|
+
JSON.stringify(['price', 'title']),
|
|
407
|
+
);
|
|
408
|
+
expect(getEncodedStringProp(data, PropId.AllowFontScaling)).toBe('true');
|
|
409
|
+
expect(getEncodedStringProp(data, PropId.MaxFontSizeMultiplier)).toBe('2');
|
|
410
|
+
expect(getEncodedStringProp(data, PropId.MinimumFontSize)).toBe('14');
|
|
411
|
+
expect(getEncodedStringProp(data, PropId.AccessibilityHeadingLevel)).toBe('1');
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('encodes extended text styles in the high-word style mask', () => {
|
|
415
|
+
const encoder = createProtocolEncoder(1024);
|
|
416
|
+
encoder.setStyle(42, {
|
|
417
|
+
fontStyle: 'italic',
|
|
418
|
+
textDecorationLine: 'underline line-through',
|
|
419
|
+
fontVariantNumeric: 0x11,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const data = encoder.getUint8Array();
|
|
423
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
424
|
+
|
|
425
|
+
expect(view.getUint8(BUFFER_HEADER_SIZE)).toBe(OpCode.SetStyle);
|
|
426
|
+
expect(view.getUint32(BUFFER_HEADER_SIZE + 1, true)).toBe(42);
|
|
427
|
+
|
|
428
|
+
const payloadStart = BUFFER_HEADER_SIZE + OP_HEADER_SIZE;
|
|
429
|
+
const maskHighLowWord = view.getUint32(payloadStart + 8, true);
|
|
430
|
+
expect(maskHighLowWord).toBe((1 << 10) | (1 << 11) | (1 << 12));
|
|
431
|
+
expect(view.getUint8(payloadStart + 16)).toBe(1);
|
|
432
|
+
expect(view.getUint8(payloadStart + 17)).toBe(3);
|
|
433
|
+
expect(view.getUint8(payloadStart + 18)).toBe(0x11);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('encodes direction and writingMode ahead of extended typography fields', () => {
|
|
437
|
+
const encoder = createProtocolEncoder(1024);
|
|
438
|
+
encoder.setStyle(42, {
|
|
439
|
+
direction: 'rtl',
|
|
440
|
+
writingMode: 'vertical-rl',
|
|
441
|
+
fontStyle: 'italic',
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const data = encoder.getUint8Array();
|
|
445
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
446
|
+
const payloadStart = BUFFER_HEADER_SIZE + OP_HEADER_SIZE;
|
|
447
|
+
|
|
448
|
+
const maskHighLowWord = view.getUint32(payloadStart + 8, true);
|
|
449
|
+
expect(maskHighLowWord).toBe((1 << 8) | (1 << 9) | (1 << 10));
|
|
450
|
+
expect(view.getUint8(payloadStart + 16)).toBe(1);
|
|
451
|
+
expect(view.getUint8(payloadStart + 17)).toBe(1);
|
|
452
|
+
expect(view.getUint8(payloadStart + 18)).toBe(1);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('serializes mixed low-word and high-word typography fields in decoder order', () => {
|
|
456
|
+
const encoder = createProtocolEncoder(1024);
|
|
457
|
+
encoder.setStyle(42, {
|
|
458
|
+
fontWeight: 700,
|
|
459
|
+
textAlign: 'right',
|
|
460
|
+
lineHeight: 18,
|
|
461
|
+
numberOfLines: 2,
|
|
462
|
+
ellipsizeMode: 'middle',
|
|
463
|
+
fontFamily: 9,
|
|
464
|
+
resizeMode: 'contain',
|
|
465
|
+
fontSize: 14,
|
|
466
|
+
textColor: { r: 1, g: 2, b: 3, a: 4 },
|
|
467
|
+
fontStyle: 'italic',
|
|
468
|
+
textDecorationLine: 'underline',
|
|
469
|
+
fontVariantNumeric: 0x01,
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const data = encoder.getUint8Array();
|
|
473
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
474
|
+
const payloadStart = BUFFER_HEADER_SIZE + OP_HEADER_SIZE;
|
|
475
|
+
|
|
476
|
+
let offset = payloadStart + 16;
|
|
477
|
+
expect(view.getUint16(offset, true)).toBe(700);
|
|
478
|
+
offset += 2;
|
|
479
|
+
|
|
480
|
+
expect(view.getUint8(offset)).toBe(2);
|
|
481
|
+
offset += 1;
|
|
482
|
+
|
|
483
|
+
expect(view.getFloat32(offset, true)).toBe(18);
|
|
484
|
+
offset += 4;
|
|
485
|
+
|
|
486
|
+
expect(view.getUint32(offset, true)).toBe(2);
|
|
487
|
+
offset += 4;
|
|
488
|
+
|
|
489
|
+
expect(view.getUint8(offset)).toBe(2);
|
|
490
|
+
offset += 1;
|
|
491
|
+
|
|
492
|
+
expect(view.getUint16(offset, true)).toBe(9);
|
|
493
|
+
offset += 2;
|
|
494
|
+
|
|
495
|
+
expect(view.getUint8(offset)).toBe(1);
|
|
496
|
+
offset += 1;
|
|
497
|
+
|
|
498
|
+
expect(view.getFloat32(offset, true)).toBe(14);
|
|
499
|
+
offset += 4;
|
|
500
|
+
|
|
501
|
+
expect([
|
|
502
|
+
view.getUint8(offset),
|
|
503
|
+
view.getUint8(offset + 1),
|
|
504
|
+
view.getUint8(offset + 2),
|
|
505
|
+
view.getUint8(offset + 3),
|
|
506
|
+
]).toEqual([1, 2, 3, 4]);
|
|
507
|
+
offset += 4;
|
|
508
|
+
|
|
509
|
+
expect(view.getUint8(offset)).toBe(1);
|
|
510
|
+
offset += 1;
|
|
511
|
+
|
|
512
|
+
expect(view.getUint8(offset)).toBe(1);
|
|
513
|
+
offset += 1;
|
|
514
|
+
|
|
515
|
+
expect(view.getUint8(offset)).toBe(0x01);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('serializes visual fields in kernel decoder order around display and blur', () => {
|
|
519
|
+
const encoder = createProtocolEncoder(1024);
|
|
520
|
+
encoder.setStyle(42, {
|
|
521
|
+
backgroundColor: { r: 9, g: 8, b: 7, a: 6 },
|
|
522
|
+
opacity: 0.5,
|
|
523
|
+
borderRadius: 12,
|
|
524
|
+
fontWeight: 600,
|
|
525
|
+
ellipsizeMode: 'middle',
|
|
526
|
+
display: 'grid',
|
|
527
|
+
zIndex: 4,
|
|
528
|
+
fontFamily: 9,
|
|
529
|
+
resizeMode: 'contain',
|
|
530
|
+
backdropBlur: 18,
|
|
531
|
+
fontSize: 14,
|
|
532
|
+
textColor: { r: 1, g: 2, b: 3, a: 4 },
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const data = encoder.getUint8Array();
|
|
536
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
537
|
+
const payloadStart = BUFFER_HEADER_SIZE + OP_HEADER_SIZE;
|
|
538
|
+
|
|
539
|
+
let offset = payloadStart + 16;
|
|
540
|
+
|
|
541
|
+
expect([
|
|
542
|
+
view.getUint8(offset),
|
|
543
|
+
view.getUint8(offset + 1),
|
|
544
|
+
view.getUint8(offset + 2),
|
|
545
|
+
view.getUint8(offset + 3),
|
|
546
|
+
]).toEqual([9, 8, 7, 6]);
|
|
547
|
+
offset += 4;
|
|
548
|
+
|
|
549
|
+
expect(view.getFloat32(offset, true)).toBe(0.5);
|
|
550
|
+
offset += 4;
|
|
551
|
+
|
|
552
|
+
expect(view.getFloat32(offset, true)).toBe(12);
|
|
553
|
+
offset += 4;
|
|
554
|
+
|
|
555
|
+
expect(view.getUint16(offset, true)).toBe(600);
|
|
556
|
+
offset += 2;
|
|
557
|
+
|
|
558
|
+
expect(view.getUint8(offset)).toBe(2);
|
|
559
|
+
offset += 1;
|
|
560
|
+
|
|
561
|
+
expect(view.getUint8(offset)).toBe(2);
|
|
562
|
+
offset += 1;
|
|
563
|
+
|
|
564
|
+
expect(view.getInt32(offset, true)).toBe(4);
|
|
565
|
+
offset += 4;
|
|
566
|
+
|
|
567
|
+
expect(view.getUint16(offset, true)).toBe(9);
|
|
568
|
+
offset += 2;
|
|
569
|
+
|
|
570
|
+
expect(view.getUint8(offset)).toBe(1);
|
|
571
|
+
offset += 1;
|
|
572
|
+
|
|
573
|
+
expect(view.getFloat32(offset, true)).toBe(18);
|
|
574
|
+
offset += 4;
|
|
575
|
+
|
|
576
|
+
expect(view.getFloat32(offset, true)).toBe(14);
|
|
577
|
+
offset += 4;
|
|
578
|
+
|
|
579
|
+
expect([
|
|
580
|
+
view.getUint8(offset),
|
|
581
|
+
view.getUint8(offset + 1),
|
|
582
|
+
view.getUint8(offset + 2),
|
|
583
|
+
view.getUint8(offset + 3),
|
|
584
|
+
]).toEqual([1, 2, 3, 4]);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('encodes shadow fields into the low-word style mask and payload', () => {
|
|
588
|
+
const encoder = createProtocolEncoder(1024);
|
|
589
|
+
encoder.setStyle(42, {
|
|
590
|
+
shadowColor: { r: 15, g: 125, b: 97, a: 255 },
|
|
591
|
+
shadowOffsetX: 0,
|
|
592
|
+
shadowOffsetY: 6,
|
|
593
|
+
shadowRadius: 18,
|
|
594
|
+
shadowOpacity: 0.14,
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
const data = encoder.getUint8Array();
|
|
598
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
599
|
+
const payloadStart = BUFFER_HEADER_SIZE + OP_HEADER_SIZE;
|
|
600
|
+
|
|
601
|
+
const maskLowHighWord = view.getUint32(payloadStart + 4, true);
|
|
602
|
+
expect(maskLowHighWord & StyleMask.SHADOW_COLOR.high).toBe(StyleMask.SHADOW_COLOR.high);
|
|
603
|
+
expect(maskLowHighWord & StyleMask.SHADOW_OFFSET.high).toBe(StyleMask.SHADOW_OFFSET.high);
|
|
604
|
+
expect(maskLowHighWord & StyleMask.SHADOW_RADIUS.high).toBe(StyleMask.SHADOW_RADIUS.high);
|
|
605
|
+
expect(maskLowHighWord & StyleMask.SHADOW_OPACITY.high).toBe(StyleMask.SHADOW_OPACITY.high);
|
|
606
|
+
|
|
607
|
+
let offset = payloadStart + 16;
|
|
608
|
+
expect([
|
|
609
|
+
view.getUint8(offset),
|
|
610
|
+
view.getUint8(offset + 1),
|
|
611
|
+
view.getUint8(offset + 2),
|
|
612
|
+
view.getUint8(offset + 3),
|
|
613
|
+
]).toEqual([15, 125, 97, 255]);
|
|
614
|
+
offset += 4;
|
|
615
|
+
|
|
616
|
+
expect(view.getFloat32(offset, true)).toBe(0);
|
|
617
|
+
offset += 4;
|
|
618
|
+
expect(view.getFloat32(offset, true)).toBe(6);
|
|
619
|
+
offset += 4;
|
|
620
|
+
expect(view.getFloat32(offset, true)).toBe(18);
|
|
621
|
+
offset += 4;
|
|
622
|
+
expect(view.getFloat32(offset, true)).toBeCloseTo(0.14);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('prefers the live main-window viewport when the stored window snapshot is stale', () => {
|
|
626
|
+
setWindowSnapshot(MAIN_WINDOW_ID, {
|
|
627
|
+
size: {
|
|
628
|
+
width: 393,
|
|
629
|
+
height: 852,
|
|
630
|
+
},
|
|
631
|
+
position: {
|
|
632
|
+
x: 0,
|
|
633
|
+
y: 0,
|
|
634
|
+
},
|
|
635
|
+
state: 'normal',
|
|
636
|
+
isFocused: true,
|
|
637
|
+
isVisible: true,
|
|
638
|
+
display: {
|
|
639
|
+
id: 'display-main',
|
|
640
|
+
size: {
|
|
641
|
+
width: 393,
|
|
642
|
+
height: 852,
|
|
643
|
+
},
|
|
644
|
+
workArea: {
|
|
645
|
+
x: 0,
|
|
646
|
+
y: 0,
|
|
647
|
+
width: 393,
|
|
648
|
+
height: 852,
|
|
649
|
+
},
|
|
650
|
+
scaleFactor: 1,
|
|
651
|
+
},
|
|
652
|
+
safeAreaInsets: zeroInsets(),
|
|
653
|
+
safeAreaRegions: emptySafeAreaRegions(),
|
|
654
|
+
keyboardState: emptyKeyboardState(),
|
|
655
|
+
devicePixelRatio: 1,
|
|
656
|
+
});
|
|
657
|
+
(
|
|
658
|
+
globalThis as typeof globalThis & {
|
|
659
|
+
exact?: {
|
|
660
|
+
screenWidth?: number;
|
|
661
|
+
screenHeight?: number;
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
).exact = {
|
|
665
|
+
screenWidth: 482,
|
|
666
|
+
screenHeight: 960,
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
expect(getScreenDimensions(0)).toEqual({
|
|
670
|
+
width: 482,
|
|
671
|
+
height: 960,
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it('uses dispatchWithDebugContext when a resize trace context is available', () => {
|
|
676
|
+
const dispatch = vi.fn();
|
|
677
|
+
const dispatchWithDebugContext = vi.fn();
|
|
678
|
+
(
|
|
679
|
+
globalThis as typeof globalThis & {
|
|
680
|
+
exact?: {
|
|
681
|
+
dispatch: typeof dispatch;
|
|
682
|
+
dispatchWithDebugContext: typeof dispatchWithDebugContext;
|
|
683
|
+
__resizeTraceContext?: unknown;
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
).exact = {
|
|
687
|
+
dispatch,
|
|
688
|
+
dispatchWithDebugContext,
|
|
689
|
+
__resizeTraceContext: {
|
|
690
|
+
benchmarkRunId: 'resize-test',
|
|
691
|
+
resizeSeq: 42,
|
|
692
|
+
stepIndex: 3,
|
|
693
|
+
},
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
const encoder = createProtocolEncoder(1024);
|
|
697
|
+
dispatchProtocol(encoder);
|
|
698
|
+
|
|
699
|
+
expect(dispatchWithDebugContext).toHaveBeenCalledTimes(1);
|
|
700
|
+
expect(dispatch).not.toHaveBeenCalled();
|
|
701
|
+
|
|
702
|
+
const [buffer, debugContextJSON] = dispatchWithDebugContext.mock.calls[0]!;
|
|
703
|
+
expect(buffer).toBeInstanceOf(Uint8Array);
|
|
704
|
+
expect(JSON.parse(debugContextJSON)).toEqual({
|
|
705
|
+
benchmarkRunId: 'resize-test',
|
|
706
|
+
resizeSeq: 42,
|
|
707
|
+
stepIndex: 3,
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it('falls back to plain dispatch when no resize trace context is present', () => {
|
|
712
|
+
const dispatch = vi.fn();
|
|
713
|
+
const dispatchWithDebugContext = vi.fn();
|
|
714
|
+
(
|
|
715
|
+
globalThis as typeof globalThis & {
|
|
716
|
+
exact?: {
|
|
717
|
+
dispatch: typeof dispatch;
|
|
718
|
+
dispatchWithDebugContext: typeof dispatchWithDebugContext;
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
).exact = {
|
|
722
|
+
dispatch,
|
|
723
|
+
dispatchWithDebugContext,
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
const encoder = createProtocolEncoder(1024);
|
|
727
|
+
dispatchProtocol(encoder);
|
|
728
|
+
|
|
729
|
+
expect(dispatch).toHaveBeenCalledTimes(1);
|
|
730
|
+
expect(dispatchWithDebugContext).not.toHaveBeenCalled();
|
|
731
|
+
});
|
|
732
|
+
});
|