diff --git a/AGENTS.md b/AGENTS.md index 314852c..e53eab9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ - Build integration: Expo plugin + `cocoapods/coverage_post_install.rb` + `android/rn-coverage*.gradle` (example applies all three). - JS/TS: `RN_COVERAGE_JS=1` → babel-plugin-istanbul; `rn-coverage js pull|report` (NYC sourceMap remap). Unit: `yarn test:coverage`. - CI: `yarn e2e:ios:dynamic` (primary), `yarn e2e:ios:static`, `yarn e2e:android` — see `.github/workflows/ci.yml` and `docs/integration/ci-appium.md`. -- Release: Conventional Commits + semantic-release via `.github/workflows/release.yml` (`workflow_dispatch` only); PR titles via `.github/workflows/pr-title.yml`. Operator docs: `docs/releasing.md`. No `@semantic-release/git`. +- Release: Conventional Commits + semantic-release via `.github/workflows/release.yml` (`workflow_dispatch` only); PR titles via `.github/workflows/pr-title.yml`. Operator docs: `docs/releasing.mdx`. No `@semantic-release/git`. - Validation: `yarn`, `yarn prepare`, `yarn test` / `yarn test:coverage`, `yarn typecheck`, CLI `--help`. - Appium e2e for this package is in-scope when the task asks; do not use RNFB slot2/3. Prefer a non-RNFB simulator (e.g. iPhone 17 on Xcode 26). - Public repository execution and validation guidance lives in `okf-bundle/`; diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eac1af4..f0d5660 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Contributions are always welcome, no matter how large or small! -This package targets **dedicated test apps only** (Pattern C). See `docs/pattern-c.md` and `AGENTS.md`. +This package targets **dedicated test apps only** (Pattern C). See `docs/coverage.mdx` and `AGENTS.md`. License: Apache-2.0. Please follow the [code of conduct](./CODE_OF_CONDUCT.md). @@ -125,4 +125,4 @@ Local check: `echo "feat: your subject" | yarn commitlint` ### Releasing -Maintainers: see [docs/releasing.md](./docs/releasing.md). Releases are **manual** (`workflow_dispatch` + semantic-release). Do not publish from a laptop unless performing the one-time human bootstrap (separate from day-to-day CI releases). +Maintainers: see [docs/releasing.mdx](./docs/releasing.mdx). Releases are **manual** (`workflow_dispatch` + semantic-release). Do not publish from a laptop unless performing the one-time human bootstrap (separate from day-to-day CI releases). diff --git a/docs.json b/docs.json index 560887a..66a3207 100644 --- a/docs.json +++ b/docs.json @@ -1,16 +1,19 @@ { "$schema": "https://docs.page/schema.json", "name": "react-native-coverage", - "description": "Native code coverage for React Native - iOS and Android - without touching the native stuff. TurboModule flush, LCOV/Jacoco reports, assert in CI.", - "favicon": "/assets/brand/invertase-honeycomb.png", + "description": "iOS, Android, and TypeScript code coverage from end-to-end tests in a dedicated React Native test app, with a CI gate for empty coverage.", + "favicon": "/assets/brand/invertase-honeycomb-96x96.png", "logo": { - "light": "/assets/brand/invertase-honeycomb.png", - "dark": "/assets/brand/invertase-honeycomb.png" + "light": "/assets/brand/invertase-honeycomb-96x96.png", + "dark": "/assets/brand/invertase-honeycomb-96x96.png" }, "theme": { "primary": "#C97A2B", "primaryDark": "#F5B454" }, + "og": { + "logo": "/assets/brand/invertase-honeycomb-256x256.png" + }, "social": { "x": "@invertaseio", "github": "invertase/react-native-coverage", @@ -25,17 +28,13 @@ "showGitHubCard": true, "links": [ { - "title": "Codecov", + "title": "Open Codecov", "href": "https://app.codecov.io/gh/invertase/react-native-coverage" - }, - { - "title": "Get started", - "href": "/app-developers", - "cta": true } ] }, "agent": { + "placeholder": "Ask about setup, the CLI, or exit codes", "questions": [ "How do I add native coverage to an Expo test app?", "How is this different from Codecov's JavaScript coverage?", @@ -43,20 +42,41 @@ "How do library maintainers get iOS native coverage from their test app?" ] }, + "tabs": [ + { + "id": "docs", + "title": "Documentation", + "href": "/" + }, + { + "id": "reference", + "title": "Reference", + "href": "/reference" + } + ], "sidebar": [ { - "group": "Getting Started", + "tab": "docs", + "group": "Introduction", "pages": [ - { "title": "Introduction", "href": "/", "icon": "house" }, - { "title": "Why native coverage", "href": "/why", "icon": "lightbulb" }, - { "title": "Pattern C", "href": "/pattern-c", "icon": "shield-halved" } + { + "title": "Overview", + "href": "/", + "icon": "house" + }, + { + "title": "Coverage", + "href": "/coverage", + "icon": "lightbulb" + } ] }, { - "group": "Guides", + "tab": "docs", + "group": "Install", "pages": [ { - "title": "App developers (Expo & RN CLI)", + "title": "App developers", "href": "/app-developers", "icon": "mobile-screen" }, @@ -65,42 +85,88 @@ "href": "/library-maintainers", "icon": "cubes" }, - { "title": "Agents", "href": "/agents", "icon": "robot" } + { + "title": "Coding agents", + "href": "/agents", + "icon": "robot" + } ] }, { - "group": "Integration", + "tab": "docs", + "group": "Integrations", "pages": [ { "title": "Android", "href": "/integration/android", "icon": "android" }, - { "title": "iOS", "href": "/integration/ios", "icon": "apple" }, { - "title": "JavaScript / TypeScript", - "href": "/integration/js", - "icon": "js" + "title": "iOS", + "href": "/integration/ios", + "icon": "apple" }, { - "title": "E2E timing", - "href": "/integration/e2e-timing", - "icon": "clock" + "title": "JavaScript and TypeScript", + "href": "/integration/js", + "icon": "js" }, { - "title": "CI (Appium)", + "title": "CI with Appium", "href": "/integration/ci-appium", "icon": "gears" + }, + { + "title": "Upload to Codecov", + "href": "/integration/codecov", + "icon": "chart-line" + } + ] + }, + { + "tab": "docs", + "group": "Troubleshooting", + "pages": [ + { + "title": "Empty or missing coverage", + "href": "/troubleshooting/empty-coverage", + "icon": "triangle-exclamation" } ] }, { + "tab": "reference", "group": "Reference", "pages": [ - { "title": "CLI", "href": "/cli", "icon": "terminal" }, - { "title": "Config", "href": "/config", "icon": "sliders" }, - { "title": "Releasing", "href": "/releasing", "icon": "rocket" } + { + "title": "Overview", + "href": "/reference", + "icon": "book" + }, + { + "title": "CLI", + "href": "/reference/cli", + "icon": "terminal" + }, + { + "title": "Configuration", + "href": "/reference/config", + "icon": "sliders" + }, + { + "title": "Releasing", + "href": "/releasing", + "icon": "rocket" + } ] } - ] + ], + "redirects": { + "/cli": "/reference/cli", + "/config": "/reference/config", + "/why": "/coverage", + "/pattern-c": "/coverage#dedicated-test-app", + "/integration/e2e-timing": "/app-developers", + "/test-app": "/coverage#dedicated-test-app" + } } diff --git a/docs/agents.mdx b/docs/agents.mdx index b04fb94..30ee9a8 100644 --- a/docs/agents.mdx +++ b/docs/agents.mdx @@ -1,105 +1,78 @@ --- -title: Agents -description: A paste-ready prompt to wire react-native-coverage into a repo, plus the exact CI contract an agent must satisfy so native coverage can't silently regress. +title: Coding agents +description: Rules and the CI contract for Cursor, Claude, or Codex setting up react-native-coverage. Install steps live on App developers. --- -This page is written for coding agents (Cursor, Claude, Codex, …) and the humans directing them. -It gives a paste-ready integration prompt and the non-negotiable contract that makes the result -trustworthy. +## Prompt -## Paste-ready prompt +Give your agent one line. It reads [App developers](/app-developers), the same page a person uses to get started: -Give this to your agent **before** it edits Gradle files or Podfiles by hand: - -```text -Integrate react-native-coverage into this repo's dedicated React Native test / -e2e harness app only (Pattern C — never the production app package.json). - -Constraints: -- New Architecture / TurboModule only -- Follow https://docs.page/invertase/react-native-coverage -- Prefer the Expo config plugin when the harness is Expo; otherwise use the bare - Gradle + CocoaPods Ruby helpers from the integration docs -- Wire libraryProjectMatchers / frameworkNamePrefixes for every native library - we need hits from -- Add CI steps that pull coverage and fail with rn-coverage assert (exit 2) - when hits are empty -- Do not invent product-app install paths; keep the package out of the shipping app - -After install: yarn/npm add react-native-coverage in the harness, apply the plugin or -manual hooks, prebuild / pod install as needed, then show me the exact CI commands -to run and what green looks like. +```txt +Read https://docs.page/invertase/react-native-coverage/app-developers.md, and set up react-native-coverage in this project. ``` -## The rules an agent must not break +Every page on this site is also available as Markdown by adding `.md` to its URL, and the site publishes an [`llms.txt`](https://docs.page/invertase/react-native-coverage/llms.txt). + +## Rules - **Pattern C is load-bearing.** `react-native-coverage` goes into a dedicated test/e2e harness - app only. Never add it to a production app's `package.json`. Autolinking scans dependencies, so - `devDependencies` alone does not keep the TurboModule out of a shipped app. + `react-native-coverage` goes in a [dedicated test app](/coverage#dedicated-test-app) only. Never add it to the shipping app's `package.json` file. Autolinking scans dependencies, so `devDependencies` does not keep the TurboModule out. -- **New Architecture only.** Do not attempt Old-Architecture bridging shims. -- **Do not regex-edit the Podfile for LLVM flags.** Use the shipped - `cocoapods/coverage_post_install.rb` helper (`apply_post_install!`). Under Expo keep - `forceDynamicFrameworks: false`; set it `true` only on bare RN hosts whose React builds as - dynamic frameworks. -- **Land Android `.ec` under the app `buildDir`.** `rn-coverage android pull` stages it where - Jacoco's `fileTree` can see it; pulling only into `artifacts/` leaves `jacocoTestReport` empty. -- **Pin CI Actions by full commit SHA.** +- Use the New Architecture only. Do not write old-architecture bridging shims. +- Do not edit the `Podfile` with a regular expression to add LLVM flags. Use the CocoaPods helper, `apply_post_install!` from the `cocoapods/coverage_post_install.rb` file. Under Expo keep `forceDynamicFrameworks: false`. Only on a React Native CLI app whose React builds as dynamic frameworks, set it `true`. +- Put the Android `.ec` file under the app `buildDir`. `rn-coverage android pull` stages it where JaCoCo's `fileTree` includes it. Pulling only into the `artifacts/` directory leaves `jacocoTestReport` empty. +- Pin GitHub Actions by full commit SHA. -## The CI contract +## CI contract -The whole point is a pipeline that **cannot pass with empty native coverage**. The sequence: +The pipeline must not pass with empty native coverage. The sequence is: - - Start Metro with `RN_COVERAGE_JS=1` for JS coverage, run the suite, and call - `Coverage.flush()` once at teardown. + + Launch the test app and run the suite. When you also want JavaScript coverage, start Metro with `RN_COVERAGE_JS=1`. - + + When the suite ends, call `Coverage.flush()`. + + ```sh rn-coverage android pull && rn-coverage android report rn-coverage ios pull && rn-coverage ios export && rn-coverage ios report ``` - + ```sh rn-coverage assert ``` -### Exit codes are the contract +### Exit codes + +`rn-coverage assert` uses these exit codes. | Code | Meaning | |------|---------| -| **0** | Success (or soft-mode empty artifact with a warning) | -| **1** | Unexpected error / bad invocation / tooling failure | -| **2** | Strict empty-hit or missing artifact — the CI presence guard tripped | +| 0 | Success. With `--no-strict`, an empty report also exits 0 with a warning. | +| 1 | Error: bad invocation or a tool failure. | +| 2 | No hits, or a report file is missing, in strict mode. | - - In strict mode (the CI default), missing or empty expected hits **exit 2**. An agent must treat - exit 2 as a hard failure to fix — never suppress it, never fall back to `--no-strict` in CI to - "make it green." A green pipeline that produced no native hits is a false negative. - + + Exit 2 is a failure to fix, never one to suppress. Do not switch CI to `--no-strict` to pass the job. A passing job with no native hits means coverage failed without failing the job. + -Configure which paths/packages must be non-empty via `assert.lcovPathIncludes` and -`assert.jacocoPackageIncludes` in [config](/config). Wire the real CLI into CI rather than -maintaining a bespoke "did anything get covered?" shell script. +Set which paths and packages must have hits with `assert.lcovPathIncludes` and `assert.jacocoPackageIncludes` on [Configuration](/reference/config). For more information about fixing an exit 2, see [Empty or missing coverage](/troubleshooting/empty-coverage). -## Verify like the reference repo +## Check the result -The canonical, working example is this repository itself — its -[Codecov dashboard](https://app.codecov.io/gh/invertase/react-native-coverage) carries separate -`e2e-ios-dynamic`, `e2e-ios-static`, and `e2e-android` flags with real device hits. Reproduce that -shape and you're done. +The working example is this repository. Its [Codecov dashboard](https://app.codecov.io/gh/invertase/react-native-coverage) carries separate [`e2e-ios-dynamic`](https://app.codecov.io/gh/invertase/react-native-coverage/flags/e2e-ios-dynamic), [`e2e-ios-static`](https://app.codecov.io/gh/invertase/react-native-coverage/flags/e2e-ios-static), and [`e2e-android`](https://app.codecov.io/gh/invertase/react-native-coverage/flags/e2e-android) flags with hits from iOS Simulator and Android emulator runs. Match that pattern: one flag per platform, each with non-zero hits. - - Commands, flags, and the strict/assert contract in full. + + Commands, flags, and the strict and assert contract in full. - - `assert.*` matchers and default artifact paths. + + The `assert.*` matchers and default report paths. diff --git a/docs/app-developers.mdx b/docs/app-developers.mdx index 9029885..f2c0be2 100644 --- a/docs/app-developers.mdx +++ b/docs/app-developers.mdx @@ -1,25 +1,19 @@ --- -title: App developers - Expo & React Native CLI -description: Wire native iOS and Android coverage into your dedicated test / e2e harness app. Expo first, React Native CLI covered too. +title: App developers +description: Install react-native-coverage in an Expo or React Native CLI test app, flush at the end of the run, and fail CI when coverage is empty. --- -This guide is for **app developers** who want native coverage from their end-to-end tests. -It covers Expo first (recommended) and bare React Native CLI. - - Install `react-native-coverage` into a **dedicated test / e2e harness app** only - ([Pattern C](/pattern-c)) — never your shipping product app. Autolinking scans dependencies, - so keep the package out of the production `package.json` entirely, not just in `devDependencies`. + Install `react-native-coverage` in a [dedicated test app](/coverage#dedicated-test-app) only, never the app you ship. -## Prerequisites +## Before you begin -- **New Architecture / TurboModule** enabled (this package is New-Arch-only). -- A **dedicated harness app** in your repo — e.g. a `tests/` or `e2e/` workspace app. -- An e2e runner that can drive the app on a simulator/emulator (Appium, Detox, Maestro, …) and - a place to call `Coverage.flush()` at teardown. +- The [test app](/coverage#dedicated-test-app) runs React Native's New Architecture. The flusher is a TurboModule and does not run on the old bridge. +- Your end-to-end app is that test app, such as a `tests/` or `e2e/` workspace app. +- You have a test runner (Appium, Detox, or Maestro) that can call `Coverage.flush()` when the suite ends. -## 1. Install (in the harness) +## Install in the test app @@ -34,15 +28,16 @@ It covers Expo first (recommended) and bare React Native CLI. -## 2. Configure the build +Run the command in the test app's folder, not the repository root. + +## Configure the build - - Add the config plugin, then prebuild. The plugin applies the Android Gradle helpers and wires - the iOS Podfile helper call for you. + + Add the config plugin, then prebuild. The plugin applies the Android Gradle helpers and adds the iOS Podfile helper call. - + ```json { "expo": { @@ -50,7 +45,7 @@ It covers Expo first (recommended) and bare React Native CLI. [ "react-native-coverage", { - "libraryProjectMatchers": ["my-native-lib"], + "libraryProjectMatchers": ["my-lib"], "frameworkNamePrefixes": ["MyLib"], "enableAndroidCoverage": true, "forceDynamicFrameworks": false @@ -61,9 +56,9 @@ It covers Expo first (recommended) and bare React Native CLI. } ``` - - `libraryProjectMatchers` — Android library projects (by name substring) you want Jacoco hits from. - - `frameworkNamePrefixes` — iOS framework name prefixes to flush LINKEDIT for. - - Keep `forceDynamicFrameworks` **false** under Expo (React-Core is force-static). + - `libraryProjectMatchers`: Android library projects, matched by name substring, that you want JaCoCo hits from. + - `frameworkNamePrefixes`: iOS framework name prefixes whose `LINKEDIT` counters are flushed. + - `forceDynamicFrameworks`: `false` under Expo, where `React-Core` is a static library. ```sh @@ -72,16 +67,16 @@ It covers Expo first (recommended) and bare React Native CLI. - Full detail: [Android integration](/integration/android) · [iOS integration](/integration/ios). + For more information, see [Android](/integration/android) and [iOS](/integration/ios). - - Apply the shipped Gradle and CocoaPods helpers manually. + + Apply the Gradle and CocoaPods helpers that ship with the package. - + ```gradle - ext.coverageLibraryProjectMatchers = ['my-native-lib'] + ext.coverageLibraryProjectMatchers = ['my-lib'] def rnCoverageRoot = new File( ["node", "--print", "require.resolve('react-native-coverage/package.json')"] .execute(null, rootDir).text.trim() @@ -89,7 +84,7 @@ It covers Expo first (recommended) and bare React Native CLI. apply from: new File(rnCoverageRoot, "android/rn-coverage.gradle") ``` - + ```gradle android { buildTypes { debug { testCoverageEnabled true } } } @@ -100,11 +95,11 @@ It covers Expo first (recommended) and bare React Native CLI. apply from: new File(rnCoverageRoot, "android/rn-coverage-jacoco.gradle") ``` - + ```ruby require_relative '../node_modules/react-native-coverage/cocoapods/coverage_post_install' - # After use_expo_modules! (if present): + # After use_expo_modules!, when present ReactNativeCoverage.install_installer_hooks! post_install do |installer| @@ -123,29 +118,45 @@ It covers Expo first (recommended) and bare React Native CLI. - Copy `react-native-coverage.config.js.example` to `react-native-coverage.config.js` if you need - host-specific paths (bundle id, product name, framework prefixes). See [Config](/config). + When the test app needs its own bundle ID, product name, or framework prefixes, copy the `react-native-coverage.config.js.example` file to the `react-native-coverage.config.js` file. For more information, see [Configuration](/reference/config). -## 3. Flush at the end of your e2e run +## Flush at the end of the test run -Call the TurboModule once at suite teardown. It dumps native buffers **and** the Istanbul -`global.__coverage__` object when your JS bundle was instrumented. +When the suite ends, call `Coverage.flush()` one time. It writes the native counters and, when you start Metro with `RN_COVERAGE_JS=1`, the Istanbul `global.__coverage__` object. ```ts import Coverage from 'react-native-coverage'; -// e.g. in an Appium/Detox afterAll hook, triggered via a testID button or deep link +// In an afterAll hook, reached through a testID button or a deep link await Coverage.flush(); ``` -See [E2E timing](/integration/e2e-timing) for exactly when to flush and pull. +Your test runner decides when. The package does not ship Appium, Detox, or Maestro. When you want JavaScript coverage, set `RN_COVERAGE_JS=1` before you start Metro. The order of a run is: + + + + Start Metro for the test app. + + + Launch the test app on the simulator or emulator. + + + Drive the app with your test runner. + + + When the last test has finished, call `Coverage.flush()` from the app. The files land in the app container: `files/coverage.ec` on Android, `.profraw` files in the simulator container on iOS, and `coverage-final.json` on both when JavaScript is instrumented. + + + Run the `rn-coverage` commands in the next section. + + -## 4. Pull, report, and gate CI +## Pull, report, and fail CI on empty coverage - + ```sh rn-coverage android pull rn-coverage ios pull @@ -153,51 +164,50 @@ See [E2E timing](/integration/e2e-timing) for exactly when to flush and pull. ```sh - rn-coverage android report # Jacoco XML - rn-coverage ios export && rn-coverage ios report # LCOV + llvm-cov report + rn-coverage android report # JaCoCo XML + rn-coverage ios export && rn-coverage ios report # LCOV and the llvm-cov report ``` ```sh - rn-coverage assert # exit 2 when there are no hits — that is the point + rn-coverage assert ``` - - `rn-coverage assert` is the package-owned replacement for one-off "did anything get covered?" - shell scripts. In strict mode (the CI default) an empty or missing artifact **exits 2**, so a - sabotaged or silently-broken pipeline fails loudly instead of shipping a false green. - +`rn-coverage assert` exits 0 when the configured paths have hits. It exits 2 when a report is missing or empty, so the CI job fails. Keep strict mode, the default, in CI. For more information, see [Empty or missing coverage](/troubleshooting/empty-coverage). -Want JavaScript/TypeScript e2e coverage remapped to your TS sources too? See -[JavaScript / TypeScript](/integration/js). +For JavaScript and TypeScript coverage remapped to your TypeScript sources, add `rn-coverage js pull` and `rn-coverage js report` from [JavaScript and TypeScript](/integration/js). -## Optionally upload to Codecov +## Optional: upload to Codecov -Upload each artifact with a distinct flag so iOS, Android, and JS stay separate: +Upload each report under its own flag so iOS, Android, and JavaScript stay separate on the dashboard. For more information about how Codecov relates to this package, see [Upload to Codecov](/integration/codecov). ```sh -# native codecov -f coverage/ios/lcov.info -F e2e-ios codecov -f coverage/android/jacocoTestReport.xml -F e2e-android -# js codecov -f coverage/js/lcov.info -F e2e-js ``` ## Next steps - - - Gradle helpers, Emma `.ec` → Jacoco, package matchers. + + + Gradle helpers, the Emma `.ec` file, and JaCoCo. + + + The CocoaPods helper, static and dynamic frameworks, and LCOV export. - - Dynamic vs static frameworks, the Ruby helper, LINKEDIT flush modes. + + Instrument Metro and remap to TypeScript lines. - - Hard-won GitHub Actions pitfalls for simulator, WDA, and Jacoco paths. + + Run the job on GitHub Actions. - + Every command, flag, and exit code. + + Every key in the `react-native-coverage.config.js` file. + diff --git a/docs/cli.mdx b/docs/cli.mdx deleted file mode 100644 index 7bc7133..0000000 --- a/docs/cli.mdx +++ /dev/null @@ -1,65 +0,0 @@ -# CLI - -Binary: `rn-coverage` - -``` -rn-coverage --help -rn-coverage [--strict|--no-strict] [-c ] - -rn-coverage android pull [--device ] [--output ] [--retries ] -rn-coverage android report [--android-dir ] [--jacoco-xml ] - -rn-coverage ios pull --device [--output ] -rn-coverage ios export --derived-data [--configuration Debug] [--app-name ] [--output ] [--arch ] -rn-coverage ios report --derived-data [--profdata ] [--output-dir ] [--arch ] -rn-coverage ios summary --derived-data [--profdata ] [--arch ] - -rn-coverage assert [--platform ios|android|all] [--lcov ] [--jacoco-xml ] - -rn-coverage js pull --platform android|ios [--device ] [--output ] -rn-coverage js report --input [--output ] [--cwd ] [--nyc-config ] -``` - -## Exit codes - -| Code | Meaning | -|------|---------| -| **0** | Success (or soft-mode empty artifact with warning) | -| **1** | Unexpected error / bad invocation / tooling failure | -| **2** | Strict empty-hit / missing artifact (CI presence guard) | - -### Strict / assert contract - -- Config default: `strict: true` (recommended for CI). -- Global flags: `--strict` / `--no-strict` override config for the process. -- Soft local (`--no-strict` or `strict: false`): missing/empty expected hits **warn and exit 0**. -- Strict CI: the same conditions **exit 2** so a sabotaged or silent pipeline fails the job. - -Commands that enforce the guard: - -| Command | Exit 2 when (strict) | -|---------|----------------------| -| `android pull` | No `.ec` after retries | -| `android report` | Gradle ok but Jacoco XML missing/empty/no matched LINE hits | -| `ios pull` | No `.profraw` in the simulator container | -| `ios export` | No `.profraw`, or LCOV has no expected path hits with `LH` > 0 (default: `packages/`) | -| `ios report` / `ios summary` | Missing `profdata` (run `ios export` first) | -| `assert` | Dedicated post-pipeline check for LCOV and/or Jacoco XML | - -## Universal (multi-arch) binaries - -`llvm-cov` cannot read coverage from a **universal** (multi-arch) Mach-O without an -`-arch` selector, so a simulator build that carries both `arm64` and `x86_64` slices -would otherwise export **0%**. `ios export`, `ios report`, and `ios summary` handle this -automatically: - -- **Thin** binary (one slice) → no `-arch` is passed (unchanged behavior). -- **Universal** binary → the host architecture is selected when present, else the first slice. -- Override with `--arch ` (e.g. `--arch arm64`) or `ios.arch` in config. - -Thin simulator builds (Xcode's default `ONLY_ACTIVE_ARCH=YES` for Debug) never needed this; -the selection only kicks in for universal builds. - -`rn-coverage assert` is the package-owned replacement for one-off shell presence scripts. Prefer wiring this CLI (exit 2) into consumer CI rather than maintaining a permanent bespoke assert. - -Matchers and default artifact paths live under `assert.*` in config (see [config.md](./config.md)). diff --git a/docs/config.mdx b/docs/config.mdx deleted file mode 100644 index ff3621c..0000000 --- a/docs/config.mdx +++ /dev/null @@ -1,28 +0,0 @@ -# Config - -Copy [`react-native-coverage.config.js.example`](https://github.com/invertase/react-native-coverage/blob/main/react-native-coverage.config.js.example) to `react-native-coverage.config.js` in your dedicated test app. - -Key fields: - -| Key | Purpose | -|-----|---------| -| `nativeModuleName` | TurboModule name (default `Coverage`) | -| `app.androidApplicationId` | `run-as` package for adb pull | -| `app.iosBundleId` | simctl container lookup | -| `app.iosProductName` | App binary / `.app` name | -| `ios.frameworkNamePrefixes` | Extra llvm-cov `-object` frameworks | -| `ios.arch` | `llvm-cov -arch` for universal binaries (empty = auto-detect; set e.g. `arm64` to force) | -| `android.coverageRelativePath` | On-device `.ec` path under app files | -| `android.libraryProjectMatchers` | Fallback Jacoco package substrings for assert | -| `android.jacocoReportXml` | Default Jacoco XML path after `android report` | -| `js.androidRelativePath` | On-device Istanbul JSON under `run-as` (default `files/coverage-final.json`) | -| `js.androidStagingPath` | adb staging path for JS JSON pull | -| `js.iosRelativePath` | Path under sim data container (default `Documents/coverage-final.json`) | -| `sourcePathRewrite` | LCOV `SF:` path normalization rules | -| `strict` | Exit **2** on empty artifacts when true (CI default) | -| `assert.lcovPathIncludes` | Substrings required in ≥1 LCOV `SF:` (default `packages/`) | -| `assert.jacocoPackageIncludes` | Jacoco package name substrings that must have LINE covered (`.` or `/`) | -| `assert.defaultLcovPath` | Default `--lcov` for `rn-coverage assert` | -| `assert.defaultJacocoXmlPath` | Default `--jacoco-xml` for `rn-coverage assert` | - -Defaults contain **no** product-specific names. diff --git a/docs/coverage.mdx b/docs/coverage.mdx new file mode 100644 index 0000000..e72988e --- /dev/null +++ b/docs/coverage.mdx @@ -0,0 +1,97 @@ +--- +title: Coverage +description: What react-native-coverage measures, the dedicated test app it runs in, and how flush, pull, report, and assert fit together. +--- + +## What it measures + +`react-native-coverage` measures whether your Objective-C++, Swift, Kotlin, and instrumented TypeScript ran during an end-to-end test on a simulator or emulator. + +| Layer | What it covers | +|-------|----------------| +| **iOS native** | LLVM counters in the app and frameworks, flushed in-process, exported to LCOV | +| **Android native** | Emma `.ec` from the process, reported as JaCoCo XML | +| **JS / TypeScript (optional)** | Istanbul `global.__coverage__` when Metro runs with `RN_COVERAGE_JS=1`, remapped to TypeScript LCOV | +| **Unit (separate)** | Jest in Node still covers JS/TS you exercise without a device | + +Jest and Istanbul in Node do not see native TurboModule code on a device. This package fills that gap. You run it from a [dedicated test app](/coverage#dedicated-test-app). + +iOS needs an in-process flush: counters live in the running app, `.profraw` files sit in the simulator container, and each dynamic framework keeps its own `LINKEDIT` section. Android dumps Emma coverage from the process. When Metro is instrumented, the same flush can also write Istanbul JSON. + +That native surface shows up as real source files with line hits, not a mock: + +File explorer showing the ios/ native directory with line coverage for Coverage.mm, CoverageProfile.mm, and CoverageConfig.h + +## How the pipeline works + +1. A [dedicated test app](/coverage#dedicated-test-app) runs the end-to-end suite under Appium, Detox, or Maestro. +2. `Coverage.flush()` writes the native counters and, when present, the Istanbul JSON. +3. `rn-coverage` pulls those files and writes LCOV for iOS and TypeScript, and JaCoCo XML for Android. +4. `rn-coverage assert` exits `2` when a report is missing or empty, so CI cannot pass on nothing. +5. Upload each report to a coverage host under its own flag when you want a dashboard. For Codecov, see [Upload to Codecov](/integration/codecov). + +The package ships the TurboModule, the CLI, the Expo config plugin, the CocoaPods helper, the Gradle JaCoCo helpers, and the Babel and NYC setup. There is no regex edit of the `Podfile`, no hand recovery of `.profraw` files, and no hand-written Gradle coverage wiring. + +After upload, a dashboard for this harness looks like this (illustration only): + +Coverage dashboard showing overall coverage, trend, sunburst graph, and the native code tree + +## Dedicated test app + +Install `react-native-coverage` only in a **dedicated test app** (a harness such as `tests/`, `e2e/`, or `example/`), never in the app you ship. + +The flusher is a TurboModule. It reads compiler counters from the running process and writes coverage files into that app's container. React Native autolinking scans every dependency of the app it builds, including `devDependencies`. If this package is in the shipping app's `package.json` under either key, the TurboModule lands in the product binary. + +The test app must: + +- Have its own `package.json` and its own native projects after `prebuild` or `pod install` +- Run React Native's **New Architecture** (required for the TurboModule) +- Drive end-to-end runs (Appium, Detox, Maestro, or similar) that exercise the native surface you care about, then call `Coverage.flush()` + +```json +{ + "name": "my-app-monorepo", + "private": true, + "workspaces": ["app", "tests"] +} +``` + +```json +{ + "name": "tests", + "private": true, + "dependencies": { + "react-native": "0.86.3", + "react-native-coverage": "^0.2.2" + } +} +``` + +This repository uses that shape: library at the root, `example/` (Expo, static libraries) and `example-dynamic/` (React Native CLI, dynamic frameworks) beside it. + +## Example from this package + +Invertase CI runs this package's harness apps and uploads the reports. The numbers below are that harness, not a product app. + +| Flag | What ran | Coverage | +|------|----------|---------:| +| [`e2e-ios-dynamic`](https://app.codecov.io/gh/invertase/react-native-coverage/flags/e2e-ios-dynamic) | iOS native, dynamic frameworks | 90.6% | +| [`e2e-android`](https://app.codecov.io/gh/invertase/react-native-coverage/flags/e2e-android) | Android native (Emma to JaCoCo) | 81.7% | +| [`e2e-ios-static`](https://app.codecov.io/gh/invertase/react-native-coverage/flags/e2e-ios-static) | iOS native, static libraries | 63.1% | +| [`unit-js`](https://app.codecov.io/gh/invertase/react-native-coverage/flags/unit-js) | Jest unit (JavaScript and TypeScript) | 52.6% | + +Those numbers come from an iOS Simulator and Android emulator running the harness under Appium. Line-level view of the TurboModule (`ios/Coverage.mm` at 71.88%): + +Line-by-line view of ios/Coverage.mm at 71.88 percent, with Objective-C++ TurboModule code covered and partially covered diff --git a/docs/index.mdx b/docs/index.mdx index 91e402f..c8eb08d 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -1,76 +1,58 @@ --- -title: Code coverage for React Native - Typescript, iOS, and Android - without touching the native stuff. -description: Flush real device coverage from a TurboModule, merge it into LCOV / Jacoco, remap TypeScript line-for-line, and gate your dev loop or CI. +title: Native code coverage for React Native +description: iOS, Android, and TypeScript code coverage from end-to-end tests in a dedicated React Native test app, with a CI gate for empty coverage. --- -Install it into a dedicated test / e2e harness app ([Pattern C](/pattern-c)) — never your shipping app. + + Install it in a [dedicated test app](/coverage#dedicated-test-app), never the app you ship. The flusher is a TurboModule, so the test app must run React Native's New Architecture. + -A TurboModule flushes real device coverage; the CLI pulls it, **merges** every framework's `.profraw` and the -app binary into clean LCOV / Jacoco, and remaps your instrumented JS line-for-line back to TypeScript — smoothing -the rough edges of modern TypeScript development by handling the line-accurate source mapping for you. Use that as -a signal in your development loop or CI to gate development iterations or CI pass/fail. +## Install -[![Codecov dashboard for react-native-coverage — overall coverage, 3-month trend, sunburst graph, and the native code tree](/assets/codecov/dashboard.png)](https://app.codecov.io/gh/invertase/react-native-coverage) +Give your coding agent this prompt: -## Start here +```txt +Read https://docs.page/invertase/react-native-coverage/app-developers.md, and set up react-native-coverage in this project. +``` + +Or pick a relevant install guide: - Add native coverage to your Expo or React Native CLI test app. + From install through `rn-coverage assert` exiting `0` on iOS and Android. - Prove your library's native code ran — in unit tests and a test app. + Keep unit tests, add the test-app layer, and require hits in your own package. - - A paste-ready prompt and the CI contract your agent must satisfy. + + Prompt, hard rules, and the assert exit-code contract. -## Why it matters - -React Native is cross-platform, but coverage tooling usually stops at the JavaScript bundle. -The Objective-C++, Swift, and Kotlin behind your TurboModules run on a device and then vanish — -**iOS native coverage especially is a black box.** This package makes that evidence a one-liner, -so you (or your agent) can prove the native path executed, not just the mock. - -[Read the full rationale →](/why) - -## Proof, live on `main` - -This repository proves its own thesis on Codecov — including the hard part, iOS: - -| Flag | What ran | Coverage | -|------|----------|---------:| -| `e2e-ios-dynamic` | iOS native, dynamic frameworks | **90.6%** | -| `e2e-android` | Android native (Emma → Jacoco) | **81.7%** | -| `e2e-ios-static` | iOS native, static libraries | **63.1%** | -| `unit-js` | Jest unit (JS/TS) | **52.6%** | - -[Browse it on Codecov →](https://app.codecov.io/gh/invertase/react-native-coverage) - -## Used in production by - -Both [**React Native Firebase**](https://github.com/invertase/react-native-firebase) and -[**React Native Google Mobile Ads**](https://github.com/invertase/react-native-google-mobile-ads) -depend on `react-native-coverage` in their [Pattern C](/pattern-c) test apps. +## Explore -

