@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,12 +1,25 @@
1
1
  import { describe, expect, it, vi, beforeEach } from 'vitest';
2
2
  import type { Config, Test, TestWatcher } from 'jest-runner';
3
3
  import type { TestResult as JestTestResult } from '@jest/test-result';
4
- import type { TestSuiteResult } from '@react-native-harness/bridge';
4
+ import type {
5
+ TestRunnerTestFinishedEvent,
6
+ TestRunnerTestStartedEvent,
7
+ TestSuiteResult,
8
+ } from '@react-native-harness/bridge';
5
9
  import { NativeCrashError, StartupStallError } from '../errors.js';
6
- import { DeviceNotRespondingError } from '@react-native-harness/bridge/server';
10
+ import {
11
+ AppBridgeDisconnectedError,
12
+ DeviceNotRespondingError,
13
+ } from '@react-native-harness/bridge/server';
7
14
  import type { HarnessSession } from '../harness-session.js';
8
15
  import { executeRun } from '../execute-run.js';
9
16
 
17
+ type EmitEvent = Parameters<typeof executeRun>[3];
18
+ type RecordedEmitEvent = {
19
+ emitEvent: EmitEvent;
20
+ calls: Array<unknown[]>;
21
+ };
22
+
10
23
  const resolveUndefined = async () => undefined;
11
24
 
12
25
  // Mock the file-runner so we control what jestResult/harnessResult each test returns
@@ -85,19 +98,41 @@ const makeSession = (overrides: Partial<HarnessSession> = {}): HarnessSession =>
85
98
  host: undefined,
86
99
  resetEnvironmentBetweenTestFiles: false,
87
100
  detectNativeCrashes: true,
101
+ runners: [
102
+ { platformId: 'android', name: 'android' },
103
+ { platformId: 'ios', name: 'ios' },
104
+ ],
88
105
  } as HarnessSession['config'],
89
- context: {} as HarnessSession['context'],
106
+ context: {
107
+ platform: {
108
+ platformId: 'android',
109
+ name: 'android',
110
+ runner: '/virtual/android-runner.js',
111
+ cli: '/virtual/android-cli.js',
112
+ config: {},
113
+ },
114
+ } as HarnessSession['context'],
90
115
  ensureAppReady: vi.fn(resolveUndefined),
91
116
  runTestFile: vi.fn(async () => makeHarnessResult()),
92
117
  restartApp: vi.fn(resolveUndefined),
93
118
  resetCrashState: vi.fn(),
94
119
  flushClientLogs: vi.fn(() => []),
95
120
  callHook: vi.fn(resolveUndefined),
121
+ onTestRunnerEvent: vi.fn(() => () => undefined),
96
122
  setRunState: vi.fn(),
97
123
  dispose: vi.fn(resolveUndefined),
98
124
  ...overrides,
99
125
  });
100
126
 
127
+ const makeEmitEvent = (): RecordedEmitEvent => {
128
+ const calls: Array<unknown[]> = [];
129
+ const emitEvent: EmitEvent = (async (...eventData) => {
130
+ calls.push(eventData);
131
+ }) as EmitEvent;
132
+
133
+ return { emitEvent, calls };
134
+ };
135
+
101
136
  // ---------------------------------------------------------------------------
102
137
  // Tests
103
138
  // ---------------------------------------------------------------------------
@@ -115,7 +150,7 @@ describe('executeRun', () => {
115
150
  callHook: vi.fn(async (name) => { hookNames.push(name as string); }),
116
151
  });
117
152
 
118
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
153
+ await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
119
154
 
120
155
  expect(hookNames[0]).toBe('run:started');
121
156
  expect(hookNames[hookNames.length - 1]).toBe('run:finished');
@@ -127,7 +162,7 @@ describe('executeRun', () => {
127
162
  callHook: vi.fn(async (name) => { hookNames.push(name as string); }),
128
163
  });
129
164
 
130
- await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
165
+ await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
131
166
 
132
167
  const fileHooks = hookNames.filter((n) =>
133
168
  n === 'test-file:started' || n === 'test-file:finished',
@@ -145,29 +180,41 @@ describe('executeRun', () => {
145
180
  ensureAppReady: vi.fn(async () => { throw new Error('unexpected'); }),
146
181
  });
147
182
 
148
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
183
+ await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
149
184
 
150
185
  expect(hookNames).toContain('run:finished');
151
186
  });
