Skip to content

Commit 2e34004

Browse files
committed
Add test that proves stale source values between unrelated renders
1 parent f8cc818 commit 2e34004

File tree

1 file changed

+118
-0
lines changed

1 file changed

+118
-0
lines changed

tests/unit/staleSourceValueTest.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/* eslint-disable no-console, @typescript-eslint/no-explicit-any */
2+
/**
3+
* Test to prove that useOnyx sourceValue persists across unrelated renders,
4+
* making it unsound for cache invalidation logic.
5+
*/
6+
7+
import {act, renderHook} from '@testing-library/react-native';
8+
import {useState} from 'react';
9+
import Onyx, {useOnyx} from '../../lib';
10+
import waitForPromisesToResolve from '../utils/waitForPromisesToResolve';
11+
12+
const ONYXKEYS = {
13+
COLLECTION: {
14+
REPORTS: 'reports_',
15+
POLICIES: 'policies_',
16+
},
17+
};
18+
19+
Onyx.init({
20+
keys: ONYXKEYS,
21+
});
22+
23+
beforeEach(async () => {
24+
await Onyx.clear();
25+
await waitForPromisesToResolve();
26+
});
27+
28+
afterEach(async () => {
29+
await waitForPromisesToResolve();
30+
// eslint-disable-next-line no-promise-executor-return
31+
await new Promise((resolve) => setTimeout(resolve, 50));
32+
await waitForPromisesToResolve();
33+
});
34+
35+
describe('Stale sourceValue Test', () => {
36+
it('should demonstrate that sourceValue persists across unrelated renders, making cache invalidation unsound', async () => {
37+
const sourceValueHistory: any[] = [];
38+
39+
// Create a component that can re-render for reasons unrelated to Onyx
40+
const {result, rerender} = renderHook(
41+
({externalState}: {externalState: number}) => {
42+
const [localState, setLocalState] = useState(0);
43+
const [reports, {sourceValue: reportsSourceValue}] = useOnyx(ONYXKEYS.COLLECTION.REPORTS);
44+
const [policies, {sourceValue: policiesSourceValue}] = useOnyx(ONYXKEYS.COLLECTION.POLICIES);
45+
46+
// Track every sourceValue we see
47+
const currentSourceValues = {
48+
externalState,
49+
localState,
50+
reportsSourceValue: reportsSourceValue ? Object.keys(reportsSourceValue) : undefined,
51+
policiesSourceValue: policiesSourceValue ? Object.keys(policiesSourceValue) : undefined,
52+
};
53+
sourceValueHistory.push(currentSourceValues);
54+
55+
return {
56+
reports,
57+
policies,
58+
reportsSourceValue,
59+
policiesSourceValue,
60+
localState,
61+
setLocalState,
62+
triggerUnrelatedRerender: () => setLocalState((prev) => prev + 1),
63+
};
64+
},
65+
{initialProps: {externalState: 1}},
66+
);
67+
68+
await act(async () => waitForPromisesToResolve());
69+
70+
console.log('\n=== Testing sourceValue persistence across unrelated renders ===');
71+
72+
// Trigger an Onyx update
73+
await act(async () => {
74+
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORTS}123`, {
75+
reportID: '123',
76+
lastMessage: 'Test message',
77+
});
78+
await waitForPromisesToResolve();
79+
});
80+
81+
const afterOnyxUpdate = sourceValueHistory[sourceValueHistory.length - 1];
82+
83+
// Trigger unrelated re-renders
84+
rerender({externalState: 2});
85+
await act(async () => waitForPromisesToResolve());
86+
const afterPropsChange = sourceValueHistory[sourceValueHistory.length - 1];
87+
88+
await act(async () => {
89+
result.current.triggerUnrelatedRerender();
90+
await waitForPromisesToResolve();
91+
});
92+
const afterStateChange = sourceValueHistory[sourceValueHistory.length - 1];
93+
94+
// Check sourceValue persistence
95+
const hasSourceAfterOnyx = afterOnyxUpdate.reportsSourceValue !== undefined;
96+
const hasSourceAfterProps = afterPropsChange.reportsSourceValue !== undefined;
97+
const hasSourceAfterState = afterStateChange.reportsSourceValue !== undefined;
98+
99+
console.log(`After Onyx update: sourceValue ${hasSourceAfterOnyx ? 'present' : 'missing'}`);
100+
console.log(`After props change: sourceValue ${hasSourceAfterProps ? 'PERSISTS' : 'cleared'}`);
101+
console.log(`After state change: sourceValue ${hasSourceAfterState ? 'PERSISTS' : 'cleared'}`);
102+
103+
if (hasSourceAfterProps || hasSourceAfterState) {
104+
console.log('Result: sourceValue persists across unrelated renders (unsound for cache invalidation)');
105+
}
106+
107+
// Expected behavior: sourceValue present after actual Onyx update
108+
expect(hasSourceAfterOnyx).toBe(true);
109+
110+
// BUG: sourceValue incorrectly persists after unrelated renders
111+
expect(hasSourceAfterProps).toBe(true); // PROVES BUG: sourceValue should be undefined here
112+
expect(hasSourceAfterState).toBe(true); // PROVES BUG: sourceValue should be undefined here
113+
114+
// For contrast: in a correct implementation, these should be false
115+
// expect(hasSourceAfterProps).toBe(false); // What SHOULD happen
116+
// expect(hasSourceAfterState).toBe(false); // What SHOULD happen
117+
});
118+
});

0 commit comments

Comments
 (0)