@react-native-harness/jest 1.2.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 (54) 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__/fixtures/mock-platform-runner.d.ts +7 -0
  6. package/dist/__tests__/fixtures/mock-platform-runner.d.ts.map +1 -0
  7. package/dist/__tests__/fixtures/mock-platform-runner.js +6 -0
  8. package/dist/__tests__/harness-session.test.d.ts +2 -0
  9. package/dist/__tests__/harness-session.test.d.ts.map +1 -0
  10. package/dist/__tests__/harness-session.test.js +74 -0
  11. package/dist/__tests__/index.test.js +59 -10
  12. package/dist/__tests__/test-file-platform-filter.test.d.ts +2 -0
  13. package/dist/__tests__/test-file-platform-filter.test.d.ts.map +1 -0
  14. package/dist/__tests__/test-file-platform-filter.test.js +47 -0
  15. package/dist/crash-monitor.d.ts +5 -4
  16. package/dist/crash-monitor.d.ts.map +1 -1
  17. package/dist/crash-monitor.js +116 -102
  18. package/dist/errors.d.ts +10 -0
  19. package/dist/errors.d.ts.map +1 -1
  20. package/dist/errors.js +41 -2
  21. package/dist/execute-run.d.ts +4 -2
  22. package/dist/execute-run.d.ts.map +1 -1
  23. package/dist/execute-run.js +85 -7
  24. package/dist/harness-session.d.ts +21 -1
  25. package/dist/harness-session.d.ts.map +1 -1
  26. package/dist/harness-session.js +134 -39
  27. package/dist/index.d.ts +7 -3
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +24 -2
  30. package/dist/run.d.ts.map +1 -1
  31. package/dist/run.js +1 -0
  32. package/dist/test-file-platform-filter.d.ts +5 -0
  33. package/dist/test-file-platform-filter.d.ts.map +1 -0
  34. package/dist/test-file-platform-filter.js +40 -0
  35. package/dist/toTestResult.d.ts +1 -0
  36. package/dist/toTestResult.d.ts.map +1 -1
  37. package/dist/toTestResult.js +2 -2
  38. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  39. package/package.json +7 -7
  40. package/src/__tests__/bridge.test.ts +37 -2
  41. package/src/__tests__/crash-monitor.test.ts +182 -231
  42. package/src/__tests__/errors.test.ts +75 -0
  43. package/src/__tests__/execute-run.test.ts +235 -34
  44. package/src/__tests__/harness-session.test.ts +93 -0
  45. package/src/__tests__/index.test.ts +89 -11
  46. package/src/__tests__/test-file-platform-filter.test.ts +83 -0
  47. package/src/crash-monitor.ts +192 -131
  48. package/src/errors.ts +62 -1
  49. package/src/execute-run.ts +125 -11
  50. package/src/harness-session.ts +177 -42
  51. package/src/index.ts +40 -11
  52. package/src/run.ts +1 -0
  53. package/src/test-file-platform-filter.ts +57 -0
  54. package/src/toTestResult.ts +3 -2
@@ -2,9 +2,10 @@ import { createHarnessBridge, } from '@react-native-harness/bridge/server';
2
2
  import { HARNESS_BRIDGE_PATH, } from '@react-native-harness/bridge';
3
3
  import { getMetroInstance, isMetroCacheReusable, waitForMetroBackedAppReady, } from '@react-native-harness/bundler-metro';
4
4
  import { createHarnessPluginManager, } from '@react-native-harness/plugins';
5
- import { logger, createCrashArtifactWriter, getTimeoutSignal, raceAbortSignals, } from '@react-native-harness/tools';
5
+ import { createCrashArtifactWriter, logger, getTimeoutSignal, raceAbortSignals, } from '@react-native-harness/tools';
6
6
  import { getConfig, ConfigSchema, } from '@react-native-harness/config';
7
7
  import { preRunMessage } from 'jest-util';
8
+ import path from 'node:path';
8
9
  import { PlatformReadyTimeoutError } from './errors.js';
