@react-native-harness/jest 1.1.0 → 1.2.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 (93) hide show
  1. package/dist/__tests__/bridge.test.d.ts +2 -0
  2. package/dist/__tests__/bridge.test.d.ts.map +1 -0
  3. package/dist/__tests__/bridge.test.js +142 -0
  4. package/dist/__tests__/client-log-handler.test.js +116 -72
  5. package/dist/__tests__/crash-monitor.test.d.ts +2 -0
  6. package/dist/__tests__/crash-monitor.test.d.ts.map +1 -0
  7. package/dist/__tests__/crash-monitor.test.js +212 -0
  8. package/dist/__tests__/execute-run.test.d.ts +2 -0
  9. package/dist/__tests__/execute-run.test.d.ts.map +1 -0
  10. package/dist/__tests__/execute-run.test.js +229 -0
  11. package/dist/__tests__/harness.test.js +35 -0
  12. package/dist/__tests__/hook-queue.test.d.ts +2 -0
  13. package/dist/__tests__/hook-queue.test.d.ts.map +1 -0
  14. package/dist/__tests__/hook-queue.test.js +56 -0
  15. package/dist/__tests__/index.test.js +94 -37
  16. package/dist/__tests__/resource-lock.test.js +24 -24
  17. package/dist/cli-args.test.d.ts +2 -0
  18. package/dist/cli-args.test.d.ts.map +1 -0
  19. package/dist/cli-args.test.js +1 -0
  20. package/dist/client-log-handler.d.ts +10 -11
  21. package/dist/client-log-handler.d.ts.map +1 -1
  22. package/dist/client-log-handler.js +36 -58
  23. package/dist/crash-monitor.d.ts +23 -0
  24. package/dist/crash-monitor.d.ts.map +1 -0
  25. package/dist/crash-monitor.js +176 -0
  26. package/dist/event-mapper.d.ts +16 -0
  27. package/dist/event-mapper.d.ts.map +1 -0
  28. package/dist/event-mapper.js +120 -0
  29. package/dist/execute-run.d.ts +4 -0
  30. package/dist/execute-run.d.ts.map +1 -0
  31. package/dist/execute-run.js +151 -0
  32. package/dist/global-setup.d.ts +3 -0
  33. package/dist/global-setup.d.ts.map +1 -0
  34. package/dist/global-setup.js +60 -0
  35. package/dist/global-teardown.d.ts +3 -0
  36. package/dist/global-teardown.d.ts.map +1 -0
  37. package/dist/global-teardown.js +11 -0
  38. package/dist/harness-reporter.d.ts +8 -0
  39. package/dist/harness-reporter.d.ts.map +1 -0
  40. package/dist/harness-reporter.js +12 -0
  41. package/dist/harness-session.d.ts +34 -0
  42. package/dist/harness-session.d.ts.map +1 -0
  43. package/dist/harness-session.js +523 -0
  44. package/dist/harness.d.ts.map +1 -1
  45. package/dist/harness.js +6 -1
  46. package/dist/hook-queue.d.ts +6 -0
  47. package/dist/hook-queue.d.ts.map +1 -0
  48. package/dist/hook-queue.js +18 -0
  49. package/dist/index.d.ts +0 -3
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +12 -230
  52. package/dist/jest-options.d.ts +2 -0
  53. package/dist/jest-options.d.ts.map +1 -0
  54. package/dist/jest-options.js +1 -0
  55. package/dist/logs.d.ts +1 -0
  56. package/dist/logs.d.ts.map +1 -1
  57. package/dist/logs.js +3 -0
  58. package/dist/resource-lock.d.ts +0 -1
  59. package/dist/resource-lock.d.ts.map +1 -1
  60. package/dist/resource-lock.js +1 -1
  61. package/dist/run.d.ts +2 -2
  62. package/dist/run.d.ts.map +1 -1
  63. package/dist/run.js +3 -3
  64. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  65. package/dist/types.d.ts +17 -0
  66. package/dist/types.d.ts.map +1 -0
  67. package/dist/types.js +1 -0
  68. package/package.json +7 -8
  69. package/src/__tests__/bridge.test.ts +184 -0
  70. package/src/__tests__/client-log-handler.test.ts +117 -81
  71. package/src/__tests__/crash-monitor.test.ts +288 -0
  72. package/src/__tests__/execute-run.test.ts +325 -0
  73. package/src/__tests__/hook-queue.test.ts +77 -0
  74. package/src/__tests__/index.test.ts +135 -55
  75. package/src/__tests__/resource-lock.test.ts +29 -42
  76. package/src/client-log-handler.ts +48 -64
  77. package/src/crash-monitor.ts +248 -0
  78. package/src/execute-run.ts +199 -0
  79. package/src/harness-session.ts +734 -0
  80. package/src/hook-queue.ts +26 -0
  81. package/src/index.ts +17 -330
  82. package/src/logs.ts +4 -0
  83. package/src/resource-lock.ts +1 -1
  84. package/src/run.ts +5 -5
  85. package/tsconfig.tsbuildinfo +1 -0
  86. package/src/__tests__/crash-supervisor.test.ts +0 -200
  87. package/src/__tests__/harness-cache.test.ts +0 -74
  88. package/src/__tests__/harness.test.ts +0 -870
  89. package/src/crash-supervisor.ts +0 -321
  90. package/src/env.d.ts +0 -9
  91. package/src/harness.ts +0 -864
  92. package/src/setup.ts +0 -127
  93. package/src/teardown.ts +0 -14
