@react-native-harness/jest 1.3.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 (49) 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__/harness-session.test.js +63 -239
  6. package/dist/__tests__/index.test.js +59 -10
  7. package/dist/__tests__/test-file-platform-filter.test.d.ts +2 -0
  8. package/dist/__tests__/test-file-platform-filter.test.d.ts.map +1 -0
  9. package/dist/__tests__/test-file-platform-filter.test.js +47 -0
  10. package/dist/crash-monitor.d.ts +5 -4
  11. package/dist/crash-monitor.d.ts.map +1 -1
  12. package/dist/crash-monitor.js +116 -102
  13. package/dist/errors.d.ts +10 -0
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +41 -2
  16. package/dist/execute-run.d.ts +4 -2
  17. package/dist/execute-run.d.ts.map +1 -1
  18. package/dist/execute-run.js +85 -7
  19. package/dist/harness-session.d.ts +21 -1
  20. package/dist/harness-session.d.ts.map +1 -1
  21. package/dist/harness-session.js +134 -39
  22. package/dist/index.d.ts +7 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +24 -2
  25. package/dist/run.d.ts.map +1 -1
  26. package/dist/run.js +1 -0
  27. package/dist/test-file-platform-filter.d.ts +5 -0
  28. package/dist/test-file-platform-filter.d.ts.map +1 -0
  29. package/dist/test-file-platform-filter.js +40 -0
  30. package/dist/toTestResult.d.ts +1 -0
  31. package/dist/toTestResult.d.ts.map +1 -1
  32. package/dist/toTestResult.js +2 -2
  33. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  34. package/package.json +7 -7
  35. package/src/__tests__/bridge.test.ts +37 -2
  36. package/src/__tests__/crash-monitor.test.ts +182 -231
  37. package/src/__tests__/errors.test.ts +75 -0
  38. package/src/__tests__/execute-run.test.ts +235 -34
  39. package/src/__tests__/harness-session.test.ts +93 -0
  40. package/src/__tests__/index.test.ts +89 -11
  41. package/src/__tests__/test-file-platform-filter.test.ts +83 -0
  42. package/src/crash-monitor.ts +192 -131
  43. package/src/errors.ts +62 -1
  44. package/src/execute-run.ts +125 -11
  45. package/src/harness-session.ts +177 -42
  46. package/src/index.ts +40 -11
  47. package/src/run.ts +1 -0
  48. package/src/test-file-platform-filter.ts +57 -0
  49. 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
  });
@@ -1,250 +1,74 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { createHarnessSession } from '../harness-session.js';
3
- const pluginManagerMocks = vi.hoisted(() => ({
4
- callHook: vi.fn(async () => undefined),
5
- }));
6
- const getConfigMock = vi.hoisted(() => vi.fn());
7
- const resolveHarnessMetroPortMock = vi.hoisted(() => vi.fn());
8
- const getMetroInstanceMock = vi.hoisted(() => vi.fn());
9
- const createHarnessBridgeMock = vi.hoisted(() => vi.fn());
10
- const createCrashMonitorMock = vi.hoisted(() => vi.fn());
11
- vi.mock('@react-native-harness/plugins', async () => {
12
- const actual = await vi.importActual('@react-native-harness/plugins');
13
- return {
14
- ...actual,
15
- createHarnessPluginManager: vi.fn(() => ({
16
- callHook: pluginManagerMocks.callHook,
17
- })),
18
- };
19
- });
20
- vi.mock('@react-native-harness/config', async () => {
21
- const actual = await vi.importActual('@react-native-harness/config');
22
- return {
23
- ...actual,
24
- getConfig: getConfigMock,
25
- };
26
- });
27
- vi.mock('../metro-port.js', async () => {
28
- const actual = await vi.importActual('../metro-port.js');
29
- return {
30
- ...actual,
31
- resolveHarnessMetroPort: resolveHarnessMetroPortMock,
32
- };
33
- });
34
- vi.mock('@react-native-harness/bundler-metro', async () => {
35
- const actual = await vi.importActual('@react-native-harness/bundler-metro');
36
- return {
37
- ...actual,
38
- getMetroInstance: getMetroInstanceMock,
39
- };
40
- });
41
- vi.mock('@react-native-harness/bridge/server', async () => {
42
- const actual = await vi.importActual('@react-native-harness/bridge/server');
43
- return {
44
- ...actual,
45
- createHarnessBridge: createHarnessBridgeMock,
46
- };
47
- });
48
- vi.mock('../crash-monitor.js', async () => {
49
- const actual = await vi.importActual('../crash-monitor.js');
50
- return {
51
- ...actual,
52
- createCrashMonitor: createCrashMonitorMock,
53
- };
54
- });
55
- const createAppMonitor = () => ({
56
- start: vi.fn(async () => undefined),
57
- stop: vi.fn(async () => undefined),
58
- dispose: vi.fn(async () => undefined),
59
- addListener: vi.fn(),
60
- removeListener: vi.fn(),
61
- });
62
- const makeBridge = () => ({
63
- ws: {},
64
- connection: null,
65
- on: vi.fn(),
66
- off: vi.fn(),
67
- nextConnection: vi.fn(),
68
- dispose: vi.fn(async () => undefined),
69
- });
70
- const makeMetroInstance = () => ({
71
- events: {
72
- addListener: vi.fn(),
73
- removeListener: vi.fn(),
74
- },
75
- waitUntilHealthy: vi.fn(async () => 'packager-status:running'),
76
- prewarm: vi.fn(async () => false),
77
- dispose: vi.fn(async () => undefined),
78
- });
79
- const makeLockManager = () => ({
80
- acquire: vi.fn(async () => ({
81
- release: vi.fn(async () => undefined),
82
- })),
83
- });
84
- const makeCrashMonitor = () => ({
85
- watch: vi.fn(),
86
- isAlive: vi.fn(() => false),
87
- stop: vi.fn(async () => undefined),
88
- start: vi.fn(async () => undefined),
89
- reset: vi.fn(),
90
- dispose: vi.fn(async () => undefined),
91
- });
92
- const makeDeviceState = () => ({
93
- permissions: {
94
- apply: vi.fn(async () => ({ mutation: null })),
95
- revert: vi.fn(),
96
- resetOutstanding: vi.fn(async () => undefined),
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',
97
9
  },
10
+ runTests: vi.fn(),
98
11
  });
