@react-native-harness/jest 1.1.0 → 1.2.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 (93) hide show
  1. package/dist/__tests__/bridge.test.d.ts +2 -0
  2. package/dist/__tests__/bridge.test.d.ts.map +1 -0
  3. package/dist/__tests__/bridge.test.js +142 -0
  4. package/dist/__tests__/client-log-handler.test.js +116 -72
  5. package/dist/__tests__/crash-monitor.test.d.ts +2 -0
  6. package/dist/__tests__/crash-monitor.test.d.ts.map +1 -0
  7. package/dist/__tests__/crash-monitor.test.js +212 -0
  8. package/dist/__tests__/execute-run.test.d.ts +2 -0
  9. package/dist/__tests__/execute-run.test.d.ts.map +1 -0
  10. package/dist/__tests__/execute-run.test.js +229 -0
  11. package/dist/__tests__/harness.test.js +35 -0
  12. package/dist/__tests__/hook-queue.test.d.ts +2 -0
  13. package/dist/__tests__/hook-queue.test.d.ts.map +1 -0
  14. package/dist/__tests__/hook-queue.test.js +56 -0
  15. package/dist/__tests__/index.test.js +94 -37
  16. package/dist/__tests__/resource-lock.test.js +24 -24
  17. package/dist/cli-args.test.d.ts +2 -0
  18. package/dist/cli-args.test.d.ts.map +1 -0
  19. package/dist/cli-args.test.js +1 -0
  20. package/dist/client-log-handler.d.ts +10 -11
  21. package/dist/client-log-handler.d.ts.map +1 -1
  22. package/dist/client-log-handler.js +36 -58
  23. package/dist/crash-monitor.d.ts +23 -0
  24. package/dist/crash-monitor.d.ts.map +1 -0
  25. package/dist/crash-monitor.js +176 -0
  26. package/dist/event-mapper.d.ts +16 -0
  27. package/dist/event-mapper.d.ts.map +1 -0
  28. package/dist/event-mapper.js +120 -0
  29. package/dist/execute-run.d.ts +4 -0
  30. package/dist/execute-run.d.ts.map +1 -0
  31. package/dist/execute-run.js +151 -0
  32. package/dist/global-setup.d.ts +3 -0
  33. package/dist/global-setup.d.ts.map +1 -0
  34. package/dist/global-setup.js +60 -0
  35. package/dist/global-teardown.d.ts +3 -0
  36. package/dist/global-teardown.d.ts.map +1 -0
  37. package/dist/global-teardown.js +11 -0
  38. package/dist/harness-reporter.d.ts +8 -0
  39. package/dist/harness-reporter.d.ts.map +1 -0
  40. package/dist/harness-reporter.js +12 -0
  41. package/dist/harness-session.d.ts +34 -0
  42. package/dist/harness-session.d.ts.map +1 -0
  43. package/dist/harness-session.js +523 -0
  44. package/dist/harness.d.ts.map +1 -1
  45. package/dist/harness.js +6 -1
  46. package/dist/hook-queue.d.ts +6 -0
  47. package/dist/hook-queue.d.ts.map +1 -0
  48. package/dist/hook-queue.js +18 -0
  49. package/dist/index.d.ts +0 -3
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +12 -230
  52. package/dist/jest-options.d.ts +2 -0
  53. package/dist/jest-options.d.ts.map +1 -0
  54. package/dist/jest-options.js +1 -0
  55. package/dist/logs.d.ts +1 -0
  56. package/dist/logs.d.ts.map +1 -1
  57. package/dist/logs.js +3 -0
  58. package/dist/resource-lock.d.ts +0 -1
  59. package/dist/resource-lock.d.ts.map +1 -1
  60. package/dist/resource-lock.js +1 -1
  61. package/dist/run.d.ts +2 -2
  62. package/dist/run.d.ts.map +1 -1
  63. package/dist/run.js +3 -3
  64. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  65. package/dist/types.d.ts +17 -0
  66. package/dist/types.d.ts.map +1 -0
  67. package/dist/types.js +1 -0
  68. package/package.json +7 -8
  69. package/src/__tests__/bridge.test.ts +184 -0
  70. package/src/__tests__/client-log-handler.test.ts +117 -81
  71. package/src/__tests__/crash-monitor.test.ts +288 -0
  72. package/src/__tests__/execute-run.test.ts +325 -0
  73. package/src/__tests__/hook-queue.test.ts +77 -0
  74. package/src/__tests__/index.test.ts +135 -55
  75. package/src/__tests__/resource-lock.test.ts +29 -42
  76. package/src/client-log-handler.ts +48 -64
  77. package/src/crash-monitor.ts +248 -0
  78. package/src/execute-run.ts +199 -0
  79. package/src/harness-session.ts +734 -0
  80. package/src/hook-queue.ts +26 -0
  81. package/src/index.ts +17 -330
  82. package/src/logs.ts +4 -0
  83. package/src/resource-lock.ts +1 -1
  84. package/src/run.ts +5 -5
  85. package/tsconfig.tsbuildinfo +1 -0
  86. package/src/__tests__/crash-supervisor.test.ts +0 -200
  87. package/src/__tests__/harness-cache.test.ts +0 -74
  88. package/src/__tests__/harness.test.ts +0 -870
  89. package/src/crash-supervisor.ts +0 -321
  90. package/src/env.d.ts +0 -9
  91. package/src/harness.ts +0 -864
  92. package/src/setup.ts +0 -127
  93. package/src/teardown.ts +0 -14
