@react-native-harness/jest 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/__tests__/bridge.test.js +29 -4
  2. package/dist/__tests__/crash-monitor.test.js +194 -176
  3. package/dist/__tests__/environment-reset.test.d.ts +2 -0
  4. package/dist/__tests__/environment-reset.test.d.ts.map +1 -0
  5. package/dist/__tests__/environment-reset.test.js +238 -0
  6. package/dist/__tests__/errors.test.js +39 -0
  7. package/dist/__tests__/execute-run.test.js +439 -29
  8. package/dist/__tests__/format-harness-error.test.d.ts +2 -0
  9. package/dist/__tests__/format-harness-error.test.d.ts.map +1 -0
  10. package/dist/__tests__/format-harness-error.test.js +27 -0
  11. package/dist/__tests__/harness-cache.test.js +62 -41
  12. package/dist/__tests__/harness-session-signal.test.d.ts +2 -0
  13. package/dist/__tests__/harness-session-signal.test.d.ts.map +1 -0
  14. package/dist/__tests__/harness-session-signal.test.js +298 -0
  15. package/dist/__tests__/harness-session.test.js +217 -234
  16. package/dist/__tests__/index.test.js +62 -11
  17. package/dist/__tests__/metro-port.test.js +2 -0
  18. package/dist/__tests__/run.test.d.ts +2 -0
  19. package/dist/__tests__/run.test.d.ts.map +1 -0
  20. package/dist/__tests__/run.test.js +48 -0
  21. package/dist/__tests__/test-file-platform-filter.test.d.ts +2 -0
  22. package/dist/__tests__/test-file-platform-filter.test.d.ts.map +1 -0
  23. package/dist/__tests__/test-file-platform-filter.test.js +47 -0
  24. package/dist/crash-monitor.d.ts +12 -4
  25. package/dist/crash-monitor.d.ts.map +1 -1
  26. package/dist/crash-monitor.js +135 -102
  27. package/dist/diagnostics/__tests__/derivers.test.d.ts +2 -0
  28. package/dist/diagnostics/__tests__/derivers.test.d.ts.map +1 -0
  29. package/dist/diagnostics/__tests__/derivers.test.js +248 -0
  30. package/dist/diagnostics/__tests__/report.test.d.ts +2 -0
  31. package/dist/diagnostics/__tests__/report.test.d.ts.map +1 -0
  32. package/dist/diagnostics/__tests__/report.test.js +97 -0
  33. package/dist/diagnostics/derivers.d.ts +24 -0
  34. package/dist/diagnostics/derivers.d.ts.map +1 -0
  35. package/dist/diagnostics/derivers.js +146 -0
  36. package/dist/diagnostics/index.d.ts +3 -0
  37. package/dist/diagnostics/index.d.ts.map +1 -0
  38. package/dist/diagnostics/index.js +2 -0
  39. package/dist/diagnostics/report.d.ts +7 -0
  40. package/dist/diagnostics/report.d.ts.map +1 -0
  41. package/dist/diagnostics/report.js +84 -0
  42. package/dist/environment-reset.d.ts +51 -0
  43. package/dist/environment-reset.d.ts.map +1 -0
  44. package/dist/environment-reset.js +83 -0
  45. package/dist/errors.d.ts +10 -0
  46. package/dist/errors.d.ts.map +1 -1
  47. package/dist/errors.js +41 -2
  48. package/dist/execute-run.d.ts +4 -2
  49. package/dist/execute-run.d.ts.map +1 -1
  50. package/dist/execute-run.js +179 -49
  51. package/dist/format-harness-error.d.ts +5 -0
  52. package/dist/format-harness-error.d.ts.map +1 -0
  53. package/dist/format-harness-error.js +43 -0
  54. package/dist/harness-session.d.ts +46 -1
  55. package/dist/harness-session.d.ts.map +1 -1
  56. package/dist/harness-session.js +386 -118
  57. package/dist/index.d.ts +7 -3
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +24 -2
  60. package/dist/run.d.ts.map +1 -1
  61. package/dist/run.js +22 -4
  62. package/dist/test-file-platform-filter.d.ts +5 -0
  63. package/dist/test-file-platform-filter.d.ts.map +1 -0
  64. package/dist/test-file-platform-filter.js +40 -0
  65. package/dist/toTestResult.d.ts +1 -0
  66. package/dist/toTestResult.d.ts.map +1 -1
  67. package/dist/toTestResult.js +2 -2
  68. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  69. package/package.json +8 -7
  70. package/src/__tests__/bridge.test.ts +45 -5
  71. package/src/__tests__/crash-monitor.test.ts +234 -199
  72. package/src/__tests__/environment-reset.test.ts +308 -0
  73. package/src/__tests__/errors.test.ts +75 -0
  74. package/src/__tests__/execute-run.test.ts +561 -37
  75. package/src/__tests__/format-harness-error.test.ts +32 -0
  76. package/src/__tests__/harness-cache.test.ts +91 -0
  77. package/src/__tests__/harness-session.test.ts +303 -0
  78. package/src/__tests__/index.test.ts +92 -12
  79. package/src/__tests__/metro-port.test.ts +2 -0
  80. package/src/__tests__/run.test.ts +74 -0
  81. package/src/__tests__/test-file-platform-filter.test.ts +83 -0
  82. package/src/crash-monitor.ts +220 -131
  83. package/src/diagnostics/__tests__/derivers.test.ts +300 -0
  84. package/src/diagnostics/__tests__/report.test.ts +128 -0
  85. package/src/diagnostics/derivers.ts +170 -0
  86. package/src/diagnostics/index.ts +2 -0
  87. package/src/diagnostics/report.ts +124 -0
  88. package/src/environment-reset.ts +138 -0
  89. package/src/errors.ts +62 -1
  90. package/src/execute-run.ts +230 -57
  91. package/src/format-harness-error.ts +73 -0
  92. package/src/harness-session.ts +535 -162
  93. package/src/index.ts +40 -11
  94. package/src/run.ts +24 -4
  95. package/src/test-file-platform-filter.ts +57 -0
  96. package/src/toTestResult.ts +3 -2
  97. package/tsconfig.json +3 -0
  98. package/tsconfig.lib.json +3 -0
