@react-native-harness/jest 1.0.0-canary.1761046904277

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 (55) 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 +23 -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 +57 -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 +9 -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 +49 -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 +7 -0
  28. package/dist/jest-preset.d.ts.map +1 -0
  29. package/dist/jest-preset.js +5 -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/toTestResult.d.ts +29 -0
  37. package/dist/toTestResult.d.ts.map +1 -0
  38. package/dist/toTestResult.js +64 -0
  39. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  40. package/dist/types.d.ts +17 -0
  41. package/dist/types.d.ts.map +1 -0
  42. package/dist/types.js +1 -0
  43. package/eslint.config.mjs +19 -0
  44. package/package.json +52 -0
  45. package/src/cli-args.ts +29 -0
  46. package/src/env.d.ts +9 -0
  47. package/src/global-setup.ts +88 -0
  48. package/src/global-teardown.ts +14 -0
  49. package/src/index.ts +105 -0
  50. package/src/jest-preset.ts +5 -0
  51. package/src/logs.ts +21 -0
  52. package/src/run.ts +147 -0
  53. package/src/toTestResult.ts +101 -0
  54. package/tsconfig.json +19 -0
  55. package/tsconfig.lib.json +24 -0
package/src/index.ts ADDED
@@ -0,0 +1,105 @@
1
+ import type {
2
+ CallbackTestRunnerInterface,
3
+ Config,
4
+ OnTestFailure,
5
+ OnTestStart,
6
+ OnTestSuccess,
7
+ Test,
8
+ TestRunnerOptions,
9
+ TestWatcher,
10
+ } from 'jest-runner';
11
+ import pLimit from 'p-limit';
12
+ import { runHarnessTestFile } from './run.js';
13
+ import { Config as HarnessConfig } from '@react-native-harness/config';
14
+ import type { Harness } from '@react-native-harness/cli/external';
15
+
16
+ class CancelRun extends Error {
17
+ constructor(message?: string) {
18
+ super(message);
19
+ this.name = 'CancelRun';
20
+ }
21
+ }
22
+
23
+ export default class JestHarness implements CallbackTestRunnerInterface {
24
+ readonly isSerial = true;
25
+
26
+ #globalConfig: Config.GlobalConfig;
27
+
28
+ constructor(globalConfig: Config.GlobalConfig) {
29
+ this.#globalConfig = globalConfig;
30
+ }
31
+
32
+ async runTests(
33
+ tests: Array<Test>,
34
+ watcher: TestWatcher,
35
+ onStart: OnTestStart,
36
+ onResult: OnTestSuccess,
37
+ onFailure: OnTestFailure,
38
+ options: TestRunnerOptions
39
+ ): Promise<void> {
40
+ if (!options.serial) {
41
+ throw new Error('Parallel test running is not supported');
42
+ }
43
+
44
+ const harness = global.HARNESS;
45
+ const harnessConfig = global.HARNESS_CONFIG;
46
+
47
+ return await this._createInBandTestRun(
48
+ tests,
49
+ watcher,
50
+ harness,
51
+ harnessConfig,
52
+ onStart,
53
+ onResult,
54
+ onFailure
55
+ );
56
+ }
57
+
58
+ async _createInBandTestRun(
59
+ tests: Array<Test>,
60
+ watcher: TestWatcher,
61
+ harness: Harness,
62
+ harnessConfig: HarnessConfig,
63
+ onStart: OnTestStart,
64
+ onResult: OnTestSuccess,
65
+ onFailure: OnTestFailure
66
+ ): Promise<void> {
67
+ const mutex = pLimit(1);
68
+ let isFirstTest = true;
69
+
70
+ return tests.reduce(
71
+ (promise, test) =>
72
+ mutex(() =>
73
+ promise
74
+ .then(async () => {
75
+ if (watcher.isInterrupted()) {
76
+ throw new CancelRun();
77
+ }
78
+
79
+ if (
80
+ harnessConfig.resetEnvironmentBetweenTestFiles &&
81
+ !isFirstTest
82
+ ) {
83
+ await new Promise((resolve) => {
84
+ harness.bridge.once('ready', resolve);
85
+ harness.environment.restart();
86
+ });
87
+ }
88
+ isFirstTest = false;
89
+
90
+ return onStart(test).then(() =>
91
+ runHarnessTestFile({
92
+ testPath: test.path,
93
+ harness,
94
+ globalConfig: this.#globalConfig,
95
+ projectConfig: test.context.config,
96
+ })
97
+ );
98
+ })
99
+ .then((result) => onResult(test, result))
100
+ .catch((err) => onFailure(test, err))
101
+ ),
102
+ Promise.resolve()
103
+ );
104
+ }
105
+ }
@@ -0,0 +1,5 @@
1
+ export default {
2
+ runner: '@react-native-harness/jest',
3
+ globalSetup: '@react-native-harness/jest/global-setup',
4
+ globalTeardown: '@react-native-harness/jest/global-teardown',
5
+ };
package/src/logs.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { TestRunnerConfig } from '@react-native-harness/config';
2
+ import chalk from 'chalk';
3
+
4
+ const TAG = chalk.supportsColor
5
+ ? chalk.reset.inverse.bold.magenta(` HARNESS `)
6
+ : 'HARNESS';
7
+
8
+ // @see https://github.com/jestjs/jest/blob/main/packages/jest-reporters/src/BaseReporter.ts#L25
9
+ export const log = (message: string): void => {
10
+ process.stderr.write(`${message}\n`);
11
+ };
12
+
13
+ export const logTestRunHeader = (runner: TestRunnerConfig): void => {
14
+ log(
15
+ `${TAG} Preparing to run tests using ${chalk.bold(runner.name)} runner\n`
16
+ );
17
+ };
18
+
19
+ export const logTestEnvironmentReady = (runner: TestRunnerConfig): void => {
20
+ log(`${TAG} Runner ${chalk.bold(runner.name)} ready\n`);
21
+ };
package/src/run.ts ADDED
@@ -0,0 +1,147 @@
1
+ import path from 'node:path';
2
+ import type { Config as JestConfig } from 'jest-runner';
3
+ import { TestResult as JestTestResult } from '@jest/test-result';
4
+ import type {
5
+ TestSuiteResult as HarnessTestSuiteResult,
6
+ TestResult as HarnessTestResult,
7
+ } from '@react-native-harness/bridge';
8
+ import type { Harness } from '@react-native-harness/cli/external';
9
+ import { formatResultsErrors } from 'jest-message-util';
10
+ import { toTestResult } from './toTestResult.js';
11
+
12
+ // Helper function to flatten nested test suites into a flat array of tests with hierarchy
13
+ const flattenTests = (
14
+ suiteResult: HarnessTestSuiteResult,
15
+ ancestorTitles: string[] = []
16
+ ): Array<HarnessTestResult & { ancestorTitles: string[] }> => {
17
+ const tests: Array<HarnessTestResult & { ancestorTitles: string[] }> = [];
18
+
19
+ // Add tests from current suite with current hierarchy
20
+ for (const test of suiteResult.tests) {
21
+ tests.push({
22
+ ...test,
23
+ ancestorTitles: [...ancestorTitles],
24
+ });
25
+ }
26
+
27
+ // Process child suites with updated hierarchy
28
+ for (const childSuite of suiteResult.suites) {
29
+ const newAncestorTitles = [...ancestorTitles, childSuite.name];
30
+ tests.push(...flattenTests(childSuite, newAncestorTitles));
31
+ }
32
+
33
+ return tests;
34
+ };
35
+
36
+ // Helper function to calculate test statistics
37
+ const calculateStats = (tests: HarnessTestResult[]) => {
38
+ let passes = 0;
39
+ let failures = 0;
40
+ let pending = 0;
41
+ let todo = 0;
42
+
43
+ for (const test of tests) {
44
+ switch (test.status) {
45
+ case 'passed':
46
+ passes++;
47
+ break;
48
+ case 'failed':
49
+ failures++;
50
+ break;
51
+ case 'skipped':
52
+ pending++;
53
+ break;
54
+ case 'todo':
55
+ todo++;
56
+ break;
57
+ }
58
+ }
59
+
60
+ return { passes, failures, pending, todo };
61
+ };
62
+
63
+ export type RunHarnessTestFile = (options: {
64
+ testPath: string;
65
+ harness: Harness;
66
+ projectConfig: JestConfig.ProjectConfig;
67
+ globalConfig: JestConfig.GlobalConfig;
68
+ }) => Promise<JestTestResult>;
69
+
70
+ export const runHarnessTestFile: RunHarnessTestFile = async ({
71
+ testPath,
72
+ globalConfig,
73
+ projectConfig,
74
+ harness,
75
+ }) => {
76
+ const start = Date.now();
77
+ const relativeTestPath = path.relative(globalConfig.rootDir, testPath);
78
+
79
+ // Extract setup files from Jest config and convert to relative paths
80
+ const setupFiles = projectConfig.setupFiles?.map((setupFile) =>
81
+ path.relative(globalConfig.rootDir, setupFile)
82
+ );
83
+ const setupFilesAfterEnv = projectConfig.setupFilesAfterEnv?.map(
84
+ (setupFile) => path.relative(globalConfig.rootDir, setupFile)
85
+ );
86
+
87
+ const client = harness.bridge.rpc.clients.at(-1)!;
88
+ const results = await client.runTests(relativeTestPath, {
89
+ testNamePattern: globalConfig.testNamePattern,
90
+ setupFiles,
91
+ setupFilesAfterEnv,
92
+ });
93
+ const end = Date.now();
94
+
95
+ const allTests = flattenTests(results);
96
+ const stats = calculateStats(allTests);
97
+
98
+ // Convert TestResult[] to the format expected by toTestResult
99
+ const tests = allTests.map((test) => {
100
+ const errorMessage = test.error?.message;
101
+ const codeFrame = test.error?.codeFrame;
102
+
103
+ return {
104
+ duration: test.duration,
105
+ errorMessage: errorMessage
106
+ ? `${errorMessage}${codeFrame ? `\n\n${codeFrame.content}` : ''}`
107
+ : undefined,
108
+ title: test.name,
109
+ status: test.status,
110
+ location: codeFrame?.location
111
+ ? { column: codeFrame.location.column, line: codeFrame.location.row }
112
+ : undefined,
113
+ ancestorTitles: test.ancestorTitles,
114
+ };
115
+ });
116
+
117
+ // Check if the entire suite was skipped
118
+ const skipped = results.status === 'skipped';
119
+
120
+ // Get error message from suite if it failed
121
+ const errorMessage = results.error?.message || null;
122
+
123
+ const totalResults = toTestResult({
124
+ stats: {
125
+ failures: stats.failures,
126
+ pending: stats.pending,
127
+ passes: stats.passes,
128
+ todo: stats.todo,
129
+ start,
130
+ end,
131
+ },
132
+ skipped,
133
+ errorMessage,
134
+ tests,
135
+ jestTestPath: testPath,
136
+ coverage: results.coverage as JestTestResult['coverage'],
137
+ });
138
+
139
+ totalResults.failureMessage = formatResultsErrors(
140
+ totalResults.testResults,
141
+ projectConfig,
142
+ globalConfig,
143
+ testPath
144
+ );
145
+
146
+ return totalResults;
147
+ };
@@ -0,0 +1,101 @@
1
+ import type { Status, TestResult } from '@jest/test-result';
2
+
3
+ export type Options = {
4
+ stats: {
5
+ failures: number;
6
+ passes: number;
7
+ pending: number;
8
+ todo: number;
9
+ start: number;
10
+ end: number;
11
+ };
12
+ skipped: boolean;
13
+ errorMessage?: string | null;
14
+ tests: Array<{
15
+ duration?: number | null;
16
+ errorMessage?: string;
17
+ testPath?: string;
18
+ title?: string;
19
+ status: Status;
20
+ location?: {
21
+ column: number;
22
+ line: number;
23
+ };
24
+ ancestorTitles?: string[];
25
+ }>;
26
+ jestTestPath: string;
27
+ coverage?: TestResult['coverage'];
28
+ };
29
+
30
+ const getPerfStats = ({ stats }: Options): TestResult['perfStats'] => {
31
+ const start = new Date(stats.start).getTime();
32
+ const end = new Date(stats.end).getTime();
33
+ const runtime = end - start;
34
+ // Note: this flag is set in 'lib/createJestRunner.ts'
35
+ const slow = false;
36
+ return {
37
+ start,
38
+ end,
39
+ runtime,
40
+ slow,
41
+ loadTestEnvironmentStart: 0,
42
+ loadTestEnvironmentEnd: 0,
43
+ setupAfterEnvStart: 0,
44
+ setupAfterEnvEnd: 0,
45
+ setupFilesStart: 0,
46
+ setupFilesEnd: 0,
47
+ };
48
+ };
49
+
50
+ const getSnapshot = (): TestResult['snapshot'] => {
51
+ return {
52
+ added: 0,
53
+ fileDeleted: false,
54
+ matched: 0,
55
+ unchecked: 0,
56
+ uncheckedKeys: [],
57
+ unmatched: 0,
58
+ updated: 0,
59
+ };
60
+ };
61
+
62
+ const getTestResults = ({
63
+ errorMessage,
64
+ tests,
65
+ jestTestPath,
66
+ }: Options): TestResult['testResults'] => {
67
+ return tests.map((test) => {
68
+ const actualErrorMessage = errorMessage || test.errorMessage;
69
+
70
+ return {
71
+ ancestorTitles: test.ancestorTitles || [],
72
+ duration: test.duration,
73
+ failureDetails: [],
74
+ failureMessages: actualErrorMessage ? [actualErrorMessage] : [],
75
+ fullName: jestTestPath || test.testPath || '',
76
+ numPassingAsserts: test.errorMessage ? 1 : 0,
77
+ status: test.status,
78
+ title: test.title || '',
79
+ location: test.location,
80
+ };
81
+ });
82
+ };
83
+
84
+ export const toTestResult = (options: Options): TestResult => {
85
+ const { stats, skipped, errorMessage, jestTestPath, coverage } = options;
86
+ return {
87
+ failureMessage: errorMessage,
88
+ leaks: false,
89
+ numFailingTests: stats.failures,
90
+ numPassingTests: stats.passes,
91
+ numPendingTests: stats.pending,
92
+ numTodoTests: stats.todo,
93
+ openHandles: [],
94
+ perfStats: getPerfStats(options),
95
+ skipped,
96
+ snapshot: getSnapshot(),
97
+ testFilePath: jestTestPath,
98
+ testResults: getTestResults(options),
99
+ coverage,
100
+ };
101
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "files": [],
4
+ "include": [],
5
+ "references": [
6
+ {
7
+ "path": "../config"
8
+ },
9
+ {
10
+ "path": "../bridge"
11
+ },
12
+ {
13
+ "path": "../cli"
14
+ },
15
+ {
16
+ "path": "./tsconfig.lib.json"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "rootDir": "src",
6
+ "outDir": "dist",
7
+ "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
8
+ "emitDeclarationOnly": false,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "types": ["node"]
11
+ },
12
+ "include": ["src/**/*.ts"],
13
+ "references": [
14
+ {
15
+ "path": "../config/tsconfig.lib.json"
16
+ },
17
+ {
18
+ "path": "../bridge/tsconfig.lib.json"
19
+ },
20
+ {
21
+ "path": "../cli/tsconfig.lib.json"
22
+ }
23
+ ]
24
+ }