diff --git a/__tests__/SettingsSwitchPerf.test.js b/__tests__/SettingsSwitchPerf.test.js index 0f18cd3..5d4b68a 100644 --- a/__tests__/SettingsSwitchPerf.test.js +++ b/__tests__/SettingsSwitchPerf.test.js @@ -1,7 +1,6 @@ import React from 'react'; import App from '../src/App'; import renderer, {act} from 'react-test-renderer'; -import AsyncStorage from '@react-native-community/async-storage'; // Mock dependencies jest.mock('react-native-background-timer', () => ({ @@ -42,13 +41,13 @@ jest.mock('../src/services/BackgroundService', () => ({ const mockSwitchRender = jest.fn(); jest.mock('react-native', () => { - const React = require('react'); - const View = props => React.createElement('View', props, props.children); - const Text = props => React.createElement('Text', props, props.children); + const RNReact = require('react'); + const View = props => RNReact.createElement('View', props, props.children); + const Text = props => RNReact.createElement('Text', props, props.children); const ScrollView = props => - React.createElement('ScrollView', props, props.children); - const TextInput = React.forwardRef((props, ref) => - React.createElement('TextInput', { + RNReact.createElement('ScrollView', props, props.children); + const TextInput = RNReact.forwardRef((props, ref) => + RNReact.createElement('TextInput', { ...props, ref, onChangeText: text => { @@ -62,15 +61,16 @@ jest.mock('react-native', () => { // We spy on Switch render const Switch = props => { mockSwitchRender(props); - return React.createElement('Switch', props); + return RNReact.createElement('Switch', props); }; - const Button = props => React.createElement('Button', props); + const Button = props => RNReact.createElement('Button', props); const ActivityIndicator = props => - React.createElement('ActivityIndicator', props); + RNReact.createElement('ActivityIndicator', props); - const Picker = props => React.createElement('Picker', props, props.children); - Picker.Item = props => React.createElement('Picker.Item', props); + const Picker = props => + RNReact.createElement('Picker', props, props.children); + Picker.Item = props => RNReact.createElement('Picker.Item', props); const PushNotificationIOS = { addEventListener: jest.fn(), @@ -121,8 +121,6 @@ it('prevents unnecessary re-renders of SettingsSwitch', async () => { // The mock of AsyncStorage returns promises, we need to wait for them. // act handles this if we await properly. - const initialRenderCount = mockSwitchRender.mock.calls.length; - // Reset count to measure update impact mockSwitchRender.mockClear(); diff --git a/__tests__/WebViewPerformance.test.js b/__tests__/WebViewPerformance.test.js new file mode 100644 index 0000000..4fe6a48 --- /dev/null +++ b/__tests__/WebViewPerformance.test.js @@ -0,0 +1,167 @@ +import React from 'react'; +import App from '../src/App'; +import renderer, {act} from 'react-test-renderer'; +import {WebView} from 'react-native-webview'; + +// 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()), +})); + +// Mock react-native-webview to capture props +jest.mock('react-native-webview', () => { + // eslint-disable-next-line no-unused-vars + const RNReact = require('react'); + const {View} = require('react-native'); + // Return a component that renders View so it's not null, + // and attach a spy to capture props. + const MockWebView = jest.fn(props => ); + return { + WebView: MockWebView, + }; +}); + +jest.mock('../src/services/BackgroundService', () => ({ + checkUrlForText: jest.fn(() => Promise.resolve()), + background_task: jest.fn(), +})); + +// Fully mock react-native to avoid renderer issues +jest.mock('react-native', () => { + const RNReact = require('react'); + const View = props => RNReact.createElement('View', props, props.children); + const Text = props => RNReact.createElement('Text', props, props.children); + const ScrollView = props => + RNReact.createElement('ScrollView', props, props.children); + const TextInput = RNReact.forwardRef((props, ref) => + RNReact.createElement('TextInput', {...props, ref}), + ); + const Switch = props => RNReact.createElement('Switch', props); + const Button = props => RNReact.createElement('Button', props); + const ActivityIndicator = props => + RNReact.createElement('ActivityIndicator', props); + + const Picker = props => + RNReact.createElement('Picker', props, props.children); + Picker.Item = props => RNReact.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('WebView Performance', () => { + it('preserves source object reference across renders (optimized)', async () => { + let component; + + // Mount App + await act(async () => { + component = renderer.create(); + }); + + const root = component.root; + + // 1. Enter URL + // Find the TextInput for URL input (UrlInput component) + const textInput = root + .findAllByType('TextInput') + .find(node => node.props.placeholder === 'Enter URL https://...'); + + if (!textInput) { + throw new Error('Could not find URL TextInput'); + } + + await act(async () => { + textInput.props.onChangeText('https://example.com'); + }); + + // 2. Start Checking + const startButton = root.find( + node => node.props.title === 'Start Checking', + ); + await act(async () => { + startButton.props.onPress(); + }); + + // Verify WebView is rendered + // WebView is a mock function, so we can check its calls. + expect(WebView).toHaveBeenCalled(); + + // Get the last call to WebView + const initialCallCount = WebView.mock.calls.length; + const initialSource = WebView.mock.calls[initialCallCount - 1][0].source; + + // 3. Toggle "Case Sensitive Search" to trigger re-render + // Find SettingsSwitch for "Case Sensitive Search" + // SettingsSwitch renders a View with Text and View/Switch. + // We can find Switch components. + // There are 2 switches. The first one is Case Sensitive Search. + // But let's be robust. + // SettingsSwitch passes label. But we can't find by prop on a functional component easily if it's not a native component. + // SettingsSwitch is imported. + // But we can find the Switch component. + + const switches = root.findAllByType('Switch'); + // Assuming the first switch is Case Sensitive Search based on order in App.js + const caseSensitiveSwitch = switches[0]; + + await act(async () => { + caseSensitiveSwitch.props.onValueChange(false); + }); + + // Verify WebView was re-rendered + const finalCallCount = WebView.mock.calls.length; + expect(finalCallCount).toBeGreaterThan(initialCallCount); + + const finalSource = WebView.mock.calls[finalCallCount - 1][0].source; + + // Assert that source objects are structurally equal + expect(initialSource).toEqual(finalSource); + + // Assert that source objects ARE referentially equal (optimized behavior) + expect(initialSource).toBe(finalSource); + }); +}); diff --git a/src/App.js b/src/App.js index c8a20bf..904ec02 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,4 @@ -import React, {useState, useEffect, useRef, useCallback} from 'react'; +import React, {useState, useEffect, useRef, useCallback, useMemo} from 'react'; import { Platform, Text, @@ -198,6 +198,8 @@ const App = () => { searchTextInputRef.current && searchTextInputRef.current.focus(); }, []); + const source = useMemo(() => ({uri: url}), [url]); + return ( { {taskSet === 'yes' && url !== '' && (