- - React Native Firebase - -        - - React Native Google Mobile Ads - -

+ + + What this package measures, how the pipeline works, and what you need to run it. + + + Android, [iOS](/integration/ios), [JavaScript and TypeScript](/integration/js), [CI with Appium](/integration/ci-appium), and [Codecov](/integration/codecov). + + + Every `rn-coverage` command, flag, and exit code, and every key in the `react-native-coverage.config.js` file. + + + Resolve empty reports and `rn-coverage assert` failures. + + -## What you get +## Help -| Piece | Role | -|-------|------| -| **TurboModule** | `flush()` — iOS LINKEDIT LLVM flush + Android Emma dump (also dumps Istanbul `global.__coverage__` when present) | -| **CLI (`rn-coverage`)** | `android pull\|report`, `ios pull\|export\|report\|summary`, `js pull\|report`, `assert` | -| **Expo config plugin** | Wires the Gradle helpers + the Podfile helper call | -| **CocoaPods Ruby helper** | Pod LLVM flags + optional dynamic-framework restore | -| **Gradle Jacoco helpers** | `android/rn-coverage.gradle` + `android/rn-coverage-jacoco.gradle` | -| **JS/TS coverage** | `babel-plugin-istanbul` + NYC source-map remap → TypeScript-accurate LCOV | + + + Ask about setup or unclear docs. + + + Report a bug or empty report that looks wrong. + + diff --git a/docs/integration/android.mdx b/docs/integration/android.mdx index 3d243e0..5d4994d 100644 --- a/docs/integration/android.mdx +++ b/docs/integration/android.mdx @@ -1,10 +1,13 @@ -# Android integration +--- +title: Android +description: Enable Emma instrumentation in an Expo or React Native CLI test app, pull the `.ec` file, and turn it into a JaCoCo XML report with rn-coverage. +--- -Enable Android test coverage in your dedicated test app (Pattern C) and wire Jacoco reporting. +Pull the Emma `.ec` file from your [dedicated test app](/coverage#dedicated-test-app) and turn it into a JaCoCo XML report. Expo needs fewer steps. -## Expo (recommended) +## Expo -Add the config plugin — it applies both Gradle helpers and enables debug `testCoverageEnabled`: +Add the config plugin. It applies both Gradle helpers and turns on `testCoverageEnabled` for the `debug` build type: ```json { @@ -13,7 +16,7 @@ Add the config plugin — it applies both Gradle helpers and enables debug `test [ "react-native-coverage", { - "libraryProjectMatchers": ["coverage-fixture", "my-native-lib"], + "libraryProjectMatchers": ["my-lib"], "enableAndroidCoverage": true } ] @@ -22,20 +25,20 @@ Add the config plugin — it applies both Gradle helpers and enables debug `test } ``` -Then: +Then generate the native Android project: ```sh npx expo prebuild ``` -Inspect `android/build.gradle` (root instrumentation) and `android/app/build.gradle` (Jacoco report + `testCoverageEnabled`). +After prebuild, the `android/build.gradle` file applies the root helper and the `android/app/build.gradle` file applies the JaCoCo report helper and sets `testCoverageEnabled`. -## Bare React Native / manual Gradle +## React Native CLI -In the **root** `android/build.gradle`: +In the root `android/build.gradle` file, add this block: ```gradle -ext.coverageLibraryProjectMatchers = ['my-native-lib'] +ext.coverageLibraryProjectMatchers = ['my-lib'] def rnCoverageRoot = new File( ["node", "--print", "require.resolve('react-native-coverage/package.json')"] .execute(null, rootDir).text.trim() @@ -43,7 +46,7 @@ def rnCoverageRoot = new File( apply from: new File(rnCoverageRoot, "android/rn-coverage.gradle") ``` -In **app** `android/app/build.gradle`: +In the `android/app/build.gradle` file, add this block: ```gradle android { @@ -61,23 +64,25 @@ def rnCoverageRoot = new File( apply from: new File(rnCoverageRoot, "android/rn-coverage-jacoco.gradle") ``` -## Runtime + CLI +These two helpers wire JaCoCo into the library and app projects. -1. Call `flush()` from the TurboModule at the end of an e2e run (Emma `RT.dumpCoverageData` → `filesDir/coverage.ec`; also dumps Istanbul JSON when Metro is instrumented). -2. `rn-coverage android pull` — output under **`android/app/build/…`** so Jacoco sees the `.ec` (not only `artifacts/`). Staging parent dirs are created for you. -3. `rn-coverage android report` (`jacocoTestReport`). -4. `rn-coverage assert` (strict empty → exit 2). -5. Optional JS: `rn-coverage js pull` / `js report` — see [JS / TypeScript](js.md). +| File | Role | +|------|------| +| `android/rn-coverage.gradle` | Enables JaCoCo and `enableAndroidTestCoverage` on every library project that matches `coverageLibraryProjectMatchers` | +| `android/rn-coverage-jacoco.gradle` | Adds the `jacocoTestReport` task and the unit and end-to-end report variants | -Jacoco package names in XML use slashes (`com/foo`); assert matchers may use dots (`com.foo`) — both work. +## Pull and report -CI pitfalls (emulator Metro, `byTestId`, etc.): [CI Appium notes](ci-appium.md). +When the suite ends, `Coverage.flush()` calls Emma's `RT.dumpCoverageData` and writes `filesDir/coverage.ec`. On the host, stage the `.ec` file, write the JaCoCo XML, and assert matched hits: -Shipped helpers: +```sh +rn-coverage android pull # stages the .ec under android/app/build/ so JaCoCo's fileTree includes it +rn-coverage android report # runs jacocoTestReport and writes the XML +rn-coverage assert # exits 2 when the XML has no matched LINE hits +``` -| File | Role | -|------|------| -| `android/rn-coverage.gradle` | Enable Jacoco + `enableAndroidTestCoverage` on matched library projects | -| `android/rn-coverage-jacoco.gradle` | `jacocoTestReport` / unit / e2e-only report tasks | +`rn-coverage android pull` creates the staging directories and lands `emulator_coverage.ec` under the app `buildDir`. JaCoCo's `executionData` is a `fileTree` over `project.buildDir` plus the matched libraries, so a file pulled only into `artifacts/` leaves `:app:jacocoTestReport` skipped with empty data. + +JaCoCo XML package names use slashes, `com/foo`. The `assert` matchers accept dots, `com.foo`, or slashes. -Pod LLVM flags are **not** applied by regex-editing the Podfile; use the Ruby helper on iOS (see [iOS integration](ios.md)). +For JavaScript coverage from the same run, add `rn-coverage js pull` and `rn-coverage js report` from [JavaScript and TypeScript](/integration/js). For more information about emulator and Metro failures in CI, see [CI with Appium](/integration/ci-appium). diff --git a/docs/integration/ci-appium.mdx b/docs/integration/ci-appium.mdx index 8329bb7..87e1498 100644 --- a/docs/integration/ci-appium.mdx +++ b/docs/integration/ci-appium.mdx @@ -1,88 +1,66 @@ -# CI Appium notes (learned the hard way) - -Practical pitfalls from getting GitHub Actions green for this package’s Appium cells. Consumer CI should copy the same patterns. - -## Matrix - -| Cell | Harness | What it proves | -|------|---------|----------------| -| `e2e:ios:dynamic` | `example-dynamic/` (bare RN, `USE_FRAMEWORKS=dynamic`) | Primary LLVM LCOV with a real dynamic `CoverageFixture.framework` | -| `e2e:ios:static` | `example/` (Expo prebuild, static merge) | Honest static cell still asserts fixture hits | -| `e2e:android` | `example/` (Expo + emulator) | Emma `.ec` → Jacoco → assert | - -Scripts: `scripts/ci/`. Specs: `e2e/`. - -## iOS / Xcode / simulator - -- Prefer **`macos-26`** + **`maxim-lobanov/setup-xcode`** with `latest-stable` (Expo SDK 57 wants Xcode 26.4+). -- Default simulator name: **`iPhone 17`**. Xcode 26.6 images do not ship a plain `iPhone 16`; wrong names fail destination lookup. -- Boot with an exact-name match, open **Simulator.app**, then poll `bootstatus` (see `scripts/ci/boot-ios-simulator.sh`). Headless `simctl boot` alone + Appium restart hung at WDA timeouts on GHA. -- Give Appium room: `simulatorStartupTimeout` ≥ 300s and WDA/session - timeouts above cold WDA's observed 3–5 minute range. -- List sims once before the run (`xcrun simctl list`) — same deflake idea as RNFB. -- Prefetch the iOS Metro bundle after `/status` is ready. Prebuild WDA once - under `artifacts/e2e/wda-derived-data` and hand it to the driver as - `usePrebuiltWDA` + `derivedDataPath`; do not place DerivedData in - `e2e/node_modules`. -- Do **not** use `usePreinstalledWDA` on a simulator. It launches the - `.xctrunner` app with plain `simctl`, which exits immediately at - `domain:dyld(6) code:1` because the XCTest frameworks are never injected; - Appium then polls `127.0.0.1:8100/status` until `wdaLaunchTimeout`. A - readiness watchdog (`IOS_WDA_READY_DEADLINE`, default 180s) aborts the - attempt once the WDA port is provably dead. -- Use two or three serialized outer WDIO attempts and - `connectionRetryCount: 0`. Between attempts, delete sessions, terminate and - reinstall the app, stop Appium, clear Appium/WDA ports, and reuse prebuilt - WDA. Never overlap `POST /session`. -- Keep `noReset: false`, `enforceAppInstall: true`, `forceAppLaunch: true`, - and do not pre-launch the app with `simctl launch`. -- Build iOS with `-derivedDataPath /ios/build` and pass that - product as required `appium:app` - (`…/ios/build/Build/Products/Debug-iphonesimulator/.app`). Never - discover `~/Library/Developer/Xcode/DerivedData` and never launch by - bundle id alone. WDA uses a **separate** derived-data folder - (`artifacts/e2e/wda-derived-data`). - -## Android / Metro / Appium - -- Debug APK loads JS from Metro. Emulator `localhost` is **not** the host — run **`adb reverse tcp:8081 tcp:8081`** (or your Metro port) before launching the app, or the bundle never loads and Appium never sees UI. -- Point Appium at the exact Gradle product (`android/app/build/outputs/apk/debug/app-debug.apk`) with `noReset: false` and `enforceAppInstall: true`. Cached AVDs otherwise keep a same-`versionCode` APK and skip the reinstall. -- React Native `testID` on Android maps to **`resource-id`**, not accessibility id. Prefer a shared helper (`byTestId`) that uses `UiSelector().resourceId(...)` on Android and `~id` on iOS. -- Allow a long `appWaitDuration` for first Metro bundle. - -## Android coverage pull → Jacoco - -1. Staging path (`android.detoxStagingPath`, default `/data/local/tmp/coverage/coverage.ec`): **`mkdir -p` the parent** before `run-as … cat … > staging`. Unlike Detox’s `/data/local/tmp/detox/`, this package does not create that directory for you (`rn-coverage android pull` does). -2. Land `emulator_coverage.ec` under the **app `buildDir`** (e.g. `android/app/build/outputs/code_coverage/`). Jacoco `executionData` is a `fileTree` over `project.buildDir` (+ matched libraries). Pulling only into `artifacts/` leaves `:app:jacocoTestReport` **SKIPPED** with empty data. -3. Assert matchers: Jacoco XML package names use **slashes** (`com/coverage/fixture`). Config often uses Java **dots** (`coverage.fixture`). `rn-coverage assert` normalizes `/` ↔ `.` — keep matchers readable either way. - -## Action pins - -Pin third-party Actions by **full commit SHA** (and comment the release tag). Typo’d SHAs fail the job before any app code runs. - -## Artifacts - -Upload `artifacts/e2e/coverage/**` and `artifacts/e2e/logs/**` with `if: always()` so failed pulls/asserts remain diagnosable. - -Each iOS cell has one narrowly filtered `simctl log stream`. Appium simulator -capture is disabled while `showXcodeLog` retains WDA host build output in the -Appium log. Full build/Appium/simulator logs stay in artifacts; live output is -limited to phase markers and failure excerpts. Screenshots, page source, app -state, process/port state, and flake classification are failure-only. - -## Reproducible setup - -CI installs the root `Gemfile.lock` with `BUNDLE_FROZEN=true` and runs -`bundle exec pod install`. The Gemfile pins `json` to `2.21.2` because `json` -3.x dropped `quirks_mode`, which CocoaPods 1.17 / ActiveSupport 7.2 / Expo -autolinking still pass to `JSON.parse`. The Expo static cell wipes generated -`example/ios/Pods` and `Podfile.lock` before install so a leftover lock cannot -disagree with `Pods/Local Podspecs` (e.g. ExpoModulesWorklets after an SDK -patch). Yarn, simulator boot, Bundler, and transient pod operations use -bounded retries. Appium driver install is idempotent: list output is -checked on stdout+stderr, and “already installed” is success. Expo-backed -Metro runs set `EXPO_UNSTABLE_HEADLESS=1`; completed logs must not contain -a standalone React Native DevTools installation failure. - -Live Appium cells are CI/operator-gated. They are not Detox jobs, must not use -RNFB slot2/slot3, and should use a dedicated iPhone 17 simulator on Xcode 26. +--- +title: CI with Appium +description: Run iOS and Android coverage on GitHub Actions with Appium, and fix the Xcode, simulator, and emulator failures that stop it. +--- + +These Appium settings make this repository's CI matrix cells pass on GitHub Actions. Use the same settings in your own [dedicated test app](/coverage#dedicated-test-app). + +## Test matrix + +The CI matrix runs three cells: + +| Cell | Test app | What it checks | +|------|----------|----------------| +| `e2e:ios:dynamic` | `example-dynamic/`, React Native CLI, `USE_FRAMEWORKS=dynamic` | LCOV with hits from a dynamic `CoverageFixture.framework` | +| `e2e:ios:static` | `example/`, Expo prebuild, static libraries | LCOV with fixture hits from the merged app binary | +| `e2e:android` | `example/`, Expo, emulator | Emma `.ec` to JaCoCo XML, then `assert` | + +The CI scripts are under `scripts/ci/` and the WebDriverIO specs under `e2e/` in the repository. + +## iOS: Xcode and the simulator + +- Use the `macos-26` runner with `maxim-lobanov/setup-xcode` at `latest-stable`. Expo SDK 57 needs Xcode 26.4 or later. +- Name the simulator `iPhone 17`. Xcode 26.6 images do not ship a plain `iPhone 16`, and a wrong name fails the destination lookup. +- Boot with an exact-name match, open `Simulator.app`, then poll `bootstatus`. A headless `simctl boot` followed by an Appium restart hung at the WebDriverAgent timeout on GitHub Actions. See the `scripts/ci/boot-ios-simulator.sh` script. +- Set `simulatorStartupTimeout` to 300 seconds or more, and set the WebDriverAgent and session timeouts above the three to five minutes a cold WebDriverAgent build takes. +- Run `xcrun simctl list` once before the run. +- After `/status` is ready, prefetch the Metro bundle. Prebuild WebDriverAgent once under `artifacts/e2e/wda-derived-data` and pass it to the driver with `usePrebuiltWDA` and `derivedDataPath`. Do not put `DerivedData` in `e2e/node_modules`. +- Do not use `usePreinstalledWDA` on a simulator. It launches the `.xctrunner` app with plain `simctl`, which exits at `domain:dyld(6) code:1` because `simctl` never injects the XCTest frameworks. Appium then polls `127.0.0.1:8100/status` until `wdaLaunchTimeout`. A readiness watchdog, `IOS_WDA_READY_DEADLINE` with a default of 180 seconds, aborts the attempt when the port is dead. +- Use two or three serialized outer WebDriverIO attempts with `connectionRetryCount: 0`. Between attempts, delete sessions, terminate and reinstall the app, stop Appium, clear the Appium and WebDriverAgent ports, and reuse the prebuilt WebDriverAgent. Never overlap `POST /session`. +- Keep `noReset: false`, `enforceAppInstall: true`, and `forceAppLaunch: true`, and do not launch the app with `simctl launch` first. +- Build with `-derivedDataPath TEST_APP/ios/build` and pass that product as `appium:app`, for example `ios/build/Build/Products/Debug-iphonesimulator/APP_NAME.app`. Do not search `~/Library/Developer/Xcode/DerivedData`, and do not launch by bundle ID alone. WebDriverAgent uses its own derived-data folder. + +## Android: emulator and Metro + +- Before you launch the app, run `adb reverse tcp:8081 tcp:8081`. A debug APK loads JavaScript from Metro, and the emulator's `localhost` is not the host. Without the reverse, the bundle never loads and Appium never finds the UI. +- Point Appium at the Gradle product, `android/app/build/outputs/apk/debug/app-debug.apk`, with `noReset: false` and `enforceAppInstall: true`. A cached Android Virtual Device (AVD) otherwise keeps an APK with the same `versionCode` and skips the reinstall. +- Use a shared helper that selects by `UiSelector().resourceId(...)` on Android and `~id` on iOS. React Native `testID` maps to `resource-id` on Android, not to the accessibility ID. +- Allow a long `appWaitDuration` for the first Metro bundle. + +## Android: pull coverage for JaCoCo + +1. Create the parent of the staging path, `android.detoxStagingPath` with a default of `/data/local/tmp/coverage/coverage.ec`, before `run-as … cat … > staging`. `rn-coverage android pull` does this for you. +2. Land `emulator_coverage.ec` under the app `buildDir`, for example `android/app/build/outputs/code_coverage/`. JaCoCo's `executionData` is a `fileTree` over `project.buildDir` plus the matched libraries. A file pulled only into `artifacts/` leaves `:app:jacocoTestReport` skipped with empty data. +3. Run `rn-coverage assert` with slash or dot package names. JaCoCo XML uses slashes, `com/coverage/fixture`, and config often uses dots, `coverage.fixture`. + +## Pin GitHub Actions + +Pin every third-party Action by full commit SHA, with the release tag in a comment. A mistyped SHA fails the job before any app code runs. + +## Upload artifacts + +Upload `artifacts/e2e/coverage/**` and `artifacts/e2e/logs/**` with `if: always()`, so a failed pull or assert can still be diagnosed. + +Each iOS cell keeps one filtered `simctl log stream`. Appium's simulator capture is off, and `showXcodeLog` keeps the WebDriverAgent build output in the Appium log. Full build, Appium, and simulator logs go to artifacts. On failure only, the harness writes screenshots, page source, app state, process and port state, and the flake classification. + +## Reproduce CI locally + +CI installs the root `Gemfile.lock` file with `BUNDLE_FROZEN=true` and runs `bundle exec pod install`. The Gemfile pins `json` to `2.21.2`, because `json` 3.x dropped `quirks_mode`, which CocoaPods 1.17, ActiveSupport 7.2, and Expo autolinking still pass to `JSON.parse`. The Expo static cell deletes the `example/ios/Pods` directory and the `Podfile.lock` file before install so a stale lock cannot disagree with the `Pods/Local Podspecs` path. Yarn, the simulator boot, Bundler, and pod operations retry a bounded number of times. Expo-backed Metro runs set `EXPO_UNSTABLE_HEADLESS=1`. + +To run a cell on your machine, use the same scripts CI does: + +```sh +yarn e2e:ios:dynamic +yarn e2e:ios:static +yarn e2e:android +``` diff --git a/docs/integration/codecov.mdx b/docs/integration/codecov.mdx new file mode 100644 index 0000000..040def7 --- /dev/null +++ b/docs/integration/codecov.mdx @@ -0,0 +1,19 @@ +--- +title: Upload to Codecov +description: Codecov displays the LCOV and JaCoCo files react-native-coverage already produced. What to upload, and how flags keep platforms separate. +--- + +Codecov does not collect native coverage from your simulator. `react-native-coverage` writes the report files; Codecov stores and displays whatever you upload. Flush, pull, report, and assert still live in this package. For how that pipeline fits together, see [Coverage](/coverage). + +## What to upload + +After pull and report, upload each file under its own Codecov flag so platforms stay separate: + +| Report | Producer | +|--------|----------| +| iOS LCOV | `rn-coverage ios export` / `ios report` | +| Android JaCoCo XML | `rn-coverage android report` | +| TypeScript LCOV | `rn-coverage js report` (when `RN_COVERAGE_JS=1`) | +| Unit LCOV | Jest `--coverage` (library maintainers) | + +Paths depend on your config and CI layout. Command examples are on [App developers](/app-developers). For a live harness upload from this package (not your product app), see [Coverage](/coverage). diff --git a/docs/integration/e2e-timing.mdx b/docs/integration/e2e-timing.mdx deleted file mode 100644 index c0c0e9e..0000000 --- a/docs/integration/e2e-timing.mdx +++ /dev/null @@ -1,20 +0,0 @@ -# E2E timing (document only) - -Glue for when to flush and pull belongs in the consumer test runner (Jet, Detox, Mocha, Appium, etc.). This package does not ship a particular e2e framework. - -Typical sequence: - -1. Run instrumented e2e against the dedicated test app (`RN_COVERAGE_JS=1` for Istanbul). -2. Invoke `Coverage.flush()` (TurboModule) once at suite teardown — native buffers **and** `global.__coverage__` when present. -3. Host-side: `rn-coverage android pull` / `rn-coverage ios pull`. -4. `rn-coverage android report` or `rn-coverage ios export` (+ optional `ios report` / `ios summary`). -5. Optional: `rn-coverage js pull` + `rn-coverage js report` (NYC source-map remap). -6. Optional: `rn-coverage assert` (exit 2 on empty artifacts when `strict: true`). - -The example app documents Appium as the intended e2e runner. CI wiring: - -- `yarn e2e:ios:dynamic` — primary bare-RN dynamic frameworks cell -- `yarn e2e:ios:static` — Expo static cell -- `yarn e2e:android` — Expo Android Jacoco + JS cell - -Scripts live under `scripts/ci/`; WDIO specs under `e2e/`. See also [CI Appium notes](ci-appium.md) and [JS / TypeScript](js.md). diff --git a/docs/integration/ios.mdx b/docs/integration/ios.mdx index 9625432..60a582b 100644 --- a/docs/integration/ios.mdx +++ b/docs/integration/ios.mdx @@ -1,10 +1,13 @@ -# iOS integration +--- +title: iOS +description: Apply LLVM coverage flags through the CocoaPods helper, flush from the TurboModule, and export LCOV from the iOS Simulator. +--- -- New Architecture only; TurboModule autolinks via the podspec. -- Flusher packaging (dynamic frameworks): **mode (c)** — Pod LINKEDIT for - configured `frameworkNamePrefixes` **and** the main executable (see spike verdict). +The [test app](/coverage#dedicated-test-app) must run the New Architecture; the TurboModule autolinks through the `Coverage.podspec` file. -## Expo (recommended) +## Expo + +1. Add the config plugin. It adds the `Podfile` helper call for you: ```json { @@ -22,37 +25,23 @@ } ``` -Keep `forceDynamicFrameworks` **false** under Expo (React-Core is force-static). Set **true** only on bare RN / RNFB hosts that build React as dynamic frameworks. - -## CI cells (honest matrix) - -| Cell | App | Linkage | Gate | -|------|-----|---------|------| -| **iOS dynamic (primary)** | `example-dynamic/` bare RN | `use_frameworks! :linkage => :dynamic` | Non-zero fixture LCOV **and** `CoverageFixture.framework` is a dylib | -| **iOS static** | `example/` Expo | staticlib merge into app / `.debug.dylib` | Non-zero fixture LCOV | -| **Android** | `example/` Expo | Jacoco-instrumented libraries | Non-zero fixture package LINE hits | - -Appium (WebDriverIO) drives the harness; see `e2e/` and `scripts/ci/run-ios-e2e-cell.sh`. - -1. Sets `ios.useFrameworks=dynamic` in Podfile properties (needed for multi-image LINKEDIT on dynamic-React hosts). -2. Requires the shipped Ruby helper and calls `apply_post_install!` once (safe split — no Podfile regex for LLVM flags). -3. Optionally restores dynamic frameworks for `Coverage` + matched fixture pods - (`forceDynamicFrameworks: true`) — **only when React is also dynamic** - (bare RN / RNFB). Leave `false` under Expo (React-Core is force-static); - CocoaPods rejects dynamic Coverage* pods that transitively depend on static - React-Core. Under Expo, Coverage* remain static libraries merged into the app. +2. Set up the native iOS project: ```sh npx expo prebuild cd ios && pod install ``` -## Bare React Native / manual Podfile +Under Expo, keep `forceDynamicFrameworks` `false`. For more information, see [Static and dynamic frameworks](/integration/ios#static-and-dynamic-frameworks). + +## React Native CLI + +Call the helper from the `Podfile`: ```ruby require_relative '../node_modules/react-native-coverage/cocoapods/coverage_post_install' -# After `use_expo_modules!` (wraps Installer so restore runs after Expo's staticlib downgrade): +# After use_expo_modules!, when present, so the restore runs after Expo's static downgrade ReactNativeCoverage.install_installer_hooks! post_install do |installer| @@ -64,14 +53,33 @@ post_install do |installer| end ``` -The helper: +The helper does the following: + +- Applies `-fprofile-instr-generate` and `-fcoverage-mapping`, with the matching link flags, to the app target and to the pods that match `framework_name_prefixes`. +- Regenerates the `ios/CoverageConfig.h` file from `framework_name_prefixes`, so the flusher can read the matching images. +- Restores `Pod::BuildType.dynamic_framework` for the Coverage pods and matched pods when Expo forced `static_library` and `force_dynamic_frameworks` is `true`. -- Applies LLVM `-fprofile-instr-generate` / `-fcoverage-mapping` (+ link flags) to the app target and matching pods. -- Regenerates `ios/CoverageConfig.h` from `framework_name_prefixes`. -- Restores `Pod::BuildType.dynamic_framework` for Coverage / fixture pods when Expo forced `static_library`. +Do not add the LLVM flags by editing the `Podfile` with a regular expression. The helper is the supported path, and the Expo plugin calls the same helper. -## Runtime + CLI +## Static and dynamic frameworks -Call `flush()` before `rn-coverage ios pull`, then `rn-coverage ios export` for LCOV. +The flusher reads LLVM counters from the `LINKEDIT` section of each loaded image: the main executable, and every framework whose name starts with one of `frameworkNamePrefixes`. + +- With static libraries, which is Expo's default, your pods are merged into the app binary. The main executable carries the counters, and one LCOV covers everything. +- With dynamic frameworks, each framework is its own image with its own counters. `frameworkNamePrefixes` selects which images the flusher reads. + +Only when React itself builds as dynamic frameworks, as in this repository's `example-dynamic/` app, set `forceDynamicFrameworks` to `true`. Under Expo, `React-Core` is a static library, and CocoaPods rejects a dynamic Coverage pod that depends on a static `React-Core`. Leave the value `false` there and the Coverage pods stay static libraries merged into the app. + +## Pull and export + +When the suite ends, `Coverage.flush()` writes the `.profraw` files into the simulator container. On the host, pull the profiles, export LCOV, and assert coverage: + +```sh +rn-coverage ios pull --device UDID # copies the .profraw files out of the container +rn-coverage ios export --derived-data DERIVED_DATA_PATH # merges them and writes LCOV +rn-coverage ios report --derived-data DERIVED_DATA_PATH # llvm-cov report, optional +rn-coverage ios summary --derived-data DERIVED_DATA_PATH # llvm-cov summary, optional +rn-coverage assert # exits 2 when the LCOV has no matched hits +``` -The Expo config plugin wires the Podfile helper call; it does **not** replace the Ruby helper for Pod LLVM flags (safe split). +Build the test app with `-derivedDataPath` so `--derived-data` points at a folder you control. The CLI handles universal (multi-arch) simulator binaries for you; `--arch` overrides the selection. For more information, see [CLI](/reference/cli). diff --git a/docs/integration/js.mdx b/docs/integration/js.mdx index ba0c283..eb79107 100644 --- a/docs/integration/js.mdx +++ b/docs/integration/js.mdx @@ -1,63 +1,101 @@ -# JavaScript / TypeScript coverage - -Complete the React Native coverage story alongside native LLVM/Jacoco: - -| Layer | Tooling | Artifact | -|-------|---------|----------| -| Unit (package) | Jest `--coverage` | `coverage/unit/lcov.info` | -| E2e JS/TS | `babel-plugin-istanbul` + NYC source-map remap | `coverage/js/lcov.info` | -| Native iOS | llvm-cov | `lcov.info` | -| Native Android | Jacoco | `jacocoTestReport.xml` | +--- +title: JavaScript and TypeScript +description: Instrument Metro with babel-plugin-istanbul, pull global.__coverage__, and remap to TypeScript lines with NYC. +--- + +Native coverage shows that your Objective-C++, Swift, and Kotlin ran. This page is the JavaScript and TypeScript side of the same end-to-end run. + +## Instrument Metro + +When `RN_COVERAGE_JS=1`, load `babel-plugin-istanbul` so you leave a normal development bundle untouched. Add this configuration to the [test app](/coverage#dedicated-test-app)'s `babel.config.js` file: + +```js +const path = require('path'); + +const workspaceRoot = path.resolve(__dirname, '..'); + +module.exports = function (api) { + // RN_COVERAGE_JS must invalidate the cached config, not stick from first use. + api.cache.using(() => process.env.RN_COVERAGE_JS); + const plugins = []; + if (process.env.RN_COVERAGE_JS === '1') { + plugins.push([ + 'istanbul', + { + cwd: workspaceRoot, + include: [ + 'tests/App.tsx', + 'tests/index.ts', + 'tests/src/**/*.{ts,tsx,js,jsx}', + 'packages/my-lib/src/**/*.{ts,tsx}', + ], + }, + ]); + } + return { + presets: ['babel-preset-expo'], + plugins, + }; +}; +``` -## Instrument Metro (e2e) +Start Metro with the variable set: -Set `RN_COVERAGE_JS=1` before starting Metro (CI scripts do this). Example babel configs load `babel-plugin-istanbul` only when that env is set. +```sh +RN_COVERAGE_JS=1 npx expo start +``` -`flush()` dumps `global.__coverage__` via TurboModule `dumpJsCoverage` **before** the native Emma/LLVM flush: +When the suite ends, `Coverage.flush()` dumps `global.__coverage__` through the TurboModule before the native flush. The file lands at `files/coverage-final.json` on Android and `Documents/coverage-final.json` on iOS. -- Android → `filesDir/coverage-final.json` (`files/coverage-final.json` under `run-as`) -- iOS → `Documents/coverage-final.json` +## Pull and report -## CLI +Pull coverage from the device, then remap it: ```sh rn-coverage js pull --platform android --output coverage/js -rn-coverage js pull --platform ios --device --output coverage/js +rn-coverage js pull --platform ios --device UDID --output coverage/js rn-coverage js report \ --input coverage/js/coverage-final.json \ --output coverage/js \ --cwd . \ - --nyc-config example/nyc.config.js + --nyc-config tests/nyc.config.js ``` -NYC is configured with `sourceMap: true` and `exclude-after-remap: true` so LCOV `SF:` paths point at **TypeScript** sources (not only the Metro-transformed JS line map). Ship a `nyc.config.js` next to the harness (see `example/nyc.config.js`). +`js report` runs NYC with `sourceMap: true` and `exclude-after-remap: true`, so the LCOV `SF:` paths point at your TypeScript sources rather than the Metro output. Put a `nyc.config.js` file next to the test app: + +```js +const path = require('path'); + +module.exports = { + 'check-coverage': false, + include: [ + 'tests/App.tsx', + 'tests/index.ts', + 'tests/src/**/*.{ts,tsx,js,jsx}', + 'packages/my-lib/src/**/*.{ts,tsx}', + ], + exclude: ['**/node_modules/**', '**/__tests__/**'], + cwd: path.resolve(__dirname, '..'), + sourceMap: true, + 'exclude-after-remap': true, + instrument: false, + reporter: ['lcov', 'text-summary', 'html'], +}; +``` -Both harness configs include their entrypoint/App source and the shared -`example/fixture-lib/src` workspace. CI runs `assert-js-lcov.js` after NYC and -requires non-zero records for both the harness and fixture library; merely -creating an LCOV file is not sufficient. +## Include workspace libraries -### Instrumentation scope is workspace-rooted + + A default `babel-plugin-istanbul` roots `test-exclude` at the Babel working directory and skips every file outside it. A yarn workspace library is a symlink, so Metro hands Babel its real path, outside the test app. The bundle builds, the suite passes, and the LCOV omits the library. + -`coverage-fixture` is a yarn workspace symlink, so Metro resolves it to the -realpath `example/fixture-lib/src/*.ts` — outside `example-dynamic/`. A -default-configured `babel-plugin-istanbul` roots `test-exclude` at the babel -cwd and silently skips everything outside it, so the bundle builds and the e2e -passes while the LCOV quietly omits the shared library. +The fix is in both preceding snippets. Pass `cwd` and `include` to the Babel plugin and to the `nyc.config.js` file, rooted at the monorepo, and list the library's sources in `include`. `SF:` paths then come out workspace-relative, such as `packages/my-lib/src/index.ts`, on every platform. -Both harnesses therefore pass explicit `cwd`/`include` options to -`babel-plugin-istanbul` and set matching `cwd`/`include` in `nyc.config.js`, -all rooted at the monorepo. `SF:` paths are workspace-relative -(`example/fixture-lib/src/index.ts`, `example-dynamic/App.tsx`) in every cell. -`scripts/ci/assert-istanbul-scope.js` runs in the `unit` job and fails if -either harness stops instrumenting the fixture sources — a device-free guard, -since the e2e cells themselves cannot catch a scope regression. +## Upload to Codecov -## Codecov +Upload the JavaScript LCOV under its own flag so it stays separate from the native reports: -CI uploads unit LCOV, e2e native LCOV/Jacoco, and e2e JS LCOV with distinct -flags (`unit-js`, `e2e-ios-dynamic`, `e2e-ios-static`, and `e2e-android`). -Reports are explicit; automatic report search is disabled. Configure the -repository-specific `CODECOV_TOKEN`. Upload failures are blocking except for -Dependabot, and uploads run only after successful report generation. +```sh +codecov -f coverage/js/lcov.info -F e2e-js +``` diff --git a/docs/library-maintainers.mdx b/docs/library-maintainers.mdx index 1727669..58af029 100644 --- a/docs/library-maintainers.mdx +++ b/docs/library-maintainers.mdx @@ -1,126 +1,90 @@ --- title: Library maintainers -description: Prove your React Native library's native code actually runs - both in unit tests and in a dedicated test app - and gate it in CI with per-flag Codecov uploads. +description: Measure a React Native library's native code from unit tests and from a test app, and fail CI when that package has no hits. --- -If you maintain a React Native library with native code, "tested" should mean your -**Objective-C++/Swift and Kotlin actually executed** — not just that a JS mock returned the value -you asserted. This page shows the two layers that get you there, and how this very repository -wires them (it's the reference implementation). +The install itself is on [App developers](/app-developers); do that first in your [test app](/coverage#dedicated-test-app), then come back here for what is specific to a library. - - `react-native-coverage` is itself a monorepo with a library at the root and harness apps in - `example/` (Expo) and `example-dynamic/` (bare, dynamic frameworks). Copy that shape. - +Unit tests and the test app measure different things. Keep both layers: -## The two layers +| Layer | Tooling | Artifact | Codecov flag | +|-------|---------|----------|--------------| +| Unit tests | Jest `--coverage` | `coverage/unit/lcov.info` | `unit-js` | +| Test app, JavaScript and TypeScript | `babel-plugin-istanbul` and the NYC (Istanbul CLI) remap | `coverage/js/lcov.info` | `e2e-js` | +| Test app, native iOS | TurboModule flush, then `llvm-cov` | `lcov.info` | `e2e-ios-dynamic`, `e2e-ios-static` | +| Test app, native Android | TurboModule flush, then JaCoCo | `jacocoTestReport.xml` | `e2e-android` | -| Layer | Tooling | Artifact | Codecov flag (suggested) | -|-------|---------|----------|--------------------------| -| Unit (package) | Jest `--coverage` | `coverage/unit/lcov.info` | `unit-js` | -| E2e JS/TS | `babel-plugin-istanbul` + NYC remap | `coverage/js/lcov.info` | `e2e-js` | -| E2e native iOS | TurboModule flush → llvm-cov | `lcov.info` | `e2e-ios-dynamic`, `e2e-ios-static` | -| E2e native Android | TurboModule flush → Jacoco | `jacocoTestReport.xml` | `e2e-android` | +## Before you begin -## Layer 1 — unit tests +- Use a monorepo with a [dedicated test app](/coverage#dedicated-test-app) that renders your library's native surface. +- Copy this repository's shape: the library at the root, with the `example/` (Expo) and `example-dynamic/` (React Native CLI, dynamic frameworks) directories beside it. -Your existing Jest suite already exercises the JS/TS surface. Turn on coverage and upload it: +## Measure unit tests + +Your Jest suite already exercises the JavaScript and TypeScript surface. Turn on coverage and upload it: ```sh -jest --coverage # → coverage/unit/lcov.info +jest --coverage # writes coverage/unit/lcov.info codecov -f coverage/unit/lcov.info -F unit-js ``` - - Unit tests can't prove native code ran — they run in Node with the native module mocked. That's - what the test-app layer is for. Keep both; they measure different things. - - -## Layer 2 — a dedicated test app - -Add a harness app to your monorepo (Pattern C) that renders and exercises your library's native -surface, then drive it with an e2e runner (Appium, Detox, Maestro …). - - - - In the harness, configure the plugin (Expo) or helpers (bare) with **your** library's - identifiers: - - ```json - { - "expo": { - "plugins": [ - [ - "react-native-coverage", - { - "libraryProjectMatchers": ["my-lib"], - "frameworkNamePrefixes": ["MyLib"], - "enableAndroidCoverage": true - } - ] - ] - } - } - ``` - - `libraryProjectMatchers` matches your Android Gradle library project(s); `frameworkNamePrefixes` - matches your iOS framework(s) so their LINKEDIT sections get flushed. - - - - Load `babel-plugin-istanbul` only when `RN_COVERAGE_JS=1`, and root `test-exclude` at the - monorepo so a symlinked workspace library is actually instrumented: - - - A default `babel-plugin-istanbul` roots `test-exclude` at the babel cwd and silently skips - anything outside it — including a yarn-workspace library resolved to its realpath. Pass - explicit `cwd`/`include` in both the babel plugin options and `nyc.config.js`, rooted at the - monorepo, or your shared library will quietly vanish from the LCOV. See - [JS / TypeScript](/integration/js) for the exact setup and the device-free guard that catches - a scope regression. - - - - - ```ts - import Coverage from 'react-native-coverage'; - await Coverage.flush(); // native buffers + global.__coverage__ when instrumented - ``` - - - - ```sh - rn-coverage android pull && rn-coverage android report - rn-coverage ios pull && rn-coverage ios export && rn-coverage ios report - rn-coverage assert - ``` - - - -## Make "empty" fail — for _your_ package specifically - -A green e2e that produced an empty LCOV is worse than no coverage: it's a false negative waiting -to rot. Configure `assert` to require non-zero hits in **your** library's paths, not just any file: +Unit tests run in Node with the native module mocked, so they cannot show that native code ran. The test app layer does that. Keep both. + +## Measure native code in a test app + +### Point coverage at your package + +In the test app, configure the plugin (Expo) or the helpers (React Native CLI) with your library's names: + +```json +{ + "expo": { + "plugins": [ + [ + "react-native-coverage", + { + "libraryProjectMatchers": ["my-lib"], + "frameworkNamePrefixes": ["MyLib"], + "enableAndroidCoverage": true + } + ] + ] + } +} +``` + +`libraryProjectMatchers` matches your Android Gradle library projects. `frameworkNamePrefixes` matches your iOS frameworks so the flush includes their `LINKEDIT` counters. + +### Include workspace libraries in JavaScript coverage + +A default `babel-plugin-istanbul` roots `test-exclude` at the Babel working directory and skips anything outside it, including a yarn workspace library resolved to its real path. Pass `cwd` and `include` to the Babel plugin and to the `nyc.config.js` file. Root both at the monorepo so the shared library stays in the LCOV. For more information about the setup, see [JavaScript and TypeScript](/integration/js). + +### Install, flush, and pull + +The install, the flush at the end of the run, and the pull and report commands are the same as for an app. Follow App developers from **Install in the test app** to **Pull, report, and fail CI on empty coverage**. + +## Fail CI when your package has no hits + +A passing end-to-end run that writes an empty LCOV hides a broken pipeline. Configure `assert` to require hits in your library's paths, not just any file: ```js // react-native-coverage.config.js module.exports = { - strict: true, // exit 2 on empty artifacts (CI default) + strict: true, // exit 2 on empty reports, the CI default assert: { - lcovPathIncludes: ['packages/my-lib'], // ≥1 iOS LCOV SF: must match - jacocoPackageIncludes: ['com.my.lib'], // ≥1 Jacoco package must have LINE hits + lcovPathIncludes: ['packages/my-lib'], // at least one LCOV SF: path must match and have hits + jacocoPackageIncludes: ['com.my.lib'], // at least one JaCoCo package must have LINE hits defaultLcovPath: 'coverage/ios/lcov.info', defaultJacocoXmlPath: 'coverage/android/jacocoTestReport.xml', }, }; ``` -See [Config](/config) for every key. +For more information about every key, see [Configuration](/reference/config). -## Per-flag Codecov uploads +## Upload one Codecov flag per platform -Upload each cell under its own flag and disable automatic report search so results stay explicit -(mirrors this repo's [`codecov.yml`](https://github.com/invertase/react-native-coverage/blob/main/codecov.yml)): +Upload each cell under its own flag and disable automatic report search, as this repository's [`codecov.yml`](https://github.com/invertase/react-native-coverage/blob/main/codecov.yml) file does: ```sh codecov -f coverage/unit/lcov.info -F unit-js @@ -129,22 +93,26 @@ codecov -f coverage/ios-static/lcov.info -F e2e-ios-static codecov -f coverage/android/jacocoTestReport.xml -F e2e-android ``` -That flag split is what produces a dashboard where iOS-dynamic, iOS-static, and Android each carry -their own number — exactly like the [proof on this repo](/why#proof-live-on-main). +That split gives iOS dynamic, iOS static, and Android their own number, as in the [example from this package](/coverage#example-from-this-package). -## Reference implementation +## Run this repository's test apps - - - Library at root, `example/` + `example-dynamic/` harnesses, `e2e/` specs, CI scripts. - - - The per-flag dashboard your setup should reproduce. +Run the coverage end-to-end scripts for this repository: + +```sh +yarn e2e:ios:dynamic # example-dynamic/, React Native CLI, dynamic frameworks +yarn e2e:ios:static # example/, Expo, static libraries +yarn e2e:android # example/, Expo, JaCoCo and JavaScript +``` + + + + Library at the root, `example/` and `example-dynamic/` test apps, `e2e/` specs, CI scripts. - - Pitfalls the reference CI already solved. + + The per-flag dashboard your setup reproduces. - - Istanbul + NYC source-map remap and the scope guard. + + The GitHub Actions job, and the failures it already fixed. diff --git a/docs/pattern-c.mdx b/docs/pattern-c.mdx deleted file mode 100644 index a6cc9ae..0000000 --- a/docs/pattern-c.mdx +++ /dev/null @@ -1,5 +0,0 @@ -# Pattern C - dedicated test apps only - -`react-native-coverage` is intended for **dedicated test / e2e harness apps** (for example a monorepo `tests/` app), not production product `package.json` trees. - -Autolinking still scans dependencies; do not rely on `devDependency` alone to keep the TurboModule out of a product app. Keep the package in the test app workspace only. diff --git a/docs/reference/cli.mdx b/docs/reference/cli.mdx new file mode 100644 index 0000000..fad464f --- /dev/null +++ b/docs/reference/cli.mdx @@ -0,0 +1,163 @@ +--- +title: CLI +description: Commands, flags, and exit codes for rn-coverage, including exit code 2 when coverage is empty. +--- + +The binary is `rn-coverage`. Run it from the [test app](/coverage#dedicated-test-app)'s folder, where the `react-native-coverage.config.js` file lives, or pass `-c`. + +## Commands + +The CLI supports these commands. + +```sh +rn-coverage --help +rn-coverage [--strict|--no-strict] [-c ] + +rn-coverage android pull [--device ] [--output ] [--retries ] +rn-coverage android report [--android-dir ] [--jacoco-xml ] + +rn-coverage ios pull --device [--output ] +rn-coverage ios export --derived-data [--configuration ] [--app-name ] [--output ] [--arch ] +rn-coverage ios report --derived-data [--configuration ] [--app-name ] [--profdata ] [--output-dir ] [--arch ] +rn-coverage ios summary --derived-data [--configuration ] [--app-name ] [--profdata ] [--arch ] + +rn-coverage js pull [--platform android|ios] [--device ] [--output ] +rn-coverage js report --input [--output ] [--cwd ] [--nyc-config ] + +rn-coverage assert [--platform ios|android|all] [--lcov ] [--jacoco-xml ] +``` + +### Global flags + +These flags apply to every command. + +| Flag | Default | What it does | +|------|---------|--------------| +| `-c, --config ` | `react-native-coverage.config.js` in the current folder | Path to the config file | +| `--strict` | from config, `true` | Exits with 2 when a report is missing or has no hits | +| `--no-strict` | | Warns and exits with 0 on the same conditions. Not for CI. | + +### android pull + +Copies the Emma `.ec` file out of the app with `run-as`, and stages it under the `android/app/build/` directory so the JaCoCo `fileTree` includes it. + +| Flag | Default | What it does | +|------|---------|--------------| +| `--device ` | the only connected device | `adb` device serial | +| `--output ` | | Extra local copy of the `.ec` file | +| `--retries ` | `15` | Attempts as the command waits for the `.ec` file to appear | + +### android report + +Runs the `jacocoTestReport` Gradle task and checks the XML it writes. + +| Flag | Default | What it does | +|------|---------|--------------| +| `--android-dir ` | `android` | Android project folder | +| `--jacoco-xml ` | `android.jacocoReportXml` from config | JaCoCo XML to check after the report | + +### ios pull + +Copies the `.profraw` files out of the simulator app container. + +| Flag | Default | What it does | +|------|---------|--------------| +| `--device ` | required | Simulator UDID | +| `--output ` | | Local output folder | + +### ios export + +Merges the `.profraw` files into a `profdata` file, runs `llvm-cov export`, and writes LCOV. + +| Flag | Default | What it does | +|------|---------|--------------| +| `--derived-data ` | required | Xcode derived data folder of the test app build | +| `--configuration ` | `Debug` | Xcode configuration | +| `--app-name ` | `app.iosProductName` from config | App product name | +| `--output ` | `coverage/ios/lcov.info` | LCOV output path | +| `--arch ` | auto | `llvm-cov -arch` for a universal binary | + +`llvm-cov` cannot read a universal (multi-arch) Mach-O without `-arch`, so a simulator build with both `arm64` and `x86_64` slices exports 0%. A thin binary gets no `-arch`. A universal binary gets the host architecture when present, otherwise the first slice. An `--arch` flag or `ios.arch` config value overrides auto selection. + +### ios report + +Writes an HTML report with `llvm-cov show -format=html`. Requires a prior `ios export` run. + +| Flag | Default | What it does | +|------|---------|--------------| +| `--derived-data ` | required | Xcode derived data folder | +| `--configuration ` | `Debug` | Xcode configuration | +| `--app-name ` | `app.iosProductName` from config | App product name | +| `--profdata ` | `coverage/ios/profdata` | Merged profdata file from `ios export` | +| `--output-dir ` | `coverage/ios/html` | HTML output folder | +| `--arch ` | auto | `llvm-cov -arch` for a universal binary | + +### ios summary + +Prints a terminal summary with `llvm-cov report`. Requires a prior `ios export` run. + +| Flag | Default | What it does | +|------|---------|--------------| +| `--derived-data ` | required | Xcode derived data folder | +| `--configuration ` | `Debug` | Xcode configuration | +| `--app-name ` | `app.iosProductName` from config | App product name | +| `--profdata ` | `coverage/ios/profdata` | Merged profdata file from `ios export` | +| `--arch ` | auto | `llvm-cov -arch` for a universal binary | + +### js pull + +Copies the Istanbul `coverage-final.json` file out of the app container. + +| Flag | Default | What it does | +|------|---------|--------------| +| `--platform ` | `android` | `android` or `ios` | +| `--device ` | | `adb` serial or simulator UDID | +| `--output ` | `coverage/js` | Local output folder | + +### js report + +Runs NYC over the Istanbul JSON and remaps the hits to TypeScript sources. + +| Flag | Default | What it does | +|------|---------|--------------| +| `--input ` | required | The `coverage-final.json` file, or a folder of Istanbul JSON files | +| `--output ` | `coverage/js` | Report folder | +| `--cwd ` | current folder | NYC `cwd` for the include globs | +| `--nyc-config ` | | Path to `nyc.config.js` | + +### assert + +Checks the LCOV and JaCoCo XML after the pipeline and exits 2 when either has no matched hits. + +| Flag | Default | What it does | +|------|---------|--------------| +| `--platform ` | `all` | `ios`, `android`, or `all` | +| `--lcov ` | `assert.defaultLcovPath` from config | LCOV file that must have hits in `assert.lcovPathIncludes` | +| `--jacoco-xml ` | `assert.defaultJacocoXmlPath` from config | JaCoCo XML that must have `LINE` hits in `assert.jacocoPackageIncludes` | + +## Exit codes + +The CLI uses these exit codes. + +| Code | Meaning | +|------|---------| +| 0 | Success. With `--no-strict`, an empty report also exits 0 with a warning. | +| 1 | Error: bad invocation or a tool failure. | +| 2 | No hits, or a report file is missing, in strict mode. | + +### Strict mode and assert + +Strict mode is the config default, `strict: true`, and the right setting for CI. `--strict` and `--no-strict` override the config for one process. With `--no-strict` or `strict: false`, a missing or empty report warns and exits 0, which is useful for local runs only. + +Each command exits with 2 in these cases. + +| Command | Exits 2 when | +|---------|--------------| +| `android pull` | No `.ec` file after the retries | +| `android report` | Gradle succeeded but the JaCoCo XML is missing, empty, or has no matched `LINE` hits | +| `ios pull` | No `.profraw` in the simulator container | +| `ios export` | No `.profraw`, or the LCOV has no path matching `assert.lcovPathIncludes` with `LH` above 0 | +| `ios report`, `ios summary` | No `profdata` file; requires a prior `ios export` run | +| `assert` | The LCOV or the JaCoCo XML has no matched hits | + +The matchers and default report paths are the `assert.*` keys on [Configuration](/reference/config). For more information about exit code 2, see [Empty or missing coverage](/troubleshooting/empty-coverage). diff --git a/docs/reference/config.mdx b/docs/reference/config.mdx new file mode 100644 index 0000000..478b946 --- /dev/null +++ b/docs/reference/config.mdx @@ -0,0 +1,90 @@ +--- +title: Configuration +description: Every key in react-native-coverage.config.js for the app, iOS, Android, JavaScript, and assert, with defaults. +--- + +`rn-coverage` reads the `react-native-coverage.config.js` file from the folder it runs in, or from `-c `. A missing file means every default in the following tables. No default names a product; set the `app` keys for your [test app](/coverage#dedicated-test-app). + +## Create the config file + +Copy the [`react-native-coverage.config.js.example`](https://github.com/invertase/react-native-coverage/blob/main/react-native-coverage.config.js.example) file to the `react-native-coverage.config.js` file in your [dedicated test app](/coverage#dedicated-test-app): + +```js +// react-native-coverage.config.js +module.exports = { + app: { + androidApplicationId: 'com.example.tests', + iosBundleId: 'com.example.tests', + iosProductName: 'Tests', + }, + ios: { frameworkNamePrefixes: ['MyLib'] }, + android: { libraryProjectMatchers: ['my-lib'] }, + assert: { + lcovPathIncludes: ['packages/my-lib'], + jacocoPackageIncludes: ['com.my.lib'], + }, +}; +``` + +## Top level + +These keys sit at the root of the file. + +| Key | Default | Purpose | +|-----|---------|---------| +| `nativeModuleName` | `'Coverage'` | TurboModule name | +| `strict` | `true` | Exit 2 when a report is missing or has no hits. `--strict` and `--no-strict` override it for one process. | +| `sourcePathRewrite` | `[]` | Rules that rewrite LCOV `SF:` paths. Each rule is `{ kind: 'after-marker', marker }` or `{ kind: 'regex', pattern, replacement }`. | + +## App + +These keys name the [dedicated test app](/coverage#dedicated-test-app). + +| Key | Default | Purpose | +|-----|---------|---------| +| `app.androidApplicationId` | `'com.example.coverage'` | Package used with `run-as` for `adb` pulls | +| `app.iosBundleId` | `'com.example.coverage'` | Bundle ID used to find the simulator app container | +| `app.iosProductName` | `'CoverageExample'` | App binary and `.app` name under derived data | + +## iOS + +These keys configure iOS coverage. + +| Key | Default | Purpose | +|-----|---------|---------| +| `ios.frameworkNamePrefixes` | `[]` | Framework name prefixes whose `LINKEDIT` counters are flushed and passed to `llvm-cov` as `-object` | +| `ios.arch` | `''` | `llvm-cov -arch` for a universal binary. Empty means auto: a thin binary needs none, and a universal binary uses the host architecture. An `'arm64'` value forces the architecture. | + +## Android + +These keys sit under `android` in the config file. + +| Key | Default | Purpose | +|-----|---------|---------| +| `libraryProjectMatchers` | `[]` | Gradle library project name substrings to instrument, and the fallback JaCoCo package matchers for `assert` | +| `coverageRelativePath` | `'files/coverage.ec'` | `.ec` path under the app's files folder on the device | +| `detoxStagingPath` | `'/data/local/tmp/coverage/coverage.ec'` | Staging path on the device that `run-as … cat` writes to before `adb pull`. `android pull` creates the parent folder. | +| `jacocoReportXml` | `'…/jacocoTestReport.xml'` | JaCoCo XML written by `android report`. Full default: `android/app/build/reports/jacoco/` `jacocoTestReport/jacocoTestReport.xml`. | + +## JavaScript + +These keys sit under `js` in the config file. + +| Key | Default | Purpose | +|-----|---------|---------| +| `androidRelativePath` | `'files/coverage-final.json'` | Istanbul JSON path under `run-as` on Android | +| `androidStagingPath` | `'/data/local/tmp/coverage/coverage-final.json'` | Staging path on the device for the `adb` pull | +| `iosRelativePath` | `'Documents/coverage-final.json'` | Istanbul JSON path under the simulator app data container | + +## Assert + +These keys sit under `assert` in the config file. + +| Key | Default | Purpose | +|-----|---------|---------| +| `lcovPathIncludes` | `['packages/']` | Substrings that at least one LCOV `SF:` path with hits must contain | +| `jacocoPackageIncludes` | `[]` | JaCoCo package name substrings that must have `LINE` hits. Dots or slashes both match. | +| `defaultLcovPath` | `'coverage/ios/lcov.info'` | LCOV file `rn-coverage assert` reads when `--lcov` is not passed | +| `defaultJacocoXmlPath` | `'…/jacocoTestReport.xml'` | JaCoCo XML `rn-coverage assert` reads when `--jacoco-xml` is not passed. Full default: `android/app/build/reports/jacoco/` `jacocoTestReport/jacocoTestReport.xml`. | + +For more information about the commands that read these keys, see [CLI](/reference/cli). diff --git a/docs/reference/index.mdx b/docs/reference/index.mdx new file mode 100644 index 0000000..539f52e --- /dev/null +++ b/docs/reference/index.mdx @@ -0,0 +1,13 @@ +--- +title: Reference +description: Look up `rn-coverage` commands, flags, and exit codes, and every key in the `react-native-coverage.config.js` file. +--- + + + + Every `rn-coverage` subcommand, global flags, and what exit code 2 means. + + + Defaults for the `app`, `ios`, `android`, `js`, and `assert` keys. + + diff --git a/docs/troubleshooting/empty-coverage.mdx b/docs/troubleshooting/empty-coverage.mdx new file mode 100644 index 0000000..3098b18 --- /dev/null +++ b/docs/troubleshooting/empty-coverage.mdx @@ -0,0 +1,42 @@ +--- +title: Empty or missing coverage +description: How to resolve empty reports and rn-coverage assert failures when coverage is missing or a library has no hits. +--- + +When a report is empty or `assert` fails, match the failure to a heading below. Each section covers a common cause and how to resolve it. + +## rn-coverage assert exits 2 + +In strict mode, the default, `assert` exits `2` when a report file is missing or when no path in it has hits. That exit code means the report is missing or empty. + +Resolve the underlying coverage gap first (use the headings below), then run `assert` again. Do not switch CI to `--no-strict`; that turns the same conditions into a warning and an exit `0`, which hides the problem. For exit codes, see [the CLI reference](/reference/cli). + +## The JaCoCo report is empty + +The `.ec` file was pulled to a folder outside the app `buildDir`. JaCoCo's `executionData` is a `fileTree` over `project.buildDir` plus the matched libraries, so `:app:jacocoTestReport` runs with no data and Gradle skips it. + +To resolve it, use `rn-coverage android pull`, which stages the `emulator_coverage.ec` file under the `android/app/build/` directory. If you pull by hand, put the file under the app `buildDir`, for example the `android/app/build/outputs/code_coverage/` directory. For pull and report, see [Android](/integration/android). + +## JavaScript report is missing a workspace library + +`babel-plugin-istanbul` roots `test-exclude` at the Babel working directory and skips every file outside it. A yarn workspace library is a symlink, so Metro hands Babel the real path, outside the [test app](/coverage#dedicated-test-app). + +To resolve it, pass `cwd` and `include` to the Babel plugin and to the `nyc.config.js` file, both rooted at the monorepo, and list the library's sources in `include`. For the snippets, see [JavaScript and TypeScript](/integration/js). + +## CocoaPods rejects the Coverage pods under Expo + +`forceDynamicFrameworks` is `true` in an Expo app. `React-Core` is a static library under Expo, and CocoaPods refuses a dynamic Coverage pod that depends on a static `React-Core`. + +To resolve it, set `forceDynamicFrameworks` to `false`. When React itself builds as dynamic frameworks, use `true`. For static and dynamic frameworks, see [iOS](/integration/ios). + +## Your iOS framework or Android library has no hits + +The flusher and the Gradle helper only touch the images and projects you name. `frameworkNamePrefixes` does not match your iOS framework, or `libraryProjectMatchers` does not match your Android library project, so the flusher instruments neither. + +To resolve it, add the framework name prefix to `frameworkNamePrefixes` and the Gradle project name substring to `libraryProjectMatchers`, in the Expo plugin options or the helper calls. For `assert`, name the same package in `assert.lcovPathIncludes` and `assert.jacocoPackageIncludes`. For these keys, see [Configuration](/reference/config). + +## CI fails before coverage is pulled + +The simulator did not boot, WebDriverAgent timed out, the emulator could not reach Metro, or a GitHub Actions pin was wrong. None of these are coverage problems, but all of them stop the run before `flush()`. + +To resolve it, work through [CI with Appium](/integration/ci-appium), which lists common failures and the settings that fix them. diff --git a/docs/why.mdx b/docs/why.mdx deleted file mode 100644 index d538e98..0000000 --- a/docs/why.mdx +++ /dev/null @@ -1,118 +0,0 @@ ---- -title: Why native coverage -description: React Native is cross-platform, but coverage tooling stops at the JS bundle. Native code - especially iOS - is a black box. Here is why that matters, and the proof it can be fixed. ---- - -## The blind spot - -React Native ships two apps in one codebase, but almost every coverage report you've ever seen -covers **one language**: JavaScript. Jest, Istanbul, and the green badge on your README all -measure the bundle. - -Meanwhile the code that makes a React Native app actually _native_ — the Objective-C++ in your -TurboModules, the Swift in your app delegate, the Kotlin behind your Android package — runs on a -real device during your e2e suite and then **disappears without a trace**. - - - iOS native coverage in particular is a black box. LLVM writes `.profraw` counters into a - sandboxed simulator container, `__llvm_profile` never flushes unless you ask it to, and dynamic - frameworks keep their coverage tables in their own LINKEDIT segments. Wiring that by hand is - miserable, so almost nobody does. - - -The result: teams ship native modules behind a green checkmark that only ever proved the -**JavaScript** ran. The native branch — the error path, the platform quirk, the thing the agent -just rewrote — may never have executed under test, and nothing tells you. - -## Why now - -In an agentic workflow, a model can confidently rewrite your `.mm` file and assure you it's -tested. Coverage is the backpressure that keeps that honest. It is the difference between -"the mock returned the value I asserted" and "the native code path physically executed on a -device." Without native coverage, an agent — or a human — can hand-wave past the hardest, -most platform-specific code in the app. - -`react-native-coverage` turns that missing evidence into a one-liner: flush from the TurboModule, -`rn-coverage pull` to merge the scattered native counters and remap your JS back to TypeScript, then -`rn-coverage assert` to turn "did the native path actually run?" into a pass/fail signal for your dev loop or CI. - -No Podfile regex. No profraw archaeology. No Gradle spelunking. - -## Show, don't tell - -This repository **proves its own thesis on Codecov, live on `main`** — including the hard part, iOS. - -| Flag | What ran | Coverage | -|------|----------|---------:| -| [`e2e-ios-dynamic`](https://app.codecov.io/gh/invertase/react-native-coverage) | iOS native, dynamic frameworks (the hard case) | **90.6%** | -| [`e2e-android`](https://app.codecov.io/gh/invertase/react-native-coverage) | Android native (Emma → Jacoco) | **81.7%** | -| [`e2e-ios-static`](https://app.codecov.io/gh/invertase/react-native-coverage) | iOS native, static libraries | **63.1%** | -| [`unit-js`](https://app.codecov.io/gh/invertase/react-native-coverage) | Jest unit (JS/TS) | **52.6%** | - -These numbers come from an actual iOS Simulator and Android emulator running the harness apps -under Appium — not from a mock. - -### The iOS native directory, with per-file line coverage - -[`ios/` on Codecov →](https://app.codecov.io/gh/invertase/react-native-coverage/tree/main/ios) - -![Codecov file explorer showing the ios/ native directory with line coverage for Coverage.mm, CoverageProfile.mm and CoverageConfig.h](/assets/codecov/ios-tree.png) - -### The TurboModule itself, line by line - -[`ios/Coverage.mm` on Codecov →](https://app.codecov.io/gh/invertase/react-native-coverage/blob/main/ios/Coverage.mm) - -Real Objective-C++ — `flush()`, `dumpJsCoverage`, `getTurboModule` — shown green where a device -executed it, at **71.88%**: - -![Codecov line-by-line view of ios/Coverage.mm at 71.88 percent, Objective-C++ TurboModule code shown covered and partially covered](/assets/codecov/ios-coverage-mm.png) - -### The whole picture - -[Repository dashboard on Codecov →](https://app.codecov.io/gh/invertase/react-native-coverage) - -![Codecov dashboard for react-native-coverage showing overall coverage, trend, sunburst graph and the native code tree](/assets/codecov/dashboard.png) - -## The gap this closes - -[**React Native Firebase**](https://github.com/invertase/react-native-firebase), one of the -most-installed libraries in the ecosystem, already depends on `react-native-coverage` in its -dedicated `tests/` app. Its Codecov shows `android-native` **live at 65.9%** — while the -`ios-native` flag still reads **`0.0%`** today. - -The slot is wired. The Android native hits are real. The iOS native number is the hardest half, -and the rollout to turn that zero into a number is exactly what this package drives. - - - If a flagship library's iOS native coverage is still catching up, yours has had no path at all — - until now. This repository already shows iOS-dynamic at 90.6%; the tooling to reproduce that - anywhere is what ships here. - - -## Used in production by - -Both [**React Native Firebase**](https://github.com/invertase/react-native-firebase) and -[**React Native Google Mobile Ads**](https://github.com/invertase/react-native-google-mobile-ads) -flush real device coverage through the `react-native-coverage` TurboModule from their -[Pattern C](/pattern-c) test apps: - -

- - React Native Firebase - -        - - React Native Google Mobile Ads - -

- -## Ready? - - - - Wire native coverage into your Expo or RN CLI test app. - - - Prove your library's native code runs, in unit tests and a test app. - -