1import { describe, expect, it } from "vite-plus/test";
2
3import { formatStartupCrashReport, parseStartupCrashRecords } from "./crash-log-model";
4
5// Verbatim shape of the entry expo-updates wrote for the build 56 launch crash.
6const BUNDLE =
7 "/Users/expo/workingdir/build/apps/mobile/ios/build/Build/Intermediates.noindex/ArchiveIntermediates/T3Code/BuildProductsPath/Release-iphoneos/main.jsbundle";
8const FATAL = {
9 timestamp: 1789277752000,
10 level: "error",
11 code: "JSRuntimeError",
12 message: [
13 "ErrorRecovery fatal exception: Fatal error: Time: 1789277752033.127930",
14 "Domain: RCTErrorDomain",
15 "Code: 0",
16 "Description: Unhandled JS Exception: TypeError: Cannot read property 'defaultModelSelection' of null",
17 "",
18 "This error is located at:",
19 ` at NewTaskFlowProvider (${BUNDLE}:590585:51)`,
20 ` at NavigationProvider (${BUNDLE}:139067:3)`,
21 " at RNSSafeAreaView (<anonymous>)",
22 ].join("\n"),
23};
24
25describe("parseStartupCrashRecords", () => {
26 it("extracts the JS error and a compact component stack from a fatal entry", () => {
27 const [record] = parseStartupCrashRecords([FATAL]);
28 expect(record?.description).toBe(
29 "TypeError: Cannot read property 'defaultModelSelection' of null",
30 );
31 expect(record?.frames).toEqual([
32 "NewTaskFlowProvider (main.jsbundle:590585:51)",
33 "NavigationProvider (main.jsbundle:139067:3)",
34 "RNSSafeAreaView",
35 ]);
36 expect(record?.detail).toContain("Domain: RCTErrorDomain");
37 });
38
39 it("ignores update-check noise and orders crashes newest first", () => {
40 const records = parseStartupCrashRecords([
41 { timestamp: 1, level: "info", code: "NoUpdatesAvailable", message: "checked" },
42 { ...FATAL, timestamp: 10 },
43 { timestamp: 5, level: "error", code: "UpdateFailedToLoad", message: "nope" },
44 { ...FATAL, timestamp: 20 },
45 ]);
46 expect(records.map((record) => record.timestamp)).toEqual([20, 10]);
47 });
48
49 it("skips a runtime error entry without a description line", () => {
50 expect(
51 parseStartupCrashRecords([
52 { ...FATAL, message: "ErrorRecovery fatal exception: Fatal exception: Name: x" },
53 ]),
54 ).toEqual([]);
55 });
56});
57
58describe("formatStartupCrashReport", () => {
59 it("writes the app version and each crash's full detail", () => {
60 const report = formatStartupCrashReport(parseStartupCrashRecords([FATAL]), {
61 version: "1.1.1",
62 build: "56",
63 });
64 expect(report.startsWith("The Knife 1.1.1 (56)\n")).toBe(true);
65 expect(report).toContain("2026-09-13T05:35:52.000Z");
66 expect(report).toContain("at NewTaskFlowProvider");
67 });
68
69 it("says so when nothing was recorded", () => {
70 expect(formatStartupCrashReport([], { version: "1.1.1", build: "56" })).toBe(
71 "The Knife 1.1.1 (56)\nNo startup crashes recorded.",
72 );
73 });
74});