@symbiote-native/test-utils 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -81,19 +81,21 @@ timer). `advanceTicks(count)` is kept for the genuine "let the queue drain N tim
81
81
  raise a tick count to fix a flaky test — that trades a fast failure for a slow one and keeps the
82
82
  race; reach for `waitUntil`/`waitForQuiet` instead.
83
83
 
84
- ## The host-primitive lowering oracle
85
-
86
- `normalizeCommitted(nodes)` strips per-mount identity (`tag`, `instanceHandle`,
87
- `parentFamilyTag`) from a committed Fabric tree, and `compareLoweringEquivalence` /
88
- `expectCommittedProps` compare two such trees — used by React, Vue, Svelte, and Solid to mount a
89
- primitive (`Pressable`, `TextInput`, …) as a framework COMPONENT and as a lowered intrinsic tag
90
- with the same props, and assert the two committed payloads agree. It exists because a lowered
91
- element inherits nothing its wrapper component did — prop defaults, alias renames, and bag folds
92
- all live in the wrapper — so a naive lowering silently drops them (a lost `ellipsizeMode`, a
93
- never-applied `id -> nativeID`) with every other test green. Write BOTH assertions on every case:
94
- `compareLoweringEquivalence` catches a fold one path lost, `expectCommittedProps` catches a fold
95
- BOTH paths lost (an equivalence check alone can't tell "both arms agree" from "both arms are
96
- broken the same way").
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.
97
99
 
98
100
  ## What it does NOT do
99
101
 
@@ -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,3 +1,4 @@
1
- export * from './fake-fabric';
2
- export * from './lowering-equivalence';
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';
3
4
  export * from './wait-for';
package/build/index.js CHANGED
@@ -1,6 +1,15 @@
1
- // @symbiote-native/test-utils: shared, framework-agnostic test harness. Imported by
2
- // the co-located tests across engine, adapters, and the example apps so the fake-Fabric
3
- // recorder and its helpers live in exactly one place.
4
- export * from './fake-fabric.js';
5
- export * from './lowering-equivalence.js';
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';
6
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 {};