From e9a9c8a3303fb1793a8279c9ee61f3f9d072dbc8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 19:16:13 +0000 Subject: [PATCH 1/2] Optimize WebView source prop with useMemo to prevent unnecessary re-renders Co-authored-by: xRahul <1639945+xRahul@users.noreply.github.com> --- __tests__/WebViewPerformance.test.js | 166 +++++++++++++++++++++++++++ src/App.js | 6 +- 2 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 __tests__/WebViewPerformance.test.js diff --git a/__tests__/WebViewPerformance.test.js b/__tests__/WebViewPerformance.test.js new file mode 100644 index 0000000..643b7c7 --- /dev/null +++ b/__tests__/WebViewPerformance.test.js @@ -0,0 +1,166 @@ +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', () => { + const React = 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', () => { + // 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('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 !== '' && ( Date: Wed, 11 Feb 2026 19:21:29 +0000 Subject: [PATCH 2/2] Fix lint errors and warnings in tests Co-authored-by: xRahul <1639945+xRahul@users.noreply.github.com> --- __tests__/SettingsSwitchPerf.test.js | 26 ++++++++++------------ __tests__/WebViewPerformance.test.js | 33 ++++++++++++++-------------- 2 files changed, 29 insertions(+), 30 deletions(-) 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 index 643b7c7..4fe6a48 100644 --- a/__tests__/WebViewPerformance.test.js +++ b/__tests__/WebViewPerformance.test.js @@ -25,7 +25,8 @@ jest.mock('@react-native-community/async-storage', () => ({ // Mock react-native-webview to capture props jest.mock('react-native-webview', () => { - const React = require('react'); + // 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. @@ -42,22 +43,22 @@ jest.mock('../src/services/BackgroundService', () => ({ // 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 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', {...props, ref}), + RNReact.createElement('ScrollView', props, props.children); + const TextInput = RNReact.forwardRef((props, ref) => + RNReact.createElement('TextInput', {...props, ref}), ); - const Switch = props => React.createElement('Switch', props); - const Button = props => React.createElement('Button', props); + const Switch = props => RNReact.createElement('Switch', 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(), @@ -105,9 +106,9 @@ describe('WebView Performance', () => { // 1. Enter URL // Find the TextInput for URL input (UrlInput component) - const textInput = root.findAllByType('TextInput').find( - node => node.props.placeholder === 'Enter URL https://...' - ); + const textInput = root + .findAllByType('TextInput') + .find(node => node.props.placeholder === 'Enter URL https://...'); if (!textInput) { throw new Error('Could not find URL TextInput');