99
- const makePlatformRunner = (overrides = {}) => ({
100
- startApp: vi.fn(async () => undefined),
101
- restartApp: vi.fn(async () => undefined),
102
- stopApp: vi.fn(async () => undefined),
103
- dispose: vi.fn(async () => undefined),
104
- isAppRunning: vi.fn(async () => false),
105
- createAppMonitor: vi.fn(() => createAppMonitor()),
106
- ...overrides,
107
- });
108
- const globalConfig = {
109
- rootDir: '/project',
110
- watch: false,
111
- watchAll: false,
112
- collectCoverage: false,
113
- };
114
- const harnessConfig = {
115
- entryPoint: 'index.js',
116
- appRegistryComponentName: 'App',
117
- runners: [
118
- {
119
- name: 'ios',
120
- platformId: 'ios',
121
- runner: './__tests__/fixtures/mock-platform-runner.ts',
122
- config: {},
12
+ const createBridge = (connection) => {
13
+ let currentConnection = connection;
14
+ let disconnectedListener = null;
15
+ return {
16
+ get connection() {
17
+ return currentConnection;
123
18
  },
124
- ],
125
- defaultRunner: 'ios',
126
- metroPort: 8081,
127
- bridgeTimeout: 1000,
128
- platformReadyTimeout: 1000,
129
- bundleStartTimeout: 1000,
130
- maxAppRestarts: 1,
131
- resetEnvironmentBetweenTestFiles: true,
132
- detectNativeCrashes: false,
133
- forwardClientLogs: false,
134
- permissions: false,
135
- plugins: [],
136
- unstable__skipAlreadyIncludedModules: false,
137
- unstable__enableMetroCache: false,
138
- disableViewFlattening: false,
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
+ };
139
34
  };
140
- describe('createHarnessSession permission cleanup', () => {
141
- let bridge;
142
- let metroInstance;
143
- let crashMonitor;
144
- let lockManager;
145
- beforeEach(() => {
146
- vi.clearAllMocks();
147
- bridge = makeBridge();
148
- metroInstance = makeMetroInstance();
149
- crashMonitor = makeCrashMonitor();
150
- lockManager = makeLockManager();
151
- getConfigMock.mockResolvedValue({ config: harnessConfig });
152
- resolveHarnessMetroPortMock.mockResolvedValue({
153
- config: harnessConfig,
154
- initialMetroPort: 8081,
155
- didFallback: false,
156
- metroPortLease: { release: vi.fn(async () => undefined) },
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,
157
43
  });
158
- getMetroInstanceMock.mockResolvedValue(metroInstance);
159
- createHarnessBridgeMock.mockResolvedValue(bridge);
160
- createCrashMonitorMock.mockReturnValue(crashMonitor);
44
+ bridge.disconnect();
45
+ await expect(waitPromise).resolves.toBe(true);
161
46
  });
162
- afterEach(() => {
163
- delete globalThis.__HARNESS_TEST_PLATFORM_RUNNER__;
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);
164
55
  });
165
- it('resets outstanding permission mutations before restart between test files', async () => {
166
- const deviceState = makeDeviceState();
167
- const platformRunner = makePlatformRunner({
168
- deviceState,
169
- isAppRunning: vi.fn(async () => true),
170
- });
171
- bridge.connection = {};
172
- crashMonitor.isAlive.mockReturnValue(true);
173
- globalThis.__HARNESS_TEST_PLATFORM_RUNNER__ = platformRunner;
174
- const session = await createHarnessSession(globalConfig, { lockManager });
175
- const callOrder = [];
176
- deviceState.permissions.resetOutstanding.mockImplementation(async () => {
177
- callOrder.push('resetOutstanding');
178
- });
179
- platformRunner.stopApp = vi.fn(async () => {
180
- callOrder.push('stopApp');
181
- });
182
- await session.restartApp('/test/example.ts');
183
- expect(deviceState.permissions.resetOutstanding).toHaveBeenCalledTimes(1);
184
- expect(deviceState.permissions.apply).not.toHaveBeenCalled();
185
- expect(callOrder).toEqual(['resetOutstanding', 'stopApp']);
186
- await session.dispose();
187
- });
188
- it('reapplies configured permissions after cleanup before restart between test files', async () => {
189
- const deviceState = makeDeviceState();
190
- const platformRunner = makePlatformRunner({
191
- deviceState,
192
- isAppRunning: vi.fn(async () => true),
193
- });
194
- bridge.connection = {};
195
- crashMonitor.isAlive.mockReturnValue(true);
196
- globalThis.__HARNESS_TEST_PLATFORM_RUNNER__ = platformRunner;
197
- const permissionsEnabledConfig = {
198
- ...harnessConfig,
199
- permissions: true,
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,
200
62
  };
201
- getConfigMock.mockResolvedValue({ config: permissionsEnabledConfig });
202
- resolveHarnessMetroPortMock.mockResolvedValue({
203
- config: permissionsEnabledConfig,
204
- initialMetroPort: 8081,
205
- didFallback: false,
206
- metroPortLease: { release: vi.fn(async () => undefined) },
207
- });
208
- const session = await createHarnessSession(globalConfig, { lockManager });
209
- const callOrder = [];
210
- deviceState.permissions.apply.mockClear();
211
- deviceState.permissions.resetOutstanding.mockImplementation(async () => {
212
- callOrder.push('resetOutstanding');
213
- });
214
- deviceState.permissions.apply.mockImplementation(async () => {
215
- callOrder.push('applyConfiguredPermissions');
216
- return { mutation: null };
217
- });
218
- platformRunner.stopApp = vi.fn(async () => {
219
- callOrder.push('stopApp');
220
- });
221
- await session.restartApp('/test/example.ts');
222
- expect(deviceState.permissions.resetOutstanding).toHaveBeenCalledTimes(1);
223
- expect(deviceState.permissions.apply).toHaveBeenCalledWith({
224
- kind: 'permission-all',
225
- decision: 'grant',
226
- });
227
- expect(callOrder).toEqual([
228
- 'resetOutstanding',
229
- 'applyConfiguredPermissions',
230
- 'stopApp',
231
- ]);
232
- await session.dispose();
233
- });
234
- it('resets outstanding permission mutations during dispose', async () => {
235
- const deviceState = makeDeviceState();
236
- const callOrder = [];
237
- const platformRunner = makePlatformRunner({ deviceState });
238
- deviceState.permissions.resetOutstanding.mockImplementation(async () => {
239
- callOrder.push('resetOutstanding');
240
- });
241
- platformRunner.dispose = vi.fn(async () => {
242
- callOrder.push('dispose');
63
+ const controller = new AbortController();
64
+ const waitPromise = waitForStartupCrash({
65
+ crashMonitor,
66
+ detectNativeCrashes: false,
67
+ testFilePath: '/test.harness.ts',
68
+ signal: controller.signal,
243
69
  });
244
- globalThis.__HARNESS_TEST_PLATFORM_RUNNER__ = platformRunner;
245
- const session = await createHarnessSession(globalConfig, { lockManager });
246
- await session.dispose();
247
- expect(deviceState.permissions.resetOutstanding).toHaveBeenCalledTimes(1);
248
- expect(callOrder).toEqual(['resetOutstanding', 'dispose']);
70
+ controller.abort(new DOMException('Aborted', 'AbortError'));
71
+ await expect(waitPromise).rejects.toThrow('Aborted');
72
+ expect(watch).not.toHaveBeenCalled();
249
73
  });
250
74
  });