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';returnfalse;}

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

github-actionsBot commented Apr 17, 2026

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3392❌ 03392
Suites✅ 461❌ 0461

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 82.9s

… 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 masterApr 17, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the fix/map-probe-newarch-v11 branch April 17, 2026 18:03
Sign up for freeto 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

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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';returnfalse;}

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

github-actionsBot commented Apr 17, 2026

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3392❌ 03392
Suites✅ 461❌ 0461

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 82.9s

… 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 masterApr 17, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the fix/map-probe-newarch-v11 branch April 17, 2026 18:03
Sign up for freeto 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

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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';returnfalse;}

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

github-actionsBot commented Apr 17, 2026

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3392❌ 03392
Suites✅ 461❌ 0461

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 82.9s

… 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 masterApr 17, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the fix/map-probe-newarch-v11 branch April 17, 2026 18:03
Sign up for freeto 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

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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';returnfalse;}

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

github-actionsBot commented Apr 17, 2026

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3392❌ 03392
Suites✅ 461❌ 0461

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 82.9s

… 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 masterApr 17, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the fix/map-probe-newarch-v11 branch April 17, 2026 18:03
Sign up for freeto 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

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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';returnfalse;}

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

github-actionsBot commented Apr 17, 2026

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3392❌ 03392
Suites✅ 461❌ 0461

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 82.9s

… 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 masterApr 17, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the fix/map-probe-newarch-v11 branch April 17, 2026 18:03
Sign up for freeto 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

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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';returnfalse;}

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

github-actionsBot commented Apr 17, 2026

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3392❌ 03392
Suites✅ 461❌ 0461

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 82.9s

… 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 masterApr 17, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the fix/map-probe-newarch-v11 branch April 17, 2026 18:03
Sign up for freeto 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

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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';returnfalse;}

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

github-actionsBot commented Apr 17, 2026

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3392❌ 03392
Suites✅ 461❌ 0461

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 82.9s

… 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 masterApr 17, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the fix/map-probe-newarch-v11 branch April 17, 2026 18:03
Sign up for freeto 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

@CraigBuckmaster@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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';returnfalse;}

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

github-actionsBot commented Apr 17, 2026

Copy link
Copy Markdown

Test Results

✅ All tests passed

PassedFailedTotal
Tests✅ 3392❌ 03392
Suites✅ 461❌ 0461

Coverage

StatementsBranchesFunctionsLines

⏱️ Duration: 82.9s

… 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 masterApr 17, 2026
6 checks passed
@CraigBuckmaster
CraigBuckmaster deleted the fix/map-probe-newarch-v11 branch April 17, 2026 18:03
Sign up for freeto 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

@CraigBuckmaster@claude