@symbiote-native/test-utils 0.4.6 → 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 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,7 @@
1
+ type INativeCall = {
2
+ module: string;
3
+ method: string;
4
+ args: unknown[];
5
+ };
6
+ export declare function nativeCallsTo(moduleName: string): INativeCall[];
7
+ export {};
@@ -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 — 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.
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. The op stream's own count, not a tree's. */
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 — the authored view name, not a committed one. */
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 CREATION order — not document order. */
46
+ /** Every authored node matching `predicate`, in creation order, not document order */
81
47
  findAll: (predicate: (node: IAuthoredNode) => boolean) => IAuthoredNode[];
82
48
  /**
83
- * Clear the recordings. The authored tree survives, as it does under `installFabric`.
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, so it can be handed straight back to the engine's own API. */
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. Fabric may commit it under a different name; that is not known here. */
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 — see `OP_SET_UNDERLAY_SHOWN`. */
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
- * Install the recording host, with the minimum slot the engine insists on.
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 — what the engine WOULD hand the renderer for it, built by the
146
- * engine's own builder rather than read off anything.
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;
@@ -1,32 +1,13 @@
1
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.
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
- * 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`.
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 — 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.
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 eventHandler;
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
- // That the ops COMMITTED this node, and nothing more. Read off `committedRootTag` and not a
97
- // live walk, т.к. `removeChild` cuts the parent link before the teardown sweep runs in the
98
- // SAME commit
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
- // REFUSED loudly, т.к. the payload is `SymbioteFabricProps.cpp`'s and that code does not run
111
- // in vitest. A test asking this belongs in `core/engine/cpp/tests/js`, where the real builder
112
- // does run
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
- // The twin of the engine's narrowed walk, and it MUST narrow here too: the vitest suite is
147
- // where the sweep is asserted, so handing back everything would pass those cases whatever the
148
- // engine does
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 the commit rules, which this host does not have. Zero is the
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
- // The imperative six ask the PLATFORM — a frame, a gesture, an announcement. Three of them
194
- // carry a request the engine MADE, so they are recorded; the measuring three want an answer
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(_handle, _relativeTo, onFail, _onSuccess) {
214
- onFail();
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.4.6",
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.5.0"
43
+ "@symbiote-native/engine": "1.7.0"
44
44
  },
45
45
  "scripts": {
46
46
  "typecheck": "tsc --build",