Skip to content

Commit 2b333fe

Browse files
committed
chore: test improvements and export RiveWorkletBridge type
1 parent 7cdd5f7 commit 2b333fe

10 files changed

Lines changed: 503 additions & 62 deletions

File tree

codecov.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
coverage:
2+
status:
3+
project:
4+
default:
5+
target: auto
6+
threshold: 1%
7+
patch:
8+
default:
9+
target: auto
10+
11+
comment:
12+
layout: "header, diff, flags, components"
13+
behavior: default
14+
require_changes: true
15+
16+
flags:
17+
unit:
18+
paths:
19+
- src/
20+
carryforward: true
21+
harness:
22+
paths:
23+
- src/
24+
carryforward: true
25+
26+
ignore:
27+
- "**/*.nitro.ts"
28+
- "**/index.ts"
29+
- "**/index.tsx"
30+
- "nitrogen/**"
31+
- "example/**"
32+
- "expo-example/**"
33+
- "lib/**"

example/__tests__/hooks.harness.tsx

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
} from '@rive-app/react-native';
1717
import type { ViewModelInstance } from '@rive-app/react-native';
1818

19-
const QUICK_START = require('../assets/rive/quick_start.riv');
2019
const DATABINDING = require('../assets/rive/databinding.riv');
2120

