Skip to content

Commit d7ace51

Browse files
authored
feat: add in-app test runner (#106)
## Summary - Add in-app test runner using react-native-harness for assertions - Add Tests page to example app for running tests on device - Initial tests for `viewModel()` and `replaceViewModel()` from #96 ## Test plan - Run example app and navigate to "Tests" page - Tap "Run All Tests" to verify both tests pass <img height="800" alt="image" src="https://github.com/user-attachments/assets/630354c3-1295-49d7-b26b-c9e9d8af469d" />
1 parent af74b8e commit d7ace51

13 files changed

Lines changed: 1214 additions & 90 deletions

File tree

.github/workflows/ci.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,62 @@ jobs:
207207
TURBO_FORCE: ${{ steps.turbo-cache-ios.outputs.cache-hit != 'true' }}
208208
run: |
209209
yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}"
210+
211+
test-harness-ios:
212+
if: false # TODO: re-enable once harness timeout issues are resolved
213+
runs-on: macos-latest
214+
env:
215+
XCODE_VERSION: 16.4
216+
steps:
217+
- name: Checkout
218+
uses: actions/checkout@v4
219+
220+
- name: Setup
221+
uses: ./.github/actions/setup
222+
223+
- name: Use appropriate Xcode version
224+
uses: maxim-lobanov/setup-xcode@v1
225+
with:
226+
xcode-version: ${{ env.XCODE_VERSION }}
227+
228+
- name: Restore cocoapods
229+
id: cocoapods-cache
230+
uses: actions/cache/restore@v4
231+
with:
232+
path: |
233+
**/ios/Pods
234+
key: ${{ runner.os }}-cocoapods-${{ hashFiles('example/ios/Podfile.lock') }}
235+
restore-keys: |
236+
${{ runner.os }}-cocoapods-
237+
238+
- name: Install cocoapods
239+
if: steps.cocoapods-cache.outputs.cache-hit != 'true'
240+
run: |
241+
cd example
242+
bundle install
243+
bundle exec pod install --project-directory=ios
244+
245+
- name: Build iOS app
246+
working-directory: example/ios
247+
run: |
248+
set -o pipefail && xcodebuild \
249+
-derivedDataPath build \
250+
-workspace RiveExample.xcworkspace \
251+
-scheme RiveExample \
252+
-sdk iphonesimulator \
253+
-configuration Debug \
254+
build \
255+
CODE_SIGNING_ALLOWED=NO
256+
257+
- name: Boot iOS Simulator
258+
uses: futureware-tech/simulator-action@v4
259+
with:
260+
model: 'iPhone 16 Pro'
261+
os_version: '18.6'
262+
263+
- name: Install app on simulator
264+
run: xcrun simctl install booted example/ios/build/Build/Products/Debug-iphonesimulator/RiveExample.app
265+
266+
- name: Run harness tests on iOS
267+
working-directory: example
268+
run: yarn test:harness:ios --debug --verbose --testTimeout 120000

example/__tests__/rive.harness.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { describe, it, beforeAll } from 'react-native-harness';
2+
import { RiveFileFactory } from '@rive-app/react-native';
3+
import type { RiveFile } from '@rive-app/react-native';
4+
import {
5+
testViewModelBasicFunctionality,
6+
testReplaceViewModelSharesState,
7+
} from '../src/testing/suites/viewModelTests';
8+
9+
const ASSET_URL =
10+
'http://localhost:8081/assets/assets/rive/viewmodelproperty.riv';
11+
12+
describe('ViewModel', () => {
13+
let file: RiveFile;
14+
15+
beforeAll(async () => {
16+
file = await RiveFileFactory.fromURL(ASSET_URL, undefined);
17+
});
18+
19+
it('viewModel() basic functionality', () => {
20+
testViewModelBasicFunctionality(file);
21+
});
22+
23+
it('replaceViewModel() replaces and shares state', () => {
24+
testReplaceViewModelSharesState(file);
25+
});
26+
});

example/ios/Podfile.lock

