fix(native): identity-stable, spy-able turboStubs#70
Open
danfry1 wants to merge 1 commit into
Open
Conversation
…ride clearing
Unmocked native modules were served by a Proxy that minted a fresh stub
object on every property access and whose get trap never consulted the
target. Consequences: NativeModules.Foo !== NativeModules.Foo, and
vi.spyOn(NativeModules.Foo, 'method') silently recorded nothing — the spy
landed on a throwaway object (a real pattern in migrated Jest suites).
- Stubs are memoized per module name in the shared boundary state, so
NativeModules.Foo === TurboModuleRegistry.get('Foo'), matching bridgeless
RN where NativeModules proxies TurboModuleRegistry.
- Methods are memoized as own properties of the proxy target on first read
(via defineProperty — sloppy-mode assignment of '__proto__' would invoke
the inherited setter and silently swap the target's prototype), and
explicitly-set properties win, so spies record and restore correctly.
- A has trap reports all properties present, consistent with the get trap's
serve-anything behavior; vi.spyOn's existence check requires it. RN 0.86
was audited for 'in'-operator feature detection against native modules
(none found).
- Hot runtime: each stub registers a callback in the surgical-reset registry
that clears per-file overrides between files while preserving stub
identity for resident libraries. The registry now runs BEFORE the
Dimensions/Appearance value-restores — those restores route through the
very stubs being cleared, so restoring first would send the restore
through a previous file's dead override and leak state (demonstrated in
pre-merge review; the hot-isolation suite now carries a dead-override
regression scenario). The leak's expression is environment-dependent, but
the clear-then-restore order is strictly safer.
Under the default engine each file gets a fresh process, so the reset
registry never fires there.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Unmocked native modules under the native engine are served by
turboStubProxies that minted a fresh stub object on every property access, and whose get trap never consulted the proxy target. Two consequences:NativeModules.Foo !== NativeModules.Foo— identity-unstable across reads (breaks instanceof-style assertions and captured-reference comparisons).vi.spyOn(NativeModules.Foo, 'method')silently recorded nothing — the spy landed on a throwaway object while callers kept receiving fresh closures. This is a real migrated-Jest-suite pattern, and the failure mode is "assertion never fires", not an error.Change
NativeModules.Foo === TurboModuleRegistry.get('Foo'), matching bridgeless RN where NativeModules proxies TurboModuleRegistry.defineProperty— sloppy-mode assignment of__proto__would invoke the inherited setter and swap the target's prototype), with explicitly-set properties winning, sovi.spyOnrecords,mockRestorerestores, and repeated reads are identity-stable.has: () => true— consistent with the get trap serving a callable for every key, and required byvi.spyOn'sinexistence check. RN 0.86's source was audited forin-operator feature detection against native modules: none found.Under the default engine each test file gets a fresh process, so the reset path never fires there.
Review
Independently adversarially reviewed pre-PR (vitest 4.1.8 spy mechanics traced against
@vitest/spysource; RN 0.86 grepped forin-detection; Proxy invariants checked;await stubthenable behavior confirmed unchanged). Review findings (reset ordering,__proto__memoization, stale comment, test-shape gap) are incorporated.Tests
tests/native-unit.test.ts: stub identity across module reads and method reads, spy + restore round-trip, NativeModules ↔ TurboModuleRegistry shared identity, reset-registry clears overrides while preserving identity.tests-native/native-module-spies.test.ts: same guarantees end-to-end under the real native engine (spy on a never-before-accessed method — fails without thehastrap; callback/promise conventions preserved after memoization).tests-native/hot-isolation/: file 1 leaves a dead override on the Appearance stub; file 2 asserts both the boundary value and the override are gone (directNativeModulesreads bypassing RN's JS-side cache).Full gate: mock 1280, native suites (stock/hot/isolation/android/navigation/soak) green, crosscheck 75/75, lint/format/typecheck clean.