From 28c678f5ff68335e249e363fc0a33cb0be828499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Tue, 27 Oct 2026 23:18:30 +0100 Subject: [PATCH] ci(ios): fix the flaky Detox e2e tests - Don't block device.launchApp on Detox's logging hint, whose simctl get_app_container call can stall for minutes on loaded runners. - Load the Map Loaded example from a local style, so its test doesn't depend on the connection to api.mapbox.com. - Don't let Detox wait for Mapbox requests, and hide LogBox toasts that cover the example list. - Retry the map tap when a main-thread stall turns it into a long press. --- .../detox-npm-20.51.4-f0776917eb.patch | 14 +++++ example/e2e/firstTest.e2e.js | 33 ++++++++++-- example/src/examples/E2E/MapLoaded.tsx | 36 +++++++++++++ example/src/setup.js | 7 ++- package.json | 3 +- yarn.lock | 54 ++++++++++++++++++- 6 files changed, 139 insertions(+), 8 deletions(-) create mode 100644 .yarn/patches/detox-npm-20.51.4-f0776917eb.patch diff --git a/.yarn/patches/detox-npm-20.51.4-f0776917eb.patch b/.yarn/patches/detox-npm-20.51.4-f0776917eb.patch new file mode 100644 index 0000000000..7f2ff15cb1 --- /dev/null +++ b/.yarn/patches/detox-npm-20.51.4-f0776917eb.patch @@ -0,0 +1,14 @@ +diff --git a/src/devices/common/drivers/ios/tools/AppleSimUtils.js b/src/devices/common/drivers/ios/tools/AppleSimUtils.js +index e722f16e58de4f9447eed0ebac0476c30d8e292e..5daf3f4749ec93eec56dd77acf3994c37396cb6d 100644 +--- a/src/devices/common/drivers/ios/tools/AppleSimUtils.js ++++ b/src/devices/common/drivers/ios/tools/AppleSimUtils.js +@@ -252,7 +252,8 @@ class AppleSimUtils { + const frameworkPath = await environment.getFrameworkPath(); + const effectiveArch = arch && (await this._isArchArgumentAccessible(udid)) ? arch : undefined; + const result = await this._launchMagically(frameworkPath, udid, bundleId, launchArgs, languageAndLocale, effectiveArch); +- await this._printLoggingHint(udid, bundleId); ++ // `simctl get_app_container` can stall for minutes right after a launch on loaded CI runners ++ this._printLoggingHint(udid, bundleId).catch(() => {}); + + return this._parseLaunchId(result); + } diff --git a/example/e2e/firstTest.e2e.js b/example/e2e/firstTest.e2e.js index e9d0d40064..eb43e13478 100644 --- a/example/e2e/firstTest.e2e.js +++ b/example/e2e/firstTest.e2e.js @@ -1,6 +1,7 @@ /* global device, element, by, waitFor, describe, it, expect, beforeAll, afterEach */ const MAP_LOAD_TIMEOUT = 60000; +const TAP_ATTEMPTS = 3; async function openExample(groupTitle, exampleTitle) { await waitFor(element(by.text(groupTitle))) @@ -15,9 +16,31 @@ async function openExample(groupTitle, exampleTitle) { await element(by.text(exampleTitle)).tap(); } +// A main thread stall between Detox's touch down and up turns a tap into a +// long press, which the map doesn't report as a press, so tap again. +async function tapUntilVisible(target, expected) { + for (let attempt = 1; ; attempt++) { + await element(target).tap(); + try { + await waitFor(element(expected)).toBeVisible().withTimeout(10000); + return; + } catch (e) { + if (attempt === TAP_ATTEMPTS) { + throw e; + } + } + } +} + describe('Maps Example App', () => { beforeAll(async () => { - await device.launchApp({ permissions: { location: 'always' } }); + await device.launchApp({ + permissions: { location: 'always' }, + launchArgs: { e2eIgnoreAllLogs: true }, + }); + // Don't wait for slow or stuck Mapbox requests: the map tests wait for + // what they need, and the others don't need the network + await device.setURLBlacklist(['.*\\.mapbox\\.com/.*']); }); afterEach(async () => { @@ -46,9 +69,9 @@ describe('Maps Example App', () => { it('should show click location', async () => { await openExample('Map', 'Show Click'); await expect(element(by.id('show-click-map-view'))).toBeVisible(); - await element(by.id('show-click-map-view')).tap(); - await waitFor(element(by.id('location-bubble-latitude'))) - .toBeVisible() - .withTimeout(10000); + await tapUntilVisible( + by.id('show-click-map-view'), + by.id('location-bubble-latitude'), + ); }); }); diff --git a/example/src/examples/E2E/MapLoaded.tsx b/example/src/examples/E2E/MapLoaded.tsx index c5e3c483f8..0016964885 100644 --- a/example/src/examples/E2E/MapLoaded.tsx +++ b/example/src/examples/E2E/MapLoaded.tsx @@ -4,6 +4,41 @@ import Mapbox from '@rnmapbox/maps'; import sheet from '../../styles/sheet'; +// Needs no network, so the e2e test checks the map itself, not the runner's connection to Mapbox +const styleJSON = JSON.stringify({ + version: 8, + sources: { + square: { + type: 'geojson', + data: { + type: 'Polygon', + coordinates: [ + [ + [19.02, 47.49], + [19.06, 47.49], + [19.06, 47.51], + [19.02, 47.51], + [19.02, 47.49], + ], + ], + }, + }, + }, + layers: [ + { + id: 'background', + type: 'background', + paint: { 'background-color': '#dfe8d4' }, + }, + { + id: 'square', + type: 'fill', + source: 'square', + paint: { 'fill-color': '#3b82f6' }, + }, + ], +}); + const MapLoaded = () => { const [loaded, setLoaded] = useState(false); @@ -11,6 +46,7 @@ const MapLoaded = () => { <> setLoaded(true)} > diff --git a/example/src/setup.js b/example/src/setup.js index fc99fbb162..80939bf52c 100644 --- a/example/src/setup.js +++ b/example/src/setup.js @@ -1,5 +1,10 @@ -import { LogBox } from 'react-native'; +import { LogBox, Platform, Settings } from 'react-native'; LogBox.ignoreLogs([ "Deep imports from the 'react-native' package are deprecated ('react-native/Libraries/Utilities/codegenNativeComponent')", ]); + +// Set by the Detox e2e tests: LogBox toasts can cover the list rows they tap +if (Platform.OS === 'ios' && Settings.get('e2eIgnoreAllLogs')) { + LogBox.ignoreAllLogs(); +} diff --git a/package.json b/package.json index d4ff9ec3cf..bc5742364e 100644 --- a/package.json +++ b/package.json @@ -157,7 +157,8 @@ ], "resolutions": { "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0" + "@typescript-eslint/parser": "^7.18.0", + "detox@^20.51.4": "patch:detox@npm%3A20.51.4#./.yarn/patches/detox-npm-20.51.4-f0776917eb.patch" }, "packageManager": "yarn@3.6.1", "react-native-builder-bob": { diff --git a/yarn.lock b/yarn.lock index cc48a13a55..8159660848 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8762,7 +8762,7 @@ __metadata: languageName: node linkType: hard -"detox@npm:^20.51.4": +"detox@npm:20.51.4": version: 20.51.4 resolution: "detox@npm:20.51.4" dependencies: @@ -8814,6 +8814,58 @@ __metadata: languageName: node linkType: hard +"detox@patch:detox@npm%3A20.51.4#./.yarn/patches/detox-npm-20.51.4-f0776917eb.patch::locator=%40rnmapbox%2Fmaps%40workspace%3A.": + version: 20.51.4 + resolution: "detox@patch:detox@npm%3A20.51.4#./.yarn/patches/detox-npm-20.51.4-f0776917eb.patch::version=20.51.4&hash=38be76&locator=%40rnmapbox%2Fmaps%40workspace%3A." + dependencies: + "@wix-pilot/core": ^3.4.2 + "@wix-pilot/detox": ^1.0.13 + ajv: ^8.6.3 + bunyan: ^1.8.12 + bunyan-debug-stream: ^3.1.0 + caf: ^15.0.1 + chalk: ^4.0.0 + execa: ^5.1.1 + find-up: ^5.0.0 + fs-extra: ^11.0.0 + funpermaproxy: ^1.1.0 + glob: ^8.0.3 + ini: ^1.3.4 + jest-environment-emit: ^1.2.0 + json-cycle: ^1.3.0 + lodash: ^4.17.11 + multi-sort-stream: ^1.0.3 + multipipe: ^4.0.0 + node-ipc: 9.2.1 + promisify-child-process: ^4.1.2 + proper-lockfile: ^3.0.2 + resolve-from: ^5.0.0 + sanitize-filename: ^1.6.1 + semver: ^7.0.0 + serialize-error: ^8.0.1 + shell-quote: ^1.7.2 + signal-exit: ^3.0.3 + stream-json: ^1.7.4 + strip-ansi: ^6.0.1 + telnet-client: 1.2.8 + tmp: ^0.2.1 + trace-event-lib: ^1.3.1 + which: ^1.3.1 + ws: ^7.0.0 + yargs: ^17.0.0 + yargs-parser: ^21.0.0 + yargs-unparser: ^2.0.0 + peerDependencies: + jest: 30.x.x || 29.x.x || 28.x.x || ^27.2.5 + peerDependenciesMeta: + jest: + optional: true + bin: + detox: local-cli/cli.js + checksum: 5591801d5854c49b7f48f35149a1b646b729869e4dd90d28a01025afbb415f24b7a316747970d6d1f43e7fa4331b973d57c708a394506f10045976e2e8cf3200 + languageName: node + linkType: hard + "devlop@npm:^1.0.0, devlop@npm:^1.1.0": version: 1.1.0 resolution: "devlop@npm:1.1.0"