2221
type UseRiveNumberContext = {
@@ -45,7 +44,7 @@ function UseRiveNumberTestComponent({
4544
instance: ViewModelInstance;
4645
context: UseRiveNumberContext;
4746
}) {
48-
const { value, setValue, error } = useRiveNumber('health', instance);
47+
const { value, setValue, error } = useRiveNumber('age', instance);
4948

5049
useEffect(() => {
5150
context.value = value;
@@ -94,54 +93,58 @@ function expectDefined<T>(value: T): asserts value is NonNullable<T> {
9493

9594
describe('useRiveNumber Hook', () => {
9695
it('returns value from number property', async () => {
97-
const file = await RiveFileFactory.fromSource(QUICK_START, undefined);
98-
const vm = file.defaultArtboardViewModel();
96+
const file = await RiveFileFactory.fromSource(DATABINDING, undefined);
97+
const vm = file.viewModelByName('Person');
9998
expectDefined(vm);
100-
const instance = vm.createDefaultInstance();
99+
const instance = vm.createInstanceByName('Gordon');
101100
expectDefined(instance);
102101

103102
const context = createUseRiveNumberContext();
104-
await render(
105-
<UseRiveNumberTestComponent instance={instance} context={context} />
106-
);
107-
108-
await waitFor(
109-
() => {
110-
expect(context.error).toBeNull();
111-
expect(typeof context.value).toBe('number');
112-
},
113-
{ timeout: 5000 }
114-
);
115-
116-
cleanup();
103+
try {
104+
await render(
105+
<UseRiveNumberTestComponent instance={instance} context={context} />
106+
);
107+
108+
await waitFor(
109+
() => {
110+
expect(context.error).toBeNull();
111+
expect(typeof context.value).toBe('number');
112+
},
113+
{ timeout: 5000 }
114+
);
115+
} finally {
116+
cleanup();
117+
}
117118
});
118119

119120
it('can set value via setValue', async () => {
120-
const file = await RiveFileFactory.fromSource(QUICK_START, undefined);
121-
const vm = file.defaultArtboardViewModel();
121+
const file = await RiveFileFactory.fromSource(DATABINDING, undefined);
122+
const vm = file.viewModelByName('Person');
122123
expectDefined(vm);
123-
const instance = vm.createDefaultInstance();
124+
const instance = vm.createInstanceByName('Gordon');
124125
expectDefined(instance);
125126

126127
const context = createUseRiveNumberContext();
127-
await render(
128-
<UseRiveNumberTestComponent instance={instance} context={context} />
129-
);
130-
131-
await waitFor(
132-
() => {
133-
expect(context.setValue).not.toBeNull();
134-
},
135-
{ timeout: 5000 }
136-
);
137-
138-
context.setValue!(42);
139-
140-
const property = instance.numberProperty('health');
141-
expectDefined(property);
142-
expect(property.value).toBe(42);
143-
144-
cleanup();
128+
try {
129+
await render(
130+
<UseRiveNumberTestComponent instance={instance} context={context} />
131+
);
132+
133+
await waitFor(
134+
() => {
135+
expect(context.setValue).not.toBeNull();
136+
},
137+
{ timeout: 5000 }
138+
);
139+
140+
context.setValue!(42);
141+
142+
const property = instance.numberProperty('age');
143+
expectDefined(property);
144+
expect(property.value).toBe(42);
145+
} finally {
146+
cleanup();
147+
}
145148
});
146149
});
147150

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import {
2+
describe,
3+
it,
4+
expect,
5+
render,
6+
waitFor,
7+
cleanup,
8+
} from 'react-native-harness';
9+
import { View } from 'react-native';
10+
import { useEffect, useMemo } from 'react';
11+
import { NitroModules } from 'react-native-nitro-modules';
12+
import { scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
13+
14+
import {
15+
Fit,
16+
RiveView,
17+
RiveFileFactory,
18+
useRiveFile,
19+
useViewModelInstance,
20+
} from '@rive-app/react-native';
21+
import type { RiveWorkletBridge } from '@rive-app/react-native';
22+
23+
const DATABINDING = require('../assets/rive/databinding.riv');
24+
const BOUNCING_BALL = require('../assets/rive/bouncing_ball.riv');
25+
26+
function expectDefined<T>(value: T): asserts value is NonNullable<T> {
27+
expect(value).toBeDefined();
28+
}
29+
30+
// Note: installWorkletDispatcher is already called in App.tsx at startup.
31+
// UI thread listeners are designed for Rive-driven value changes (animation/data binding),
32+
// not for JS-thread value changes. Testing the full UI thread listener flow requires
33+
// a Rive file with animation that drives property changes.
34+
35+
describe('Worklet Bridge', () => {
36+
it('RiveWorkletBridge HybridObject can be created', () => {
37+
const bridge =
38+
NitroModules.createHybridObject<RiveWorkletBridge>('RiveWorkletBridge');
39+
expect(bridge).toBeDefined();
40+
});
41+
42+
it('property can be boxed for worklet use', async () => {
43+
const file = await RiveFileFactory.fromSource(DATABINDING, undefined);
44+
const vm = file.viewModelByName('Person');
45+
expectDefined(vm);
46+
const instance = vm.createInstanceByName('Gordon');
47+
expectDefined(instance);
48+
49+
const property = instance.numberProperty('age');
50+
expectDefined(property);
51+
52+
// Verify boxing works (required for passing to worklets)
53+
const boxedProperty = NitroModules.box(property);
54+
expect(boxedProperty).toBeDefined();
55+
56+
// Verify the property value can be read
57+
expect(property.value).toBe(30); // Gordon's age is 30
58+
});
59+
60+
// TODO: for some reason those wont run in harness environment
61+
if (!global.RN_HARNESS) {
62+
it('JS thread listener is called when Rive animation changes value', async () => {
63+
// Listeners are notified when Rive animation/data binding changes values.
64+
// This test uses bouncing_ball.riv which has an animation that drives ypos.
65+
66+
let receivedValue: number | undefined;
67+
68+
function ListenerTestComponent({
69+
onResult,
70+
}: {
71+
onResult: (value: number) => void;
72+
}) {
73+
const { riveFile } = useRiveFile(BOUNCING_BALL);
74+
const instance = useViewModelInstance(riveFile);
75+
76+
const property = useMemo(
77+
() => instance?.numberProperty('ypos'),
78+
[instance]
79+
);
80+
81+
useEffect(
82+
() =>
83+
property?.addListener((value) => {
84+
onResult(value);
85+
}),
86+
[property, onResult]
87+
);
88+
89+
if (!riveFile || !instance) {
90+
return <View />;
91+
}
92+
93+
return (
94+
<RiveView
95+
style={{ width: 100, height: 100 }}
96+
autoPlay={true}
97+
dataBind={instance}
98+
fit={Fit.Contain}
99+
file={riveFile}
100+
/>
101+
);
102+
}
103+
104+
try {
105+
await render(
106+
<ListenerTestComponent
107+
onResult={(value) => {
108+
receivedValue = value;
109+
}}
110+
/>
111+
);
112+
113+
await waitFor(
114+
() => {
115+
expect(receivedValue).toBeDefined();
116+
expect(typeof receivedValue).toBe('number');
117+
},
118+
{ timeout: 5000 }
119+
);
120+
} finally {
121+
cleanup();
122+
}
123+
});
124+
125+
it('UI thread listener is called when Rive animation changes value', async () => {
126+
// Same as above but listener is registered via scheduleOnUI, so callback runs on UI thread
127+
128+
type ListenerResult = {
129+
calledOnUIThread: boolean;
130+
receivedValue: number;
131+
};
132+
133+
let result: ListenerResult | undefined;
134+
135+
function UIThreadListenerTestComponent({
136+
onResult,
137+
}: {
138+
onResult: (r: ListenerResult) => void;
139+
}) {
140+
const { riveFile } = useRiveFile(BOUNCING_BALL);
141+
const instance = useViewModelInstance(riveFile);
142+
143+
const property = useMemo(
144+
() => instance?.numberProperty('ypos'),
145+
[instance]
146+
);
147+
148+
useEffect(() => {
149+
if (!property) return;
150+
151+
const boxedProperty = NitroModules.box(property);
152+
153+
const reportResult = (r: ListenerResult) => {
154+
onResult(r);
155+
};
156+
157+
scheduleOnUI(() => {
158+
'worklet';
159+
const prop = boxedProperty.unbox();
160+
prop.addListener((value: number) => {
161+
'worklet';
162+
// Check if we're on UI thread
163+
const isOnUIThread =
164+
typeof global._WORKLET !== 'undefined' &&
165+
global._WORKLET === true;
166+
scheduleOnRN(reportResult, {
167+
calledOnUIThread: isOnUIThread,
168+
receivedValue: value,
169+
});
170+
});
171+
});
172+
173+
return () => {
174+
property.removeListeners();
175+
};
176+
}, [property, onResult]);
177+
178+
if (!riveFile || !instance) {
179+
return <View />;
180+
}
181+
182+
return (
183+
<RiveView
184+
style={{ width: 100, height: 100 }}
185+
autoPlay={true}
186+
dataBind={instance}
187+
fit={Fit.Contain}
188+
file={riveFile}
189+
/>
190+
);
191+
}
192+
193+
try {
194+
await render(
195+
<UIThreadListenerTestComponent
196+
onResult={(r) => {
197+
result = r;
198+
}}
199+
/>
200+
);
201+
202+
// Wait for animation to trigger the listener (bouncing ball should change ypos quickly)
203+
await waitFor(
204+
() => {
205+
expect(result).toBeDefined();
206+
expect(result!.calledOnUIThread).toBe(true);
207+
expect(typeof result!.receivedValue).toBe('number');
208+
},
209+
{ timeout: 5000 }
210+
);
211+
} finally {
212+
cleanup();
213+
}
214+
});
215+
}
216+
});

example/ios/Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1904,7 +1904,7 @@ PODS:
19041904
- ReactCommon/turbomodule/core
19051905
- RNWorklets
19061906
- Yoga
1907-
- RNRive (0.1.5):
1907+
- RNRive (0.2.0):
19081908
- DoubleConversion
19091909
- glog
19101910
- hermes-engine
@@ -2330,7 +2330,7 @@ SPEC CHECKSUMS:
23302330
RNCPicker: 83c74db2de8274d8a8f3e18d91dea174a708f8c4
23312331
RNGestureHandler: bff91bb5ab5688265c70f74180ef718b94f33fe3
23322332
RNReanimated: 9a24892f34ea317264883806d2e3de7ce34eab90
2333-
RNRive: 73aa1ec7d3ef4da1030e81643808adb538bc05ca
2333+
RNRive: 509163d7fb4dcced11210670f0f6e7cb2f904e30
23342334
RNWorklets: ddf16938b1ed7e878563a4fc8a690968ef3d27f1
23352335
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
23362336
Yoga: 9f110fc4b7aa538663cba3c14cbb1c335f43c13f

example/src/exercisers/RiveToReactNativeExample.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
type ViewModelInstance,
2323
type ViewModelNumberProperty,
2424
} from '@rive-app/react-native';
25-
import { type Metadata } from '../helpers/metadata';
25+
import { type Metadata } from '../shared/metadata';
2626

2727
declare global {
2828
var __callMicrotasks: () => void;

0 commit comments

Comments
 (0)