@@ -0,0 +1,26 @@
1
+ export type HookQueue = {
2
+ schedule: (work: () => Promise<void>) => void;
3
+ drain: () => Promise<void>;
4
+ };
5
+
6
+ export const createHookQueue = (): HookQueue => {
7
+ let tail: Promise<void> = Promise.resolve();
8
+ let firstError: unknown;
9
+
10
+ const schedule = (work: () => Promise<void>): void => {
11
+ tail = tail.then(work).catch((err) => {
12
+ firstError ??= err;
13
+ });
14
+ };
15
+
16
+ const drain = async (): Promise<void> => {
17
+ await tail;
18
+ if (firstError !== undefined) {
19
+ const err = firstError;
20
+ firstError = undefined;
21
+ throw err;
22
+ }
23
+ };
24
+
25
+ return { schedule, drain };
26
+ };
package/src/index.ts CHANGED
@@ -8,55 +8,16 @@ import type {
8
8
  TestRunnerOptions,
9
9
  TestWatcher,
10
10
  } from 'jest-runner';
11
- import pLimit from 'p-limit';
12
- import { runHarnessTestFile } from './run.js';
13
- import { Config as HarnessConfig } from '@react-native-harness/config';
14
- import { type Harness, type HarnessRunState } from './harness.js';
15
- import { setup } from './setup.js';
16
- import { teardown } from './teardown.js';
11
+ import { createHarnessSession, type HarnessSession } from './harness-session.js';
12
+ import { executeRun } from './execute-run.js';
17
13
  import { HarnessError } from '@react-native-harness/tools';
18
14
  import { getErrorMessage } from './logs.js';
19
- import { DeviceNotRespondingError } from '@react-native-harness/bridge/server';
20
- import { NativeCrashError, StartupStallError } from './errors.js';
21
- import { logger } from '@react-native-harness/tools';
22
- import { randomUUID } from 'node:crypto';
23
- import path from 'node:path';
24
-
25
- const runLogger = logger.child('run');
26
-
27
- const createRunSummary = () => ({
28
- passed: 0,
29
- failed: 0,
30
- skipped: 0,
31
- todo: 0,
32
- });
33
-
34
- const applyJestResultToSummary = (
35
- summary: ReturnType<typeof createRunSummary>,
36
- result: {
37
- numPassingTests: number;
38
- numFailingTests: number;
39
- numPendingTests: number;
40
- numTodoTests: number;
41
- }
42
- ) => {
43
- summary.passed += result.numPassingTests;
44
- summary.failed += result.numFailingTests;
45
- summary.skipped += result.numPendingTests;
46
- summary.todo += result.numTodoTests;
47
- };
48
-
49
- class CancelRun extends Error {
50
- constructor(message?: string) {
51
- super(message);
52
- this.name = 'CancelRun';
53
- }
54
- }
55
15
 