@@ -1,42 +1,99 @@
1
- import { describe, expect, it, vi } from 'vitest';
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ import { HarnessError } from '@react-native-harness/tools';
2
3
  import JestHarness from '../index.js';
3
- import { StartupStallError } from '../errors.js';
4
+ const resolveUndefined = async () => undefined;
5
+ // ---------------------------------------------------------------------------
6
+ // Module mocks
7
+ // ---------------------------------------------------------------------------
8
+ const mockSession = {
9
+ config: { metroPort: 8081 },
10
+ context: {},
11
+ ensureAppReady: vi.fn(resolveUndefined),
12
+ runTestFile: vi.fn(async () => ({
13
+ name: '',
14
+ tests: [],
15
+ suites: [],
16
+ status: 'passed',
17
+ duration: 0,
18
+ })),
19
+ restartApp: vi.fn(resolveUndefined),
20
+ resetCrashState: vi.fn(),
21
+ flushClientLogs: vi.fn(() => []),
22
+ callHook: vi.fn(resolveUndefined),
23
+ setRunState: vi.fn(),
24
+ dispose: vi.fn(resolveUndefined),
25
+ };
26
+ const mockCreateHarnessSession = vi.hoisted(() => vi.fn(async () => mockSession));
27
+ const mockExecuteRun = vi.hoisted(() => vi.fn(async () => undefined));
28
+ vi.mock('../harness-session.js', () => ({
29
+ createHarnessSession: mockCreateHarnessSession,
30
+ }));
31
+ vi.mock('../execute-run.js', () => ({
32
+ executeRun: mockExecuteRun,
33
+ }));
34
+ // ---------------------------------------------------------------------------
35
+ // Helpers
36
+ // ---------------------------------------------------------------------------
37
+ const makeOptions = () => ({ serial: true });
38
+ const makeWatcher = () => ({ isInterrupted: () => false });
39
+ const makeTest = () => ({
40
+ path: '/test/example.ts',
41
+ context: { config: {} },
42
+ });
43
+ const makeGlobalConfig = (overrides = {}) => ({ rootDir: '/project', watch: false, watchAll: false, collectCoverage: false, ...overrides });
44
+ // ---------------------------------------------------------------------------
45
+ // Tests
46
+ // ---------------------------------------------------------------------------
47
+ beforeEach(() => {
48
+ vi.clearAllMocks();
49
+ mockSession.dispose.mockResolvedValue(undefined);
50
+ mockExecuteRun.mockResolvedValue(undefined);
51
+ mockCreateHarnessSession.mockResolvedValue(mockSession);
52
+ });
4
53
  describe('JestHarness', () => {
5
- it('reports StartupStallError without a stack trace', async () => {
6
- const runner = new JestHarness({});
7
- const onFailure = vi.fn();
8
- const harness = {
9
- ensureAppReady: vi
10
- .fn()
11
- .mockRejectedValue(new StartupStallError(1500, 3)),
12
- callHook: vi.fn(async () => undefined),
13
- setRunState: vi.fn(),
14
- getRunState: vi.fn(() => null),
15
- crashSupervisor: {
16
- beginTestRun: vi.fn(),
17
- waitForCrash: vi.fn(),
18
- cancelCrashWaiters: vi.fn(),
19
- reset: vi.fn(),
20
- },
21
- };
22
- await runner._createInBandTestRun([
23
- {
24
- path: '/tmp/example.harness.ts',
25
- context: {
26
- config: {},
27
- },
28
- },
29
- ], {
30
- isInterrupted: () => false,
31
- }, harness, {
32
- detectNativeCrashes: true,
33
- resetEnvironmentBetweenTestFiles: false,
34
- }, () => Promise.resolve(), () => Promise.resolve(), onFailure);
35
- expect(onFailure).toHaveBeenCalledWith(expect.objectContaining({
36
- path: '/tmp/example.harness.ts',
37
- }), {
38
- message: 'The app did not request its Metro bundle after 3 launch attempts within 1500ms. Last Metro status: unknown.',
39
- stack: '',
54
+ describe('session lifecycle', () => {
55
+ it('creates a session on the first run', async () => {
56
+ const runner = new JestHarness(makeGlobalConfig());
57
+ await runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions());
58
+ expect(mockCreateHarnessSession).toHaveBeenCalledOnce();
59
+ });
60
+ it('reuses the session across runs in watch mode', async () => {
61
+ const runner = new JestHarness(makeGlobalConfig({ watch: true }));
62
+ await runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions());
63
+ await runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions());
64
+ expect(mockCreateHarnessSession).toHaveBeenCalledOnce();
65
+ expect(mockSession.dispose).not.toHaveBeenCalled();
66
+ });
67
+ it('disposes the session after each run in normal mode', async () => {
68
+ const runner = new JestHarness(makeGlobalConfig());
69
+ await runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions());
70
+ expect(mockSession.dispose).toHaveBeenCalledOnce();
71
+ });
72
+ it('creates a fresh session for each run in normal mode', async () => {
73
+ const runner = new JestHarness(makeGlobalConfig());
74
+ await runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions());
75
+ await runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions());
76
+ expect(mockCreateHarnessSession).toHaveBeenCalledTimes(2);
77
+ });
78
+ });
79
+ describe('error handling', () => {
80
+ it('converts HarnessError into a formatted string before throwing', async () => {
81
+ class TestHarnessError extends HarnessError {
82
+ constructor() { super('something went wrong'); this.name = 'TestHarnessError'; }
83
+ }
84
+ mockCreateHarnessSession.mockRejectedValue(new TestHarnessError());
85
+ const runner = new JestHarness(makeGlobalConfig());
86
+ await expect(runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions())).rejects.toBeTypeOf('string');
87
+ });
88
+ it('propagates non-HarnessError exceptions as-is', async () => {
89
+ const cause = new TypeError('unexpected');
90
+ mockCreateHarnessSession.mockRejectedValue(cause);
91
+ const runner = new JestHarness(makeGlobalConfig());
92
+ await expect(runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions())).rejects.toBe(cause);
93
+ });
94
+ it('throws when called without serial flag', async () => {
95
+ const runner = new JestHarness(makeGlobalConfig());
96
+ await expect(runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), { serial: false })).rejects.toThrow('Parallel test running is not supported');
40
97
  });