152
187
  });
153
188
 
154
189
  describe('happy path', () => {
155
- it('calls onStart, ensureAppReady, runTestFile, onResult in order', async () => {
190
+ it('emits file start, ensureAppReady, runTestFile, file success in order', async () => {
156
191
  const order: string[] = [];
157
192
  const session = makeSession({
158
193
  ensureAppReady: vi.fn(async () => { order.push('ensureAppReady'); }),
159
194
  });
160
- const onStart = vi.fn(async () => { order.push('onStart'); });
161
- const onResult = vi.fn(async () => { order.push('onResult'); });
195
+ const emitEvent: EmitEvent = (async (eventName, ..._eventData) => {
196
+ if (eventName === 'test-file-start') {
197
+ order.push('test-file-start');
198
+ }
199
+
200
+ if (eventName === 'test-file-success') {
201
+ order.push('test-file-success');
202
+ }
203
+ }) as EmitEvent;
162
204
 
163
205
  mockRunHarnessTestFile.mockImplementation(async () => {
164
206
  order.push('runTestFile');
165
207
  return makeFileRunResult();
166
208
  });
167
209
 
168
- await executeRun(session, [makeTest()], makeWatcher(), onStart, onResult, vi.fn(), makeGlobalConfig());
210
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
169
211
 
170
- expect(order).toEqual(['onStart', 'ensureAppReady', 'runTestFile', 'onResult']);
212
+ expect(order).toEqual([
213
+ 'test-file-start',
214
+ 'ensureAppReady',
215
+ 'runTestFile',
216
+ 'test-file-success',
217
+ ]);
171
218
  });
172
219
 
173
220
  it('accumulates passing test counts in run:finished summary', async () => {
@@ -182,7 +229,7 @@ describe('executeRun', () => {
182
229
  makeFileRunResult({ jestResult: makeJestResult({ numPassingTests: 3 }) }),
183
230
  );
184
231
 
185
- await executeRun(session, [makeTest(), makeTest('/b.ts')], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
232
+ await executeRun(session, [makeTest(), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
186
233
 
187
234
  const payload = finishedPayload as { summary: { passed: number }; status: string };
188
235
  expect(payload.summary.passed).toBe(6);
@@ -196,55 +243,146 @@ describe('executeRun', () => {
196
243
  .mockReturnValueOnce([])
197
244
  .mockReturnValueOnce(clientLogs),
198
245
  });
199
- const onResult = vi.fn();
246
+ const { emitEvent, calls } = makeEmitEvent();
200
247
 
201
248
  await executeRun(
202
249
  session,
203
250
  [makeTest()],
204
251
  makeWatcher(),
205
- vi.fn(),
206
- onResult,
207
- vi.fn(),
252
+ emitEvent,
208
253
  makeGlobalConfig(),
209
254
  );
210
255
 
211
256
  expect(session.flushClientLogs).toHaveBeenCalledTimes(2);
212
- expect(onResult).toHaveBeenCalledWith(
257
+ expect(calls).toContainEqual([
258
+ 'test-file-success',
213
259
  expect.anything(),
214
260
  expect.objectContaining({ console: clientLogs }),
215
- );
261
+ ]);
262
+ });
263
+
264
+ it('emits test-case events before file success', async () => {
265
+ let testRunnerListener:
266
+ | ((event: TestRunnerTestStartedEvent | TestRunnerTestFinishedEvent) => void)
267
+ | undefined;
268
+ const { emitEvent, calls: emittedEvents } = makeEmitEvent();
269
+ const session = makeSession({
270
+ onTestRunnerEvent: vi.fn((listener) => {
271
+ testRunnerListener = listener as typeof testRunnerListener;
272
+ return () => undefined;
273
+ }),
274
+ });
275
+
276
+ mockRunHarnessTestFile.mockImplementation(async () => {
277
+ testRunnerListener?.({
278
+ type: 'test-started',
279
+ file: 'example.ts',
280
+ suite: 'suite',
281
+ name: 'works',
282
+ ancestorTitles: ['suite'],
283
+ fullName: 'suite works',
284
+ startedAt: 10,
285
+ declarationMode: 'only',
286
+ });
287
+ testRunnerListener?.({
288
+ type: 'test-finished',
289
+ file: 'example.ts',
290
+ suite: 'suite',
291
+ name: 'works',
292
+ ancestorTitles: ['suite'],
293
+ fullName: 'suite works',
294
+ startedAt: 10,
295
+ declarationMode: 'only',
296
+ duration: 5,
297
+ status: 'passed',
298
+ });
299
+
300
+ return makeFileRunResult();
301
+ });
302
+
303
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
304
+
305
+ expect(emittedEvents).toEqual([
306
+ ['test-file-start', expect.anything()],
307
+ [
308
+ 'test-case-start',
309
+ 'example.ts',
310
+ expect.objectContaining({
311
+ ancestorTitles: ['suite'],
312
+ fullName: 'suite works',
313
+ mode: 'only',
314
+ title: 'works',
315
+ startedAt: 10,
316
+ }),
317
+ ],
318
+ [
319
+ 'test-case-result',
320
+ 'example.ts',
321
+ expect.objectContaining({
322
+ ancestorTitles: ['suite'],
323
+ fullName: 'suite works',
324
+ numPassingAsserts: 1,
325
+ startedAt: 10,
326
+ status: 'passed',
327
+ title: 'works',
328
+ }),
329
+ ],
330
+ ['test-file-success', expect.anything(), expect.anything()],
331
+ ]);
216
332
  });
217
333
  });
218
334
 
219
335
  describe('runtime failures', () => {
220
336
  it('passes StartupStallError to onFailure with an empty stack', async () => {
221
- const onFailure = vi.fn();
337
+ const { emitEvent, calls } = makeEmitEvent();
222
338
  const session = makeSession({
223
339
  ensureAppReady: vi.fn().mockRejectedValue(new StartupStallError(1500, 3)),
224
340
  });
225
341
 
226
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), vi.fn(), onFailure, makeGlobalConfig());
342
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
227
343
 
228
- expect(onFailure).toHaveBeenCalledWith(
344
+ expect(calls).toContainEqual([
345
+ 'test-file-failure',
229
346
  expect.objectContaining({ path: '/test/example.ts' }),
230
347
  expect.objectContaining({ message: expect.stringContaining('1500'), stack: '' }),
231
- );
348
+ ]);
232
349
  });
233
350
 
234
351
  it('passes DeviceNotRespondingError to onFailure with an empty stack', async () => {
235
- const onFailure = vi.fn();
352
+ const { emitEvent, calls } = makeEmitEvent();
236
353
  const session = makeSession({
237
354
  ensureAppReady: vi.fn().mockRejectedValue(
238
355
  new DeviceNotRespondingError('runTests', []),
239
356
  ),
240
357
  });
241
358
 
242
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), vi.fn(), onFailure, makeGlobalConfig());
359
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
243
360
 
244
- expect(onFailure).toHaveBeenCalledWith(
361
+ expect(calls).toContainEqual([
362
+ 'test-file-failure',
245
363
  expect.anything(),
246
364
  expect.objectContaining({ stack: '' }),
247
- );
365
+ ]);
366
+ });
367
+
368
+ it('passes AppBridgeDisconnectedError to onFailure with an empty stack', async () => {
369
+ const { emitEvent, calls } = makeEmitEvent();
370
+ const session = makeSession({
371
+ ensureAppReady: vi.fn().mockRejectedValue(
372
+ new AppBridgeDisconnectedError('app-disconnected'),
373
+ ),
374
+ });
375
+
376
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
377
+
378
+ expect(calls).toContainEqual([
379
+ 'test-file-failure',
380
+ expect.anything(),
381
+ expect.objectContaining({
382
+ message: expect.stringContaining('The app bridge disconnected during test execution.'),
383
+ stack: '',
384
+ }),
385
+ ]);
248
386
  });
249
387
 
250
388
  it('calls resetCrashState after a NativeCrashError', async () => {
@@ -254,7 +392,7 @@ describe('executeRun', () => {
254
392
  ),
255
393
  });
256
394
 
257
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
395
+ await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
258
396
 
259
397
  expect(session.resetCrashState).toHaveBeenCalled();
260
398
  });
@@ -268,7 +406,7 @@ describe('executeRun', () => {
268
406
  ensureAppReady: vi.fn().mockRejectedValue(new StartupStallError(1000, 2)),
269
407
  });
270
408
 
271
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
409
+ await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
272
410
 
273
411
  const payload = finishedPayload as { summary: { failed: number }; status: string };
274
412
  expect(payload.summary.failed).toBe(1);
@@ -282,20 +420,18 @@ describe('executeRun', () => {
282
420
  const session = makeSession({
283
421
  callHook: vi.fn(async (name) => { hookNames.push(name as string); }),
284
422
  });
285
- const onStart = vi.fn();
423
+ const { emitEvent, calls } = makeEmitEvent();
286
424
 
287
425
  await executeRun(
288
426
  session,
289
427
  [makeTest('/a.ts'), makeTest('/b.ts')],
290
428
  makeWatcher(true /* interrupted */),
291
- onStart,
292
- vi.fn(),
293
- vi.fn(),
429
+ emitEvent,
294
430
  makeGlobalConfig(),
295
431
  );
296
432
 
297
433
  // No test was started; watcher was already interrupted.
298
- expect(onStart).not.toHaveBeenCalled();
434
+ expect(calls).not.toContainEqual(['test-file-start', expect.anything()]);
299
435
  expect(hookNames).toContain('run:finished');
300
436
  });
301
437
  });
@@ -307,6 +443,10 @@ describe('executeRun', () => {
307
443
  metroPort: 8081,
308
444
  resetEnvironmentBetweenTestFiles: true,
309
445
  detectNativeCrashes: false,
446
+ runners: [
447
+ { platformId: 'android', name: 'android' },
448
+ { platformId: 'ios', name: 'ios' },
449
+ ],
310
450
  } as HarnessSession['config'],
311
451
  });
