@react-native-harness/jest 1.2.0 → 1.4.0-rc.1

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 (54) hide show
  1. package/dist/__tests__/bridge.test.js +23 -2
  2. package/dist/__tests__/crash-monitor.test.js +146 -192
  3. package/dist/__tests__/errors.test.js +39 -0
  4. package/dist/__tests__/execute-run.test.js +190 -26
  5. package/dist/__tests__/fixtures/mock-platform-runner.d.ts +7 -0
  6. package/dist/__tests__/fixtures/mock-platform-runner.d.ts.map +1 -0
  7. package/dist/__tests__/fixtures/mock-platform-runner.js +6 -0
  8. package/dist/__tests__/harness-session.test.d.ts +2 -0
  9. package/dist/__tests__/harness-session.test.d.ts.map +1 -0
  10. package/dist/__tests__/harness-session.test.js +74 -0
  11. package/dist/__tests__/index.test.js +59 -10
  12. package/dist/__tests__/test-file-platform-filter.test.d.ts +2 -0
  13. package/dist/__tests__/test-file-platform-filter.test.d.ts.map +1 -0
  14. package/dist/__tests__/test-file-platform-filter.test.js +47 -0
  15. package/dist/crash-monitor.d.ts +5 -4
  16. package/dist/crash-monitor.d.ts.map +1 -1
  17. package/dist/crash-monitor.js +116 -102
  18. package/dist/errors.d.ts +10 -0
  19. package/dist/errors.d.ts.map +1 -1
  20. package/dist/errors.js +41 -2
  21. package/dist/execute-run.d.ts +4 -2
  22. package/dist/execute-run.d.ts.map +1 -1
  23. package/dist/execute-run.js +85 -7
  24. package/dist/harness-session.d.ts +21 -1
  25. package/dist/harness-session.d.ts.map +1 -1
  26. package/dist/harness-session.js +134 -39
  27. package/dist/index.d.ts +7 -3
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +24 -2
  30. package/dist/run.d.ts.map +1 -1
  31. package/dist/run.js +1 -0
  32. package/dist/test-file-platform-filter.d.ts +5 -0
  33. package/dist/test-file-platform-filter.d.ts.map +1 -0
  34. package/dist/test-file-platform-filter.js +40 -0
  35. package/dist/toTestResult.d.ts +1 -0
  36. package/dist/toTestResult.d.ts.map +1 -1
  37. package/dist/toTestResult.js +2 -2
  38. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  39. package/package.json +7 -7
  40. package/src/__tests__/bridge.test.ts +37 -2
  41. package/src/__tests__/crash-monitor.test.ts +182 -231
  42. package/src/__tests__/errors.test.ts +75 -0
  43. package/src/__tests__/execute-run.test.ts +235 -34
  44. package/src/__tests__/harness-session.test.ts +93 -0
  45. package/src/__tests__/index.test.ts +89 -11
  46. package/src/__tests__/test-file-platform-filter.test.ts +83 -0
  47. package/src/crash-monitor.ts +192 -131
  48. package/src/errors.ts +62 -1
  49. package/src/execute-run.ts +125 -11
  50. package/src/harness-session.ts +177 -42
  51. package/src/index.ts +40 -11
  52. package/src/run.ts +1 -0
  53. package/src/test-file-platform-filter.ts +57 -0
  54. package/src/toTestResult.ts +3 -2
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi, beforeEach } from 'vitest';
2
2
  import { NativeCrashError, StartupStallError } from '../errors.js';
3
- import { DeviceNotRespondingError } from '@react-native-harness/bridge/server';
3
+ import { AppBridgeDisconnectedError, DeviceNotRespondingError, } from '@react-native-harness/bridge/server';
4
4
  import { executeRun } from '../execute-run.js';
5
5
  const resolveUndefined = async () => undefined;
6
6
  // Mock the file-runner so we control what jestResult/harnessResult each test returns
@@ -66,18 +66,38 @@ const makeSession = (overrides = {}) => ({
66
66
  host: undefined,
67
67
  resetEnvironmentBetweenTestFiles: false,
68
68
  detectNativeCrashes: true,
69
+ runners: [
70
+ { platformId: 'android', name: 'android' },
71
+ { platformId: 'ios', name: 'ios' },
72
+ ],
73
+ },
74
+ context: {
75
+ platform: {
76
+ platformId: 'android',
77
+ name: 'android',
78
+ runner: '/virtual/android-runner.js',
79
+ cli: '/virtual/android-cli.js',
80
+ config: {},
81
+ },
69
82
  },
70
- context: {},
71
83
  ensureAppReady: vi.fn(resolveUndefined),
72
84
  runTestFile: vi.fn(async () => makeHarnessResult()),
73
85
  restartApp: vi.fn(resolveUndefined),
74
86
  resetCrashState: vi.fn(),
75
87
  flushClientLogs: vi.fn(() => []),
76
88
  callHook: vi.fn(resolveUndefined),
89
+ onTestRunnerEvent: vi.fn(() => () => undefined),
77
90
  setRunState: vi.fn(),
78
91
  dispose: vi.fn(resolveUndefined),
79
92
  ...overrides,
80
93
  });
94
+ const makeEmitEvent = () => {
95
+ const calls = [];
96
+ const emitEvent = (async (...eventData) => {
97
+ calls.push(eventData);
98
+ });
99
+ return { emitEvent, calls };
100
+ };
81
101
  // ---------------------------------------------------------------------------
82
102
  // Tests
83
103
  // ---------------------------------------------------------------------------
@@ -92,7 +112,7 @@ describe('executeRun', () => {
92
112
  const session = makeSession({
93
113
  callHook: vi.fn(async (name) => { hookNames.push(name); }),
94
114
  });
95
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
115
+ await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
96
116
  expect(hookNames[0]).toBe('run:started');
97
117
  expect(hookNames[hookNames.length - 1]).toBe('run:finished');
98
118
  });
@@ -101,7 +121,7 @@ describe('executeRun', () => {
101
121
  const session = makeSession({
102
122
  callHook: vi.fn(async (name) => { hookNames.push(name); }),
103
123
  });
104
- await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
124
+ await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
105
125
  const fileHooks = hookNames.filter((n) => n === 'test-file:started' || n === 'test-file:finished');
106
126
  expect(fileHooks).toEqual([
107
127
  'test-file:started', 'test-file:finished',
@@ -114,24 +134,35 @@ describe('executeRun', () => {
114
134
  callHook: vi.fn(async (name) => { hookNames.push(name); }),
115
135
  ensureAppReady: vi.fn(async () => { throw new Error('unexpected'); }),
116
136
  });
117
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
137
+ await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
118
138
  expect(hookNames).toContain('run:finished');
119
139
  });
120
140
  });
121
141
  describe('happy path', () => {
122
- it('calls onStart, ensureAppReady, runTestFile, onResult in order', async () => {
142
+ it('emits file start, ensureAppReady, runTestFile, file success in order', async () => {
123
143
  const order = [];
124
144
  const session = makeSession({
125
145
  ensureAppReady: vi.fn(async () => { order.push('ensureAppReady'); }),
126
146
  });
127
- const onStart = vi.fn(async () => { order.push('onStart'); });
128
- const onResult = vi.fn(async () => { order.push('onResult'); });
147
+ const emitEvent = (async (eventName, ..._eventData) => {
148
+ if (eventName === 'test-file-start') {
149
+ order.push('test-file-start');
150
+ }
151
+ if (eventName === 'test-file-success') {
152
+ order.push('test-file-success');
153
+ }
154
+ });
129
155
  mockRunHarnessTestFile.mockImplementation(async () => {
130
156
  order.push('runTestFile');
131
157
  return makeFileRunResult();
132
158
  });
133
- await executeRun(session, [makeTest()], makeWatcher(), onStart, onResult, vi.fn(), makeGlobalConfig());
134
- expect(order).toEqual(['onStart', 'ensureAppReady', 'runTestFile', 'onResult']);
159
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
160
+ expect(order).toEqual([
161
+ 'test-file-start',
162
+ 'ensureAppReady',
163
+ 'runTestFile',
164
+ 'test-file-success',
165
+ ]);
135
166
  });
136
167
  it('accumulates passing test counts in run:finished summary', async () => {
137
168
  let finishedPayload;
@@ -142,7 +173,7 @@ describe('executeRun', () => {
142
173
  }),
143
174
  });
144
175
  mockRunHarnessTestFile.mockResolvedValue(makeFileRunResult({ jestResult: makeJestResult({ numPassingTests: 3 }) }));
145
- await executeRun(session, [makeTest(), makeTest('/b.ts')], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
176
+ await executeRun(session, [makeTest(), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
146
177
  const payload = finishedPayload;
147
178
  expect(payload.summary.passed).toBe(6);
148
179
  expect(payload.status).toBe('passed');
@@ -154,34 +185,124 @@ describe('executeRun', () => {
154
185
  .mockReturnValueOnce([])
155
186
  .mockReturnValueOnce(clientLogs),
156
187
  });
157
- const onResult = vi.fn();
158
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), onResult, vi.fn(), makeGlobalConfig());
188
+ const { emitEvent, calls } = makeEmitEvent();
189
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
159
190
  expect(session.flushClientLogs).toHaveBeenCalledTimes(2);
160
- expect(onResult).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ console: clientLogs }));
191
+ expect(calls).toContainEqual([
192
+ 'test-file-success',
193
+ expect.anything(),
194
+ expect.objectContaining({ console: clientLogs }),
195
+ ]);
196
+ });
197
+ it('emits test-case events before file success', async () => {
198
+ let testRunnerListener;
199
+ const { emitEvent, calls: emittedEvents } = makeEmitEvent();
200
+ const session = makeSession({
201
+ onTestRunnerEvent: vi.fn((listener) => {
202
+ testRunnerListener = listener;
203
+ return () => undefined;
204
+ }),
205
+ });
206
+ mockRunHarnessTestFile.mockImplementation(async () => {
207
+ testRunnerListener?.({
208
+ type: 'test-started',
209
+ file: 'example.ts',
210
+ suite: 'suite',
211
+ name: 'works',
212
+ ancestorTitles: ['suite'],
213
+ fullName: 'suite works',
214
+ startedAt: 10,
215
+ declarationMode: 'only',
216
+ });
217
+ testRunnerListener?.({
218
+ type: 'test-finished',
219
+ file: 'example.ts',
220
+ suite: 'suite',
221
+ name: 'works',
222
+ ancestorTitles: ['suite'],
223
+ fullName: 'suite works',
224
+ startedAt: 10,
225
+ declarationMode: 'only',
226
+ duration: 5,
227
+ status: 'passed',
228
+ });
229
+ return makeFileRunResult();
230
+ });
231
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
232
+ expect(emittedEvents).toEqual([
233
+ ['test-file-start', expect.anything()],
234
+ [
235
+ 'test-case-start',
236
+ 'example.ts',
237
+ expect.objectContaining({
238
+ ancestorTitles: ['suite'],
239
+ fullName: 'suite works',
240
+ mode: 'only',
241
+ title: 'works',
242
+ startedAt: 10,
243
+ }),
244
+ ],
245
+ [
246
+ 'test-case-result',
247
+ 'example.ts',
248
+ expect.objectContaining({
249
+ ancestorTitles: ['suite'],
250
+ fullName: 'suite works',
251
+ numPassingAsserts: 1,
252
+ startedAt: 10,
253
+ status: 'passed',
254
+ title: 'works',
255
+ }),
256
+ ],
257
+ ['test-file-success', expect.anything(), expect.anything()],
258
+ ]);
161
259
  });
162
260
  });
163
261
  describe('runtime failures', () => {
164
262
  it('passes StartupStallError to onFailure with an empty stack', async () => {
165
- const onFailure = vi.fn();
263
+ const { emitEvent, calls } = makeEmitEvent();
166
264
  const session = makeSession({
167
265
  ensureAppReady: vi.fn().mockRejectedValue(new StartupStallError(1500, 3)),
168
266
  });
169
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), vi.fn(), onFailure, makeGlobalConfig());
170
- expect(onFailure).toHaveBeenCalledWith(expect.objectContaining({ path: '/test/example.ts' }), expect.objectContaining({ message: expect.stringContaining('1500'), stack: '' }));
267
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
268
+ expect(calls).toContainEqual([
269
+ 'test-file-failure',
270
+ expect.objectContaining({ path: '/test/example.ts' }),
271
+ expect.objectContaining({ message: expect.stringContaining('1500'), stack: '' }),
272
+ ]);
171
273
  });
172
274
  it('passes DeviceNotRespondingError to onFailure with an empty stack', async () => {
173
- const onFailure = vi.fn();
275
+ const { emitEvent, calls } = makeEmitEvent();
174
276
  const session = makeSession({
175
277
  ensureAppReady: vi.fn().mockRejectedValue(new DeviceNotRespondingError('runTests', [])),
176
278
  });
177
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), vi.fn(), onFailure, makeGlobalConfig());
178
- expect(onFailure).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ stack: '' }));
279
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
280
+ expect(calls).toContainEqual([
281
+ 'test-file-failure',
282
+ expect.anything(),
283
+ expect.objectContaining({ stack: '' }),
284
+ ]);
285
+ });
286
+ it('passes AppBridgeDisconnectedError to onFailure with an empty stack', async () => {
287
+ const { emitEvent, calls } = makeEmitEvent();
288
+ const session = makeSession({
289
+ ensureAppReady: vi.fn().mockRejectedValue(new AppBridgeDisconnectedError('app-disconnected')),
290
+ });
291
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
292
+ expect(calls).toContainEqual([
293
+ 'test-file-failure',
294
+ expect.anything(),
295
+ expect.objectContaining({
296
+ message: expect.stringContaining('The app bridge disconnected during test execution.'),
297
+ stack: '',
298
+ }),
299
+ ]);
179
300
  });
180
301
  it('calls resetCrashState after a NativeCrashError', async () => {
181
302
  const session = makeSession({
182
303
  ensureAppReady: vi.fn().mockRejectedValue(new NativeCrashError('example.ts', { phase: 'execution', source: 'polling' })),
183
304
  });
184
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
305
+ await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
185
306
  expect(session.resetCrashState).toHaveBeenCalled();
186
307
  });
187
308
  it('counts runtime failures in the summary', async () => {
@@ -193,7 +314,7 @@ describe('executeRun', () => {
193
314
  }),
194
315
  ensureAppReady: vi.fn().mockRejectedValue(new StartupStallError(1000, 2)),
195
316
  });
196
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
317
+ await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
197
318
  const payload = finishedPayload;
198
319
  expect(payload.summary.failed).toBe(1);
199
320
  expect(payload.status).toBe('failed');
@@ -205,10 +326,10 @@ describe('executeRun', () => {
205
326
  const session = makeSession({
206
327
  callHook: vi.fn(async (name) => { hookNames.push(name); }),
207
328
  });
208
- const onStart = vi.fn();
209
- await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(true /* interrupted */), onStart, vi.fn(), vi.fn(), makeGlobalConfig());
329
+ const { emitEvent, calls } = makeEmitEvent();
330
+ await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(true /* interrupted */), emitEvent, makeGlobalConfig());
210
331
  // No test was started; watcher was already interrupted.
211
- expect(onStart).not.toHaveBeenCalled();
332
+ expect(calls).not.toContainEqual(['test-file-start', expect.anything()]);
212
333
  expect(hookNames).toContain('run:finished');
213
334
  });
214
335
  });
@@ -219,11 +340,54 @@ describe('executeRun', () => {
219
340
  metroPort: 8081,
220
341
  resetEnvironmentBetweenTestFiles: true,
221
342
  detectNativeCrashes: false,
343
+ runners: [
344
+ { platformId: 'android', name: 'android' },
345
+ { platformId: 'ios', name: 'ios' },
346
+ ],
222
347
  },
223
348
  });
224
- await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts'), makeTest('/c.ts')], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
349
+ await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts'), makeTest('/c.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
225
350
  // restartApp should be called for tests 2 and 3, not test 1.
226
351
  expect(session.restartApp).toHaveBeenCalledTimes(2);
227
352
  });
228
353
  });
354
+ describe('platform-specific test files', () => {
355
+ it('skips files for other platforms without running them on the device', async () => {
356
+ const session = makeSession();
357
+ const { emitEvent, calls } = makeEmitEvent();
358
+ await executeRun(session, [
359
+ makeTest('/project/smoke.harness.ts'),
360
+ makeTest('/project/kotlin.android.harness.ts'),
361
+ makeTest('/project/swift.ios.harness.ts'),
362
+ ], makeWatcher(), emitEvent, makeGlobalConfig());
363
+ expect(mockRunHarnessTestFile).toHaveBeenCalledTimes(2);
364
+ expect(mockRunHarnessTestFile).toHaveBeenCalledWith(expect.objectContaining({ testPath: '/project/smoke.harness.ts' }));
365
+ expect(mockRunHarnessTestFile).toHaveBeenCalledWith(expect.objectContaining({ testPath: '/project/kotlin.android.harness.ts' }));
366
+ expect(session.ensureAppReady).toHaveBeenCalledTimes(2);
367
+ expect(calls).toContainEqual([
368
+ 'test-file-success',
369
+ expect.objectContaining({ path: '/project/swift.ios.harness.ts' }),
370
+ expect.objectContaining({ skipped: true }),
371
+ ]);
372
+ });
373
+ it('does not restart the app for skipped platform-specific files', async () => {
374
+ const session = makeSession({
375
+ config: {
376
+ metroPort: 8081,
377
+ resetEnvironmentBetweenTestFiles: true,
378
+ detectNativeCrashes: false,
379
+ runners: [
380
+ { platformId: 'android', name: 'android' },
381
+ { platformId: 'ios', name: 'ios' },
382
+ ],
383
+ },
384
+ });
385
+ await executeRun(session, [
386
+ makeTest('/project/swift.ios.harness.ts'),
387
+ makeTest('/project/kotlin.android.harness.ts'),
388
+ ], makeWatcher(), vi.fn(), makeGlobalConfig());
389
+ expect(session.restartApp).not.toHaveBeenCalled();
390
+ expect(mockRunHarnessTestFile).toHaveBeenCalledTimes(1);
391
+ });
392
+ });
229
393
  });
@@ -0,0 +1,7 @@
1
+ import type { HarnessPlatformInitOptions, HarnessPlatformRunner } from '@react-native-harness/platforms';
2
+ import type { Config as HarnessConfig } from '@react-native-harness/config';
3
+ declare global {
4
+ var __HARNESS_TEST_PLATFORM_RUNNER__: HarnessPlatformRunner | undefined;
5
+ }
6
+ export default function mockPlatformRunner(_platformConfig: Record<string, unknown>, _harnessConfig: HarnessConfig, _init: HarnessPlatformInitOptions): Promise<HarnessPlatformRunner>;
7
+ //# sourceMappingURL=mock-platform-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-platform-runner.d.ts","sourceRoot":"","sources":["../../../src/__tests__/fixtures/mock-platform-runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAC1B,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE5E,OAAO,CAAC,MAAM,CAAC;IACb,IAAI,gCAAgC,EAChC,qBAAqB,GACrB,SAAS,CAAC;CACf;AAED,wBAA8B,kBAAkB,CAC9C,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,cAAc,EAAE,aAAa,EAC7B,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,qBAAqB,CAAC,CAMhC"}
@@ -0,0 +1,6 @@
1
+ export default async function mockPlatformRunner(_platformConfig, _harnessConfig, _init) {
2
+ if (!globalThis.__HARNESS_TEST_PLATFORM_RUNNER__) {
3
+ throw new Error('Missing __HARNESS_TEST_PLATFORM_RUNNER__ test fixture');
4
+ }
5
+ return globalThis.__HARNESS_TEST_PLATFORM_RUNNER__;
6
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=harness-session.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness-session.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/harness-session.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { waitForBridgeDisconnectOrTimeout, waitForStartupCrash, } from '../harness-session.js';
3
+ const createConnection = () => ({
4
+ device: {
5
+ platform: 'ios',
6
+ manufacturer: 'Apple',
7
+ model: 'iPhone',
8
+ osVersion: '18.0',
9
+ },
10
+ runTests: vi.fn(),
11
+ });
12
+ const createBridge = (connection) => {
13
+ let currentConnection = connection;
14
+ let disconnectedListener = null;
15
+ return {
16
+ get connection() {
17
+ return currentConnection;
18
+ },
19
+ on: vi.fn((event, listener) => {
20
+ if (event === 'disconnected') {
21
+ disconnectedListener = listener;
22
+ }
23
+ }),
24
+ off: vi.fn((event, listener) => {
25
+ if (event === 'disconnected' && disconnectedListener === listener) {
26
+ disconnectedListener = null;
27
+ }
28
+ }),
29
+ disconnect: () => {
30
+ currentConnection = null;
31
+ disconnectedListener?.();
32
+ },
33
+ };
34
+ };
35
+ describe('waitForBridgeDisconnectOrTimeout', () => {
36
+ it('returns true when the bridge disconnects before the timeout', async () => {
37
+ const connection = createConnection();
38
+ const bridge = createBridge(connection);
39
+ const waitPromise = waitForBridgeDisconnectOrTimeout({
40
+ bridge,
41
+ connection,
42
+ timeoutMs: 50,
43
+ });
44
+ bridge.disconnect();
45
+ await expect(waitPromise).resolves.toBe(true);
46
+ });
47
+ it('returns false when the bridge stays connected through the timeout', async () => {
48
+ const connection = createConnection();
49
+ const bridge = createBridge(connection);
50
+ await expect(waitForBridgeDisconnectOrTimeout({
51
+ bridge,
52
+ connection,
53
+ timeoutMs: 10,
54
+ })).resolves.toBe(false);
55
+ });
56
+ });
57
+ describe('waitForStartupCrash', () => {
58
+ it('does not install a startup crash watch when native crash detection is disabled', async () => {
59
+ const watch = vi.fn();
60
+ const crashMonitor = {
61
+ watch,
62
+ };
63
+ const controller = new AbortController();
64
+ const waitPromise = waitForStartupCrash({
65
+ crashMonitor,
66
+ detectNativeCrashes: false,
67
+ testFilePath: '/test.harness.ts',
68
+ signal: controller.signal,
69
+ });
70
+ controller.abort(new DOMException('Aborted', 'AbortError'));
71
+ await expect(waitPromise).rejects.toThrow('Aborted');
72
+ expect(watch).not.toHaveBeenCalled();
73
+ });
74
+ });
@@ -8,6 +8,7 @@ const resolveUndefined = async () => undefined;
8
8
  const mockSession = {
9
9
  config: { metroPort: 8081 },
10
10
  context: {},
11
+ onTestRunnerEvent: vi.fn(() => () => undefined),
11
12
  ensureAppReady: vi.fn(resolveUndefined),
12
13
  runTestFile: vi.fn(async () => ({
13
14
  name: '',
@@ -41,40 +42,88 @@ const makeTest = () => ({
41
42
  context: { config: {} },
42
43
  });
43
44
  const makeGlobalConfig = (overrides = {}) => ({ rootDir: '/project', watch: false, watchAll: false, collectCoverage: false, ...overrides });
45
+ const typedMockExecuteRun = mockExecuteRun;
46
+ const makeTestCaseStartPayload = () => [
47
+ '/test/example.ts',
48
+ {
49
+ ancestorTitles: [],
50
+ fullName: 'suite case',
51
+ mode: undefined,
52
+ title: 'case',
53
+ startedAt: 1,
54
+ },
55
+ ];
56
+ const makeTestCaseResultPayload = () => [
57
+ '/test/example.ts',
58
+ {
59
+ ancestorTitles: [],
60
+ duration: 1,
61
+ failureDetails: [],
62
+ failureMessages: [],
63
+ fullName: 'suite case',
64
+ numPassingAsserts: 1,
65
+ startedAt: 1,
66
+ status: 'passed',
67
+ title: 'case',
68
+ },
69
+ ];
44
70
  // ---------------------------------------------------------------------------
45
71
  // Tests
46
72
  // ---------------------------------------------------------------------------
47
73
  beforeEach(() => {
48
74
  vi.clearAllMocks();
49
75
  mockSession.dispose.mockResolvedValue(undefined);
50
- mockExecuteRun.mockResolvedValue(undefined);
76
+ typedMockExecuteRun.mockResolvedValue(undefined);
51
77
  mockCreateHarnessSession.mockResolvedValue(mockSession);
52
78
  });
53
79
  describe('JestHarness', () => {
54
80
  describe('session lifecycle', () => {
55
81
  it('creates a session on the first run', async () => {
56
82
  const runner = new JestHarness(makeGlobalConfig());
57
- await runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions());
83
+ await runner.runTests([makeTest()], makeWatcher(), makeOptions());
58
84
  expect(mockCreateHarnessSession).toHaveBeenCalledOnce();
59
85
  });
60
86
  it('reuses the session across runs in watch mode', async () => {
61
87
  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());
88
+ await runner.runTests([makeTest()], makeWatcher(), makeOptions());
89
+ await runner.runTests([makeTest()], makeWatcher(), makeOptions());
64
90
  expect(mockCreateHarnessSession).toHaveBeenCalledOnce();
65
91
  expect(mockSession.dispose).not.toHaveBeenCalled();
66
92
  });
67
93
  it('disposes the session after each run in normal mode', async () => {
68
94
  const runner = new JestHarness(makeGlobalConfig());
69
- await runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions());
95
+ await runner.runTests([makeTest()], makeWatcher(), makeOptions());
70
96
  expect(mockSession.dispose).toHaveBeenCalledOnce();
71
97
  });
72
98
  it('creates a fresh session for each run in normal mode', async () => {
73
99
  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());
100
+ await runner.runTests([makeTest()], makeWatcher(), makeOptions());
101
+ await runner.runTests([makeTest()], makeWatcher(), makeOptions());
76
102
  expect(mockCreateHarnessSession).toHaveBeenCalledTimes(2);
77
103
  });
104
+ it('emits registered Jest events', async () => {
105
+ const runner = new JestHarness(makeGlobalConfig());
106
+ const listener = vi.fn();
107
+ const eventPayload = makeTestCaseStartPayload();
108
+ runner.on('test-case-start', listener);
109
+ typedMockExecuteRun.mockImplementationOnce(async (_session, _tests, _watcher, emitEvent) => {
110
+ await emitEvent('test-case-start', ...eventPayload);
111
+ });
112
+ await runner.runTests([makeTest()], makeWatcher(), makeOptions());
113
+ expect(listener).toHaveBeenCalledWith(eventPayload);
114
+ });
115
+ it('stops emitting after listener unsubscribe', async () => {
116
+ const runner = new JestHarness(makeGlobalConfig());
117
+ const listener = vi.fn();
118
+ const unsubscribe = runner.on('test-case-result', listener);
119
+ const eventPayload = makeTestCaseResultPayload();
120
+ unsubscribe();
121
+ typedMockExecuteRun.mockImplementationOnce(async (_session, _tests, _watcher, emitEvent) => {
122
+ await emitEvent('test-case-result', ...eventPayload);
123
+ });
124
+ await runner.runTests([makeTest()], makeWatcher(), makeOptions());
125
+ expect(listener).not.toHaveBeenCalled();
126
+ });
78
127
  });
79
128
  describe('error handling', () => {
80
129
  it('converts HarnessError into a formatted string before throwing', async () => {
@@ -83,17 +132,17 @@ describe('JestHarness', () => {
83
132
  }
84
133
  mockCreateHarnessSession.mockRejectedValue(new TestHarnessError());
85
134
  const runner = new JestHarness(makeGlobalConfig());
86
- await expect(runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions())).rejects.toBeTypeOf('string');
135
+ await expect(runner.runTests([makeTest()], makeWatcher(), makeOptions())).rejects.toBeTypeOf('string');
87
136
  });
88
137
  it('propagates non-HarnessError exceptions as-is', async () => {
89
138
  const cause = new TypeError('unexpected');
90
139
  mockCreateHarnessSession.mockRejectedValue(cause);
91
140
  const runner = new JestHarness(makeGlobalConfig());
92
- await expect(runner.runTests([makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeOptions())).rejects.toBe(cause);
141
+ await expect(runner.runTests([makeTest()], makeWatcher(), makeOptions())).rejects.toBe(cause);
93
142
  });
94
143
  it('throws when called without serial flag', async () => {
95
144
  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');
145
+ await expect(runner.runTests([makeTest()], makeWatcher(), { serial: false })).rejects.toThrow('Parallel test running is not supported');
97
146
  });
98
147
  });
99
148
  });
@@ -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,12 @@ 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;
16
18
  dispose: () => Promise<void>;
17
19
  };
18
20
  export type CrashMonitorOptions = {
19
- appMonitor: AppMonitor;
20
- platformRunner: HarnessPlatformRunner;
21
+ appSession?: AppSession | null;
21
22
  };
22
- export declare const createCrashMonitor: ({ appMonitor, platformRunner, }: CrashMonitorOptions) => CrashMonitor;
23
+ export declare const createCrashMonitor: ({ appSession: initialAppSession, }?: CrashMonitorOptions) => CrashMonitor;
23
24
  //# 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,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,YAyL7B,CAAC"}