@react-native-harness/jest 1.1.0-rc.1 → 1.1.0-rc.2

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 (68) hide show
  1. package/dist/__tests__/harness-cache.test.d.ts +2 -0
  2. package/dist/__tests__/harness-cache.test.d.ts.map +1 -0
  3. package/dist/__tests__/harness-cache.test.js +51 -0
  4. package/dist/__tests__/harness.test.js +340 -47
  5. package/dist/__tests__/index.test.d.ts +2 -0
  6. package/dist/__tests__/index.test.d.ts.map +1 -0
  7. package/dist/__tests__/index.test.js +42 -0
  8. package/dist/cli-args.d.ts +1 -0
  9. package/dist/cli-args.d.ts.map +1 -1
  10. package/dist/cli-args.js +5 -0
  11. package/dist/crash-supervisor.d.ts.map +1 -1
  12. package/dist/crash-supervisor.js +7 -6
  13. package/dist/errors.d.ts +1 -0
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +1 -0
  16. package/dist/harness.d.ts +23 -1
  17. package/dist/harness.d.ts.map +1 -1
  18. package/dist/harness.js +396 -50
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +205 -58
  21. package/dist/logs.d.ts +1 -0
  22. package/dist/logs.d.ts.map +1 -1
  23. package/dist/logs.js +3 -0
  24. package/dist/run.d.ts +7 -1
  25. package/dist/run.d.ts.map +1 -1
  26. package/dist/run.js +13 -8
  27. package/dist/setup.d.ts.map +1 -1
  28. package/dist/setup.js +18 -2
  29. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  30. package/package.json +7 -6
  31. package/src/__tests__/harness-cache.test.ts +74 -0
  32. package/src/__tests__/harness.test.ts +469 -51
  33. package/src/__tests__/index.test.ts +61 -0
  34. package/src/cli-args.ts +6 -0
  35. package/src/crash-supervisor.ts +13 -6
  36. package/src/env.d.ts +1 -1
  37. package/src/errors.ts +5 -0
  38. package/src/harness.ts +504 -58
  39. package/src/index.ts +276 -64
  40. package/src/logs.ts +4 -0
  41. package/src/run.ts +20 -10
  42. package/src/setup.ts +22 -1
  43. package/tsconfig.json +3 -3
  44. package/tsconfig.lib.json +6 -6
  45. package/dist/cli-args.test.d.ts +0 -2
  46. package/dist/cli-args.test.d.ts.map +0 -1
  47. package/dist/cli-args.test.js +0 -1
  48. package/dist/crash-monitor.d.ts +0 -16
  49. package/dist/crash-monitor.d.ts.map +0 -1
  50. package/dist/crash-monitor.js +0 -75
  51. package/dist/event-mapper.d.ts +0 -16
  52. package/dist/event-mapper.d.ts.map +0 -1
  53. package/dist/event-mapper.js +0 -120
  54. package/dist/global-setup.d.ts +0 -3
  55. package/dist/global-setup.d.ts.map +0 -1
  56. package/dist/global-setup.js +0 -60
  57. package/dist/global-teardown.d.ts +0 -3
  58. package/dist/global-teardown.d.ts.map +0 -1
  59. package/dist/global-teardown.js +0 -11
  60. package/dist/harness-reporter.d.ts +0 -8
  61. package/dist/harness-reporter.d.ts.map +0 -1
  62. package/dist/harness-reporter.js +0 -12
  63. package/dist/jest-options.d.ts +0 -2
  64. package/dist/jest-options.d.ts.map +0 -1
  65. package/dist/jest-options.js +0 -1
  66. package/dist/types.d.ts +0 -17
  67. package/dist/types.d.ts.map +0 -1
  68. package/dist/types.js +0 -1
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=harness-cache.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness-cache.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/harness-cache.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,51 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ const mocks = vi.hoisted(() => ({
3
+ isMetroCacheReusable: vi.fn(),
4
+ logMetroCacheReused: vi.fn(),
5
+ logMetroPrewarmCompleted: vi.fn(),
6
+ }));
7
+ vi.mock('@react-native-harness/bundler-metro', () => ({
8
+ isMetroCacheReusable: mocks.isMetroCacheReusable,
9
+ }));
10
+ vi.mock('../logs.js', () => ({
11
+ logMetroCacheReused: mocks.logMetroCacheReused,
12
+ logMetroPrewarmCompleted: mocks.logMetroPrewarmCompleted,
13
+ }));
14
+ import { maybeLogMetroCacheReuse } from '../harness.js';
15
+ const platform = {
16
+ name: 'ios',
17
+ platformId: 'ios',
18
+ runner: '/virtual/platform-runner.js',
19
+ config: {},
20
+ };
21
+ const createHarnessConfig = (overrides = {}) => ({
22
+ appRegistryComponentName: 'App',
23
+ webSocketPort: 8081,
24
+ disableViewFlattening: false,
25
+ bridgeTimeout: 5000,
26
+ entryPoint: 'index.js',
27
+ unstable__enableMetroCache: true,
28
+ forwardClientLogs: false,
29
+ ...overrides,
30
+ });
31
+ describe('maybeLogMetroCacheReuse', () => {
32
+ beforeEach(() => {
33
+ vi.clearAllMocks();
34
+ mocks.isMetroCacheReusable.mockReturnValue(false);
35
+ });
36
+ it('logs when an existing metro cache will be reused', () => {
37
+ mocks.isMetroCacheReusable.mockReturnValue(true);
38
+ maybeLogMetroCacheReuse(createHarnessConfig(), platform, '/tmp/project');
39
+ expect(mocks.isMetroCacheReusable).toHaveBeenCalledWith('/tmp/project');
40
+ expect(mocks.logMetroCacheReused).toHaveBeenCalledWith(platform);
41
+ });
42
+ it('does not log when metro cache reuse is disabled', () => {
43
+ maybeLogMetroCacheReuse(createHarnessConfig({ unstable__enableMetroCache: false }), platform, '/tmp/project');
44
+ expect(mocks.isMetroCacheReusable).not.toHaveBeenCalled();
45
+ expect(mocks.logMetroCacheReused).not.toHaveBeenCalled();
46
+ });
47
+ it('does not log when the metro cache is absent', () => {
48
+ maybeLogMetroCacheReuse(createHarnessConfig(), platform, '/tmp/project');
49
+ expect(mocks.logMetroCacheReused).not.toHaveBeenCalled();
50
+ });
51
+ });
@@ -1,86 +1,208 @@
1
1
  import { EventEmitter } from 'node:events';
2
- import { describe, expect, it, vi } from 'vitest';
3
- import { waitForAppReady } from '../harness.js';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { definePlugin } from '@react-native-harness/plugins';
4
4
  import { createCrashSupervisor } from '../crash-supervisor.js';
5
+ const mocks = vi.hoisted(() => ({
6
+ createCrashArtifactWriter: vi.fn(() => ({})),
7
+ getBridgeServer: vi.fn(),
8
+ getMetroInstance: vi.fn(),
9
+ isMetroCacheReusable: vi.fn(() => false),
10
+ logMetroCacheReused: vi.fn(),
11
+ waitForMetroBackedAppReady: vi.fn(),
12
+ }));
13
+ vi.mock('@react-native-harness/bundler-metro', async () => {
14
+ const actual = await vi.importActual('@react-native-harness/bundler-metro');
15
+ return {
16
+ ...actual,
17
+ getMetroInstance: mocks.getMetroInstance,
18
+ isMetroCacheReusable: mocks.isMetroCacheReusable,
19
+ waitForMetroBackedAppReady: mocks.waitForMetroBackedAppReady,
20
+ };
21
+ });
22
+ vi.mock('@react-native-harness/bridge/server', () => ({
23
+ getBridgeServer: mocks.getBridgeServer,
24
+ }));
25
+ vi.mock('../logs.js', () => ({
26
+ logMetroCacheReused: mocks.logMetroCacheReused,
27
+ }));
28
+ vi.mock('@react-native-harness/tools', async () => {
29
+ const actual = await vi.importActual('@react-native-harness/tools');
30
+ return {
31
+ ...actual,
32
+ createCrashArtifactWriter: mocks.createCrashArtifactWriter,
33
+ };
34
+ });
35
+ import { getHarness, waitForAppReady } from '../harness.js';
36
+ import { StartupStallError } from '../errors.js';
5
37
  const createBridgeServer = () => {
6
38
  const emitter = new EventEmitter();
7
39
  return {
8
40
  serverBridge: {
41
+ rpc: {
42
+ clients: [],
43
+ },
9
44
  on: emitter.on.bind(emitter),
10
45
  once: emitter.once.bind(emitter),
11
46
  off: emitter.off.bind(emitter),
47
+ dispose: vi.fn(),
12
48
  },
13
49
  emitReady: () => {
14
- emitter.emit('ready');
50
+ emitter.emit('ready', {
51
+ platform: 'ios',
52
+ manufacturer: 'Apple',
53
+ model: 'Simulator',
54
+ osVersion: '18.0',
55
+ });
56
+ },
57
+ emitEvent: (event) => {
58
+ emitter.emit('event', event);
59
+ },
60
+ emitDisconnect: () => {
61
+ emitter.emit('disconnect');
15
62
  },
16
63
  };
17
64
  };
18
- const createAppMonitor = () => {
65
+ const createReporter = () => {
19
66
  const listeners = new Set();
20
67
  return {
21
- start: async () => undefined,
22
- stop: async () => undefined,
23
- dispose: async () => undefined,
24
68
  addListener: (listener) => {
25
69
  listeners.add(listener);
26
70
  },
27
71
  removeListener: (listener) => {
28
72
  listeners.delete(listener);
29
73
  },
74
+ emit: (event) => {
75
+ listeners.forEach((listener) => listener(event));
76
+ },
77
+ clearAllListeners: () => {
78
+ listeners.clear();
79
+ },
80
+ };
81
+ };
82
+ const createMetroInstance = (overrides = {}) => ({
83
+ events: createReporter(),
84
+ waitUntilHealthy: vi.fn(async () => 'HTTP 200: packager-status:running'),
85
+ prewarm: vi.fn(async () => false),
86
+ dispose: vi.fn(async () => undefined),
87
+ ...overrides,
88
+ });
89
+ const createAppMonitor = () => {
90
+ const listeners = new Set();
91
+ return {
92
+ appMonitor: {
93
+ start: vi.fn(async () => undefined),
94
+ stop: vi.fn(async () => undefined),
95
+ dispose: vi.fn(async () => undefined),
96
+ addListener: (listener) => {
97
+ listeners.add(listener);
98
+ },
99
+ removeListener: (listener) => {
100
+ listeners.delete(listener);
101
+ },
102
+ },
103
+ emit: (event) => {
104
+ for (const listener of listeners) {
105
+ listener(event);
106
+ }
107
+ },
30
108
  };
31
109
  };
32
- const createPlatformRunner = (restartApp) => ({
33
- startApp: async () => undefined,
34
- restartApp,
35
- stopApp: async () => undefined,
36
- dispose: async () => undefined,
37
- isAppRunning: async () => true,
38
- createAppMonitor: createAppMonitor,
110
+ const createPlatformRunner = (overrides = {}) => ({
111
+ startApp: vi.fn(async () => undefined),
112
+ restartApp: vi.fn(async () => undefined),
113
+ stopApp: vi.fn(async () => undefined),
114
+ dispose: vi.fn(async () => undefined),
115
+ isAppRunning: vi.fn(async () => true),
116
+ createAppMonitor: () => createAppMonitor().appMonitor,
117
+ ...overrides,
118
+ });
119
+ const createHarnessConfig = (overrides = {}) => ({
120
+ appRegistryComponentName: 'App',
121
+ bridgeTimeout: 60_000,
122
+ bundleStartTimeout: 60_000,
123
+ crashDetectionInterval: 500,
124
+ defaultRunner: 'ios',
125
+ detectNativeCrashes: true,
126
+ disableViewFlattening: false,
127
+ entryPoint: 'index.js',
128
+ forwardClientLogs: false,
129
+ maxAppRestarts: 2,
130
+ metroPort: 8081,
131
+ resetEnvironmentBetweenTestFiles: true,
132
+ runners: [],
133
+ unstable__enableMetroCache: false,
134
+ unstable__skipAlreadyIncludedModules: false,
135
+ webSocketPort: 3001,
136
+ ...overrides,
137
+ });
138
+ beforeEach(() => {
139
+ vi.clearAllMocks();
140
+ });
141
+ afterEach(() => {
142
+ delete globalThis.__HARNESS_PLATFORM_RUNNER__;
39
143
  });
40
144
  describe('waitForAppReady', () => {
41
- it('passes launch options to the initial launch', async () => {
145
+ it('delegates startup orchestration to bundler-metro and resolves readiness from the bridge', async () => {
42
146
  const { serverBridge, emitReady } = createBridgeServer();
43
- const restartApp = vi.fn().mockResolvedValue(undefined);
44
- const platformInstance = createPlatformRunner(restartApp);
147
+ const metroInstance = createMetroInstance();
148
+ const platformInstance = createPlatformRunner();
149
+ const { appMonitor } = createAppMonitor();
45
150
  const crashSupervisor = createCrashSupervisor({
46
- appMonitor: createAppMonitor(),
151
+ appMonitor,
47
152
  platformRunner: platformInstance,
48
153
  });
49
- const promise = waitForAppReady({
154
+ mocks.waitForMetroBackedAppReady.mockImplementationOnce(async (options) => {
155
+ options.onAttemptStart?.();
156
+ const readyPromise = options.waitForReady(new AbortController().signal);
157
+ emitReady();
158
+ await readyPromise;
159
+ options.onAttemptReset?.();
160
+ });
161
+ await waitForAppReady({
162
+ metroInstance,
50
163
  serverBridge,
51
164
  platformInstance,
52
- bridgeTimeout: 5000,
165
+ platformId: 'ios',
166
+ bundleStartTimeout: 1_500,
167
+ readyTimeout: 2_500,
168
+ maxAppRestarts: 3,
53
169
  testFilePath: '/tmp/test.harness.ts',
54
170
  crashSupervisor,
55
- appLaunchOptions: {
56
- extras: {
57
- mode: 'startup',
58
- },
59
- },
60
171
  });
61
- await Promise.resolve();
62
- expect(restartApp).toHaveBeenCalledWith({
63
- extras: {
64
- mode: 'startup',
65
- },
66
- });
67
- emitReady();
68
- await promise;
172
+ expect(mocks.waitForMetroBackedAppReady).toHaveBeenCalledWith(expect.objectContaining({
173
+ metro: metroInstance,
174
+ platformId: 'ios',
175
+ bundleStartTimeout: 1_500,
176
+ readyTimeout: 2_500,
177
+ maxAppRestarts: 3,
178
+ startAttempt: expect.any(Function),
179
+ waitForReady: expect.any(Function),
180
+ waitForCrash: expect.any(Function),
181
+ }));
182
+ expect(crashSupervisor.isReady()).toBe(true);
69
183
  await crashSupervisor.dispose();
70
184
  });
71
- it('does not retry launch when the app never becomes ready', async () => {
72
- vi.useFakeTimers();
185
+ it('passes launch options through the shared Metro startup helper', async () => {
73
186
  const { serverBridge } = createBridgeServer();
74
- const restartApp = vi.fn().mockResolvedValue(undefined);
75
- const platformInstance = createPlatformRunner(restartApp);
187
+ const metroInstance = createMetroInstance();
188
+ const restartApp = vi.fn(async () => undefined);
189
+ const platformInstance = createPlatformRunner({ restartApp });
190
+ const { appMonitor } = createAppMonitor();
76
191
  const crashSupervisor = createCrashSupervisor({
77
- appMonitor: createAppMonitor(),
192
+ appMonitor,
78
193
  platformRunner: platformInstance,
79
194
  });
80
- const promise = waitForAppReady({
195
+ mocks.waitForMetroBackedAppReady.mockImplementationOnce(async (options) => {
196
+ await options.startAttempt();
197
+ });
198
+ await waitForAppReady({
199
+ metroInstance,
81
200
  serverBridge,
82
201
  platformInstance,
83
- bridgeTimeout: 1000,
202
+ platformId: 'ios',
203
+ bundleStartTimeout: 1_500,
204
+ readyTimeout: 2_500,
205
+ maxAppRestarts: 3,
84
206
  testFilePath: '/tmp/test.harness.ts',
85
207
  crashSupervisor,
86
208
  appLaunchOptions: {
@@ -89,18 +211,189 @@ describe('waitForAppReady', () => {
89
211
  },
90
212
  },
91
213
  });
92
- await Promise.resolve();
93
- await vi.advanceTimersByTimeAsync(1000);
94
- await expect(promise).rejects.toMatchObject({
95
- name: 'AbortError',
96
- });
97
- expect(restartApp).toHaveBeenCalledTimes(1);
98
214
  expect(restartApp).toHaveBeenCalledWith({
99
215
  extras: {
100
216
  mode: 'startup',
101
217
  },
102
218
  });
103
219
  await crashSupervisor.dispose();
104
- vi.useRealTimers();
220
+ });
221
+ });
222
+ describe('getHarness', () => {
223
+ it('routes ensureAppReady through the shared Metro startup helper', async () => {
224
+ const { serverBridge, emitReady } = createBridgeServer();
225
+ const appMonitor = createAppMonitor();
226
+ const restartApp = vi.fn(async () => undefined);
227
+ const platformInstance = createPlatformRunner({
228
+ restartApp,
229
+ createAppMonitor: () => appMonitor.appMonitor,
230
+ });
231
+ const metroInstance = createMetroInstance();
232
+ mocks.getBridgeServer.mockResolvedValue(serverBridge);
233
+ mocks.getMetroInstance.mockResolvedValue(metroInstance);
234
+ mocks.waitForMetroBackedAppReady.mockImplementationOnce(async (options) => {
235
+ await options.startAttempt();
236
+ const readyPromise = options.waitForReady(new AbortController().signal);
237
+ emitReady();
238
+ await readyPromise;
239
+ });
240
+ globalThis.__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance);
241
+ const platform = {
242
+ config: {
243
+ appLaunchOptions: {
244
+ extras: {
245
+ source: 'ensure-ready',
246
+ },
247
+ },
248
+ },
249
+ name: 'ios',
250
+ platformId: 'ios',
251
+ runner: `data:text/javascript,${encodeURIComponent('export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);')}`,
252
+ };
253
+ const harness = await getHarness(createHarnessConfig(), platform, '/tmp/project');
254
+ await harness.ensureAppReady('/tmp/example.harness.ts');
255
+ expect(mocks.waitForMetroBackedAppReady).toHaveBeenCalledTimes(1);
256
+ expect(restartApp).toHaveBeenCalledWith({
257
+ extras: {
258
+ source: 'ensure-ready',
259
+ },
260
+ });
261
+ await harness.dispose();
262
+ });
263
+ it('routes restart(testFilePath) through the shared Metro startup helper', async () => {
264
+ const { serverBridge, emitReady } = createBridgeServer();
265
+ const appMonitor = createAppMonitor();
266
+ const restartApp = vi.fn(async () => undefined);
267
+ const stopApp = vi.fn(async () => undefined);
268
+ const platformInstance = createPlatformRunner({
269
+ restartApp,
270
+ stopApp,
271
+ isAppRunning: vi.fn(async () => false),
272
+ createAppMonitor: () => appMonitor.appMonitor,
273
+ });
274
+ const metroInstance = createMetroInstance();
275
+ mocks.getBridgeServer.mockResolvedValue(serverBridge);
276
+ mocks.getMetroInstance.mockResolvedValue(metroInstance);
277
+ mocks.waitForMetroBackedAppReady.mockImplementationOnce(async (options) => {
278
+ await options.startAttempt();
279
+ const readyPromise = options.waitForReady(new AbortController().signal);
280
+ emitReady();
281
+ await readyPromise;
282
+ });
283
+ globalThis.__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance);
284
+ const platform = {
285
+ config: {
286
+ appLaunchOptions: {
287
+ extras: {
288
+ source: 'restart',
289
+ },
290
+ },
291
+ },
292
+ name: 'ios',
293
+ platformId: 'ios',
294
+ runner: `data:text/javascript,${encodeURIComponent('export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);')}`,
295
+ };
296
+ const harness = await getHarness(createHarnessConfig(), platform, '/tmp/project');
297
+ await harness.restart('/tmp/restart.harness.ts');
298
+ expect(stopApp).toHaveBeenCalledTimes(1);
299
+ expect(mocks.waitForMetroBackedAppReady).toHaveBeenCalledTimes(1);
300
+ expect(restartApp).toHaveBeenCalledWith({
301
+ extras: {
302
+ source: 'restart',
303
+ },
304
+ });
305
+ await harness.dispose();
306
+ });
307
+ });
308
+ describe('plugins', () => {
309
+ it('invokes lifecycle and runtime plugin handlers for configured plugins', async () => {
310
+ const { serverBridge, emitReady, emitEvent, emitDisconnect } = createBridgeServer();
311
+ const appMonitor = createAppMonitor();
312
+ const platformInstance = createPlatformRunner({
313
+ createAppMonitor: () => appMonitor.appMonitor,
314
+ });
315
+ const observedHooks = [];
316
+ mocks.getBridgeServer.mockResolvedValue(serverBridge);
317
+ const metroInstance = createMetroInstance();
318
+ mocks.getMetroInstance.mockResolvedValue(metroInstance);
319
+ globalThis.__HARNESS_PLATFORM_RUNNER__ = vi.fn(async () => platformInstance);
320
+ const plugin = definePlugin({
321
+ name: 'test-plugin',
322
+ createState: () => ({
323
+ creationCount: 0,
324
+ }),
325
+ hooks: {
326
+ harness: {
327
+ beforeCreation: (ctx) => {
328
+ ctx.state.creationCount += 1;
329
+ observedHooks.push(`beforeCreation:${ctx.platform.platformId}:${ctx.appLaunchOptions == null ? 'no-launch-options' : 'launch-options'}`);
330
+ },
331
+ beforeDispose: (ctx) => {
332
+ observedHooks.push(`beforeDispose:${ctx.state.creationCount}:${ctx.reason}`);
333
+ },
334
+ },
335
+ runtime: {
336
+ ready: (ctx) => {
337
+ observedHooks.push(`runtime.ready:${ctx.runId}:${ctx.device.platform}`);
338
+ },
339
+ disconnected: (ctx) => {
340
+ observedHooks.push(`runtime.disconnected:${ctx.reason}`);
341
+ },
342
+ },
343
+ collection: {
344
+ started: (ctx) => {
345
+ observedHooks.push(`collection.started:${ctx.file}`);
346
+ },
347
+ },
348
+ },
349
+ });
350
+ const platform = {
351
+ config: {
352
+ appLaunchOptions: {
353
+ environment: {
354
+ MODE: 'test',
355
+ },
356
+ },
357
+ },
358
+ name: 'ios',
359
+ platformId: 'ios',
360
+ runner: `data:text/javascript,${encodeURIComponent('export default (...args) => globalThis.__HARNESS_PLATFORM_RUNNER__(...args);')}`,
361
+ };
362
+ const harness = await getHarness(createHarnessConfig({
363
+ plugins: [plugin],
364
+ }), platform, '/tmp/project');
365
+ harness.setRunState({
366
+ runId: 'run-1',
367
+ startTime: Date.now(),
368
+ testFiles: ['example.harness.ts'],
369
+ watchMode: false,
370
+ coverageEnabled: false,
371
+ summary: {
372
+ passed: 1,
373
+ failed: 0,
374
+ skipped: 0,
375
+ todo: 0,
376
+ },
377
+ status: 'passed',
378
+ });
379
+ emitReady();
380
+ emitEvent({
381
+ type: 'collection-started',
382
+ file: 'example.harness.ts',
383
+ });
384
+ emitDisconnect();
385
+ await harness.dispose();
386
+ expect(observedHooks).toEqual([
387
+ 'beforeCreation:ios:launch-options',
388
+ 'runtime.ready:run-1:ios',
389
+ 'collection.started:example.harness.ts',
390
+ 'runtime.disconnected:bridge-disconnected',
391
+ 'beforeDispose:1:normal',
392
+ ]);
393
+ });
394
+ });
395
+ describe('StartupStallError', () => {
396
+ it('includes the configured timeout and attempt count', () => {
397
+ expect(new StartupStallError(1_500, 4).message).toBe('The app did not request its Metro bundle after 4 launch attempts within 1500ms. Last Metro status: unknown.');
105
398
  });
106
399
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/index.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import JestHarness from '../index.js';
3
+ import { StartupStallError } from '../errors.js';
4
+ 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: '',
40
+ });
41
+ });
42
+ });
@@ -1,5 +1,6 @@
1
1
  export type HarnessCliArgs = {
2
2
  harnessRunner?: string;
3
+ metroPort?: number;
3
4
  };
4
5
  export declare const getAdditionalCliArgs: () => HarnessCliArgs;
5
6
  //# sourceMappingURL=cli-args.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cli-args.d.ts","sourceRoot":"","sources":["../src/cli-args.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,cAAc,GAAG;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,eAAO,MAAM,oBAAoB,QAAO,cAevC,CAAC"}
1
+ {"version":3,"file":"cli-args.d.ts","sourceRoot":"","sources":["../src/cli-args.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,cAAc,GAAG;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,eAAO,MAAM,oBAAoB,QAAO,cAoBvC,CAAC"}
package/dist/cli-args.js CHANGED
@@ -5,6 +5,10 @@ export const getAdditionalCliArgs = () => {
5
5
  .option('harnessRunner', {
6
6
  type: 'string',
7
7
  description: 'Specify which Harness runner to use',
8
+ })
9
+ .option('metroPort', {
10
+ type: 'number',
11
+ description: 'Override the Metro bundler port',
8
12
  })
9
13
  .strict(false)
10
14
  .help(false)
@@ -13,5 +17,6 @@ export const getAdditionalCliArgs = () => {
13
17
  .parseSync();
14
18
  return {
15
19
  harnessRunner: argv.harnessRunner,
20
+ metroPort: argv.metroPort,
16
21
  };
17
22
  };
@@ -1 +1 @@
1
- {"version":3,"file":"crash-supervisor.d.ts","sourceRoot":"","sources":["../src/crash-supervisor.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EAIf,KAAK,qBAAqB,EAC3B,MAAM,iCAAiC,CAAC;AAQzC,MAAM,MAAM,oBAAoB,GAC5B,MAAM,GACN,WAAW,GACX,OAAO,GACP,SAAS,GACT,WAAW,CAAC;AAEhB,MAAM,MAAM,eAAe,GAAG;IAC5B,iBAAiB,EAAE,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACzD,WAAW,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,YAAY,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;IACvD,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,qBAAqB,CAAC;CACvC,CAAC;AAmCF,eAAO,MAAM,qBAAqB,GAAI,iCAGnC,sBAAsB,KAAG,eA6O3B,CAAC"}
1
+ {"version":3,"file":"crash-supervisor.d.ts","sourceRoot":"","sources":["../src/crash-supervisor.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EAIf,KAAK,qBAAqB,EAC3B,MAAM,iCAAiC,CAAC;AAUzC,MAAM,MAAM,oBAAoB,GAC5B,MAAM,GACN,WAAW,GACX,OAAO,GACP,SAAS,GACT,WAAW,CAAC;AAEhB,MAAM,MAAM,eAAe,GAAG;IAC5B,iBAAiB,EAAE,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACzD,WAAW,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,YAAY,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;IACvD,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,qBAAqB,CAAC;CACvC,CAAC;AAmCF,eAAO,MAAM,qBAAqB,GAAI,iCAGnC,sBAAsB,KAAG,eAkP3B,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { NativeCrashError, } from './errors.js';
2
2
  import { logger } from '@react-native-harness/tools';
3
+ const crashLogger = logger.child('crash');
3
4
  const getCrashPhase = (state) => state === 'running' ? 'execution' : 'startup';
4
5
  const mergeCrashDetails = (phase, initialDetails, enrichedDetails, fallbackSummary) => ({
5
6
  phase,
@@ -43,13 +44,13 @@ export const createCrashSupervisor = ({ appMonitor, platformRunner, }) => {
43
44
  return;
44
45
  }
45
46
  if (!activeTestFilePath) {
46
- logger.debug(`Ignoring crash signal without active test: ${reason}`);
47
+ crashLogger.debug('ignoring crash signal without active test: %s', reason);
47
48
  return;
48
49
  }
49
50
  isResolvingCrash = true;
50
- logger.debug(`Native crash detected during ${activeTestFilePath} (state: ${state}, reason: ${reason || '(none)'})`);
51
+ crashLogger.debug('native crash detected during %s (state=%s reason=%s)', activeTestFilePath, state, reason || '(none)');
51
52
  for (const line of details?.rawLines ?? []) {
52
- logger.debug(line);
53
+ crashLogger.debug('%s', line);
53
54
  }
54
55
  const phase = getCrashPhase(state);
55
56
  const testFilePath = activeTestFilePath;
@@ -61,7 +62,7 @@ export const createCrashSupervisor = ({ appMonitor, platformRunner, }) => {
61
62
  occurredAt: Date.now(),
62
63
  });
63
64
  const mergedDetails = mergeCrashDetails(phase, details, enrichedDetails, reason);
64
- logger.debug('Crash details:', {
65
+ crashLogger.debug('crash details: %o', {
65
66
  phase: mergedDetails.phase,
66
67
  source: mergedDetails.source,
67
68
  summary: mergedDetails.summary,
@@ -83,11 +84,11 @@ export const createCrashSupervisor = ({ appMonitor, platformRunner, }) => {
83
84
  try {
84
85
  const isRunning = await platformRunner.isAppRunning();
85
86
  if (!isRunning) {
86
- handleCrash(reason, details);
87
+ void handleCrash(reason, details);
87
88
  }
88
89
  }
89
90
  catch (error) {
90
- logger.debug('Crash confirmation failed', error);
91
+ crashLogger.debug('crash confirmation failed', error);
91
92
  }
92
93
  };
93
94
  const appMonitorListener = (event) => {
package/dist/errors.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { HarnessError } from '@react-native-harness/tools';
2
2
  import type { AppCrashDetails } from '@react-native-harness/platforms';
3
+ export { StartupStallError, type StartupStallCode, type StartupStallDetails, } from '@react-native-harness/bundler-metro';
3
4
  export declare class NoRunnerSpecifiedError extends HarnessError {
4
5
  constructor();
5
6
  }
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAEvE,qBAAa,sBAAuB,SAAQ,YAAY;;CAKvD;AAED,qBAAa,mBAAoB,SAAQ,YAAY;aACvB,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAI/C;AAED,qBAAa,0BAA2B,SAAQ,YAAY;;CAK3D;AAED,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,WAAW,CAAC;AAEvD,MAAM,MAAM,kBAAkB,GAAG,eAAe,GAAG;IACjD,KAAK,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAsDF,qBAAa,gBAAiB,SAAQ,YAAY;aAE9B,YAAY,EAAE,MAAM;aACpB,OAAO,EAAE,kBAAkB;aAC3B,aAAa,CAAC,EAAE,MAAM;gBAFtB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,kBAAkB,EAC3B,aAAa,CAAC,EAAE,MAAM,YAAA;IAOxC,IAAI,KAAK,qBAER;CACF"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,EACL,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,GACzB,MAAM,qCAAqC,CAAC;AAE7C,qBAAa,sBAAuB,SAAQ,YAAY;;CAKvD;AAED,qBAAa,mBAAoB,SAAQ,YAAY;aACvB,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAI/C;AAED,qBAAa,0BAA2B,SAAQ,YAAY;;CAK3D;AAED,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,WAAW,CAAC;AAEvD,MAAM,MAAM,kBAAkB,GAAG,eAAe,GAAG;IACjD,KAAK,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAsDF,qBAAa,gBAAiB,SAAQ,YAAY;aAE9B,YAAY,EAAE,MAAM;aACpB,OAAO,EAAE,kBAAkB;aAC3B,aAAa,CAAC,EAAE,MAAM;gBAFtB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,kBAAkB,EAC3B,aAAa,CAAC,EAAE,MAAM,YAAA;IAOxC,IAAI,KAAK,qBAER;CACF"}
package/dist/errors.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { HarnessError } from '@react-native-harness/tools';
2
+ export { StartupStallError, } from '@react-native-harness/bundler-metro';
2
3
  export class NoRunnerSpecifiedError extends HarnessError {
3
4
  constructor() {
4
5
  super('No runner specified');