@symbiote-native/test-utils 0.4.7 → 0.5.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/build/index.d.ts +4 -0
- package/build/index.js +4 -0
- package/build/list-harness.d.ts +19 -0
- package/build/list-harness.js +65 -0
- package/build/native-calls.d.ts +7 -0
- package/build/native-calls.js +17 -0
- package/build/recording-host.d.ts +29 -97
- package/build/recording-host.js +37 -63
- package/build/rn-device-event.d.ts +1 -0
- package/build/rn-device-event.js +10 -0
- package/build/window-dimensions.d.ts +13 -0
- package/build/window-dimensions.js +12 -0
- package/package.json +2 -2
package/build/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export { createRecordingHost, installRecordingFabric, payloadOf, type IAuthoredNode, type IRecordingHost, } from './recording-host';
|
|
2
2
|
export { censusLive, createLiveTree, type ILiveCensus, type ILiveNode, type ILiveTree, } from './live-tree';
|
|
3
|
+
export { createListHarness, type IBox, type IListHarness, } from './list-harness';
|
|
3
4
|
export { trackHostCrossings, type IHostCrossingTracker, } from './host-crossings';
|
|
4
5
|
export * from './wait-for';
|
|
6
|
+
export { nativeCallsTo } from './native-calls';
|
|
7
|
+
export { emitRnDeviceEvent } from './rn-device-event';
|
|
8
|
+
export { emitWindowDimensions, seedWindowDimensions, TEST_WINDOW, } from './window-dimensions';
|
package/build/index.js
CHANGED
|
@@ -10,6 +10,10 @@ export { createRecordingHost, installRecordingFabric, payloadOf, } from './recor
|
|
|
10
10
|
// (`is this still mounted`, `did the pop remove it`) that `find` cannot answer, because `find`
|
|
11
11
|
// searches the creation log. Anchors flatten, as the commit walk flattens them. See its header.
|
|
12
12
|
export { censusLive, createLiveTree, } from './live-tree.js';
|
|
13
|
+
export { createListHarness, } from './list-harness.js';
|
|
13
14
|
// How many times JS crossed the host boundary — generic over whichever host is installed.
|
|
14
15
|
export { trackHostCrossings, } from './host-crossings.js';
|
|
15
16
|
export * from './wait-for.js';
|
|
17
|
+
export { nativeCallsTo } from './native-calls.js';
|
|
18
|
+
export { emitRnDeviceEvent } from './rn-device-event.js';
|
|
19
|
+
export { emitWindowDimensions, seedWindowDimensions, TEST_WINDOW, } from './window-dimensions.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ILiveNode, ILiveTree } from './live-tree';
|
|
2
|
+
import type { IAuthoredNode, IRecordingHost } from './recording-host';
|
|
3
|
+
export type IBox = {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
};
|
|
7
|
+
export type IListHarness = {
|
|
8
|
+
scrollView: () => IAuthoredNode;
|
|
9
|
+
contentView: () => ILiveNode;
|
|
10
|
+
cells: () => ILiveNode[];
|
|
11
|
+
shape: () => string;
|
|
12
|
+
simulateLayout: (args: {
|
|
13
|
+
viewport: IBox;
|
|
14
|
+
content: IBox;
|
|
15
|
+
}) => void;
|
|
16
|
+
simulateContentLayout: (box: IBox) => void;
|
|
17
|
+
simulateScroll: (y: number) => void;
|
|
18
|
+
};
|
|
19
|
+
export declare function createListHarness(fabric: IRecordingHost, live: ILiveTree): IListHarness;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Drives a windowed list over the recording host: layout and scroll events aimed at its scroll
|
|
2
|
+
// and content views, and a reading of the cells and spacers it currently renders
|
|
3
|
+
function textOf(node) {
|
|
4
|
+
if (typeof node.payload.text === 'string')
|
|
5
|
+
return node.payload.text;
|
|
6
|
+
for (const child of node.children) {
|
|
7
|
+
const found = textOf(child);
|
|
8
|
+
if (found !== undefined)
|
|
9
|
+
return found;
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
function handleOf(node) {
|
|
14
|
+
const { instanceHandle } = node;
|
|
15
|
+
if (typeof instanceHandle !== 'object' || instanceHandle === null) {
|
|
16
|
+
throw new Error('the node has no instance handle to aim an event at');
|
|
17
|
+
}
|
|
18
|
+
return instanceHandle;
|
|
19
|
+
}
|
|
20
|
+
export function createListHarness(fabric, live) {
|
|
21
|
+
let viewport = { width: 0, height: 0 };
|
|
22
|
+
let content = { width: 0, height: 0 };
|
|
23
|
+
const scrollView = () => {
|
|
24
|
+
const node = fabric.find(one => one.viewName === 'RCTScrollView');
|
|
25
|
+
if (node === undefined)
|
|
26
|
+
throw new Error('no scroll view was created');
|
|
27
|
+
return node;
|
|
28
|
+
};
|
|
29
|
+
const contentView = () => {
|
|
30
|
+
const container = live.findLive(scrollView().handle, one => one.viewName === 'RCTScrollContentView');
|
|
31
|
+
if (container === undefined)
|
|
32
|
+
throw new Error('no scroll content view');
|
|
33
|
+
return container;
|
|
34
|
+
};
|
|
35
|
+
const cells = () => contentView().children.filter(child => textOf(child) !== undefined);
|
|
36
|
+
const simulateContentLayout = (box) => {
|
|
37
|
+
content = box;
|
|
38
|
+
fabric.fireEvent(handleOf(contentView()), 'topLayout', {
|
|
39
|
+
layout: { x: 0, y: 0, ...box },
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
scrollView,
|
|
44
|
+
contentView,
|
|
45
|
+
cells,
|
|
46
|
+
shape: () => contentView()
|
|
47
|
+
.children.map(child => textOf(child) ?? `[${String(child.payload.height ?? 0)}]`)
|
|
48
|
+
.join(' '),
|
|
49
|
+
simulateLayout: args => {
|
|
50
|
+
viewport = args.viewport;
|
|
51
|
+
fabric.fireEvent(handleOf(scrollView()), 'topLayout', {
|
|
52
|
+
layout: { x: 0, y: 0, ...viewport },
|
|
53
|
+
});
|
|
54
|
+
simulateContentLayout(args.content);
|
|
55
|
+
},
|
|
56
|
+
simulateContentLayout,
|
|
57
|
+
simulateScroll: y => {
|
|
58
|
+
fabric.fireEvent(handleOf(scrollView()), 'topScroll', {
|
|
59
|
+
contentOffset: { x: 0, y },
|
|
60
|
+
contentSize: content,
|
|
61
|
+
layoutMeasurement: viewport,
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// What a test sent to a native module through RN's `TurboModuleRegistry`, which vitest.config.ts
|
|
2
|
+
// stubs and records into `globalThis.__symbioteNativeCalls`
|
|
3
|
+
function isNativeCall(value) {
|
|
4
|
+
return (typeof value === 'object' &&
|
|
5
|
+
value !== null &&
|
|
6
|
+
'module' in value &&
|
|
7
|
+
'method' in value &&
|
|
8
|
+
'args' in value);
|
|
9
|
+
}
|
|
10
|
+
export function nativeCallsTo(moduleName) {
|
|
11
|
+
const recorded = Reflect.get(globalThis, '__symbioteNativeCalls');
|
|
12
|
+
if (!Array.isArray(recorded))
|
|
13
|
+
return [];
|
|
14
|
+
return recorded
|
|
15
|
+
.filter(isNativeCall)
|
|
16
|
+
.filter(call => call.module === moduleName);
|
|
17
|
+
}
|
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A tree host that RECORDS and derives nothing
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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.
|
|
2
|
+
* A tree host that RECORDS and derives nothing, for test files that never read a tree
|
|
3
|
+
* It keeps the AUTHORED tree (who was appended to whom, which props were set), not what COMMITS
|
|
4
|
+
* A test that needs the committed tree belongs in `core/engine/cpp/tests/js`
|
|
17
5
|
*/
|
|
18
6
|
import { type ISymbioteNode } from '@symbiote-native/engine';
|
|
19
|
-
import type { ITreeHost } from '@symbiote-native/engine';
|
|
7
|
+
import type { IDomRect, ITreeHost } from '@symbiote-native/engine';
|
|
20
8
|
export type IRecorded = {
|
|
21
9
|
handle: ISymbioteNode;
|
|
22
10
|
instanceHandle: unknown;
|
|
@@ -30,18 +18,12 @@ export type IRecorded = {
|
|
|
30
18
|
committedRootTag: number | undefined;
|
|
31
19
|
};
|
|
32
20
|
export type IRecordingHost = ITreeHost & {
|
|
33
|
-
/** How many commits the engine asked for
|
|
21
|
+
/** How many commits the engine asked for, the op stream's own count */
|
|
34
22
|
commits: number;
|
|
35
|
-
/**
|
|
36
|
-
* The imperative calls the engine made, in order.
|
|
37
|
-
*
|
|
38
|
-
* These are the engine's own OUTPUT — it asked the platform to scroll, to focus, to take the
|
|
39
|
-
* responder — so recording them is reading what our code did, not deciding what the renderer
|
|
40
|
-
* would have done with it. The handle is the authored node, which is what a test holds.
|
|
41
|
-
*/
|
|
23
|
+
/** The imperative calls the engine made, in order, on the authored node a test holds */
|
|
42
24
|
commands: {
|
|
43
25
|
handle: object;
|
|
44
|
-
/** As the ops named it
|
|
26
|
+
/** As the ops named it, not a committed name */
|
|
45
27
|
viewName: string;
|
|
46
28
|
commandName: string;
|
|
47
29
|
args: readonly unknown[];
|
|
@@ -55,101 +37,51 @@ export type IRecordingHost = ITreeHost & {
|
|
|
55
37
|
handle: object;
|
|
56
38
|
eventType: string;
|
|
57
39
|
}[];
|
|
58
|
-
/**
|
|
59
|
-
* Hand an event to the handler the engine registered, naming the target yourself.
|
|
60
|
-
*
|
|
61
|
-
* Not a tree read, and not the platform deciding anything: the engine installs this handler
|
|
62
|
-
* through `registerEventHandler` and this plays it back verbatim. What a REAL gesture needs —
|
|
63
|
-
* hit-testing coordinates to a target — is the renderer's answer and is not on offer here; a
|
|
64
|
-
* caller must already know which node it means, which is what an engine-side dispatch test does.
|
|
65
|
-
*/
|
|
40
|
+
/** Hands an event to the registered handler, naming the target yourself, no hit-testing */
|
|
66
41
|
fireEvent: (handle: object, topLevelType: string, nativeEvent?: Record<string, unknown>) => void;
|
|
67
|
-
/** The slot call, kept here so one object owns both halves of the round trip
|
|
42
|
+
/** The slot call, kept here so one object owns both halves of the round trip */
|
|
68
43
|
registerEventHandler: (handler: IEventHandler) => void;
|
|
69
|
-
/**
|
|
70
|
-
* The first AUTHORED node matching `predicate` — the handle a test needs when the adapter, not
|
|
71
|
-
* the test, created the node.
|
|
72
|
-
*
|
|
73
|
-
* **The authored tree, and the word carries the whole caveat.** These are the nodes the ops named
|
|
74
|
-
* and the props they carried; nothing here says which of them Fabric kept, what it renamed, or
|
|
75
|
-
* what it flattened away. So this answers "find the node the app wrote" — to read its props, to
|
|
76
|
-
* hand it to `fabricProps`, to fire an event at it — and it does NOT answer any question about
|
|
77
|
-
* shape. That one belongs to `committedTree()` in `core/engine/cpp/tests/js`.
|
|
78
|
-
*/
|
|
44
|
+
/** The first authored node matching `predicate`, for a node the adapter created */
|
|
79
45
|
find: (predicate: (node: IAuthoredNode) => boolean) => IAuthoredNode | undefined;
|
|
80
|
-
/** Every authored node matching `predicate`, in
|
|
46
|
+
/** Every authored node matching `predicate`, in creation order, not document order */
|
|
81
47
|
findAll: (predicate: (node: IAuthoredNode) => boolean) => IAuthoredNode[];
|
|
82
48
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* What it also clears, and it is load-bearing: the list `find` searches. A file that mounts a
|
|
86
|
-
* fresh tree per case reuses its `testID`s, so a `find` spanning cases answers with the FIRST
|
|
87
|
-
* match — an earlier case's node, carrying an earlier case's props. Caught by a converted test
|
|
88
|
-
* whose second case read the first case's payload and reported a missing prop.
|
|
49
|
+
* Clears the recordings and the list `find` searches, the authored tree survives
|
|
50
|
+
* A file reusing its `testID`s per case would otherwise find an earlier case's node
|
|
89
51
|
*/
|
|
90
52
|
reset: () => void;
|
|
91
53
|
forget: () => void;
|
|
54
|
+
/**
|
|
55
|
+
* What `measureLayout` answers, by the two handles
|
|
56
|
+
* No Yoga runs headless, so a test that needs a rect names it here, no answer means it fails
|
|
57
|
+
*/
|
|
58
|
+
answerMeasureLayout: (answer: ((handle: object, relativeTo: object) => IDomRect | undefined) | undefined) => void;
|
|
92
59
|
};
|
|
93
60
|
type IEventHandler = (handle: object, topLevelType: string, nativeEvent: Record<string, unknown>) => void;
|
|
94
|
-
/**
|
|
95
|
-
* A node as the OPS described it. Not a committed node and not pretending to be one — there is no
|
|
96
|
-
* tag, because this host never spoke to Fabric and any number here would be an invention.
|
|
97
|
-
*/
|
|
61
|
+
/** A node as the ops described it, with no tag because this host never spoke to Fabric */
|
|
98
62
|
export type IAuthoredNode = {
|
|
99
|
-
/** The engine node itself,
|
|
63
|
+
/** The engine node itself, to hand back to the engine's own API */
|
|
100
64
|
handle: ISymbioteNode;
|
|
101
|
-
/**
|
|
102
|
-
* The object the engine gave Fabric to hand back with an event — undefined for a raw text or an
|
|
103
|
-
* anchor, which are created with none.
|
|
104
|
-
*
|
|
105
|
-
* It is what `fireEvent` has to be aimed at: the engine's event handler resolves a target by
|
|
106
|
-
* this object, not by the node, so passing the node would silently deliver to nothing.
|
|
107
|
-
*/
|
|
65
|
+
/** The object `fireEvent` must be aimed at, undefined for a raw text or an anchor */
|
|
108
66
|
instanceHandle: unknown;
|
|
109
|
-
/** As the ops named it
|
|
67
|
+
/** As the ops named it, Fabric may commit it under a different name */
|
|
110
68
|
viewName: string;
|
|
111
|
-
/**
|
|
112
|
-
* The intrinsic tag, for a node a host behavior attached to; empty for every other node.
|
|
113
|
-
*
|
|
114
|
-
* Recorded so a test can ASK what a node is — the real host resolves a tag's platform props off
|
|
115
|
-
* it and a plain `RCTView` cannot be told from a `<pressable>` any other way. It changes nothing
|
|
116
|
-
* about the payload this host builds; see the `OP_SET_TAG` case.
|
|
117
|
-
*/
|
|
69
|
+
/** The intrinsic tag of a node a host behavior attached to, empty for every other node */
|
|
118
70
|
tagName: string;
|
|
119
|
-
/**
|
|
120
|
-
* Which event names a BEHAVIOR owns currently have an app callback wired, by name.
|
|
121
|
-
*
|
|
122
|
-
* The presence only — the callback never crosses and is not here. It is what lets a platform rule
|
|
123
|
-
* resolve a key that depends on whether the app wired anything (`focusable` on a touchable is
|
|
124
|
-
* `focusable !== false && onPress !== undefined && !disabled`). Recorded for the same reason
|
|
125
|
-
* `tagName` is: so a test can ask what the host was TOLD, separately from what a rule made of it.
|
|
126
|
-
*/
|
|
71
|
+
/** Which event names a behavior owns have an app callback wired, presence only */
|
|
127
72
|
ownedListeners: Record<string, boolean>;
|
|
128
|
-
/** Whether a behavior's feedback is showing
|
|
73
|
+
/** Whether a behavior's feedback is showing, see `OP_SET_UNDERLAY_SHOWN` */
|
|
129
74
|
underlayShown: boolean;
|
|
130
75
|
props: Readonly<Record<string, unknown>>;
|
|
131
76
|
};
|
|
132
77
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* `createSurface` calls `installEventHandler`, which reaches `globalThis.nativeFabricUIManager` and
|
|
136
|
-
* throws if it is absent — so a host alone is not enough to get a surface open. The slot below
|
|
137
|
-
* exists to be PRESENT: every method answers nothing except `registerEventHandler`, which hands the
|
|
138
|
-
* engine's handler to the host so `fireEvent` can play it back.
|
|
139
|
-
*
|
|
140
|
-
* What that does NOT buy is a real gesture. Deciding which node a touch lands on is hit-testing,
|
|
141
|
-
* and that is the renderer's answer; a test that needs it belongs in `core/engine/cpp/tests/js`.
|
|
78
|
+
* Installs the recording host with the minimum slot the engine insists on
|
|
79
|
+
* `createSurface` throws without `nativeFabricUIManager`, so the slot only has to be present
|
|
142
80
|
*/
|
|
143
81
|
export declare function installRecordingFabric(): IRecordingHost;
|
|
144
82
|
/**
|
|
145
|
-
* The Fabric payload for a node
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* This exists because the two are easy to confuse and the difference bites: a node's PROPS are the
|
|
149
|
-
* author's bag (`style` is still an object), while the PAYLOAD is what `fabricProps` makes of it —
|
|
150
|
-
* style flattened into top-level keys, the aria fold run, the ten RN processors applied. A test
|
|
151
|
-
* asking about `padding` or `accessibilityRole` or a parsed `backgroundSize` means the payload, and
|
|
152
|
-
* reading the bag instead comes back `undefined` with nothing to explain why.
|
|
83
|
+
* The Fabric payload for a node, built by the engine's own builder
|
|
84
|
+
* Props are the author's bag (`style` is an object), the payload is what `fabricProps` makes of it
|
|
153
85
|
*/
|
|
154
86
|
export declare function payloadOf(node: ISymbioteNode): Record<string, unknown>;
|
|
155
87
|
export declare function createRecordingHost(): IRecordingHost;
|
package/build/recording-host.js
CHANGED
|
@@ -1,32 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A tree host that RECORDS and derives nothing
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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.
|
|
2
|
+
* A tree host that RECORDS and derives nothing, for test files that never read a tree
|
|
3
|
+
* It keeps the AUTHORED tree (who was appended to whom, which props were set), not what COMMITS
|
|
4
|
+
* A test that needs the committed tree belongs in `core/engine/cpp/tests/js`
|
|
17
5
|
*/
|
|
18
6
|
import { fabricProps, propsOf, setTreeHost, } from '@symbiote-native/engine';
|
|
19
7
|
import { createRecordedTree } from './recorded-tree.js';
|
|
20
8
|
/**
|
|
21
|
-
*
|
|
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`.
|
|
9
|
+
* Installs the recording host with the minimum slot the engine insists on
|
|
10
|
+
* `createSurface` throws without `nativeFabricUIManager`, so the slot only has to be present
|
|
30
11
|
*/
|
|
31
12
|
export function installRecordingFabric() {
|
|
32
13
|
const host = createRecordingHost();
|
|
@@ -38,6 +19,7 @@ export function installRecordingFabric() {
|
|
|
38
19
|
dispatchCommand() { },
|
|
39
20
|
setIsJSResponder() { },
|
|
40
21
|
sendAccessibilityEvent() { },
|
|
22
|
+
configureNextLayoutAnimation() { },
|
|
41
23
|
measure() { },
|
|
42
24
|
measureInWindow() { },
|
|
43
25
|
measureLayout() { },
|
|
@@ -47,27 +29,18 @@ export function installRecordingFabric() {
|
|
|
47
29
|
return host;
|
|
48
30
|
}
|
|
49
31
|
/**
|
|
50
|
-
* The Fabric payload for a node
|
|
51
|
-
*
|
|
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.
|
|
32
|
+
* The Fabric payload for a node, built by the engine's own builder
|
|
33
|
+
* Props are the author's bag (`style` is an object), the payload is what `fabricProps` makes of it
|
|
58
34
|
*/
|
|
59
35
|
export function payloadOf(node) {
|
|
60
36
|
return fabricProps(node, propsOf(node));
|
|
61
37
|
}
|
|
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
|
-
*/
|
|
38
|
+
// Where a tag would be, this host never speaks to Fabric so any number would be an invention
|
|
66
39
|
const NO_TAG = -1;
|
|
67
40
|
export function createRecordingHost() {
|
|
68
41
|
const tree = createRecordedTree();
|
|
69
42
|
const { nodeOf } = tree;
|
|
70
|
-
let
|
|
43
|
+
let measureLayoutAnswer;
|
|
71
44
|
const commands = [];
|
|
72
45
|
const responderHandovers = [];
|
|
73
46
|
const accessibilityEvents = [];
|
|
@@ -76,8 +49,19 @@ export function createRecordingHost() {
|
|
|
76
49
|
commands.length = 0;
|
|
77
50
|
responderHandovers.length = 0;
|
|
78
51
|
accessibilityEvents.length = 0;
|
|
52
|
+
measureLayoutAnswer = undefined;
|
|
79
53
|
};
|
|
54
|
+
let eventHandler;
|
|
80
55
|
return {
|
|
56
|
+
registerEventHandler(handler) {
|
|
57
|
+
eventHandler = handler;
|
|
58
|
+
},
|
|
59
|
+
fireEvent(handle, topLevelType, nativeEvent = {}) {
|
|
60
|
+
if (eventHandler === undefined) {
|
|
61
|
+
throw new Error('no event handler registered by the renderer');
|
|
62
|
+
}
|
|
63
|
+
eventHandler(handle, topLevelType, nativeEvent);
|
|
64
|
+
},
|
|
81
65
|
get commits() {
|
|
82
66
|
return tree.commits;
|
|
83
67
|
},
|
|
@@ -91,11 +75,9 @@ export function createRecordingHost() {
|
|
|
91
75
|
propsOf(handle) {
|
|
92
76
|
return nodeOf(handle, 'propsOf').props;
|
|
93
77
|
},
|
|
94
|
-
// Nothing is memoized here, so there is nothing to invalidate
|
|
78
|
+
// Nothing is memoized here, so there is nothing to invalidate
|
|
95
79
|
markPropsDirty() { },
|
|
96
|
-
//
|
|
97
|
-
// live walk, т.к. `removeChild` cuts the parent link before the teardown sweep runs in the
|
|
98
|
-
// SAME commit
|
|
80
|
+
// Read off `committedRootTag`, т.к. `removeChild` cuts the parent link before the sweep
|
|
99
81
|
committedRecordOf(handle) {
|
|
100
82
|
const node = tree.recordedOf(handle);
|
|
101
83
|
if (node === undefined || node.committedRootTag === undefined) {
|
|
@@ -107,9 +89,7 @@ export function createRecordingHost() {
|
|
|
107
89
|
rootTag: node.committedRootTag,
|
|
108
90
|
};
|
|
109
91
|
},
|
|
110
|
-
//
|
|
111
|
-
// in vitest. A test asking this belongs in `core/engine/cpp/tests/js`, where the real builder
|
|
112
|
-
// does run
|
|
92
|
+
// Refused loudly, т.к. the payload builder is C++ and does not run here
|
|
113
93
|
committedPayloadOf() {
|
|
114
94
|
throw new Error('recording host: committedPayloadOf needs the real payload builder — move this to an itest');
|
|
115
95
|
},
|
|
@@ -143,9 +123,7 @@ export function createRecordingHost() {
|
|
|
143
123
|
walk(nodeOf(root, 'subtreesOf'));
|
|
144
124
|
return out;
|
|
145
125
|
},
|
|
146
|
-
//
|
|
147
|
-
// where the sweep is asserted, so handing back everything would pass those cases whatever the
|
|
148
|
-
// engine does
|
|
126
|
+
// Narrows like the engine's walk, else the sweep cases would pass whatever the engine does
|
|
149
127
|
teardownSubtreesOf(roots) {
|
|
150
128
|
const out = [];
|
|
151
129
|
const walk = (node, isRoot) => {
|
|
@@ -180,8 +158,7 @@ export function createRecordingHost() {
|
|
|
180
158
|
};
|
|
181
159
|
for (const root of roots)
|
|
182
160
|
walk(nodeOf(root, 'census'));
|
|
183
|
-
// The skip counts belong to
|
|
184
|
-
// honest answer: nothing here was skipped, because nothing here was decided.
|
|
161
|
+
// The skip counts belong to commit rules this host lacks, so zero is the honest answer
|
|
185
162
|
return {
|
|
186
163
|
nodes,
|
|
187
164
|
anchors: 0,
|
|
@@ -190,9 +167,8 @@ export function createRecordingHost() {
|
|
|
190
167
|
flattenWidths: [],
|
|
191
168
|
};
|
|
192
169
|
},
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
// only a platform has, and there is none here.
|
|
170
|
+
// Three imperative calls carry a request the engine made and are recorded
|
|
171
|
+
// The measuring three want an answer only a platform has, and there is none here
|
|
196
172
|
dispatchCommand(handle, commandName, args) {
|
|
197
173
|
commands.push({
|
|
198
174
|
handle,
|
|
@@ -210,27 +186,25 @@ export function createRecordingHost() {
|
|
|
210
186
|
getBoundingClientRect() {
|
|
211
187
|
return { x: 0, y: 0, width: 0, height: 0 };
|
|
212
188
|
},
|
|
213
|
-
measureLayout(
|
|
214
|
-
|
|
189
|
+
measureLayout(handle, relativeTo, onFail, onSuccess) {
|
|
190
|
+
const rect = measureLayoutAnswer?.(handle, relativeTo);
|
|
191
|
+
if (rect === undefined)
|
|
192
|
+
onFail();
|
|
193
|
+
else
|
|
194
|
+
onSuccess(rect.x, rect.y, rect.width, rect.height);
|
|
195
|
+
},
|
|
196
|
+
answerMeasureLayout(answer) {
|
|
197
|
+
measureLayoutAnswer = answer;
|
|
215
198
|
},
|
|
216
199
|
setIsJSResponder(handle, isResponder, blockNativeResponder) {
|
|
217
200
|
responderHandovers.push({ handle, isResponder, blockNativeResponder });
|
|
218
201
|
},
|
|
219
|
-
registerEventHandler(handler) {
|
|
220
|
-
eventHandler = handler;
|
|
221
|
-
},
|
|
222
202
|
find(predicate) {
|
|
223
203
|
return tree.authored.find(predicate);
|
|
224
204
|
},
|
|
225
205
|
findAll(predicate) {
|
|
226
206
|
return tree.authored.filter(predicate);
|
|
227
207
|
},
|
|
228
|
-
fireEvent(handle, topLevelType, nativeEvent = {}) {
|
|
229
|
-
if (eventHandler === undefined) {
|
|
230
|
-
throw new Error('no event handler registered by the renderer');
|
|
231
|
-
}
|
|
232
|
-
eventHandler(handle, topLevelType, nativeEvent);
|
|
233
|
-
},
|
|
234
208
|
reset: clearRecords,
|
|
235
209
|
forget() {
|
|
236
210
|
tree.forget();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function emitRnDeviceEvent(eventType: string, payload: unknown): void;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Native pushes a device event through RN's `RCTDeviceEventEmitter`, which is where RN's own
|
|
2
|
+
// `Dimensions`, `Appearance` and `AppState` listen once the engine forwards to them
|
|
3
|
+
import RCTDeviceEventEmitter from 'react-native/Libraries/EventEmitter/RCTDeviceEventEmitter';
|
|
4
|
+
export function emitRnDeviceEvent(eventType, payload) {
|
|
5
|
+
const emit = Reflect.get(RCTDeviceEventEmitter, 'emit');
|
|
6
|
+
if (typeof emit !== 'function') {
|
|
7
|
+
throw new Error('RN device event emitter has no emit');
|
|
8
|
+
}
|
|
9
|
+
Reflect.apply(emit, RCTDeviceEventEmitter, [eventType, payload]);
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const TEST_WINDOW: {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
scale: number;
|
|
5
|
+
fontScale: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function seedWindowDimensions(): void;
|
|
8
|
+
export declare function emitWindowDimensions(window: typeof TEST_WINDOW, screen?: {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
scale: number;
|
|
12
|
+
fontScale: number;
|
|
13
|
+
}): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// A headless run has no `DeviceInfo` to answer `Dimensions.get`, so anything that reads the window
|
|
2
|
+
// at mount (InputAccessoryView's content width) throws until a test seeds one
|
|
3
|
+
import { Dimensions } from '@symbiote-native/engine';
|
|
4
|
+
import { emitRnDeviceEvent } from './rn-device-event.js';
|
|
5
|
+
export const TEST_WINDOW = { width: 390, height: 844, scale: 3, fontScale: 1 };
|
|
6
|
+
export function seedWindowDimensions() {
|
|
7
|
+
Dimensions.set({ window: TEST_WINDOW, screen: TEST_WINDOW });
|
|
8
|
+
}
|
|
9
|
+
// Native pushes new metrics as a `didUpdateDimensions` device event, which RN's `Dimensions` hears
|
|
10
|
+
export function emitWindowDimensions(window, screen = window) {
|
|
11
|
+
emitRnDeviceEvent('didUpdateDimensions', { window, screen });
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/test-utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Shared, framework-agnostic fake-Fabric test harness for SymbioteNative — the fake nativeFabricUIManager recorder used by the co-located test suites across the engine, every adapter, and the example apps.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@symbiote-native/engine": "1.
|
|
43
|
+
"@symbiote-native/engine": "1.7.0"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"typecheck": "tsc --build",
|