Skip to content

fix: probe MapLibre v11 via package exports, not legacy NativeModules name#1499

Merged
CraigBuckmaster merged 6 commits into
masterfrom
fix/map-probe-newarch-v11
Apr 17, 2026
Merged

fix: probe MapLibre v11 via package exports, not legacy NativeModules name#1499
CraigBuckmaster merged 6 commits into
masterfrom
fix/map-probe-newarch-v11

Conversation

@CraigBuckmaster
Copy link
Copy Markdown
Owner

Root cause

After the SDK 54 migration (PR #1495) the app launches fine on TestFlight but the Map tab renders the "Map unavailable in Expo Go" fallback instead of the actual map. The TestFlight build is NOT Expo Go — the card copy is misleading, but the underlying issue is real.

The probe in isMapNativeAvailable.ts opened with a cheap presence check:

if (NativeModules?.MLRNModule == null) {
  _probeError = 'MLRNModule native module not registered';
  return false;
}

Under MapLibre v10 on the legacy bridge, MLRNModule existed as a legacy bridge module and this check worked. Under MapLibre v11 + React Native New Architecture, the native module registers via TurboModuleRegistry as MLRNNetworkModule (see v11 source). NativeModules.MLRNModule is permanently null because that name never gets registered at all.

Net effect: the probe short-circuits to false on every call on v11 builds, regardless of whether MapLibre is actually wired up. The map screen always falls back to MapUnavailableCard.

Fix

Drop the legacy-bridge presence check. Instead, the probe now proves MapLibre is linked by:

  1. Successfully require()-ing the package (catches the Expo Go case — require throws with "Cannot find module")
  2. Finding the Map named export (catches v10 residue — v10 exported MapView, v11 exports Map)
  3. Attempting NetworkManager.setConnected(true) as a cheap native-touch, but swallowing any throw since this method is Android-only in v11 and will legitimately no-op on iOS

This matches MapLibre v11's actual JS surface rather than assumptions inherited from v10.

Better diagnostics

The previous MapUnavailableCard hard-coded "Map unavailable in Expo Go" and gated the diagnostic reason behind __DEV__. Neither fit TestFlight where the build is NOT Expo Go and __DEV__ is false.

The card now:

  • Shows generic "Map unavailable" when the failure reason doesn't match the Expo Go fingerprint ("Cannot find module" / "Unable to resolve module")
  • Shows the diagnostic reason in production when the failure is unexpected — so testers can report what they see instead of us guessing
  • Hides the "how to create a dev build" link when we're clearly not in Expo Go

Test changes

__tests__/unit/isMapNativeAvailable.test.ts rewritten to mutate the jest-mocked MapLibre package (via jest.doMock + jest.resetModules()) instead of NativeModules.MLRNModule. New cases cover:

  • Working v11 build (package loads, Map export present)
  • Expo Go (require throws)
  • Stale v10 residue (package loads but no Map export)
  • iOS path (setConnected throws but probe still returns true)

jest.setup.js had a manual NativeModules.MLRNModule = {} hack to force map-rendering tests through the old probe's happy path. Removed — the new probe ignores NativeModules entirely and relies on the MapLibre jest mock (already present in setup) which exports Map, so tests flow through the happy path naturally.

Verification

After merge:

git pull
cd app
npm test   # should pass, including the 4 new probe cases
eas build --platform ios --profile production
eas submit --platform ios --latest

The map tab should now render the actual map. If something unexpected fails, the card will now tell us what instead of lying about Expo Go.

Files changed

  • app/src/utils/isMapNativeAvailable.ts — core fix
  • app/src/components/map/MapUnavailableCard.tsx — UX + diagnostics
  • app/__tests__/unit/isMapNativeAvailable.test.ts — rewritten
  • app/jest.setup.js — removed stale NativeModules hack

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 17, 2026

Test Results

✅ All tests passed

Passed Failed Total
Tests ✅ 3392 ❌ 0 3392
Suites ✅ 461 ❌ 0 461

Coverage

Statements Branches Functions Lines

⏱️ Duration: 82.9s

claude added 2 commits April 17, 2026 17:51
… mock

Two test suites still leaned on the old v10 pattern of mutating
`NativeModules.MLRNModule` to force the native-availability probe to
return false. The v11 probe consults the `@maplibre/maplibre-react-native`
package's JS export surface instead, so the old hack is a no-op — the
dispatcher happy path ran and the assertions mismatched:

- `__tests__/screens/MapScreenDispatcher.test.tsx` asserted that
  MapUnavailableCard rendered, but the probe saw `Map` on the jest.setup
  mock and returned true, so the dispatcher mounted `MapScreenNative`
  behind Suspense instead (ActivityIndicator). Fix: mock
  `@/utils/isMapNativeAvailable` directly. The card's accessibility
  label is `heading`, which is `"Map unavailable in Expo Go"` when the
  reason matches the Expo-Go fingerprint (`Cannot find module`) — so
  the test now pins both the probe result and the diagnostic reason to
  reach that heading deterministically.

- `__tests__/hooks/useMapTileCache.test.ts` mocked MapLibre with only
  `OfflineManager`. `useMapTileCache` calls `isMapNativeAvailable()`
  first, which requires a `Map` export to return true — so the probe
  short-circuited to false and the hook never touched OfflineManager,
  breaking every assertion. Fix: add `Map` + `NetworkManager` to the
  test's local mock so the probe treats the module as linked. Core
  OfflineManager assertions unchanged.

Full suite: 3380 tests pass, tsc clean.
…ors per-test doMock

Two cases in the probe test suite passed in isolation and under the
plain `npm test` harness but failed under the CI command
`npx jest --coverage --ci --verbose --json`:

  ● returns false when MapLibre package is missing (Expo Go)
  ● returns false when MapLibre package loads but Map export is absent

Both expected `freshProbe()` to return false; both saw true. The probe
was finding a `Map` export on the module even though each test had a
`jest.doMock` that either threw or omitted `Map`.

Root cause: the file used a top-level hoisted `jest.mock` for
`@maplibre/maplibre-react-native` as the "happy path" factory, plus
`beforeEach(() => { jest.resetModules(); __resetMapNativeProbeForTests(); })`
and `jest.doMock(...)` inside each test. Outside of --coverage this
pattern worked — the test-body `doMock` won. Under --coverage (plus the
global jest.setup.js mock that also registers a `Map` export) the
hoisted setup factory kept resolving first, so the test-body `doMock`
never reached the probe.

Fix: wrap each test body in `jest.isolateModules(() => { … })`.
`isolateModules` creates a scoped module registry AND re-resolves mock
factories for that scope, so the per-test `jest.doMock` is guaranteed
to be the one `require()` sees inside the block. The top-level hoisted
`jest.mock` is removed since each test now declares its own factory
explicitly, and the `beforeEach` reset plumbing is no longer needed —
isolateModules handles both.

Verified under the exact CI command:
  npx jest --coverage --ci --verbose --json --outputFile=test-results.json
→ 3380 passed / 460 suites.
@CraigBuckmaster CraigBuckmaster merged commit c40eef5 into master Apr 17, 2026
6 checks passed
@CraigBuckmaster CraigBuckmaster deleted the fix/map-probe-newarch-v11 branch April 17, 2026 18:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants