@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
@@ -7,11 +7,13 @@ import {
7
7
  HARNESS_BRIDGE_PATH,
8
8
  type HarnessContext,
9
9
  type BridgeEvents,
10
+ type TestRunnerEvents,
10
11
  type TestExecutionOptions,
11
12
  type TestSuiteResult,
12
13
  } from '@react-native-harness/bridge';
13
14
  import {
14
15
  type AppLaunchOptions,
16
+ type AppSession,
15
17
  type HarnessPlatform,
16
18
  type HarnessPlatformInitOptions,
17
19
  type HarnessPlatformRunner,
@@ -33,8 +35,8 @@ import {
33
35
  type HarnessRunSummary,
34
36
  } from '@react-native-harness/plugins';
35
37
  import {
36
- logger,
37
38
  createCrashArtifactWriter,
39
+ logger,
38
40
  getTimeoutSignal,
39
41
  raceAbortSignals,
40
42
  } from '@react-native-harness/tools';
@@ -45,6 +47,7 @@ import {
45
47
  } from '@react-native-harness/config';
46
48
  import type { Config as JestConfig } from 'jest-runner';
47
49
  import { preRunMessage } from 'jest-util';
50
+ import path from 'node:path';
48
51
  import { PlatformReadyTimeoutError } from './errors.js';
49
52
  import { NoRunnerSpecifiedError, RunnerNotFoundError } from './errors.js';
50
53
  import { createCrashMonitor, type CrashMonitor } from './crash-monitor.js';
@@ -75,6 +78,48 @@ import {
75
78
  const sessionLogger = logger.child('runtime');
76
79
  const defaultResourceLockManager = createResourceLockManager();
77
80
  const ignorePromiseRejection = () => undefined;
81
+ const isBridgeDisconnectError = (error: unknown) =>
82
+ error instanceof Error && error.message === 'App bridge disconnected';
83
+ const TEST_RUN_BRIDGE_STABILITY_WAIT_MS = 500;
84
+
85
+ type DisconnectObservableBridge = {
86
+ readonly connection: AppConnection | null;
87
+ on: (event: 'disconnected', listener: () => void) => void;
88
+ off: (event: 'disconnected', listener: () => void) => void;
89
+ };
90
+
91
+ export const waitForBridgeDisconnectOrTimeout = async ({
92
+ bridge,
93
+ connection,
94
+ timeoutMs,
95
+ }: {
96
+ bridge: DisconnectObservableBridge;
97
+ connection: AppConnection;
98
+ timeoutMs: number;
99
+ }): Promise<boolean> => {
100
+ if (bridge.connection !== connection) {
101
+ return true;
102
+ }
103
+
104
+ return await new Promise<boolean>((resolve) => {
105
+ const onDisconnected = () => {
106
+ cleanup();
107
+ resolve(true);
108
+ };
109
+
110
+ const cleanup = () => {
111
+ clearTimeout(timer);
112
+ bridge.off('disconnected', onDisconnected);
113
+ };
114
+
115
+ const timer = setTimeout(() => {
116
+ cleanup();
117
+ resolve(bridge.connection !== connection);
118
+ }, timeoutMs);
119
+
120
+ bridge.on('disconnected', onDisconnected);
121
+ });
122
+ };
78
123
 
79
124
  export type HarnessRunState = {
80
125
  readonly runId: string;
@@ -92,6 +137,7 @@ export type HarnessRunTestsOptions = Exclude<TestExecutionOptions, 'platform'>;
92
137
  export type HarnessSession = {
93
138
  readonly config: HarnessConfig;
94
139
  readonly context: HarnessContext;
140
+ onTestRunnerEvent: (listener: (event: TestRunnerEvents) => void) => () => void;
95
141
  runTestFile: (path: string, options: HarnessRunTestsOptions) => Promise<TestSuiteResult>;
96
142
  ensureAppReady: (testFilePath: string) => Promise<void>;
97
143
  restartApp: (testFilePath?: string) => Promise<void>;
@@ -147,13 +193,37 @@ const withPlatformReadyTimeout = async <T>(options: {
147
193
  type AppReadyOptions = {
148
194
  metroInstance: MetroInstance;
149
195
  bridge: HarnessBridge;
150
- platformInstance: HarnessPlatformRunner;
151
196
  platformId: string;
152
197
  bundleStartTimeout: number;
153
198
  readyTimeout: number;
154
199
  maxAppRestarts: number;
155
200
  crashMonitor: CrashMonitor;
156
- appLaunchOptions?: AppLaunchOptions;
201
+ detectNativeCrashes: boolean;
202
+ restartAppSession: () => Promise<AppSession>;
203
+ };
204
+
205
+ export const waitForStartupCrash = async ({
206
+ crashMonitor,
207
+ detectNativeCrashes,
208
+ testFilePath,
209
+ signal,
210
+ }: {
211
+ crashMonitor: CrashMonitor;
212
+ detectNativeCrashes: boolean;
213
+ testFilePath: string;
214
+ signal: AbortSignal;
215
+ }) => {
216
+ if (!detectNativeCrashes) {
217
+ return await waitForAbort(signal);
218
+ }
219
+
220
+ const watch = crashMonitor.watch(testFilePath, 'startup');
221
+ watch.promise.catch(ignorePromiseRejection); // suppress unhandled-rejection when abort wins race
222
+ try {
223
+ return await Promise.race([watch.promise, waitForAbort(signal)]);
224
+ } finally {
225
+ watch.cancel();
226
+ }
157
227
  };
158
228
 
159
229
  const waitForAppReady = async (
@@ -163,13 +233,13 @@ const waitForAppReady = async (
163
233
  const {
164
234
  metroInstance,
165
235
  bridge,
166
- platformInstance,
167
236
  platformId,
168
237
  bundleStartTimeout,
169
238
  readyTimeout,
170
239
  maxAppRestarts,
171
240
  crashMonitor,
172
- appLaunchOptions,
241
+ detectNativeCrashes,
242
+ restartAppSession,
173
243
  } = base;
174
244
 
175
245
  const logWait = (message: string, ...args: unknown[]) =>
@@ -184,7 +254,7 @@ const waitForAppReady = async (
184
254
  signal: new AbortController().signal,
185
255
  startAttempt: async () => {
186
256
  logWait('launching app for %s', testFilePath);
187
- await platformInstance.restartApp(appLaunchOptions);
257
+ await restartAppSession();
188
258
  logWait('launch request completed, waiting for bridge ready');
189
259
  },
190
260
  waitForReady: async (signal) => {
@@ -195,7 +265,7 @@ const waitForAppReady = async (
195
265
  // connection from a previous run would resolve the promise before startAttempt
196
266
  // even restarts the app — leaving bridge.connection null after the restart.
197
267
  await new Promise<void>((resolve, reject) => {
198
- const onConnected = (_conn: AppConnection) => { cleanup(); resolve(); };
268
+ const onConnected = () => { cleanup(); resolve(); };
199
269
  const onAbort = () => { cleanup(); reject(signal.reason ?? new DOMException('Aborted', 'AbortError')); };
200
270
  const cleanup = () => {
201
271
  bridge.off('connected', onConnected);
@@ -208,14 +278,13 @@ const waitForAppReady = async (
208
278
  logWait('runtime ready received');
209
279
  },
210
280
  waitForCrash: async (signal) => {
211
- const watch = crashMonitor.watch(testFilePath, 'startup');
212
- watch.promise.catch(ignorePromiseRejection); // suppress unhandled-rejection when abort wins race
213
- try {
214
- logWait('waiting for crash or runtime ready');
215
- return await Promise.race([watch.promise, waitForAbort(signal)]);
216
- } finally {
217
- watch.cancel();
218
- }
281
+ logWait('waiting for crash or runtime ready');
282
+ return await waitForStartupCrash({
283
+ crashMonitor,
284
+ detectNativeCrashes,
285
+ testFilePath,
286
+ signal,
287
+ });
219
288
  },
220
289
  onAttemptStart: () => {
221
290
  logWait('beginning launch attempt for %s', testFilePath);
@@ -413,6 +482,11 @@ export const createHarnessSession = async (
413
482
  const clientLogCollector = createClientLogCollector();
414
483
 
415
484
  const context: HarnessContext = { platform };
485
+ const crashArtifactWriter = createCrashArtifactWriter({
486
+ runnerName: platform.name,
487
+ platformId: platform.platformId,
488
+ rootDir: path.join(projectRoot, '.harness', 'crash-reports'),
489
+ });
416
490
 
417
491
  const bridge = await createHarnessBridge({
418
492
  noServer: true,
@@ -446,6 +520,7 @@ export const createHarnessSession = async (
446
520
  return await import(platform.runner).then((module) =>
447
521
  module.default(platform.config, runtimeConfig, {
448
522
  signal,
523
+ crashArtifactWriter,
449
524
  } satisfies HarnessPlatformInitOptions),
450
525
  ).then((instance) => {
451
526
  sessionLogger.debug('platform runner initialized');
@@ -460,27 +535,42 @@ export const createHarnessSession = async (
460
535
  throw error;
461
536
  }
462
537
 
463
- const crashArtifactWriter = createCrashArtifactWriter({
464
- runnerName: platform.name,
465
- platformId: platform.platformId,
466
- });
467
- const appMonitor = platformInstance.createAppMonitor({ crashArtifactWriter });
468
538
  const appLaunchOptions = (platform.config as { appLaunchOptions?: AppLaunchOptions }).appLaunchOptions;
469
539
 
470
- const crashMonitor = createCrashMonitor({ appMonitor, platformRunner: platformInstance });
540
+ let currentAppSession: AppSession | null = null;
541
+ const crashMonitor = createCrashMonitor();
542
+
543
+ const disposeCurrentAppSession = async () => {
544
+ const session = currentAppSession;
545
+ currentAppSession = null;
546
+ crashMonitor.setAppSession(null);
547
+ if (session) {
548
+ await session.dispose();
549
+ }
550
+ };
551
+
552
+ const restartAppSession = async (): Promise<AppSession> => {
553
+ await crashMonitor.stop();
554
+ await disposeCurrentAppSession();
555
+ const session = await platformInstance.createAppSession(appLaunchOptions);
556
+ currentAppSession = session;
557
+ crashMonitor.setAppSession(session);
558
+ await crashMonitor.start();
559
+ return session;
560
+ };
471
561
 
472
562
  // Pre-build the options that are constant across all app-ready calls;
473
563
  // only testFilePath varies per call.
474
564
  const appReadyBaseOptions: AppReadyOptions = {
475
565
  metroInstance,
476
566
  bridge,
477
- platformInstance,
478
567
  platformId: platform.platformId,
479
568
  bundleStartTimeout: runtimeConfig.bundleStartTimeout ?? 60000,
480
569
  readyTimeout: runtimeConfig.bridgeTimeout,
481
570
  maxAppRestarts: runtimeConfig.maxAppRestarts ?? 2,
482
571
  crashMonitor,
483
- appLaunchOptions,
572
+ detectNativeCrashes: runtimeConfig.detectNativeCrashes !== false,
573
+ restartAppSession,
484
574
  };
485
575
 
486
576
  // --- Event listeners ---
@@ -497,6 +587,19 @@ export const createHarnessSession = async (
497
587
  const flushClientLogs = (): ClientLogBuffer => clientLogCollector.flush();
498
588
 
499
589
  const clientLogListener = clientLogCollector.handleEvent;
590
+ const testRunnerEventListeners = new Set<(event: TestRunnerEvents) => void>();
591
+ const onTestRunnerEvent = (event: BridgeEvents) => {
592
+ if (
593
+ event.type === 'test-started' ||
594
+ event.type === 'test-finished' ||
595
+ event.type === 'suite-started' ||
596
+ event.type === 'suite-finished' ||
597
+ event.type === 'file-started' ||
598
+ event.type === 'file-finished'
599
+ ) {
600
+ testRunnerEventListeners.forEach((listener) => listener(event));
601
+ }
602
+ };
500
603
 
501
604
  const onConnected = (conn: AppConnection) => {
502
605
  const runId = getCurrentRunId();
@@ -508,11 +611,15 @@ export const createHarnessSession = async (
508
611
  const runId = getCurrentRunId();
509
612
  if (!runId) return;
510
613
  hooks.schedule(() => pluginManager.callHook('runtime:disconnected', { runId, reason: 'bridge-disconnected' }));
614
+ if (runtimeConfig.detectNativeCrashes !== false) {
615
+ crashMonitor.handleBridgeDisconnect();
616
+ }
511
617
  };
512
618
 
513
619
  bridge.on('connected', onConnected);
514
620
  bridge.on('disconnected', onDisconnected);
515
621
  bridge.on('event', bridgeEventListener);
622
+ bridge.on('event', onTestRunnerEvent);
516
623
  metroInstance.events.addListener(onMetroEvent);
517
624
  if (runtimeConfig.forwardClientLogs) {
518
625
  metroInstance.events.addListener(clientLogListener);
@@ -557,11 +664,13 @@ export const createHarnessSession = async (
557
664
  bridge.off('connected', onConnected);
558
665
  bridge.off('disconnected', onDisconnected);
559
666
  bridge.off('event', bridgeEventListener);
667
+ bridge.off('event', onTestRunnerEvent);
560
668
 
561
669
  const nativeCoverageConfig = runtimeConfig.coverage?.native?.ios;
562
670
  if (nativeCoverageConfig?.pods?.length && platformInstance.collectNativeCoverage) {
563
671
  try {
564
- await platformInstance.stopApp();
672
+ await crashMonitor.stop();
673
+ await disposeCurrentAppSession();
565
674
  const lcovPath = await platformInstance.collectNativeCoverage({
566
675
  pods: nativeCoverageConfig.pods,
567
676
  outputDir: projectRoot,
@@ -578,6 +687,7 @@ export const createHarnessSession = async (
578
687
  try {
579
688
  await Promise.all([
580
689
  crashMonitor.dispose(),
690
+ disposeCurrentAppSession(),
581
691
  bridge.dispose(),
582
692
  platformInstance.dispose(),
583
693
  metroInstance.dispose(),
@@ -614,8 +724,6 @@ export const createHarnessSession = async (
614
724
  try {
615
725
  await pluginManager.callHook('harness:before-creation', { appLaunchOptions });
616
726
  await hooks.drain();
617
- await appMonitor.start();
618
- sessionLogger.debug('app monitor started');
619
727
  await pluginManager.callHook('harness:before-run', { appLaunchOptions });
620
728
  await hooks.drain();
621
729
  } catch (error) {
@@ -634,9 +742,12 @@ export const createHarnessSession = async (
634
742
  await hooks.drain();
635
743
  sessionLogger.debug('ensuring app is ready for %s', testFilePath);
636
744
 
637
- if (crashMonitor.isAlive() && bridge.connection !== null && await platformInstance.isAppRunning()) {
638
- sessionLogger.debug('reusing existing ready app for %s', testFilePath);
639
- return;
745
+ if (crashMonitor.isAlive() && bridge.connection !== null && currentAppSession) {
746
+ const state = await currentAppSession.getState();
747
+ if (state.status === 'running') {
748
+ sessionLogger.debug('reusing existing ready app for %s', testFilePath);
749
+ return;
750
+ }
640
751
  }
641
752
 
642
753
  crashMonitor.reset();
@@ -655,17 +766,16 @@ export const createHarnessSession = async (
655
766
  testFilePath ? 'stop-and-ensure-ready' : 'direct-restart',
656
767
  );
657
768
 
658
- if (testFilePath) {
659
- await platformInstance.stopApp();
660
- } else {
661
- await platformInstance.restartApp(appLaunchOptions);
662
- }
663
-
664
- crashMonitor.reset();
665
- await crashMonitor.start();
769
+ await disposeCurrentAppSession();
666
770
 
667
771
  if (testFilePath) {
668
772
  await ensureAppReady(testFilePath);
773
+ } else {
774
+ const session = await platformInstance.createAppSession(appLaunchOptions);
775
+ currentAppSession = session;
776
+ crashMonitor.setAppSession(session);
777
+ crashMonitor.reset();
778
+ await crashMonitor.start();
669
779
  }
670
780
 
671
781
  await hooks.drain();
@@ -681,7 +791,7 @@ export const createHarnessSession = async (
681
791
  if (!conn) throw new Error('No active app connection');
682
792
  sessionLogger.debug('running test file on client: %s', testPath);
683
793
 
684
- if (!runtimeConfig.detectNativeCrashes) {
794
+ if (runtimeConfig.detectNativeCrashes === false) {
685
795
  const result = await conn.runTests(testPath, { ...options, runner: platform.runner });
686
796
  await hooks.drain();
687
797
  return result;
@@ -692,12 +802,31 @@ export const createHarnessSession = async (
692
802
  // crash wins the race or cancel() is called after the test run wins.
693
803
  crashWatch.promise.catch(ignorePromiseRejection);
694
804
  try {
695
- const result = await Promise.race([
696
- conn.runTests(testPath, { ...options, runner: platform.runner }),
697
- crashWatch.promise,
698
- ]);
805
+ const testRunPromise = conn.runTests(testPath, {
806
+ ...options,
807
+ runner: platform.runner,
808
+ });
809
+ const result = await Promise.race([testRunPromise, crashWatch.promise]);
810
+ const bridgeDisconnected = await waitForBridgeDisconnectOrTimeout({
811
+ bridge,
812
+ connection: conn,
813
+ timeoutMs: TEST_RUN_BRIDGE_STABILITY_WAIT_MS,
814
+ });
815
+
816
+ if (bridgeDisconnected) {
817
+ return await crashWatch.promise;
818
+ }
819
+
699
820
  await hooks.drain();
700
821
  return result;
822
+ } catch (error) {
823
+ if (isBridgeDisconnectError(error)) {
824
+ const result = await crashWatch.promise;
825
+ await hooks.drain();
826
+ return result;
827
+ }
828
+
829
+ throw error;
701
830
  } finally {
702
831
  crashWatch.cancel();
703
832
  }
@@ -706,6 +835,12 @@ export const createHarnessSession = async (
706
835
  return {
707
836
  config: runtimeConfig,
708
837
  context,
838
+ onTestRunnerEvent: (listener) => {
839
+ testRunnerEventListeners.add(listener);
840
+ return () => {
841
+ testRunnerEventListeners.delete(listener);
842
+ };
843
+ },
709
844
  runTestFile,
710
845
  ensureAppReady,
711
846
  restartApp,
package/src/index.ts CHANGED
@@ -1,10 +1,8 @@
1
1
  import type {
2
- CallbackTestRunnerInterface,
3
2
  Config,
4
- OnTestFailure,
5
- OnTestStart,
6
- OnTestSuccess,
3
+ EmittingTestRunnerInterface,
7
4
  Test,
5
+ TestEvents,
8
6
  TestRunnerOptions,
9
7
  TestWatcher,
10
8
  } from 'jest-runner';
@@ -13,22 +11,41 @@ import { executeRun } from './execute-run.js';
13
11
  import { HarnessError } from '@react-native-harness/tools';
14
12
  import { getErrorMessage } from './logs.js';
15
13
 
16
- export default class JestHarness implements CallbackTestRunnerInterface {
14
+ type TestEventListener<Name extends keyof TestEvents> = (
15
+ eventData: TestEvents[Name],
16
+ ) => void | Promise<void>;
17
+
18
+ export default class JestHarness implements EmittingTestRunnerInterface {
17
19
  readonly isSerial = true;
20
+ readonly supportsEventEmitters = true as const;
18
21
 
19
22
  #globalConfig: Config.GlobalConfig;
20
23
  #session: HarnessSession | null = null;
24
+ #listeners = new Map<keyof TestEvents, Set<TestEventListener<keyof TestEvents>>>();
21
25
 
22
26
  constructor(globalConfig: Config.GlobalConfig) {
23
27
  this.#globalConfig = globalConfig;
24
28
  }
25
29
 
30
+ on<Name extends keyof TestEvents>(
31
+ eventName: Name,
32
+ listener: TestEventListener<Name>,
33
+ ): () => void {
34
+ const listeners = this.#listeners.get(eventName) ?? new Set();
35
+ listeners.add(listener as TestEventListener<keyof TestEvents>);
36
+ this.#listeners.set(eventName, listeners);
37
+
38
+ return () => {
39
+ listeners.delete(listener as TestEventListener<keyof TestEvents>);
40
+ if (listeners.size === 0) {
41
+ this.#listeners.delete(eventName);
42
+ }
43
+ };
44
+ }
45
+
26
46
  async runTests(
27
47
  tests: Array<Test>,
28
48
  watcher: TestWatcher,
29
- onStart: OnTestStart,
30
- onResult: OnTestSuccess,
31
- onFailure: OnTestFailure,
32
49
  options: TestRunnerOptions,
33
50
  ): Promise<void> {
34
51
  if (!options.serial) {
@@ -46,9 +63,7 @@ export default class JestHarness implements CallbackTestRunnerInterface {
46
63
  this.#session,
47
64
  tests,
48
65
  watcher,
49
- onStart,
50
- onResult,
51
- onFailure,
66
+ this.#emit,
52
67
  this.#globalConfig,
53
68
  );
54
69
  } catch (error) {
@@ -63,4 +78,18 @@ export default class JestHarness implements CallbackTestRunnerInterface {
63
78
  }
64
79
  }
65
80
  }
81
+
82
+ #emit = async <Name extends keyof TestEvents>(
83
+ eventName: Name,
84
+ ...eventData: TestEvents[Name]
85
+ ): Promise<void> => {
86
+ const listeners = this.#listeners.get(eventName);
87
+ if (!listeners) {
88
+ return;
89
+ }
90
+
91
+ for (const listener of listeners) {
92
+ await listener(eventData);
93
+ }
94
+ };
66
95
  }
package/src/run.ts CHANGED
@@ -111,6 +111,7 @@ export const runHarnessTestFile: RunHarnessTestFile = async ({
111
111
  ? `${errorMessage}${codeFrame ? `\n\n${codeFrame.content}` : ''}`
112
112
  : undefined,
113
113
  title: test.name,
114
+ fullName: test.fullName,
114
115
  status: test.status,
115
116
  location: codeFrame?.location
116
117
  ? { column: codeFrame.location.column, line: codeFrame.location.row }
@@ -0,0 +1,57 @@
1
+ import path from 'node:path';
2
+ import type { TestResult as JestTestResult } from '@jest/test-result';
3
+ import { toTestResult } from './toTestResult.js';
4
+
5
+ const PLATFORM_SPECIFIC_HARNESS_FILE =
6
+ /\.([^.]+)\.harness\.(?:[mc]?[jt]sx?)$/;
7
+
8
+ export const getHarnessTestFilePlatform = (
9
+ testPath: string,
10
+ knownPlatformIds: ReadonlySet<string>,
11
+ ): string | null => {
12
+ const match = path.basename(testPath).match(PLATFORM_SPECIFIC_HARNESS_FILE);
13
+ if (!match) {
14
+ return null;
15
+ }
16
+
17
+ const candidate = match[1];
18
+ return knownPlatformIds.has(candidate) ? candidate : null;
19
+ };
20
+
21
+ export const shouldRunHarnessTestFile = (
22
+ testPath: string,
23
+ platformId: string,
24
+ knownPlatformIds: ReadonlySet<string>,
25
+ ): boolean => {
26
+ const filePlatform = getHarnessTestFilePlatform(testPath, knownPlatformIds);
27
+ return filePlatform == null || filePlatform === platformId;
28
+ };
29
+
30
+ export const createPlatformSkippedTestResult = (
31
+ testPath: string,
32
+ ): JestTestResult => {
33
+ const now = Date.now();
34
+ const title = path.basename(testPath);
35
+
36
+ return toTestResult({
37
+ stats: {
38
+ failures: 0,
39
+ passes: 0,
40
+ pending: 1,
41
+ todo: 0,
42
+ start: now,
43
+ end: now,
44
+ },
45
+ skipped: true,
46
+ errorMessage: null,
47
+ tests: [
48
+ {
49
+ status: 'skipped',
50
+ title,
51
+ fullName: title,
52
+ testPath,
53
+ },
54
+ ],
55
+ jestTestPath: testPath,
56
+ });
57
+ };
@@ -16,6 +16,7 @@ export type Options = {
16
16
  errorMessage?: string;
17
17
  testPath?: string;
18
18
  title?: string;
19
+ fullName?: string;
19
20
  status: Status;
20
21
  location?: {
21
22
  column: number;
@@ -72,8 +73,8 @@ const getTestResults = ({
72
73
  duration: test.duration,
73
74
  failureDetails: [],
74
75
  failureMessages: actualErrorMessage ? [actualErrorMessage] : [],
75
- fullName: jestTestPath || test.testPath || '',
76
- numPassingAsserts: test.errorMessage ? 1 : 0,
76
+ fullName: test.fullName || test.testPath || jestTestPath || '',
77
+ numPassingAsserts: test.status === 'passed' ? 1 : 0,
77
78
  status: test.status,
78
79
  title: test.title || '',
79
80
  location: test.location,