Skip to content

Commit 3d9322e

Browse files
committed
Add native crash plugin review coverage
1 parent c8066a4 commit 3d9322e

1 file changed

Lines changed: 131 additions & 3 deletions

File tree

packages/react-native/test/plugins/NativeCrashPlugin.test.ts

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
import { beforeEach, describe, expect, test, vi } from "vitest";
22

33
import { ExceptionlessClient } from "@exceptionless/core";
4+
import type { CrashReport, ExceptionlessNativeModuleInterface } from "../../src/native/ExceptionlessNativeModule.js";
45
import { NativeCrashPlugin } from "../../src/plugins/NativeCrashPlugin.js";
56

6-
// Mock the native module
7+
const nativeModuleMock = vi.hoisted(() => ({
8+
current: null as ExceptionlessNativeModuleInterface | null
9+
}));
10+
711
vi.mock("../../src/native/ExceptionlessNativeModule.js", () => ({
8-
getNativeModule: () => null,
9-
isNativeModuleAvailable: () => false
12+
getNativeModule: () => nativeModuleMock.current,
13+
isNativeModuleAvailable: () => nativeModuleMock.current !== null
1014
}));
1115

1216
describe("NativeCrashPlugin", () => {
1317
let plugin: NativeCrashPlugin;
1418
let client: ExceptionlessClient;
1519

1620
beforeEach(() => {
21+
nativeModuleMock.current = null;
22+
vi.clearAllMocks();
23+
1724
plugin = new NativeCrashPlugin();
1825
client = new ExceptionlessClient();
1926
client.config.apiKey = "UNIT_TEST_API_KEY";
@@ -55,4 +62,125 @@ describe("NativeCrashPlugin", () => {
5562
warnSpy.mockRestore();
5663
Object.defineProperty(Platform, "OS", { value: "ios", writable: true });
5764
});
65+
66+
test("should submit pending native crash reports and clear them after successful submission", async () => {
67+
const { Platform } = await import("react-native");
68+
Object.defineProperty(Platform, "OS", { value: "ios", writable: true });
69+
70+
const report = createCrashReport();
71+
const nativeModule = createNativeModule([report]);
72+
nativeModuleMock.current = nativeModule;
73+
74+
const submit = vi.fn().mockResolvedValue(undefined);
75+
const setProperty = vi.fn().mockReturnThis();
76+
const createUnhandledException = vi.spyOn(client, "createUnhandledException").mockReturnValue({
77+
setProperty,
78+
submit
79+
} as never);
80+
81+
await plugin.startup({ client, log: client.config.services.log });
82+
83+
expect(nativeModule.install).toHaveBeenCalledOnce();
84+
expect(nativeModule.hasPendingCrashReport).toHaveBeenCalledOnce();
85+
expect(nativeModule.getPendingCrashReports).toHaveBeenCalledOnce();
86+
expect(createUnhandledException).toHaveBeenCalledOnce();
87+
88+
const [error, source] = createUnhandledException.mock.calls[0];
89+
expect(source).toBe("NativeCrashReporter");
90+
expect(error).toBeInstanceOf(Error);
91+
expect(error.name).toBe("NSInvalidArgumentException");
92+
expect(error.message).toBe("Native crash from unit test");
93+
expect(error.stack).toContain(" at -[CrashyViewController crash] (unknown+42)");
94+
expect(error.stack).toContain(" at 0x100000abc (ExceptionlessExpoExample)");
95+
96+
expect(setProperty).toHaveBeenCalledWith("native_crash", {
97+
signal_name: "SIGABRT",
98+
signal_code: "0",
99+
exception_type: "NSException",
100+
crashed_thread: 3,
101+
device: report.device,
102+
timestamp: report.timestamp
103+
});
104+
expect(submit).toHaveBeenCalledOnce();
105+
expect(nativeModule.clearPendingCrashReports).toHaveBeenCalledOnce();
106+
expect(submit.mock.invocationCallOrder[0]).toBeLessThan(nativeModule.clearPendingCrashReports.mock.invocationCallOrder[0]);
107+
});
108+
109+
test("should not clear pending native crash reports when a pending marker returns no reports", async () => {
110+
const { Platform } = await import("react-native");
111+
Object.defineProperty(Platform, "OS", { value: "ios", writable: true });
112+
113+
const nativeModule = createNativeModule([]);
114+
nativeModuleMock.current = nativeModule;
115+
116+
const warnSpy = vi.spyOn(client.config.services.log, "warn");
117+
const createUnhandledException = vi.spyOn(client, "createUnhandledException");
118+
119+
await plugin.startup({ client, log: client.config.services.log });
120+
121+
expect(nativeModule.install).toHaveBeenCalledOnce();
122+
expect(nativeModule.hasPendingCrashReport).toHaveBeenCalledOnce();
123+
expect(nativeModule.getPendingCrashReports).toHaveBeenCalledOnce();
124+
expect(createUnhandledException).not.toHaveBeenCalled();
125+
expect(nativeModule.clearPendingCrashReports).not.toHaveBeenCalled();
126+
expect(warnSpy).toHaveBeenCalledWith("Native crash reporter indicated a pending crash, but no crash reports were returned.");
127+
});
58128
});
129+
130+
function createNativeModule(reports: CrashReport[], hasPending = true) {
131+
return {
132+
install: vi.fn(),
133+
hasPendingCrashReport: vi.fn().mockResolvedValue(hasPending),
134+
getPendingCrashReports: vi.fn().mockResolvedValue(reports),
135+
clearPendingCrashReports: vi.fn().mockResolvedValue(undefined)
136+
};
137+
}
138+
139+
function createCrashReport(): CrashReport {
140+
return {
141+
timestamp: "2026-05-31T03:00:00.000Z",
142+
signal_name: "SIGABRT",
143+
signal_code: "0",
144+
exception_type: "NSException",
145+
exception_name: "NSInvalidArgumentException",
146+
exception_reason: "Native crash from unit test",
147+
crashed_thread: 3,
148+
threads: [
149+
{
150+
thread_id: 1,
151+
crashed: false,
152+
frames: [
153+
{
154+
address: "0x100000001",
155+
image: "UIKit",
156+
symbol: "UIApplicationMain",
157+
offset: 12
158+
}
159+
]
160+
},
161+
{
162+
thread_id: 3,
163+
crashed: true,
164+
frames: [
165+
{
166+
address: "0x100000123",
167+
image: null,
168+
symbol: "-[CrashyViewController crash]",
169+
offset: 42
170+
},
171+
{
172+
address: "0x100000abc",
173+
image: "ExceptionlessExpoExample",
174+
symbol: null,
175+
offset: null
176+
}
177+
]
178+
}
179+
],
180+
device: {
181+
model: "iPad Pro 11-inch",
182+
os_version: "iOS 18.0",
183+
app_version: "1.0.0"
184+
}
185+
};
186+
}

0 commit comments

Comments
 (0)