Skip to content

Commit ff0c79a

Browse files
committed
feat: add harness-based jest runner
1 parent d1745df commit ff0c79a

20 files changed

Lines changed: 1325 additions & 457 deletions

apps/playground/jest.config.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = {
2+
projects: [
3+
{
4+
runner: '@react-native-harness/jest',
5+
testMatch: [
6+
'<rootDir>/src/__tests__/**/*.(test|spec|harness).(js|jsx|ts|tsx)',
7+
],
8+
},
9+
],
10+
};

apps/playground/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,9 @@
55
"dependencies": {
66
"react-native": "*",
77
"react-native-harness": "workspace:*"
8+
},
9+
"devDependencies": {
10+
"@react-native-harness/jest": "workspace:*",
11+
"jest": "^30.2.0"
812
}
913
}

apps/playground/tsconfig.app.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
"react-native-harness.d.ts"
3434
],
3535
"references": [
36+
{
37+
"path": "../../packages/jest/tsconfig.lib.json"
38+
},
3639
{
3740
"path": "../../packages/react-native-harness/tsconfig.lib.json"
3841
}

apps/playground/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"files": [],
44
"include": [],
55
"references": [
6+
{
7+
"path": "../../packages/jest"
8+
},
69
{
710
"path": "../../packages/react-native-harness"
811
},

packages/cli/package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@
55
"main": "./dist/index.js",
66
"module": "./dist/index.js",
77
"types": "./dist/index.d.ts",
8+
"exports": {
9+
"./package.json": "./package.json",
10+
".": {
11+
"development": "./src/index.ts",
12+
"types": "./dist/index.d.ts",
13+
"import": "./dist/index.js",
14+
"default": "./dist/index.js"
15+
},
16+
"./external": {
17+
"development": "./src/external.ts",
18+
"types": "./dist/external.d.ts",
19+
"import": "./dist/external.js",
20+
"default": "./dist/external.js"
21+
}
22+
},
823
"dependencies": {
924
"@react-native-harness/bridge": "workspace:*",
1025
"@react-native-harness/config": "workspace:*",

packages/cli/src/external.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { TestRunnerConfig } from '@react-native-harness/config';
2+
import { Environment } from './platforms/platform-adapter.js';
3+
import {
4+
BridgeServer,
5+
getBridgeServer,
6+
} from '@react-native-harness/bridge/server';
7+
import { BridgeTimeoutError } from './errors/errors.js';
8+
import { getPlatformAdapter } from './platforms/platform-registry.js';
9+
10+
export type Harness = {
11+
environment: Environment;
12+
bridge: BridgeServer;
13+
};
14+
15+
export const getHarness = async (
16+
runner: TestRunnerConfig
17+
): Promise<Harness> => {
18+
const bridgeTimeout = 60000;
19+
const platformAdapter = await getPlatformAdapter(runner.platform);
20+
const serverBridge = await getBridgeServer({
21+
port: 3001,
22+
});
23+
24+
const readyPromise = new Promise<void>((resolve, reject) => {
25+
const timeout = setTimeout(() => {
26+
reject(
27+
new BridgeTimeoutError(bridgeTimeout, runner.name, runner.platform)
28+
);
29+
}, bridgeTimeout);
30+
31+
serverBridge.once('ready', () => {
32+
clearTimeout(timeout);
33+
resolve();
34+
});
35+
});
36+
37+
const environment = await platformAdapter.getEnvironment(runner);
38+
await readyPromise;
39+
40+
return {
41+
environment,
42+
bridge: serverBridge,
43+
};
44+
};

packages/jest/README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

packages/jest/eslint.config.mjs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import baseConfig from '../../eslint.config.mjs';
2+
3+
export default [
4+
...baseConfig,
5+
{
6+
files: ['**/*.json'],
7+
rules: {
8+
'@nx/dependency-checks': [
9+
'error',
10+
{
11+
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'],
12+
},
13+
],
14+
},
15+
languageOptions: {
16+
parser: await import('jsonc-eslint-parser'),
17+
},
18+
},
19+
];

packages/jest/package.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "@react-native-harness/jest",
3+
"version": "1.0.0-alpha.15",
4+
"type": "module",
5+
"main": "./dist/index.js",
6+
"module": "./dist/index.js",
7+
"types": "./dist/index.d.ts",
8+
"exports": {
9+
"./package.json": "./package.json",
10+
".": {
11+
"development": "./src/index.ts",
12+
"types": "./dist/index.d.ts",
13+
"import": "./dist/index.js",
14+
"default": "./dist/index.js"
15+
}
16+
},
17+
"dependencies": {
18+
"@jest/test-result": "^30.2.0",
19+
"jest-runner": "^30.2.0",
20+
"p-limit": "^7.1.1",
21+
"tslib": "^2.3.0",
22+
"yargs": "^17.7.2",
23+
"@react-native-harness/cli": "workspace:*",
24+
"@react-native-harness/bridge": "workspace:*",
25+
"@react-native-harness/config": "workspace:*"
26+
},
27+
"devDependencies": {
28+
"@types/yargs": "^17.0.32"
29+
}
30+
}

packages/jest/src/cli-args.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import yargs from 'yargs';
2+
import { hideBin } from 'yargs/helpers';
3+
4+
export type HarnessCliArgs = {
5+
harnessRunner?: string;
6+
};
7+
8+
export const getAdditionalCliArgs = (): HarnessCliArgs => {
9+
const argv = yargs(hideBin(process.argv))
10+
.option('harnessRunner', {
11+
type: 'string',
12+
description: 'Specify which Harness runner to use',
13+
coerce: (value: string) => {
14+
if (!value || value.trim().length === 0) {
15+
throw new Error('harnessRunner must be a non-empty string');
16+
}
17+
return value.trim();
18+
},
19+
})
20+
.strict(false)
21+
.help(false)
22+
.version(false)
23+
.exitProcess(false)
24+
.parseSync();
25+
26+
return {
27+
harnessRunner: argv.harnessRunner,
28+
};
29+
};

0 commit comments

Comments
 (0)