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');