Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .nx/version-plans/version-plan-1779367800000.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
__default__: patch
---

Native crash monitoring was rebuilt around platform-owned app lifecycle monitors, improving crash correlation and artifact collection on Android and iOS while keeping physical-device crash detection reliable without extra app-side dependencies.
1 change: 0 additions & 1 deletion actions/shared/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4422,7 +4422,6 @@ var ConfigSchema = external_exports.object({
unstable__enableMetroCache: external_exports.boolean().optional().default(false),
permissions: external_exports.boolean().optional().default(false).describe("Enable platform-specific permission prompt automation. When false, Harness does not start permission-handling helpers such as the iOS XCTest agent."),
detectNativeCrashes: external_exports.boolean().optional().default(true),
crashDetectionInterval: external_exports.number().min(100, "Crash detection interval must be at least 100ms").default(500),
disableViewFlattening: external_exports.boolean().optional().default(false).describe("Disable view flattening in React Native. This will set collapsable={true} for all View components to ensure they are not flattened by the native layout engine."),
coverage: external_exports.object({
root: external_exports.string().optional().describe(`Root directory for coverage instrumentation in monorepo setups. Specifies the directory from which coverage data should be collected. Use ".." for create-react-native-library projects where tests run from example/ but source files are in parent directory. Passed to babel-plugin-istanbul's cwd option.`),
Expand Down
3 changes: 1 addition & 2 deletions apps/playground/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ apply plugin: "com.facebook.react"
*/
react {
/* Folders */
// The root of your project, i.e. where "package.json" lives. Default is '../..'
// root = file("../../")
root = file("../../")
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
reactNativeDir = file("../../../../node_modules/react-native")
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ class MainApplication : Application(), ReactApplication {
context = applicationContext,
packageList =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(PlaygroundCrashPackage())
},
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.harnessplayground

import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.annotations.ReactModule

@ReactModule(name = PlaygroundCrashModule.NAME)
class PlaygroundCrashModule(
reactContext: ReactApplicationContext,
) : NativePlaygroundCrashSpec(reactContext) {
override fun getName(): String = NAME

override fun crash(message: String) {
val exception = IllegalStateException(
message.ifEmpty { "Intentional PlaygroundCrash crash" },
)

Thread(
{ throw exception },
"PlaygroundCrash",
).start()

Thread.sleep(10_000)
}

override fun crashHandled(message: String): Boolean {
throw IllegalStateException(
message.ifEmpty { "Intentional PlaygroundCrash handled error" },
)
}

companion object {
const val NAME = "PlaygroundCrash"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.harnessplayground

import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider

class PlaygroundCrashPackage : BaseReactPackage() {
override fun getModule(
name: String,
reactContext: ReactApplicationContext,
): NativeModule? =
if (name == PlaygroundCrashModule.NAME) {
PlaygroundCrashModule(reactContext)
} else {
null
}

override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
ReactModuleInfoProvider {
mapOf(
PlaygroundCrashModule.NAME to
ReactModuleInfo(
PlaygroundCrashModule.NAME,
PlaygroundCrashModule.NAME,
false,
false,
false,
true,
),
)
}
}
16 changes: 14 additions & 2 deletions apps/playground/harness-logging-plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,20 @@ export const harnessLoggingPlugin = () => ({
'app.exited',
(ctx) => `runId=${ctx.runId} testFile=${ctx.testFile ?? 'n/a'}`
),
possibleCrash: logWithDetails(
'app.possibleCrash',
crashSuspected: logWithDetails(
'app.crashSuspected',
(ctx) => `runId=${ctx.runId} testFile=${ctx.testFile ?? 'n/a'}`
),
crashConfirmed: logWithDetails(
'app.crashConfirmed',
(ctx) => `runId=${ctx.runId} testFile=${ctx.testFile ?? 'n/a'}`
),
crashReportReady: logWithDetails(
'app.crashReportReady',
(ctx) => `runId=${ctx.runId} artifact=${ctx.artifactPath ?? 'n/a'}`
),
monitorWarning: logWithDetails(
'app.monitorWarning',
(ctx) => `runId=${ctx.runId} testFile=${ctx.testFile ?? 'n/a'}`
),
},
Expand Down
10 changes: 8 additions & 2 deletions apps/playground/ios/HarnessPlayground.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
7B16895B86EC58090B0C2218 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
PGCRASH0032CA45674006654EE /* PlaygroundCrash.mm in Sources */ = {isa = PBXBuildFile; fileRef = PGCRASH0022CA45674006654EE /* PlaygroundCrash.mm */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -25,6 +26,8 @@
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = HarnessPlayground/AppDelegate.swift; sourceTree = "<group>"; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = HarnessPlayground/LaunchScreen.storyboard; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
PGCRASH0012CA45674006654EE /* PlaygroundCrash.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = PlaygroundCrash.h; path = HarnessPlayground/PlaygroundCrash.h; sourceTree = "<group>"; };
PGCRASH0022CA45674006654EE /* PlaygroundCrash.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = PlaygroundCrash.mm; path = HarnessPlayground/PlaygroundCrash.mm; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -44,6 +47,8 @@
children = (
13B07FB51A68108700A75B9A /* Images.xcassets */,
761780EC2CA45674006654EE /* AppDelegate.swift */,
PGCRASH0012CA45674006654EE /* PlaygroundCrash.h */,
PGCRASH0022CA45674006654EE /* PlaygroundCrash.mm */,
13B07FB61A68108700A75B9A /* Info.plist */,
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
Expand Down Expand Up @@ -247,6 +252,7 @@
buildActionMask = 2147483647;
files = (
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */,
PGCRASH0032CA45674006654EE /* PlaygroundCrash.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -260,7 +266,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = "$(HARNESS_PLAYGROUND_DEVELOPMENT_TEAM)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = HarnessPlayground/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
Expand Down Expand Up @@ -289,7 +295,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = "$(HARNESS_PLAYGROUND_DEVELOPMENT_TEAM)";
INFOPLIST_FILE = HarnessPlayground/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
Expand Down
5 changes: 5 additions & 0 deletions apps/playground/ios/HarnessPlayground/PlaygroundCrash.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#import <PlaygroundCrashSpec/PlaygroundCrashSpec.h>

@interface PlaygroundCrash : NSObject <NativePlaygroundCrashSpec>

@end
47 changes: 47 additions & 0 deletions apps/playground/ios/HarnessPlayground/PlaygroundCrash.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#import "PlaygroundCrash.h"

#import <React/RCTLog.h>

@implementation PlaygroundCrash

- (void)crash:(NSString *)message
{
NSString *reason =
message.length > 0 ? message : @"Intentional PlaygroundCrash crash";
RCTLogInfo(@"[PlaygroundCrash] %@", reason);

NSThread *thread = [[NSThread alloc] initWithBlock:^{
@throw [NSException exceptionWithName:@"PlaygroundCrash"
reason:reason
userInfo:nil];
}];
thread.name = @"PlaygroundCrash";
[thread start];

[NSThread sleepForTimeInterval:10.0];
}

- (NSNumber *)crashHandled:(NSString *)message
{
NSString *reason = message.length > 0
? message
: @"Intentional PlaygroundCrash handled error";
RCTLogInfo(@"[PlaygroundCrash] handled %@", reason);

@throw [NSException exceptionWithName:@"PlaygroundCrash"
reason:reason
userInfo:nil];
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativePlaygroundCrashSpecJSI>(params);
}

+ (NSString *)moduleName
{
return @"PlaygroundCrash";
}

@end
6 changes: 3 additions & 3 deletions apps/playground/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ PODS:
- FBLazyVector (0.82.1)
- fmt (11.0.2)
- glog (0.3.5)
- HarnessUI (1.1.0):
- HarnessUI (1.2.0):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -2691,7 +2691,7 @@ SPEC CHECKSUMS:
FBLazyVector: 0aa6183b9afe3c31fc65b5d1eeef1f3c19b63bfa
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
HarnessUI: 01740b858c62c55d42995d4ca459ead036b96c9a
HarnessUI: c5f2b106cfb3944569b791515e304b5e96d63bb6
hermes-engine: 273e30e7fb618279934b0b95ffab60ecedb7acf5
NitroImage: dfec7a8d5e6ba8228ed780bc70041e762cbbbd0b
NitroModules: b24827b7772f5a030aef074547a2393a6e03579e
Expand Down Expand Up @@ -2758,7 +2758,7 @@ SPEC CHECKSUMS:
React-utils: abf37b162f560cd0e3e5d037af30bb796512246d
React-webperformancenativemodule: 50a57c713a90d27ae3ab947a6c9c8859bcb49709
ReactAppDependencyProvider: a45ef34bb22dc1c9b2ac1f74167d9a28af961176
ReactCodegen: 65ae48ae967a383859da021028e6e8dd7b2d97d1
ReactCodegen: 7cbd647ef54597eb03252f261ce11338f72c1576
ReactCommon: 804dc80944fa90b86800b43c871742ec005ca424
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
VisionCamera: 889238ad98665463fcc2fa44385614979263cfc7
Expand Down
13 changes: 13 additions & 0 deletions apps/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
"name": "@react-native-harness/playground",
"version": "0.0.1",
"private": true,
"codegenConfig": {
"name": "PlaygroundCrashSpec",
"type": "modules",
"jsSrcsDir": "src/specs",
"android": {
"javaPackageName": "com.harnessplayground"
},
"ios": {
"modulesProvider": {
"PlaygroundCrash": "PlaygroundCrash"
}
}
},
"scripts": {
"test:harness": "jest --selectProjects react-native-harness"
},
Expand Down
2 changes: 1 addition & 1 deletion apps/playground/rn-harness.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default {
}),
applePlatform({
name: 'ios',
device: appleSimulator('iPhone 17 Pro', '26.4'),
device: appleSimulator('iPhone 17 Pro', '26.2'),
bundleId: 'com.harnessplayground',
}),
applePlatform({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { describe, it } from 'react-native-harness';
import { Platform } from 'react-native';
import PlaygroundCrash from '../../native/PlaygroundCrash';

PlaygroundCrash.crash(
`HARNESS_CRASH_MONITOR_EVALUATION_CRASH platform=${Platform.OS}`
);

describe('Crash monitor: crash during test file evaluation', () => {
it('should never run because the test file crashes before registration', () => {
throw new Error('This test should not run after an evaluation crash');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { describe, it } from 'react-native-harness';
import { Platform } from 'react-native';
import PlaygroundCrash from '../../native/PlaygroundCrash';

describe('Crash monitor: crash during test execution', () => {
it('crashes the native app from inside an it clause', () => {
PlaygroundCrash.crash(
`HARNESS_CRASH_MONITOR_IT_CRASH platform=${Platform.OS}`
);

throw new Error('This line should not run after the native crash');
});
});
79 changes: 79 additions & 0 deletions apps/playground/src/__tests__/logbox-disabled.harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, it, expect, isLogBoxSuppressed } from 'react-native-harness';
import {
LogBox,
Platform,
type TurboModule,
TurboModuleRegistry,
} from 'react-native';
import PlaygroundCrash from '../native/PlaygroundCrash';

const HANDLED_ERROR_MARKER = 'HARNESS_LOGBOX_HANDLED_NATIVE_ERROR';
const CONSOLE_PROBE_MARKER = 'HARNESS_LOGBOX_CONSOLE_PROBE';

type NativeLogBoxModule = TurboModule & {
show: () => void;
hide: () => void;
};

type HarnessLogBox = typeof LogBox & {
addException: (error: unknown) => void;
};

describe('LogBox disabled for harness', () => {
it('suppresses LogBox UI during harness runs', () => {
expect(isLogBoxSuppressed()).toBe(true);
});

it('noops LogBox.addException so errors are not shown in-app', () => {
const harnessLogBox = LogBox as HarnessLogBox;

expect(() =>
harnessLogBox.addException({
message: 'LogBox probe — should not open UI',
stack: [],
id: 0,
isFatal: true,
extraData: {},
}),
).not.toThrow();
});

it('noops the native LogBox TurboModule when linked', () => {
const nativeLogBox =
TurboModuleRegistry.get<NativeLogBoxModule>('LogBox');

if (nativeLogBox == null) {
return;
}

expect(() => nativeLogBox.show()).not.toThrow();
expect(() => nativeLogBox.hide()).not.toThrow();
});

it('surfaces handled native errors on the sync Turbo Module path', () => {
const marker = `${HANDLED_ERROR_MARKER} platform=${Platform.OS}`;

expect(() => PlaygroundCrash.crashHandled(marker)).toThrow(
new RegExp(HANDLED_ERROR_MARKER),
);
});

it('still forwards explicit console.error calls (Metro client_log path)', () => {
const consoleErrors: unknown[][] = [];
const originalConsoleError = console.error;

console.error = (...args: unknown[]) => {
consoleErrors.push(args);
originalConsoleError.apply(console, args);
};

console.error(CONSOLE_PROBE_MARKER);
console.error = originalConsoleError;

expect(
consoleErrors.some((args) =>
String(args[0] ?? '').includes(CONSOLE_PROBE_MARKER),
),
).toBe(true);
});
});
1 change: 1 addition & 0 deletions apps/playground/src/native/PlaygroundCrash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '../specs/NativePlaygroundCrash';
Loading
Loading