9
10
  import { NoRunnerSpecifiedError, RunnerNotFoundError } from './errors.js';
10
11
  import { createCrashMonitor } from './crash-monitor.js';
@@ -18,6 +19,28 @@ import { logMetroCacheReused, logMetroPortFallback, logNativeCoverageCollected,
18
19
  const sessionLogger = logger.child('runtime');
19
20
  const defaultResourceLockManager = createResourceLockManager();
20
21
  const ignorePromiseRejection = () => undefined;
22
+ const isBridgeDisconnectError = (error) => error instanceof Error && error.message === 'App bridge disconnected';
23
+ const TEST_RUN_BRIDGE_STABILITY_WAIT_MS = 500;
24
+ export const waitForBridgeDisconnectOrTimeout = async ({ bridge, connection, timeoutMs, }) => {
25
+ if (bridge.connection !== connection) {
26
+ return true;
27
+ }
28
+ return await new Promise((resolve) => {
29
+ const onDisconnected = () => {
30
+ cleanup();
31
+ resolve(true);
32
+ };
33
+ const cleanup = () => {
34
+ clearTimeout(timer);
35
+ bridge.off('disconnected', onDisconnected);
36
+ };
37
+ const timer = setTimeout(() => {
38
+ cleanup();
39
+ resolve(bridge.connection !== connection);
40
+ }, timeoutMs);
41
+ bridge.on('disconnected', onDisconnected);
42
+ });
43
+ };
21
44
  // ---------------------------------------------------------------------------
22
45
  // Internal helpers
23
46
  // ---------------------------------------------------------------------------
@@ -46,8 +69,21 @@ const withPlatformReadyTimeout = async (options) => {
46
69
  throw error;
47
70
  }
48
71
  };
