@symbiote-native/test-utils 0.4.1 → 0.4.3

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.
Files changed (2) hide show
  1. package/README.md +79 -54
  2. 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
- `installFabric()` that puts a fresh, faithful fake `nativeFabricUIManager` on `globalThis` and
5
- returns a handle to inspect what a renderer committed. It replaces the per-file fake slot every
6
- headless test used to copy-paste (×65 across the repo) with one implementation, shared by the
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 { installFabric } from '@symbiote-native/test-utils';
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.serialize(fabric.committed)).toContain('Taps: 1');
39
+ expect(fabric.propsOf(button!.handle).testID).toBe('tap-target');
40
+ fabric.reset();
39
41
  });
40
42
  ```
41
43
 
42
- Call `installFabric()` ONCE per test file, at module scope, and `.reset()` the handle between
43
- tests. Do not re-install per test: the engine's `getSlot()` (`core/engine/src/fabric.ts`) reads the
44
- slot's methods once and caches them for the process lifetime, so a second `installFabric()` after
45
- anything has committed swaps the global while the engine keeps writing through the handle it
46
- already bound — the new recorder just stays empty, with nothing to signal why. File-level isolation
47
- comes from the test runner; `reset()` is what separates tests within a file.
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 `installFabric()` gives you
52
+ ## What `installRecordingFabric()` gives you
50
53
 
51
- - **`committed`** — the child set from the most recent `completeRoot`, and **`appRoot()`** — unwraps
52
- RN's synthetic `box-none` AppContainer root so a test doesn't re-check that invariant by
53
- hand.
54
- - **`created`** and **`find(predicate)`** — every node ever `createNode`'d this run (clones
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`** and **`counts`** — every imperative command dispatched at a committed node, and
59
- call counters (`createNode` / `completeRoot`) for tests asserting "exactly N native nodes".
60
- - **`serialize(nodes)`** — a committed tree as `RCTView(RCTText(RCTRawText "text"))` shorthand, for
61
- a one-line snapshot instead of walking `IFakeNode` by hand.
62
- - **`reset()`** — zeroes the counters and clears `committed`/`created` (the registered event handler
63
- survives), for reusing one `installFabric()` call across several assertions in one test.
64
-
65
- The fake's persistence semantics are **faithful to real Fabric**, not simplified: every clone gets a
66
- new identity, `clone*WithNewProps` **merges** the diff onto the previous props exactly like native
67
- Fabric does with the engine's minimal-diff payload (a removed key arrives as literal `null` and
68
- stays `null`, so a test can tell "explicitly reset" apart from "never set"), and `appendChild`
69
- throws on an illegal family reparent. A persistence bug in the fake is fixed once, here, for every
70
- test that depends on it.
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, stableTicks?, timeoutMs?)` waits until `sample()` returns the same
78
- value across `stableTicks` consecutive macrotasks and returns that settled value — the shape for
79
- "work has stopped arriving" (a batched commit, a zoneless change-detection pass, a press-timing
80
- timer). `advanceTicks(count)` is kept for the genuine "let the queue drain N times" case. Do not
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 `installFabric()`;
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/test-utils",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
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.2.0"
43
+ "@symbiote-native/engine": "1.3.0"
44
44
  },
45
45
  "scripts": {
46
46
  "typecheck": "tsc --build",