@react-native-harness/jest 1.0.0-alpha.17

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 (61) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +40 -0
  3. package/dist/cli-args.d.ts +5 -0
  4. package/dist/cli-args.d.ts.map +1 -0
  5. package/dist/cli-args.js +17 -0
  6. package/dist/cli-args.test.d.ts +2 -0
  7. package/dist/cli-args.test.d.ts.map +1 -0
  8. package/dist/cli-args.test.js +1 -0
  9. package/dist/event-mapper.d.ts +16 -0
  10. package/dist/event-mapper.d.ts.map +1 -0
  11. package/dist/event-mapper.js +120 -0
  12. package/dist/global-setup.d.ts +3 -0
  13. package/dist/global-setup.d.ts.map +1 -0
  14. package/dist/global-setup.js +60 -0
  15. package/dist/global-teardown.d.ts +3 -0
  16. package/dist/global-teardown.d.ts.map +1 -0
  17. package/dist/global-teardown.js +11 -0
  18. package/dist/harness-reporter.d.ts +8 -0
  19. package/dist/harness-reporter.d.ts.map +1 -0
  20. package/dist/harness-reporter.js +12 -0
  21. package/dist/index.d.ts +11 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +64 -0
  24. package/dist/jest-options.d.ts +2 -0
  25. package/dist/jest-options.d.ts.map +1 -0
  26. package/dist/jest-options.js +1 -0
  27. package/dist/jest-preset.d.ts +5 -0
  28. package/dist/jest-preset.d.ts.map +1 -0
  29. package/dist/jest-preset.js +3 -0
  30. package/dist/logs.d.ts +5 -0
  31. package/dist/logs.d.ts.map +1 -0
  32. package/dist/logs.js +14 -0
  33. package/dist/run.d.ts +11 -0
  34. package/dist/run.d.ts.map +1 -0
  35. package/dist/run.js +98 -0
  36. package/dist/setup.d.ts +3 -0
  37. package/dist/setup.d.ts.map +1 -0
  38. package/dist/setup.js +60 -0
  39. package/dist/teardown.d.ts +3 -0
  40. package/dist/teardown.d.ts.map +1 -0
  41. package/dist/teardown.js +11 -0
  42. package/dist/toTestResult.d.ts +29 -0
  43. package/dist/toTestResult.d.ts.map +1 -0
  44. package/dist/toTestResult.js +64 -0
  45. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  46. package/dist/types.d.ts +17 -0
  47. package/dist/types.d.ts.map +1 -0
  48. package/dist/types.js +1 -0
  49. package/eslint.config.mjs +20 -0
  50. package/package.json +52 -0
  51. package/src/cli-args.ts +23 -0
  52. package/src/env.d.ts +9 -0
  53. package/src/index.ts +118 -0
  54. package/src/jest-preset.ts +3 -0
  55. package/src/logs.ts +21 -0
  56. package/src/run.ts +147 -0
  57. package/src/setup.ts +95 -0
  58. package/src/teardown.ts +15 -0
  59. package/src/toTestResult.ts +101 -0
  60. package/tsconfig.json +19 -0
  61. package/tsconfig.lib.json +25 -0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Callstack
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ ![harness-banner](https://react-native-harness.dev/harness-banner.jpg)
2
+
3
+ ### Experimental Jest Runner for React Native Harness
4
+
5
+ [![mit licence][license-badge]][license]
6
+ [![npm downloads][npm-downloads-badge]][npm-downloads]
7
+ [![Chat][chat-badge]][chat]
8
+ [![PRs Welcome][prs-welcome-badge]][prs-welcome]
9
+
10
+ ⚠️ **EXPERIMENTAL** ⚠️
11
+
12
+ An experimental Jest runner that integrates React Native Harness with Jest's infrastructure. This package allows you to leverage existing Jest features like watchers, coverage reporting, and the familiar developer experience while running React Native Harness tests.
13
+
14
+ ## Features
15
+
16
+ - **Jest Integration**: Re-uses Jest's existing infrastructure for a familiar developer experience
17
+ - **Watch Mode**: Automatic test re-running when files change
18
+ - **Coverage Reports**: Built-in code coverage support through Jest
19
+ - **Serial Execution**: Tests run sequentially to ensure proper React Native environment management
20
+ - **Harness Configuration**: Supports all React Native Harness runners and configurations
21
+
22
+ ## Usage
23
+
24
+ This runner is designed to work with Jest's test runner system. Configure it in your Jest configuration to run React Native Harness tests through Jest's interface.
25
+
26
+ ## Made with ❤️ at Callstack
27
+
28
+ `@react-native-harness/jest` is an open source project and will always remain free to use. If you think it's cool, please star it 🌟. [Callstack][callstack-readme-with-love] is a group of React and React Native geeks, contact us at [hello@callstack.com](mailto:hello@callstack.com) if you need any help with these or just want to say hi!
29
+
30
+ Like the project? ⚛️ [Join the team](https://callstack.com/careers/?utm_campaign=Senior_RN&utm_source=github&utm_medium=readme) who does amazing stuff for clients and drives React Native Open Source! 🔥
31
+
32
+ [callstack-readme-with-love]: https://callstack.com/?utm_source=github.com&utm_medium=referral&utm_campaign=react-native-harness&utm_term=readme-with-love
33
+ [license-badge]: https://img.shields.io/npm/l/@react-native-harness/jest?style=for-the-badge
34
+ [license]: https://github.com/callstackincubator/react-native-harness/blob/main/LICENSE
35
+ [npm-downloads-badge]: https://img.shields.io/npm/dm/@react-native-harness/jest?style=for-the-badge
36
+ [npm-downloads]: https://www.npmjs.com/package/@react-native-harness/jest
37
+ [prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
38
+ [prs-welcome]: ../../CONTRIBUTING.md
39
+ [chat-badge]: https://img.shields.io/discord/426714625279524876.svg?style=for-the-badge
40
+ [chat]: https://discord.gg/xgGt7KAjxv
@@ -0,0 +1,5 @@
1
+ export type HarnessCliArgs = {
2
+ harnessRunner?: string;
3
+ };
4
+ export declare const getAdditionalCliArgs: () => HarnessCliArgs;
5
+ //# sourceMappingURL=cli-args.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-args.d.ts","sourceRoot":"","sources":["../src/cli-args.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,cAAc,GAAG;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,eAAO,MAAM,oBAAoB,QAAO,cAevC,CAAC"}
@@ -0,0 +1,17 @@
1
+ import yargs from 'yargs';
2
+ import { hideBin } from 'yargs/helpers';
3
+ export const getAdditionalCliArgs = () => {
4
+ const argv = yargs(hideBin(process.argv))
5
+ .option('harnessRunner', {
6
+ type: 'string',
7
+ description: 'Specify which Harness runner to use',
8
+ })
9
+ .strict(false)
10
+ .help(false)
11
+ .version(false)
12
+ .exitProcess(false)
13
+ .parseSync();
14
+ return {
15
+ harnessRunner: argv.harnessRunner,
16
+ };
17
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli-args.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-args.test.d.ts","sourceRoot":"","sources":["../src/cli-args.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import type { TestEvents } from '@jest/test-result';
2
+ import type { Test } from 'jest-runner';
3
+ import type { BridgeEvents } from '@react-native-harness/bridge';
4
+ import type { TestCaseResult } from '@jest/test-result';
5
+ export type EventMapperState = {
6
+ currentTest?: Test;
7
+ suiteHierarchy: string[];
8
+ testResults: Map<string, TestCaseResult>;
9
+ fileStartTime: number;
10
+ };
11
+ export type HarnessEventMapper = {
12
+ handleBridgeEvent: (event: BridgeEvents, test: Test) => void;
13
+ reset: () => void;
14
+ };
15
+ export declare const getHarnessEventMapper: (emit: <T extends keyof TestEvents>(eventName: T, eventData: TestEvents[T]) => void) => HarnessEventMapper;
16
+ //# sourceMappingURL=event-mapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-mapper.d.ts","sourceRoot":"","sources":["../src/event-mapper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,KAAK,EASV,YAAY,EACb,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGxD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,CAAC,EAAE,IAAI,CAAC;IACnB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACzC,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iBAAiB,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAC7D,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,MAAM,CAAC,CAAC,SAAS,MAAM,UAAU,EAC/B,SAAS,EAAE,CAAC,EACZ,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC,KACrB,IAAI,KACR,kBA8JF,CAAC"}
@@ -0,0 +1,120 @@
1
+ export const getHarnessEventMapper = (emit) => {
2
+ const state = {
3
+ suiteHierarchy: [],
4
+ testResults: new Map(),
5
+ fileStartTime: 0,
6
+ };
7
+ const handleBridgeEvent = (event, test) => {
8
+ // Filter to only process test runner events
9
+ if (event.type.startsWith('test-') ||
10
+ event.type.startsWith('suite-') ||
11
+ event.type.startsWith('file-')) {
12
+ switch (event.type) {
13
+ case 'file-started':
14
+ handleFileStarted(event, test);
15
+ break;
16
+ case 'file-finished':
17
+ handleFileFinished(event, test);
18
+ break;
19
+ case 'suite-started':
20
+ handleSuiteStarted(event);
21
+ break;
22
+ case 'suite-finished':
23
+ handleSuiteFinished(event);
24
+ break;
25
+ case 'test-started':
26
+ handleTestStarted(event, test);
27
+ break;
28
+ case 'test-finished':
29
+ handleTestFinished(event, test);
30
+ break;
31
+ }
32
+ }
33
+ };
34
+ const handleFileStarted = (event, test) => {
35
+ state.currentTest = test;
36
+ state.fileStartTime = Date.now();
37
+ state.suiteHierarchy = [];
38
+ state.testResults.clear();
39
+ // Don't emit test-file-start here - that should be handled by the Jest runner
40
+ // when it actually starts running the test file
41
+ };
42
+ const handleFileFinished = (_event, _test) => {
43
+ // File finished - all test results should already be emitted
44
+ // The final TestResult will be handled by the runHarnessTestFile function
45
+ };
46
+ const handleSuiteStarted = (event) => {
47
+ state.suiteHierarchy.push(event.name);
48
+ };
49
+ const handleSuiteFinished = (event) => {
50
+ // Remove the suite from hierarchy when it finishes
51
+ const index = state.suiteHierarchy.lastIndexOf(event.name);
52
+ if (index !== -1) {
53
+ state.suiteHierarchy.splice(index, 1);
54
+ }
55
+ };
56
+ const handleTestStarted = (event, test) => {
57
+ const testCaseStartInfo = {
58
+ ancestorTitles: [...state.suiteHierarchy],
59
+ fullName: `${state.suiteHierarchy.join(' ')} ${event.name}`.trim(),
60
+ title: event.name,
61
+ mode: 'skip', // Default mode, will be updated based on test status
62
+ };
63
+ emit('test-case-start', [test.path, testCaseStartInfo]);
64
+ };
65
+ const handleTestFinished = (event, test) => {
66
+ const testCaseResult = {
67
+ ancestorTitles: [...state.suiteHierarchy],
68
+ duration: event.duration,
69
+ failureDetails: [],
70
+ failureMessages: event.error ? [formatError(event.error)] : [],
71
+ fullName: `${state.suiteHierarchy.join(' ')} ${event.name}`.trim(),
72
+ numPassingAsserts: event.status === 'passed' ? 1 : 0,
73
+ status: mapStatus(event.status),
74
+ title: event.name,
75
+ startedAt: Date.now() - event.duration,
76
+ };
77
+ state.testResults.set(event.name, testCaseResult);
78
+ emit('test-case-result', [test.path, testCaseResult]);
79
+ };
80
+ const mapStatus = (status) => {
81
+ return status;
82
+ };
83
+ const formatError = (error) => {
84
+ let errorMessage = `${error.name}: ${error.message}`;
85
+ if (error.codeFrame) {
86
+ errorMessage += '\n\n' + formatCodeFrame(error.codeFrame);
87
+ }
88
+ return errorMessage;
89
+ };
90
+ const formatCodeFrame = (codeFrame) => {
91
+ const lines = codeFrame.content.split('\n');
92
+ const lineNumber = codeFrame.location?.row || 1;
93
+ const column = codeFrame.location?.column || 1;
94
+ let formatted = `\n${codeFrame.fileName}:${lineNumber}:${column}\n`;
95
+ // Add context lines around the error
96
+ const startLine = Math.max(0, lineNumber - 3);
97
+ const endLine = Math.min(lines.length, lineNumber + 2);
98
+ for (let i = startLine; i < endLine; i++) {
99
+ const lineNum = i + 1;
100
+ const line = lines[i];
101
+ const isErrorLine = lineNum === lineNumber;
102
+ formatted += `${lineNum.toString().padStart(3)} | ${line}\n`;
103
+ if (isErrorLine && column > 0) {
104
+ const pointer = ' '.repeat(column + 5) + '^';
105
+ formatted += pointer + '\n';
106
+ }
107
+ }
108
+ return formatted;
109
+ };
110
+ const reset = () => {
111
+ state.suiteHierarchy = [];
112
+ state.testResults.clear();
113
+ state.fileStartTime = 0;
114
+ state.currentTest = undefined;
115
+ };
116
+ return {
117
+ handleBridgeEvent,
118
+ reset,
119
+ };
120
+ };
@@ -0,0 +1,3 @@
1
+ import type { Config as JestConfig } from 'jest-runner';
2
+ export default function (globalConfig: JestConfig.GlobalConfig): Promise<void>;
3
+ //# sourceMappingURL=global-setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"global-setup.d.ts","sourceRoot":"","sources":["../src/global-setup.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AA4CxD,yBAA+B,YAAY,EAAE,UAAU,CAAC,YAAY,iBA4CnE"}
@@ -0,0 +1,60 @@
1
+ import { getConfig, } from '@react-native-harness/config';
2
+ import { getHarness as getHarnessExternal, NoRunnerSpecifiedError, RunnerNotFoundError, } from '@react-native-harness/cli/external';
3
+ import { preRunMessage } from 'jest-util';
4
+ import { getAdditionalCliArgs } from './cli-args.js';
5
+ import { logTestEnvironmentReady, logTestRunHeader } from './logs.js';
6
+ const getHarnessConfig = async (globalConfig) => {
7
+ const projectRoot = globalConfig.rootDir;
8
+ const { config: harnessConfig } = await getConfig(projectRoot);
9
+ return harnessConfig;
10
+ };
11
+ const getHarnessRunner = (config, cliArgs) => {
12
+ const selectedRunnerName = cliArgs.harnessRunner ?? config.defaultRunner;
13
+ if (!selectedRunnerName) {
14
+ throw new NoRunnerSpecifiedError(config.runners);
15
+ }
16
+ const runner = config.runners.find((runner) => runner.name === selectedRunnerName);
17
+ if (!runner) {
18
+ throw new RunnerNotFoundError(selectedRunnerName, config.runners);
19
+ }
20
+ return runner;
21
+ };
22
+ const getHarness = async (runner) => {
23
+ return await getHarnessExternal(runner);
24
+ };
25
+ export default async function (globalConfig) {
26
+ preRunMessage.remove(process.stderr);
27
+ const harnessConfig = global.HARNESS_CONFIG ?? (await getHarnessConfig(globalConfig));
28
+ const isWatchMode = globalConfig.watch || globalConfig.watchAll;
29
+ if (global.HARNESS) {
30
+ // Do not setup again if HARNESS is already initialized
31
+ // This is useful when running tests in watch mode
32
+ if (harnessConfig.resetEnvironmentBetweenTestFiles) {
33
+ // In watch mode, we want to restart the environment before each test run
34
+ await new Promise((resolve) => {
35
+ global.HARNESS.bridge.once('ready', resolve);
36
+ global.HARNESS.environment.restart();
37
+ });
38
+ }
39
+ return;
40
+ }
41
+ if (isWatchMode) {
42
+ // In watch mode, we want to dispose the Harness when the process exits.
43
+ process.on('exit', async () => {
44
+ await global.HARNESS.bridge.dispose();
45
+ await global.HARNESS.environment.dispose();
46
+ });
47
+ }
48
+ const cliArgs = getAdditionalCliArgs();
49
+ const selectedRunner = getHarnessRunner(harnessConfig, cliArgs);
50
+ if (globalConfig.collectCoverage) {
51
+ // This is going to be used by @react-native-harness/babel-preset
52
+ // to enable instrumentation of test files.
53
+ process.env.RN_HARNESS_COLLECT_COVERAGE = 'true';
54
+ }
55
+ logTestRunHeader(selectedRunner);
56
+ const harness = await getHarness(selectedRunner);
57
+ logTestEnvironmentReady(selectedRunner);
58
+ global.HARNESS_CONFIG = harnessConfig;
59
+ global.HARNESS = harness;
60
+ }
@@ -0,0 +1,3 @@
1
+ import type { Config as JestConfig } from 'jest-runner';
2
+ export default function (globalConfig: JestConfig.GlobalConfig): Promise<void>;
3
+ //# sourceMappingURL=global-teardown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"global-teardown.d.ts","sourceRoot":"","sources":["../src/global-teardown.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AAExD,yBAA+B,YAAY,EAAE,UAAU,CAAC,YAAY,iBAYnE"}
@@ -0,0 +1,11 @@
1
+ export default async function (globalConfig) {
2
+ const isWatchMode = globalConfig.watch || globalConfig.watchAll;
3
+ if (isWatchMode) {
4
+ // In watch mode, we don't want to dispose the Harness.
5
+ return;
6
+ }
7
+ if (global.HARNESS) {
8
+ await global.HARNESS.bridge.dispose();
9
+ await global.HARNESS.environment.dispose();
10
+ }
11
+ }
@@ -0,0 +1,8 @@
1
+ import { BaseReporter } from '@jest/reporters';
2
+ import { Config } from '@jest/types';
3
+ export default class HarnessReporter extends BaseReporter {
4
+ #private;
5
+ constructor(globalConfig: Config.GlobalConfig);
6
+ onRunStart(): void;
7
+ }
8
+ //# sourceMappingURL=harness-reporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness-reporter.d.ts","sourceRoot":"","sources":["../src/harness-reporter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,MAAM,CAAC,OAAO,OAAO,eAAgB,SAAQ,YAAY;;gBAE3C,YAAY,EAAE,MAAM,CAAC,YAAY;IAKpC,UAAU,IAAI,IAAI;CAM5B"}
@@ -0,0 +1,12 @@
1
+ import { BaseReporter } from '@jest/reporters';
2
+ export default class HarnessReporter extends BaseReporter {
3
+ #globalConfig;
4
+ constructor(globalConfig) {
5
+ super();
6
+ this.#globalConfig = globalConfig;
7
+ }
8
+ onRunStart() {
9
+ super.onRunStart();
10
+ this.log(`Selected runner: ${this.#globalConfig.selectedRunner.name}`);
11
+ }
12
+ }
@@ -0,0 +1,11 @@
1
+ import type { CallbackTestRunnerInterface, Config, OnTestFailure, OnTestStart, OnTestSuccess, Test, TestRunnerOptions, TestWatcher } from 'jest-runner';
2
+ import { Config as HarnessConfig } from '@react-native-harness/config';
3
+ import { type Harness } from '@react-native-harness/cli/external';
4
+ export default class JestHarness implements CallbackTestRunnerInterface {
5
+ #private;
6
+ readonly isSerial = true;
7
+ constructor(globalConfig: Config.GlobalConfig);
8
+ runTests(tests: Array<Test>, watcher: TestWatcher, onStart: OnTestStart, onResult: OnTestSuccess, onFailure: OnTestFailure, options: TestRunnerOptions): Promise<void>;
9
+ _createInBandTestRun(tests: Array<Test>, watcher: TestWatcher, harness: Harness, harnessConfig: HarnessConfig, onStart: OnTestStart, onResult: OnTestSuccess, onFailure: OnTestFailure): Promise<void>;
10
+ }
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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;AAGrB,OAAO,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAe,KAAK,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAW/E,MAAM,CAAC,OAAO,OAAO,WAAY,YAAW,2BAA2B;;IACrE,QAAQ,CAAC,QAAQ,QAAQ;gBAIb,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;IA8BV,oBAAoB,CACxB,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,OAAO,EAChB,aAAa,EAAE,aAAa,EAC5B,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,aAAa,EACvB,SAAS,EAAE,aAAa,GACvB,OAAO,CAAC,IAAI,CAAC;CAuCjB"}
package/dist/index.js ADDED
@@ -0,0 +1,64 @@
1
+ import pLimit from 'p-limit';
2
+ import { runHarnessTestFile } from './run.js';
3
+ import { formatError } from '@react-native-harness/cli/external';
4
+ import { setup } from './setup.js';
5
+ import { teardown } from './teardown.js';
6
+ class CancelRun extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = 'CancelRun';
10
+ }
11
+ }
12
+ export default class JestHarness {
13
+ isSerial = true;
14
+ #globalConfig;
15
+ constructor(globalConfig) {
16
+ this.#globalConfig = globalConfig;
17
+ }
18
+ async runTests(tests, watcher, onStart, onResult, onFailure, options) {
19
+ if (!options.serial) {
20
+ throw new Error('Parallel test running is not supported');
21
+ }
22
+ try {
23
+ // This is necessary as Harness may throw and we want to catch it and display a helpful error message.
24
+ await setup(this.#globalConfig);
25
+ const harness = global.HARNESS;
26
+ const harnessConfig = global.HARNESS_CONFIG;
27
+ return await this._createInBandTestRun(tests, watcher, harness, harnessConfig, onStart, onResult, onFailure);
28
+ }
29
+ catch (error) {
30
+ // Jest will print strings as they are, without processing them further.
31
+ throw formatError(error);
32
+ }
33
+ finally {
34
+ // This is necessary as Harness may throw and we want to catch it and display a helpful error message.
35
+ await teardown(this.#globalConfig);
36
+ }
37
+ }
38
+ async _createInBandTestRun(tests, watcher, harness, harnessConfig, onStart, onResult, onFailure) {
39
+ const mutex = pLimit(1);
40
+ let isFirstTest = true;
41
+ return tests.reduce((promise, test) => mutex(() => promise
42
+ .then(async () => {
43
+ if (watcher.isInterrupted()) {
44
+ throw new CancelRun();
45
+ }
46
+ if (harnessConfig.resetEnvironmentBetweenTestFiles &&
47
+ !isFirstTest) {
48
+ await new Promise((resolve) => {
49
+ harness.bridge.once('ready', resolve);
50
+ harness.environment.restart();
51
+ });
52
+ }
53
+ isFirstTest = false;
54
+ return onStart(test).then(() => runHarnessTestFile({
55
+ testPath: test.path,
56
+ harness,
57
+ globalConfig: this.#globalConfig,
58
+ projectConfig: test.context.config,
59
+ }));
60
+ })
61
+ .then((result) => onResult(test, result))
62
+ .catch((err) => onFailure(test, err))), Promise.resolve());
63
+ }
64
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=jest-options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jest-options.d.ts","sourceRoot":"","sources":["../src/jest-options.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ declare const _default: {
2
+ runner: string;
3
+ };
4
+ export default _default;
5
+ //# sourceMappingURL=jest-preset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jest-preset.d.ts","sourceRoot":"","sources":["../src/jest-preset.ts"],"names":[],"mappings":";;;AAAA,wBAEE"}
@@ -0,0 +1,3 @@
1
+ export default {
2
+ runner: '@react-native-harness/jest',
3
+ };
package/dist/logs.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { TestRunnerConfig } from '@react-native-harness/config';
2
+ export declare const log: (message: string) => void;
3
+ export declare const logTestRunHeader: (runner: TestRunnerConfig) => void;
4
+ export declare const logTestEnvironmentReady: (runner: TestRunnerConfig) => void;
5
+ //# sourceMappingURL=logs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logs.d.ts","sourceRoot":"","sources":["../src/logs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAQhE,eAAO,MAAM,GAAG,GAAI,SAAS,MAAM,KAAG,IAErC,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,QAAQ,gBAAgB,KAAG,IAI3D,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,gBAAgB,KAAG,IAElE,CAAC"}
package/dist/logs.js ADDED
@@ -0,0 +1,14 @@
1
+ import chalk from 'chalk';
2
+ const TAG = chalk.supportsColor
3
+ ? chalk.reset.inverse.bold.magenta(` HARNESS `)
4
+ : 'HARNESS';
5
+ // @see https://github.com/jestjs/jest/blob/main/packages/jest-reporters/src/BaseReporter.ts#L25
6
+ export const log = (message) => {
7
+ process.stderr.write(`${message}\n`);
8
+ };
9
+ export const logTestRunHeader = (runner) => {
10
+ log(`${TAG} Preparing to run tests using ${chalk.bold(runner.name)} runner\n`);
11
+ };
12
+ export const logTestEnvironmentReady = (runner) => {
13
+ log(`${TAG} Runner ${chalk.bold(runner.name)} ready\n`);
14
+ };
package/dist/run.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { Config as JestConfig } from 'jest-runner';
2
+ import { TestResult as JestTestResult } from '@jest/test-result';
3
+ import type { Harness } from '@react-native-harness/cli/external';
4
+ export type RunHarnessTestFile = (options: {
5
+ testPath: string;
6
+ harness: Harness;
7
+ projectConfig: JestConfig.ProjectConfig;
8
+ globalConfig: JestConfig.GlobalConfig;
9
+ }) => Promise<JestTestResult>;
10
+ export declare const runHarnessTestFile: RunHarnessTestFile;
11
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +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;AAKjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAuDlE,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,UAAU,CAAC,aAAa,CAAC;IACxC,YAAY,EAAE,UAAU,CAAC,YAAY,CAAC;CACvC,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;AAE9B,eAAO,MAAM,kBAAkB,EAAE,kBA6EhC,CAAC"}
package/dist/run.js ADDED
@@ -0,0 +1,98 @@
1
+ import path from 'node:path';
2
+ import { formatResultsErrors } from 'jest-message-util';
3
+ import { toTestResult } from './toTestResult.js';
4
+ // Helper function to flatten nested test suites into a flat array of tests with hierarchy
5
+ const flattenTests = (suiteResult, ancestorTitles = []) => {
6
+ const tests = [];
7
+ // Add tests from current suite with current hierarchy
8
+ for (const test of suiteResult.tests) {
9
+ tests.push({
10
+ ...test,
11
+ ancestorTitles: [...ancestorTitles],
12
+ });
13
+ }
14
+ // Process child suites with updated hierarchy
15
+ for (const childSuite of suiteResult.suites) {
16
+ const newAncestorTitles = [...ancestorTitles, childSuite.name];
17
+ tests.push(...flattenTests(childSuite, newAncestorTitles));
18
+ }
19
+ return tests;
20
+ };
21
+ // Helper function to calculate test statistics
22
+ const calculateStats = (tests) => {
23
+ let passes = 0;
24
+ let failures = 0;
25
+ let pending = 0;
26
+ let todo = 0;
27
+ for (const test of tests) {
28
+ switch (test.status) {
29
+ case 'passed':
30
+ passes++;
31
+ break;
32
+ case 'failed':
33
+ failures++;
34
+ break;
35
+ case 'skipped':
36
+ pending++;
37
+ break;
38
+ case 'todo':
39
+ todo++;
40
+ break;
41
+ }
42
+ }
43
+ return { passes, failures, pending, todo };
44
+ };
45
+ export const runHarnessTestFile = async ({ testPath, globalConfig, projectConfig, harness, }) => {
46
+ const start = Date.now();
47
+ const relativeTestPath = path.relative(globalConfig.rootDir, testPath);
48
+ // Extract setup files from Jest config and convert to relative paths
49
+ const setupFiles = projectConfig.setupFiles?.map((setupFile) => path.relative(globalConfig.rootDir, setupFile));
50
+ const setupFilesAfterEnv = projectConfig.setupFilesAfterEnv?.map((setupFile) => path.relative(globalConfig.rootDir, setupFile));
51
+ const client = harness.bridge.rpc.clients.at(-1);
52
+ const results = await client.runTests(relativeTestPath, {
53
+ testNamePattern: globalConfig.testNamePattern,
54
+ setupFiles,
55
+ setupFilesAfterEnv,
56
+ });
57
+ const end = Date.now();
58
+ const allTests = flattenTests(results);
59
+ const stats = calculateStats(allTests);
60
+ // Convert TestResult[] to the format expected by toTestResult
61
+ const tests = allTests.map((test) => {
62
+ const errorMessage = test.error?.message;
63
+ const codeFrame = test.error?.codeFrame;
64
+ return {
65
+ duration: test.duration,
66
+ errorMessage: errorMessage
67
+ ? `${errorMessage}${codeFrame ? `\n\n${codeFrame.content}` : ''}`
68
+ : undefined,
69
+ title: test.name,
70
+ status: test.status,
71
+ location: codeFrame?.location
72
+ ? { column: codeFrame.location.column, line: codeFrame.location.row }
73
+ : undefined,
74
+ ancestorTitles: test.ancestorTitles,
75
+ };
76
+ });
77
+ // Check if the entire suite was skipped
78
+ const skipped = results.status === 'skipped';
79
+ // Get error message from suite if it failed
80
+ const errorMessage = results.error?.message || null;
81
+ const totalResults = toTestResult({
82
+ stats: {
83
+ failures: stats.failures,
84
+ pending: stats.pending,
85
+ passes: stats.passes,
86
+ todo: stats.todo,
87
+ start,
88
+ end,
89
+ },
90
+ skipped,
91
+ errorMessage,
92
+ tests,
93
+ jestTestPath: testPath,
94
+ coverage: results.coverage,
95
+ });
96
+ totalResults.failureMessage = formatResultsErrors(totalResults.testResults, projectConfig, globalConfig, testPath);
97
+ return totalResults;
98
+ };
@@ -0,0 +1,3 @@
1
+ import type { Config as JestConfig } from 'jest-runner';
2
+ export declare const setup: (globalConfig: JestConfig.GlobalConfig) => Promise<void>;
3
+ //# sourceMappingURL=setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AA4CxD,eAAO,MAAM,KAAK,GAAU,cAAc,UAAU,CAAC,YAAY,kBA4ChE,CAAC"}