41
98
  });
42
99
  });
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
- import { createResourceLockManager, hashResourceLockKey, } from '../resource-lock.js';
5
+ import { createResourceLockManager } from '../resource-lock.js';
6
6
  describe('resource lock manager', () => {
7
7
  let rootDir;
8
8
  beforeEach(async () => {
@@ -54,10 +54,11 @@ describe('resource lock manager', () => {
54
54
  await expect(acquirePromise).rejects.toMatchObject({
55
55
  name: 'AbortError',
56
56
  });
57
- const queueDir = path.join(rootDir, hashResourceLockKey(key), 'queue');
58
- const queuedEntries = await fs.readdir(queueDir);
59
- expect(queuedEntries).toHaveLength(0);
57
+ // The aborted waiter must have cleaned up its ticket. Verify by releasing
58
+ // the first lock and confirming a fresh acquire completes immediately.
60
59
  await firstLease.release();
60
+ const cleanupLease = await manager.acquire(key);
61
+ await cleanupLease.release();
61
62
  });
62
63
  it('keeps queued tickets alive while the waiting process is still active', async () => {
63
64
  const manager = createResourceLockManager({
@@ -76,29 +77,27 @@ describe('resource lock manager', () => {
76
77
  await secondLease.release();
77
78
  });
78
79
  it('reclaims a stale owner before granting the lock', async () => {
79
- const manager = createResourceLockManager({
80
+ const key = 'web:browser:chromium';
81
+ // Simulate a live process holding the lock.
82
+ const manager1 = createResourceLockManager({
83
+ rootDir,
84
+ pollIntervalMs: 5,
85
+ heartbeatIntervalMs: 20,
86
+ });
87
+ const staleLease = await manager1.acquire(key);
88
+ // A second manager whose isProcessActive always returns false will consider
89
+ // any owner — including the live one above — immediately stale and reclaim it.
90
+ const manager2 = createResourceLockManager({
80
91
  rootDir,
81
92
  pollIntervalMs: 5,
82
93
  heartbeatIntervalMs: 20,
83
94
  staleLockTimeoutMs: 50,
84
95
  isProcessActive: () => false,
85
96
  });
86
- const key = 'web:browser:chromium';
87
- const keyDir = path.join(rootDir, hashResourceLockKey(key));
88
- const queueDir = path.join(keyDir, 'queue');
89
- const ownerFilePath = path.join(keyDir, 'owner.json');
90
- await fs.mkdir(queueDir, { recursive: true });
91
- await fs.writeFile(ownerFilePath, JSON.stringify({
92
- ticketId: 'stale-owner',
93
- key,
94
- pid: 999999,
95
- createdAt: Date.now() - 1000,
96
- heartbeatAt: Date.now() - 1000,
97
- }), 'utf8');
98
- const lease = await manager.acquire(key);
99
- const owner = JSON.parse(await fs.readFile(ownerFilePath, 'utf8'));
100
- expect(owner.ticketId).not.toBe('stale-owner');
97
+ const lease = await manager2.acquire(key);
101
98
  await lease.release();
99
+ // manager1 was evicted; its release is best-effort.
100
+ await staleLease.release();
102
101
  });
103
102
  it('keeps owner metadata valid when heartbeat writes overlap', async () => {
104
103
  const manager = createResourceLockManager({
@@ -108,20 +107,21 @@ describe('resource lock manager', () => {
108
107
  staleLockTimeoutMs: 200,
109
108
  });
110
109
  const key = 'ios:simulator:iPhone 17 Pro:26.2';
111
- const ownerFilePath = path.join(rootDir, hashResourceLockKey(key), 'owner.json');
112
110
  const actualWriteFile = fs.writeFile.bind(fs);
113
111
  const writeFileSpy = vi
114
112
  .spyOn(fs, 'writeFile')
115
113
  .mockImplementation(async (file, data, options) => {
116
- if (typeof file === 'string' &&
117
- file.startsWith(ownerFilePath) &&
118
- file !== ownerFilePath) {
114
+ // Delay atomic temp-file writes to simulate overlapping heartbeat flushes.
115
+ if (typeof file === 'string' && file.startsWith(rootDir) && file.endsWith('.tmp')) {
119
116
  await new Promise((resolve) => setTimeout(resolve, 25));
120
117
  }
121
118
  return await actualWriteFile(file, data, options);
122
119
  });
123
120
  try {
124
121
  const lease = await manager.acquire(key);
122
+ // Discover the owner file after acquire creates the key directory.
123
+ const [keyDirName] = await fs.readdir(rootDir);
124
+ const ownerFilePath = path.join(rootDir, keyDirName, 'owner.json');
125
125
  const initialOwner = JSON.parse(await fs.readFile(ownerFilePath, 'utf8'));
126
126
  await new Promise((resolve) => setTimeout(resolve, 80));
127
127
  for (let index = 0; index < 5; index += 1) {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli-args.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-args.test.d.ts","sourceRoot":"","sources":["../src/cli-args.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ export {};
@@ -1,23 +1,22 @@
1
1
  import type { ReportableEvent } from '@react-native-harness/bundler-metro';
2
+ import type { TestResult as JestTestResult } from '@jest/test-result';
2
3
  export type ClientLogEvent = Extract<ReportableEvent, {
3
4
  type: 'client_log';
4
5
  }>;
6
+ export type ClientLogBuffer = NonNullable<JestTestResult['console']>;
7
+ export type ClientLogEntry = ClientLogBuffer[number];
8
+ export type ClientLogCollector = {
9
+ handleEvent: (event: ReportableEvent) => void;
10
+ flush: () => ClientLogBuffer;
11
+ };
5
12
  /**
6
13
  * Formats a client log event data array into a string message.
7
14
  * Uses util.format for printf-style format specifier support (%s, %d, %j, etc.)
8
15
  */
9
16
  export declare const formatClientLogMessage: (data: unknown[]) => string;
10
17
  /**
11
- * Formats a client log event into a log line with styled level prefix.
18
+ * Formats a client log event into Jest's buffered console entry format.
12
19
  */
13
- export declare const formatClientLogLine: (event: ClientLogEvent) => string;
14
- /**
15
- * Handles a client_log event by formatting and logging it.
16
- * Returns true if the event was handled, false otherwise.
17
- */
18
- export declare const handleClientLogEvent: (event: ReportableEvent) => boolean;
19
- /**
20
- * Creates a client log event listener that can be added to the Metro reporter.
21
- */
22
- export declare const createClientLogListener: () => ((event: ReportableEvent) => void);
20
+ export declare const formatClientLogEntry: (event: ClientLogEvent) => ClientLogEntry | null;
21
+ export declare const createClientLogCollector: () => ClientLogCollector;
23
22
  //# sourceMappingURL=client-log-handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client-log-handler.d.ts","sourceRoot":"","sources":["../src/client-log-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAK3E,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,eAAe,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;AA6C9E;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GAAI,MAAM,OAAO,EAAE,KAAG,MAKxD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAO,cAAc,KAAG,MAI3D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAO,eAAe,KAAG,OAiB7D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,uBAAuB,QAAO,CAAC,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAI3E,CAAC"}
1
+ {"version":3,"file":"client-log-handler.d.ts","sourceRoot":"","sources":["../src/client-log-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAC3E,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGtE,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,eAAe,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;AAC9E,MAAM,MAAM,eAAe,GAAG,WAAW,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAAC;AACrE,MAAM,MAAM,cAAc,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;AACrD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,WAAW,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC9C,KAAK,EAAE,MAAM,eAAe,CAAC;CAC9B,CAAC;AAuBF;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GAAI,MAAM,OAAO,EAAE,KAAG,MAMxD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAC/B,OAAO,cAAc,KACpB,cAAc,GAAG,IAYnB,CAAC;AAEF,eAAO,MAAM,wBAAwB,QAAO,kBAoB3C,CAAC"}
@@ -1,41 +1,19 @@
1
- import chalk from 'chalk';
2
1
  import util from 'node:util';
3
- import { log } from './logs.js';
4
2
  /**
5
- * Gets the display level for a log level.
3
+ * Gets the Jest console level for a client log level.
6
4
  * Note: Metro treats 'trace' as 'log' because Hermes doesn't include stack traces.
7
5
  */
8
- const getDisplayLevel = (level) => {
9
- // Metro converts trace to log for display (Hermes doesn't provide stack traces)
10
- if (level === 'trace') {
11
- return 'LOG';
12
- }
13
- return level.toUpperCase();
14
- };
15
- /**
16
- * Creates a styled tag for a log level with colored box appearance.
17
- */
18
- const createLevelTag = (level) => {
19
- const displayLevel = getDisplayLevel(level);
20
- const label = ` ${displayLevel} `;
21
- if (!chalk.supportsColor) {
22
- return `[${displayLevel}]`;
23
- }
6
+ const getJestLogLevel = (level) => {
24
7
  switch (level) {
25
- case 'error':
26
- return chalk.reset.inverse.bold.red(label);
27
- case 'warn':
28
- return chalk.reset.inverse.bold.yellow(label);
29
- case 'info':
30
- return chalk.reset.inverse.bold.cyan(label);
31
- case 'debug':
32
- return chalk.reset.inverse.bold.blue(label);
8
+ case 'group':
9
+ case 'groupCollapsed':
10
+ case 'groupEnd':
11
+ return null;
33
12
  case 'trace':
34
- // Trace displays as LOG but with a distinct color
35
- return chalk.reset.inverse.bold.magenta(label);
13
+ return 'log';
36
14
  case 'log':
37
15
  default:
38
- return chalk.reset.inverse.bold.white(label);
16
+ return level;
39
17
  }
40
18
  };
41
19
  /**
@@ -49,36 +27,36 @@ export const formatClientLogMessage = (data) => {
49
27
  return util.format(...data);
50
28
  };
51
29
  /**
52
- * Formats a client log event into a log line with styled level prefix.
53
- */
54
- export const formatClientLogLine = (event) => {
55
- const tag = createLevelTag(event.level);
56
- const message = formatClientLogMessage(event.data);
57
- return `${tag} ${message}`;
58
- };
59
- /**
60
- * Handles a client_log event by formatting and logging it.
61
- * Returns true if the event was handled, false otherwise.
30
+ * Formats a client log event into Jest's buffered console entry format.
62
31
  */
63
- export const handleClientLogEvent = (event) => {
64
- if (event.type !== 'client_log') {
65
- return false;
32
+ export const formatClientLogEntry = (event) => {
33
+ const logLevel = getJestLogLevel(event.level);
34
+ if (!logLevel) {
35
+ return null;
66
36
  }
67
- // Skip group events - they don't produce output
68
- if (event.level === 'group' ||
69
- event.level === 'groupCollapsed' ||
70
- event.level === 'groupEnd') {
71
- return true;
72
- }
73
- const logLine = formatClientLogLine(event);
74
- log(logLine);
75
- return true;
37
+ const message = formatClientLogMessage(event.data);
38
+ return {
39
+ message,
40
+ origin: '',
41
+ type: logLevel,
42
+ };
76
43
  };
77
- /**
78
- * Creates a client log event listener that can be added to the Metro reporter.
79
- */
80
- export const createClientLogListener = () => {
81
- return (event) => {
82
- handleClientLogEvent(event);
44
+ export const createClientLogCollector = () => {
45
+ let buffer = [];
46
+ return {
47
+ handleEvent: (event) => {
48
+ if (event.type !== 'client_log') {
49
+ return;
50
+ }
51
+ const entry = formatClientLogEntry(event);
52
+ if (entry) {
53
+ buffer.push(entry);
54
+ }
55
+ },
56
+ flush: () => {
57
+ const flushed = buffer;
58
+ buffer = [];
59
+ return flushed;
60
+ },
83
61
  };
84
62
  };
@@ -0,0 +1,23 @@
1
+ import { type AppMonitor, type HarnessPlatformRunner } from '@react-native-harness/platforms';
2
+ import { type NativeCrashPhase } from './errors.js';
3
+ export declare class CrashWatchCancelledError extends Error {
4
+ constructor();
5
+ }
6
+ export type CrashWatch = {
7
+ readonly promise: Promise<never>;
8
+ cancel: () => void;
9
+ };
10
+ export type CrashMonitor = {
11
+ watch: (testFilePath: string, phase: NativeCrashPhase) => CrashWatch;
12
+ isAlive: () => boolean;
13
+ stop: () => Promise<void>;
14
+ start: () => Promise<void>;
15
+ reset: () => void;
16
+ dispose: () => Promise<void>;
17
+ };
18
+ export type CrashMonitorOptions = {
19
+ appMonitor: AppMonitor;
20
+ platformRunner: HarnessPlatformRunner;
21
+ };
22
+ export declare const createCrashMonitor: ({ appMonitor, platformRunner, }: CrashMonitorOptions) => CrashMonitor;
23
+ //# sourceMappingURL=crash-monitor.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,176 @@
1
+ import { NativeCrashError, } from './errors.js';
2
+ import { logger } from '@react-native-harness/tools';
3
+ const crashLogger = logger.child('crash');
4
+ export class CrashWatchCancelledError extends Error {
5
+ constructor() {
6
+ super('Crash watch was cancelled');
7
+ this.name = 'CrashWatchCancelledError';
8
+ }
9
+ }
10
+ const mergeCrashDetails = (phase, initial, enriched, fallbackSummary) => ({
11
+ 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,
22
+ });
23
+ export const createCrashMonitor = ({ appMonitor, platformRunner, }) => {
24
+ let alive = false;
25
+ let monitoring = true;
26
+ let isResolvingCrash = false;
27
+ let disposed = false;
28
+ // Both updated when watch() is called so crashes are attributed to the
29
+ // correct test file and lifecycle phase.
30
+ let currentTestFilePath = '';
31
+ let currentPhase = 'startup';
32
+ 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) => {
43
+ const pending = [...watchers];
44
+ watchers.clear();
45
+ for (const fn of pending)
46
+ fn(err);
47
+ };
48
+ const handleCrash = async (phase, details, fallbackSummary) => {
49
+ if (isResolvingCrash)
50
+ 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
+ }
57
+ 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));
74
+ }
75
+ finally {
76
+ isResolvingCrash = false;
77
+ }
78
+ };
79
+ const confirmAndHandleCrash = async (phase, details, fallbackSummary) => {
80
+ if (disposed || !monitoring)
81
+ 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
+ }
91
+ };
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
+ }
112
+ 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
+ }
131
+ }
132
+ };
133
+ appMonitor.addListener(appMonitorListener);
134
+ const watch = (testFilePath, phase) => {
135
+ currentTestFilePath = testFilePath;
136
+ currentPhase = phase;
137
+ let rejectFn;
138
+ const promise = new Promise((_, reject) => {
139
+ rejectFn = (err) => {
140
+ watchers.delete(rejectFn);
141
+ reject(err);
142
+ };
143
+ watchers.add(rejectFn);
144
+ });
145
+ const cancel = () => {
146
+ rejectFn(new CrashWatchCancelledError());
147
+ };
148
+ return { promise, cancel };
149
+ };
150
+ return {
151
+ watch,
152
+ isAlive: () => alive,
153
+ stop: async () => {
154
+ monitoring = false;
155
+ await appMonitor.stop();
156
+ },
157
+ start: async () => {
158
+ monitoring = true;
159
+ await appMonitor.start();
160
+ },
161
+ reset: () => {
162
+ alive = false;
163
+ watchers.clear();
164
+ isResolvingCrash = false;
165
+ currentTestFilePath = '';
166
+ },
167
+ dispose: async () => {
168
+ disposed = true;
169
+ monitoring = false;
170
+ watchers.clear();
171
+ isResolvingCrash = false;
172
+ appMonitor.removeListener(appMonitorListener);
173
+ await appMonitor.dispose();
174
+ },
175
+ };
176
+ };
@@ -0,0 +1,16 @@
1
+ import type { TestEvents } from '@jest/test-result';
2
+ import type { Test } from 'jest-runner';
3
+ import type { BridgeEvents } from '@react-native-harness/bridge';
4
+ import type { TestCaseResult } from '@jest/test-result';
5
+ export type EventMapperState = {
6
+ currentTest?: Test;
7
+ suiteHierarchy: string[];
8
+ testResults: Map<string, TestCaseResult>;
9
+ fileStartTime: number;
10
+ };
11
+ export type HarnessEventMapper = {
12
+ handleBridgeEvent: (event: BridgeEvents, test: Test) => void;
13
+ reset: () => void;
14
+ };
15
+ export declare const getHarnessEventMapper: (emit: <T extends keyof TestEvents>(eventName: T, eventData: TestEvents[T]) => void) => HarnessEventMapper;
16
+ //# sourceMappingURL=event-mapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-mapper.d.ts","sourceRoot":"","sources":["../src/event-mapper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,KAAK,EASV,YAAY,EACb,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGxD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,CAAC,EAAE,IAAI,CAAC;IACnB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACzC,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iBAAiB,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAC7D,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,MAAM,CAAC,CAAC,SAAS,MAAM,UAAU,EAC/B,SAAS,EAAE,CAAC,EACZ,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC,KACrB,IAAI,KACR,kBA8JF,CAAC"}