@react-native-harness/jest 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/__tests__/bridge.test.js +29 -4
  2. package/dist/__tests__/crash-monitor.test.js +194 -176
  3. package/dist/__tests__/environment-reset.test.d.ts +2 -0
  4. package/dist/__tests__/environment-reset.test.d.ts.map +1 -0
  5. package/dist/__tests__/environment-reset.test.js +238 -0
  6. package/dist/__tests__/errors.test.js +39 -0
  7. package/dist/__tests__/execute-run.test.js +439 -29
  8. package/dist/__tests__/format-harness-error.test.d.ts +2 -0
  9. package/dist/__tests__/format-harness-error.test.d.ts.map +1 -0
  10. package/dist/__tests__/format-harness-error.test.js +27 -0
  11. package/dist/__tests__/harness-cache.test.js +62 -41
  12. package/dist/__tests__/harness-session-signal.test.d.ts +2 -0
  13. package/dist/__tests__/harness-session-signal.test.d.ts.map +1 -0
  14. package/dist/__tests__/harness-session-signal.test.js +298 -0
  15. package/dist/__tests__/harness-session.test.js +217 -234
  16. package/dist/__tests__/index.test.js +62 -11
  17. package/dist/__tests__/metro-port.test.js +2 -0
  18. package/dist/__tests__/run.test.d.ts +2 -0
  19. package/dist/__tests__/run.test.d.ts.map +1 -0
  20. package/dist/__tests__/run.test.js +48 -0
  21. package/dist/__tests__/test-file-platform-filter.test.d.ts +2 -0
  22. package/dist/__tests__/test-file-platform-filter.test.d.ts.map +1 -0
  23. package/dist/__tests__/test-file-platform-filter.test.js +47 -0
  24. package/dist/crash-monitor.d.ts +12 -4
  25. package/dist/crash-monitor.d.ts.map +1 -1
  26. package/dist/crash-monitor.js +135 -102
  27. package/dist/diagnostics/__tests__/derivers.test.d.ts +2 -0
  28. package/dist/diagnostics/__tests__/derivers.test.d.ts.map +1 -0
  29. package/dist/diagnostics/__tests__/derivers.test.js +248 -0
  30. package/dist/diagnostics/__tests__/report.test.d.ts +2 -0
  31. package/dist/diagnostics/__tests__/report.test.d.ts.map +1 -0
  32. package/dist/diagnostics/__tests__/report.test.js +97 -0
  33. package/dist/diagnostics/derivers.d.ts +24 -0
  34. package/dist/diagnostics/derivers.d.ts.map +1 -0
  35. package/dist/diagnostics/derivers.js +146 -0
  36. package/dist/diagnostics/index.d.ts +3 -0
  37. package/dist/diagnostics/index.d.ts.map +1 -0
  38. package/dist/diagnostics/index.js +2 -0
  39. package/dist/diagnostics/report.d.ts +7 -0
  40. package/dist/diagnostics/report.d.ts.map +1 -0
  41. package/dist/diagnostics/report.js +84 -0
  42. package/dist/environment-reset.d.ts +51 -0
  43. package/dist/environment-reset.d.ts.map +1 -0
  44. package/dist/environment-reset.js +83 -0
  45. package/dist/errors.d.ts +10 -0
  46. package/dist/errors.d.ts.map +1 -1
  47. package/dist/errors.js +41 -2
  48. package/dist/execute-run.d.ts +4 -2
  49. package/dist/execute-run.d.ts.map +1 -1
  50. package/dist/execute-run.js +179 -49
  51. package/dist/format-harness-error.d.ts +5 -0
  52. package/dist/format-harness-error.d.ts.map +1 -0
  53. package/dist/format-harness-error.js +43 -0
  54. package/dist/harness-session.d.ts +46 -1
  55. package/dist/harness-session.d.ts.map +1 -1
  56. package/dist/harness-session.js +386 -118
  57. package/dist/index.d.ts +7 -3
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +24 -2
  60. package/dist/run.d.ts.map +1 -1
  61. package/dist/run.js +22 -4
  62. package/dist/test-file-platform-filter.d.ts +5 -0
  63. package/dist/test-file-platform-filter.d.ts.map +1 -0
  64. package/dist/test-file-platform-filter.js +40 -0
  65. package/dist/toTestResult.d.ts +1 -0
  66. package/dist/toTestResult.d.ts.map +1 -1
  67. package/dist/toTestResult.js +2 -2
  68. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  69. package/package.json +8 -7
  70. package/src/__tests__/bridge.test.ts +45 -5
  71. package/src/__tests__/crash-monitor.test.ts +234 -199
  72. package/src/__tests__/environment-reset.test.ts +308 -0
  73. package/src/__tests__/errors.test.ts +75 -0
  74. package/src/__tests__/execute-run.test.ts +561 -37
  75. package/src/__tests__/format-harness-error.test.ts +32 -0
  76. package/src/__tests__/harness-cache.test.ts +91 -0
  77. package/src/__tests__/harness-session.test.ts +303 -0
  78. package/src/__tests__/index.test.ts +92 -12
  79. package/src/__tests__/metro-port.test.ts +2 -0
  80. package/src/__tests__/run.test.ts +74 -0
  81. package/src/__tests__/test-file-platform-filter.test.ts +83 -0
  82. package/src/crash-monitor.ts +220 -131
  83. package/src/diagnostics/__tests__/derivers.test.ts +300 -0
  84. package/src/diagnostics/__tests__/report.test.ts +128 -0
  85. package/src/diagnostics/derivers.ts +170 -0
  86. package/src/diagnostics/index.ts +2 -0
  87. package/src/diagnostics/report.ts +124 -0
  88. package/src/environment-reset.ts +138 -0
  89. package/src/errors.ts +62 -1
  90. package/src/execute-run.ts +230 -57
  91. package/src/format-harness-error.ts +73 -0
  92. package/src/harness-session.ts +535 -162
  93. package/src/index.ts +40 -11
  94. package/src/run.ts +24 -4
  95. package/src/test-file-platform-filter.ts +57 -0
  96. package/src/toTestResult.ts +3 -2
  97. package/tsconfig.json +3 -0
  98. package/tsconfig.lib.json +3 -0
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { runHarnessTestFile } from '../run.js';
3
+ vi.mock('jest-message-util', () => ({
4
+ formatResultsErrors: vi.fn(() => ''),
5
+ }));
6
+ const createHarnessResult = () => ({
7
+ name: 'root',
8
+ tests: [],
9
+ suites: [],
10
+ status: 'passed',
11
+ duration: 0,
12
+ });
13
+ const createSession = (testTimeout) => ({
14
+ config: { testTimeout },
15
+ context: {
16
+ platform: {
17
+ runner: './runner.js',
18
+ },
19
+ },
20
+ runTestFile: vi.fn(async () => createHarnessResult()),
21
+ });
22
+ const createGlobalConfig = (overrides = {}) => ({
23
+ rootDir: '/project',
24
+ ...overrides,
25
+ });
26
+ const createProjectConfig = (overrides = {}) => overrides;
27
+ describe('runHarnessTestFile', () => {
28
+ it('uses Harness config testTimeout when Jest config does not set one', async () => {
29
+ const session = createSession(15000);
30
+ await runHarnessTestFile({
31
+ testPath: '/project/example.harness.ts',
32
+ session,
33
+ globalConfig: createGlobalConfig(),
34
+ projectConfig: createProjectConfig(),
35
+ });
36
+ expect(session.runTestFile).toHaveBeenCalledWith('example.harness.ts', expect.objectContaining({ testTimeout: 15000 }));
37
+ });
38
+ it('keeps Harness config testTimeout above Jest config testTimeout', async () => {
39
+ const session = createSession(15000);
40
+ await runHarnessTestFile({
41
+ testPath: '/project/example.harness.ts',
42
+ session,
43
+ globalConfig: createGlobalConfig({ testTimeout: 20000 }),
44
+ projectConfig: createProjectConfig({ testTimeout: 30000 }),
45
+ });
46
+ expect(session.runTestFile).toHaveBeenCalledWith('example.harness.ts', expect.objectContaining({ testTimeout: 15000 }));
47
+ });
48
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=test-file-platform-filter.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-file-platform-filter.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/test-file-platform-filter.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createPlatformSkippedTestResult, getHarnessTestFilePlatform, shouldRunHarnessTestFile, } from '../test-file-platform-filter.js';
3
+ const knownPlatformIds = new Set(['android', 'ios', 'web', 'vega']);
4
+ describe('getHarnessTestFilePlatform', () => {
5
+ it('returns null for shared harness test files', () => {
6
+ expect(getHarnessTestFilePlatform('/tests/smoke.harness.ts', knownPlatformIds)).toBeNull();
7
+ expect(getHarnessTestFilePlatform('/tests/smoke.harness.tsx', knownPlatformIds)).toBeNull();
8
+ expect(getHarnessTestFilePlatform('/tests/smoke.harness.mjs', knownPlatformIds)).toBeNull();
9
+ });
10
+ it('returns the platform id for platform-specific harness test files', () => {
11
+ expect(getHarnessTestFilePlatform('/tests/kotlin.android.harness.ts', knownPlatformIds)).toBe('android');
12
+ expect(getHarnessTestFilePlatform('/tests/swift.ios.harness.ts', knownPlatformIds)).toBe('ios');
13
+ expect(getHarnessTestFilePlatform('/tests/browser.web.harness.ts', knownPlatformIds)).toBe('web');
14
+ });
15
+ it('returns null when the segment before .harness is not a known platform id', () => {
16
+ expect(getHarnessTestFilePlatform('/tests/smoke.harness.ts', knownPlatformIds)).toBeNull();
17
+ expect(getHarnessTestFilePlatform('/tests/custom.foo.harness.ts', knownPlatformIds)).toBeNull();
18
+ });
19
+ });
20
+ describe('shouldRunHarnessTestFile', () => {
21
+ it('runs shared harness test files on every platform', () => {
22
+ expect(shouldRunHarnessTestFile('/tests/smoke.harness.ts', 'android', knownPlatformIds)).toBe(true);
23
+ expect(shouldRunHarnessTestFile('/tests/smoke.harness.ts', 'ios', knownPlatformIds)).toBe(true);
24
+ });
25
+ it('runs platform-specific files only on the matching platform', () => {
26
+ expect(shouldRunHarnessTestFile('/tests/kotlin.android.harness.ts', 'android', knownPlatformIds)).toBe(true);
27
+ expect(shouldRunHarnessTestFile('/tests/kotlin.android.harness.ts', 'ios', knownPlatformIds)).toBe(false);
28
+ expect(shouldRunHarnessTestFile('/tests/swift.ios.harness.ts', 'ios', knownPlatformIds)).toBe(true);
29
+ expect(shouldRunHarnessTestFile('/tests/swift.ios.harness.ts', 'android', knownPlatformIds)).toBe(false);
30
+ });
31
+ });
32
+ describe('createPlatformSkippedTestResult', () => {
33
+ it('marks the entire test file as skipped', () => {
34
+ const result = createPlatformSkippedTestResult('/tests/swift.ios.harness.ts');
35
+ expect(result.skipped).toBe(true);
36
+ expect(result.numPassingTests).toBe(0);
37
+ expect(result.numFailingTests).toBe(0);
38
+ expect(result.numPendingTests).toBe(1);
39
+ expect(result.testResults).toHaveLength(1);
40
+ expect(result.testResults[0]).toEqual(expect.objectContaining({
41
+ status: 'skipped',
42
+ title: 'swift.ios.harness.ts',
43
+ fullName: 'swift.ios.harness.ts',
44
+ }));
45
+ expect(result.testFilePath).toBe('/tests/swift.ios.harness.ts');
46
+ });
47
+ });
@@ -1,4 +1,4 @@
1
- import { type AppMonitor, type HarnessPlatformRunner } from '@react-native-harness/platforms';
1
+ import { type AppSession } from '@react-native-harness/platforms';
2
2
  import { type NativeCrashPhase } from './errors.js';
3
3
  export declare class CrashWatchCancelledError extends Error {
4
4
  constructor();
@@ -13,11 +13,19 @@ export type CrashMonitor = {
13
13
  stop: () => Promise<void>;
14
14
  start: () => Promise<void>;
15
15
  reset: () => void;
16
+ setAppSession: (session: AppSession | null) => void;
17
+ handleBridgeDisconnect: () => void;
18
+ /**
19
+ * Opens a window during which handleBridgeDisconnect() is suppressed,
20
+ * for use around operations (like a runtime reset) that intentionally
21
+ * disconnect the bridge without the app process dying. Returns a closer
22
+ * that ends the window; safe to call multiple times or nest.
23
+ */
24
+ expectDisconnect: () => () => void;
16
25
  dispose: () => Promise<void>;
17
26
  };
18
27
  export type CrashMonitorOptions = {
19
- appMonitor: AppMonitor;
20
- platformRunner: HarnessPlatformRunner;
28
+ appSession?: AppSession | null;
21
29
  };
22
- export declare const createCrashMonitor: ({ appMonitor, platformRunner, }: CrashMonitorOptions) => CrashMonitor;
30
+ export declare const createCrashMonitor: ({ appSession: initialAppSession, }?: CrashMonitorOptions) => CrashMonitor;
23
31
  //# sourceMappingURL=crash-monitor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"crash-monitor.d.ts","sourceRoot":"","sources":["../src/crash-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EAIf,KAAK,qBAAqB,EAC3B,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,aAAa,CAAC;AAKrB,qBAAa,wBAAyB,SAAQ,KAAK;;CAKlD;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACjC,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,KAAK,UAAU,CAAC;IACrE,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,qBAAqB,CAAC;CACvC,CAAC;AA6BF,eAAO,MAAM,kBAAkB,GAAI,iCAGhC,mBAAmB,KAAG,YA+KxB,CAAC"}
1
+ {"version":3,"file":"crash-monitor.d.ts","sourceRoot":"","sources":["../src/crash-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EAKhB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAKL,KAAK,gBAAgB,EAEtB,MAAM,aAAa,CAAC;AAOrB,qBAAa,wBAAyB,SAAQ,KAAK;;CAKlD;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACjC,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,KAAK,UAAU,CAAC;IACrE,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,aAAa,EAAE,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAC;IACpD,sBAAsB,EAAE,MAAM,IAAI,CAAC;IACnC;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,MAAM,IAAI,CAAC;IACnC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;CAChC,CAAC;AA0EF,eAAO,MAAM,kBAAkB,GAAI,qCAEhC,mBAAwB,KAAG,YA8M7B,CAAC"}
@@ -1,136 +1,144 @@
1
- import { NativeCrashError, } from './errors.js';
1
+ import { NativeCrashError, RuntimeDisconnectError, } from './errors.js';
2
2
  import { logger } from '@react-native-harness/tools';
3
3
  const crashLogger = logger.child('crash');
4
+ const CRASH_CLASSIFICATION_SETTLE_MS = 1500;
5
+ const CRASH_LOG_WINDOW_MS = 3000;
4
6
  export class CrashWatchCancelledError extends Error {
5
7
  constructor() {
6
8
  super('Crash watch was cancelled');
7
9
  this.name = 'CrashWatchCancelledError';
8
10
  }
9
11
  }
10
- const mergeCrashDetails = (phase, initial, enriched, fallbackSummary) => ({
12
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13
+ const isCrashIndicator = (line) => /uncaught exception|terminating app due to|fatal error|EXC_[A-Z_]+|termination reason|crash|abort/i.test(line) || /\bSIG[A-Z]{2,}\b/.test(line);
14
+ const getMatchingCrashLines = (logs, occurredAt) => logs
15
+ .filter((log) => Math.abs(log.occurredAt - occurredAt) <= CRASH_LOG_WINDOW_MS)
16
+ .map((log) => log.line)
17
+ .filter(isCrashIndicator);
18
+ const buildNativeCrashDetails = (phase, rawLines, summary) => ({
11
19
  phase,
12
- source: enriched?.source ?? initial?.source,
13
- summary: enriched?.summary ?? initial?.summary ?? fallbackSummary,
14
- signal: enriched?.signal ?? initial?.signal,
15
- exceptionType: enriched?.exceptionType ?? initial?.exceptionType,
16
- processName: enriched?.processName ?? initial?.processName,
17
- pid: enriched?.pid ?? initial?.pid,
18
- stackTrace: enriched?.stackTrace ?? initial?.stackTrace,
19
- rawLines: enriched?.rawLines ?? initial?.rawLines,
20
- artifactType: enriched?.artifactType ?? initial?.artifactType,
21
- artifactPath: enriched?.artifactPath ?? initial?.artifactPath,
20
+ source: rawLines.length > 0 ? 'logs' : 'bridge',
21
+ summary: rawLines.length > 0 ? rawLines.join('\n') : summary,
22
+ rawLines: rawLines.length > 0 ? rawLines : undefined,
22
23
  });
23
- export const createCrashMonitor = ({ appMonitor, platformRunner, }) => {
24
+ const getStatePid = (state) => {
25
+ if (state && 'pid' in state) {
26
+ return state.pid;
27
+ }
28
+ return undefined;
29
+ };
30
+ const mergeCrashDetails = (fallback, extracted) => {
31
+ if (!extracted) {
32
+ return fallback;
33
+ }
34
+ return {
35
+ ...fallback,
36
+ ...extracted,
37
+ phase: fallback.phase,
38
+ source: extracted.source ?? fallback.source,
39
+ summary: extracted.summary ?? fallback.summary,
40
+ rawLines: extracted.rawLines ?? fallback.rawLines,
41
+ };
42
+ };
43
+ const buildRuntimeDisconnectDetails = (phase, rawLines) => ({
44
+ phase,
45
+ source: 'bridge',
46
+ summary: 'The runtime bridge disconnected, but the app session still appears to be running.',
47
+ rawLines: rawLines.length > 0 ? rawLines : undefined,
48
+ });
49
+ export const createCrashMonitor = ({ appSession: initialAppSession = null, } = {}) => {
24
50
  let alive = false;
25
51
  let monitoring = true;
26
52
  let isResolvingCrash = false;
27
53
  let disposed = false;
28
- // Both updated when watch() is called so crashes are attributed to the
29
- // correct test file and lifecycle phase.
54
+ let appSession = null;
55
+ let pendingTimer = null;
30
56
  let currentTestFilePath = '';
31
57
  let currentPhase = 'startup';
58
+ let expectedDisconnectDepth = 0;
32
59
  const watchers = new Set();
33
- const getCrashDetailsProvider = () => {
34
- if ('getCrashDetails' in appMonitor) {
35
- return appMonitor;
36
- }
37
- if (platformRunner.getCrashDetails) {
38
- return platformRunner;
39
- }
40
- return null;
41
- };
42
- const notifyCrash = (err) => {
60
+ const notifyFailure = (err) => {
43
61
  const pending = [...watchers];
44
62
  watchers.clear();
45
63
  for (const fn of pending)
46
64
  fn(err);
47
65
  };
48
- const handleCrash = async (phase, details, fallbackSummary) => {
49
- if (isResolvingCrash)
66
+ const clearPendingTimer = () => {
67
+ if (pendingTimer) {
68
+ clearTimeout(pendingTimer);
69
+ pendingTimer = null;
70
+ }
71
+ };
72
+ const classify = async (pending, trigger) => {
73
+ if (disposed || !monitoring || isResolvingCrash) {
50
74
  return;
51
- isResolvingCrash = true;
52
- alive = false;
53
- crashLogger.debug('native crash detected (phase=%s)', phase);
54
- for (const line of details?.rawLines ?? []) {
55
- crashLogger.debug('%s', line);
56
75
  }
76
+ isResolvingCrash = true;
57
77
  try {
58
- const enriched = await getCrashDetailsProvider()?.getCrashDetails?.({
59
- processName: details?.processName,
60
- pid: details?.pid,
61
- occurredAt: Date.now(),
62
- });
63
- const merged = mergeCrashDetails(phase, details, enriched, fallbackSummary);
64
- crashLogger.debug('crash details: %o', {
65
- phase: merged.phase,
66
- source: merged.source,
67
- summary: merged.summary,
68
- signal: merged.signal,
69
- exceptionType: merged.exceptionType,
70
- processName: merged.processName,
71
- pid: merged.pid,
72
- });
73
- notifyCrash(new NativeCrashError(currentTestFilePath, merged));
78
+ const session = appSession;
79
+ const state = await session?.getState();
80
+ const logs = session?.getLogs() ?? [];
81
+ const rawLines = getMatchingCrashLines(logs, pending.occurredAt);
82
+ if (state?.status === 'running' && trigger === 'bridge-disconnect') {
83
+ crashLogger.debug('runtime bridge disconnected without confirmed app death');
84
+ notifyFailure(new RuntimeDisconnectError(pending.testFilePath, buildRuntimeDisconnectDetails(pending.phase, rawLines)));
85
+ return;
86
+ }
87
+ alive = false;
88
+ crashLogger.debug('native crash detected (phase=%s)', pending.phase);
89
+ for (const line of rawLines) {
90
+ crashLogger.debug('%s', line);
91
+ }
92
+ const fallbackSummary = trigger === 'bridge-disconnect'
93
+ ? 'The app process exited after the runtime bridge disconnected, but no crash log lines were found.'
94
+ : 'The app process exited, but no crash log lines were found.';
95
+ const details = buildNativeCrashDetails(pending.phase, rawLines, fallbackSummary);
96
+ const extractedDetails = session?.getCrashDetails
97
+ ? await session
98
+ .getCrashDetails({
99
+ occurredAt: pending.occurredAt,
100
+ pid: getStatePid(state),
101
+ processName: details.processName,
102
+ testFilePath: pending.testFilePath,
103
+ })
104
+ .catch((error) => {
105
+ crashLogger.warn('failed to extract native crash details: %s', error);
106
+ return null;
107
+ })
108
+ : null;
109
+ notifyFailure(new NativeCrashError(pending.testFilePath, mergeCrashDetails(details, extractedDetails)));
74
110
  }
75
111
  finally {
76
112
  isResolvingCrash = false;
113
+ pendingTimer = null;
77
114
  }
78
115
  };
79
- const confirmAndHandleCrash = async (phase, details, fallbackSummary) => {
80
- if (disposed || !monitoring)
116
+ const startCrashResolution = (trigger) => {
117
+ if (disposed || !monitoring || isResolvingCrash) {
81
118
  return;
82
- try {
83
- const isRunning = await platformRunner.isAppRunning();
84
- if (!isRunning) {
85
- void handleCrash(phase, details, fallbackSummary);
86
- }
87
- }
88
- catch (error) {
89
- crashLogger.debug('crash confirmation failed', error);
90
119
  }
120
+ clearPendingTimer();
121
+ const pending = {
122
+ testFilePath: currentTestFilePath,
123
+ phase: currentPhase,
124
+ occurredAt: Date.now(),
125
+ };
126
+ pendingTimer = setTimeout(() => {
127
+ void classify(pending, trigger);
128
+ }, trigger === 'bridge-disconnect' ? CRASH_CLASSIFICATION_SETTLE_MS : 0);
91
129
  };
92
- const extractCrashDetails = (event) => event.crashDetails
93
- ? {
94
- source: event.crashDetails.source ?? event.source,
95
- summary: event.crashDetails.summary,
96
- signal: event.crashDetails.signal,
97
- exceptionType: event.crashDetails.exceptionType,
98
- processName: event.crashDetails.processName,
99
- pid: event.crashDetails.pid ?? event.pid,
100
- stackTrace: event.crashDetails.stackTrace,
101
- rawLines: event.crashDetails.rawLines ??
102
- (event.line ? [event.line] : undefined),
103
- }
104
- : undefined;
105
- const appMonitorListener = (event) => {
106
- if (disposed || !monitoring)
107
- return;
108
- if (event.type === 'app_started') {
109
- alive = true;
110
- return;
111
- }
130
+ const appSessionListener = (event) => {
112
131
  if (event.type === 'app_exited') {
113
- const details = extractCrashDetails(event);
114
- if (event.isConfirmed ?? event.source === 'polling') {
115
- void handleCrash(currentPhase, details);
116
- }
117
- else {
118
- void confirmAndHandleCrash(currentPhase, details);
119
- }
120
- return;
121
- }
122
- if (event.type === 'possible_crash') {
123
- const details = extractCrashDetails(event);
124
- const fallback = `possible crash signal (${event.source ?? 'unknown'})`;
125
- if (event.isConfirmed) {
126
- void handleCrash(currentPhase, details, fallback);
127
- }
128
- else {
129
- void confirmAndHandleCrash(currentPhase, details, fallback);
130
- }
132
+ startCrashResolution('app-exit');
131
133
  }
132
134
  };
133
- appMonitor.addListener(appMonitorListener);
135
+ const setAppSession = (session) => {
136
+ appSession?.removeListener(appSessionListener);
137
+ appSession = session;
138
+ alive = Boolean(session);
139
+ session?.addListener(appSessionListener);
140
+ };
141
+ setAppSession(initialAppSession);
134
142
  const watch = (testFilePath, phase) => {
135
143
  currentTestFilePath = testFilePath;
136
144
  currentPhase = phase;
@@ -147,30 +155,55 @@ export const createCrashMonitor = ({ appMonitor, platformRunner, }) => {
147
155
  };
148
156
  return { promise, cancel };
149
157
  };
158
+ const expectDisconnect = () => {
159
+ expectedDisconnectDepth += 1;
160
+ let closed = false;
161
+ return () => {
162
+ if (closed) {
163
+ return;
164
+ }
165
+ closed = true;
166
+ expectedDisconnectDepth = Math.max(0, expectedDisconnectDepth - 1);
167
+ };
168
+ };
150
169
  return {
151
170
  watch,
152
171
  isAlive: () => alive,
153
172
  stop: async () => {
154
173
  monitoring = false;
155
- await appMonitor.stop();
174
+ clearPendingTimer();
175
+ await sleep(0);
156
176
  },
157
177
  start: async () => {
158
178
  monitoring = true;
159
- await appMonitor.start();
160
179
  },
161
180
  reset: () => {
162
- alive = false;
181
+ alive = Boolean(appSession);
163
182
  watchers.clear();
164
183
  isResolvingCrash = false;
165
184
  currentTestFilePath = '';
185
+ expectedDisconnectDepth = 0;
186
+ clearPendingTimer();
166
187
  },
188
+ setAppSession,
189
+ handleBridgeDisconnect: () => {
190
+ if (expectedDisconnectDepth > 0) {
191
+ crashLogger.debug('ignoring expected bridge disconnect');
192
+ return;
193
+ }
194
+ startCrashResolution('bridge-disconnect');
195
+ },
196
+ expectDisconnect,
167
197
  dispose: async () => {
168
198
  disposed = true;
169
199
  monitoring = false;
200
+ expectedDisconnectDepth = 0;
170
201
  watchers.clear();
171
202
  isResolvingCrash = false;
172
- appMonitor.removeListener(appMonitorListener);
173
- await appMonitor.dispose();
203
+ clearPendingTimer();
204
+ appSession?.removeListener(appSessionListener);
205
+ appSession = null;
206
+ alive = false;
174
207
  },
175
208
  };
176
209
  };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=derivers.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derivers.test.d.ts","sourceRoot":"","sources":["../../../src/diagnostics/__tests__/derivers.test.ts"],"names":[],"mappings":""}