Skip to content

Commit 0e5503a

Browse files
committed
test: add navigation lifecycle harness test
Verifies RiveView preserves state after screen detach/reattach using react-native-screens ScreenContainer.
1 parent a4edac3 commit 0e5503a

File tree

3 files changed

+153
-0
lines changed

3 files changed

+153
-0
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import {
2+
describe,
3+
it,
4+
expect,
5+
render,
6+
waitFor,
7+
cleanup,
8+
} from 'react-native-harness';
9+
import { useEffect } from 'react';
10+
import { ScreenContainer, Screen } from 'react-native-screens';
11+
import {
12+
RiveView,
13+
RiveFileFactory,
14+
Fit,
15+
type RiveFile,
16+
type RiveViewRef,
17+
} from '@rive-app/react-native';
18+
import type { ViewModelInstance } from '@rive-app/react-native';
19+
20+
const QUICK_START = require('../assets/rive/quick_start.riv');
21+
22+
function expectDefined<T>(value: T): asserts value is NonNullable<T> {
23+
expect(value).toBeDefined();
24+
}
25+
26+
type TestContext = {
27+
ref: RiveViewRef | null;
28+
error: string | null;
29+
};
30+
31+
function ScreenWithRive({
32+
file,
33+
instance,
34+
context,
35+
activityState,
36+
}: {
37+
file: RiveFile;
38+
instance: ViewModelInstance;
39+
context: TestContext;
40+
activityState: 0 | 2;
41+
}) {
42+
useEffect(() => {
43+
return () => {
44+
context.ref = null;
45+
};
46+
}, [context]);
47+
48+
return (
49+
<ScreenContainer style={{ width: 200, height: 200 }}>
50+
<Screen activityState={activityState} style={{ flex: 1 }}>
51+
<RiveView
52+
hybridRef={{
53+
f: (ref: RiveViewRef | null) => {
54+
context.ref = ref;
55+
},
56+
}}
57+
style={{ flex: 1 }}
58+
file={file}
59+
autoPlay={false}
60+
dataBind={instance}
61+
fit={Fit.Contain}
62+
stateMachineName="State Machine 1"
63+
onError={(e) => {
64+
context.error = e.message;
65+
}}
66+
/>
67+
</Screen>
68+
</ScreenContainer>
69+
);
70+
}
71+
72+
describe('navigation lifecycle (PR #46)', () => {
73+
it('RiveView preserves state after screen detach/reattach', async () => {
74+
const file = await RiveFileFactory.fromSource(QUICK_START, undefined);
75+
const vm = file.defaultArtboardViewModel();
76+
expectDefined(vm);
77+
const instance = vm.createDefaultInstance();
78+
expectDefined(instance);
79+
80+
const context: TestContext = { ref: null, error: null };
81+
82+
const { rerender } = await render(
83+
<ScreenWithRive
84+
file={file}
85+
instance={instance}
86+
context={context}
87+
activityState={2}
88+
/>
89+
);
90+
91+
await waitFor(
92+
() => {
93+
expect(context.ref).not.toBeNull();
94+
},
95+
{ timeout: 5000 }
96+
);
97+
await context.ref!.awaitViewReady();
98+
99+
// Set health to 75 via the native view's VMI
100+
const boundVmi = context.ref!.getViewModelInstance();
101+
expectDefined(boundVmi);
102+
const health = boundVmi.numberProperty('health');
103+
expectDefined(health);
104+
health.value = 75;
105+
106+
// Detach screen (simulates navigating away — triggers onDetachedFromWindow)
107+
await rerender(
108+
<ScreenWithRive
109+
file={file}
110+
instance={instance}
111+
context={context}
112+
activityState={0}
113+
/>
114+
);
115+
await new Promise((r) => setTimeout(r, 300));
116+
117+
// Reattach screen (simulates navigating back)
118+
await rerender(
119+
<ScreenWithRive
120+
file={file}
121+
instance={instance}
122+
context={context}
123+
activityState={2}
124+
/>
125+
);
126+
await new Promise((r) => setTimeout(r, 300));
127+
128+
// Query the VMI from the native view after reattach
129+
expect(context.error).toBeNull();
130+
const reattachedVmi = context.ref!.getViewModelInstance();
131+
expectDefined(reattachedVmi);
132+
const reattachedHealth = reattachedVmi.numberProperty('health');
133+
expectDefined(reattachedHealth);
134+
expect(reattachedHealth.value).toBe(75);
135+
136+
cleanup();
137+
});
138+
});

example/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"react-native-nitro-modules": "0.35.0",
2525
"react-native-reanimated": "4.1.5",
2626
"react-native-safe-area-context": "^5.4.0",
27+
"react-native-screens": "^4.18.0",
2728
"react-native-worklets": "0.6.1"
2829
},
2930
"devDependencies": {

yarn.lock

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14458,6 +14458,7 @@ __metadata:
1445814458
react-native-nitro-modules: 0.35.0
1445914459
react-native-reanimated: 4.1.5
1446014460
react-native-safe-area-context: ^5.4.0
14461+
react-native-screens: ^4.18.0
1446114462
react-native-worklets: 0.6.1
1446214463
languageName: unknown
1446314464
linkType: soft
@@ -14472,6 +14473,19 @@ __metadata:
1447214473
languageName: node
1447314474
linkType: hard
1447414475

14476+
"react-native-screens@npm:^4.18.0":
14477+
version: 4.24.0
14478+
resolution: "react-native-screens@npm:4.24.0"
14479+
dependencies:
14480+
react-freeze: ^1.0.0
14481+
warn-once: ^0.1.0
14482+
peerDependencies:
14483+
react: "*"
14484+
react-native: "*"
14485+
checksum: 670873e5a358db95a7eeaae94a1548f5461e3a0d315bb81b7c52b544826a245fed878b13dab8037eb2371c54d3ebced472642c18889892c17f91a92964d40108
14486+
languageName: node
14487+
linkType: hard
14488+
1447514489
"react-native-screens@npm:~4.16.0":
1447614490
version: 4.16.0
1447714491
resolution: "react-native-screens@npm:4.16.0"

0 commit comments

Comments
 (0)