@symbiote-native/test-utils 0.4.2 → 0.4.4
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 +79 -54
- package/build/wait-for.js +2 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @symbiote-native/test-utils
|
|
2
2
|
|
|
3
3
|
The **shared fake-Fabric test harness** of [SymbioteNative](../../README.md) — one
|
|
4
|
-
`
|
|
5
|
-
returns a handle
|
|
6
|
-
headless test used to copy-paste (×65 across the repo) with one implementation, shared
|
|
7
|
-
engine, every adapter, and the example apps' own colocated `vitest` suites.
|
|
4
|
+
`installRecordingFabric()` that puts a fresh, faithful fake `nativeFabricUIManager` on
|
|
5
|
+
`globalThis` and returns a handle recording what a renderer did. It replaces the per-file fake
|
|
6
|
+
slot every headless test used to copy-paste (×65 across the repo) with one implementation, shared
|
|
7
|
+
by the engine, every adapter, and the example apps' own colocated `vitest` suites.
|
|
8
8
|
|
|
9
9
|
> New to SymbioteNative? The [root README](../../README.md) has the architecture and the
|
|
10
10
|
> [Testing](../../README.md#testing) section this package's harness is the foundation of.
|
|
@@ -24,10 +24,11 @@ npm install -D @symbiote-native/test-utils
|
|
|
24
24
|
## Use it
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
|
-
import {
|
|
27
|
+
import { installRecordingFabric } from '@symbiote-native/test-utils';
|
|
28
|
+
|
|
29
|
+
const fabric = installRecordingFabric();
|
|
28
30
|
|
|
29
31
|
test('tap increments the counter', () => {
|
|
30
|
-
const fabric = installFabric();
|
|
31
32
|
mount(1, createElement(App));
|
|
32
33
|
|
|
33
34
|
const button = fabric.find(
|
|
@@ -35,72 +36,96 @@ test('tap increments the counter', () => {
|
|
|
35
36
|
);
|
|
36
37
|
fabric.fireEvent(button!.instanceHandle, 'topClick', {});
|
|
37
38
|
|
|
38
|
-
expect(fabric.
|
|
39
|
+
expect(fabric.propsOf(button!.handle).testID).toBe('tap-target');
|
|
40
|
+
fabric.reset();
|
|
39
41
|
});
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
Call `
|
|
43
|
-
tests. Do not re-install per test: the engine's `getSlot()` (`core/engine/src/fabric.ts`)
|
|
44
|
-
slot's methods once and caches them for the process lifetime, so a second
|
|
45
|
-
anything has committed swaps the global while the engine keeps
|
|
46
|
-
already bound — the new recorder just stays empty, with nothing to
|
|
47
|
-
comes from the test runner; `reset()` is what separates tests
|
|
44
|
+
Call `installRecordingFabric()` ONCE per test file, at module scope, and `.reset()` the handle
|
|
45
|
+
between tests. Do not re-install per test: the engine's `getSlot()` (`core/engine/src/fabric.ts`)
|
|
46
|
+
reads the slot's methods once and caches them for the process lifetime, so a second
|
|
47
|
+
`installRecordingFabric()` after anything has committed swaps the global while the engine keeps
|
|
48
|
+
writing through the handle it already bound — the new recorder just stays empty, with nothing to
|
|
49
|
+
signal why. File-level isolation comes from the test runner; `reset()` is what separates tests
|
|
50
|
+
within a file.
|
|
48
51
|
|
|
49
|
-
## What `
|
|
52
|
+
## What `installRecordingFabric()` gives you
|
|
50
53
|
|
|
51
|
-
- **`
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
excluded), and a lookup by predicate (e.g. "the app's own `View` with this `testID`").
|
|
54
|
+
- **`find(predicate)`** and **`findAll(predicate)`** — every AUTHORED node the ops ever named
|
|
55
|
+
(clones excluded), searched in CREATION order. This is the creation LOG, not a live tree: a node
|
|
56
|
+
the app removed still answers `find` — see "Reading the live tree" below for a residency
|
|
57
|
+
question ("is this still mounted") the log can't answer.
|
|
56
58
|
- **`fireEvent(handle, topLevelType, nativeEvent?)`** — delivers a native event to whatever handler
|
|
57
59
|
the renderer registered, the same `instanceHandle` round-trip real Fabric does.
|
|
58
|
-
- **`commands
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
60
|
+
- **`commits`**, **`commands`**, **`responderHandovers`**, **`accessibilityEvents`** — counters and
|
|
61
|
+
logs of what the engine asked the platform to do, for tests asserting "exactly N commits" or
|
|
62
|
+
inspecting an imperative call (`dispatchCommand`, `setIsJSResponder`, `sendAccessibilityEvent`).
|
|
63
|
+
- **`reset()`** — clears the recordings and counters (the registered event handler survives), for
|
|
64
|
+
reusing one `installRecordingFabric()` call across several assertions in one test. **`forget()`**
|
|
65
|
+
additionally drops the `find`/`findAll` creation log itself — a file that mounts a fresh tree per
|
|
66
|
+
case and reuses `testID`s needs this, or `find` keeps answering with an earlier case's node.
|
|
67
|
+
|
|
68
|
+
This host **records what it's handed and derives nothing** — no flattening, no clone protocol, no
|
|
69
|
+
view-name rewriting. `propsOf(handle)` reads the author's own bag (`style` is still an object);
|
|
70
|
+
`payloadOf(handle)` (below) is what the engine would actually hand Fabric. A question about
|
|
71
|
+
committed SHAPE (what Fabric kept, renamed, or flattened) belongs in
|
|
72
|
+
`core/engine/cpp/tests/js` — asking it here gets `undefined`, not a plausible guess.
|
|
73
|
+
|
|
74
|
+
## Reading the live tree
|
|
75
|
+
|
|
76
|
+
`createLiveTree(fabric)` is a lens **over** the recording host, for residency questions the
|
|
77
|
+
creation log can't answer (a popped route, an evicted list cell, a portal toggled off) — it walks
|
|
78
|
+
the engine's own live child links, so a removed node stops appearing:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { createLiveTree } from '@symbiote-native/test-utils';
|
|
82
|
+
|
|
83
|
+
const live = createLiveTree(fabric);
|
|
84
|
+
const root = live.appRoot(); // the app's own box-none root, RN's synthetic AppContainer unwrapped
|
|
85
|
+
expect(live.serialize(root)).toContain('RCTText "Taps: 1"');
|
|
86
|
+
expect(live.texts(root)).toContain('Taps: 1');
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- **`appRoot()`** — the app's `box-none` container, unwrapped so a test doesn't re-check that
|
|
90
|
+
invariant by hand.
|
|
91
|
+
- **`nodeOf(handle)`** — a positional read (`viewName`, `props`, `payload`, `children`) for one
|
|
92
|
+
handle; **`walkLive`/`findAllLive`/`findLive`** walk from a root, **anchors flattened** (the
|
|
93
|
+
commit walk's own rule — Svelte leaves an anchor per block, Angular one per composed component).
|
|
94
|
+
- **`serialize(root)`** — a subtree as `RCTView(RCTText(RCTRawText "text"))` shorthand.
|
|
95
|
+
- **`texts(root)`** — every raw text under `root`, in TREE order (what a reordering list can't get
|
|
96
|
+
from the creation log, which is in creation order).
|
|
97
|
+
- **`outline(root)`** — a depth-indented `viewName` list, for asserting an exact shape.
|
|
98
|
+
|
|
99
|
+
**`payloadOf(handle)`** (top-level export, also `nodeOf(handle).payload`) is what the engine WOULD
|
|
100
|
+
hand the renderer — style flattened, the aria fold and RN's processors run — as against `props`,
|
|
101
|
+
the author's own bag. Read `padding`, `accessibilityRole`, or a parsed `backgroundSize` off the
|
|
102
|
+
payload, not the props, or the read comes back `undefined` with nothing to explain why.
|
|
103
|
+
|
|
104
|
+
## Measuring the engine, not the app
|
|
105
|
+
|
|
106
|
+
**`censusLive(...roots)`** counts a live subtree off the engine's own child links —
|
|
107
|
+
`{ nodes, anchors, nonAnchors }` — for "how many nodes did the adapter allocate" probes, without
|
|
108
|
+
asking any host (works identically against a real device). **`trackHostCrossings(host)`** wraps a
|
|
109
|
+
tree host's own methods in place and counts calls into them (`applyOps` excluded) — for "does the
|
|
110
|
+
dispatch code cross the host once per event, not once per ancestor" assertions.
|
|
71
111
|
|
|
72
112
|
## Waiting for async settling
|
|
73
113
|
|
|
74
114
|
`waitUntil(condition, label, timeoutMs?)` polls once per macrotask until `condition()` holds and
|
|
75
115
|
throws (naming `label`) on timeout — the honest replacement for a fixed `setTimeout` tick count,
|
|
76
116
|
which is only a proxy for "the framework has settled" and breaks under a loaded test run.
|
|
77
|
-
`waitForQuiet(sample, label,
|
|
78
|
-
|
|
79
|
-
"work has stopped arriving" (a batched commit, a zoneless change-detection pass, a
|
|
80
|
-
timer). `advanceTicks(count)`
|
|
117
|
+
`waitForQuiet(sample, label, options?)` waits until `sample()` returns the same value across
|
|
118
|
+
`stableTicks` consecutive macrotasks AND `quietMs` of wall time, then returns the settled value —
|
|
119
|
+
the shape for "work has stopped arriving" (a batched commit, a zoneless change-detection pass, a
|
|
120
|
+
press-timing timer). `advanceTicks(count)` and `advanceMs(durationMs?)` are kept for the genuine
|
|
121
|
+
"let the queue drain" case — a fixed number of macrotasks, or a fixed wall-time window. Do not
|
|
81
122
|
raise a tick count to fix a flaky test — that trades a fast failure for a slow one and keeps the
|
|
82
123
|
race; reach for `waitUntil`/`waitForQuiet` instead.
|
|
83
124
|
|
|
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
|
-
|
|
100
125
|
## What it does NOT do
|
|
101
126
|
|
|
102
|
-
- It is not a mocking framework — there's nothing to configure beyond calling
|
|
103
|
-
the fake always behaves like real Fabric's clone-on-write contract.
|
|
127
|
+
- It is not a mocking framework — there's nothing to configure beyond calling
|
|
128
|
+
`installRecordingFabric()`; the fake always behaves like real Fabric's clone-on-write contract.
|
|
104
129
|
- It does not stand in for on-device verification — see [Testing](../../README.md#testing) for how
|
|
105
130
|
this headless layer and the on-device `Detox` layer divide the work.
|
|
106
131
|
|
package/build/wait-for.js
CHANGED
|
@@ -8,19 +8,14 @@
|
|
|
8
8
|
// proxy breaks, and the failure reads as a product bug — a press that produced no event, change
|
|
9
9
|
// detection that never stopped — rather than as the harness giving up early.
|
|
10
10
|
//
|
|
11
|
-
// Measured 2026-08-19: three tests across two adapters failed only inside a loaded `vitest run`
|
|
12
|
-
// and passed in isolation, every one of them on a fixed tick count.
|
|
13
|
-
//
|
|
14
11
|
// Use `waitUntil` when a condition names what you are waiting for, and `waitForQuiet` when the
|
|
15
12
|
// thing to wait for is the ABSENCE of further work. Do not raise a tick count to fix a flake —
|
|
16
13
|
// that trades a fast failure for a slow one and keeps the race.
|
|
17
14
|
const TICK_MS = 0;
|
|
18
15
|
const DEFAULT_TIMEOUT_MS = 2_000;
|
|
19
16
|
// A tick count cannot express "no work arrived", only "the queue drained N times" — and how much
|
|
20
|
-
// wall time that spans is a property of the machine.
|
|
21
|
-
//
|
|
22
|
-
// quiet before the list's own batch and a loaded one caught it. The producer is RN's VirtualizedList
|
|
23
|
-
// batching period (50ms), the longest deferred one in this repo; quiet has to outlast it.
|
|
17
|
+
// wall time that spans is a property of the machine. The longest deferred producer in this repo
|
|
18
|
+
// is RN's VirtualizedList batching period (50ms); quiet has to outlast it.
|
|
24
19
|
const DEFAULT_QUIET_MS = 75;
|
|
25
20
|
// Read off the host rather than imported, the same reason the engine's animations/raf.ts does:
|
|
26
21
|
// this package's tsconfig carries no DOM and no Node lib, so the ambient names do not exist.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/test-utils",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
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.3.
|
|
43
|
+
"@symbiote-native/engine": "1.3.1"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"typecheck": "tsc --build",
|