From ae0edb11d069d9117123799c6478e457c387cb1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Tue, 20 Jan 2026 07:43:02 +0100 Subject: [PATCH 1/7] fix(coverage): add coverageRoot option for monorepo setups In create-react-native-library projects, tests run from example/ but source files are in ../src/. babel-plugin-istanbul uses cwd as its root and skips files outside it, causing 0% coverage. Added coverageRoot config option to specify the root directory for coverage instrumentation. This is passed to babel-plugin-istanbul's cwd option. Fixes #58 --- packages/babel-preset/src/preset.ts | 21 +++++++++++++++++++-- packages/config/src/types.ts | 7 +++++++ packages/jest/src/setup.ts | 4 ++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/babel-preset/src/preset.ts b/packages/babel-preset/src/preset.ts index 1d9a4b1..54df99c 100644 --- a/packages/babel-preset/src/preset.ts +++ b/packages/babel-preset/src/preset.ts @@ -1,10 +1,27 @@ import resolveWeakPlugin from './resolve-weak-plugin'; +import path from 'path'; + +const getIstanbulPlugin = (): string | [string, object] | null => { + if (!process.env.RN_HARNESS_COLLECT_COVERAGE) { + return null; + } + + const coverageRoot = process.env.RN_HARNESS_COVERAGE_ROOT; + if (coverageRoot) { + return [ + 'babel-plugin-istanbul', + { cwd: path.resolve(process.cwd(), coverageRoot) }, + ]; + } + + return 'babel-plugin-istanbul'; +}; export const rnHarnessPlugins = [ '@babel/plugin-transform-class-static-block', resolveWeakPlugin, - process.env.RN_HARNESS_COLLECT_COVERAGE ? 'babel-plugin-istanbul' : null, -].filter((plugin): plugin is string => plugin !== null); + getIstanbulPlugin(), +].filter((plugin) => plugin !== null); export const rnHarnessPreset = () => { if (!process.env.RN_HARNESS) { diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index e0580cc..06093a1 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -46,6 +46,13 @@ export const ConfigSchema = z .min(100, 'Crash detection interval must be at least 100ms') .default(500), + coverageRoot: z + .string() + .optional() + .describe( + 'Root directory for coverage instrumentation. Defaults to cwd. Use ".." for create-react-native-library projects where source is in parent directory.' + ), + // Deprecated property - used for migration detection include: z.array(z.string()).optional(), }) diff --git a/packages/jest/src/setup.ts b/packages/jest/src/setup.ts index d22e53f..612c3ab 100644 --- a/packages/jest/src/setup.ts +++ b/packages/jest/src/setup.ts @@ -68,6 +68,10 @@ export const setup = async (globalConfig: JestConfig.GlobalConfig) => { // This is going to be used by @react-native-harness/babel-preset // to enable instrumentation of test files. process.env.RN_HARNESS_COLLECT_COVERAGE = 'true'; + + if (harnessConfig.coverageRoot) { + process.env.RN_HARNESS_COVERAGE_ROOT = harnessConfig.coverageRoot; + } } logTestRunHeader(selectedRunner); From 50eab12bf0fac2d20b802fcb601898c5454b6c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Tue, 20 Jan 2026 14:17:41 +0100 Subject: [PATCH 2/7] refactor: use nested coverage.root instead of flat coverageRoot --- actions/shared/index.cjs | 14 +++- docs/coverage-investigation.md | 98 ++++++++++++++++++++++++++ docs/issues/coverage-monorepo-issue.md | 53 ++++++++++++++ packages/config/src/types.ts | 16 +++-- packages/jest/src/setup.ts | 4 +- 5 files changed, 176 insertions(+), 9 deletions(-) create mode 100644 docs/coverage-investigation.md create mode 100644 docs/issues/coverage-monorepo-issue.md diff --git a/actions/shared/index.cjs b/actions/shared/index.cjs index 41e40ce..f708bf9 100644 --- a/actions/shared/index.cjs +++ b/actions/shared/index.cjs @@ -4209,16 +4209,28 @@ var coerce = { var NEVER = INVALID; // ../config/dist/types.js +var RunnerSchema = external_exports.object({ + name: external_exports.string().min(1, "Runner name is required").regex(/^[a-zA-Z0-9._-]+$/, "Runner name can only contain alphanumeric characters, dots, underscores, and hyphens"), + config: external_exports.record(external_exports.any()), + runner: external_exports.string() +}); var ConfigSchema = external_exports.object({ entryPoint: external_exports.string().min(1, "Entry point is required"), appRegistryComponentName: external_exports.string().min(1, "App registry component name is required"), - runners: external_exports.array(external_exports.any()).min(1, "At least one runner is required"), + runners: external_exports.array(RunnerSchema).min(1, "At least one runner is required"), defaultRunner: external_exports.string().optional(), webSocketPort: external_exports.number().optional().default(3001), bridgeTimeout: external_exports.number().min(1e3, "Bridge timeout must be at least 1 second").default(6e4), + bundleStartTimeout: external_exports.number().min(1e3, "Bundle start timeout must be at least 1 second").default(15e3), + maxAppRestarts: external_exports.number().min(0, "Max app restarts must be non-negative").default(2), resetEnvironmentBetweenTestFiles: external_exports.boolean().optional().default(true), unstable__skipAlreadyIncludedModules: external_exports.boolean().optional().default(false), unstable__enableMetroCache: external_exports.boolean().optional().default(false), + detectNativeCrashes: external_exports.boolean().optional().default(true), + crashDetectionInterval: external_exports.number().min(100, "Crash detection interval must be at least 100ms").default(500), + coverage: external_exports.object({ + root: external_exports.string().optional().describe('Root directory for coverage instrumentation. Defaults to cwd. Use ".." for create-react-native-library projects where source is in parent directory.') + }).optional(), // Deprecated property - used for migration detection include: external_exports.array(external_exports.string()).optional() }).refine((config) => { diff --git a/docs/coverage-investigation.md b/docs/coverage-investigation.md new file mode 100644 index 0000000..2f6bac2 --- /dev/null +++ b/docs/coverage-investigation.md @@ -0,0 +1,98 @@ +# Coverage Investigation + +## The Real Issue: Monorepo/create-react-native-library Projects + +Coverage reports 0% in create-react-native-library projects because: + +1. Tests run from `example/` directory (`process.cwd()`) +2. Source files are in `../src/` +3. `babel-plugin-istanbul` uses `cwd` as its root +4. Istanbul's `test-exclude` **skips files outside cwd** +5. Files in `../src/` get skipped → **0% coverage** + +``` +[istanbul] shouldSkip(/project/src/index.tsx) = true + cwd=/project/example + include=[] +``` + +## The Fix + +Added `coverage.root` config option to specify where source files live. + +### Usage + +```javascript +// rn-harness.config.mjs +export default { + entryPoint: './src/App.tsx', + appRegistryComponentName: 'main', + coverage: { + root: '..', // Point to parent where library src/ lives + }, + runners: [/* ... */], +}; +``` + +### Implementation + +1. **`packages/config/src/types.ts`** - Added `coverage.root` to config schema +2. **`packages/babel-preset/src/preset.ts`** - Pass `cwd` option to babel-plugin-istanbul when `RN_HARNESS_COVERAGE_ROOT` is set +3. **`packages/jest/src/setup.ts`** - Pass `coverage.root` from config to env var + +--- + +## Ruled Out: Env Var Timing Issue + +We initially suspected `rnHarnessPlugins` was evaluated before `RN_HARNESS_COLLECT_COVERAGE` was set. + +**Investigation with file-based logging proved this is NOT the issue:** + +``` +PID 54120 (Jest main process): + 05:43:37.331Z - babel-preset loaded, env var = undefined + 05:43:37.348Z - setup() sets RN_HARNESS_COLLECT_COVERAGE = true + +PID 54299 (Metro worker - spawned ~5s later): + 05:43:42.055Z - babel-preset loaded, env var = true ← Inherited from parent! +``` + +Metro workers **inherit** env vars from the parent process. By the time Metro forks workers, the env var is already set. + +--- + +## How to Troubleshoot Coverage Issues + +If you encounter 0% coverage: + +### 1. Check if it's a monorepo/cwd issue + +Add logging to see what istanbul is doing: + +```typescript +// In babel-preset, temporarily add: +console.log('[istanbul] cwd:', process.cwd()); +console.log('[istanbul] file being transformed:', filename); +``` + +If files are outside `cwd`, that's the issue → use `coverageRoot` config. + +### 2. Check env var timing (unlikely to be the issue) + +Add to `packages/babel-preset/src/preset.ts`: +```typescript +import { appendFileSync } from 'fs'; +const log = (msg: string) => { + try { appendFileSync('/tmp/harness-debug.log', `${new Date().toISOString()} ${msg}\n`); } catch {} +}; +log(`[babel-preset] PID=${process.pid}, RN_HARNESS_COLLECT_COVERAGE=${process.env.RN_HARNESS_COLLECT_COVERAGE}`); +``` + +Run tests and check: +```bash +rm -f /tmp/harness-debug.log +pnpm test:harness --coverage +cat /tmp/harness-debug.log +``` + +Metro workers (different PIDs) should see `RN_HARNESS_COLLECT_COVERAGE=true`. diff --git a/docs/issues/coverage-monorepo-issue.md b/docs/issues/coverage-monorepo-issue.md new file mode 100644 index 0000000..eb37bcd --- /dev/null +++ b/docs/issues/coverage-monorepo-issue.md @@ -0,0 +1,53 @@ +# Bug Report: `--coverage` reports 0% in create-react-native-library monorepo setups + +## Describe the bug + +When running harness tests with `--coverage` in a create-react-native-library project, coverage always reports 0% for library source files even though tests pass and execute the code. + +**What I expect:** Coverage should show actual percentages for files in `../src/` + +**What actually happens:** Coverage shows 0% for all files + +``` +# Expected +File | % Stmts | +src/index.tsx | 47.36 | + +# Actual +All files | 0 | +``` + +**Root cause:** `babel-plugin-istanbul` uses `process.cwd()` as its working directory. In create-react-native-library projects, tests run from `example/` but source files are in `../src/`. Istanbul's `test-exclude` skips files outside `cwd`, so all library source files are excluded from coverage. + +## System Info + +``` +(Any create-react-native-library project) +``` + +## React Native Harness Version + +1.0.0-alpha.23 + +## Reproduction + +https://github.com/rive-app/rive-nitro-react-native + +(Or any project created with create-react-native-library) + +## Steps to reproduce + +1. Create a library with `npx create-react-native-library` +2. Set up react-native-harness in the `example/` directory +3. Add harness tests that import and use library code from `../src/` +4. Run `yarn test:harness:ios --coverage` +5. Observe coverage shows 0% for all library files + +**Debug output confirming the issue:** +``` +[istanbul] shouldSkip(/project/src/index.tsx) = true + cwd=/project/example + include=[] +``` + +Files outside `cwd` with empty `include` are always skipped by istanbul's `test-exclude`. diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 06093a1..e88336f 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -46,12 +46,16 @@ export const ConfigSchema = z .min(100, 'Crash detection interval must be at least 100ms') .default(500), - coverageRoot: z - .string() - .optional() - .describe( - 'Root directory for coverage instrumentation. Defaults to cwd. Use ".." for create-react-native-library projects where source is in parent directory.' - ), + coverage: z + .object({ + root: z + .string() + .optional() + .describe( + 'Root directory for coverage instrumentation. Defaults to cwd. Use ".." for create-react-native-library projects where source is in parent directory.' + ), + }) + .optional(), // Deprecated property - used for migration detection include: z.array(z.string()).optional(), diff --git a/packages/jest/src/setup.ts b/packages/jest/src/setup.ts index 612c3ab..c41d784 100644 --- a/packages/jest/src/setup.ts +++ b/packages/jest/src/setup.ts @@ -69,8 +69,8 @@ export const setup = async (globalConfig: JestConfig.GlobalConfig) => { // to enable instrumentation of test files. process.env.RN_HARNESS_COLLECT_COVERAGE = 'true'; - if (harnessConfig.coverageRoot) { - process.env.RN_HARNESS_COVERAGE_ROOT = harnessConfig.coverageRoot; + if (harnessConfig.coverage?.root) { + process.env.RN_HARNESS_COVERAGE_ROOT = harnessConfig.coverage.root; } } From 2341d9c162c2558da11098b32adbe40bbc37c928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Tue, 20 Jan 2026 14:20:26 +0100 Subject: [PATCH 3/7] remove issue template from PR --- docs/issues/coverage-monorepo-issue.md | 53 -------------------------- 1 file changed, 53 deletions(-) delete mode 100644 docs/issues/coverage-monorepo-issue.md diff --git a/docs/issues/coverage-monorepo-issue.md b/docs/issues/coverage-monorepo-issue.md deleted file mode 100644 index eb37bcd..0000000 --- a/docs/issues/coverage-monorepo-issue.md +++ /dev/null @@ -1,53 +0,0 @@ -# Bug Report: `--coverage` reports 0% in create-react-native-library monorepo setups - -## Describe the bug - -When running harness tests with `--coverage` in a create-react-native-library project, coverage always reports 0% for library source files even though tests pass and execute the code. - -**What I expect:** Coverage should show actual percentages for files in `../src/` - -**What actually happens:** Coverage shows 0% for all files - -``` -# Expected -File | % Stmts | -src/index.tsx | 47.36 | - -# Actual -All files | 0 | -``` - -**Root cause:** `babel-plugin-istanbul` uses `process.cwd()` as its working directory. In create-react-native-library projects, tests run from `example/` but source files are in `../src/`. Istanbul's `test-exclude` skips files outside `cwd`, so all library source files are excluded from coverage. - -## System Info - -``` -(Any create-react-native-library project) -``` - -## React Native Harness Version - -1.0.0-alpha.23 - -## Reproduction - -https://github.com/rive-app/rive-nitro-react-native - -(Or any project created with create-react-native-library) - -## Steps to reproduce - -1. Create a library with `npx create-react-native-library` -2. Set up react-native-harness in the `example/` directory -3. Add harness tests that import and use library code from `../src/` -4. Run `yarn test:harness:ios --coverage` -5. Observe coverage shows 0% for all library files - -**Debug output confirming the issue:** -``` -[istanbul] shouldSkip(/project/src/index.tsx) = true - cwd=/project/example - include=[] -``` - -Files outside `cwd` with empty `include` are always skipped by istanbul's `test-exclude`. From f43e21369d17f46c882d41467846c7a7e9fb301c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Tue, 20 Jan 2026 14:20:34 +0100 Subject: [PATCH 4/7] remove local docs from PR --- docs/coverage-investigation.md | 98 ---------------------------------- 1 file changed, 98 deletions(-) delete mode 100644 docs/coverage-investigation.md diff --git a/docs/coverage-investigation.md b/docs/coverage-investigation.md deleted file mode 100644 index 2f6bac2..0000000 --- a/docs/coverage-investigation.md +++ /dev/null @@ -1,98 +0,0 @@ -# Coverage Investigation - -## The Real Issue: Monorepo/create-react-native-library Projects - -Coverage reports 0% in create-react-native-library projects because: - -1. Tests run from `example/` directory (`process.cwd()`) -2. Source files are in `../src/` -3. `babel-plugin-istanbul` uses `cwd` as its root -4. Istanbul's `test-exclude` **skips files outside cwd** -5. Files in `../src/` get skipped → **0% coverage** - -``` -[istanbul] shouldSkip(/project/src/index.tsx) = true - cwd=/project/example - include=[] -``` - -## The Fix - -Added `coverage.root` config option to specify where source files live. - -### Usage - -```javascript -// rn-harness.config.mjs -export default { - entryPoint: './src/App.tsx', - appRegistryComponentName: 'main', - coverage: { - root: '..', // Point to parent where library src/ lives - }, - runners: [/* ... */], -}; -``` - -### Implementation - -1. **`packages/config/src/types.ts`** - Added `coverage.root` to config schema -2. **`packages/babel-preset/src/preset.ts`** - Pass `cwd` option to babel-plugin-istanbul when `RN_HARNESS_COVERAGE_ROOT` is set -3. **`packages/jest/src/setup.ts`** - Pass `coverage.root` from config to env var - ---- - -## Ruled Out: Env Var Timing Issue - -We initially suspected `rnHarnessPlugins` was evaluated before `RN_HARNESS_COLLECT_COVERAGE` was set. - -**Investigation with file-based logging proved this is NOT the issue:** - -``` -PID 54120 (Jest main process): - 05:43:37.331Z - babel-preset loaded, env var = undefined - 05:43:37.348Z - setup() sets RN_HARNESS_COLLECT_COVERAGE = true - -PID 54299 (Metro worker - spawned ~5s later): - 05:43:42.055Z - babel-preset loaded, env var = true ← Inherited from parent! -``` - -Metro workers **inherit** env vars from the parent process. By the time Metro forks workers, the env var is already set. - ---- - -## How to Troubleshoot Coverage Issues - -If you encounter 0% coverage: - -### 1. Check if it's a monorepo/cwd issue - -Add logging to see what istanbul is doing: - -```typescript -// In babel-preset, temporarily add: -console.log('[istanbul] cwd:', process.cwd()); -console.log('[istanbul] file being transformed:', filename); -``` - -If files are outside `cwd`, that's the issue → use `coverageRoot` config. - -### 2. Check env var timing (unlikely to be the issue) - -Add to `packages/babel-preset/src/preset.ts`: -```typescript -import { appendFileSync } from 'fs'; -const log = (msg: string) => { - try { appendFileSync('/tmp/harness-debug.log', `${new Date().toISOString()} ${msg}\n`); } catch {} -}; -log(`[babel-preset] PID=${process.pid}, RN_HARNESS_COLLECT_COVERAGE=${process.env.RN_HARNESS_COLLECT_COVERAGE}`); -``` - -Run tests and check: -```bash -rm -f /tmp/harness-debug.log -pnpm test:harness --coverage -cat /tmp/harness-debug.log -``` - -Metro workers (different PIDs) should see `RN_HARNESS_COLLECT_COVERAGE=true`. From 06465d27b5cca1c19ec0806ca33591cef308457c Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 21 Jan 2026 08:56:59 +0100 Subject: [PATCH 5/7] chore: version plan --- .nx/version-plans/version-plan-1768982019500.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .nx/version-plans/version-plan-1768982019500.md diff --git a/.nx/version-plans/version-plan-1768982019500.md b/.nx/version-plans/version-plan-1768982019500.md new file mode 100644 index 0000000..a372f49 --- /dev/null +++ b/.nx/version-plans/version-plan-1768982019500.md @@ -0,0 +1,5 @@ +--- +__default__: prerelease +--- + +Enables collection of coverage data in monorepository scenarios through the new coverageRoot configuration option. From bf5331f8f2ca0225ad409056640a32b9ad24a4e9 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 21 Jan 2026 08:58:58 +0100 Subject: [PATCH 6/7] docs: update website --- .../docs/getting-started/configuration.mdx | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/website/src/docs/getting-started/configuration.mdx b/website/src/docs/getting-started/configuration.mdx index cbcb9e5..6ccbc24 100644 --- a/website/src/docs/getting-started/configuration.mdx +++ b/website/src/docs/getting-started/configuration.mdx @@ -103,6 +103,9 @@ For Expo projects, the `entryPoint` should be set to the path specified in the ` // Optional: Reset environment between test files (default: true) resetEnvironmentBetweenTestFiles?: boolean; + + // Optional: Root directory for coverage instrumentation in monorepos (default: process.cwd()) + coverageRoot?: string; } ``` @@ -324,6 +327,30 @@ const config = { export default config; ``` +## Coverage Root + +The coverage root option specifies the root directory for coverage instrumentation in monorepository setups. This is particularly important when your tests run from a different directory than where your source files are located. + +```javascript +{ + coverageRoot: '../', // Use parent directory as coverage root +} +``` + +**Default:** `process.cwd()` (current working directory) + +This option is passed to babel-plugin-istanbul's `cwd` option and ensures that coverage data is collected correctly in monorepo scenarios where: + +- Tests run from an `example/` directory but source files are in `../src/` +- Libraries are structured with separate test and source directories +- Projects have nested directory structures that don't align with the current working directory + +Without specifying `coverageRoot`, babel-plugin-istanbul may skip instrumenting files outside the current working directory, resulting in incomplete coverage reports. + +:::tip When to use coverageRoot +Set `coverageRoot` when you notice 0% coverage in your reports or when source files are not being instrumented for coverage. This commonly occurs in create-react-native-library projects and other monorepo setups. +::: + ## Finding Device and Simulator IDs ### Android Emulators From 56e1755617219d2247efe14cd3d04a9f39024504 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 21 Jan 2026 09:01:26 +0100 Subject: [PATCH 7/7] chore: update description of coverage --- actions/shared/index.cjs | 2 +- packages/config/src/types.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/actions/shared/index.cjs b/actions/shared/index.cjs index f708bf9..19ff21d 100644 --- a/actions/shared/index.cjs +++ b/actions/shared/index.cjs @@ -4229,7 +4229,7 @@ var ConfigSchema = external_exports.object({ detectNativeCrashes: external_exports.boolean().optional().default(true), crashDetectionInterval: external_exports.number().min(100, "Crash detection interval must be at least 100ms").default(500), coverage: external_exports.object({ - root: external_exports.string().optional().describe('Root directory for coverage instrumentation. Defaults to cwd. Use ".." for create-react-native-library projects where source is in parent directory.') + root: external_exports.string().optional().describe(`Root directory for coverage instrumentation in monorepo setups. Specifies the directory from which coverage data should be collected. Use ".." for create-react-native-library projects where tests run from example/ but source files are in parent directory. Passed to babel-plugin-istanbul's cwd option.`) }).optional(), // Deprecated property - used for migration detection include: external_exports.array(external_exports.string()).optional() diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index e88336f..87f289f 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -52,7 +52,10 @@ export const ConfigSchema = z .string() .optional() .describe( - 'Root directory for coverage instrumentation. Defaults to cwd. Use ".." for create-react-native-library projects where source is in parent directory.' + 'Root directory for coverage instrumentation in monorepo setups. ' + + 'Specifies the directory from which coverage data should be collected. ' + + 'Use ".." for create-react-native-library projects where tests run from example/ ' + + 'but source files are in parent directory. Passed to babel-plugin-istanbul\'s cwd option.' ), }) .optional(),