From 0a7359257770ef52e01f9c40d7dc118b95143425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 1 Apr 2026 14:25:31 +0200 Subject: [PATCH 1/3] fix: make libimobiledevice optional when native crash detection is off --- .../src/__tests__/instance.test.ts | 42 ++++++++++++++++++- packages/platform-ios/src/instance.ts | 26 ++++++++++-- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/packages/platform-ios/src/__tests__/instance.test.ts b/packages/platform-ios/src/__tests__/instance.test.ts index 975863d..cec2f9b 100644 --- a/packages/platform-ios/src/__tests__/instance.test.ts +++ b/packages/platform-ios/src/__tests__/instance.test.ts @@ -15,6 +15,11 @@ const harnessConfig = { metroPort: DEFAULT_METRO_PORT, } as HarnessConfig; +const harnessConfigWithoutNativeCrashDetection = { + metroPort: DEFAULT_METRO_PORT, + detectNativeCrashes: false, +} as HarnessConfig; + describe('iOS platform instance dependency validation', () => { beforeEach(() => { vi.restoreAllMocks(); @@ -43,7 +48,7 @@ describe('iOS platform instance dependency validation', () => { expect(assertInstalled).not.toHaveBeenCalled(); }); - it('validates libimobiledevice before creating a physical device instance', async () => { + it('validates libimobiledevice before creating a physical device instance when native crash detection is enabled', async () => { const assertInstalled = vi .spyOn(libimobiledevice, 'assertLibimobiledeviceInstalled') .mockRejectedValue(new Error('missing')); @@ -85,7 +90,7 @@ describe('iOS platform instance dependency validation', () => { expect(getSimulatorId).toHaveBeenCalled(); }); - it('does not try to discover the physical device when the dependency is missing', async () => { + it('does not try to discover the physical device when the dependency is missing and native crash detection is enabled', async () => { vi.spyOn(libimobiledevice, 'assertLibimobiledeviceInstalled').mockRejectedValue( new Error('missing') ); @@ -102,4 +107,37 @@ describe('iOS platform instance dependency validation', () => { ).rejects.toThrow('missing'); expect(getDeviceId).not.toHaveBeenCalled(); }); + + it('skips libimobiledevice validation when native crash detection is disabled', async () => { + const assertInstalled = vi + .spyOn(libimobiledevice, 'assertLibimobiledeviceInstalled') + .mockRejectedValue(new Error('missing')); + vi.spyOn(devicectl, 'getDevice').mockResolvedValue({ + identifier: 'physical-device-id', + deviceProperties: { + name: 'My iPhone', + osVersionNumber: '18.0', + }, + hardwareProperties: { + marketingName: 'iPhone', + productType: 'iPhone17,1', + udid: '00008140-001600222422201C', + }, + }); + vi.spyOn(devicectl, 'isAppInstalled').mockResolvedValue(true); + + const config = { + name: 'ios-device', + device: { type: 'physical' as const, name: 'My iPhone' }, + bundleId: 'com.harnessplayground', + }; + + await expect( + getApplePhysicalDevicePlatformInstance( + config, + harnessConfigWithoutNativeCrashDetection + ) + ).resolves.toBeDefined(); + expect(assertInstalled).not.toHaveBeenCalled(); + }); }); diff --git a/packages/platform-ios/src/instance.ts b/packages/platform-ios/src/instance.ts index 1386466..2e08a03 100644 --- a/packages/platform-ios/src/instance.ts +++ b/packages/platform-ios/src/instance.ts @@ -1,4 +1,5 @@ import { + AppMonitor, AppNotInstalledError, CreateAppMonitorOptions, DeviceNotFoundError, @@ -22,6 +23,14 @@ import { } from './app-monitor.js'; import { assertLibimobiledeviceInstalled } from './libimobiledevice.js'; +const createNoopAppMonitor = (): AppMonitor => ({ + start: async () => {}, + stop: async () => {}, + dispose: async () => {}, + addListener: () => {}, + removeListener: () => {}, +}); + export const getAppleSimulatorPlatformInstance = async ( config: ApplePlatformConfig, harnessConfig: HarnessConfig @@ -100,7 +109,11 @@ export const getApplePhysicalDevicePlatformInstance = async ( harnessConfig: HarnessConfig ): Promise => { assertAppleDevicePhysical(config.device); - await assertLibimobiledeviceInstalled(); + const detectNativeCrashes = harnessConfig.detectNativeCrashes; + + if (detectNativeCrashes) { + await assertLibimobiledeviceInstalled(); + } if (harnessConfig.metroPort !== DEFAULT_METRO_PORT) { throw new Error( @@ -153,12 +166,17 @@ export const getApplePhysicalDevicePlatformInstance = async ( isAppRunning: async () => { return await devicectl.isAppRunning(deviceId, config.bundleId); }, - createAppMonitor: (options?: CreateAppMonitorOptions) => - createIosDeviceAppMonitor({ + createAppMonitor: (options?: CreateAppMonitorOptions) => { + if (!detectNativeCrashes) { + return createNoopAppMonitor(); + } + + return createIosDeviceAppMonitor({ deviceId, libimobiledeviceUdid: hardwareUdid, bundleId: config.bundleId, crashArtifactWriter: options?.crashArtifactWriter, - }), + }); + }, }; }; From 5c4c71fca613ac1ad78012b65bcd9232e4619775 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 2 Apr 2026 11:31:43 +0200 Subject: [PATCH 2/3] fix: disable mobile crash monitors when detection is off --- .../disable-native-crash-monitoring.md | 5 ++ .../src/__tests__/instance.test.ts | 77 ++++++++++++++++++- packages/platform-android/src/instance.ts | 33 ++++++-- .../src/__tests__/instance.test.ts | 32 ++++++++ packages/platform-ios/src/instance.ts | 14 +++- 5 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 .nx/version-plans/disable-native-crash-monitoring.md diff --git a/.nx/version-plans/disable-native-crash-monitoring.md b/.nx/version-plans/disable-native-crash-monitoring.md new file mode 100644 index 0000000..fd3af62 --- /dev/null +++ b/.nx/version-plans/disable-native-crash-monitoring.md @@ -0,0 +1,5 @@ +--- +__default__: patch +--- + +Mobile runners now fully disable native crash monitoring when `detectNativeCrashes` is set to `false`, including iOS simulators and Android emulators and physical devices. This keeps crash-monitor setup aligned with the runtime setting while preserving the existing default behavior of enabling native crash detection when the option is omitted. diff --git a/packages/platform-android/src/__tests__/instance.test.ts b/packages/platform-android/src/__tests__/instance.test.ts index debe02d..1310bba 100644 --- a/packages/platform-android/src/__tests__/instance.test.ts +++ b/packages/platform-android/src/__tests__/instance.test.ts @@ -18,6 +18,10 @@ import { HarnessAppPathError, HarnessEmulatorConfigError } from '../errors.js'; const harnessConfig = { metroPort: DEFAULT_METRO_PORT, } as HarnessConfig; +const harnessConfigWithoutNativeCrashDetection = { + metroPort: DEFAULT_METRO_PORT, + detectNativeCrashes: false, +} as HarnessConfig; const init = { signal: new AbortController().signal, }; @@ -504,7 +508,53 @@ describe('Android platform instance', () => { ).rejects.toBeInstanceOf(HarnessEmulatorConfigError); }); - it('keeps physical device behavior unchanged', async () => { + it('returns a noop emulator app monitor when native crash detection is disabled', async () => { + vi.spyOn( + await import('../environment.js'), + 'ensureAndroidEmulatorEnvironment' + ).mockResolvedValue('/tmp/android-sdk'); + vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']); + vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35'); + vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554'); + vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true); + vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined); + vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined); + vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234); + vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue( + undefined + ); + + const instance = await getAndroidEmulatorPlatformInstance( + { + name: 'android', + device: { + type: 'emulator', + name: 'Pixel_8_API_35', + avd: { + apiLevel: 35, + profile: 'pixel_8', + diskSize: '1G', + heapSize: '1G', + }, + }, + bundleId: 'com.harnessplayground', + activityName: '.MainActivity', + }, + harnessConfigWithoutNativeCrashDetection, + init + ); + + const listener = vi.fn(); + const appMonitor = instance.createAppMonitor(); + + await expect(appMonitor.start()).resolves.toBeUndefined(); + await expect(appMonitor.stop()).resolves.toBeUndefined(); + await expect(appMonitor.dispose()).resolves.toBeUndefined(); + expect(appMonitor.addListener(listener)).toBeUndefined(); + expect(appMonitor.removeListener(listener)).toBeUndefined(); + }); + + it('returns a noop physical device app monitor when native crash detection is disabled', async () => { vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['012345']); vi.spyOn(adb, 'getDeviceInfo').mockResolvedValue({ manufacturer: 'motorola', @@ -530,8 +580,31 @@ describe('Android platform instance', () => { bundleId: 'com.harnessplayground', activityName: '.MainActivity', }, - harnessConfig + harnessConfigWithoutNativeCrashDetection ) ).resolves.toBeDefined(); + + const instance = await getAndroidPhysicalDevicePlatformInstance( + { + name: 'android-device', + device: { + type: 'physical', + manufacturer: 'motorola', + model: 'moto g72', + }, + bundleId: 'com.harnessplayground', + activityName: '.MainActivity', + }, + harnessConfigWithoutNativeCrashDetection + ); + + const listener = vi.fn(); + const appMonitor = instance.createAppMonitor(); + + await expect(appMonitor.start()).resolves.toBeUndefined(); + await expect(appMonitor.stop()).resolves.toBeUndefined(); + await expect(appMonitor.dispose()).resolves.toBeUndefined(); + expect(appMonitor.addListener(listener)).toBeUndefined(); + expect(appMonitor.removeListener(listener)).toBeUndefined(); }); }); diff --git a/packages/platform-android/src/instance.ts b/packages/platform-android/src/instance.ts index 3f8c4bb..7a626e8 100644 --- a/packages/platform-android/src/instance.ts +++ b/packages/platform-android/src/instance.ts @@ -32,9 +32,18 @@ import { } from './environment.js'; import { isInteractive } from '@react-native-harness/tools'; import fs from 'node:fs'; +import type { AppMonitor } from '@react-native-harness/platforms'; const androidInstanceLogger = logger.child('android-instance'); +const createNoopAppMonitor = (): AppMonitor => ({ + start: async () => {}, + stop: async () => {}, + dispose: async () => {}, + addListener: () => {}, + removeListener: () => {}, +}); + const getHarnessAppPath = (): string => { const appPath = process.env.HARNESS_APP_PATH; @@ -168,6 +177,7 @@ export const getAndroidEmulatorPlatformInstance = async ( init: HarnessPlatformInitOptions ): Promise => { assertAndroidDeviceEmulator(config.device); + const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true; const emulatorConfig = config.device; const emulatorName = emulatorConfig.name; const avdConfig = emulatorConfig.avd; @@ -284,13 +294,18 @@ export const getAndroidEmulatorPlatformInstance = async ( isAppRunning: async () => { return await adb.isAppRunning(adbId, config.bundleId); }, - createAppMonitor: (options?: CreateAppMonitorOptions) => - createAndroidAppMonitor({ + createAppMonitor: (options?: CreateAppMonitorOptions) => { + if (!detectNativeCrashes) { + return createNoopAppMonitor(); + } + + return createAndroidAppMonitor({ adbId, bundleId: config.bundleId, appUid, crashArtifactWriter: options?.crashArtifactWriter, - }), + }); + }, }; }; @@ -299,6 +314,7 @@ export const getAndroidPhysicalDevicePlatformInstance = async ( harnessConfig: HarnessConfig ): Promise => { assertAndroidDevicePhysical(config.device); + const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true; const adbId = await getAdbId(config.device); @@ -348,12 +364,17 @@ export const getAndroidPhysicalDevicePlatformInstance = async ( isAppRunning: async () => { return await adb.isAppRunning(adbId, config.bundleId); }, - createAppMonitor: (options?: CreateAppMonitorOptions) => - createAndroidAppMonitor({ + createAppMonitor: (options?: CreateAppMonitorOptions) => { + if (!detectNativeCrashes) { + return createNoopAppMonitor(); + } + + return createAndroidAppMonitor({ adbId, bundleId: config.bundleId, appUid, crashArtifactWriter: options?.crashArtifactWriter, - }), + }); + }, }; }; diff --git a/packages/platform-ios/src/__tests__/instance.test.ts b/packages/platform-ios/src/__tests__/instance.test.ts index 08793c5..e3816f7 100644 --- a/packages/platform-ios/src/__tests__/instance.test.ts +++ b/packages/platform-ios/src/__tests__/instance.test.ts @@ -159,6 +159,38 @@ describe('iOS platform instance dependency validation', () => { expect(assertInstalled).not.toHaveBeenCalled(); }); + it('returns a noop simulator app monitor when native crash detection is disabled', async () => { + vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid'); + vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true); + vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted'); + vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue( + undefined + ); + + const instance = await getAppleSimulatorPlatformInstance( + { + name: 'ios', + device: { + type: 'simulator', + name: 'iPhone 16 Pro', + systemVersion: '18.0', + }, + bundleId: 'com.harnessplayground', + }, + harnessConfigWithoutNativeCrashDetection, + init + ); + + const listener = vi.fn(); + const appMonitor = instance.createAppMonitor(); + + await expect(appMonitor.start()).resolves.toBeUndefined(); + await expect(appMonitor.stop()).resolves.toBeUndefined(); + await expect(appMonitor.dispose()).resolves.toBeUndefined(); + expect(appMonitor.addListener(listener)).toBeUndefined(); + expect(appMonitor.removeListener(listener)).toBeUndefined(); + }); + it('reuses a booted simulator and does not shut it down on dispose', async () => { vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid'); vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted'); diff --git a/packages/platform-ios/src/instance.ts b/packages/platform-ios/src/instance.ts index 2d7dce2..ae68924 100644 --- a/packages/platform-ios/src/instance.ts +++ b/packages/platform-ios/src/instance.ts @@ -57,6 +57,7 @@ export const getAppleSimulatorPlatformInstance = async ( init: HarnessPlatformInitOptions ): Promise => { assertAppleDeviceSimulator(config.device); + const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true; const udid = await simctl.getSimulatorId( config.device.name, @@ -153,12 +154,17 @@ export const getAppleSimulatorPlatformInstance = async ( isAppRunning: async () => { return await simctl.isAppRunning(udid, config.bundleId); }, - createAppMonitor: (options?: CreateAppMonitorOptions) => - createIosSimulatorAppMonitor({ + createAppMonitor: (options?: CreateAppMonitorOptions) => { + if (!detectNativeCrashes) { + return createNoopAppMonitor(); + } + + return createIosSimulatorAppMonitor({ udid, bundleId: config.bundleId, crashArtifactWriter: options?.crashArtifactWriter, - }), + }); + }, }; }; @@ -167,7 +173,7 @@ export const getApplePhysicalDevicePlatformInstance = async ( harnessConfig: HarnessConfig ): Promise => { assertAppleDevicePhysical(config.device); - const detectNativeCrashes = harnessConfig.detectNativeCrashes; + const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true; if (detectNativeCrashes) { await assertLibimobiledeviceInstalled(); From 6ef9fd63c4d957e608238e6bc232a7a44232dd19 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 2 Apr 2026 11:36:41 +0200 Subject: [PATCH 3/3] fix: satisfy noop app monitor lint rules --- packages/platform-android/src/instance.ts | 10 +++++----- packages/platform-ios/src/instance.ts | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/platform-android/src/instance.ts b/packages/platform-android/src/instance.ts index 7a626e8..335e9cf 100644 --- a/packages/platform-android/src/instance.ts +++ b/packages/platform-android/src/instance.ts @@ -37,11 +37,11 @@ import type { AppMonitor } from '@react-native-harness/platforms'; const androidInstanceLogger = logger.child('android-instance'); const createNoopAppMonitor = (): AppMonitor => ({ - start: async () => {}, - stop: async () => {}, - dispose: async () => {}, - addListener: () => {}, - removeListener: () => {}, + start: async () => undefined, + stop: async () => undefined, + dispose: async () => undefined, + addListener: () => undefined, + removeListener: () => undefined, }); const getHarnessAppPath = (): string => { diff --git a/packages/platform-ios/src/instance.ts b/packages/platform-ios/src/instance.ts index ae68924..f5c52a7 100644 --- a/packages/platform-ios/src/instance.ts +++ b/packages/platform-ios/src/instance.ts @@ -44,11 +44,11 @@ const getHarnessAppPath = (): string => { }; const createNoopAppMonitor = (): AppMonitor => ({ - start: async () => {}, - stop: async () => {}, - dispose: async () => {}, - addListener: () => {}, - removeListener: () => {}, + start: async () => undefined, + stop: async () => undefined, + dispose: async () => undefined, + addListener: () => undefined, + removeListener: () => undefined, }); export const getAppleSimulatorPlatformInstance = async (