@symbiote-native/test-utils 0.2.0 → 0.3.1
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/README.md +28 -0
- package/build/host-crossings.d.ts +21 -0
- package/build/host-crossings.js +61 -0
- package/build/index.d.ts +3 -1
- package/build/index.js +14 -4
- package/build/live-tree.d.ts +114 -0
- package/build/live-tree.js +135 -0
- package/build/recording-host.d.ts +144 -0
- package/build/recording-host.js +385 -0
- package/build/wait-for.d.ts +9 -1
- package/build/wait-for.js +30 -7
- package/package.json +14 -1
- package/build/fake-fabric.d.ts +0 -42
- package/build/fake-fabric.js +0 -110
package/README.md
CHANGED
|
@@ -69,6 +69,34 @@ stays `null`, so a test can tell "explicitly reset" apart from "never set"), and
|
|
|
69
69
|
throws on an illegal family reparent. A persistence bug in the fake is fixed once, here, for every
|
|
70
70
|
test that depends on it.
|
|
71
71
|
|
|
72
|
+
## Waiting for async settling
|
|
73
|
+
|
|
74
|
+
`waitUntil(condition, label, timeoutMs?)` polls once per macrotask until `condition()` holds and
|
|
75
|
+
throws (naming `label`) on timeout — the honest replacement for a fixed `setTimeout` tick count,
|
|
76
|
+
which is only a proxy for "the framework has settled" and breaks under a loaded test run.
|
|
77
|
+
`waitForQuiet(sample, label, stableTicks?, timeoutMs?)` waits until `sample()` returns the same
|
|
78
|
+
value across `stableTicks` consecutive macrotasks and returns that settled value — the shape for
|
|
79
|
+
"work has stopped arriving" (a batched commit, a zoneless change-detection pass, a press-timing
|
|
80
|
+
timer). `advanceTicks(count)` is kept for the genuine "let the queue drain N times" case. Do not
|
|
81
|
+
raise a tick count to fix a flaky test — that trades a fast failure for a slow one and keeps the
|
|
82
|
+
race; reach for `waitUntil`/`waitForQuiet` instead.
|
|
83
|
+
|
|
84
|
+
## Committed-payload assertions
|
|
85
|
+
|
|
86
|
+
`normalizeCommitted(nodes)` strips per-mount identity (`tag`, `instanceHandle`, `parentFamilyTag`)
|
|
87
|
+
from a committed Fabric tree. `expectCommittedProps(tree, testID, expected)` finds the node carrying
|
|
88
|
+
`testID` and requires `expected`'s keys to be present with those values — the check that a per-
|
|
89
|
+
primitive fold RAN. It exists because a bare tag inherits nothing a wrapper component used to do:
|
|
90
|
+
prop defaults, alias renames and bag folds all lived in the wrapper, so one that failed to move down
|
|
91
|
+
is silently dropped (a lost `ellipsizeMode`, a never-applied `id -> nativeID`) with every other test
|
|
92
|
+
green.
|
|
93
|
+
|
|
94
|
+
Pass the value a fold PRODUCES, never the one the author wrote — `{ nativeID: 'x' }` for an authored
|
|
95
|
+
`id="x"`. An expectation restating the input passes with the fold deleted.
|
|
96
|
+
|
|
97
|
+
`assertCommittedSomething(tree, name)` is the control: `committed` is `[]` until `completeRoot`
|
|
98
|
+
runs, so a mount that never flushed satisfies almost anything read off it.
|
|
99
|
+
|
|
72
100
|
## What it does NOT do
|
|
73
101
|
|
|
74
102
|
- It is not a mocking framework — there's nothing to configure beyond calling `installFabric()`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many times JS asked the installed tree host a question, since the last reset.
|
|
3
|
+
*
|
|
4
|
+
* Generic over any `ITreeHost` — `installFabric()`'s own `applierWalk.hostCrossings` did the same
|
|
5
|
+
* counting, but wrapped only its OWN mirror host, so a file measuring "does the dispatch code cross
|
|
6
|
+
* the host once per event, not once per ancestor" had no port to `installRecordingFabric()`. The
|
|
7
|
+
* subject here is a JS-SIDE call pattern (how many times the engine's dispatch code reaches across
|
|
8
|
+
* the host boundary), not anything about what Fabric does with the call — so it needs no committed
|
|
9
|
+
* tree and no clone protocol, only a host whose methods can be counted, which any `ITreeHost` is.
|
|
10
|
+
*
|
|
11
|
+
* `applyOps` is excluded from the count, matching the mirror's own convention: it is the mutation
|
|
12
|
+
* batch a commit carries, not a "question", and counting it would conflate two different budgets.
|
|
13
|
+
*/
|
|
14
|
+
import type { ITreeHost } from '@symbiote-native/engine';
|
|
15
|
+
export type IHostCrossingTracker = {
|
|
16
|
+
/** Calls into the tracked host's methods (excluding `applyOps`) since the last `reset()`. */
|
|
17
|
+
crossings: number;
|
|
18
|
+
reset(): void;
|
|
19
|
+
};
|
|
20
|
+
/** Wraps `host`'s own methods IN PLACE, so whatever holds `host` (e.g. `setTreeHost`) sees the count. */
|
|
21
|
+
export declare function trackHostCrossings(host: ITreeHost): IHostCrossingTracker;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many times JS asked the installed tree host a question, since the last reset.
|
|
3
|
+
*
|
|
4
|
+
* Generic over any `ITreeHost` — `installFabric()`'s own `applierWalk.hostCrossings` did the same
|
|
5
|
+
* counting, but wrapped only its OWN mirror host, so a file measuring "does the dispatch code cross
|
|
6
|
+
* the host once per event, not once per ancestor" had no port to `installRecordingFabric()`. The
|
|
7
|
+
* subject here is a JS-SIDE call pattern (how many times the engine's dispatch code reaches across
|
|
8
|
+
* the host boundary), not anything about what Fabric does with the call — so it needs no committed
|
|
9
|
+
* tree and no clone protocol, only a host whose methods can be counted, which any `ITreeHost` is.
|
|
10
|
+
*
|
|
11
|
+
* `applyOps` is excluded from the count, matching the mirror's own convention: it is the mutation
|
|
12
|
+
* batch a commit carries, not a "question", and counting it would conflate two different budgets.
|
|
13
|
+
*/
|
|
14
|
+
// The mirror's own `countingHost` (tree-applier.ts) can safely `Object.keys()` its host, because
|
|
15
|
+
// the object it wraps carries ONLY these members. `installRecordingFabric()`'s host does not: it is
|
|
16
|
+
// simultaneously the installed `ITreeHost` AND the test-facing API (`fireEvent`, `reset`, `commands`,
|
|
17
|
+
// `find`…), all on one object, so `Object.keys` would ALSO wrap and count `fireEvent` itself —
|
|
18
|
+
// measured: a 20-event budget that should read 1 crossing/event read 2, because firing the event was
|
|
19
|
+
// one counted call before the dispatch code's own `ancestorsOf` added the second. Listed explicitly,
|
|
20
|
+
// matching `ITreeHost` in tree-host.ts — keep the two in sync if that interface gains a member.
|
|
21
|
+
const TREE_HOST_METHOD_NAMES = [
|
|
22
|
+
'applyOps',
|
|
23
|
+
'propOf',
|
|
24
|
+
'propsOf',
|
|
25
|
+
'markPropsDirty',
|
|
26
|
+
'committedRecordOf',
|
|
27
|
+
'parentOf',
|
|
28
|
+
'childrenOf',
|
|
29
|
+
'nextSiblingOf',
|
|
30
|
+
'parentsOf',
|
|
31
|
+
'subtreesOf',
|
|
32
|
+
'ancestorsOf',
|
|
33
|
+
'census',
|
|
34
|
+
'dispatchCommand',
|
|
35
|
+
'sendAccessibilityEvent',
|
|
36
|
+
'measure',
|
|
37
|
+
'measureInWindow',
|
|
38
|
+
'measureLayout',
|
|
39
|
+
'setIsJSResponder',
|
|
40
|
+
];
|
|
41
|
+
/** Wraps `host`'s own methods IN PLACE, so whatever holds `host` (e.g. `setTreeHost`) sees the count. */
|
|
42
|
+
export function trackHostCrossings(host) {
|
|
43
|
+
const tracker = {
|
|
44
|
+
crossings: 0,
|
|
45
|
+
reset() {
|
|
46
|
+
tracker.crossings = 0;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
for (const name of TREE_HOST_METHOD_NAMES) {
|
|
50
|
+
if (name === 'applyOps')
|
|
51
|
+
continue;
|
|
52
|
+
const member = Reflect.get(host, name);
|
|
53
|
+
if (typeof member !== 'function')
|
|
54
|
+
continue;
|
|
55
|
+
Reflect.set(host, name, (...args) => {
|
|
56
|
+
tracker.crossings += 1;
|
|
57
|
+
return Reflect.apply(member, host, args);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return tracker;
|
|
61
|
+
}
|
package/build/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { createRecordingHost, installRecordingFabric, payloadOf, type IAuthoredNode, type IRecordingHost, } from './recording-host';
|
|
2
|
+
export { censusLive, createLiveTree, type ILiveCensus, type ILiveNode, type ILiveTree, } from './live-tree';
|
|
3
|
+
export { trackHostCrossings, type IHostCrossingTracker, } from './host-crossings';
|
|
2
4
|
export * from './wait-for';
|
package/build/index.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
// @symbiote-native/test-utils: shared, framework-agnostic test harness. Imported by
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
1
|
+
// @symbiote-native/test-utils: shared, framework-agnostic test harness. Imported by the co-located
|
|
2
|
+
// tests across engine, adapters, and the example apps.
|
|
3
|
+
//
|
|
4
|
+
// The TypeScript mirror (`tree-applier.ts`/`fake-fabric.ts`, `installFabric()`) that used to stand
|
|
5
|
+
// here is GONE (`.docs/mirror-elimination.md`) — every test commits into either the real C++ engine
|
|
6
|
+
// (`core/engine/cpp/tests/js/*.itest.ts`, driven by `scripts/run-itests.mjs`) or
|
|
7
|
+
// `installRecordingFabric()` below, which records the op stream and derives nothing.
|
|
8
|
+
export { createRecordingHost, installRecordingFabric, payloadOf, } from './recording-host.js';
|
|
9
|
+
// Reading the tree that is ON SCREEN over that recording — the lens for every residency question
|
|
10
|
+
// (`is this still mounted`, `did the pop remove it`) that `find` cannot answer, because `find`
|
|
11
|
+
// searches the creation log. Anchors flatten, as the commit walk flattens them. See its header.
|
|
12
|
+
export { censusLive, createLiveTree, } from './live-tree.js';
|
|
13
|
+
// How many times JS crossed the host boundary — generic over whichever host is installed.
|
|
14
|
+
export { trackHostCrossings, } from './host-crossings.js';
|
|
5
15
|
export * from './wait-for.js';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the tree that is ON SCREEN, over the recording host.
|
|
3
|
+
*
|
|
4
|
+
* **This is not a second tree.** It derives nothing and decides nothing: the children come from the
|
|
5
|
+
* engine's own child links, the view name from `componentOf`, the props from `propsOf`, the payload
|
|
6
|
+
* from the engine's own `fabricProps`. It is a lens, and the reason it has to exist as one is that
|
|
7
|
+
* the obvious alternative is wrong in a way that passes.
|
|
8
|
+
*
|
|
9
|
+
* **`host.find` searches the CREATION LOG.** A recording remembers every node it ever saw created,
|
|
10
|
+
* so a node the app removed ten renders ago still answers it. Half of what a converted test asks is
|
|
11
|
+
* a residency question — a popped route, an evicted list cell, a portal toggled off — and phrased
|
|
12
|
+
* against the record, "this is gone" passes forever whatever the adapter did. Those questions walk
|
|
13
|
+
* the LIVE child links from a known root, which is what everything here does.
|
|
14
|
+
*
|
|
15
|
+
* **Anchors are FLATTENED**, the commit walk's own rule (`renderableChildren`). An anchor is
|
|
16
|
+
* structural bookkeeping nothing native ever sees, so its children stand in its place. It is not a
|
|
17
|
+
* detail on two adapters: Svelte leaves an anchor per block, Angular one per composed component,
|
|
18
|
+
* and a positional read that counts them is reading a different tree than the one it means.
|
|
19
|
+
*
|
|
20
|
+
* The one fact taken from the recording rather than the engine is `instanceHandle` — the object the
|
|
21
|
+
* engine gives Fabric per element, which `fireEvent` has to be aimed at. That is why this binds a
|
|
22
|
+
* host at all.
|
|
23
|
+
*/
|
|
24
|
+
import { type ISymbioteNode } from '@symbiote-native/engine';
|
|
25
|
+
import type { IRecordingHost } from './recording-host';
|
|
26
|
+
/** A node as it currently stands, with both of its readings and its renderable children. */
|
|
27
|
+
export type ILiveNode = {
|
|
28
|
+
/** The Fabric view name it CURRENTLY resolves to — `componentOf`, not the name it was created under. */
|
|
29
|
+
viewName: string;
|
|
30
|
+
/**
|
|
31
|
+
* The INTRINSIC tag the engine was told, or `''` for a node no behavior attached to.
|
|
32
|
+
*
|
|
33
|
+
* What the host was TOLD, as against what a rule made of it — which is the only durable way to
|
|
34
|
+
* locate a node whose platform props are a tag rule in `SymbioteFabricProps.cpp`. The payload
|
|
35
|
+
* this type exposes is built by the TypeScript `fabricProps`, which deliberately carries no copy
|
|
36
|
+
* of those rules, so a locator written as `payload.<key the rule writes>` finds nothing. Several
|
|
37
|
+
* were, and expired together the day the sticky pin moved.
|
|
38
|
+
*/
|
|
39
|
+
tagName: string;
|
|
40
|
+
/** The author's bag: `style` is still an object, the RN processors have not run. */
|
|
41
|
+
props: Readonly<Record<string, unknown>>;
|
|
42
|
+
/** What the engine would hand the renderer: style flattened, the aria fold and processors run. */
|
|
43
|
+
readonly payload: Record<string, unknown>;
|
|
44
|
+
handle: ISymbioteNode;
|
|
45
|
+
/** What `fireEvent` must be aimed at; `undefined` for a raw text or an anchor. */
|
|
46
|
+
instanceHandle: unknown;
|
|
47
|
+
/** The renderable children, anchors flattened. A getter, so a walk costs only what it reads. */
|
|
48
|
+
readonly children: ILiveNode[];
|
|
49
|
+
};
|
|
50
|
+
export type ILiveTree = {
|
|
51
|
+
/**
|
|
52
|
+
* The app's own root — one `box-none` container, the same node `installFabric`'s `appRoot()`
|
|
53
|
+
* returned. Note it is the SURFACE node, and the trees disagree about its NAME: it is created as
|
|
54
|
+
* an `RCTView` and then set to the surface component, so the recording says `RCTView`,
|
|
55
|
+
* `componentOf` says `#surface`, and Fabric commits it as `RootView`.
|
|
56
|
+
*/
|
|
57
|
+
appRoot: () => ISymbioteNode;
|
|
58
|
+
/** One handle as a readable node — the entry point for a positional walk from a known root. */
|
|
59
|
+
nodeOf: (handle: ISymbioteNode) => ILiveNode;
|
|
60
|
+
/** Pre-order, the root included. */
|
|
61
|
+
walkLive: (root: ISymbioteNode, visit: (node: ILiveNode) => void) => void;
|
|
62
|
+
findAllLive: (root: ISymbioteNode, predicate: (node: ILiveNode) => boolean) => ILiveNode[];
|
|
63
|
+
findLive: (root: ISymbioteNode, predicate: (node: ILiveNode) => boolean) => ILiveNode | undefined;
|
|
64
|
+
/** A depth-indented `viewName` outline, for asserting an exact shape. */
|
|
65
|
+
outline: (root: ISymbioteNode) => string[];
|
|
66
|
+
/**
|
|
67
|
+
* The subtree as one line — `RCTView(RCTText(RCTRawText "hello"))`.
|
|
68
|
+
*
|
|
69
|
+
* Byte-compatible with the string the stand-in's own `serialize` produced, so a converted
|
|
70
|
+
* expectation does not have to be rewritten: same bracketing, same empty separator between
|
|
71
|
+
* siblings, the raw text's own string quoted after its name. It reads the FLATTENED tree, which
|
|
72
|
+
* is the right comparison — the committed tree it is being read against cannot hold an anchor.
|
|
73
|
+
*/
|
|
74
|
+
serialize: (root: ISymbioteNode) => string;
|
|
75
|
+
/**
|
|
76
|
+
* Every raw text under `root`, in TREE order — what the screen currently says.
|
|
77
|
+
*
|
|
78
|
+
* Tree order is the half a search over the recording cannot give: the record is in CREATION
|
|
79
|
+
* order, and for a list that reorders its rows the two disagree without either being wrong.
|
|
80
|
+
*/
|
|
81
|
+
texts: (root: ISymbioteNode) => string[];
|
|
82
|
+
};
|
|
83
|
+
export declare function createLiveTree(host: IRecordingHost): ILiveTree;
|
|
84
|
+
/** How many nodes the adapter allocated, and how many of those are anchors. */
|
|
85
|
+
export type ILiveCensus = {
|
|
86
|
+
/** Every retained node in the subtree, anchors INCLUDED. */
|
|
87
|
+
nodes: number;
|
|
88
|
+
anchors: number;
|
|
89
|
+
/** `nodes - anchors`: what the adapter allocated for something the app actually wrote. */
|
|
90
|
+
nonAnchors: number;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Count the retained subtree, asking no host.
|
|
94
|
+
*
|
|
95
|
+
* This replaces `censusRetainedTree` in the anchor-cost probes, and the replacement is not a
|
|
96
|
+
* convenience. `censusRetainedTree` delegates to `treeHost().census()`, which the TypeScript applier
|
|
97
|
+
* answers truthfully and NOTHING ELSE does — the native host returns zeroes deliberately (census is
|
|
98
|
+
* off the ABI, `native-tree-host.ts` says why), so a probe reading it on a device has always seen
|
|
99
|
+
* nothing. A number only the stand-in can produce is the definition of a mirror measurement.
|
|
100
|
+
*
|
|
101
|
+
* What those probes actually claim is about the ENGINE: how many nodes the adapter allocated, and
|
|
102
|
+
* how many of them are anchors. Both are read here off the engine's own child links and its own
|
|
103
|
+
* `isAnchor`, so the count means the same thing under every host.
|
|
104
|
+
*
|
|
105
|
+
* It does NOT answer the applier's `renderable`, and that is deliberate: `renderable` subtracts the
|
|
106
|
+
* empty raw texts as well, which is the COMMIT's rule and not the tree's. `empty-raw-text.itest.ts`
|
|
107
|
+
* is where that one is settled, against the real commit.
|
|
108
|
+
*
|
|
109
|
+
* The walk does not flatten anchors — it is counting them, which is the opposite of what `walkLive`
|
|
110
|
+
* is for.
|
|
111
|
+
*
|
|
112
|
+
* Variadic, because a surface holds a LIST of top-level nodes: `censusLive(...surface.children)`.
|
|
113
|
+
*/
|
|
114
|
+
export declare function censusLive(...roots: readonly ISymbioteNode[]): ILiveCensus;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the tree that is ON SCREEN, over the recording host.
|
|
3
|
+
*
|
|
4
|
+
* **This is not a second tree.** It derives nothing and decides nothing: the children come from the
|
|
5
|
+
* engine's own child links, the view name from `componentOf`, the props from `propsOf`, the payload
|
|
6
|
+
* from the engine's own `fabricProps`. It is a lens, and the reason it has to exist as one is that
|
|
7
|
+
* the obvious alternative is wrong in a way that passes.
|
|
8
|
+
*
|
|
9
|
+
* **`host.find` searches the CREATION LOG.** A recording remembers every node it ever saw created,
|
|
10
|
+
* so a node the app removed ten renders ago still answers it. Half of what a converted test asks is
|
|
11
|
+
* a residency question — a popped route, an evicted list cell, a portal toggled off — and phrased
|
|
12
|
+
* against the record, "this is gone" passes forever whatever the adapter did. Those questions walk
|
|
13
|
+
* the LIVE child links from a known root, which is what everything here does.
|
|
14
|
+
*
|
|
15
|
+
* **Anchors are FLATTENED**, the commit walk's own rule (`renderableChildren`). An anchor is
|
|
16
|
+
* structural bookkeeping nothing native ever sees, so its children stand in its place. It is not a
|
|
17
|
+
* detail on two adapters: Svelte leaves an anchor per block, Angular one per composed component,
|
|
18
|
+
* and a positional read that counts them is reading a different tree than the one it means.
|
|
19
|
+
*
|
|
20
|
+
* The one fact taken from the recording rather than the engine is `instanceHandle` — the object the
|
|
21
|
+
* engine gives Fabric per element, which `fireEvent` has to be aimed at. That is why this binds a
|
|
22
|
+
* host at all.
|
|
23
|
+
*/
|
|
24
|
+
import { childrenOf, componentOf, fabricProps, isAnchor, propsOf, } from '@symbiote-native/engine';
|
|
25
|
+
export function createLiveTree(host) {
|
|
26
|
+
const kidsOf = (handle) => childrenOf(handle).flatMap(child => isAnchor(child) ? kidsOf(child) : [child]);
|
|
27
|
+
const nodeOf = (handle) => ({
|
|
28
|
+
viewName: componentOf(handle),
|
|
29
|
+
tagName: host.find(one => one.handle === handle)?.tagName ?? '',
|
|
30
|
+
props: propsOf(handle),
|
|
31
|
+
get payload() {
|
|
32
|
+
return fabricProps(handle, propsOf(handle));
|
|
33
|
+
},
|
|
34
|
+
handle,
|
|
35
|
+
instanceHandle: host.find(one => one.handle === handle)?.instanceHandle,
|
|
36
|
+
get children() {
|
|
37
|
+
return kidsOf(handle).map(nodeOf);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const walkLive = (root, visit) => {
|
|
41
|
+
visit(nodeOf(root));
|
|
42
|
+
for (const child of kidsOf(root))
|
|
43
|
+
walkLive(child, visit);
|
|
44
|
+
};
|
|
45
|
+
const findAllLive = (root, predicate) => {
|
|
46
|
+
const found = [];
|
|
47
|
+
walkLive(root, node => {
|
|
48
|
+
if (predicate(node))
|
|
49
|
+
found.push(node);
|
|
50
|
+
});
|
|
51
|
+
return found;
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
nodeOf,
|
|
55
|
+
walkLive,
|
|
56
|
+
findAllLive,
|
|
57
|
+
findLive: (root, predicate) => findAllLive(root, predicate)[0],
|
|
58
|
+
appRoot: () => {
|
|
59
|
+
const root = host.find(node => node.props.pointerEvents === 'box-none');
|
|
60
|
+
if (root === undefined)
|
|
61
|
+
throw new Error('no app root was created');
|
|
62
|
+
return root.handle;
|
|
63
|
+
},
|
|
64
|
+
serialize: (root) => {
|
|
65
|
+
const one = (handle) => {
|
|
66
|
+
const name = componentOf(handle);
|
|
67
|
+
const text = name === 'RCTRawText'
|
|
68
|
+
? ` "${String(fabricProps(handle, propsOf(handle)).text)}"`
|
|
69
|
+
: '';
|
|
70
|
+
const kids = kidsOf(handle);
|
|
71
|
+
const nested = kids.length === 0 ? '' : `(${kids.map(one).join('')})`;
|
|
72
|
+
return `${name}${text}${nested}`;
|
|
73
|
+
};
|
|
74
|
+
return one(root);
|
|
75
|
+
},
|
|
76
|
+
texts: (root) => {
|
|
77
|
+
const out = [];
|
|
78
|
+
const visit = (handle) => {
|
|
79
|
+
if (componentOf(handle) === 'RCTRawText') {
|
|
80
|
+
out.push(String(fabricProps(handle, propsOf(handle)).text));
|
|
81
|
+
}
|
|
82
|
+
for (const child of kidsOf(handle))
|
|
83
|
+
visit(child);
|
|
84
|
+
};
|
|
85
|
+
visit(root);
|
|
86
|
+
return out;
|
|
87
|
+
},
|
|
88
|
+
outline: (root) => {
|
|
89
|
+
const lines = [];
|
|
90
|
+
const visit = (handle, depth) => {
|
|
91
|
+
lines.push(`${' '.repeat(depth)}${componentOf(handle)}`);
|
|
92
|
+
for (const child of kidsOf(handle))
|
|
93
|
+
visit(child, depth + 1);
|
|
94
|
+
};
|
|
95
|
+
visit(root, 0);
|
|
96
|
+
return lines;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Count the retained subtree, asking no host.
|
|
102
|
+
*
|
|
103
|
+
* This replaces `censusRetainedTree` in the anchor-cost probes, and the replacement is not a
|
|
104
|
+
* convenience. `censusRetainedTree` delegates to `treeHost().census()`, which the TypeScript applier
|
|
105
|
+
* answers truthfully and NOTHING ELSE does — the native host returns zeroes deliberately (census is
|
|
106
|
+
* off the ABI, `native-tree-host.ts` says why), so a probe reading it on a device has always seen
|
|
107
|
+
* nothing. A number only the stand-in can produce is the definition of a mirror measurement.
|
|
108
|
+
*
|
|
109
|
+
* What those probes actually claim is about the ENGINE: how many nodes the adapter allocated, and
|
|
110
|
+
* how many of them are anchors. Both are read here off the engine's own child links and its own
|
|
111
|
+
* `isAnchor`, so the count means the same thing under every host.
|
|
112
|
+
*
|
|
113
|
+
* It does NOT answer the applier's `renderable`, and that is deliberate: `renderable` subtracts the
|
|
114
|
+
* empty raw texts as well, which is the COMMIT's rule and not the tree's. `empty-raw-text.itest.ts`
|
|
115
|
+
* is where that one is settled, against the real commit.
|
|
116
|
+
*
|
|
117
|
+
* The walk does not flatten anchors — it is counting them, which is the opposite of what `walkLive`
|
|
118
|
+
* is for.
|
|
119
|
+
*
|
|
120
|
+
* Variadic, because a surface holds a LIST of top-level nodes: `censusLive(...surface.children)`.
|
|
121
|
+
*/
|
|
122
|
+
export function censusLive(...roots) {
|
|
123
|
+
let nodes = 0;
|
|
124
|
+
let anchors = 0;
|
|
125
|
+
const visit = (handle) => {
|
|
126
|
+
nodes += 1;
|
|
127
|
+
if (isAnchor(handle))
|
|
128
|
+
anchors += 1;
|
|
129
|
+
for (const child of childrenOf(handle))
|
|
130
|
+
visit(child);
|
|
131
|
+
};
|
|
132
|
+
for (const root of roots)
|
|
133
|
+
visit(root);
|
|
134
|
+
return { nodes, anchors, nonAnchors: nodes - anchors };
|
|
135
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
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 { type ISymbioteNode } from '@symbiote-native/engine';
|
|
19
|
+
import type { ITreeHost } from '@symbiote-native/engine';
|
|
20
|
+
export type IRecordingHost = ITreeHost & {
|
|
21
|
+
/** How many commits the engine asked for. The op stream's own count, not a tree's. */
|
|
22
|
+
commits: number;
|
|
23
|
+
/**
|
|
24
|
+
* The imperative calls the engine made, in order.
|
|
25
|
+
*
|
|
26
|
+
* These are the engine's own OUTPUT — it asked the platform to scroll, to focus, to take the
|
|
27
|
+
* responder — so recording them is reading what our code did, not deciding what the renderer
|
|
28
|
+
* would have done with it. The handle is the authored node, which is what a test holds.
|
|
29
|
+
*/
|
|
30
|
+
commands: {
|
|
31
|
+
handle: object;
|
|
32
|
+
/** As the ops named it — the authored view name, not a committed one. */
|
|
33
|
+
viewName: string;
|
|
34
|
+
commandName: string;
|
|
35
|
+
args: readonly unknown[];
|
|
36
|
+
}[];
|
|
37
|
+
responderHandovers: {
|
|
38
|
+
handle: object;
|
|
39
|
+
isResponder: boolean;
|
|
40
|
+
blockNativeResponder: boolean;
|
|
41
|
+
}[];
|
|
42
|
+
accessibilityEvents: {
|
|
43
|
+
handle: object;
|
|
44
|
+
eventType: string;
|
|
45
|
+
}[];
|
|
46
|
+
/**
|
|
47
|
+
* Hand an event to the handler the engine registered, naming the target yourself.
|
|
48
|
+
*
|
|
49
|
+
* Not a tree read, and not the platform deciding anything: the engine installs this handler
|
|
50
|
+
* through `registerEventHandler` and this plays it back verbatim. What a REAL gesture needs —
|
|
51
|
+
* hit-testing coordinates to a target — is the renderer's answer and is not on offer here; a
|
|
52
|
+
* caller must already know which node it means, which is what an engine-side dispatch test does.
|
|
53
|
+
*/
|
|
54
|
+
fireEvent: (handle: object, topLevelType: string, nativeEvent?: Record<string, unknown>) => void;
|
|
55
|
+
/** The slot call, kept here so one object owns both halves of the round trip. */
|
|
56
|
+
registerEventHandler: (handler: IEventHandler) => void;
|
|
57
|
+
/**
|
|
58
|
+
* The first AUTHORED node matching `predicate` — the handle a test needs when the adapter, not
|
|
59
|
+
* the test, created the node.
|
|
60
|
+
*
|
|
61
|
+
* **The authored tree, and the word carries the whole caveat.** These are the nodes the ops named
|
|
62
|
+
* and the props they carried; nothing here says which of them Fabric kept, what it renamed, or
|
|
63
|
+
* what it flattened away. So this answers "find the node the app wrote" — to read its props, to
|
|
64
|
+
* hand it to `fabricProps`, to fire an event at it — and it does NOT answer any question about
|
|
65
|
+
* shape. That one belongs to `committedTree()` in `core/engine/cpp/tests/js`.
|
|
66
|
+
*/
|
|
67
|
+
find: (predicate: (node: IAuthoredNode) => boolean) => IAuthoredNode | undefined;
|
|
68
|
+
/** Every authored node matching `predicate`, in CREATION order — not document order. */
|
|
69
|
+
findAll: (predicate: (node: IAuthoredNode) => boolean) => IAuthoredNode[];
|
|
70
|
+
/**
|
|
71
|
+
* Clear the recordings. The authored tree survives, as it does under `installFabric`.
|
|
72
|
+
*
|
|
73
|
+
* What it also clears, and it is load-bearing: the list `find` searches. A file that mounts a
|
|
74
|
+
* fresh tree per case reuses its `testID`s, so a `find` spanning cases answers with the FIRST
|
|
75
|
+
* match — an earlier case's node, carrying an earlier case's props. Caught by a converted test
|
|
76
|
+
* whose second case read the first case's payload and reported a missing prop.
|
|
77
|
+
*/
|
|
78
|
+
reset: () => void;
|
|
79
|
+
forget: () => void;
|
|
80
|
+
};
|
|
81
|
+
type IEventHandler = (handle: object, topLevelType: string, nativeEvent: Record<string, unknown>) => void;
|
|
82
|
+
/**
|
|
83
|
+
* A node as the OPS described it. Not a committed node and not pretending to be one — there is no
|
|
84
|
+
* tag, because this host never spoke to Fabric and any number here would be an invention.
|
|
85
|
+
*/
|
|
86
|
+
export type IAuthoredNode = {
|
|
87
|
+
/** The engine node itself, so it can be handed straight back to the engine's own API. */
|
|
88
|
+
handle: ISymbioteNode;
|
|
89
|
+
/**
|
|
90
|
+
* The object the engine gave Fabric to hand back with an event — undefined for a raw text or an
|
|
91
|
+
* anchor, which are created with none.
|
|
92
|
+
*
|
|
93
|
+
* It is what `fireEvent` has to be aimed at: the engine's event handler resolves a target by
|
|
94
|
+
* this object, not by the node, so passing the node would silently deliver to nothing.
|
|
95
|
+
*/
|
|
96
|
+
instanceHandle: unknown;
|
|
97
|
+
/** As the ops named it. Fabric may commit it under a different name; that is not known here. */
|
|
98
|
+
viewName: string;
|
|
99
|
+
/**
|
|
100
|
+
* The intrinsic tag, for a node a host behavior attached to; empty for every other node.
|
|
101
|
+
*
|
|
102
|
+
* Recorded so a test can ASK what a node is — the real host resolves a tag's platform props off
|
|
103
|
+
* it and a plain `RCTView` cannot be told from a `<pressable>` any other way. It changes nothing
|
|
104
|
+
* about the payload this host builds; see the `OP_SET_TAG` case.
|
|
105
|
+
*/
|
|
106
|
+
tagName: string;
|
|
107
|
+
/**
|
|
108
|
+
* Which event names a BEHAVIOR owns currently have an app callback wired, by name.
|
|
109
|
+
*
|
|
110
|
+
* The presence only — the callback never crosses and is not here. It is what lets a platform rule
|
|
111
|
+
* resolve a key that depends on whether the app wired anything (`focusable` on a touchable is
|
|
112
|
+
* `focusable !== false && onPress !== undefined && !disabled`). Recorded for the same reason
|
|
113
|
+
* `tagName` is: so a test can ask what the host was TOLD, separately from what a rule made of it.
|
|
114
|
+
*/
|
|
115
|
+
ownedListeners: Record<string, boolean>;
|
|
116
|
+
/** Whether a behavior's feedback is showing — see `OP_SET_UNDERLAY_SHOWN`. */
|
|
117
|
+
underlayShown: boolean;
|
|
118
|
+
props: Readonly<Record<string, unknown>>;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Install the recording host, with the minimum slot the engine insists on.
|
|
122
|
+
*
|
|
123
|
+
* `createSurface` calls `installEventHandler`, which reaches `globalThis.nativeFabricUIManager` and
|
|
124
|
+
* throws if it is absent — so a host alone is not enough to get a surface open. The slot below
|
|
125
|
+
* exists to be PRESENT: every method answers nothing except `registerEventHandler`, which hands the
|
|
126
|
+
* engine's handler to the host so `fireEvent` can play it back.
|
|
127
|
+
*
|
|
128
|
+
* What that does NOT buy is a real gesture. Deciding which node a touch lands on is hit-testing,
|
|
129
|
+
* and that is the renderer's answer; a test that needs it belongs in `core/engine/cpp/tests/js`.
|
|
130
|
+
*/
|
|
131
|
+
export declare function installRecordingFabric(): IRecordingHost;
|
|
132
|
+
/**
|
|
133
|
+
* The Fabric payload for a node — what the engine WOULD hand the renderer for it, built by the
|
|
134
|
+
* engine's own builder rather than read off anything.
|
|
135
|
+
*
|
|
136
|
+
* This exists because the two are easy to confuse and the difference bites: a node's PROPS are the
|
|
137
|
+
* author's bag (`style` is still an object), while the PAYLOAD is what `fabricProps` makes of it —
|
|
138
|
+
* style flattened into top-level keys, the aria fold run, the ten RN processors applied. A test
|
|
139
|
+
* asking about `padding` or `accessibilityRole` or a parsed `backgroundSize` means the payload, and
|
|
140
|
+
* reading the bag instead comes back `undefined` with nothing to explain why.
|
|
141
|
+
*/
|
|
142
|
+
export declare function payloadOf(node: ISymbioteNode): Record<string, unknown>;
|
|
143
|
+
export declare function createRecordingHost(): IRecordingHost;
|
|
144
|
+
export {};
|
|
@@ -0,0 +1,385 @@
|
|
|
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
|
+
nextSiblingOf(handle) {
|
|
284
|
+
const node = nodeOf(handle, 'nextSiblingOf');
|
|
285
|
+
const siblings = node.parent?.children;
|
|
286
|
+
if (siblings === undefined)
|
|
287
|
+
return undefined;
|
|
288
|
+
return siblings[siblings.indexOf(node) + 1]?.handle;
|
|
289
|
+
},
|
|
290
|
+
parentsOf(handles) {
|
|
291
|
+
return handles.map(handle => nodeOf(handle, 'parentsOf').parent?.handle);
|
|
292
|
+
},
|
|
293
|
+
subtreesOf(roots) {
|
|
294
|
+
const out = [];
|
|
295
|
+
const walk = (node) => {
|
|
296
|
+
out.push(node.handle);
|
|
297
|
+
for (const child of node.children)
|
|
298
|
+
walk(child);
|
|
299
|
+
};
|
|
300
|
+
for (const root of roots)
|
|
301
|
+
walk(nodeOf(root, 'subtreesOf'));
|
|
302
|
+
return out;
|
|
303
|
+
},
|
|
304
|
+
ancestorsOf(handle) {
|
|
305
|
+
const chain = [];
|
|
306
|
+
for (let node = nodeOf(handle, 'ancestorsOf'); node !== undefined; node = node.parent) {
|
|
307
|
+
chain.push(node.handle);
|
|
308
|
+
}
|
|
309
|
+
return chain;
|
|
310
|
+
},
|
|
311
|
+
census(roots) {
|
|
312
|
+
let nodes = 0;
|
|
313
|
+
const walk = (node) => {
|
|
314
|
+
nodes += 1;
|
|
315
|
+
for (const child of node.children)
|
|
316
|
+
walk(child);
|
|
317
|
+
};
|
|
318
|
+
for (const root of roots)
|
|
319
|
+
walk(nodeOf(root, 'census'));
|
|
320
|
+
// The skip counts belong to the commit rules, which this host does not have. Zero is the
|
|
321
|
+
// honest answer: nothing here was skipped, because nothing here was decided.
|
|
322
|
+
return {
|
|
323
|
+
nodes,
|
|
324
|
+
anchors: 0,
|
|
325
|
+
emptyRawTexts: 0,
|
|
326
|
+
renderable: nodes,
|
|
327
|
+
flattenWidths: [],
|
|
328
|
+
};
|
|
329
|
+
},
|
|
330
|
+
// The imperative six ask the PLATFORM — a frame, a gesture, an announcement. Three of them
|
|
331
|
+
// carry a request the engine MADE, so they are recorded; the measuring three want an answer
|
|
332
|
+
// only a platform has, and there is none here.
|
|
333
|
+
dispatchCommand(handle, commandName, args) {
|
|
334
|
+
host.commands.push({
|
|
335
|
+
handle,
|
|
336
|
+
viewName: nodeOf(handle, 'dispatchCommand').viewName,
|
|
337
|
+
commandName,
|
|
338
|
+
args,
|
|
339
|
+
});
|
|
340
|
+
},
|
|
341
|
+
sendAccessibilityEvent(handle, eventType) {
|
|
342
|
+
host.accessibilityEvents.push({ handle, eventType });
|
|
343
|
+
},
|
|
344
|
+
measure(_handle, _callback) { },
|
|
345
|
+
measureInWindow(_handle, _callback) { },
|
|
346
|
+
measureLayout(_handle, _relativeTo, onFail, _onSuccess) {
|
|
347
|
+
onFail();
|
|
348
|
+
},
|
|
349
|
+
setIsJSResponder(handle, isResponder, blockNativeResponder) {
|
|
350
|
+
host.responderHandovers.push({
|
|
351
|
+
handle,
|
|
352
|
+
isResponder,
|
|
353
|
+
blockNativeResponder,
|
|
354
|
+
});
|
|
355
|
+
},
|
|
356
|
+
registerEventHandler(handler) {
|
|
357
|
+
eventHandler = handler;
|
|
358
|
+
},
|
|
359
|
+
find(predicate) {
|
|
360
|
+
return created.find(predicate);
|
|
361
|
+
},
|
|
362
|
+
findAll(predicate) {
|
|
363
|
+
return created.filter(predicate);
|
|
364
|
+
},
|
|
365
|
+
fireEvent(handle, topLevelType, nativeEvent = {}) {
|
|
366
|
+
if (eventHandler === undefined) {
|
|
367
|
+
throw new Error('no event handler registered by the renderer');
|
|
368
|
+
}
|
|
369
|
+
eventHandler(handle, topLevelType, nativeEvent);
|
|
370
|
+
},
|
|
371
|
+
reset() {
|
|
372
|
+
created = [];
|
|
373
|
+
host.commits = 0;
|
|
374
|
+
host.commands.length = 0;
|
|
375
|
+
host.responderHandovers.length = 0;
|
|
376
|
+
host.accessibilityEvents.length = 0;
|
|
377
|
+
},
|
|
378
|
+
forget() {
|
|
379
|
+
recorded = new WeakMap();
|
|
380
|
+
created = [];
|
|
381
|
+
host.reset();
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
return host;
|
|
385
|
+
}
|
package/build/wait-for.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
export declare function waitUntil(condition: () => boolean, label: string, timeoutMs?: number): Promise<void>;
|
|
2
|
-
export
|
|
2
|
+
export interface IQuietOptions {
|
|
3
|
+
/** Consecutive macrotasks the sample must hold. */
|
|
4
|
+
readonly stableTicks?: number;
|
|
5
|
+
/** Wall time the sample must hold, so the settle does not measure machine speed. */
|
|
6
|
+
readonly quietMs?: number;
|
|
7
|
+
readonly timeoutMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function waitForQuiet(sample: () => number, label: string, options?: IQuietOptions): Promise<number>;
|
|
3
10
|
export declare function advanceTicks(count: number): Promise<void>;
|
|
11
|
+
export declare function advanceMs(durationMs?: number): Promise<void>;
|
package/build/wait-for.js
CHANGED
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
// that trades a fast failure for a slow one and keeps the race.
|
|
17
17
|
const TICK_MS = 0;
|
|
18
18
|
const DEFAULT_TIMEOUT_MS = 2_000;
|
|
19
|
+
// 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.
|
|
24
|
+
const DEFAULT_QUIET_MS = 75;
|
|
19
25
|
// Read off the host rather than imported, the same reason the engine's animations/raf.ts does:
|
|
20
26
|
// this package's tsconfig carries no DOM and no Node lib, so the ambient names do not exist.
|
|
21
27
|
function tick() {
|
|
@@ -42,21 +48,30 @@ export async function waitUntil(condition, label, timeoutMs = DEFAULT_TIMEOUT_MS
|
|
|
42
48
|
await tick();
|
|
43
49
|
}
|
|
44
50
|
}
|
|
45
|
-
// Waits until `sample`
|
|
46
|
-
//
|
|
47
|
-
//
|
|
51
|
+
// Waits until `sample` stops changing — for both `stableTicks` consecutive macrotasks and
|
|
52
|
+
// `quietMs` of wall time. Returns the settled value so a caller can assert against what actually
|
|
53
|
+
// happened rather than re-reading it.
|
|
48
54
|
//
|
|
49
55
|
// This is the honest way to write a "does not free-run" test: settle FIRST on a real condition,
|
|
50
56
|
// then observe. Asserting no-growth over a fixed window without settling first only tests whether
|
|
51
|
-
// the machine was fast enough
|
|
52
|
-
|
|
57
|
+
// the machine was fast enough — and a tick-only settle has the same defect one layer in, which is
|
|
58
|
+
// why quiet is a duration here and not a count.
|
|
59
|
+
export async function waitForQuiet(sample, label, options = {}) {
|
|
60
|
+
const { stableTicks = 3, quietMs = DEFAULT_QUIET_MS, timeoutMs = DEFAULT_TIMEOUT_MS, } = options;
|
|
53
61
|
const deadline = now() + timeoutMs;
|
|
54
62
|
let last = sample();
|
|
55
63
|
let stable = 0;
|
|
56
|
-
|
|
64
|
+
let quietSince = now();
|
|
65
|
+
while (stable < stableTicks || now() - quietSince < quietMs) {
|
|
57
66
|
await tick();
|
|
58
67
|
const current = sample();
|
|
59
|
-
|
|
68
|
+
if (current === last) {
|
|
69
|
+
stable += 1;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
stable = 0;
|
|
73
|
+
quietSince = now();
|
|
74
|
+
}
|
|
60
75
|
last = current;
|
|
61
76
|
if (now() > deadline) {
|
|
62
77
|
throw new Error(`waitForQuiet timed out after ${timeoutMs}ms, still changing: ${label}`);
|
|
@@ -70,3 +85,11 @@ export async function advanceTicks(count) {
|
|
|
70
85
|
for (let index = 0; index < count; index += 1)
|
|
71
86
|
await tick();
|
|
72
87
|
}
|
|
88
|
+
// Observes for a DURATION rather than a tick count. The counterpart to `waitForQuiet` for the
|
|
89
|
+
// second half of a "does not free-run" test: the window in which a late producer would show up is
|
|
90
|
+
// wall time, so the observation has to be too.
|
|
91
|
+
export async function advanceMs(durationMs = DEFAULT_QUIET_MS) {
|
|
92
|
+
const until = now() + durationMs;
|
|
93
|
+
while (now() < until)
|
|
94
|
+
await tick();
|
|
95
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/test-utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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": {
|
|
@@ -17,6 +17,16 @@
|
|
|
17
17
|
"main": "./build/index.js",
|
|
18
18
|
"module": "./build/index.js",
|
|
19
19
|
"types": "./build/index.d.ts",
|
|
20
|
+
"keywords": [
|
|
21
|
+
"react-native",
|
|
22
|
+
"symbiote-native",
|
|
23
|
+
"testing",
|
|
24
|
+
"test-utils",
|
|
25
|
+
"fabric",
|
|
26
|
+
"vitest",
|
|
27
|
+
"test-harness",
|
|
28
|
+
"mock"
|
|
29
|
+
],
|
|
20
30
|
"exports": {
|
|
21
31
|
".": {
|
|
22
32
|
"types": "./build/index.d.ts",
|
|
@@ -29,6 +39,9 @@
|
|
|
29
39
|
"publishConfig": {
|
|
30
40
|
"access": "public"
|
|
31
41
|
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@symbiote-native/engine": "1.0.0"
|
|
44
|
+
},
|
|
32
45
|
"scripts": {
|
|
33
46
|
"typecheck": "tsc --build",
|
|
34
47
|
"format": "prettier --write \"src/**/*.{ts,tsx}\""
|
package/build/fake-fabric.d.ts
DELETED
|
@@ -1,42 +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
|
-
/** Call counters, for tests that assert "exactly N native nodes were created". */
|
|
23
|
-
counts: {
|
|
24
|
-
createNode: number;
|
|
25
|
-
completeRoot: number;
|
|
26
|
-
};
|
|
27
|
-
/**
|
|
28
|
-
* RN wraps every commit in a synthetic `box-none` AppContainer root.
|
|
29
|
-
* Returns it, asserting it is the single expected root, so each test unwraps the
|
|
30
|
-
* AppContainer the same way instead of re-checking the invariant by hand.
|
|
31
|
-
*/
|
|
32
|
-
appRoot(): IFakeNode;
|
|
33
|
-
/** Find the first `createNode`'d node matching a predicate (e.g. the app's own View). */
|
|
34
|
-
find(predicate: (node: IFakeNode) => boolean): IFakeNode | undefined;
|
|
35
|
-
/** Deliver a native event to the renderer's registered handler. */
|
|
36
|
-
fireEvent(handle: unknown, topLevelType: string, nativeEvent?: Record<string, unknown>): void;
|
|
37
|
-
/** Serialize a node list to `RCTView(RCTText(RCTRawText "text"))` shorthand. */
|
|
38
|
-
serialize(nodes: IFakeNode[]): string;
|
|
39
|
-
/** Zero the counters and clear `committed` / `created` (the event handler survives). */
|
|
40
|
-
reset(): void;
|
|
41
|
-
}
|
|
42
|
-
export declare function installFabric(): IFabricRecorder;
|
package/build/fake-fabric.js
DELETED
|
@@ -1,110 +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
|
-
export function installFabric() {
|
|
14
|
-
let committed = [];
|
|
15
|
-
const created = [];
|
|
16
|
-
const commands = [];
|
|
17
|
-
const counts = { createNode: 0, completeRoot: 0 };
|
|
18
|
-
let eventHandler;
|
|
19
|
-
const slot = {
|
|
20
|
-
createNode(tag, viewName, _rootTag, props, instanceHandle) {
|
|
21
|
-
counts.createNode += 1;
|
|
22
|
-
const node = {
|
|
23
|
-
tag,
|
|
24
|
-
viewName,
|
|
25
|
-
props,
|
|
26
|
-
children: [],
|
|
27
|
-
instanceHandle,
|
|
28
|
-
};
|
|
29
|
-
created.push(node);
|
|
30
|
-
return node;
|
|
31
|
-
},
|
|
32
|
-
cloneNodeWithNewProps: (node, newProps) => ({
|
|
33
|
-
...node,
|
|
34
|
-
props: mergeFabricProps(node.props, newProps),
|
|
35
|
-
}),
|
|
36
|
-
cloneNodeWithNewChildren: (node) => ({
|
|
37
|
-
...node,
|
|
38
|
-
children: [],
|
|
39
|
-
}),
|
|
40
|
-
cloneNodeWithNewChildrenAndProps: (node, newProps) => ({
|
|
41
|
-
...node,
|
|
42
|
-
props: mergeFabricProps(node.props, newProps),
|
|
43
|
-
children: [],
|
|
44
|
-
}),
|
|
45
|
-
createChildSet: () => [],
|
|
46
|
-
appendChild(parent, child) {
|
|
47
|
-
if (child.parentFamilyTag !== undefined &&
|
|
48
|
-
child.parentFamilyTag !== parent.tag) {
|
|
49
|
-
throw new Error(`Fabric family reparent: child ${child.viewName}#${child.tag} already belongs to parent #${child.parentFamilyTag}, cannot append to ${parent.viewName}#${parent.tag}`);
|
|
50
|
-
}
|
|
51
|
-
child.parentFamilyTag = parent.tag;
|
|
52
|
-
parent.children.push(child);
|
|
53
|
-
return parent;
|
|
54
|
-
},
|
|
55
|
-
appendChildToSet(childSet, child) {
|
|
56
|
-
childSet.push(child);
|
|
57
|
-
},
|
|
58
|
-
completeRoot(_rootTag, childSet) {
|
|
59
|
-
counts.completeRoot += 1;
|
|
60
|
-
committed = childSet;
|
|
61
|
-
},
|
|
62
|
-
registerEventHandler(handler) {
|
|
63
|
-
eventHandler = handler;
|
|
64
|
-
},
|
|
65
|
-
dispatchCommand(node, commandName, args) {
|
|
66
|
-
commands.push({ node, commandName, args });
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
Object.assign(globalThis, { nativeFabricUIManager: slot });
|
|
70
|
-
const serializeNode = (node) => {
|
|
71
|
-
const text = node.viewName === 'RCTRawText' ? ` "${String(node.props.text)}"` : '';
|
|
72
|
-
const kids = node.children.length
|
|
73
|
-
? `(${node.children.map(serializeNode).join('')})`
|
|
74
|
-
: '';
|
|
75
|
-
return `${node.viewName}${text}${kids}`;
|
|
76
|
-
};
|
|
77
|
-
return {
|
|
78
|
-
get committed() {
|
|
79
|
-
return committed;
|
|
80
|
-
},
|
|
81
|
-
created,
|
|
82
|
-
commands,
|
|
83
|
-
counts,
|
|
84
|
-
appRoot() {
|
|
85
|
-
const root = committed[0];
|
|
86
|
-
if (committed.length !== 1 || root?.props.pointerEvents !== 'box-none') {
|
|
87
|
-
throw new Error(`expected a single box-none AppContainer root, got ${committed.length} node(s)`);
|
|
88
|
-
}
|
|
89
|
-
return root;
|
|
90
|
-
},
|
|
91
|
-
find(predicate) {
|
|
92
|
-
return created.find(predicate);
|
|
93
|
-
},
|
|
94
|
-
fireEvent(handle, topLevelType, nativeEvent = {}) {
|
|
95
|
-
if (!eventHandler)
|
|
96
|
-
throw new Error('no event handler registered by the renderer');
|
|
97
|
-
eventHandler(handle, topLevelType, nativeEvent);
|
|
98
|
-
},
|
|
99
|
-
serialize(nodes) {
|
|
100
|
-
return nodes.map(serializeNode).join('');
|
|
101
|
-
},
|
|
102
|
-
reset() {
|
|
103
|
-
committed = [];
|
|
104
|
-
created.length = 0;
|
|
105
|
-
commands.length = 0;
|
|
106
|
-
counts.createNode = 0;
|
|
107
|
-
counts.completeRoot = 0;
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
}
|