Lines changed: 56 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1880,7 +1880,7 @@ PODS:
18801880
- ReactCommon/turbomodule/core
18811881
- RNWorklets
18821882
- Yoga
1883-
- RNRive (0.1.2):
1883+
- RNRive (0.1.3):
18841884
- DoubleConversion
18851885
- glog
18861886
- hermes-engine
@@ -2234,76 +2234,76 @@ SPEC CHECKSUMS:
22342234
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
22352235
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
22362236
hermes-engine: 314be5250afa5692b57b4dd1705959e1973a8ebe
2237-
NitroModules: 0af9a8516f3d8f101976d60e1f34e2a22f401600
2237+
NitroModules: 7f50ee216f8403e8eb243acfc504f3f856d6914c
22382238
RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809
22392239
RCTDeprecation: 83ffb90c23ee5cea353bd32008a7bca100908f8c
22402240
RCTRequired: eb7c0aba998009f47a540bec9e9d69a54f68136e
22412241
RCTTypeSafety: 659ae318c09de0477fd27bbc9e140071c7ea5c93
22422242
React: c2d3aa44c49bb34e4dfd49d3ee92da5ebacc1c1c
22432243
React-callinvoker: 1bdfb7549b5af266d85757193b5069f60659ef9d
2244-
React-Core: 10597593fdbae06f0089881e025a172e51d4a769
2245-
React-CoreModules: 6907b255529dd46895cf687daa67b24484a612c2
2246-
React-cxxreact: a9f5b8180d6955bc3f6a3fcd657c4d9b4d95c1f6
2244+
React-Core: 7150cf9b6a5af063b37003062689f1691e79c020
2245+
React-CoreModules: 15a85e6665d61678942da6ae485b351f4c699049
2246+
React-cxxreact: 74f9de59259ac951923f5726aa14f0398f167af9
22472247
React-debug: e74e76912b91e08d580c481c34881899ccf63da9
2248-
React-defaultsnativemodule: 11f6ee2cf69bf3af9d0f28a6253def33d21b5266
2249-
React-domnativemodule: f940bbc4fa9e134190acbf3a4a9f95621b5a8f51
2250-
React-Fabric: 6f5c357bf3a42ff11f8844ad3fc7a1eb04f4b9de
2251-
React-FabricComponents: 10e0c0209822ac9e69412913a8af1ca33573379b
2252-
React-FabricImage: f582e764072dfa4715ae8c42979a5bace9cbcc12
2248+
React-defaultsnativemodule: 628285212bbd65417d40ad6a9f8781830fda6c98
2249+
React-domnativemodule: 185d9808198405c176784aaf33403d713bd24fb7
2250+
React-Fabric: c814804affbe1952e16149ddd20256e1bccae67e
2251+
React-FabricComponents: 81ef47d596966121784afec9924f9562a29b1691
2252+
React-FabricImage: f14f371d678aa557101def954ac3ba27e48948ff
22532253
React-featureflags: d5facceff8f8f6de430e0acecf4979a9a0839ba9
2254-
React-featureflagsnativemodule: a7dd141f1ef4b7c1331af0035689fbc742a49ff4
2255-
React-graphics: 36ae3407172c1c77cea29265d2b12b90aaef6aa0
2256-
React-hermes: 9116d4e6d07abeb519a2852672de087f44da8f12
2257-
React-idlecallbacksnativemodule: ae7f5ffc6cf2d2058b007b78248e5b08172ad5c3
2258-
React-ImageManager: 9daee0dc99ad6a001d4b9e691fbf37107e2b7b54
2259-
React-jserrorhandler: 1e6211581071edaf4ecd5303147328120c73f4dc
2260-
React-jsi: 753ba30c902f3a41fa7f956aca8eea3317a44ee6
2261-
React-jsiexecutor: 47520714aa7d9589c51c0f3713dfbfca4895d4f9
2262-
React-jsinspector: cfd27107f6d6f1076a57d88c932401251560fe5f
2263-
React-jsinspectortracing: 76a7d791f3c0c09a0d2bf6f46dfb0e79a4fcc0ac
2264-
React-jsitooling: 995e826570dd58f802251490486ebd3244a037ab
2265-
React-jsitracing: 094ae3d8c123cea67b50211c945b7c0443d3e97b
2266-
React-logger: 8edfcedc100544791cd82692ca5a574240a16219
2267-
React-Mapbuffer: c3f4b608e4a59dd2f6a416ef4d47a14400194468
2268-
React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6
2269-
react-native-safe-area-context: 0b8555c40461feb7198e999912a3446602e7c601
2270-
React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e
2254+
React-featureflagsnativemodule: 96f0ab285382d95c90f663e02526a5ceefa95a11
2255+
React-graphics: 1a66ee0a3f093b125b853f6370296fadcaf6f233
2256+
React-hermes: 8b86e5f54a65ecb69cdf22b3a00a11562eda82d2
2257+
React-idlecallbacksnativemodule: 5c25ab145c602264d00cb26a397ab52e0efa031c
2258+
React-ImageManager: 15e34bd5ef1ac4a18e96660817ef70a7f99ee8c2
2259+
React-jserrorhandler: 02cdf2cd45350108be1ffd2b164578936dbbdff7
2260+
React-jsi: 6af1987cfbb1b6621664fdbf6c7b62bd4d38c923
2261+
React-jsiexecutor: 51f372998e0303585cb0317232b938d694663cbd
2262+
React-jsinspector: 3539ad976d073bfaa8a7d2fa9bef35e70e55033e
2263+
React-jsinspectortracing: e8dbacaf67c201f23052ca1c2bae2f7b84dec443
2264+
React-jsitooling: 95a34f41e3c249d42181de13b4f8d854f178ca9f
2265+
React-jsitracing: 25b029cf5cad488252d46da19dd8c4c134fd5fe4
2266+
React-logger: 368570a253f00879a1e4fea24ed4047e72e7bbf3
2267+
React-Mapbuffer: c04fcda1c6281fc0a6824c7dcc1633dd217ac1ec
2268+
React-microtasksnativemodule: ca2804a25fdcefffa0aa942aa23ab53b99614a34
2269+
react-native-safe-area-context: bc59472155ffb889a1ffe16c19a04c0cd451562b
2270+
React-NativeModulesApple: 452b86b29fae99ed0a4015dca3ad9cd222f88abf
22712271
React-oscompat: ef5df1c734f19b8003e149317d041b8ce1f7d29c
2272-
React-perflogger: 9a151e0b4c933c9205fd648c246506a83f31395d
2273-
React-performancetimeline: 5b0dfc0acba29ea0269ddb34cd6dd59d3b8a1c66
2272+
React-perflogger: 6fd2f6811533e9c19a61e855c3033eecbf4ad2a0
2273+
React-performancetimeline: abf31259d794c9274b3ea19c5016186925eec6c4
22742274
React-RCTActionSheet: a499b0d6d9793886b67ba3e16046a3fef2cdbbc3
2275-
React-RCTAnimation: cc64adc259aabc3354b73065e2231d796dfce576
2276-
React-RCTAppDelegate: 9d523da768f1c9e84c5f3b7e3624d097dfb0e16b
2277-
React-RCTBlob: e727f53eeefded7e6432eb76bd22b57bc880e5d1
2278-
React-RCTFabric: 58590aa4fdb4ad546c06a7449b486cf6844e991f
2279-
React-RCTFBReactNativeSpec: 9064c63d99e467a3893e328ba3612745c3c3a338
2280-
React-RCTImage: 7159cbdbb18a09d97ba1a611416eced75b3ccb29
2281-
React-RCTLinking: 46293afdb859bccc63e1d3dedc6901a3c04ef360
2282-
React-RCTNetwork: 4a6cd18f5bcd0363657789c64043123a896b1170
2283-
React-RCTRuntime: 5ab904fd749aa52f267ef771d265612582a17880
2284-
React-RCTSettings: 61e361dc85136d1cb0e148b7541993d2ee950ea7
2285-
React-RCTText: abd1e196c3167175e6baef18199c6d9d8ac54b4e
2286-
React-RCTVibration: 490e0dcb01a3fe4a0dfb7bc51ad5856d8b84f343
2275+
React-RCTAnimation: 2595dcb10a82216a511b54742f8c28d793852ac6
2276+
React-RCTAppDelegate: f03604b70f57c9469a84a159d8abecf793a5bcff
2277+
React-RCTBlob: e00f9b4e2f151938f4d9864cf33ebf24ac03328a
2278+
React-RCTFabric: 3945d116fd271598db262d4e6ed5691d431ed9e8
2279+
React-RCTFBReactNativeSpec: 0f4d4f0da938101f2ca9d5333a8f46e527ad2819
2280+
React-RCTImage: dac5e9f8ec476aefe6e60ee640ebc1dfaf1a4dbe
2281+
React-RCTLinking: 494b785a40d952a1dfbe712f43214376e5f0e408
2282+
React-RCTNetwork: b3d7c30cd21793e268db107dd0980cb61b3c1c44
2283+
React-RCTRuntime: a8ff419d437228e7b8a793b14f9d711e1cbb82af
2284+
React-RCTSettings: a060c7e381a3896104761b8eed7e284d95e37df3
2285+
React-RCTText: 4f272b72dbb61f390d8c8274528f9fdbff983806
2286+
React-RCTVibration: 0e5326220719aca12473d703aa46693e3b4ce67a
22872287
React-rendererconsistency: 351fdbc5c1fe4da24243d939094a80f0e149c7a1
2288-
React-renderercss: 3438814bee838ae7840a633ab085ac81699fd5cf
2289-
React-rendererdebug: 0ac2b9419ad6f88444f066d4b476180af311fb1e
2288+
React-renderercss: d333f2ada83969591100d91ec6b23ca2e17e1507
2289+
React-rendererdebug: 039e5949b72ba63c703de020701e3fd152434c61
22902290
React-rncore: 57ed480649bb678d8bdc386d20fee8bf2b0c307c
2291-
React-RuntimeApple: 8b7a9788f31548298ba1990620fe06b40de65ad7
2292-
React-RuntimeCore: e03d96fbd57ce69fd9bca8c925942194a5126dbc
2291+
React-RuntimeApple: 344a5e1105256000afabaa8df12c3e4cab880340
2292+
React-RuntimeCore: 0e48fb5e5160acc0334c7a723a42d42cef4b58b6
22932293
React-runtimeexecutor: d60846710facedd1edb70c08b738119b3ee2c6c2
2294-
React-RuntimeHermes: aab794755d9f6efd249b61f3af4417296904e3ba
2295-
React-runtimescheduler: c3cd124fa5db7c37f601ee49ca0d97019acd8788
2294+
React-RuntimeHermes: 064286a03871d932c99738e0f8ef854962ab4b99
2295+
React-runtimescheduler: e917ab17ae08c204af1ebf8f669b7e411b0220c8
22962296
React-timing: a90f4654cbda9c628614f9bee68967f1768bd6a5
2297-
React-utils: a612d50555b6f0f90c74b7d79954019ad47f5de6
2298-
ReactAppDependencyProvider: 04d5eb15eb46be6720e17a4a7fa92940a776e584
2299-
ReactCodegen: c63eda03ba1d94353fb97b031fc84f75a0d125ba
2300-
ReactCommon: 76d2dc87136d0a667678668b86f0fca0c16fdeb0
2297+
React-utils: 51c4e71608b8133fecc9a15801d244ae7bdf3758
2298+
ReactAppDependencyProvider: d5dcc564f129632276bd3184e60f053fcd574d6b
2299+
ReactCodegen: fda99a79c866370190e162083a35602fdc314e5d
2300+
ReactCommon: 4d0da92a5eb8da86c08e3ec34bd23ab439fb2461
23012301
RiveRuntime: ecde073222c6279c6f7f439132ac7f0a461b344b
2302-
RNCPicker: 28c076ae12a1056269ec0305fe35fac3086c477d
2303-
RNGestureHandler: 6b39f4e43e4b3a0fb86de9531d090ff205a011d5
2304-
RNReanimated: 66b68ebe3baf7ec9e716bd059d700726f250d344
2305-
RNRive: b056121a82044307a6f2030a9616f50a6cb6d9ec
2306-
RNWorklets: b1faafefb82d9f29c4018404a0fb33974b494a7b
2302+
RNCPicker: 83c74db2de8274d8a8f3e18d91dea174a708f8c4
2303+
RNGestureHandler: bff91bb5ab5688265c70f74180ef718b94f33fe3
2304+
RNReanimated: 9a24892f34ea317264883806d2e3de7ce34eab90
2305+
RNRive: ae2494d65061ffcc98a957e1966c9b16ee0d8ff2
2306+
RNWorklets: ddf16938b1ed7e878563a4fc8a690968ef3d27f1
23072307
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
23082308
Yoga: 9f110fc4b7aa538663cba3c14cbb1c335f43c13f
23092309

