react-native-coverage 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/Coverage.podspec +20 -0
  2. package/LICENSE +202 -0
  3. package/README.md +125 -0
  4. package/android/build.gradle +51 -0
  5. package/android/rn-coverage-jacoco.gradle +132 -0
  6. package/android/rn-coverage.gradle +68 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/java/com/coverage/CoverageModule.kt +53 -0
  9. package/android/src/main/java/com/coverage/CoveragePackage.kt +35 -0
  10. package/app.plugin.js +1 -0
  11. package/bin/rn-coverage.js +11 -0
  12. package/cocoapods/coverage_post_install.rb +197 -0
  13. package/docs/cli.md +51 -0
  14. package/docs/config.md +27 -0
  15. package/docs/index.md +16 -0
  16. package/docs/integration/android.md +83 -0
  17. package/docs/integration/ci-appium.md +41 -0
  18. package/docs/integration/e2e-timing.md +20 -0
  19. package/docs/integration/ios.md +77 -0
  20. package/docs/integration/js.md +38 -0
  21. package/docs/pattern-c.md +5 -0
  22. package/docs/releasing.md +88 -0
  23. package/docs.json +24 -0
  24. package/ios/Coverage.h +5 -0
  25. package/ios/Coverage.mm +47 -0
  26. package/ios/CoverageConfig.h +21 -0
  27. package/ios/CoverageProfile.h +22 -0
  28. package/ios/CoverageProfile.mm +358 -0
  29. package/lib/module/NativeCoverage.js +10 -0
  30. package/lib/module/NativeCoverage.js.map +1 -0
  31. package/lib/module/assert-coverage.js +181 -0
  32. package/lib/module/assert-coverage.js.map +1 -0
  33. package/lib/module/cli/index.js +229 -0
  34. package/lib/module/cli/index.js.map +1 -0
  35. package/lib/module/config.js +83 -0
  36. package/lib/module/config.js.map +1 -0
  37. package/lib/module/exit-codes.js +22 -0
  38. package/lib/module/exit-codes.js.map +1 -0
  39. package/lib/module/index.js +35 -0
  40. package/lib/module/index.js.map +1 -0
  41. package/lib/module/js-coverage.js +172 -0
  42. package/lib/module/js-coverage.js.map +1 -0
  43. package/lib/module/node.js +15 -0
  44. package/lib/module/node.js.map +1 -0
  45. package/lib/module/package.json +1 -0
  46. package/lib/module/path-rewrite.js +44 -0
  47. package/lib/module/path-rewrite.js.map +1 -0
  48. package/lib/module/process-ios-native-coverage.js +258 -0
  49. package/lib/module/process-ios-native-coverage.js.map +1 -0
  50. package/lib/module/pull-native-coverage.js +210 -0
  51. package/lib/module/pull-native-coverage.js.map +1 -0
  52. package/lib/typescript/example-dynamic/App.d.ts +6 -0
  53. package/lib/typescript/example-dynamic/App.d.ts.map +1 -0
  54. package/lib/typescript/package.json +1 -0
  55. package/lib/typescript/src/NativeCoverage.d.ts +16 -0
  56. package/lib/typescript/src/NativeCoverage.d.ts.map +1 -0
  57. package/lib/typescript/src/assert-coverage.d.ts +53 -0
  58. package/lib/typescript/src/assert-coverage.d.ts.map +1 -0
  59. package/lib/typescript/src/cli/index.d.ts +4 -0
  60. package/lib/typescript/src/cli/index.d.ts.map +1 -0
  61. package/lib/typescript/src/config.d.ts +93 -0
  62. package/lib/typescript/src/config.d.ts.map +1 -0
  63. package/lib/typescript/src/exit-codes.d.ts +16 -0
  64. package/lib/typescript/src/exit-codes.d.ts.map +1 -0
  65. package/lib/typescript/src/index.d.ts +16 -0
  66. package/lib/typescript/src/index.d.ts.map +1 -0
  67. package/lib/typescript/src/js-coverage.d.ts +36 -0
  68. package/lib/typescript/src/js-coverage.d.ts.map +1 -0
  69. package/lib/typescript/src/node.d.ts +14 -0
  70. package/lib/typescript/src/node.d.ts.map +1 -0
  71. package/lib/typescript/src/path-rewrite.d.ts +12 -0
  72. package/lib/typescript/src/path-rewrite.d.ts.map +1 -0
  73. package/lib/typescript/src/process-ios-native-coverage.d.ts +61 -0
  74. package/lib/typescript/src/process-ios-native-coverage.d.ts.map +1 -0
  75. package/lib/typescript/src/pull-native-coverage.d.ts +38 -0
  76. package/lib/typescript/src/pull-native-coverage.d.ts.map +1 -0
  77. package/package.json +184 -0
  78. package/plugin/build/index.d.ts +21 -0
  79. package/plugin/build/index.js +153 -0
  80. package/react-native-coverage.config.js.example +47 -0
  81. package/react-native.config.js +9 -0
  82. package/src/NativeCoverage.ts +16 -0
  83. package/src/assert-coverage.ts +260 -0
  84. package/src/cli/index.ts +420 -0
  85. package/src/config.ts +189 -0
  86. package/src/exit-codes.ts +20 -0
  87. package/src/index.tsx +35 -0
  88. package/src/js-coverage.ts +213 -0
  89. package/src/node.ts +52 -0
  90. package/src/path-rewrite.ts +52 -0
  91. package/src/process-ios-native-coverage.ts +425 -0
  92. package/src/pull-native-coverage.ts +269 -0
