@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.
- package/build/recorded-tree.d.ts +14 -0
- package/build/recorded-tree.js +182 -0
- package/build/recording-host.d.ts +12 -0
- package/build/recording-host.js +42 -211
- package/build/wait-for.js +2 -7
- package/package.json +2 -2
|
@@ -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;
|
package/build/recording-host.js
CHANGED
|
@@ -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 {
|
|
19
|
-
import {
|
|
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
|
-
|
|
68
|
+
const tree = createRecordedTree();
|
|
69
|
+
const { nodeOf } = tree;
|
|
69
70
|
let eventHandler;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
308
|
-
// the sweep
|
|
309
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
223
|
+
return tree.authored.find(predicate);
|
|
385
224
|
},
|
|
386
225
|
findAll(predicate) {
|
|
387
|
-
return
|
|
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
|
-
|
|
404
|
-
|
|
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.
|
|
21
|
-
//
|
|
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
|
+
"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.
|
|
43
|
+
"@symbiote-native/engine": "1.4.0"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"typecheck": "tsc --build",
|