@@ -1,6 +1,7 @@
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
+ import { createDiagnostics } from '@react-native-harness/tools';
4
5
  import { executeRun } from '../execute-run.js';
5
6
  const resolveUndefined = async () => undefined;
6
7
  // Mock the file-runner so we control what jestResult/harnessResult each test returns
@@ -9,6 +10,15 @@ const mockRunHarnessTestFile = vi.hoisted(() => vi.fn());
9
10
  vi.mock('../run.js', () => ({
10
11
  runHarnessTestFile: mockRunHarnessTestFile,
11
12
  }));
13
+ // Mock the reporting module so diagnostics tests can inspect the entries that
14
+ // would have been reported without touching the real filesystem, and so
15
+ // asserting on them doesn't race with executeRun's own diagnostics.flush().
16
+ const mockPrintSummary = vi.hoisted(() => vi.fn());
17
+ const mockWriteTraceFile = vi.hoisted(() => vi.fn(async () => '/fake/trace.json'));
18
+ vi.mock('../diagnostics/index.js', () => ({
19
+ printSummary: mockPrintSummary,
20
+ writeTraceFile: mockWriteTraceFile,
21
+ }));
12
22
  // ---------------------------------------------------------------------------
13
23
  // Helpers
14
24
  // ---------------------------------------------------------------------------
@@ -66,18 +76,40 @@ const makeSession = (overrides = {}) => ({
66
76
  host: undefined,
67
77
  resetEnvironmentBetweenTestFiles: false,
68
78
  detectNativeCrashes: true,
79
+ runners: [
80
+ { platformId: 'android', name: 'android' },
81
+ { platformId: 'ios', name: 'ios' },
82
+ ],
83
+ },
84
+ context: {
85
+ platform: {
86
+ platformId: 'android',
87
+ name: 'android',
88
+ runner: '/virtual/android-runner.js',
89
+ cli: '/virtual/android-cli.js',
90
+ config: {},
91
+ },
69
92
  },
70
- context: {},
93
+ diagnostics: createDiagnostics({ enabled: false }),
71
94
  ensureAppReady: vi.fn(resolveUndefined),
72
95
  runTestFile: vi.fn(async () => makeHarnessResult()),
73
96
  restartApp: vi.fn(resolveUndefined),
97
+ resetEnvironment: vi.fn(resolveUndefined),
74
98
  resetCrashState: vi.fn(),
75
99
  flushClientLogs: vi.fn(() => []),
76
100
  callHook: vi.fn(resolveUndefined),
101
+ onTestRunnerEvent: vi.fn(() => () => undefined),
77
102
  setRunState: vi.fn(),
78
103
  dispose: vi.fn(resolveUndefined),
79
104
  ...overrides,
80
105
  });
