@symbiote-native/test-utils 0.4.3 → 0.4.5

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,14 @@
1
+ import { type IMutationBatch } from '@symbiote-native/engine/mutation-buffer';
2
+ import type { IRecorded } from './recording-host';
3
+ export type IRecordedTree = {
4
+ /** How many commits the ops asked for. */
5
+ commits: number;
6
+ /** Every node the ops created, in creation order, which is what `find` searches. */
7
+ readonly authored: IRecorded[];
8
+ applyOps: (batch: IMutationBatch) => void;
9
+ recordedOf: (handle: object) => IRecorded | undefined;
10
+ nodeOf: (handle: object, what: string) => IRecorded;
11
+ reset: () => void;
12
+ forget: () => void;
13
+ };
14
+ export declare function createRecordedTree(): IRecordedTree;
@@ -0,0 +1,182 @@
1
+ // The authored tree an op batch describes, and the walk that builds it. Apart from
2
+ // `recording-host` so that file holds the `ITreeHost` surface and this one the mechanism
3
+ 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';
4
+ import { isSymbioteNode } from '@symbiote-native/engine';
5
+ function detach(node) {
6
+ const { parent } = node;
7
+ if (parent === undefined)
8
+ return;
9
+ const at = parent.children.indexOf(node);
10
+ if (at >= 0)
11
+ parent.children.splice(at, 1);
12
+ node.parent = undefined;
13
+ }
14
+ // Lands a `rootTag` onto a whole subtree, the moment the real engine hands every node in the walk
15
+ // its own `ShadowNode`. Never cleared afterwards, see `IRecorded.committedRootTag`
16
+ function markCommitted(node, rootTag) {
17
+ node.committedRootTag = rootTag;
18
+ for (const child of node.children)
19
+ markCommitted(child, rootTag);
20
+ }
21
+ function applyStructureOp(ctx, op) {
22
+ const { strings, instanceHandles } = ctx.batch;
23
+ switch (op.code) {
24
+ case OP_CREATE_ELEMENT:
25
+ // Slot 4 indexes `instanceHandles`, the object Fabric would hand back with an event, which is
26
+ // what a test firing one has to name
27
+ ctx.create(op.a, strings[op.b], {}, instanceHandles[op.d]);
28
+ return true;
29
+ case OP_CREATE_RAW_TEXT:
30
+ ctx.create(op.a, 'RCTRawText', { text: strings[op.b] });
31
+ return true;
32
+ case OP_CREATE_ANCHOR:
33
+ ctx.create(op.a, '', {});
34
+ return true;
35
+ case OP_APPEND_CHILD: {
36
+ const parent = ctx.at(op.a);
37
+ const child = ctx.at(op.b);
38
+ detach(child);
39
+ child.parent = parent;
40
+ parent.children.push(child);
41
+ return true;
42
+ }
43
+ case OP_INSERT_BEFORE: {
44
+ const parent = ctx.at(op.a);
45
+ const child = ctx.at(op.b);
46
+ const before = ctx.at(op.c);
47
+ detach(child);
48
+ child.parent = parent;
49
+ const index = parent.children.indexOf(before);
50
+ const landing = index < 0 ? parent.children.length : index;
51
+ parent.children.splice(landing, 0, child);
52
+ return true;
53
+ }
54
+ case OP_REMOVE_CHILD:
55
+ detach(ctx.at(op.b));
56
+ return true;
57
+ default:
58
+ return false;
59
+ }
60
+ }
61
+ // `OP_SET_TAG`, `OP_SET_OWNED_LISTENER` and `OP_SET_UNDERLAY_SHOWN` are RECORDED and not acted on,
62
+ // т.к. what each one means is resolved in `SymbioteFabricProps.cpp`, and this host must not grow a
63
+ // second copy of those rules. A test that needs the resolved key reads a committed payload instead
64
+ function applyValueOp(ctx, op) {
65
+ const { strings, values } = ctx.batch;
66
+ switch (op.code) {
67
+ case OP_SET_PROP: {
68
+ const node = ctx.at(op.a);
69
+ if (op.c === NO_VALUE)
70
+ delete node.props[strings[op.b]];
71
+ else
72
+ node.props[strings[op.b]] = values[op.c];
73
+ return true;
74
+ }
75
+ case OP_SET_TEXT:
76
+ ctx.at(op.a).props.text = strings[op.b];
77
+ return true;
78
+ case OP_SET_COMPONENT:
79
+ ctx.at(op.a).viewName = strings[op.b];
80
+ return true;
81
+ case OP_SET_TAG:
82
+ ctx.at(op.a).tagName = strings[op.b];
83
+ return true;
84
+ case OP_SET_OWNED_LISTENER:
85
+ ctx.at(op.a).ownedListeners[strings[op.b]] = op.c !== 0;
86
+ return true;
87
+ case OP_SET_UNDERLAY_SHOWN:
88
+ ctx.at(op.a).underlayShown = op.b !== 0;
89
+ return true;
90
+ case OP_COMMIT:
91
+ ctx.onCommit(op.a, ctx.at(op.b));
92
+ return true;
93
+ default:
94
+ return false;
95
+ }
96
+ }
97
+ function walkBatch(ctx) {
98
+ const { ops } = ctx.batch;
99
+ for (let cursor = 0; cursor + OP_STRIDE <= ops.length; cursor += OP_STRIDE) {
100
+ const op = {
101
+ code: ops[cursor],
102
+ a: ops[cursor + 1],
103
+ b: ops[cursor + 2],
104
+ c: ops[cursor + 3],
105
+ d: ops[cursor + 4],
106
+ };
107
+ if (applyStructureOp(ctx, op))
108
+ continue;
109
+ if (applyValueOp(ctx, op))
110
+ continue;
111
+ throw new Error(`applyOps: unknown opcode ${String(op.code)}`);
112
+ }
113
+ }
114
+ function handleAt(batch, slot) {
115
+ const handle = batch.handles[slot];
116
+ if (handle === undefined) {
117
+ throw new Error(`applyOps: slot ${slot} is outside this batch's handles`);
118
+ }
119
+ return handle;
120
+ }
121
+ function newRecord(handle, viewName, props, instanceHandle) {
122
+ // Checked rather than assumed: the batch types handles as `object`, and the whole value of
123
+ // holding the node is being able to hand it back to the engine's own API
124
+ if (!isSymbioteNode(handle)) {
125
+ throw new Error('applyOps: a handle in this batch is not an engine node');
126
+ }
127
+ return {
128
+ handle,
129
+ instanceHandle,
130
+ viewName,
131
+ tagName: '',
132
+ ownedListeners: {},
133
+ underlayShown: false,
134
+ props,
135
+ parent: undefined,
136
+ children: [],
137
+ committedRootTag: undefined,
138
+ };
139
+ }
140
+ export function createRecordedTree() {
141
+ let recorded = new WeakMap();
142
+ // A `WeakMap` cannot be enumerated and `find` has to start somewhere. Strong references, so
143
+ // `forget()` is what a long file calls to stop this growing
144
+ const authored = [];
145
+ const nodeOf = (handle, what) => {
146
+ const node = recorded.get(handle);
147
+ if (node === undefined) {
148
+ throw new Error(`${what}: handle names no node in this tree`);
149
+ }
150
+ return node;
151
+ };
152
+ const tree = {
153
+ commits: 0,
154
+ authored,
155
+ recordedOf: handle => recorded.get(handle),
156
+ nodeOf,
157
+ applyOps(batch) {
158
+ walkBatch({
159
+ batch,
160
+ at: slot => nodeOf(handleAt(batch, slot), 'applyOps'),
161
+ create: (slot, viewName, props, instanceHandle) => {
162
+ const node = newRecord(handleAt(batch, slot), viewName, props, instanceHandle);
163
+ recorded.set(node.handle, node);
164
+ authored.push(node);
165
+ },
166
+ onCommit: (rootTag, node) => {
167
+ tree.commits += 1;
168
+ markCommitted(node, rootTag);
169
+ },
170
+ });
171
+ },
172
+ reset() {
173
+ authored.length = 0;
174
+ tree.commits = 0;
175
+ },
176
+ forget() {
177
+ recorded = new WeakMap();
178
+ tree.reset();
179
+ },
180
+ };
181
+ return tree;
182
+ }
@@ -17,6 +17,18 @@
17
17
  */