312
452
 
@@ -314,7 +454,7 @@ describe('executeRun', () => {
314
454
  session,
315
455
  [makeTest('/a.ts'), makeTest('/b.ts'), makeTest('/c.ts')],
316
456
  makeWatcher(),
317
- vi.fn(), vi.fn(), vi.fn(),
457
+ makeEmitEvent().emitEvent,
318
458
  makeGlobalConfig(),
319
459
  );
320
460
 
@@ -322,4 +462,65 @@ describe('executeRun', () => {
322
462
  expect(session.restartApp).toHaveBeenCalledTimes(2);
323
463
  });
324
464
  });
465
+
466
+ describe('platform-specific test files', () => {
467
+ it('skips files for other platforms without running them on the device', async () => {
468
+ const session = makeSession();
469
+ const { emitEvent, calls } = makeEmitEvent();
470
+
471
+ await executeRun(
472
+ session,
473
+ [
474
+ makeTest('/project/smoke.harness.ts'),
475
+ makeTest('/project/kotlin.android.harness.ts'),
476
+ makeTest('/project/swift.ios.harness.ts'),
477
+ ],
478
+ makeWatcher(),
479
+ emitEvent,
480
+ makeGlobalConfig(),
481
+ );
482
+
483
+ expect(mockRunHarnessTestFile).toHaveBeenCalledTimes(2);
484
+ expect(mockRunHarnessTestFile).toHaveBeenCalledWith(
485
+ expect.objectContaining({ testPath: '/project/smoke.harness.ts' }),
486
+ );
487
+ expect(mockRunHarnessTestFile).toHaveBeenCalledWith(
488
+ expect.objectContaining({ testPath: '/project/kotlin.android.harness.ts' }),
489
+ );
490
+ expect(session.ensureAppReady).toHaveBeenCalledTimes(2);
491
+ expect(calls).toContainEqual([
492
+ 'test-file-success',
493
+ expect.objectContaining({ path: '/project/swift.ios.harness.ts' }),
494
+ expect.objectContaining({ skipped: true }),
495
+ ]);
496
+ });
497
+
498
+ it('does not restart the app for skipped platform-specific files', async () => {
499
+ const session = makeSession({
500
+ config: {
501
+ metroPort: 8081,
502
+ resetEnvironmentBetweenTestFiles: true,
503
+ detectNativeCrashes: false,
504
+ runners: [
505
+ { platformId: 'android', name: 'android' },
506
+ { platformId: 'ios', name: 'ios' },
507
+ ],
508
+ } as HarnessSession['config'],
509
+ });
510
+
511
+ await executeRun(
512
+ session,
513
+ [
514
+ makeTest('/project/swift.ios.harness.ts'),
515
+ makeTest('/project/kotlin.android.harness.ts'),
516
+ ],
517
+ makeWatcher(),
518
+ vi.fn(),
519
+ makeGlobalConfig(),
520
+ );
521
+
522
+ expect(session.restartApp).not.toHaveBeenCalled();
523
+ expect(mockRunHarnessTestFile).toHaveBeenCalledTimes(1);
524
+ });
525
+ });
325
526
  });
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { AppConnection } from '@react-native-harness/bridge/server';
3
+ import {
4
+ waitForBridgeDisconnectOrTimeout,
5
+ waitForStartupCrash,
6
+ } from '../harness-session.js';
7
+ import type { CrashMonitor } from '../crash-monitor.js';
8
+
9
+ const createConnection = (): AppConnection => ({
10
+ device: {
11
+ platform: 'ios',
12
+ manufacturer: 'Apple',
13
+ model: 'iPhone',
14
+ osVersion: '18.0',
15
+ },
16
+ runTests: vi.fn(),
17
+ });
18
+
19
+ const createBridge = (connection: AppConnection | null) => {
20
+ let currentConnection = connection;
21
+ let disconnectedListener: (() => void) | null = null;
22
+
23
+ return {
24
+ get connection() {
25
+ return currentConnection;
26
+ },
27
+ on: vi.fn((event: 'disconnected', listener: () => void) => {
28
+ if (event === 'disconnected') {
29
+ disconnectedListener = listener;
30
+ }
31
+ }),
32
+ off: vi.fn((event: 'disconnected', listener: () => void) => {
33
+ if (event === 'disconnected' && disconnectedListener === listener) {
34
+ disconnectedListener = null;
35
+ }
36
+ }),
37
+ disconnect: () => {
38
+ currentConnection = null;
39
+ disconnectedListener?.();
40
+ },
41
+ };
42
+ };
43
+
44
+ describe('waitForBridgeDisconnectOrTimeout', () => {
45
+ it('returns true when the bridge disconnects before the timeout', async () => {
46
+ const connection = createConnection();
47
+ const bridge = createBridge(connection);
48
+
49
+ const waitPromise = waitForBridgeDisconnectOrTimeout({
50
+ bridge,
51
+ connection,
52
+ timeoutMs: 50,
53
+ });
54
+
55
+ bridge.disconnect();
56
+
57
+ await expect(waitPromise).resolves.toBe(true);
58
+ });
59
+
60
+ it('returns false when the bridge stays connected through the timeout', async () => {
61
+ const connection = createConnection();
62
+ const bridge = createBridge(connection);
63
+
64
+ await expect(
65
+ waitForBridgeDisconnectOrTimeout({
66
+ bridge,
67
+ connection,
68
+ timeoutMs: 10,
69
+ }),
70
+ ).resolves.toBe(false);
71
+ });
72
+ });
73
+
74
+ describe('waitForStartupCrash', () => {
75
+ it('does not install a startup crash watch when native crash detection is disabled', async () => {
76
+ const watch = vi.fn();
77
+ const crashMonitor = {
78
+ watch,
79
+ } as unknown as CrashMonitor;
80
+ const controller = new AbortController();
81
+ const waitPromise = waitForStartupCrash({
82
+ crashMonitor,
83
+ detectNativeCrashes: false,
84
+ testFilePath: '/test.harness.ts',
85
+ signal: controller.signal,
86
+ });
87
+
88
+ controller.abort(new DOMException('Aborted', 'AbortError'));
89
+
90
+ await expect(waitPromise).rejects.toThrow('Aborted');
91
+ expect(watch).not.toHaveBeenCalled();
92
+ });
93
+ });