106
+ const makeEmitEvent = () => {
107
+ const calls = [];
108
+ const emitEvent = (async (...eventData) => {
109
+ calls.push(eventData);
110
+ });
111
+ return { emitEvent, calls };
112
+ };
81
113
  // ---------------------------------------------------------------------------
82
114
  // Tests
83
115
  // ---------------------------------------------------------------------------
@@ -92,7 +124,7 @@ describe('executeRun', () => {
92
124
  const session = makeSession({
93
125
  callHook: vi.fn(async (name) => { hookNames.push(name); }),
94
126
  });
95
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
127
+ await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
96
128
  expect(hookNames[0]).toBe('run:started');
97
129
  expect(hookNames[hookNames.length - 1]).toBe('run:finished');
98
130
  });
@@ -101,7 +133,7 @@ describe('executeRun', () => {
101
133
  const session = makeSession({
102
134
  callHook: vi.fn(async (name) => { hookNames.push(name); }),
103
135
  });
104
- await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
136
+ await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
105
137
  const fileHooks = hookNames.filter((n) => n === 'test-file:started' || n === 'test-file:finished');
106
138
  expect(fileHooks).toEqual([
107
139
  'test-file:started', 'test-file:finished',
@@ -114,24 +146,79 @@ describe('executeRun', () => {
114
146
  callHook: vi.fn(async (name) => { hookNames.push(name); }),
115
147
  ensureAppReady: vi.fn(async () => { throw new Error('unexpected'); }),
116
148
  });
117
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
149
+ await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
118
150
  expect(hookNames).toContain('run:finished');
119
151
  });
120
152
  });