example/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module.exports = {
2-
preset: 'react-native',
2+
preset: 'react-native-harness',
33
};

example/metro.config.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const path = require('path');
22
const { getDefaultConfig } = require('@react-native/metro-config');
33
const { getConfig } = require('react-native-builder-bob/metro-config');
4+
const { withRnHarness } = require('react-native-harness/metro');
45

56
const root = path.resolve(__dirname, '..');
67

@@ -13,7 +14,9 @@ config.transformer.unstable_allowRequireContext = true;
1314
*
1415
* @type {import('metro-config').MetroConfig}
1516
*/
16-
module.exports = getConfig(config, {
17-
root,
18-
project: __dirname,
19-
});
17+
module.exports = withRnHarness(
18+
getConfig(config, {
19+
root,
20+
project: __dirname,
21+
})
22+
);

example/package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
"ios": "react-native run-ios",
88
"start": "react-native start",
99
"build:android": "react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a\"",
10-
"build:ios": "react-native build-ios --mode Debug"
10+
"build:ios": "react-native build-ios --mode Debug",
11+
"test:harness:ios": "react-native-harness --harnessRunner ios",
12+
"test:harness:android": "react-native-harness --harnessRunner android"
1113
},
1214
"dependencies": {
1315
"@react-native-picker/picker": "^2.11.4",
@@ -28,12 +30,17 @@
2830
"@react-native-community/cli": "18.0.0",
2931
"@react-native-community/cli-platform-android": "18.0.0",
3032
"@react-native-community/cli-platform-ios": "18.0.0",
33+
"@react-native-harness/platform-android": "^1.0.0-alpha.20",
34+
"@react-native-harness/platform-apple": "^1.0.0-alpha.20",
3135
"@react-native/babel-preset": "0.79.2",
3236
"@react-native/metro-config": "0.79.2",
3337
"@react-native/typescript-config": "0.79.2",
38+
"@types/deep-equal": "^1.0.4",
3439
"@types/react": "^19.0.0",
3540
"babel-plugin-react-compiler": "^1.0.0",
36-
"react-native-builder-bob": "^0.40.10"
41+
"deep-equal": "^2.2.3",
42+
"react-native-builder-bob": "^0.40.10",
43+
"react-native-harness": "^1.0.0-alpha.20"
3744
},
3845
"engines": {
3946
"node": ">=18"

example/rn-harness.config.mjs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { androidPlatform, androidEmulator } from '@react-native-harness/platform-android';
2+
import { applePlatform, appleSimulator } from '@react-native-harness/platform-apple';
3+
4+
// Allow CI to override device/version via environment variables
5+
const deviceModel = process.env.DEVICE_MODEL || 'iPhone 16 Pro';
6+
const iosVersion = process.env.IOS_VERSION || '18.6';
7+
8+
export default {
9+
entryPoint: './index.js',
10+
appRegistryComponentName: 'RiveExample',
11+
runners: [
12+
androidPlatform({
13+
name: 'android',
14+
device: androidEmulator('Pixel_8_API_35'),
15+
bundleId: 'rive.example',
16+
}),
17+
applePlatform({
18+
name: 'ios',
19+
device: appleSimulator(deviceModel, iosVersion),
20+
bundleId: 'rive.example',
21+
}),
22+
],
23+
defaultRunner: 'ios',
24+
};

0 commit comments

Comments
 (0)