56
16
  export default class JestHarness implements CallbackTestRunnerInterface {
57
17
  readonly isSerial = true;
58
18
 
59
19
  #globalConfig: Config.GlobalConfig;
20
+ #session: HarnessSession | null = null;
60
21
 
61
22
  constructor(globalConfig: Config.GlobalConfig) {
62
23
  this.#globalConfig = globalConfig;
@@ -68,312 +29,38 @@ export default class JestHarness implements CallbackTestRunnerInterface {
68
29
  onStart: OnTestStart,
69
30
  onResult: OnTestSuccess,
70
31
  onFailure: OnTestFailure,
71
- options: TestRunnerOptions
32
+ options: TestRunnerOptions,
72
33
  ): Promise<void> {
73
34
  if (!options.serial) {
74
35
  throw new Error('Parallel test running is not supported');
75
36
  }
76
37
 
77
- try {
78
- // This is necessary as Harness may throw and we want to catch it and display a helpful error message.
79
- await setup(this.#globalConfig);
38
+ const isWatchMode = this.#globalConfig.watch || this.#globalConfig.watchAll;
80
39
 
81
- const harness = global.HARNESS;
82
- const harnessConfig = global.HARNESS_CONFIG;
40
+ try {
41
+ if (!this.#session) {
42
+ this.#session = await createHarnessSession(this.#globalConfig);
43
+ }
83
44
 
84
- return await this._createInBandTestRun(
45
+ await executeRun(
46
+ this.#session,
85
47
  tests,
86
48
  watcher,
87
- harness,
88
- harnessConfig,
89
49
  onStart,
90
50
  onResult,
91
- onFailure
51
+ onFailure,
52
+ this.#globalConfig,
92
53
  );
93
54
  } catch (error) {
94
55
  if (error instanceof HarnessError) {
95
- // Jest will print strings as they are, without processing them further.
96
56
  throw getErrorMessage(error);
97
57
  }
98
-
99
58
  throw error;
100
59
  } finally {
101
- // This is necessary as Harness may throw and we want to catch it and display a helpful error message.
102
- await teardown(this.#globalConfig);
103
- }
104
- }
105
-
106
- async _createInBandTestRun(
107
- tests: Array<Test>,
108
- watcher: TestWatcher,
109
- harness: Harness,
110
- harnessConfig: HarnessConfig,
111
- onStart: OnTestStart,
112
- onResult: OnTestSuccess,
113
- onFailure: OnTestFailure
114
- ): Promise<void> {
115
- const mutex = pLimit(1);
116
- let isFirstTest = true;
117
- const startTime = Date.now();
118
- const runId = randomUUID();
119
- const watchMode = this.#globalConfig.watch || this.#globalConfig.watchAll;
120
- const rootDir = this.#globalConfig.rootDir ?? process.cwd();
121
- const testFiles = tests.map((test) =>
122
- path.relative(rootDir, test.path)
123
- );
124
- const summary = createRunSummary();
125
- const updateRunState = (overrides: Partial<HarnessRunState> = {}) => {
126
- const nextRunState: HarnessRunState = {
127
- runId,
128
- startTime,
129
- testFiles,
130
- watchMode,
131
- coverageEnabled: this.#globalConfig.collectCoverage,
132
- summary,
133
- status: summary.failed > 0 ? 'failed' : 'passed',
134
- ...overrides,
135
- };
136
-
137
- harness.setRunState(nextRunState);
138
- return nextRunState;
139
- };
140
-
141
- updateRunState();
142
- runLogger.debug(
143
- 'run started: runId=%s files=%d watchMode=%s coverage=%s',
144
- runId,
145
- testFiles.length,
146
- watchMode,
147
- this.#globalConfig.collectCoverage
148
- );
149
- await harness.callHook('run:started', {
150
- runId,
151
- startTime,
152
- testFiles,
153
- watchMode,
154
- coverageEnabled: this.#globalConfig.collectCoverage,
155
- });
156
- await harness.callHook('metro:initialized', {
157
- runId,
158
- port: harnessConfig.metroPort,
159
- host: harnessConfig.host?.trim() || undefined,
160
- });
161
-
162
- try {
163
- await tests.reduce(
164
- (promise, test) =>
165
- mutex(() => {
166
- let didApplySummary = false;
167
-
168
- return promise
169
- .then(async () => {
170
- const relativeTestPath = path.relative(
171
- rootDir,
172
- test.path
173
- );
174
- const testFileStartedAt = Date.now();
175
- let didEmitTestFileFinished = false;
176
-
177
- await harness.callHook('test-file:started', {
178
- runId,
179
- file: relativeTestPath,
180
- });
181
- runLogger.debug('test file started: %s', relativeTestPath);
182
-
183
- const emitTestFileFinished = async (options: {
184
- status: 'passed' | 'failed' | 'skipped' | 'todo';
185
- duration: number;
186
- result: Awaited<ReturnType<typeof runHarnessTestFile>>['harnessResult'] | null;
187
- }) => {
188
- didEmitTestFileFinished = true;
189
- runLogger.debug(
190
- 'test file finished: %s status=%s duration=%dms',
191
- relativeTestPath,
192
- options.status,
193
- options.duration
194
- );
195
- await harness.callHook('test-file:finished', {
196
- runId,
197
- file: relativeTestPath,
198
- duration: options.duration,
199
- status: options.status,
200
- result: options.result,
201
- });
202
- };
203
-
204
- try {
205
- if (watcher.isInterrupted()) {
206
- throw new CancelRun();
207
- }
208
-
209
- if (
210
- harnessConfig.resetEnvironmentBetweenTestFiles &&
211
- !isFirstTest
212
- ) {
213
- runLogger.debug(
214
- 'resetting environment before %s',
215
- relativeTestPath
216
- );
217
- await harness.restart(test.path);
218
- }
219
- isFirstTest = false;
220
-
221
- const result = await onStart(test).then(async () => {
222
- if (!harnessConfig.detectNativeCrashes) {
223
- runLogger.debug(
224
- 'native crash detection disabled for %s',
225
- relativeTestPath
226
- );
227
- await harness.ensureAppReady(test.path);
228
- return runHarnessTestFile({
229
- testPath: test.path,
230
- harness,
231
- globalConfig: this.#globalConfig,
232
- projectConfig: test.context.config,
233
- });
234
- }
235
-
236
- await harness.ensureAppReady(test.path);
237
- harness.crashSupervisor.beginTestRun(test.path);
238
- runLogger.debug(
239
- 'native crash detection armed for %s',
240
- relativeTestPath
241
- );
242
- const crashPromise =
243
- harness.crashSupervisor.waitForCrash(test.path);
244
-
245
- try {
246
- return await Promise.race([
247
- runHarnessTestFile({
248
- testPath: test.path,
249
- harness,
250
- globalConfig: this.#globalConfig,
251
- projectConfig: test.context.config,
252
- }),
253
- crashPromise,
254
- ]);
255
- } finally {
256
- harness.crashSupervisor.cancelCrashWaiters();
257
- }
258
- });
259
-
260
- applyJestResultToSummary(summary, result.jestResult);
261
- didApplySummary = true;
262
- updateRunState();
263
- await emitTestFileFinished({
264
- status: result.harnessResult.status,
265
- duration: result.duration,
266
- result: result.harnessResult,
267
- });
268
-
269
- return result;
270
- } catch (error) {
271
- if (!didEmitTestFileFinished) {
272
- await emitTestFileFinished({
273
- status: 'failed',
274
- duration: Date.now() - testFileStartedAt,
275
- result: null,
276
- });
277
- }
278
-
279
- throw error;
280
- }
281
- })
282
- .then(async (result) => {
283
- if (!result) {
284
- return;
285
- }
286
-
287
- await onResult(test, result.jestResult);
288
- })
289
- .catch(async (err) => {
290
- if (
291
- err instanceof NativeCrashError ||
292
- err instanceof StartupStallError ||
293
- err instanceof DeviceNotRespondingError
294
- ) {
295
- runLogger.debug(
296
- 'classified runtime failure for %s: %s',
297
- test.path,
298
- err.name
299
- );
300
- summary.failed += 1;
301
- updateRunState();
302
- }
303
-
304
- if (err instanceof NativeCrashError) {
305
- harness.crashSupervisor.reset();
306
- onFailure(test, {
307
- message: err.message,
308
- stack: '',
309
- });
310
-
311
- return;
312
- }
313
-
314
- if (err instanceof StartupStallError) {
315
- onFailure(test, {
316
- message: err.message,
317
- stack: '',
318
- });
319
-
320
- return;
321
- }
322
-
323
- if (err instanceof DeviceNotRespondingError) {
324
- onFailure(test, {
325
- message: err.message,
326
- stack: '',
327
- });
328
-
329
- return;
330
- }
331
-
332
- if (!(err instanceof CancelRun) && !didApplySummary) {
333
- summary.failed += 1;
334
- }
335
- updateRunState({ error: err });
336
- onFailure(test, err);
337
- });
338
- }),
339
- Promise.resolve()
340
- );
341
-
342
- const runState = updateRunState();
343
- runLogger.debug(
344
- 'run finished: runId=%s status=%s duration=%dms',
345
- runId,
346
- runState.status ?? 'passed',
347
- Date.now() - startTime
348
- );
349
- await harness.callHook('run:finished', {
350
- runId,
351
- startTime,
352
- duration: Date.now() - startTime,
353
- testFiles,
354
- summary,
355
- status: runState.status ?? 'passed',
356
- });
357
- } catch (error) {
358
- const runState = updateRunState({
359
- error,
360
- status: 'failed',
361
- });
362
- runLogger.debug(
363
- 'run failed: runId=%s status=%s',
364
- runId,
365
- runState.status ?? 'failed'
366
- );
367
- await harness.callHook('run:finished', {
368
- runId,
369
- startTime,
370
- duration: Date.now() - startTime,
371
- testFiles,
372
- summary,
373
- status: runState.status ?? 'failed',
374
- error,
375
- });
376
- throw error;
60
+ if (!isWatchMode) {
61
+ await this.#session?.dispose();
62
+ this.#session = null;
63
+ }
377
64
  }
378
65
  }
379
66
  }
package/src/logs.ts CHANGED
@@ -56,6 +56,10 @@ export const logMetroPortFallback = (
56
56
  );
57
57
  };
58
58
 
59
+ export const logNativeCoverageCollected = (lcovPath: string): void => {
60
+ log(`${TAG} Native coverage written to ${chalk.bold(lcovPath)}\n`);
61
+ };
62
+
59
63
  export const getErrorMessage = (error: HarnessError): string => {
60
64
  return `${ERROR_TAG} ${error.message}\n`;
61
65
  };
@@ -60,7 +60,7 @@ const wait = async (ms: number): Promise<void> => {
60
60
  const createAbortError = () =>
61
61
  new DOMException('The operation was aborted', 'AbortError');
62
62
 
63
- export const hashResourceLockKey = (key: string): string => {
63
+ const hashResourceLockKey = (key: string): string => {
64
64
  return crypto.createHash('sha256').update(key).digest('hex');
65
65
  };
66
66
 
package/src/run.ts CHANGED
@@ -5,7 +5,7 @@ import type {
5
5
  TestSuiteResult as HarnessTestSuiteResult,
6
6
  TestResult as HarnessTestResult,
7
7
  } from '@react-native-harness/bridge';
8
- import type { Harness } from './harness.js';
8
+ import type { HarnessSession } from './harness-session.js';
9
9
  import { formatResultsErrors } from 'jest-message-util';
10
10
  import { toTestResult } from './toTestResult.js';
11
11
 
@@ -62,7 +62,7 @@ const calculateStats = (tests: HarnessTestResult[]) => {
62
62
 
63
63
  export type RunHarnessTestFile = (options: {
64
64
  testPath: string;
65
- harness: Harness;
65
+ session: HarnessSession;
66
66
  projectConfig: JestConfig.ProjectConfig;
67
67
  globalConfig: JestConfig.GlobalConfig;
68
68
  }) => Promise<{
@@ -76,7 +76,7 @@ export const runHarnessTestFile: RunHarnessTestFile = async ({
76
76
  testPath,
77
77
  globalConfig,
78
78
  projectConfig,
79
- harness,
79
+ session,
80
80
  }) => {
81
81
  const start = Date.now();
82
82
  const relativeTestPath = path.relative(globalConfig.rootDir, testPath);
@@ -89,11 +89,11 @@ export const runHarnessTestFile: RunHarnessTestFile = async ({
89
89
  (setupFile) => path.relative(globalConfig.rootDir, setupFile)
90
90
  );
91
91
 
92
- const harnessResult = await harness.runTests(relativeTestPath, {
92
+ const harnessResult = await session.runTestFile(relativeTestPath, {
93
93
  testNamePattern: globalConfig.testNamePattern,
94
94
  setupFiles,
95
95
  setupFilesAfterEnv,
96
- runner: harness.context.platform.runner,
96
+ runner: session.context.platform.runner,
97
97
  });
98
98
  const end = Date.now();
99
99
 
@@ -0,0 +1 @@
1
+ {"fileNames":[],"fileInfos":[],"root":[],"options":{"composite":true,"declarationMap":true,"emitDeclarationOnly":true,"importHelpers":true,"module":199,"noEmitOnError":true,"noFallthroughCasesInSwitch":true,"noImplicitOverride":true,"noImplicitReturns":true,"noUnusedLocals":true,"skipLibCheck":true,"strict":true,"target":9},"version":"5.9.3"}
@@ -1,200 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import {
3
- type AppMonitor,
4
- type AppCrashDetails,
5
- type AppMonitorEvent,
6
- type AppMonitorListener,
7
- type HarnessPlatformRunner,
8
- } from '@react-native-harness/platforms';
9
- import { createCrashSupervisor } from '../crash-supervisor.js';
10
-
11
- type MockCrashDetailsGetter = (
12
- options: {
13
- processName?: string;
14
- pid?: number;
15
- occurredAt: number;
16
- }
17
- ) => Promise<AppCrashDetails | null>;
18
-
19
- const createMockAppMonitor = (options?: {
20
- getCrashDetails?: MockCrashDetailsGetter;
21
- }) => {
22
- const listeners = new Set<AppMonitorListener>();
23
- const noop = async () => undefined;
24
-
25
- const appMonitor: AppMonitor & {
26
- getCrashDetails?: MockCrashDetailsGetter;
27
- } = {
28
- start: noop,
29
- stop: noop,
30
- dispose: noop,
31
- addListener: (listener) => {
32
- listeners.add(listener);
33
- },
34
- removeListener: (listener) => {
35
- listeners.delete(listener);
36
- },
37
- };
38
-
39
- if (options?.getCrashDetails) {
40
- appMonitor.getCrashDetails = options.getCrashDetails;
41
- }
42
-
43
- const emit = (event: AppMonitorEvent) => {
44
- for (const listener of listeners) {
45
- listener(event);
46
- }
47
- };
48
-
49
- return {
50
- appMonitor,
51
- emit,
52
- };
53
- };
54
-
55
- const createPlatformRunner = ({
56
- isAppRunning,
57
- getCrashDetails,
58
- }: {
59
- isAppRunning: () => Promise<boolean>;
60
- getCrashDetails?: HarnessPlatformRunner['getCrashDetails'];
61
- }): HarnessPlatformRunner => ({
62
- startApp: async () => undefined,
63
- restartApp: async () => undefined,
64
- stopApp: async () => undefined,
65
- dispose: async () => undefined,
66
- isAppRunning,
67
- createAppMonitor: () => {
68
- throw new Error('Not used in unit tests');
69
- },
70
- getCrashDetails,
71
- });
72
-
73
- describe('crash-supervisor', () => {
74
- it('rejects the active test when the app exits during launch', async () => {
75
- const monitor = createMockAppMonitor();
76
- const supervisor = createCrashSupervisor({
77
- appMonitor: monitor.appMonitor,
78
- platformRunner: createPlatformRunner({
79
- isAppRunning: vi.fn().mockResolvedValue(false),
80
- }),
81
- });
82
-
83
- supervisor.beginLaunch('/tmp/startup.harness.ts');
84
-
85
- const crashPromise = supervisor.waitForCrash('/tmp/startup.harness.ts');
86
- const expectation =
87
- expect(crashPromise).rejects.toMatchObject({
88
- name: 'NativeCrashError',
89
- phase: 'startup',
90
- details: {
91
- signal: 'SIGSEGV',
92
- processName: 'com.harnessplayground',
93
- },
94
- });
95
- monitor.emit({
96
- type: 'app_exited',
97
- source: 'logs',
98
- crashDetails: {
99
- source: 'logs',
100
- signal: 'SIGSEGV',
101
- processName: 'com.harnessplayground',
102
- rawLines: ['Fatal signal 11 (SIGSEGV)'],
103
- },
104
- });
105
-
106
- await expectation;
107
- await supervisor.dispose();
108
- });
109
-
110
- it('treats polling exit events as immediately confirmed crashes', async () => {
111
- const monitor = createMockAppMonitor();
112
- const isAppRunning = vi.fn().mockResolvedValue(true);
113
- const supervisor = createCrashSupervisor({
114
- appMonitor: monitor.appMonitor,
115
- platformRunner: createPlatformRunner({
116
- isAppRunning,
117
- }),
118
- });
119
-
120
- supervisor.beginLaunch('/tmp/polling-exit.harness.ts');
121
-
122
- const crashPromise = supervisor.waitForCrash('/tmp/polling-exit.harness.ts');
123
- monitor.emit({ type: 'app_exited', source: 'polling' });
124
-
125
- await expect(crashPromise).rejects.toMatchObject({
126
- name: 'NativeCrashError',
127
- phase: 'startup',
128
- });
129
- expect(isAppRunning).not.toHaveBeenCalled();
130
- await supervisor.dispose();
131
- });
132
-
133
- it('does not reject when monitoring is stopped', async () => {
134
- const monitor = createMockAppMonitor();
135
- const supervisor = createCrashSupervisor({
136
- appMonitor: monitor.appMonitor,
137
- platformRunner: createPlatformRunner({
138
- isAppRunning: vi.fn().mockResolvedValue(false),
139
- }),
140
- });
141
-
142
- supervisor.beginTestRun('/tmp/restart.harness.ts');
143
- await supervisor.stop();
144
-
145
- const reject = vi.fn();
146
- void supervisor.waitForCrash('/tmp/restart.harness.ts').catch(reject);
147
-
148
- monitor.emit({ type: 'app_exited', source: 'polling' });
149
- await Promise.resolve();
150
-
151
- expect(reject).not.toHaveBeenCalled();
152
- await supervisor.dispose();
153
- });
154
-
155
- it('prefers crash details from the started app monitor over the runner', async () => {
156
- const monitorGetCrashDetails = vi.fn().mockResolvedValue({
157
- source: 'logs',
158
- summary: 'full crash block',
159
- rawLines: ['full crash block'],
160
- });
161
- const runnerGetCrashDetails = vi.fn().mockResolvedValue({
162
- source: 'logs',
163
- summary: 'runner details',
164
- rawLines: ['runner details'],
165
- });
166
- const monitor = createMockAppMonitor({
167
- getCrashDetails: monitorGetCrashDetails,
168
- });
169
- const supervisor = createCrashSupervisor({
170
- appMonitor: monitor.appMonitor,
171
- platformRunner: createPlatformRunner({
172
- isAppRunning: vi.fn().mockResolvedValue(false),
173
- getCrashDetails: runnerGetCrashDetails,
174
- }),
175
- });
176
-
177
- supervisor.beginTestRun('/tmp/monitor-details.harness.ts');
178
-
179
- const crashPromise = supervisor.waitForCrash('/tmp/monitor-details.harness.ts');
180
- monitor.emit({
181
- type: 'possible_crash',
182
- source: 'logs',
183
- crashDetails: {
184
- source: 'logs',
185
- processName: 'com.harnessplayground',
186
- pid: 1234,
187
- rawLines: ['partial line'],
188
- },
189
- });
190
-
191
- await expect(crashPromise).rejects.toMatchObject({
192
- details: {
193
- summary: 'full crash block',
194
- },
195
- });
196
- expect(monitorGetCrashDetails).toHaveBeenCalled();
197
- expect(runnerGetCrashDetails).not.toHaveBeenCalled();
198
- await supervisor.dispose();
199
- });
200
- });