153
+ describe('diagnostics', () => {
154
+ beforeEach(() => {
155
+ mockPrintSummary.mockClear();
156
+ mockWriteTraceFile.mockClear();
157
+ });
158
+ it('records run.total and run.file spans when diagnostics is enabled', async () => {
159
+ const diagnostics = createDiagnostics({ enabled: true });
160
+ const session = makeSession({ diagnostics });
161
+ await executeRun(session, [makeTest('/a.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
162
+ // executeRun flushes and hands entries to the (mocked) reporter in its
163
+ // finally block, so we inspect what was reported rather than flushing
164
+ // the diagnostics instance ourselves.
165
+ expect(mockPrintSummary).toHaveBeenCalledTimes(1);
166
+ const entries = mockPrintSummary.mock.calls[0]?.[0];
167
+ const runEntry = entries.find((e) => e.label === 'run.total');
168
+ const fileEntry = entries.find((e) => e.label === 'run.file');
169
+ expect(runEntry).toMatchObject({ status: 'ok', attrs: { status: 'passed' } });
170
+ expect(runEntry?.attrs?.runId).toBeTypeOf('string');
171
+ expect(fileEntry).toMatchObject({ status: 'ok', attrs: { file: '../a.ts', status: 'passed' } });
172
+ expect(fileEntry?.attrs?.runId).toBeTypeOf('string');
173
+ expect(mockWriteTraceFile).toHaveBeenCalledWith(entries, expect.objectContaining({ runId: expect.any(String) }));
174
+ });
175
+ it('still records run.total when a test throws', async () => {
176
+ const diagnostics = createDiagnostics({ enabled: true });
177
+ const session = makeSession({
178
+ diagnostics,
179
+ ensureAppReady: vi.fn(async () => { throw new Error('unexpected'); }),
180
+ });
181
+ await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
182
+ const entries = mockPrintSummary.mock.calls[0]?.[0];
183
+ expect(entries.find((e) => e.label === 'run.total')).toBeDefined();
184
+ expect(entries.find((e) => e.label === 'run.file')).toMatchObject({
185
+ attrs: { status: 'failed' },
186
+ });
187
+ });
188
+ it('is a no-op and does not generate a report when diagnostics is disabled', async () => {
189
+ const diagnostics = createDiagnostics({ enabled: false });
190
+ const session = makeSession({ diagnostics });
191
+ await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
192
+ expect(diagnostics.flush()).toEqual([]);
193
+ expect(mockPrintSummary).not.toHaveBeenCalled();
194
+ expect(mockWriteTraceFile).not.toHaveBeenCalled();
195
+ });
196
+ });
121
197
  describe('happy path', () => {
122
- it('calls onStart, ensureAppReady, runTestFile, onResult in order', async () => {
198
+ it('emits file start, ensureAppReady, runTestFile, file success in order', async () => {
123
199
  const order = [];
124
200
  const session = makeSession({
125
201
  ensureAppReady: vi.fn(async () => { order.push('ensureAppReady'); }),
126
202
  });
127
- const onStart = vi.fn(async () => { order.push('onStart'); });
128
- const onResult = vi.fn(async () => { order.push('onResult'); });
203
+ const emitEvent = (async (eventName) => {
204
+ if (eventName === 'test-file-start') {
205
+ order.push('test-file-start');
206
+ }
207
+ if (eventName === 'test-file-success') {
208
+ order.push('test-file-success');
209
+ }
210
+ });
129
211
  mockRunHarnessTestFile.mockImplementation(async () => {
130
212
  order.push('runTestFile');
131
213
  return makeFileRunResult();
132
214
  });
133
- await executeRun(session, [makeTest()], makeWatcher(), onStart, onResult, vi.fn(), makeGlobalConfig());
134
- expect(order).toEqual(['onStart', 'ensureAppReady', 'runTestFile', 'onResult']);
215
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
216
+ expect(order).toEqual([
217
+ 'test-file-start',
218
+ 'ensureAppReady',
219
+ 'runTestFile',
220
+ 'test-file-success',
221
+ ]);
135
222
  });
136
223
  it('accumulates passing test counts in run:finished summary', async () => {
137
224
  let finishedPayload;
@@ -142,11 +229,19 @@ describe('executeRun', () => {
142
229
  }),
143
230
  });
144
231
  mockRunHarnessTestFile.mockResolvedValue(makeFileRunResult({ jestResult: makeJestResult({ numPassingTests: 3 }) }));
145
- 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());
146
233
  const payload = finishedPayload;
147
234
  expect(payload.summary.passed).toBe(6);
148
235
  expect(payload.status).toBe('passed');
149
236
  });
237
+ it('marks the final run state as completed', async () => {
238
+ const session = makeSession();
239
+ await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
240
+ expect(session.setRunState).toHaveBeenLastCalledWith(expect.objectContaining({
241
+ completed: true,
242
+ status: 'passed',
243
+ }));
244
+ });
150
245
  it('attaches buffered client logs to the Jest result', async () => {
151
246
  const clientLogs = [{ message: 'Loaded screen', origin: '', type: 'warn' }];
152
247
  const session = makeSession({
@@ -154,34 +249,174 @@ describe('executeRun', () => {
154
249
  .mockReturnValueOnce([])
155
250
  .mockReturnValueOnce(clientLogs),
156
251
  });
157
- const onResult = vi.fn();
158
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), onResult, vi.fn(), makeGlobalConfig());
252
+ const { emitEvent, calls } = makeEmitEvent();
253
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
159
254
  expect(session.flushClientLogs).toHaveBeenCalledTimes(2);
160
- expect(onResult).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ console: clientLogs }));
255
+ expect(calls).toContainEqual([
256
+ 'test-file-success',
257
+ expect.anything(),
258
+ expect.objectContaining({ console: clientLogs }),
259
+ ]);
260
+ });
261
+ it('emits test-case events before file success', async () => {
262
+ let testRunnerListener;
263
+ const { emitEvent, calls: emittedEvents } = makeEmitEvent();
264
+ const session = makeSession({
265
+ onTestRunnerEvent: vi.fn((listener) => {
266
+ testRunnerListener = listener;
267
+ return () => undefined;
268
+ }),
269
+ });
270
+ mockRunHarnessTestFile.mockImplementation(async () => {
271
+ testRunnerListener?.({
272
+ type: 'test-started',
273
+ file: 'example.ts',
274
+ suite: 'suite',
275
+ name: 'works',
276
+ ancestorTitles: ['suite'],
277
+ fullName: 'suite works',
278
+ startedAt: 10,
279
+ declarationMode: 'only',
280
+ });
281
+ testRunnerListener?.({
282
+ type: 'test-finished',
283
+ file: 'example.ts',
284
+ suite: 'suite',
285
+ name: 'works',
286
+ ancestorTitles: ['suite'],
287
+ fullName: 'suite works',
288
+ startedAt: 10,
289
+ declarationMode: 'only',
290
+ duration: 5,
291
+ status: 'passed',
292
+ });
293
+ return makeFileRunResult();
294
+ });
295
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
296
+ expect(emittedEvents).toEqual([
297
+ ['test-file-start', expect.anything()],
298
+ [
299
+ 'test-case-start',
300
+ 'example.ts',
301
+ expect.objectContaining({
302
+ ancestorTitles: ['suite'],
303
+ fullName: 'suite works',
304
+ mode: 'only',
305
+ title: 'works',
306
+ startedAt: 10,
307
+ }),
308
+ ],
309
+ [
310
+ 'test-case-result',
311
+ 'example.ts',
312
+ expect.objectContaining({
313
+ ancestorTitles: ['suite'],
314
+ fullName: 'suite works',
315
+ numPassingAsserts: 1,
316
+ startedAt: 10,
317
+ status: 'passed',
318
+ title: 'works',
319
+ }),
320
+ ],
321
+ ['test-file-success', expect.anything(), expect.anything()],
322
+ ]);
323
+ });
324
+ it('includes pending promise diagnostics in live test-case failures', async () => {
325
+ let testRunnerListener;
326
+ const { emitEvent, calls: emittedEvents } = makeEmitEvent();
327
+ const session = makeSession({
328
+ onTestRunnerEvent: vi.fn((listener) => {
329
+ testRunnerListener = listener;
330
+ return () => undefined;
331
+ }),
332
+ });
333
+ mockRunHarnessTestFile.mockImplementation(async () => {
334
+ testRunnerListener?.({
335
+ type: 'test-finished',
336
+ file: 'example.ts',
337
+ suite: 'suite',
338
+ name: 'hangs',
339
+ ancestorTitles: ['suite'],
340
+ fullName: 'suite hangs',
341
+ startedAt: 100,
342
+ duration: 50,
343
+ status: 'failed',
344
+ error: {
345
+ name: 'TestCaseTimeoutError',
346
+ message: 'Test timed out after 50ms: suite hangs',
347
+ diagnostics: {
348
+ pendingPromises: {
349
+ total: 1,
350
+ items: [
351
+ {
352
+ id: 7,
353
+ createdAt: 110,
354
+ stack: 'Error: Promise created\n at hangs (example.ts:10:5)',
355
+ },
356
+ ],
357
+ },
358
+ },
359
+ },
360
+ });
361
+ return makeFileRunResult();
362
+ });
363
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
364
+ expect(emittedEvents).toContainEqual([
365
+ 'test-case-result',
366
+ 'example.ts',
367
+ expect.objectContaining({
368
+ failureMessages: [
369
+ expect.stringContaining('Pending promises at timeout: 1\n\nPromise #7, created 10ms after test start:'),
370
+ ],
371
+ }),
372
+ ]);
161
373
  });
162
374
  });
