Skip to content

DO NOT MERGE: native iOS coverage setup (local harness fork)#247

Draft
mfazekas wants to merge 110 commits into
feat/rive-ios-experimentalfrom
setup/native-ios-coverage
Draft

DO NOT MERGE: native iOS coverage setup (local harness fork)#247
mfazekas wants to merge 110 commits into
feat/rive-ios-experimentalfrom
setup/native-ios-coverage

Conversation

@mfazekas
Copy link
Copy Markdown
Collaborator

@mfazekas mfazekas commented May 8, 2026

Do not merge — this PR contains local file: and portal: references to the react-native-harness fork.

What this does

Sets up native iOS code coverage collection using @react-native-harness/coverage-ios from the mfazekas/react-native-harness fork (feat/native-ios-coverage branch).

How to use

Prerequisites: clone the harness fork at /Users/boga/Work/Margelo/react-native-harness and check out feat/native-ios-coverage.

1. Fix harness fork (one-time setup)

The packages/cli package is used as the portal target for react-native-harness, but it's missing three things that Jest/Metro/npx need. Apply these fixes in the harness fork:

a) Add jest-preset.cjs — Jest requires react-native-harness/jest-preset at the package root:

cat > packages/cli/jest-preset.cjs << 'SHIM'
module.exports = {
    runner: '@react-native-harness/jest',
};
SHIM

b) Add metro.cjs — Metro config imports react-native-harness/metro. Uses realpathSync to handle --preserve-symlinks:

cat > packages/cli/metro.cjs << 'SHIM'
const path = require('path');
const fs = require('fs');
const realDir = path.dirname(fs.realpathSync(__filename));
module.exports = require(path.join(realDir, '..', 'metro', 'dist', 'index.js'));
SHIM

c) Add bin.cjs — Without a bin entry, npx downloads a fresh copy from npm instead of using the local portal:

cat > packages/cli/bin.cjs << 'SHIM'
#!/usr/bin/env node
import('./dist/index.js');
SHIM
chmod +x packages/cli/bin.cjs

d) Update packages/cli/package.json — add the exports and bin fields:

{
  "bin": {
    "react-native-harness": "./bin.cjs",
    "harness": "./bin.cjs"
  },
  "exports": {
    ...existing exports...,
    "./jest-preset": "./jest-preset.cjs",
    "./metro": "./metro.cjs"
  }
}

e) Fix packages/coverage-ios/package.json — add missing fields that cause pod validation errors:

{
  "homepage": "https://github.com/callstackincubator/react-native-harness",
  "author": "React Native Harness contributors"
}

2. Build and run coverage

yarn install
cd example/ios && pod install  # should print [HarnessCoverage] Instrumenting pods
cd ..

# Build with Xcode 16.4 (not Xcode 26 beta)
DEVELOPER_DIR=/Applications/Xcode16.4.app/Contents/Developer xcodebuild build \
  -workspace ios/RiveExample.xcworkspace -scheme RiveExample \
  -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 16 Pro,OS=18.6" \
  -derivedDataPath ios/build

# Install on the correct simulator
xcrun simctl install "iPhone 16 Pro" ios/build/Build/Products/Debug-iphonesimulator/RiveExample.app

# Run tests with coverage
NODE_OPTIONS="--preserve-symlinks" npx react-native-harness --harnessRunner ios --coverage

3. Collect native coverage

The debug dylib path depends on where the app is installed. Find it with:

APP_PATH=$(xcrun simctl get_app_container "iPhone 16 Pro" rive.example)

Then merge and export:

xcrun llvm-profdata merge -sparse /tmp/harness-coverage/*.profraw -o coverage/native-ios.profdata
xcrun llvm-cov export --format=lcov \
  --instr-profile=coverage/native-ios.profdata \
  "$APP_PATH/RiveExample.debug.dylib" \
  > coverage/native-ios.lcov

# HTML report
genhtml coverage/native-ios.lcov -o coverage/native-ios-html \
  --ignore-errors inconsistent,corrupt,unsupported
open coverage/native-ios-html/index.html

Why these fixes are needed

The portal resolution "react-native-harness": "portal:.../packages/cli" maps the package to the CLI sub-package, but Jest expects react-native-harness/jest-preset, Metro expects react-native-harness/metro, and npx needs a bin entry — none of which exist in the CLI package. The correct package (packages/react-native-harness) can't be used as a portal target because yarn refuses to write into a different monorepo root. These shims bridge the gap.

Known issue

The resolve-coverage-pods.mjs script needs NODE_OPTIONS="--preserve-symlinks" when using portal resolutions. After yarn install, manually patch node_modules/@react-native-harness/coverage-ios/scripts/harness_coverage_hook.rb line 19 to add this. This will be fixed upstream.

mfazekas and others added 30 commits April 24, 2026 13:30
Add new experimental iOS backend (ios/new/) with synchronous API,
move legacy backend files to ios/legacy/, add getEnums() support,
retry listener streams on missingData, and restore TestComponentOverlay.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
getEnums() in legacy now throws directing users to the experimental
backend instead of creating throwaway Worker+File instances.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
… binding

Each Worker has its own C++ command server with its own m_artboards handle map.
Creating separate Workers per file meant artboard handles from one file were
invalid on another file's server. Using a shared singleton Worker fixes cross-file
artboard property set. Also wires fit/alignment through experimental Fit enum and
improves asset type detection with audio/font magic bytes.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Happy <yesreply@happy.engineering>
Passing fit to the Rive() constructor breaks layout mode because
the MTKView drawable isn't ready yet. Set rive.fit after
setupRiveUIView() instead.
Port Flutter data binding tests for VM access, enums, creation
variants, list properties, artboard and image properties. Includes
new .riv test assets and react-native-harness upgrade to alpha.25.
…ceByIndex

Prevents fatal crash when passing negative numbers to Swift APIs
expecting unsigned integers.
CocoaPods doesn't embed SPM-resolved dynamic frameworks automatically.
Patches the embed script to include RiveRuntime when USE_RIVE_SPM=1.
Move 19 backend-specific files to src/legacy/java/, add Gradle sourceSets
switching via USE_RIVE_NEW_API property, prepare empty experimental dirs.
Uses the new handle-based Rive Android SDK (app.rive.*) with CommandQueue,
path-based ViewModelInstance property access, and Kotlin Flows for reactivity.
Throws UnsupportedOperationException for SMI inputs, text runs, and events.
Also fixes Gradle property resolution to use rootProject.findProperty.
Custom TextureView renderer with Choreographer render loop, CommandQueue
polling infrastructure, ViewModel resolution and property validation,
and example Compose test activities for manual testing.
Also fix viewModelByIndex/ByName on Android legacy to catch SDK
exceptions instead of letting them propagate as JniExceptions.
…fferences

Fix colorProperty setter Double→Int overflow on Android legacy
(toLong().toInt()). Make enum and replaceViewModel tests tolerant
of Android SDK behavioral differences.
Use .viewModelDefault(from: .name(vmName)) for default instance
creation instead of artboard-based lookup that only worked for
the default artboard's VM. Skip instanceName, color get/set, and
non-existent property checks on experimental iOS where the SDK
doesn't support them.
…anceName

Add async variants for viewModelByName, defaultArtboardViewModel, and
ViewModel create methods. Pass instanceName through at creation time on
both iOS and Android experimental backends as a workaround for the SDK
not exposing ViewModelInstance.name.
…rimental iOS

Color.argbValue is now public in rive-ios 6.15.2 — implement getValue via
blockingAsync, addListener via valueStream, and fix setter crash by using
UInt32(bitPattern:) for negative ARGB values from JS.
- Always use SPM for RiveRuntime (remove CocoaPods fallback)
- Add RiveSPMEmbedFix module to auto-embed RiveRuntime.framework
- Pin SPM to exactVersion instead of upToNextMajorVersion
- Replace USE_RIVE_SPM with USE_RIVE_EXPERIMENTAL_RUNTIME to select
  legacy vs experimental backend independently of SPM
…erimental iOS test issues

- Rename USE_RIVE_EXPERIMENTAL_RUNTIME to USE_RIVE_NEW_API (matches Android)
- Remove USE_RIVE_SPM and all SPM embedding hacks from podspec/Podfile
- Use standard CocoaPods dependency for RiveRuntime
- Emit initial value in experimental addListener (number/string/bool/enum/color)
- Guard tests that crash on experimental iOS (list ops, autoPlay, artboard/image loading)
- Handle createInstanceByName throwing on experimental backend
mfazekas added 29 commits April 24, 2026 13:30
…allable methods

Nitro calls hybrid methods on the JS thread, not the main thread.
MainActor.assumeIsolated crashes at runtime when not on main.
…nd (#214)

Port of #209 fix to the experimental backend. The legacy backend
defaulted `layoutScaleFactor` to `resources.displayMetrics.density` when
unset, but the experimental backend defaulted to `1f`, causing the
artboard to render at pixel dimensions instead of dp.
…nings (#201)

## Summary
- Adds `RiveLog` — a general-purpose logging system that surfaces native
logs to JS via a configurable handler
- Default handler uses `console.error/warn/log` so logs show in the RN
console out of the box
- On both platforms, hooks into the Rive SDK's pluggable
`RiveLog.Logger` to unify C++ runtime logs (state machine, artboard,
command queue diagnostics) through the JS handler
- Adds `DeprecationWarning` utility that emits once-per-session warnings
when deprecated blocking methods are called

## Test plan
- [ ] Build exerciser on iOS + Android
- [ ] Call a deprecated method (e.g. `riveFile.viewModelByName(...)`) —
verify warning in RN console
- [ ] Call same method again — verify no duplicate warning
- [ ] `RiveLog.setHandler(() => {})` — verify all logs suppressed
- [ ] `RiveLog.resetHandler()` — verify back to default console output
- [ ] Verify `onError` prop on RiveView still works independently
… backend

RiveUIView creates its MTKView lazily during the first layout pass.
Setting rive.fit immediately after creating the view had no effect
because the drawable didn't exist yet. Store the desired fit as
pendingFit and apply it in layoutSubviews once the MTKView is present.
Add `RiveLog.setLogLevel(level)` to filter log output by severity.
Levels: `debug`, `info`, `warn`, `error`. Default is `warn`.

## Test plan
- `RiveLog.setLogLevel('error')` suppresses warn/info/debug logs
- `RiveLog.setLogLevel('debug')` shows all logs
Bump RiveRuntime from 6.19.0 to 6.19.1. Remove `@_spi(RiveExperimental)`
imports — no longer needed with this version.
The experimental factory already has `var backend: String {
"experimental" }`. This adds the matching property to the legacy side so
`RiveFileFactory.backend` works for both backends.
`Int32(value)` traps when ARGB color values exceed `Int32.max` (e.g.
`0xFF0000FF` = opaque blue). Uses `Int64` + `truncatingIfNeeded` for
safe conversion.
- Pass `fit:` directly in `Rive()` constructor instead of setting it
post-creation. Removes the `layoutSubviews`/`pendingFit` workaround
since rive-ios 6.19.1 handles this (rive-ios#443)
- Warn when `updateReferencedAssets` is called on experimental backend
(not supported — concurrency API can't update already-bound artboard
assets)
- Switch asset registration from parallel `TaskGroup` to sequential to
ensure command queue ordering
- Add debug logging to `ExperimentalAssetLoader`
- Add explicit `type` fields to OutOfBandAssets example
- Add `expo-font` plugin for `kanit_regular.ttf` in expo examples

## Test plan
- QuickStart + DataBindingArtboards render correctly with `Fit.Layout`
on first mount
- Out-of-Band Assets example loads initial assets correctly
- `updateReferencedAssets` logs a warning instead of silently failing
Add addInstanceAsync, addInstanceAtAsync, removeInstanceAsync,
removeInstanceAtAsync, swapAsync. Deprecate sync versions that
return before knowing if the operation succeeded.
The library module already depends on the correct version via
package.json runtimeVersions. The pin caused version conflicts.
… app

Debug activities that import app.rive.* directly, causing the example
app to need an explicit rive-android dependency. Easy to recreate
when needed for native SDK debugging.
Removes the workaround that stripped the `RiveRuntime.Swift` submodule
from modulemaps to avoid ODR conflicts. No longer needed since the
upstream XCFramework switched from C++/ObjC++ interop to C/ObjC interop
— the generated Swift header no longer emits `swift::` namespace types.

Tested building with both Xcode 16.4 and Xcode 26.2 with the unstripped
modulemap — no ODR errors.

**Note:** This PR depends on a RiveRuntime XCFramework built with C/ObjC
interop. If the build fails with ODR errors like
`'swift::Optional::init' has different definitions in different
modules`, the workaround still needs to be re-added (revert this PR).
You can verify by checking `grep -c 'namespace swift'
RiveRuntime-Swift.h` — it should be 0.
- Rename `ExperimentalViewConfiguration` → `ViewConfiguration`,
`ExperimentalBindData` → `BindData`, `ExperimentalAssetLoader` →
`AssetLoader`
- Rename `toExperimentalFit` → `toRiveFit`, `toExperimentalAlignment` →
`toRiveAlignment`
- Remove `RCTLog` debug lines from `RiveReactNativeView` and
`HybridRiveFileFactory`
- Remove `RCTLogInfo` from `AssetLoader` — error/warn logs are
sufficient
- Update "Experimental API" comments to "concurrency API"
Navigating away from a Rive screen produced:
```
Draw failed: Artboard instance is null
Failed to swap EGL buffers: EGL_BAD_SURFACE
```

The command queue processes commands in order: `close(artboard)` then
`draw(artboard)`. The draw finds the artboard already closed.

Fix: don't call `.close()` in `dispose()` — just null the handles and
stop the render loop. The command queue drains naturally and resources
are cleaned up by GC.

## Test plan
- Open any Rive example → navigate back → no errors in logcat
…race (#244)

- **OOB assets not showing**: `registerAssets` launched fire-and-forget
coroutines — file was loaded before assets finished registering. Now
`suspend` with `awaitAll()`.
- **updateReferencedAssets**: logs warning (same as iOS — not supported
in experimental backend).
- **NestedViewModel example**: fixed layout so Rive view doesn't get
squeezed to zero when log entries appear.

## Test plan
- Out-of-Band Assets: image, font, audio all load on Android
- NestedViewModel: Rive view stays visible after pressing Replace
Add coverage-ios package, configure RNRive + RiveRuntime pod
instrumentation, and document the integration process.
Remove stale `isExperimentalIOS` test skips and fix Android ViewModel
name resolution.

- **Test skips removed:** Color property (`argbValue` now public in
rive-ios 6.19.2), artboard/list/image databinding (crashes fixed in
experimental renderer)
- **Android fix:** Use `getDefaultViewModelInfo()` (available since
rive-android 11.3.2) to resolve ViewModel name for
`defaultArtboardViewModel()`, enabling `createInstanceByName` and other
name-dependent operations

All 105 harness tests pass on both iOS experimental and Android.
The rive-android API is no longer experimental in 11.4.1. Matches the
iOS rename done in #240.
Font file needed by the font fallback example. Referenced in
app.config.js via expo-font plugin.
Bump RiveRuntime from 6.19.2 to 6.20.0. Fixes Swift/ObjC interop issue
in the released version.
Adds coverage-ios dependency and portal resolutions pointing to the local
react-native-harness fork (feat/native-ios-coverage branch).
@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch from 80b8072 to f299fe1 Compare May 19, 2026 06:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant