diff --git a/__tests__/HandlerStability.test.js b/__tests__/HandlerStability.test.js new file mode 100644 index 0000000..f3540bc --- /dev/null +++ b/__tests__/HandlerStability.test.js @@ -0,0 +1,129 @@ +import React from 'react'; +import App from '../src/App'; +import renderer, {act} from 'react-test-renderer'; +import SettingsSwitch from '../src/components/SettingsSwitch'; +import SearchInput from '../src/components/SearchInput'; + +// Mock dependencies +jest.mock('react-native-background-timer', () => ({ + stopBackgroundTimer: jest.fn(), + runBackgroundTimer: jest.fn(), +})); + +jest.mock('react-native-push-notification', () => ({ + configure: jest.fn(), + localNotification: jest.fn(), +})); + +jest.mock('@react-native-community/async-storage', () => ({ + setItem: jest.fn(() => Promise.resolve()), + multiSet: jest.fn(() => Promise.resolve()), + getItem: jest.fn(() => Promise.resolve(null)), + getAllKeys: jest.fn(() => Promise.resolve([])), + multiGet: jest.fn(() => Promise.resolve([])), + removeItem: jest.fn(() => Promise.resolve()), +})); + +jest.mock('react-native-webview', () => { + return { + WebView: () => null, + }; +}); + +jest.mock('../src/services/BackgroundService', () => ({ + checkUrlForText: jest.fn(), + background_task: jest.fn(), +})); + +// Fully mock react-native to avoid renderer issues +jest.mock('react-native', () => { + // eslint-disable-next-line no-shadow + const React = require('react'); + const View = props => React.createElement('View', props, props.children); + const Text = props => React.createElement('Text', props, props.children); + const ScrollView = props => + React.createElement('ScrollView', props, props.children); + const TextInput = React.forwardRef((props, ref) => + React.createElement('TextInput', {...props, ref}), + ); + const Switch = props => React.createElement('Switch', props); + const Button = props => React.createElement('Button', props); + const ActivityIndicator = props => + React.createElement('ActivityIndicator', props); + + const Picker = props => React.createElement('Picker', props, props.children); + Picker.Item = props => React.createElement('Picker.Item', props); + + const PushNotificationIOS = { + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + requestPermissions: jest.fn(() => Promise.resolve({})), + checkPermissions: jest.fn(), + FetchResult: { + NoData: 'NoData', + NewData: 'NewData', + Failed: 'Failed', + }, + }; + + return { + Platform: { + OS: 'ios', + select: obj => obj.ios, + }, + View, + Text, + ScrollView, + TextInput, + Switch, + Button, + ActivityIndicator, + Picker, + PushNotificationIOS, + StyleSheet: { + create: obj => obj, + flatten: obj => obj, + }, + }; +}); + +describe('Handler Stability Benchmark', () => { + it('checks if SettingsSwitch handler is stable across renders', async () => { + let component; + await act(async () => { + component = renderer.create(); + }); + + const root = component.root; + + // Helper to find the specific SettingsSwitch + const findSwitch = () => { + return root + .findAllByType(SettingsSwitch) + .find(node => node.props.label === 'Search Absence of Text:'); + }; + + const settingsSwitchBefore = findSwitch(); + const handlerBefore = settingsSwitchBefore.props.onValueChange; + + // Trigger a re-render by changing unrelated state (searchText) + const searchInput = root.findByType(SearchInput); + + // We need to find the underlying TextInput mock and call onChangeText + const textInput = searchInput.findByType('TextInput'); + + await act(async () => { + textInput.props.onChangeText('new search text'); + }); + + const settingsSwitchAfter = findSwitch(); + const handlerAfter = settingsSwitchAfter.props.onValueChange; + + // Check equality + const isStable = handlerBefore === handlerAfter; + console.log(`Handler is stable: ${isStable}`); + + // For the optimization, we expect this to be TRUE + expect(isStable).toBe(true); + }); +}); diff --git a/src/App.js b/src/App.js index a5641d9..b24741e 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,4 @@ -import React, {useState, useEffect, useRef} from 'react'; +import React, {useState, useEffect, useRef, useCallback} from 'react'; import { Platform, Text, @@ -45,6 +45,14 @@ PushNotification.configure({ requestPermissions: true, }); +const persist = async (key, value) => { + try { + await AsyncStorage.setItem(key, value); + } catch (error) { + console.log(error); + } +}; + const App = () => { const [url, setUrl] = useState(''); const [searchText, setSearchText] = useState(''); @@ -114,13 +122,11 @@ const App = () => { loadState(); }, []); - const persist = async (key, value) => { - try { - await AsyncStorage.setItem(key, value); - } catch (error) { - console.log(error); - } - }; + const handleSearchAbsenceChange = useCallback(value => { + const valStr = value ? 'yes' : 'no'; + setSearchAbsence(valStr); + persist('searchAbsence', valStr); + }, []); const createPrefetchJobs = async () => { try { @@ -227,11 +233,7 @@ const App = () => { { - const valStr = value ? 'yes' : 'no'; - setSearchAbsence(valStr); - persist('searchAbsence', valStr); - }} + onValueChange={handleSearchAbsenceChange} />