@@ -0,0 +1,35 @@
1
+ package com.coverage
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import java.util.HashMap
9
+
10
+ class CoveragePackage : BaseReactPackage() {
11
+ override fun getModule(
12
+ name: String,
13
+ reactContext: ReactApplicationContext,
14
+ ): NativeModule? =
15
+ if (name == CoverageModule.NAME) {
16
+ CoverageModule(reactContext)
17
+ } else {
18
+ null
19
+ }
20
+
21
+ override fun getReactModuleInfoProvider() =
22
+ ReactModuleInfoProvider {
23
+ mapOf(
24
+ CoverageModule.NAME to
25
+ ReactModuleInfo(
26
+ name = CoverageModule.NAME,
27
+ className = CoverageModule.NAME,
28
+ canOverrideExistingModule = false,
29
+ needsEagerInit = false,
30
+ isCxxModule = false,
31
+ isTurboModule = true,
32
+ ),
33
+ )
34
+ }
35
+ }
package/app.plugin.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./plugin/build');
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry. Requires `yarn prepare` (bob build) so lib/ exists.
4
+ */
5
+ import('../lib/module/cli/index.js').catch((error) => {
6
+ console.error(
7
+ '[rn-coverage] Failed to load CLI. Run `yarn prepare` first.\n',
8
+ error
9
+ );
10
+ process.exit(1);
11
+ });
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ # react-native-coverage CocoaPods helper (safe split).
4
+ #
5
+ # Expo / config plugin owns Android + stable iOS app-target mods.
6
+ # Pod LLVM coverage flags (and optional dynamic-framework restore) live here:
7
+ #
8
+ # require_relative '../node_modules/react-native-coverage/cocoapods/coverage_post_install'
9
+ # # After use_expo_modules! — wrap Installer so restore runs after Expo's staticlib downgrade:
10
+ # ReactNativeCoverage.install_installer_hooks!
11
+ #
12
+ # post_install do |installer|
13
+ # ReactNativeCoverage.apply_post_install!(
14
+ # installer,
15
+ # framework_name_prefixes: ['CoverageFixture'],
16
+ # force_dynamic_frameworks: true
17
+ # )
18
+ # end
19
+
20
+ require 'fileutils'
21
+
22
+ module ReactNativeCoverage
23
+ module_function
24
+
25
+ DEFAULT_FRAMEWORK_PREFIXES = ['CoverageFixture'].freeze
26
+ COVERAGE_POD_NAMES = %w[Coverage CoverageFixture].freeze
27
+
28
+ # Wrap Installer AFTER Expo's `use_expo_modules!` patch. Call from the Podfile
29
+ # below `use_expo_modules!` (Expo plugin injects the call).
30
+ # Using prepend-at-require races Expo's define_method and recurses forever.
31
+ def install_installer_hooks!(options = {})
32
+ if options.key?(:force_dynamic_frameworks)
33
+ self.force_dynamic_frameworks = options[:force_dynamic_frameworks]
34
+ end
35
+ return if @installer_hooks_installed
36
+
37
+ installer_class = Pod::Installer
38
+ original = installer_class.instance_method(:run_podfile_pre_install_hooks)
39
+
40
+ installer_class.define_method(:run_podfile_pre_install_hooks) do
41
+ original.bind_call(self)
42
+ ReactNativeCoverage.restore_dynamic_frameworks!(self)
43
+ end
44
+
45
+ @installer_hooks_installed = true
46
+ end
47
+
48
+ def restore_dynamic_frameworks!(installer, pod_names: nil)
49
+ return unless force_dynamic_frameworks_enabled?
50
+
51
+ names = Array(pod_names || dynamic_framework_pod_names)
52
+ restored = []
53
+
54
+ installer.pod_targets.each do |target|
55
+ next unless names.any? { |name| target.name == name || target.name.include?(name) }
56
+
57
+ target.define_singleton_method(:build_type) do
58
+ Pod::BuildType.dynamic_framework
59
+ end
60
+ restored << target.name
61
+ end
62
+
63
+ return if restored.empty?
64
+
65
+ Pod::UI.puts '[react-native-coverage] Restored dynamic frameworks for: ' \
66
+ "#{restored.uniq.join(', ')}".green
67
+ end
68
+
69
+ def force_dynamic_frameworks_enabled?
70
+ # Default false: Expo force-statics React-Core, so dynamic Coverage* pods
71
+ # fail CocoaPods transitive static→dynamic validation. Opt in on bare/RNFB.
72
+ @force_dynamic_frameworks == true
73
+ end
74
+
75
+ def force_dynamic_frameworks=(value)
76
+ @force_dynamic_frameworks = value
77
+ end
78
+
79
+ def dynamic_framework_pod_names
80
+ @dynamic_framework_pod_names || COVERAGE_POD_NAMES
81
+ end
82
+
83
+ def dynamic_framework_pod_names=(names)
84
+ @dynamic_framework_pod_names = Array(names)
85
+ end
86
+
87
+ def apply_ios_native_coverage!(build_settings, link_profile:)
88
+ build_settings['CLANG_ENABLE_CODE_COVERAGE'] = 'YES'
89
+ build_settings['OTHER_CFLAGS'] =
90
+ '$(inherited) -fprofile-instr-generate -fcoverage-mapping'
91
+ build_settings['OTHER_SWIFT_FLAGS'] =
92
+ '$(inherited) -profile-generate -profile-coverage-mapping'
93
+ return unless link_profile
94
+
95
+ # -Wl,-u keeps __llvm_profile_set_filename from being dead-stripped so
96
+ # per-image flush can retarget coverage-%m.profraw under dynamic frameworks.
97
+ build_settings['OTHER_LDFLAGS'] = [
98
+ '$(inherited)',
99
+ '-fprofile-instr-generate',
100
+ '-Wl,-u,___llvm_profile_set_filename',
101
+ '-L$(DT_TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)',
102
+ '-L$(SDKROOT)/usr/lib/swift',
103
+ ].join(' ')
104
+ end
105
+
106
+ def write_coverage_config_header!(prefixes:)
107
+ header_path = File.expand_path('../ios/CoverageConfig.h', __dir__)
108
+ list = Array(prefixes)
109
+ list = DEFAULT_FRAMEWORK_PREFIXES if list.empty?
110
+
111
+ entries = list.map { |prefix| " \"#{prefix}\"" }.join(",\n")
112
+ contents = <<~HEADER
113
+ /**
114
+ * Generated by react-native-coverage CocoaPods helper.
115
+ * Do not edit by hand — regenerate via apply_post_install! /
116
+ * Expo config plugin `frameworkNamePrefixes`.
117
+ */
118
+ #pragma once
119
+
120
+ #ifndef COVERAGE_ENABLED
121
+ #define COVERAGE_ENABLED 1
122
+ #endif
123
+
124
+ #ifndef COVERAGE_PROFILE_FILE_PATTERN
125
+ #define COVERAGE_PROFILE_FILE_PATTERN "coverage-%m.profraw"
126
+ #endif
127
+
128
+ #ifndef COVERAGE_FRAMEWORK_PREFIX_COUNT
129
+ #define COVERAGE_FRAMEWORK_PREFIX_COUNT #{list.length}
130
+ static const char *const COVERAGE_FRAMEWORK_PREFIXES[] = {
131
+ #{entries}
132
+ };
133
+ #endif
134
+ HEADER
135
+
136
+ FileUtils.mkdir_p(File.dirname(header_path))
137
+ File.write(header_path, contents)
138
+ Pod::UI.puts "[react-native-coverage] Wrote #{header_path} " \
139
+ "(prefixes=#{list.inspect})".green
140
+ end
141
+
142
+ def apply_post_install!(installer, options = {})
143
+ prefixes = Array(options.fetch(:framework_name_prefixes, DEFAULT_FRAMEWORK_PREFIXES))
144
+ link_profile = options.fetch(:link_profile, true)
145
+ instrument_app = options.fetch(:instrument_app, true)
146
+ quiet = options.fetch(:quiet, false)
147
+ write_header = options.fetch(:write_config_header, true)
148
+ force_dynamic = options.fetch(:force_dynamic_frameworks, false)
149
+
150
+ self.force_dynamic_frameworks = force_dynamic
151
+ self.dynamic_framework_pod_names =
152
+ options.fetch(:dynamic_framework_pod_names, COVERAGE_POD_NAMES + prefixes)
153
+
154
+ # Belt-and-suspenders: re-apply if Expo/other hooks ran after our prepend.
155
+ restore_dynamic_frameworks!(installer) if force_dynamic
156
+
157
+ write_coverage_config_header!(prefixes: prefixes) if write_header
158
+
159
+ if instrument_app
160
+ installer.aggregate_targets.each do |aggregate_target|
161
+ user_project = aggregate_target.user_project
162
+ next if user_project.nil?
163
+
164
+ user_project.native_targets.each do |target|
165
+ target.build_configurations.each do |config|
166
+ apply_ios_native_coverage!(config.build_settings, link_profile: true)
167
+ end
168
+ end
169
+ user_project.save
170
+ end
171
+ end
172
+
173
+ installer.pods_project.targets.each do |target|
174
+ matched = prefixes.any? { |prefix| target.name.include?(prefix) }
175
+ # Always link profile into the Coverage flusher Pod under dynamic linkage
176
+ # so configure/set_filename and the final pod write resolve.
177
+ is_coverage_pod = target.name == 'Coverage'
178
+ next unless matched || is_coverage_pod
179
+
180
+ target.build_configurations.each do |config|
181
+ apply_ios_native_coverage!(
182
+ config.build_settings,
183
+ link_profile: link_profile || is_coverage_pod
184
+ )
185
+ end
186
+ end
187
+
188
+ unless quiet
189
+ Pod::UI.puts '[react-native-coverage] applied LLVM coverage flags ' \
190
+ "(prefixes=#{prefixes.inspect}, force_dynamic=#{force_dynamic})"
191
+ end
192
+ end
193
+ end
194
+
195
+ # Do not auto-install hooks on require — Expo patches Installer during
196
+ # `use_expo_modules!`. Call `ReactNativeCoverage.install_installer_hooks!`
197
+ # from the Podfile after that (Expo config plugin injects the call).
package/docs/cli.md ADDED
@@ -0,0 +1,51 @@
1
+ # CLI
2
+
3
+ Binary: `rn-coverage`
4
+
5
+ ```
6
+ rn-coverage --help
7
+ rn-coverage [--strict|--no-strict] [-c <config>]
8
+
9
+ rn-coverage android pull [--device <serial>] [--output <dir>] [--retries <n>]
10
+ rn-coverage android report [--android-dir <path>] [--jacoco-xml <path>]
11
+
12
+ rn-coverage ios pull --device <udid> [--output <dir>]
13
+ rn-coverage ios export --derived-data <path> [--configuration Debug] [--app-name <name>] [--output <path>]
14
+ rn-coverage ios report --derived-data <path> [--profdata <path>] [--output-dir <path>]
15
+ rn-coverage ios summary --derived-data <path> [--profdata <path>]
16
+
17
+ rn-coverage assert [--platform ios|android|all] [--lcov <path>] [--jacoco-xml <path>]
18
+
19
+ rn-coverage js pull --platform android|ios [--device <id>] [--output <dir>]
20
+ rn-coverage js report --input <coverage-final.json> [--output <dir>] [--cwd <path>] [--nyc-config <path>]
21
+ ```
22
+
23
+ ## Exit codes
24
+
25
+ | Code | Meaning |
26
+ |------|---------|
27
+ | **0** | Success (or soft-mode empty artifact with warning) |
28
+ | **1** | Unexpected error / bad invocation / tooling failure |
29
+ | **2** | Strict empty-hit / missing artifact (CI presence guard) |
30
+
31
+ ### Strict / assert contract
32
+
33
+ - Config default: `strict: true` (recommended for CI).
34
+ - Global flags: `--strict` / `--no-strict` override config for the process.
35
+ - Soft local (`--no-strict` or `strict: false`): missing/empty expected hits **warn and exit 0**.
36
+ - Strict CI: the same conditions **exit 2** so a sabotaged or silent pipeline fails the job.
37
+
38
+ Commands that enforce the guard:
39
+
40
+ | Command | Exit 2 when (strict) |
41
+ |---------|----------------------|
42
+ | `android pull` | No `.ec` after retries |
43
+ | `android report` | Gradle ok but Jacoco XML missing/empty/no matched LINE hits |
44
+ | `ios pull` | No `.profraw` in the simulator container |
45
+ | `ios export` | No `.profraw`, or LCOV has no expected path hits with `LH` > 0 (default: `packages/`) |
46
+ | `ios report` / `ios summary` | Missing `profdata` (run `ios export` first) |
47
+ | `assert` | Dedicated post-pipeline check for LCOV and/or Jacoco XML |
48
+
49
+ `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.
50
+
51
+ Matchers and default artifact paths live under `assert.*` in config (see [config.md](./config.md)).
package/docs/config.md ADDED
@@ -0,0 +1,27 @@
1
+ # Config
2
+
3
+ Copy [`react-native-coverage.config.js.example`](../react-native-coverage.config.js.example) to `react-native-coverage.config.js` in your dedicated test app.
4
+
5
+ Key fields:
6
+
7
+ | Key | Purpose |
8
+ |-----|---------|
9
+ | `nativeModuleName` | TurboModule name (default `Coverage`) |
10
+ | `app.androidApplicationId` | `run-as` package for adb pull |
11
+ | `app.iosBundleId` | simctl container lookup |
12
+ | `app.iosProductName` | App binary / `.app` name |
13
+ | `ios.frameworkNamePrefixes` | Extra llvm-cov `-object` frameworks |
14
+ | `android.coverageRelativePath` | On-device `.ec` path under app files |
15
+ | `android.libraryProjectMatchers` | Fallback Jacoco package substrings for assert |
16
+ | `android.jacocoReportXml` | Default Jacoco XML path after `android report` |
17
+ | `js.androidRelativePath` | On-device Istanbul JSON under `run-as` (default `files/coverage-final.json`) |
18
+ | `js.androidStagingPath` | adb staging path for JS JSON pull |
19
+ | `js.iosRelativePath` | Path under sim data container (default `Documents/coverage-final.json`) |
20
+ | `sourcePathRewrite` | LCOV `SF:` path normalization rules |
21
+ | `strict` | Exit **2** on empty artifacts when true (CI default) |
22
+ | `assert.lcovPathIncludes` | Substrings required in ≥1 LCOV `SF:` (default `packages/`) |
23
+ | `assert.jacocoPackageIncludes` | Jacoco package name substrings that must have LINE covered (`.` or `/`) |
24
+ | `assert.defaultLcovPath` | Default `--lcov` for `rn-coverage assert` |
25
+ | `assert.defaultJacocoXmlPath` | Default `--jacoco-xml` for `rn-coverage assert` |
26
+
27
+ Defaults contain **no** product-specific names.
package/docs/index.md ADDED
@@ -0,0 +1,16 @@
1
+ # react-native-coverage
2
+
3
+ Coverage tooling for **dedicated React Native test apps** (Pattern C): native TurboModule flush (LLVM / Jacoco), Istanbul/NYC TypeScript coverage with source-map remap, pull/export CLI, Expo config plugin, and docs.
4
+
5
+ This package is **New Architecture / TurboModule only**. It does not depend on `expo-modules-core`.
6
+
7
+ ## Quick links
8
+
9
+ - [Pattern C](pattern-c.md) — dedicated test apps only
10
+ - [Android integration](integration/android.md) — Gradle helpers + Expo plugin
11
+ - [iOS integration](integration/ios.md) — Ruby helper + dynamic frameworks
12
+ - [JS / TypeScript](integration/js.md) — Istanbul + NYC remap + Codecov
13
+ - [E2E timing](integration/e2e-timing.md)
14
+ - [CI Appium notes](integration/ci-appium.md) — GHA pitfalls (simulator, adb reverse, Jacoco paths)
15
+ - [CLI](cli.md)
16
+ - [Config](config.md)
@@ -0,0 +1,83 @@
1
+ # Android integration
2
+
3
+ Enable Android test coverage in your dedicated test app (Pattern C) and wire Jacoco reporting.
4
+
5
+ ## Expo (recommended)
6
+
7
+ Add the config plugin — it applies both Gradle helpers and enables debug `testCoverageEnabled`:
8
+
9
+ ```json
10
+ {
11
+ "expo": {
12
+ "plugins": [
13
+ [
14
+ "react-native-coverage",
15
+ {
16
+ "libraryProjectMatchers": ["coverage-fixture", "my-native-lib"],
17
+ "enableAndroidCoverage": true
18
+ }
19
+ ]
20
+ ]
21
+ }
22
+ }
23
+ ```
24
+
25
+ Then:
26
+
27
+ ```sh
28
+ npx expo prebuild
29
+ ```
30
+
31
+ Inspect `android/build.gradle` (root instrumentation) and `android/app/build.gradle` (Jacoco report + `testCoverageEnabled`).
32
+
33
+ ## Bare React Native / manual Gradle
34
+
35
+ In the **root** `android/build.gradle`:
36
+
37
+ ```gradle
38
+ ext.coverageLibraryProjectMatchers = ['my-native-lib']
39
+ def rnCoverageRoot = new File(
40
+ ["node", "--print", "require.resolve('react-native-coverage/package.json')"]
41
+ .execute(null, rootDir).text.trim()
42
+ ).parentFile
43
+ apply from: new File(rnCoverageRoot, "android/rn-coverage.gradle")
44
+ ```
45
+
46
+ In **app** `android/app/build.gradle`:
47
+
48
+ ```gradle
49
+ android {
50
+ buildTypes {
51
+ debug {
52
+ testCoverageEnabled true
53
+ }
54
+ }
55
+ }
56
+
57
+ def rnCoverageRoot = new File(
58
+ ["node", "--print", "require.resolve('react-native-coverage/package.json')"]
59
+ .execute(null, rootDir).text.trim()
60
+ ).parentFile
61
+ apply from: new File(rnCoverageRoot, "android/rn-coverage-jacoco.gradle")
62
+ ```
63
+
64
+ ## Runtime + CLI
65
+
66
+ 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).
67
+ 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.
68
+ 3. `rn-coverage android report` (`jacocoTestReport`).
69
+ 4. `rn-coverage assert` (strict empty → exit 2).
70
+ 5. Optional JS: `rn-coverage js pull` / `js report` — see [JS / TypeScript](js.md).
71
+
72
+ Jacoco package names in XML use slashes (`com/foo`); assert matchers may use dots (`com.foo`) — both work.
73
+
74
+ CI pitfalls (emulator Metro, `byTestId`, etc.): [CI Appium notes](ci-appium.md).
75
+
76
+ Shipped helpers:
77
+
78
+ | File | Role |
79
+ |------|------|
80
+ | `android/rn-coverage.gradle` | Enable Jacoco + `enableAndroidTestCoverage` on matched library projects |
81
+ | `android/rn-coverage-jacoco.gradle` | `jacocoTestReport` / unit / e2e-only report tasks |
82
+
83
+ Pod LLVM flags are **not** applied by regex-editing the Podfile; use the Ruby helper on iOS (see [iOS integration](ios.md)).
@@ -0,0 +1,41 @@
1
+ # CI Appium notes (learned the hard way)
2
+
3
+ Practical pitfalls from getting GitHub Actions green for this package’s Appium cells. Consumer CI should copy the same patterns.
4
+
5
+ ## Matrix
6
+
7
+ | Cell | Harness | What it proves |
8
+ |------|---------|----------------|
9
+ | `e2e:ios:dynamic` | `example-dynamic/` (bare RN, `USE_FRAMEWORKS=dynamic`) | Primary LLVM LCOV with a real dynamic `CoverageFixture.framework` |
10
+ | `e2e:ios:static` | `example/` (Expo prebuild, static merge) | Honest static cell still asserts fixture hits |
11
+ | `e2e:android` | `example/` (Expo + emulator) | Emma `.ec` → Jacoco → assert |
12
+
13
+ Scripts: `scripts/ci/`. Specs: `e2e/`.
14
+
15
+ ## iOS / Xcode / simulator
16
+
17
+ - Prefer **`macos-26`** + **`maxim-lobanov/setup-xcode`** with `latest-stable` (Expo SDK 57 wants Xcode 26.4+).
18
+ - Default simulator name: **`iPhone 17`**. Xcode 26.6 images do not ship a plain `iPhone 16`; wrong names fail destination lookup.
19
+ - 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.
20
+ - Give Appium room: `simulatorStartupTimeout` ≥ 300s, long WDA timeouts (helpers in `e2e/helpers.js`).
21
+ - List sims once before the run (`xcrun simctl list`) — same deflake idea as RNFB.
22
+
23
+ ## Android / Metro / Appium
24
+
25
+ - 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.
26
+ - 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.
27
+ - Allow a long `appWaitDuration` for first Metro bundle.
28
+
29
+ ## Android coverage pull → Jacoco
30
+
31
+ 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).
32
+ 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.
33
+ 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.
34
+
35
+ ## Action pins
36
+
37
+ Pin third-party Actions by **full commit SHA** (and comment the release tag). Typo’d SHAs fail the job before any app code runs.
38
+
39
+ ## Artifacts
40
+
41
+ Upload `artifacts/e2e/coverage/**` and `artifacts/e2e/logs/**` with `if: always()` so failed pulls/asserts remain diagnosable.
@@ -0,0 +1,20 @@
1
+ # E2E timing (document only)
2
+
3
+ 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.
4
+
5
+ Typical sequence:
6
+
7
+ 1. Run instrumented e2e against the dedicated test app (`RN_COVERAGE_JS=1` for Istanbul).
8
+ 2. Invoke `Coverage.flush()` (TurboModule) once at suite teardown — native buffers **and** `global.__coverage__` when present.
9
+ 3. Host-side: `rn-coverage android pull` / `rn-coverage ios pull`.
10
+ 4. `rn-coverage android report` or `rn-coverage ios export` (+ optional `ios report` / `ios summary`).
11
+ 5. Optional: `rn-coverage js pull` + `rn-coverage js report` (NYC source-map remap).
12
+ 6. Optional: `rn-coverage assert` (exit 2 on empty artifacts when `strict: true`).
13
+
14
+ The example app documents Appium as the intended e2e runner. CI wiring:
15
+
16
+ - `yarn e2e:ios:dynamic` — primary bare-RN dynamic frameworks cell
17
+ - `yarn e2e:ios:static` — Expo static cell
18
+ - `yarn e2e:android` — Expo Android Jacoco + JS cell
19
+
20
+ Scripts live under `scripts/ci/`; WDIO specs under `e2e/`. See also [CI Appium notes](ci-appium.md) and [JS / TypeScript](js.md).
@@ -0,0 +1,77 @@
1
+ # iOS integration
2
+
3
+ - New Architecture only; TurboModule autolinks via the podspec.
4
+ - Flusher packaging (dynamic frameworks): **mode (c)** — Pod LINKEDIT for
5
+ configured `frameworkNamePrefixes` **and** the main executable (see spike verdict).
6
+
7
+ ## Expo (recommended)
8
+
9
+ ```json
10
+ {
11
+ "expo": {
12
+ "plugins": [
13
+ [
14
+ "react-native-coverage",
15
+ {
16
+ "frameworkNamePrefixes": ["MyLib"],
17
+ "forceDynamicFrameworks": false
18
+ }
19
+ ]
20
+ ]
21
+ }
22
+ }
23
+ ```
24
+
25
+ Keep `forceDynamicFrameworks` **false** under Expo (React-Core is force-static). Set **true** only on bare RN / RNFB hosts that build React as dynamic frameworks.
26
+
27
+ ## CI cells (honest matrix)
28
+
29
+ | Cell | App | Linkage | Gate |
30
+ |------|-----|---------|------|
31
+ | **iOS dynamic (primary)** | `example-dynamic/` bare RN | `use_frameworks! :linkage => :dynamic` | Non-zero fixture LCOV **and** `CoverageFixture.framework` is a dylib |
32
+ | **iOS static** | `example/` Expo | staticlib merge into app / `.debug.dylib` | Non-zero fixture LCOV |
33
+ | **Android** | `example/` Expo | Jacoco-instrumented libraries | Non-zero fixture package LINE hits |
34
+
35
+ Appium (WebDriverIO) drives the harness; see `e2e/` and `scripts/ci/run-ios-e2e-cell.sh`.
36
+
37
+ 1. Sets `ios.useFrameworks=dynamic` in Podfile properties (needed for multi-image LINKEDIT on dynamic-React hosts).
38
+ 2. Requires the shipped Ruby helper and calls `apply_post_install!` once (safe split — no Podfile regex for LLVM flags).
39
+ 3. Optionally restores dynamic frameworks for `Coverage` + matched fixture pods
40
+ (`forceDynamicFrameworks: true`) — **only when React is also dynamic**
41
+ (bare RN / RNFB). Leave `false` under Expo (React-Core is force-static);
42
+ CocoaPods rejects dynamic Coverage* pods that transitively depend on static
43
+ React-Core. Under Expo, Coverage* remain static libraries merged into the app.
44
+
45
+ ```sh
46
+ npx expo prebuild
47
+ cd ios && pod install
48
+ ```
49
+
50
+ ## Bare React Native / manual Podfile
51
+
52
+ ```ruby
53
+ require_relative '../node_modules/react-native-coverage/cocoapods/coverage_post_install'
54
+
55
+ # After `use_expo_modules!` (wraps Installer so restore runs after Expo's staticlib downgrade):
56
+ ReactNativeCoverage.install_installer_hooks!
57
+
58
+ post_install do |installer|
59
+ ReactNativeCoverage.apply_post_install!(
60
+ installer,
61
+ framework_name_prefixes: ['MyLib'],
62
+ force_dynamic_frameworks: true
63
+ )
64
+ end
65
+ ```
66
+
67
+ The helper:
68
+
69
+ - Applies LLVM `-fprofile-instr-generate` / `-fcoverage-mapping` (+ link flags) to the app target and matching pods.
70
+ - Regenerates `ios/CoverageConfig.h` from `framework_name_prefixes`.
71
+ - Restores `Pod::BuildType.dynamic_framework` for Coverage / fixture pods when Expo forced `static_library`.
72
+
73
+ ## Runtime + CLI
74
+
75
+ Call `flush()` before `rn-coverage ios pull`, then `rn-coverage ios export` for LCOV.
76
+
77
+ The Expo config plugin wires the Podfile helper call; it does **not** replace the Ruby helper for Pod LLVM flags (safe split).
@@ -0,0 +1,38 @@
1
+ # JavaScript / TypeScript coverage
2
+
3
+ Complete the React Native coverage story alongside native LLVM/Jacoco:
4
+
5
+ | Layer | Tooling | Artifact |
6
+ |-------|---------|----------|
7
+ | Unit (package) | Jest `--coverage` | `coverage/unit/lcov.info` |
8
+ | E2e JS/TS | `babel-plugin-istanbul` + NYC source-map remap | `coverage/js/lcov.info` |
9
+ | Native iOS | llvm-cov | `lcov.info` |
10
+ | Native Android | Jacoco | `jacocoTestReport.xml` |
11
+
12
+ ## Instrument Metro (e2e)
13
+
14
+ 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.
15
+
16
+ `flush()` dumps `global.__coverage__` via TurboModule `dumpJsCoverage` **before** the native Emma/LLVM flush:
17
+
18
+ - Android → `filesDir/coverage-final.json` (`files/coverage-final.json` under `run-as`)
19
+ - iOS → `Documents/coverage-final.json`
20
+
21
+ ## CLI
22
+
23
+ ```sh
24
+ rn-coverage js pull --platform android --output coverage/js
25
+ rn-coverage js pull --platform ios --device <udid> --output coverage/js
26
+
27
+ rn-coverage js report \
28
+ --input coverage/js/coverage-final.json \
29
+ --output coverage/js \
30
+ --cwd example \
31
+ --nyc-config example/nyc.config.js
32
+ ```
33
+
34
+ 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`).
35
+
36
+ ## Codecov
37
+
38
+ CI uploads unit LCOV, e2e native LCOV/Jacoco, and e2e JS LCOV with distinct `flags` (`unit-js`, `e2e-ios-dynamic`, …). Wire `CODECOV_TOKEN` (or Codecov GitHub app OIDC) on the repo for private uploads; public repos may work tokenless depending on Codecov settings.
@@ -0,0 +1,5 @@
1
+ # Pattern C — dedicated test apps only
2
+
3
+ `react-native-coverage` is intended for **dedicated test / e2e harness apps** (for example a monorepo `tests/` app), not production product `package.json` trees.
4
+
5
+ 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.