@react-native-harness/jest 1.1.0-rc.1 → 1.1.0-rc.3

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 (58) hide show
  1. package/dist/__tests__/errors.test.js +6 -1
  2. package/dist/__tests__/harness-cache.test.d.ts +2 -0
  3. package/dist/__tests__/harness-cache.test.d.ts.map +1 -0
  4. package/dist/__tests__/harness-cache.test.js +51 -0
  5. package/dist/__tests__/harness.test.js +484 -47
  6. package/dist/__tests__/index.test.d.ts +2 -0
  7. package/dist/__tests__/index.test.d.ts.map +1 -0
  8. package/dist/__tests__/index.test.js +42 -0
  9. package/dist/__tests__/resource-lock.test.d.ts +2 -0
  10. package/dist/__tests__/resource-lock.test.d.ts.map +1 -0
  11. package/dist/__tests__/resource-lock.test.js +103 -0
  12. package/dist/action-hooks.d.ts +9 -0
  13. package/dist/action-hooks.d.ts.map +1 -0
  14. package/dist/action-hooks.js +36 -0
  15. package/dist/cli-args.d.ts +1 -0
  16. package/dist/cli-args.d.ts.map +1 -1
  17. package/dist/cli-args.js +5 -0
  18. package/dist/crash-supervisor.d.ts.map +1 -1
  19. package/dist/crash-supervisor.js +7 -6
  20. package/dist/errors.d.ts +5 -0
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js +10 -2
  23. package/dist/harness.d.ts +23 -1
  24. package/dist/harness.d.ts.map +1 -1
  25. package/dist/harness.js +574 -136
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +205 -58
  28. package/dist/logs.d.ts +4 -0
  29. package/dist/logs.d.ts.map +1 -1
  30. package/dist/logs.js +12 -0
  31. package/dist/resource-lock.d.ts +25 -0
  32. package/dist/resource-lock.d.ts.map +1 -0
  33. package/dist/resource-lock.js +278 -0
  34. package/dist/run.d.ts +7 -1
  35. package/dist/run.d.ts.map +1 -1
  36. package/dist/run.js +13 -8
  37. package/dist/setup.d.ts.map +1 -1
  38. package/dist/setup.js +29 -2
  39. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  40. package/package.json +7 -6
  41. package/src/__tests__/errors.test.ts +18 -6
  42. package/src/__tests__/harness-cache.test.ts +74 -0
  43. package/src/__tests__/harness.test.ts +702 -49
  44. package/src/__tests__/index.test.ts +61 -0
  45. package/src/__tests__/resource-lock.test.ts +138 -0
  46. package/src/action-hooks.ts +66 -0
  47. package/src/cli-args.ts +6 -0
  48. package/src/crash-supervisor.ts +13 -6
  49. package/src/env.d.ts +1 -1
  50. package/src/errors.ts +15 -4
  51. package/src/harness.ts +743 -157
  52. package/src/index.ts +276 -64
  53. package/src/logs.ts +17 -3
  54. package/src/resource-lock.ts +450 -0
  55. package/src/run.ts +20 -10
  56. package/src/setup.ts +42 -1
  57. package/tsconfig.json +3 -3
  58. package/tsconfig.lib.json +6 -6
package/src/index.ts CHANGED
@@ -11,13 +11,40 @@ import type {
11
11
  import pLimit from 'p-limit';
12
12
  import { runHarnessTestFile } from './run.js';
13
13
  import { Config as HarnessConfig } from '@react-native-harness/config';
14
- import { type Harness } from './harness.js';
14
+ import { type Harness, type HarnessRunState } from './harness.js';
15
15
  import { setup } from './setup.js';
16
16
  import { teardown } from './teardown.js';
17
17
  import { HarnessError } from '@react-native-harness/tools';
18
18
  import { getErrorMessage } from './logs.js';
19
19
  import { DeviceNotRespondingError } from '@react-native-harness/bridge/server';
20
- import { NativeCrashError } from './errors.js';
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
+ };
21
48
 