163
375
  describe('runtime failures', () => {
164
376
  it('passes StartupStallError to onFailure with an empty stack', async () => {
165
- const onFailure = vi.fn();
377
+ const { emitEvent, calls } = makeEmitEvent();
166
378
  const session = makeSession({
167
379
  ensureAppReady: vi.fn().mockRejectedValue(new StartupStallError(1500, 3)),
168
380
  });
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: '' }));
381
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
382
+ expect(calls).toContainEqual([
383
+ 'test-file-failure',
384
+ expect.objectContaining({ path: '/test/example.ts' }),
385
+ expect.objectContaining({ message: expect.stringContaining('1500'), stack: '' }),
386
+ ]);
171
387
  });
172
388
  it('passes DeviceNotRespondingError to onFailure with an empty stack', async () => {
173
- const onFailure = vi.fn();
389
+ const { emitEvent, calls } = makeEmitEvent();
174
390
  const session = makeSession({
175
391
  ensureAppReady: vi.fn().mockRejectedValue(new DeviceNotRespondingError('runTests', [])),
176
392
  });
177
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), vi.fn(), onFailure, makeGlobalConfig());
178
- expect(onFailure).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ stack: '' }));
393
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
394
+ expect(calls).toContainEqual([
395
+ 'test-file-failure',
396
+ expect.anything(),
397
+ expect.objectContaining({ stack: '' }),
398
+ ]);
399
+ });
400
+ it('passes AppBridgeDisconnectedError to onFailure with an empty stack', async () => {
401
+ const { emitEvent, calls } = makeEmitEvent();
402
+ const session = makeSession({
403
+ ensureAppReady: vi.fn().mockRejectedValue(new AppBridgeDisconnectedError('app-disconnected')),
404
+ });
405
+ await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig());
406
+ expect(calls).toContainEqual([
407
+ 'test-file-failure',
408
+ expect.anything(),
409
+ expect.objectContaining({
410
+ message: expect.stringContaining('The app bridge disconnected during test execution.'),
411
+ stack: '',
412
+ }),
413
+ ]);
179
414
  });