72
+ export const waitForStartupCrash = async ({ crashMonitor, detectNativeCrashes, testFilePath, signal, }) => {
73
+ if (!detectNativeCrashes) {
74
+ return await waitForAbort(signal);
75
+ }
76
+ const watch = crashMonitor.watch(testFilePath, 'startup');
77
+ watch.promise.catch(ignorePromiseRejection); // suppress unhandled-rejection when abort wins race
78
+ try {
79
+ return await Promise.race([watch.promise, waitForAbort(signal)]);
80
+ }
81
+ finally {
82
+ watch.cancel();
83
+ }
84
+ };
49
85
  const waitForAppReady = async (base, testFilePath) => {
50
- const { metroInstance, bridge, platformInstance, platformId, bundleStartTimeout, readyTimeout, maxAppRestarts, crashMonitor, appLaunchOptions, } = base;
86
+ const { metroInstance, bridge, platformId, bundleStartTimeout, readyTimeout, maxAppRestarts, crashMonitor, detectNativeCrashes, restartAppSession, } = base;
51
87
  const logWait = (message, ...args) => sessionLogger.debug(`waitForAppReady: ${message}`, ...args);
52
88
  return await waitForMetroBackedAppReady({
53
89
  metro: metroInstance,
@@ -58,7 +94,7 @@ const waitForAppReady = async (base, testFilePath) => {
58
94
  signal: new AbortController().signal,
59
95
  startAttempt: async () => {
60
96
  logWait('launching app for %s', testFilePath);
61
- await platformInstance.restartApp(appLaunchOptions);
97
+ await restartAppSession();
62
98
  logWait('launch request completed, waiting for bridge ready');
63
99
  },
64
100
  waitForReady: async (signal) => {
@@ -69,7 +105,7 @@ const waitForAppReady = async (base, testFilePath) => {
69
105
  // connection from a previous run would resolve the promise before startAttempt
70
106
  // even restarts the app — leaving bridge.connection null after the restart.
71
107
  await new Promise((resolve, reject) => {
72
- const onConnected = (_conn) => { cleanup(); resolve(); };
108
+ const onConnected = () => { cleanup(); resolve(); };
73
109
  const onAbort = () => { cleanup(); reject(signal.reason ?? new DOMException('Aborted', 'AbortError')); };
74
110
  const cleanup = () => {
75
111
  bridge.off('connected', onConnected);
@@ -85,15 +121,13 @@ const waitForAppReady = async (base, testFilePath) => {
85
121
  logWait('runtime ready received');
86
122
  },
87
123
  waitForCrash: async (signal) => {
88
- const watch = crashMonitor.watch(testFilePath, 'startup');
89
- watch.promise.catch(ignorePromiseRejection); // suppress unhandled-rejection when abort wins race
90
- try {
91
- logWait('waiting for crash or runtime ready');
92
- return await Promise.race([watch.promise, waitForAbort(signal)]);
93
- }
94
- finally {
95
- watch.cancel();
96
- }
124
+ logWait('waiting for crash or runtime ready');
125
+ return await waitForStartupCrash({
126
+ crashMonitor,
127
+ detectNativeCrashes,
128
+ testFilePath,
129
+ signal,
130
+ });
97
131
  },
98
132
  onAttemptStart: () => {
99
133
  logWait('beginning launch attempt for %s', testFilePath);
@@ -242,6 +276,11 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
242
276
  const getCurrentRunId = () => currentRun?.runId;
243
277
  const clientLogCollector = createClientLogCollector();
244
278
  const context = { platform };
279
+ const crashArtifactWriter = createCrashArtifactWriter({
280
+ runnerName: platform.name,
281
+ platformId: platform.platformId,
282
+ rootDir: path.join(projectRoot, '.harness', 'crash-reports'),
283
+ });
245
284
  const bridge = await createHarnessBridge({
246
285
  noServer: true,
247
286
  timeout: runtimeConfig.bridgeTimeout,
@@ -268,6 +307,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
268
307
  work: async (signal) => {
269
308
  return await import(platform.runner).then((module) => module.default(platform.config, runtimeConfig, {
270
309
  signal,
310
+ crashArtifactWriter,
271
311
  })).then((instance) => {
272
312
  sessionLogger.debug('platform runner initialized');
273
313
  return instance;
@@ -281,25 +321,38 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
281
321
  await bridge.dispose();
282
322
  throw error;
283
323
  }
284
- const crashArtifactWriter = createCrashArtifactWriter({
285
- runnerName: platform.name,
286
- platformId: platform.platformId,
287
- });
288
- const appMonitor = platformInstance.createAppMonitor({ crashArtifactWriter });
289
324
  const appLaunchOptions = platform.config.appLaunchOptions;
290
- const crashMonitor = createCrashMonitor({ appMonitor, platformRunner: platformInstance });
325
+ let currentAppSession = null;
326
+ const crashMonitor = createCrashMonitor();
327
+ const disposeCurrentAppSession = async () => {
328
+ const session = currentAppSession;
329
+ currentAppSession = null;
330
+ crashMonitor.setAppSession(null);
331
+ if (session) {
332
+ await session.dispose();
333
+ }
334
+ };
335
+ const restartAppSession = async () => {
336
+ await crashMonitor.stop();
337
+ await disposeCurrentAppSession();
338
+ const session = await platformInstance.createAppSession(appLaunchOptions);
339
+ currentAppSession = session;
340
+ crashMonitor.setAppSession(session);
341
+ await crashMonitor.start();
342
+ return session;
343
+ };
291
344
  // Pre-build the options that are constant across all app-ready calls;
292
345
  // only testFilePath varies per call.
293
346
  const appReadyBaseOptions = {
294
347
  metroInstance,
295
348
  bridge,
296
- platformInstance,
297
349
  platformId: platform.platformId,
298
350
  bundleStartTimeout: runtimeConfig.bundleStartTimeout ?? 60000,
299
351
  readyTimeout: runtimeConfig.bridgeTimeout,
300
352
  maxAppRestarts: runtimeConfig.maxAppRestarts ?? 2,
301
353
  crashMonitor,
302
- appLaunchOptions,
354
+ detectNativeCrashes: runtimeConfig.detectNativeCrashes !== false,
355
+ restartAppSession,
303
356
  };
304
357
  // --- Event listeners ---
305
358
  const bridgeEventListener = buildBridgeHookScheduler(hooks, pluginManager, getCurrentRunId);
@@ -311,6 +364,17 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
311
364
  };
312
365
  const flushClientLogs = () => clientLogCollector.flush();
313
366
  const clientLogListener = clientLogCollector.handleEvent;
367
+ const testRunnerEventListeners = new Set();
368
+ const onTestRunnerEvent = (event) => {
369
+ if (event.type === 'test-started' ||
370
+ event.type === 'test-finished' ||
371
+ event.type === 'suite-started' ||
372
+ event.type === 'suite-finished' ||
373
+ event.type === 'file-started' ||
374
+ event.type === 'file-finished') {
375
+ testRunnerEventListeners.forEach((listener) => listener(event));
376
+ }
377
+ };
314
378
  const onConnected = (conn) => {
315
379
  const runId = getCurrentRunId();
316
380
  if (!runId)
@@ -322,10 +386,14 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
322
386
  if (!runId)
323
387
  return;
324
388
  hooks.schedule(() => pluginManager.callHook('runtime:disconnected', { runId, reason: 'bridge-disconnected' }));
389
+ if (runtimeConfig.detectNativeCrashes !== false) {
390
+ crashMonitor.handleBridgeDisconnect();
391
+ }
325
392
  };
326
393
  bridge.on('connected', onConnected);
327
394
  bridge.on('disconnected', onDisconnected);
328
395
  bridge.on('event', bridgeEventListener);
396
+ bridge.on('event', onTestRunnerEvent);
329
397
  metroInstance.events.addListener(onMetroEvent);
330
398
  if (runtimeConfig.forwardClientLogs) {
331
399
  metroInstance.events.addListener(clientLogListener);
@@ -365,10 +433,12 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
365
433
  bridge.off('connected', onConnected);
366
434
  bridge.off('disconnected', onDisconnected);
367
435
  bridge.off('event', bridgeEventListener);
436
+ bridge.off('event', onTestRunnerEvent);
368
437
  const nativeCoverageConfig = runtimeConfig.coverage?.native?.ios;
369
438
  if (nativeCoverageConfig?.pods?.length && platformInstance.collectNativeCoverage) {
370
439
  try {
371
- await platformInstance.stopApp();
440
+ await crashMonitor.stop();
441
+ await disposeCurrentAppSession();
372
442
  const lcovPath = await platformInstance.collectNativeCoverage({
373
443
  pods: nativeCoverageConfig.pods,
374
444
  outputDir: projectRoot,
@@ -385,6 +455,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
385
455
  try {
386
456
  await Promise.all([
387
457
  crashMonitor.dispose(),
458
+ disposeCurrentAppSession(),
388
459
  bridge.dispose(),
389
460
  platformInstance.dispose(),
390
461
  metroInstance.dispose(),
@@ -419,8 +490,6 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
419
490
  try {
420
491
  await pluginManager.callHook('harness:before-creation', { appLaunchOptions });
421
492
  await hooks.drain();
422
- await appMonitor.start();
423
- sessionLogger.debug('app monitor started');
424
493
  await pluginManager.callHook('harness:before-run', { appLaunchOptions });
425
494
  await hooks.drain();
426
495
  }
@@ -436,9 +505,12 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
436
505
  const ensureAppReady = async (testFilePath) => {
437
506
  await hooks.drain();
438
507
  sessionLogger.debug('ensuring app is ready for %s', testFilePath);
439
- if (crashMonitor.isAlive() && bridge.connection !== null && await platformInstance.isAppRunning()) {
440
- sessionLogger.debug('reusing existing ready app for %s', testFilePath);
441
- return;
508
+ if (crashMonitor.isAlive() && bridge.connection !== null && currentAppSession) {
509
+ const state = await currentAppSession.getState();
510
+ if (state.status === 'running') {
511
+ sessionLogger.debug('reusing existing ready app for %s', testFilePath);
512
+ return;
513
+ }
442
514
  }
443
515
  crashMonitor.reset();
444
516
  sessionLogger.debug('app not ready, waiting for launch and runtime readiness');
@@ -450,16 +522,16 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
450
522
  await hooks.drain();
451
523
  await crashMonitor.stop();
452
524
  sessionLogger.debug('restarting app (testFile=%s mode=%s)', testFilePath ?? 'n/a', testFilePath ? 'stop-and-ensure-ready' : 'direct-restart');
525
+ await disposeCurrentAppSession();
453
526
  if (testFilePath) {
454
- await platformInstance.stopApp();
527
+ await ensureAppReady(testFilePath);
455
528
  }
456
529
  else {
457
- await platformInstance.restartApp(appLaunchOptions);
458
- }
459
- crashMonitor.reset();
460
- await crashMonitor.start();
461
- if (testFilePath) {
462
- await ensureAppReady(testFilePath);
530
+ const session = await platformInstance.createAppSession(appLaunchOptions);
531
+ currentAppSession = session;
532
+ crashMonitor.setAppSession(session);
533
+ crashMonitor.reset();
534
+ await crashMonitor.start();
463
535
  }
464
536
  await hooks.drain();
465
537
  sessionLogger.debug('restart completed');
@@ -470,7 +542,7 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
470
542
  if (!conn)
471
543
  throw new Error('No active app connection');
472
544
  sessionLogger.debug('running test file on client: %s', testPath);
473
- if (!runtimeConfig.detectNativeCrashes) {
545
+ if (runtimeConfig.detectNativeCrashes === false) {
474
546
  const result = await conn.runTests(testPath, { ...options, runner: platform.runner });
475
547
  await hooks.drain();
476
548
  return result;
@@ -480,13 +552,30 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
480
552
  // crash wins the race or cancel() is called after the test run wins.
481
553
  crashWatch.promise.catch(ignorePromiseRejection);
482
554
  try {
483
- const result = await Promise.race([
484
- conn.runTests(testPath, { ...options, runner: platform.runner }),
485
- crashWatch.promise,
486
- ]);
555
+ const testRunPromise = conn.runTests(testPath, {
556
+ ...options,
557
+ runner: platform.runner,
558
+ });
559
+ const result = await Promise.race([testRunPromise, crashWatch.promise]);
560
+ const bridgeDisconnected = await waitForBridgeDisconnectOrTimeout({
561
+ bridge,
562
+ connection: conn,
563
+ timeoutMs: TEST_RUN_BRIDGE_STABILITY_WAIT_MS,
564
+ });
565
+ if (bridgeDisconnected) {
566
+ return await crashWatch.promise;
567
+ }
487
568
  await hooks.drain();
488
569
  return result;
489
570
  }
571
+ catch (error) {
572
+ if (isBridgeDisconnectError(error)) {
573
+ const result = await crashWatch.promise;
574
+ await hooks.drain();
575
+ return result;
576
+ }
577
+ throw error;
578
+ }
490
579
  finally {
491
580
  crashWatch.cancel();
492
581
  }
@@ -494,6 +583,12 @@ export const createHarnessSession = async (globalConfig, { lockManager = default
494
583
  return {
495
584
  config: runtimeConfig,
496
585
  context,
586
+ onTestRunnerEvent: (listener) => {
587
+ testRunnerEventListeners.add(listener);
588
+ return () => {
589
+ testRunnerEventListeners.delete(listener);
590
+ };
591
+ },
497
592
  runTestFile,
498
593
  ensureAppReady,
499
594
  restartApp,
package/dist/index.d.ts CHANGED
@@ -1,8 +1,12 @@
1
- import type { CallbackTestRunnerInterface, Config, OnTestFailure, OnTestStart, OnTestSuccess, Test, TestRunnerOptions, TestWatcher } from 'jest-runner';
2
- export default class JestHarness implements CallbackTestRunnerInterface {
1
+ import type { Config, EmittingTestRunnerInterface, Test, TestEvents, TestRunnerOptions, TestWatcher } from 'jest-runner';
2
+ type TestEventListener<Name extends keyof TestEvents> = (eventData: TestEvents[Name]) => void | Promise<void>;
3
+ export default class JestHarness implements EmittingTestRunnerInterface {
3
4
  #private;
4
5
  readonly isSerial = true;
6
+ readonly supportsEventEmitters: true;
5
7
  constructor(globalConfig: Config.GlobalConfig);
6
- runTests(tests: Array<Test>, watcher: TestWatcher, onStart: OnTestStart, onResult: OnTestSuccess, onFailure: OnTestFailure, options: TestRunnerOptions): Promise<void>;
8
+ on<Name extends keyof TestEvents>(eventName: Name, listener: TestEventListener<Name>): () => void;
9
+ runTests(tests: Array<Test>, watcher: TestWatcher, options: TestRunnerOptions): Promise<void>;
7
10
  }
11
+ export {};
8
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,2BAA2B,EAC3B,MAAM,EACN,aAAa,EACb,WAAW,EACX,aAAa,EACb,IAAI,EACJ,iBAAiB,EACjB,WAAW,EACZ,MAAM,aAAa,CAAC;AAMrB,MAAM,CAAC,OAAO,OAAO,WAAY,YAAW,2BAA2B;;IACrE,QAAQ,CAAC,QAAQ,QAAQ;gBAKb,YAAY,EAAE,MAAM,CAAC,YAAY;IAIvC,QAAQ,CACZ,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,aAAa,EACvB,SAAS,EAAE,aAAa,EACxB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,IAAI,CAAC;CAiCjB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,2BAA2B,EAC3B,IAAI,EACJ,UAAU,EACV,iBAAiB,EACjB,WAAW,EACZ,MAAM,aAAa,CAAC;AAMrB,KAAK,iBAAiB,CAAC,IAAI,SAAS,MAAM,UAAU,IAAI,CACtD,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC,KACxB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B,MAAM,CAAC,OAAO,OAAO,WAAY,YAAW,2BAA2B;;IACrE,QAAQ,CAAC,QAAQ,QAAQ;IACzB,QAAQ,CAAC,qBAAqB,EAAG,IAAI,CAAU;gBAMnC,YAAY,EAAE,MAAM,CAAC,YAAY;IAI7C,EAAE,CAAC,IAAI,SAAS,MAAM,UAAU,EAC9B,SAAS,EAAE,IAAI,EACf,QAAQ,EAAE,iBAAiB,CAAC,IAAI,CAAC,GAChC,MAAM,IAAI;IAaP,QAAQ,CACZ,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,IAAI,CAAC;CA6CjB"}
package/dist/index.js CHANGED
@@ -4,12 +4,25 @@ import { HarnessError } from '@react-native-harness/tools';
4
4
  import { getErrorMessage } from './logs.js';
5
5
  export default class JestHarness {
6
6
  isSerial = true;
7
+ supportsEventEmitters = true;
7
8
  #globalConfig;
8
9
  #session = null;
10
+ #listeners = new Map();
9
11
  constructor(globalConfig) {
10
12
  this.#globalConfig = globalConfig;
11
13
  }
12
- async runTests(tests, watcher, onStart, onResult, onFailure, options) {
14
+ on(eventName, listener) {
15
+ const listeners = this.#listeners.get(eventName) ?? new Set();
16
+ listeners.add(listener);
17
+ this.#listeners.set(eventName, listeners);
18
+ return () => {
19
+ listeners.delete(listener);
20
+ if (listeners.size === 0) {
21
+ this.#listeners.delete(eventName);
22
+ }
23
+ };
24
+ }
25
+ async runTests(tests, watcher, options) {
13
26
  if (!options.serial) {
14
27
  throw new Error('Parallel test running is not supported');
15
28
  }
@@ -18,7 +31,7 @@ export default class JestHarness {
18
31
  if (!this.#session) {
19
32
  this.#session = await createHarnessSession(this.#globalConfig);
20
33
  }
21
- await executeRun(this.#session, tests, watcher, onStart, onResult, onFailure, this.#globalConfig);
34
+ await executeRun(this.#session, tests, watcher, this.#emit, this.#globalConfig);
22
35
  }
23
36
  catch (error) {
24
37
  if (error instanceof HarnessError) {
@@ -33,4 +46,13 @@ export default class JestHarness {
33
46
  }
34
47
  }
35
48
  }
49
+ #emit = async (eventName, ...eventData) => {
50
+ const listeners = this.#listeners.get(eventName);
51
+ if (!listeners) {
52
+ return;
53
+ }
54
+ for (const listener of listeners) {
55
+ await listener(eventData);
56
+ }
57
+ };
36
58
  }
package/dist/run.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EACV,eAAe,IAAI,sBAAsB,EAE1C,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAuD3D,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,cAAc,CAAC;IACxB,aAAa,EAAE,UAAU,CAAC,aAAa,CAAC;IACxC,YAAY,EAAE,UAAU,CAAC,YAAY,CAAC;CACvC,KAAK,OAAO,CAAC;IACZ,UAAU,EAAE,cAAc,CAAC;IAC3B,aAAa,EAAE,sBAAsB,CAAC;IACtC,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CAAC;AAEH,eAAO,MAAM,kBAAkB,EAAE,kBAkFhC,CAAC"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EACV,eAAe,IAAI,sBAAsB,EAE1C,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAuD3D,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,cAAc,CAAC;IACxB,aAAa,EAAE,UAAU,CAAC,aAAa,CAAC;IACxC,YAAY,EAAE,UAAU,CAAC,YAAY,CAAC;CACvC,KAAK,OAAO,CAAC;IACZ,UAAU,EAAE,cAAc,CAAC;IAC3B,aAAa,EAAE,sBAAsB,CAAC;IACtC,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CAAC;AAEH,eAAO,MAAM,kBAAkB,EAAE,kBAmFhC,CAAC"}
package/dist/run.js CHANGED
@@ -67,6 +67,7 @@ export const runHarnessTestFile = async ({ testPath, globalConfig, projectConfig
67
67
  ? `${errorMessage}${codeFrame ? `\n\n${codeFrame.content}` : ''}`
68
68
  : undefined,
69
69
  title: test.name,
70
+ fullName: test.fullName,
70
71
  status: test.status,
71
72
  location: codeFrame?.location
72
73
  ? { column: codeFrame.location.column, line: codeFrame.location.row }
@@ -0,0 +1,5 @@
1
+ import type { TestResult as JestTestResult } from '@jest/test-result';
2
+ export declare const getHarnessTestFilePlatform: (testPath: string, knownPlatformIds: ReadonlySet<string>) => string | null;
3
+ export declare const shouldRunHarnessTestFile: (testPath: string, platformId: string, knownPlatformIds: ReadonlySet<string>) => boolean;
4
+ export declare const createPlatformSkippedTestResult: (testPath: string) => JestTestResult;
5
+ //# sourceMappingURL=test-file-platform-filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-file-platform-filter.d.ts","sourceRoot":"","sources":["../src/test-file-platform-filter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAMtE,eAAO,MAAM,0BAA0B,GACrC,UAAU,MAAM,EAChB,kBAAkB,WAAW,CAAC,MAAM,CAAC,KACpC,MAAM,GAAG,IAQX,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,UAAU,MAAM,EAChB,YAAY,MAAM,EAClB,kBAAkB,WAAW,CAAC,MAAM,CAAC,KACpC,OAGF,CAAC;AAEF,eAAO,MAAM,+BAA+B,GAC1C,UAAU,MAAM,KACf,cAyBF,CAAC"}
@@ -0,0 +1,40 @@
1
+ import path from 'node:path';
2
+ import { toTestResult } from './toTestResult.js';
3
+ const PLATFORM_SPECIFIC_HARNESS_FILE = /\.([^.]+)\.harness\.(?:[mc]?[jt]sx?)$/;
4
+ export const getHarnessTestFilePlatform = (testPath, knownPlatformIds) => {
5
+ const match = path.basename(testPath).match(PLATFORM_SPECIFIC_HARNESS_FILE);
6
+ if (!match) {
7
+ return null;
8
+ }
9
+ const candidate = match[1];
10
+ return knownPlatformIds.has(candidate) ? candidate : null;
11
+ };
12
+ export const shouldRunHarnessTestFile = (testPath, platformId, knownPlatformIds) => {
13
+ const filePlatform = getHarnessTestFilePlatform(testPath, knownPlatformIds);
14
+ return filePlatform == null || filePlatform === platformId;
15
+ };
16
+ export const createPlatformSkippedTestResult = (testPath) => {
17
+ const now = Date.now();
18
+ const title = path.basename(testPath);
19
+ return toTestResult({
20
+ stats: {
21
+ failures: 0,
22
+ passes: 0,
23
+ pending: 1,
24
+ todo: 0,
25
+ start: now,
26
+ end: now,
27
+ },
28
+ skipped: true,
29
+ errorMessage: null,
30
+ tests: [
31
+ {
32
+ status: 'skipped',
33
+ title,
34
+ fullName: title,
35
+ testPath,
36
+ },
37
+ ],
38
+ jestTestPath: testPath,
39
+ });
40
+ };
@@ -15,6 +15,7 @@ export type Options = {
15
15
  errorMessage?: string;
16
16
  testPath?: string;
17
17
  title?: string;
18
+ fullName?: string;
18
19
  status: Status;
19
20
  location?: {
20
21
  column: number;
@@ -1 +1 @@
1
- {"version":3,"file":"toTestResult.d.ts","sourceRoot":"","sources":["../src/toTestResult.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE5D,MAAM,MAAM,OAAO,GAAG;IACpB,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,KAAK,CAAC;QACX,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE;YACT,MAAM,EAAE,MAAM,CAAC;YACf,IAAI,EAAE,MAAM,CAAC;SACd,CAAC;QACF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;KAC3B,CAAC,CAAC;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC;CACnC,CAAC;AAwDF,eAAO,MAAM,YAAY,GAAI,SAAS,OAAO,KAAG,UAiB/C,CAAC"}
1
+ {"version":3,"file":"toTestResult.d.ts","sourceRoot":"","sources":["../src/toTestResult.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE5D,MAAM,MAAM,OAAO,GAAG;IACpB,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,KAAK,CAAC;QACX,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE;YACT,MAAM,EAAE,MAAM,CAAC;YACf,IAAI,EAAE,MAAM,CAAC;SACd,CAAC;QACF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;KAC3B,CAAC,CAAC;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC;CACnC,CAAC;AAwDF,eAAO,MAAM,YAAY,GAAI,SAAS,OAAO,KAAG,UAiB/C,CAAC"}
@@ -36,8 +36,8 @@ const getTestResults = ({ errorMessage, tests, jestTestPath, }) => {
36
36
  duration: test.duration,
37
37
  failureDetails: [],
38
38
  failureMessages: actualErrorMessage ? [actualErrorMessage] : [],
39
- fullName: jestTestPath || test.testPath || '',
40
- numPassingAsserts: test.errorMessage ? 1 : 0,
39
+ fullName: test.fullName || test.testPath || jestTestPath || '',
40
+ numPassingAsserts: test.status === 'passed' ? 1 : 0,
41
41
  status: test.status,
42
42
  title: test.title || '',
43
43
  location: test.location,