@symbiote-native/test-utils 0.3.0 → 0.4.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.
@@ -0,0 +1,409 @@
1
+ /**
2
+ * A tree host that RECORDS and derives nothing — for the 141 test files that never read a tree.
3
+ *
4
+ * Those files call `installFabric()` only because that is how a test gets a host at all: they
5
+ * assert on state machines, prop resolution, listeners, styles. Attaching them to a second
6
+ * implementation of Fabric's tree rules buys them nothing and costs the project a mirror.
7
+ *
8
+ * **Why this is not one.** It keeps the AUTHORED tree — who was appended to whom, what props were
9
+ * set — because that is what the ops literally say and what an adapter's own seam asks back
10
+ * (Solid's `getParentNode`, Vue's `nextSibling`, Svelte's `firstChild`). It does not decide what
11
+ * COMMITS: no flattening, no stacking contexts, no virtual nodes, no clone protocol, no view-name
12
+ * rewriting. There is no derived answer here to be wrong, which is the whole difference between
13
+ * recording a statement and re-deriving a conclusion.
14
+ *
15
+ * A test that needs the committed tree belongs in `core/engine/cpp/tests/js`, where React Native
16
+ * answers. Asking one of those questions here gets `undefined` rather than a plausible lie.
17
+ */
18
+ import { NO_VALUE, OP_APPEND_CHILD, OP_COMMIT, OP_CREATE_ANCHOR, OP_CREATE_ELEMENT, OP_CREATE_RAW_TEXT, OP_INSERT_BEFORE, OP_REMOVE_CHILD, OP_SET_COMPONENT, OP_SET_OWNED_LISTENER, OP_SET_TAG, OP_SET_UNDERLAY_SHOWN, OP_SET_PROP, OP_SET_TEXT, OP_STRIDE, } from '@symbiote-native/engine/mutation-buffer';
19
+ import { fabricProps, isSymbioteNode, propsOf, setTreeHost, } from '@symbiote-native/engine';
20
+ /**
21
+ * Install the recording host, with the minimum slot the engine insists on.
22
+ *
23
+ * `createSurface` calls `installEventHandler`, which reaches `globalThis.nativeFabricUIManager` and
24
+ * throws if it is absent — so a host alone is not enough to get a surface open. The slot below
25
+ * exists to be PRESENT: every method answers nothing except `registerEventHandler`, which hands the
26
+ * engine's handler to the host so `fireEvent` can play it back.
27
+ *
28
+ * What that does NOT buy is a real gesture. Deciding which node a touch lands on is hit-testing,
29
+ * and that is the renderer's answer; a test that needs it belongs in `core/engine/cpp/tests/js`.
30
+ */
31
+ export function installRecordingFabric() {
32
+ const host = createRecordingHost();
33
+ Object.assign(globalThis, {
34
+ nativeFabricUIManager: {
35
+ registerEventHandler(handler) {
36
+ host.registerEventHandler(handler);
37
+ },
38
+ dispatchCommand() { },
39
+ setIsJSResponder() { },
40
+ sendAccessibilityEvent() { },
41
+ measure() { },
42
+ measureInWindow() { },
43
+ measureLayout() { },
44
+ },
45
+ });
46
+ setTreeHost(host);
47
+ return host;
48
+ }
49
+ /**
50
+ * The Fabric payload for a node — what the engine WOULD hand the renderer for it, built by the
51
+ * engine's own builder rather than read off anything.
52
+ *
53
+ * This exists because the two are easy to confuse and the difference bites: a node's PROPS are the
54
+ * author's bag (`style` is still an object), while the PAYLOAD is what `fabricProps` makes of it —
55
+ * style flattened into top-level keys, the aria fold run, the ten RN processors applied. A test
56
+ * asking about `padding` or `accessibilityRole` or a parsed `backgroundSize` means the payload, and
57
+ * reading the bag instead comes back `undefined` with nothing to explain why.
58
+ */
59
+ export function payloadOf(node) {
60
+ return fabricProps(node, propsOf(node));
61
+ }
62
+ /**
63
+ * Where a tag would be. Not a Fabric tag and not pretending to be one — this host never speaks to
64
+ * Fabric, so any number here would be an invention.
65
+ */
66
+ const NO_TAG = -1;
67
+ export function createRecordingHost() {
68
+ let recorded = new WeakMap();
69
+ let eventHandler;
70
+ // The WeakMap above cannot be enumerated, and `find` has to start somewhere. Strong references,
71
+ // so `forget()` is what a long file calls to stop this growing — the same deal `installFabric`'s
72
+ // `created` array made.
73
+ let created = [];
74
+ const nodeOf = (handle, what) => {
75
+ const node = recorded.get(handle);
76
+ if (node === undefined) {
77
+ throw new Error(`${what}: handle names no node in this tree`);
78
+ }
79
+ return node;
80
+ };
81
+ const detach = (node) => {
82
+ const { parent } = node;
83
+ if (parent === undefined)
84
+ return;
85
+ const at = parent.children.indexOf(node);
86
+ if (at >= 0)
87
+ parent.children.splice(at, 1);
88
+ node.parent = undefined;
89
+ };
90
+ // Lands a rootTag onto a whole subtree at OP_COMMIT, the same moment the real engine hands every
91
+ // node in the walk its own `ShadowNode`. `committedRootTag` never gets cleared afterwards, so a
92
+ // node removed after this still answers `committedRecordOf` — see that field's own comment.
93
+ const markCommitted = (node, rootTag) => {
94
+ node.committedRootTag = rootTag;
95
+ for (const child of node.children)
96
+ markCommitted(child, rootTag);
97
+ };
98
+ const host = {
99
+ commits: 0,
100
+ commands: [],
101
+ responderHandovers: [],
102
+ accessibilityEvents: [],
103
+ applyOps(batch) {
104
+ const { ops, strings, values, handles, instanceHandles } = batch;
105
+ const handleAt = (slot) => {
106
+ const handle = handles[slot];
107
+ if (handle === undefined) {
108
+ throw new Error(`applyOps: slot ${slot} is outside this batch's handles`);
109
+ }
110
+ return handle;
111
+ };
112
+ const at = (slot) => nodeOf(handleAt(slot), 'applyOps');
113
+ const create = (slot, viewName, props, instanceHandle) => {
114
+ const handle = handleAt(slot);
115
+ // Checked rather than assumed: the batch types handles as `object`, and the whole value of
116
+ // holding the node is being able to hand it back to the engine's own API.
117
+ if (!isSymbioteNode(handle)) {
118
+ throw new Error('applyOps: a handle in this batch is not an engine node');
119
+ }
120
+ const node = {
121
+ handle,
122
+ instanceHandle,
123
+ viewName,
124
+ tagName: '',
125
+ ownedListeners: {},
126
+ underlayShown: false,
127
+ props,
128
+ parent: undefined,
129
+ children: [],
130
+ committedRootTag: undefined,
131
+ };
132
+ recorded.set(handle, node);
133
+ created.push(node);
134
+ };
135
+ for (let cursor = 0; cursor + OP_STRIDE <= ops.length; cursor += OP_STRIDE) {
136
+ const code = ops[cursor];
137
+ const a = ops[cursor + 1];
138
+ const b = ops[cursor + 2];
139
+ const c = ops[cursor + 3];
140
+ const d = ops[cursor + 4];
141
+ switch (code) {
142
+ case OP_CREATE_ELEMENT:
143
+ // Slot 4 is the index into `instanceHandles` — the object Fabric would hand back with
144
+ // an event. Recorded because it is what the op SAYS, and because a test firing an
145
+ // event has to name the same object the engine registered.
146
+ create(a, strings[b], {}, instanceHandles[d]);
147
+ break;
148
+ case OP_CREATE_RAW_TEXT:
149
+ create(a, 'RCTRawText', { text: strings[b] });
150
+ break;
151
+ case OP_CREATE_ANCHOR:
152
+ create(a, '', {});
153
+ break;
154
+ case OP_APPEND_CHILD: {
155
+ const child = at(b);
156
+ detach(child);
157
+ child.parent = at(a);
158
+ at(a).children.push(child);
159
+ break;
160
+ }
161
+ case OP_INSERT_BEFORE: {
162
+ const parent = at(a);
163
+ const child = at(b);
164
+ const before = at(c);
165
+ detach(child);
166
+ child.parent = parent;
167
+ const index = parent.children.indexOf(before);
168
+ parent.children.splice(index < 0 ? parent.children.length : index, 0, child);
169
+ break;
170
+ }
171
+ case OP_REMOVE_CHILD:
172
+ detach(at(b));
173
+ break;
174
+ case OP_SET_PROP: {
175
+ const node = at(a);
176
+ if (c === NO_VALUE)
177
+ delete node.props[strings[b]];
178
+ else
179
+ node.props[strings[b]] = values[c];
180
+ break;
181
+ }
182
+ case OP_SET_TEXT:
183
+ at(a).props.text = strings[b];
184
+ break;
185
+ case OP_SET_COMPONENT:
186
+ at(a).viewName = strings[b];
187
+ break;
188
+ // RECORDED AND NOT ACTED ON, deliberately. The real host resolves a tag's platform props
189
+ // off this (`foldPressableProps`); this host builds its payload through the TypeScript
190
+ // `fabricProps`, which carries no copy of those rules and must not grow one — a second
191
+ // implementation is how a test goes green over a rule that no longer runs. A test that
192
+ // needs to read what a tag actually sends belongs in `core/engine/cpp/tests/js`.
193
+ case OP_SET_TAG:
194
+ at(a).tagName = strings[b];
195
+ break;
196
+ // RECORDED, NOT APPLIED, exactly like the tag above. The real host resolves `focusable`'s
197
+ // three-leg touchable form off this bit; this host builds its payload through the
198
+ // TypeScript `fabricProps`, which carries no copy of the tag rules and must not grow one.
199
+ // So a test that wants the resolved key reads the committed payload in an itest, and what
200
+ // this records is the fact a test can ASK about.
201
+ case OP_SET_OWNED_LISTENER:
202
+ at(a).ownedListeners[strings[b]] = c !== 0;
203
+ break;
204
+ // Same treatment, and for the same reason: the real host paints TouchableHighlight's
205
+ // underlay from this bit (`foldTouchableHighlightUnderlay`), and this one must not grow a
206
+ // copy of that rule. Recorded so a test can ask whether the MACHINE flipped it — which is
207
+ // the half that is still JS — while what the flip LOOKS like is asserted on a committed
208
+ // payload in `core/engine/cpp/tests/js/touchable-highlight-underlay.itest.ts`.
209
+ case OP_SET_UNDERLAY_SHOWN:
210
+ at(a).underlayShown = b !== 0;
211
+ break;
212
+ case OP_COMMIT: {
213
+ host.commits += 1;
214
+ markCommitted(at(b), a);
215
+ break;
216
+ }
217
+ default:
218
+ throw new Error(`applyOps: unknown opcode ${String(code)}`);
219
+ }
220
+ }
221
+ },
222
+ propOf(handle, key) {
223
+ return nodeOf(handle, 'propOf').props[key];
224
+ },
225
+ propsOf(handle) {
226
+ return nodeOf(handle, 'propsOf').props;
227
+ },
228
+ // Nothing is memoized here, so there is nothing to invalidate.
229
+ markPropsDirty() { },
230
+ /**
231
+ * A record for a node standing under a surface the ops COMMITTED, and nothing more.
232
+ *
233
+ * The engine gates every imperative call on this, so answering `undefined` always would make
234
+ * `dispatchCommand` unreachable and quietly turn four command tests green-by-absence. What is
235
+ * answered here is read straight off the op stream — there WAS a commit, and this node is under
236
+ * that surface — with no claim about what the renderer did with it: no tag Fabric minted, no
237
+ * view that survived flattening, no guarantee it is mounted. A test that needs any of those
238
+ * belongs in `core/engine/cpp/tests/js`.
239
+ *
240
+ * `handle` is the authored node and `tag` is `NO_TAG`, so a test reading either gets something
241
+ * obviously not-from-Fabric rather than a plausible number. `rootTag` is real, not invented —
242
+ * see `IRecorded.committedRootTag`'s own comment for why the JS-level surface identifier is a
243
+ * different unknown from the native Fabric tag.
244
+ *
245
+ * Read off `committedRootTag` (set once per node at the commit that landed it, never cleared),
246
+ * not a live walk to a committed root — `removeChild` cuts the parent link before a behavior's
247
+ * teardown sweep runs in the SAME commit, so a walk answers `undefined` for exactly the node a
248
+ * parting write (`TextInput`'s unmount blur) needs to still reach. `node->committed` in the real
249
+ * engine works the same way, and for the same reason.
250
+ */
251
+ committedRecordOf(handle) {
252
+ const node = recorded.get(handle);
253
+ if (node === undefined || node.committedRootTag === undefined) {
254
+ return undefined;
255
+ }
256
+ return {
257
+ handle,
258
+ tag: NO_TAG,
259
+ rootTag: node.committedRootTag,
260
+ };
261
+ },
262
+ /**
263
+ * REFUSED, loudly, and that is the whole point of implementing it here.
264
+ *
265
+ * This host records the OPS it was handed; it never builds a Fabric payload, because building
266
+ * one is `SymbioteFabricProps.cpp`'s job and that code does not exist in a vitest process. An
267
+ * answer synthesised from the JS twin would be a test asserting against the wrong
268
+ * implementation — the exact drift `ITreeHost.committedPayloadOf` was added to close.
269
+ *
270
+ * So a test asking this question belongs in `core/engine/cpp/tests/js/` (`pnpm run test:itest`),
271
+ * where the real builder runs. `.props` on this host stays the read for "what did the adapter
272
+ * SAY", which is a different and still-useful question.
273
+ */
274
+ committedPayloadOf() {
275
+ throw new Error('recording host: committedPayloadOf needs the real payload builder — move this to an itest');
276
+ },
277
+ parentOf(handle) {
278
+ return nodeOf(handle, 'parentOf').parent?.handle;
279
+ },
280
+ childrenOf(handle) {
281
+ return nodeOf(handle, 'childrenOf').children.map(child => child.handle);
282
+ },
283
+ firstChildOf(handle) {
284
+ return nodeOf(handle, 'firstChildOf').children[0]?.handle;
285
+ },
286
+ nextSiblingOf(handle) {
287
+ const node = nodeOf(handle, 'nextSiblingOf');
288
+ const siblings = node.parent?.children;
289
+ if (siblings === undefined)
290
+ return undefined;
291
+ return siblings[siblings.indexOf(node) + 1]?.handle;
292
+ },
293
+ parentsOf(handles) {
294
+ return handles.map(handle => nodeOf(handle, 'parentsOf').parent?.handle);
295
+ },
296
+ subtreesOf(roots) {
297
+ const out = [];
298
+ const walk = (node) => {
299
+ out.push(node.handle);
300
+ for (const child of node.children)
301
+ walk(child);
302
+ };
303
+ for (const root of roots)
304
+ walk(nodeOf(root, 'subtreesOf'));
305
+ return out;
306
+ },
307
+ // The twin of the engine's narrowed walk, and it MUST narrow here too: the vitest suite is where
308
+ // the sweep's behaviour is asserted, so a recording host that handed back everything would make
309
+ // every one of those cases pass whatever the engine does.
310
+ teardownSubtreesOf(roots) {
311
+ const out = [];
312
+ const walk = (node, isRoot) => {
313
+ const reserved = out.length;
314
+ out.push(node.handle);
315
+ let isWanted = isRoot || node.tagName !== '';
316
+ for (const child of node.children) {
317
+ if (walk(child, false))
318
+ isWanted = true;
319
+ }
320
+ if (!isWanted)
321
+ out.length = reserved;
322
+ return isWanted;
323
+ };
324
+ for (const root of roots)
325
+ walk(nodeOf(root, 'teardownSubtreesOf'), true);
326
+ return out;
327
+ },
328
+ ancestorsOf(handle) {
329
+ const chain = [];
330
+ for (let node = nodeOf(handle, 'ancestorsOf'); node !== undefined; node = node.parent) {
331
+ chain.push(node.handle);
332
+ }
333
+ return chain;
334
+ },
335
+ census(roots) {
336
+ let nodes = 0;
337
+ const walk = (node) => {
338
+ nodes += 1;
339
+ for (const child of node.children)
340
+ walk(child);
341
+ };
342
+ for (const root of roots)
343
+ walk(nodeOf(root, 'census'));
344
+ // The skip counts belong to the commit rules, which this host does not have. Zero is the
345
+ // honest answer: nothing here was skipped, because nothing here was decided.
346
+ return {
347
+ nodes,
348
+ anchors: 0,
349
+ emptyRawTexts: 0,
350
+ renderable: nodes,
351
+ flattenWidths: [],
352
+ };
353
+ },
354
+ // The imperative six ask the PLATFORM — a frame, a gesture, an announcement. Three of them
355
+ // carry a request the engine MADE, so they are recorded; the measuring three want an answer
356
+ // only a platform has, and there is none here.
357
+ dispatchCommand(handle, commandName, args) {
358
+ host.commands.push({
359
+ handle,
360
+ viewName: nodeOf(handle, 'dispatchCommand').viewName,
361
+ commandName,
362
+ args,
363
+ });
364
+ },
365
+ sendAccessibilityEvent(handle, eventType) {
366
+ host.accessibilityEvents.push({ handle, eventType });
367
+ },
368
+ measure(_handle, _callback) { },
369
+ measureInWindow(_handle, _callback) { },
370
+ measureLayout(_handle, _relativeTo, onFail, _onSuccess) {
371
+ onFail();
372
+ },
373
+ setIsJSResponder(handle, isResponder, blockNativeResponder) {
374
+ host.responderHandovers.push({
375
+ handle,
376
+ isResponder,
377
+ blockNativeResponder,
378
+ });
379
+ },
380
+ registerEventHandler(handler) {
381
+ eventHandler = handler;
382
+ },
383
+ find(predicate) {
384
+ return created.find(predicate);
385
+ },
386
+ findAll(predicate) {
387
+ return created.filter(predicate);
388
+ },
389
+ fireEvent(handle, topLevelType, nativeEvent = {}) {
390
+ if (eventHandler === undefined) {
391
+ throw new Error('no event handler registered by the renderer');
392
+ }
393
+ eventHandler(handle, topLevelType, nativeEvent);
394
+ },
395
+ reset() {
396
+ created = [];
397
+ host.commits = 0;
398
+ host.commands.length = 0;
399
+ host.responderHandovers.length = 0;
400
+ host.accessibilityEvents.length = 0;
401
+ },
402
+ forget() {
403
+ recorded = new WeakMap();
404
+ created = [];
405
+ host.reset();
406
+ },
407
+ };
408
+ return host;
409
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/test-utils",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Shared, framework-agnostic fake-Fabric test harness for SymbioteNative — the fake nativeFabricUIManager recorder used by the co-located test suites across the engine, every adapter, and the example apps.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -39,6 +39,9 @@
39
39
  "publishConfig": {
40
40
  "access": "public"
41
41
  },
42
+ "dependencies": {
43
+ "@symbiote-native/engine": "1.1.0"
44
+ },
42
45
  "scripts": {
43
46
  "typecheck": "tsc --build",
44
47
  "format": "prettier --write \"src/**/*.{ts,tsx}\""
@@ -1,51 +0,0 @@
1
- export interface IFakeNode {
2
- tag: number;
3
- viewName: string;
4
- props: Record<string, unknown>;
5
- children: IFakeNode[];
6
- instanceHandle: unknown;
7
- /** Fabric family parent. Clones keep the same tag/family, so reparenting a family is illegal. */
8
- parentFamilyTag?: number;
9
- }
10
- export type IEventHandler = (instanceHandle: unknown, topLevelType: string, nativeEvent: Record<string, unknown>) => void;
11
- export interface IFabricRecorder {
12
- /** The child set handed to the most recent `completeRoot`. */
13
- committed: IFakeNode[];
14
- /** Every node ever `createNode`'d this run (clones excluded). */
15
- created: IFakeNode[];
16
- /** Every imperative command dispatched at a committed Fabric node. */
17
- commands: Array<{
18
- node: IFakeNode;
19
- commandName: string;
20
- args: readonly unknown[];
21
- }>;
22
- /**
23
- * Call counters, for tests that assert "exactly N native nodes were created" — and for pricing
24
- * a commit's PROTOCOL half against its walk half. `appendChild` and `clone` are the two Fabric
25
- * makes unavoidable: a parent whose child set changed is cloned empty and re-appends every child
26
- * handle, so those counts are what a real JSI boundary would charge no matter how cheap the JS
27
- * walk above them gets. Counting them here is the only way a headless run can say which half a
28
- * proposed optimisation is even aimed at.
29
- */
30
- counts: {
31
- createNode: number;
32
- completeRoot: number;
33
- appendChild: number;
34
- clone: number;
35
- };
36
- /**
37
- * RN wraps every commit in a synthetic `box-none` AppContainer root.
38
- * Returns it, asserting it is the single expected root, so each test unwraps the
39
- * AppContainer the same way instead of re-checking the invariant by hand.
40
- */
41
- appRoot(): IFakeNode;
42
- /** Find the first `createNode`'d node matching a predicate (e.g. the app's own View). */
43
- find(predicate: (node: IFakeNode) => boolean): IFakeNode | undefined;
44
- /** Deliver a native event to the renderer's registered handler. */
45
- fireEvent(handle: unknown, topLevelType: string, nativeEvent?: Record<string, unknown>): void;
46
- /** Serialize a node list to `RCTView(RCTText(RCTRawText "text"))` shorthand. */
47
- serialize(nodes: IFakeNode[]): string;
48
- /** Zero the counters and clear `committed` / `created` (the event handler survives). */
49
- reset(): void;
50
- }
51
- export declare function installFabric(): IFabricRecorder;
@@ -1,151 +0,0 @@
1
- // One shared fake `nativeFabricUIManager` for the unit suite. `installFabric()` puts a
2
- // fresh recording slot on `globalThis` and returns a handle to inspect what was committed.
3
- //
4
- // Mirrors real Fabric's clone-on-write semantics: every clone gets a NEW identity;
5
- // `*NewProps` MERGES the diff onto previous props (the engine always sends a minimal diff —
6
- // see `diffProps` in commit.ts). A removed key arrives as literal `null` and stays `null`,
7
- // not deleted, so a test can tell "explicitly reset" apart from "never set". `*Children`
8
- // variants reset children (the engine re-appends).
9
- // See the header comment above for the merge/null-removal semantics this mirrors.
10
- function mergeFabricProps(previous, diff) {
11
- return { ...previous, ...diff };
12
- }
13
- // Fabric clones keep the node's FAMILY, so a handle that already belongs to one parent can never
14
- // be appended under another. Enforced on both routes into a parent's child list — the append loop
15
- // and the batched clone — so switching between them cannot quietly drop the check.
16
- function assertSameFamily(parent, child) {
17
- if (child.parentFamilyTag !== undefined &&
18
- child.parentFamilyTag !== parent.tag) {
19
- throw new Error(`Fabric family reparent: child ${child.viewName}#${child.tag} already belongs to parent #${child.parentFamilyTag}, cannot append to ${parent.viewName}#${parent.tag}`);
20
- }
21
- }
22
- // `Array.isArray` narrows to `any[]`, which leaves the OTHER branch of the union un-narrowed;
23
- // an explicit predicate keeps both sides typed without a cast.
24
- function isFakeNodeList(value) {
25
- return Array.isArray(value);
26
- }
27
- // A clone with no child list comes back EMPTY, exactly as the real binding does.
28
- function adoptChildren(parent, children) {
29
- if (children === undefined)
30
- return [];
31
- for (const child of children) {
32
- assertSameFamily(parent, child);
33
- child.parentFamilyTag = parent.tag;
34
- }
35
- return [...children];
36
- }
37
- export function installFabric() {
38
- let committed = [];
39
- const created = [];
40
- const commands = [];
41
- const counts = { createNode: 0, completeRoot: 0, appendChild: 0, clone: 0 };
42
- let eventHandler;
43
- const slot = {
44
- createNode(tag, viewName, _rootTag, props, instanceHandle) {
45
- counts.createNode += 1;
46
- const node = {
47
- tag,
48
- viewName,
49
- props,
50
- children: [],
51
- instanceHandle,
52
- };
53
- created.push(node);
54
- return node;
55
- },
56
- cloneNodeWithNewProps: (node, newProps) => {
57
- counts.clone += 1;
58
- return { ...node, props: mergeFabricProps(node.props, newProps) };
59
- },
60
- // Both clone-with-children forms take the child list as an OPTIONAL trailing/second argument,
61
- // mirroring UIManagerBinding.cpp: `cloneNodeWithNewChildren(node, children?)` and the 3-arg
62
- // `cloneNodeWithNewChildrenAndProps(node, children, props)`. The engine probes support by
63
- // ARITY, so these must keep 2 and 3 declared parameters — a default value on any of them
64
- // would drop `.length` below the threshold and silently send every test down the append loop.
65
- cloneNodeWithNewChildren: (node, children) => {
66
- counts.clone += 1;
67
- return { ...node, children: adoptChildren(node, children) };
68
- },
69
- cloneNodeWithNewChildrenAndProps: (node, childrenOrProps, maybeProps) => {
70
- counts.clone += 1;
71
- const children = isFakeNodeList(childrenOrProps)
72
- ? childrenOrProps
73
- : undefined;
74
- const newProps = isFakeNodeList(childrenOrProps)
75
- ? (maybeProps ?? {})
76
- : childrenOrProps;
77
- return {
78
- ...node,
79
- props: mergeFabricProps(node.props, newProps),
80
- children: adoptChildren(node, children),
81
- };
82
- },
83
- createChildSet: () => [],
84
- appendChild(parent, child) {
85
- counts.appendChild += 1;
86
- assertSameFamily(parent, child);
87
- child.parentFamilyTag = parent.tag;
88
- parent.children.push(child);
89
- return parent;
90
- },
91
- appendChildToSet(childSet, child) {
92
- childSet.push(child);
93
- },
94
- completeRoot(_rootTag, childSet) {
95
- counts.completeRoot += 1;
96
- committed = childSet;
97
- },
98
- registerEventHandler(handler) {
99
- eventHandler = handler;
100
- },
101
- dispatchCommand(node, commandName, args) {
102
- commands.push({ node, commandName, args });
103
- },
104
- };
105
- Object.assign(globalThis, { nativeFabricUIManager: slot });
106
- const serializeNode = (node) => {
107
- const text = node.viewName === 'RCTRawText' ? ` "${String(node.props.text)}"` : '';
108
- const kids = node.children.length
109
- ? `(${node.children.map(serializeNode).join('')})`
110
- : '';
111
- return `${node.viewName}${text}${kids}`;
112
- };
113
- return {
114
- get committed() {
115
- return committed;
116
- },
117
- created,
118
- commands,
119
- counts,
120
- appRoot() {
121
- const root = committed[0];
122
- if (committed.length !== 1 || root?.props.pointerEvents !== 'box-none') {
123
- throw new Error(`expected a single box-none AppContainer root, got ${committed.length} node(s)`);
124
- }
125
- return root;
126
- },
127
- find(predicate) {
128
- return created.find(predicate);
129
- },
130
- fireEvent(handle, topLevelType, nativeEvent = {}) {
131
- if (!eventHandler)
132
- throw new Error('no event handler registered by the renderer');
133
- eventHandler(handle, topLevelType, nativeEvent);
134
- },
135
- serialize(nodes) {
136
- return nodes.map(serializeNode).join('');
137
- },
138
- reset() {
139
- committed = [];
140
- created.length = 0;
141
- commands.length = 0;
142
- // Every counter, not a subset: `appendChild` and `clone` were left out, so any assertion
143
- // on them across a reset read the PREVIOUS phase's total and could not fail. Both are now
144
- // the metric that prices the clone protocol, so a stale one is a silent wrong answer.
145
- counts.createNode = 0;
146
- counts.completeRoot = 0;
147
- counts.appendChild = 0;
148
- counts.clone = 0;
149
- },
150
- };
151
- }