180
415
  it('calls resetCrashState after a NativeCrashError', async () => {
181
416
  const session = makeSession({
182
417
  ensureAppReady: vi.fn().mockRejectedValue(new NativeCrashError('example.ts', { phase: 'execution', source: 'polling' })),
183
418
  });
184
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
419
+ await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
185
420
  expect(session.resetCrashState).toHaveBeenCalled();
186
421
  });
187
422
  it('counts runtime failures in the summary', async () => {
@@ -193,7 +428,7 @@ describe('executeRun', () => {
193
428
  }),
194
429
  ensureAppReady: vi.fn().mockRejectedValue(new StartupStallError(1000, 2)),
195
430
  });
196
- await executeRun(session, [makeTest()], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
431
+ await executeRun(session, [makeTest()], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
197
432
  const payload = finishedPayload;
198
433
  expect(payload.summary.failed).toBe(1);
199
434
  expect(payload.status).toBe('failed');
@@ -205,25 +440,200 @@ describe('executeRun', () => {
205
440
  const session = makeSession({
206
441
  callHook: vi.fn(async (name) => { hookNames.push(name); }),
207
442
  });
208
- const onStart = vi.fn();
209
- await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(true /* interrupted */), onStart, vi.fn(), vi.fn(), makeGlobalConfig());
443
+ const { emitEvent, calls } = makeEmitEvent();
444
+ await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(true /* interrupted */), emitEvent, makeGlobalConfig());
210
445
  // No test was started; watcher was already interrupted.
211
- expect(onStart).not.toHaveBeenCalled();
446
+ expect(calls).not.toContainEqual(['test-file-start', expect.anything()]);
212
447
  expect(hookNames).toContain('run:finished');
213
448
  });
214
449
  });
