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. diff --git a/actions/shared/index.cjs b/actions/shared/index.cjs index 41e40ce..19ff21d 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 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() }).refine((config) => { 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..87f289f 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -46,6 +46,20 @@ export const ConfigSchema = z .min(100, 'Crash detection interval must be at least 100ms') .default(500), + coverage: z + .object({ + root: z + .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: z.array(z.string()).optional(), }) diff --git a/packages/jest/src/setup.ts b/packages/jest/src/setup.ts index d22e53f..c41d784 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.coverage?.root) { + process.env.RN_HARNESS_COVERAGE_ROOT = harnessConfig.coverage.root; + } } logTestRunHeader(selectedRunner); 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