22
49
  class CancelRun extends Error {
23
50
  constructor(message?: string) {
@@ -87,81 +114,266 @@ export default class JestHarness implements CallbackTestRunnerInterface {
87
114
  ): Promise<void> {
88
115
  const mutex = pLimit(1);
89
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
+ });
90
268
 
91
- return tests.reduce(
92
- (promise, test) =>
93
- mutex(() =>
94
- promise
95
- .then(async () => {
96
- if (watcher.isInterrupted()) {
97
- throw new CancelRun();
98
- }
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
+ }
99
286
 
287
+ await onResult(test, result.jestResult);
288
+ })
289
+ .catch(async (err) => {
100
290
  if (
101
- harnessConfig.resetEnvironmentBetweenTestFiles &&
102
- !isFirstTest
291
+ err instanceof NativeCrashError ||
292
+ err instanceof StartupStallError ||
293
+ err instanceof DeviceNotRespondingError
103
294
  ) {
104
- await harness.restart(test.path);
295
+ runLogger.debug(
296
+ 'classified runtime failure for %s: %s',
297
+ test.path,
298
+ err.name
299
+ );
300
+ summary.failed += 1;
301
+ updateRunState();
105
302
  }
106
- isFirstTest = false;
107
-
108
- return onStart(test).then(async () => {
109
- if (!harnessConfig.detectNativeCrashes) {
110
- await harness.ensureAppReady(test.path);
111
- return runHarnessTestFile({
112
- testPath: test.path,
113
- harness,
114
- globalConfig: this.#globalConfig,
115
- projectConfig: test.context.config,
303
+
304
+ if (err instanceof NativeCrashError) {
305
+ harness.crashSupervisor.reset();
306
+ onFailure(test, {
307
+ message: err.message,
308
+ stack: '',
116
309
  });
117
- }
118
310
 
119
- await harness.ensureAppReady(test.path);
120
- harness.crashSupervisor.beginTestRun(test.path);
121
- const crashPromise = harness.crashSupervisor.waitForCrash(test.path);
311
+ return;
312
+ }
122
313
 
123
- try {
124
- const result = await Promise.race([
125
- runHarnessTestFile({
126
- testPath: test.path,
127
- harness,
128
- globalConfig: this.#globalConfig,
129
- projectConfig: test.context.config,
130
- }),
131
- crashPromise,
132
- ]);
314
+ if (err instanceof StartupStallError) {
315
+ onFailure(test, {
316
+ message: err.message,
317
+ stack: '',
318
+ });
133
319
 
134
- return result;
135
- } finally {
136
- harness.crashSupervisor.cancelCrashWaiters();
320
+ return;
137
321
  }
138
- });
139
- })
140
- .then((result) => onResult(test, result))
141
- .catch(async (err) => {
142
- if (err instanceof NativeCrashError) {
143
- harness.crashSupervisor.reset();
144
- onFailure(test, {
145
- message: err.message,
146
- stack: '',
147
- });
148
322
 
149
- return;
150
- }
323
+ if (err instanceof DeviceNotRespondingError) {
324
+ onFailure(test, {
325
+ message: err.message,
326
+ stack: '',
327
+ });
151
328
 
152
- if (err instanceof DeviceNotRespondingError) {
153
- onFailure(test, {
154
- message: err.message,
155
- stack: '',
156
- });
329
+ return;
330
+ }
157
331
 
158
- return;
159
- }
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
+ );
160
341
 
161
- onFailure(test, err);
162
- })
163
- ),
164
- Promise.resolve()
165
- );
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;
377
+ }
166
378
  }
167
379
  }
package/src/logs.ts CHANGED
@@ -25,10 +25,24 @@ export const logTestEnvironmentReady = (runner: HarnessPlatform): void => {
25
25
  log(`${TAG} Runner ${chalk.bold(runner.name)} ready\n`);
26
26
  };
27
27
 
28
+ export const logRunnerWaitingInQueue = (runner: HarnessPlatform): void => {
29
+ log(`${TAG} Runner ${chalk.bold(runner.name)} is busy, waiting in queue\n`);
30
+ };
31
+
32
+ export const logRunnerStillWaitingInQueue = (runner: HarnessPlatform): void => {
33
+ log(`${TAG} Still waiting in queue for ${chalk.bold(runner.name)} runner\n`);
34
+ };
35
+
36
+ export const logRunnerStarting = (runner: HarnessPlatform): void => {
37
+ log(`${TAG} Runner ${chalk.bold(runner.name)} is starting\n`);
38
+ };
39
+
28
40
  export const logMetroPrewarmCompleted = (runner: HarnessPlatform): void => {
29
- log(
30
- `${TAG} Metro pre-warm for ${chalk.bold(runner.name)} completed\n`
31
- );
41
+ log(`${TAG} Metro pre-warm for ${chalk.bold(runner.name)} completed\n`);
42
+ };
43
+
44
+ export const logMetroCacheReused = (runner: HarnessPlatform): void => {
45
+ log(`${TAG} Reusing Metro cache for ${chalk.bold(runner.name)}\n`);
32
46
  };
33
47
 
34
48
  export const getErrorMessage = (error: HarnessError): string => {