215
450
  describe('resetEnvironmentBetweenTestFiles', () => {
216
- it('calls restartApp before the second test but not the first', async () => {
451
+ it('calls resetEnvironment before the second test but not the first', async () => {
452
+ const session = makeSession({
453
+ config: {
454
+ metroPort: 8081,
455
+ resetEnvironmentBetweenTestFiles: true,
456
+ detectNativeCrashes: false,
457
+ runners: [
458
+ { platformId: 'android', name: 'android' },
459
+ { platformId: 'ios', name: 'ios' },
460
+ ],
461
+ },
462
+ });
463
+ await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts'), makeTest('/c.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
464
+ // resetEnvironment should be called for tests 2 and 3, not test 1.
465
+ expect(session.resetEnvironment).toHaveBeenCalledTimes(2);
466
+ expect(session.restartApp).not.toHaveBeenCalled();
467
+ });
468
+ it('calls resetEnvironment with the "runtime" strategy configured', async () => {
469
+ const session = makeSession({
470
+ config: {
471
+ metroPort: 8081,
472
+ resetEnvironmentBetweenTestFiles: 'runtime',
473
+ detectNativeCrashes: false,
474
+ runners: [
475
+ { platformId: 'android', name: 'android' },
476
+ { platformId: 'ios', name: 'ios' },
477
+ ],
478
+ },
479
+ });
480
+ await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
481
+ expect(session.resetEnvironment).toHaveBeenCalledTimes(1);
482
+ expect(session.resetEnvironment).toHaveBeenCalledWith('/b.ts');
483
+ });
484
+ it('does not call resetEnvironment when resetEnvironmentBetweenTestFiles is false', async () => {
485
+ const session = makeSession({
486
+ config: {
487
+ metroPort: 8081,
488
+ resetEnvironmentBetweenTestFiles: false,
489
+ detectNativeCrashes: false,
490
+ runners: [
491
+ { platformId: 'android', name: 'android' },
492
+ { platformId: 'ios', name: 'ios' },
493
+ ],
494
+ },
495
+ });
496
+ await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
497
+ expect(session.resetEnvironment).not.toHaveBeenCalled();
498
+ });
499
+ it('restarts after a test case timeout before the next runnable file', async () => {
500
+ const timedOutResult = makeHarnessResult('failed');
501
+ timedOutResult.tests = [
502
+ {
503
+ name: 'hangs',
504
+ status: 'failed',
505
+ duration: 10,
506
+ error: {
507
+ name: 'TestCaseTimeoutError',
508
+ message: 'Test timed out after 10ms: hangs',
509
+ },
510
+ },
511
+ ];
512
+ mockRunHarnessTestFile
513
+ .mockResolvedValueOnce(makeFileRunResult({
514
+ harnessResult: timedOutResult,
515
+ jestResult: makeJestResult({
516
+ numFailingTests: 1,
517
+ numPassingTests: 0,
518
+ }),
519
+ }))
520
+ .mockResolvedValueOnce(makeFileRunResult());
521
+ const session = makeSession({
522
+ config: {
523
+ metroPort: 8081,
524
+ resetEnvironmentBetweenTestFiles: false,
525
+ detectNativeCrashes: false,
526
+ runners: [
527
+ { platformId: 'android', name: 'android' },
528
+ { platformId: 'ios', name: 'ios' },
529
+ ],
530
+ },
531
+ });
532
+ await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
533
+ expect(session.restartApp).toHaveBeenCalledTimes(1);
534
+ expect(session.restartApp).toHaveBeenCalledWith('/a.ts');
535
+ });
536
+ it('restarts after a timeout in the last runnable file', async () => {
537
+ const timedOutResult = makeHarnessResult('failed');
538
+ timedOutResult.tests = [
539
+ {
540
+ name: 'hangs',
541
+ status: 'failed',
542
+ duration: 10,
543
+ error: {
544
+ name: 'TestCaseTimeoutError',
545
+ message: 'Test timed out after 10ms: hangs',
546
+ },
547
+ },
548
+ ];
549
+ mockRunHarnessTestFile.mockResolvedValueOnce(makeFileRunResult({
550
+ harnessResult: timedOutResult,
551
+ jestResult: makeJestResult({
552
+ numFailingTests: 1,
553
+ numPassingTests: 0,
554
+ }),
555
+ }));
556
+ const session = makeSession({
557
+ config: {
558
+ metroPort: 8081,
559
+ resetEnvironmentBetweenTestFiles: false,
560
+ detectNativeCrashes: false,
561
+ runners: [
562
+ { platformId: 'android', name: 'android' },
563
+ { platformId: 'ios', name: 'ios' },
564
+ ],
565
+ },
566
+ });
567
+ await executeRun(session, [makeTest('/a.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig({ watch: true }));
568
+ expect(session.restartApp).toHaveBeenCalledTimes(1);
569
+ expect(session.restartApp).toHaveBeenCalledWith('/a.ts');
570
+ });
571
+ it('restarts after a suite hook timeout', async () => {
572
+ const timedOutResult = makeHarnessResult('failed');
573
+ timedOutResult.suites = [
574
+ {
575
+ name: 'suite',
576
+ tests: [],
577
+ suites: [],
578
+ status: 'failed',
579
+ duration: 10,
580
+ error: {
581
+ name: 'SuiteHookTimeoutError',
582
+ message: 'beforeAll hook timed out after 10ms in suite: suite',
583
+ },
584
+ },
585
+ ];
586
+ mockRunHarnessTestFile.mockResolvedValueOnce(makeFileRunResult({
587
+ harnessResult: timedOutResult,
588
+ jestResult: makeJestResult({
589
+ numFailingTests: 1,
590
+ numPassingTests: 0,
591
+ }),
592
+ }));
593
+ const session = makeSession();
594
+ await executeRun(session, [makeTest('/a.ts')], makeWatcher(), makeEmitEvent().emitEvent, makeGlobalConfig());
595
+ expect(session.restartApp).toHaveBeenCalledTimes(1);
596
+ expect(session.restartApp).toHaveBeenCalledWith('/a.ts');
597
+ });
598
+ });
599
+ describe('platform-specific test files', () => {
600
+ it('skips files for other platforms without running them on the device', async () => {
601
+ const session = makeSession();
602
+ const { emitEvent, calls } = makeEmitEvent();
603
+ await executeRun(session, [
604
+ makeTest('/project/smoke.harness.ts'),
605
+ makeTest('/project/kotlin.android.harness.ts'),
606
+ makeTest('/project/swift.ios.harness.ts'),
607
+ ], makeWatcher(), emitEvent, makeGlobalConfig());
608
+ expect(mockRunHarnessTestFile).toHaveBeenCalledTimes(2);
609
+ expect(mockRunHarnessTestFile).toHaveBeenCalledWith(expect.objectContaining({ testPath: '/project/smoke.harness.ts' }));
610
+ expect(mockRunHarnessTestFile).toHaveBeenCalledWith(expect.objectContaining({ testPath: '/project/kotlin.android.harness.ts' }));
611
+ expect(session.ensureAppReady).toHaveBeenCalledTimes(2);
612
+ expect(calls).toContainEqual([
613
+ 'test-file-success',
614
+ expect.objectContaining({ path: '/project/swift.ios.harness.ts' }),
615
+ expect.objectContaining({ skipped: true }),
616
+ ]);
617
+ });
618
+ it('does not restart the app for skipped platform-specific files', async () => {
217
619
  const session = makeSession({
218
620
  config: {
219
621
  metroPort: 8081,
220
622
  resetEnvironmentBetweenTestFiles: true,
221
623
  detectNativeCrashes: false,
624
+ runners: [
625
+ { platformId: 'android', name: 'android' },
626
+ { platformId: 'ios', name: 'ios' },
627
+ ],
222
628
  },
223
629
  });
224
- await executeRun(session, [makeTest('/a.ts'), makeTest('/b.ts'), makeTest('/c.ts')], makeWatcher(), vi.fn(), vi.fn(), vi.fn(), makeGlobalConfig());
225
- // restartApp should be called for tests 2 and 3, not test 1.
226
- expect(session.restartApp).toHaveBeenCalledTimes(2);
630
+ await executeRun(session, [
631
+ makeTest('/project/swift.ios.harness.ts'),
632
+ makeTest('/project/kotlin.android.harness.ts'),
633
+ ], makeWatcher(), vi.fn(), makeGlobalConfig());
634
+ expect(session.restartApp).not.toHaveBeenCalled();
635
+ expect(session.resetEnvironment).not.toHaveBeenCalled();
636
+ expect(mockRunHarnessTestFile).toHaveBeenCalledTimes(1);
227
637
  });
228
638
  });
229
639
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=format-harness-error.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-harness-error.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/format-harness-error.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { formatHarnessErrorMessage } from '../format-harness-error.js';
3
+ describe('formatHarnessErrorMessage', () => {
4
+ it('formats pending promise diagnostics for timeout errors', () => {
5
+ const message = formatHarnessErrorMessage({
6
+ name: 'TestCaseTimeoutError',
7
+ message: 'Test timed out after 50ms: suite hangs',
8
+ diagnostics: {
9
+ pendingPromises: {
10
+ total: 2,
11
+ items: [
12
+ {
13
+ id: 7,
14
+ createdAt: 110,
15
+ stack: 'Error: Promise created\n at hangs (example.ts:10:5)',
16
+ },
17
+ ],
18
+ },
19
+ },
20
+ }, { testStartedAt: 100 });
21
+ expect(message).toContain('Test timed out after 50ms: suite hangs');
22
+ expect(message).toContain('Pending promises at timeout: 2');
23
+ expect(message).toContain('Showing 1 of 2 pending promises.');
24
+ expect(message).toContain('Promise #7, created 10ms after test start:');
25
+ expect(message).toContain(' Error: Promise created');
26
+ });
27
+ });