Skip to content

Commit 7d40bc3

Browse files
committed
test: add regression tests for B1 network probe, B3 nav hook walker, B4 Jotai detection
10 new tests covering three recent bug fixes: - B3: navigation state found in any hook position (not just first) - B4: Jotai store reads atoms, handles throws, validates get() - B1: network probe falls back to hook mode when CDP fires no events
1 parent a5f1042 commit 7d40bc3

2 files changed

Lines changed: 202 additions & 0 deletions

File tree

scripts/cdp-bridge/test/integration/cdp-client-lifecycle.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,21 @@ describe('CDPClient lifecycle', () => {
7979
}
8080
});
8181

82+
test('falls back to hook mode when Network.enable fires no events (B1)', async () => {
83+
const client = new CDPClient(server.port);
84+
try {
85+
await client.autoConnect(server.port);
86+
// The probe fires a fetch via evaluate, but fake server doesn't emit
87+
// Network.requestWillBeSent, so the probe times out and falls back to hooks
88+
const ok = await poll(() => client.helpersInjected, 15000, 300);
89+
assert.ok(ok, 'Helpers should be injected');
90+
assert.equal(client.networkMode, 'hook',
91+
'Network mode should be "hook" — probe got no CDP events from fake server');
92+
} finally {
93+
await client.disconnect();
94+
}
95+
});
96+
8297
test('autoConnect fails when no Hermes targets are available', async () => {
8398
const { createServer } = await import('node:http');
8499
const srv = createServer((req, res) => {
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { test } from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import vm from 'node:vm';
4+
import { INJECTED_HELPERS } from '../../dist/injected-helpers.js';
5+
6+
/**
7+
* Create a VM sandbox with mock React DevTools globals.
8+
* The INJECTED_HELPERS IIFE reads from globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__
9+
* to find the fiber root, and from globalThis for store/nav globals.
10+
*/
11+
function createSandbox(opts = {}) {
12+
const sandbox = {
13+
globalThis: {},
14+
Array, Object, JSON, Map, WeakSet, Error, Date, parseInt, parseFloat,
15+
typeof: undefined,
16+
console: { log() {}, error() {}, warn() {}, info() {}, debug() {} },
17+
String,
18+
Number,
19+
Boolean,
20+
RegExp,
21+
Symbol,
22+
Set,
23+
Promise,
24+
setTimeout,
25+
clearTimeout,
26+
};
27+
// Share the globalThis reference
28+
sandbox.globalThis = sandbox;
29+
30+
// Set up React DevTools hook with a mock fiber root
31+
if (opts.fiberRoot) {
32+
sandbox.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
33+
renderers: new Map([[1, {}]]),
34+
getFiberRoots: () => new Set([{ current: opts.fiberRoot }]),
35+
};
36+
}
37+
38+
// Copy any extra globals
39+
if (opts.globals) {
40+
Object.assign(sandbox, opts.globals);
41+
}
42+
43+
vm.createContext(sandbox);
44+
vm.runInContext(INJECTED_HELPERS, sandbox);
45+
return sandbox;
46+
}
47+
48+
// ── B3: Navigation state hook walker ─────────────────────────────────
49+
50+
test('getNavState: finds nav state in first hook position', () => {
51+
const navState = { routes: [{ name: 'Home' }], index: 0, routeNames: ['Home'] };
52+
const fiber = {
53+
type: { displayName: 'NavigationContainer' },
54+
memoizedState: {
55+
memoizedState: navState,
56+
next: null,
57+
},
58+
child: null, sibling: null,
59+
};
60+
const sandbox = createSandbox({ fiberRoot: fiber });
61+
const result = JSON.parse(sandbox.__RN_AGENT.getNavState());
62+
assert.equal(result.routeName, 'Home');
63+
assert.deepEqual(result.stack, ['Home']);
64+
});
65+
66+
test('getNavState: finds nav state in third hook position (B3 regression)', () => {
67+
const navState = { routes: [{ name: 'Profile' }, { name: 'Settings' }], index: 1, routeNames: ['Profile', 'Settings'] };
68+
// Simulate: useState, useRef, then useReducer with nav state
69+
const fiber = {
70+
type: { displayName: 'NavigationContainer' },
71+
memoizedState: {
72+
memoizedState: 'not-nav-state', // useState
73+
next: {
74+
memoizedState: { current: null }, // useRef
75+
next: {
76+
memoizedState: navState, // nav state in 3rd position
77+
next: null,
78+
},
79+
},
80+
},
81+
child: null, sibling: null,
82+
};
83+
const sandbox = createSandbox({ fiberRoot: fiber });
84+
const result = JSON.parse(sandbox.__RN_AGENT.getNavState());
85+
assert.equal(result.routeName, 'Settings');
86+
assert.equal(result.index, 1);
87+
});
88+
89+
test('getNavState: finds nav state in queue.lastRenderedState', () => {
90+
const navState = { routes: [{ name: 'Dashboard' }], index: 0, routeNames: ['Dashboard'] };
91+
const fiber = {
92+
type: { displayName: 'NavigationContainer' },
93+
memoizedState: {
94+
memoizedState: null,
95+
queue: { lastRenderedState: navState },
96+
next: null,
97+
},
98+
child: null, sibling: null,
99+
};
100+
const sandbox = createSandbox({ fiberRoot: fiber });
101+
const result = JSON.parse(sandbox.__RN_AGENT.getNavState());
102+
assert.equal(result.routeName, 'Dashboard');
103+
});
104+
105+
test('getNavState: finds nav state via ExpoRoot fiber name', () => {
106+
const navState = { routes: [{ name: 'Index' }], index: 0, routeNames: ['Index'] };
107+
const fiber = {
108+
type: { name: 'ExpoRoot' },
109+
memoizedState: { memoizedState: navState, next: null },
110+
child: null, sibling: null,
111+
};
112+
const sandbox = createSandbox({ fiberRoot: fiber });
113+
const result = JSON.parse(sandbox.__RN_AGENT.getNavState());
114+
assert.equal(result.routeName, 'Index');
115+
});
116+
117+
test('getNavState: returns error when no nav state in hooks', () => {
118+
const fiber = {
119+
type: { displayName: 'NavigationContainer' },
120+
memoizedState: {
121+
memoizedState: 'just a string',
122+
next: { memoizedState: 42, next: null },
123+
},
124+
child: null, sibling: null,
125+
};
126+
const sandbox = createSandbox({ fiberRoot: fiber });
127+
const result = JSON.parse(sandbox.__RN_AGENT.getNavState());
128+
assert.ok(result.error, 'Should return an error when no nav state found');
129+
});
130+
131+
// ── B4: Jotai store detection ────────────────────────────────────────
132+
133+
test('getStoreState: reads Jotai atoms via __JOTAI_STORE__ + __JOTAI_ATOMS__', () => {
134+
const countAtom = { __brand: 'countAtom' };
135+
const userAtom = { __brand: 'userAtom' };
136+
const atomValues = new Map([[countAtom, 42], [userAtom, { name: 'Alice' }]]);
137+
138+
const sandbox = createSandbox({
139+
globals: {
140+
__JOTAI_STORE__: { get: (atom) => atomValues.get(atom) },
141+
__JOTAI_ATOMS__: { count: countAtom, user: userAtom },
142+
},
143+
});
144+
const result = JSON.parse(sandbox.__RN_AGENT.getStoreState(undefined, 'jotai'));
145+
assert.equal(result.type, 'jotai');
146+
assert.equal(result.state.count, 42);
147+
assert.deepEqual(result.state.user, { name: 'Alice' });
148+
});
149+
150+
test('getStoreState: handles Jotai atom that throws', () => {
151+
const goodAtom = { __brand: 'good' };
152+
const badAtom = { __brand: 'bad' };
153+
154+
const sandbox = createSandbox({
155+
globals: {
156+
__JOTAI_STORE__: {
157+
get: (atom) => {
158+
if (atom === badAtom) throw new Error('derived atom not ready');
159+
return 'ok';
160+
},
161+
},
162+
__JOTAI_ATOMS__: { good: goodAtom, bad: badAtom },
163+
},
164+
});
165+
const result = JSON.parse(sandbox.__RN_AGENT.getStoreState(undefined, 'jotai'));
166+
assert.equal(result.type, 'jotai');
167+
assert.equal(result.state.good, 'ok');
168+
assert.match(result.state.bad, /error.*derived atom not ready/);
169+
});
170+
171+
test('getStoreState: skips Jotai when store lacks get function', () => {
172+
const sandbox = createSandbox({
173+
globals: {
174+
__JOTAI_STORE__: { noGetMethod: true },
175+
__JOTAI_ATOMS__: { count: {} },
176+
},
177+
});
178+
const result = JSON.parse(sandbox.__RN_AGENT.getStoreState());
179+
assert.ok(result.__agent_error, 'Should return no-store error when Jotai store lacks get()');
180+
});
181+
182+
test('getStoreState: no-store error includes Jotai hint', () => {
183+
const sandbox = createSandbox({});
184+
const result = JSON.parse(sandbox.__RN_AGENT.getStoreState());
185+
assert.ok(result.hint3, 'hint3 should exist for Jotai');
186+
assert.match(result.hint3, /JOTAI_STORE/);
187+
});

0 commit comments

Comments
 (0)