18
18
  import { type ISymbioteNode } from '@symbiote-native/engine';
19
19
  import type { ITreeHost } from '@symbiote-native/engine';
20
+ export type IRecorded = {
21
+ handle: ISymbioteNode;
22
+ instanceHandle: unknown;
23
+ viewName: string;
24
+ tagName: string;
25
+ ownedListeners: Record<string, boolean>;
26
+ underlayShown: boolean;
27
+ props: Record<string, unknown>;
28
+ parent: IRecorded | undefined;
29
+ children: IRecorded[];
30
+ committedRootTag: number | undefined;
31
+ };
20
32
  export type IRecordingHost = ITreeHost & {
21
33
  /** How many commits the engine asked for. The op stream's own count, not a tree's. */
22
34
  commits: number;
@@ -15,8 +15,8 @@
15
15
  * A test that needs the committed tree belongs in `core/engine/cpp/tests/js`, where React Native
16
16
  * answers. Asking one of those questions here gets `undefined` rather than a plausible lie.
17
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';
18
+ import { fabricProps, propsOf, setTreeHost, } from '@symbiote-native/engine';
19
+ import { createRecordedTree } from './recorded-tree.js';
20
20
  /**
21
21
  * Install the recording host, with the minimum slot the engine insists on.
22
22
  *
@@ -65,160 +65,26 @@ export function payloadOf(node) {
65
65
  */
66
66
  const NO_TAG = -1;
67
67
  export function createRecordingHost() {
68
- let recorded = new WeakMap();
68
+ const tree = createRecordedTree();
69
+ const { nodeOf } = tree;
69
70
  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;
71
+ const commands = [];
72
+ const responderHandovers = [];
73
+ const accessibilityEvents = [];
74
+ const clearRecords = () => {
75
+ tree.reset();
76
+ commands.length = 0;
77
+ responderHandovers.length = 0;
78
+ accessibilityEvents.length = 0;
80
79
  };
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
- },
80
+ return {
81
+ get commits() {
82
+ return tree.commits;
83
+ },
84
+ commands,
85
+ responderHandovers,
86
+ accessibilityEvents,
87
+ applyOps: tree.applyOps,
222
88
  propOf(handle, key) {
223
89
  return nodeOf(handle, 'propOf').props[key];
224
90
  },
@@ -227,29 +93,11 @@ export function createRecordingHost() {
227
93
  },
228
94
  // Nothing is memoized here, so there is nothing to invalidate.
229
95
  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
- */
96
+ // That the ops COMMITTED this node, and nothing more. Read off `committedRootTag` and not a
97
+ // live walk, т.к. `removeChild` cuts the parent link before the teardown sweep runs in the
98
+ // SAME commit
251
99
  committedRecordOf(handle) {
252
- const node = recorded.get(handle);
100
+ const node = tree.recordedOf(handle);
253
101
  if (node === undefined || node.committedRootTag === undefined) {
254
102
  return undefined;
255
103
  }
@@ -259,18 +107,9 @@ export function createRecordingHost() {
259
107
  rootTag: node.committedRootTag,
260
108
  };
261
109
  },
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
- */
110
+ // REFUSED loudly, т.к. the payload is `SymbioteFabricProps.cpp`'s and that code does not run
111
+ // in vitest. A test asking this belongs in `core/engine/cpp/tests/js`, where the real builder
112
+ // does run
274
113
  committedPayloadOf() {
275
114
  throw new Error('recording host: committedPayloadOf needs the real payload builder — move this to an itest');
276
115
  },
@@ -304,9 +143,9 @@ export function createRecordingHost() {
304
143
  walk(nodeOf(root, 'subtreesOf'));
305
144
  return out;
306
145
  },
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.
146
+ // The twin of the engine's narrowed walk, and it MUST narrow here too: the vitest suite is
147
+ // where the sweep is asserted, so handing back everything would pass those cases whatever the
148
+ // engine does
310
149
  teardownSubtreesOf(roots) {
311
150
  const out = [];
312
151
  const walk = (node, isRoot) => {
@@ -355,7 +194,7 @@ export function createRecordingHost() {
355
194
  // carry a request the engine MADE, so they are recorded; the measuring three want an answer
356
195
  // only a platform has, and there is none here.
357
196
  dispatchCommand(handle, commandName, args) {
358
- host.commands.push({
197
+ commands.push({
359
198
  handle,
360
199
  viewName: nodeOf(handle, 'dispatchCommand').viewName,
361
200
  commandName,
@@ -363,28 +202,28 @@ export function createRecordingHost() {
363
202
  });
364
203
  },
365
204
  sendAccessibilityEvent(handle, eventType) {
366
- host.accessibilityEvents.push({ handle, eventType });
205
+ accessibilityEvents.push({ handle, eventType });
367
206
  },
368
207
  measure(_handle, _callback) { },
369
208
  measureInWindow(_handle, _callback) { },
209
+ // No Yoga layout runs headless, so every node reads one zero rect instead of a fake number
210
+ getBoundingClientRect() {
211
+ return { x: 0, y: 0, width: 0, height: 0 };
212
+ },
370
213
  measureLayout(_handle, _relativeTo, onFail, _onSuccess) {
371
214
  onFail();
372
215
  },
373
216
  setIsJSResponder(handle, isResponder, blockNativeResponder) {
374
- host.responderHandovers.push({
375
- handle,
376
- isResponder,
377
- blockNativeResponder,
378
- });
217
+ responderHandovers.push({ handle, isResponder, blockNativeResponder });
379
218
  },
380
219
  registerEventHandler(handler) {
381
220
  eventHandler = handler;
382
221
  },
383
222
  find(predicate) {
384
- return created.find(predicate);
223
+ return tree.authored.find(predicate);
385
224
  },
386
225
  findAll(predicate) {
387
- return created.filter(predicate);
226
+ return tree.authored.filter(predicate);
388
227
  },
389
228
  fireEvent(handle, topLevelType, nativeEvent = {}) {
390
229
  if (eventHandler === undefined) {
@@ -392,18 +231,10 @@ export function createRecordingHost() {
392
231
  }
393
232
  eventHandler(handle, topLevelType, nativeEvent);
394
233
  },
395
- reset() {
396
- created = [];
397
- host.commits = 0;
398
- host.commands.length = 0;
399
- host.responderHandovers.length = 0;
400
- host.accessibilityEvents.length = 0;
401
- },
234
+ reset: clearRecords,
402
235
  forget() {
403
- recorded = new WeakMap();
404
- created = [];
405
- host.reset();
236
+ tree.forget();
237
+ clearRecords();
406
238
  },
407
239
  };
408
- return host;
409
240
  }
package/build/wait-for.js CHANGED
@@ -8,19 +8,14 @@
8
8
  // proxy breaks, and the failure reads as a product bug — a press that produced no event, change
9
9
  // detection that never stopped — rather than as the harness giving up early.
10
10
  //
11
- // Measured 2026-08-19: three tests across two adapters failed only inside a loaded `vitest run`
12
- // and passed in isolation, every one of them on a fixed tick count.
13
- //
14
11
  // Use `waitUntil` when a condition names what you are waiting for, and `waitForQuiet` when the
15
12
  // thing to wait for is the ABSENCE of further work. Do not raise a tick count to fix a flake —
16
13
  // that trades a fast failure for a slow one and keeps the race.
17
14
  const TICK_MS = 0;
18
15
  const DEFAULT_TIMEOUT_MS = 2_000;
19
16
  // A tick count cannot express "no work arrived", only "the queue drained N times" — and how much
20
- // wall time that spans is a property of the machine. Measured 2026-09-02 on the FlatList window:
21
- // exactly one deferred commit lands 30-60 ticks after a 3-tick settle, so an idle machine declared
22
- // quiet before the list's own batch and a loaded one caught it. The producer is RN's VirtualizedList
23
- // batching period (50ms), the longest deferred one in this repo; quiet has to outlast it.
17
+ // wall time that spans is a property of the machine. The longest deferred producer in this repo
18
+ // is RN's VirtualizedList batching period (50ms); quiet has to outlast it.
24
19
  const DEFAULT_QUIET_MS = 75;
25
20
  // Read off the host rather than imported, the same reason the engine's animations/raf.ts does:
26
21
  // this package's tsconfig carries no DOM and no Node lib, so the ambient names do not exist.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/test-utils",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
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": {
@@ -40,7 +40,7 @@
40
40
  "access": "public"
41
41
  },
42
42
  "dependencies": {
43
- "@symbiote-native/engine": "1.3.0"
43
+ "@symbiote-native/engine": "1.4.0"
44
44
  },
45
45
  "scripts": {
46
46
  "typecheck": "tsc --build",