diff --git a/packages/bruno-app/src/components/Preferences/Beta/index.js b/packages/bruno-app/src/components/Preferences/Beta/index.js index cd1d68fb7..8a4ea4cf2 100644 --- a/packages/bruno-app/src/components/Preferences/Beta/index.js +++ b/packages/bruno-app/src/components/Preferences/Beta/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback, useRef } from 'react'; import { useFormik } from 'formik'; import { useSelector, useDispatch } from 'react-redux'; import { savePreferences } from 'providers/ReduxStore/slices/app'; @@ -73,16 +73,19 @@ const Beta = ({ close }) => { .catch((err) => console.log(err) && toast.error('Failed to update beta preferences')); }, [dispatch, preferences]); + const handleSaveRef = useRef(handleSave); + handleSaveRef.current = handleSave; + const debouncedSave = useCallback( debounce((values) => { betaSchema.validate(values, { abortEarly: true }) .then((validatedValues) => { - handleSave(validatedValues); + handleSaveRef.current(validatedValues); }) .catch((error) => { }); }, 500), - [handleSave, betaSchema] + [betaSchema] ); // Auto-save when form values change @@ -91,7 +94,7 @@ const Beta = ({ close }) => { debouncedSave(formik.values); } return () => { - debouncedSave.cancel(); + debouncedSave.flush(); }; }, [formik.values, formik.dirty, formik.isValid, debouncedSave]); diff --git a/packages/bruno-app/src/components/Preferences/Cache/index.js b/packages/bruno-app/src/components/Preferences/Cache/index.js index 28ef91483..a17ef6caa 100644 --- a/packages/bruno-app/src/components/Preferences/Cache/index.js +++ b/packages/bruno-app/src/components/Preferences/Cache/index.js @@ -68,7 +68,7 @@ const Cache = () => { debouncedSave(formik.values); } return () => { - debouncedSave.cancel(); + debouncedSave.flush(); }; }, [formik.values, formik.dirty, formik.isValid, debouncedSave]); diff --git a/packages/bruno-app/src/components/Preferences/Display/Font/index.js b/packages/bruno-app/src/components/Preferences/Display/Font/index.js index 2f5c50df3..398477d00 100644 --- a/packages/bruno-app/src/components/Preferences/Display/Font/index.js +++ b/packages/bruno-app/src/components/Preferences/Display/Font/index.js @@ -38,11 +38,14 @@ const Font = () => { }); }, [dispatch, preferences]); + const handleSaveRef = useRef(handleSave); + handleSaveRef.current = handleSave; + const debouncedSave = useCallback( debounce((font, fontSize) => { - handleSave(font, fontSize); + handleSaveRef.current(font, fontSize); }, 500), - [handleSave] + [] ); useEffect(() => { @@ -52,7 +55,7 @@ const Font = () => { } debouncedSave(codeFont, codeFontSize); return () => { - debouncedSave.cancel(); + debouncedSave.flush(); }; }, [codeFont, codeFontSize, debouncedSave]); diff --git a/packages/bruno-app/src/components/Preferences/General/index.js b/packages/bruno-app/src/components/Preferences/General/index.js index f8b8f112c..9310c415d 100644 --- a/packages/bruno-app/src/components/Preferences/General/index.js +++ b/packages/bruno-app/src/components/Preferences/General/index.js @@ -127,16 +127,19 @@ const General = () => { .catch((err) => console.log(err) && toast.error('Failed to update preferences')); }, [dispatch, preferences]); + const handleSaveRef = useRef(handleSave); + handleSaveRef.current = handleSave; + const debouncedSave = useCallback( debounce((values) => { preferencesSchema.validate(values, { abortEarly: true }) .then((validatedValues) => { - handleSave(validatedValues); + handleSaveRef.current(validatedValues); }) .catch((error) => { }); }, 500), - [handleSave] + [] ); useEffect(() => { @@ -144,7 +147,7 @@ const General = () => { debouncedSave(formik.values); } return () => { - debouncedSave.cancel(); + debouncedSave.flush(); }; }, [formik.values, formik.dirty, formik.isValid, debouncedSave]); diff --git a/packages/bruno-app/src/components/Preferences/ProxySettings/index.js b/packages/bruno-app/src/components/Preferences/ProxySettings/index.js index e80020872..63728e11f 100644 --- a/packages/bruno-app/src/components/Preferences/ProxySettings/index.js +++ b/packages/bruno-app/src/components/Preferences/ProxySettings/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback, useRef } from 'react'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import debounce from 'lodash/debounce'; @@ -75,11 +75,14 @@ const ProxySettings = ({ close }) => { }); }, [dispatch, preferences, proxySchema]); + const onUpdateRef = useRef(onUpdate); + onUpdateRef.current = onUpdate; + const debouncedSave = useCallback( debounce((values) => { - onUpdate(values); + onUpdateRef.current(values); }, 500), - [onUpdate] + [] ); const [passwordVisible, setPasswordVisible] = useState(false); @@ -107,7 +110,7 @@ const ProxySettings = ({ close }) => { debouncedSave(formik.values); } return () => { - debouncedSave.cancel(); + debouncedSave.flush(); }; }, [formik.values, formik.dirty, debouncedSave]); diff --git a/tests/preferences/tab-switch-persistence/tab-switch-persistence.spec.ts b/tests/preferences/tab-switch-persistence/tab-switch-persistence.spec.ts new file mode 100644 index 000000000..12f089922 --- /dev/null +++ b/tests/preferences/tab-switch-persistence/tab-switch-persistence.spec.ts @@ -0,0 +1,190 @@ +import { test, expect } from '../../../playwright'; + +test.describe('Preferences Tab Switch Persistence', () => { + test('should persist General tab SSL setting when immediately switching tabs', async ({ page }) => { + // Open preferences + await page.locator('.preferences-button').click(); + await page.getByRole('tab', { name: 'General' }).waitFor({ state: 'visible' }); + + // Navigate to General tab + await page.getByRole('tab', { name: 'General' }).click(); + await page.waitForTimeout(300); + + // Get the initial state of SSL verification checkbox + const sslCheckbox = page.locator('#sslVerification'); + await sslCheckbox.waitFor({ state: 'visible' }); + const initialChecked = await sslCheckbox.isChecked(); + + // Toggle the SSL verification checkbox + await sslCheckbox.click(); + + // Immediately switch to another tab (don't wait for debounce) + await page.getByRole('tab', { name: 'Themes' }).click(); + await page.waitForTimeout(100); + + // Switch back to General tab + await page.getByRole('tab', { name: 'General' }).click(); + await sslCheckbox.waitFor({ state: 'visible' }); + + // Verify the setting was persisted (should be opposite of initial state) + const newChecked = await sslCheckbox.isChecked(); + expect(newChecked).toBe(!initialChecked); + + // Restore original state + await sslCheckbox.click(); + await page.waitForTimeout(600); + }); + + test('should persist Store Cookies setting when immediately switching tabs', async ({ page }) => { + // Open preferences + await page.locator('.preferences-button').click(); + await page.getByRole('tab', { name: 'General' }).waitFor({ state: 'visible' }); + + // Navigate to General tab + await page.getByRole('tab', { name: 'General' }).click(); + await page.waitForTimeout(300); + + // Get the initial state of Store Cookies checkbox + const storeCookiesCheckbox = page.locator('#storeCookies'); + await storeCookiesCheckbox.waitFor({ state: 'visible' }); + const initialChecked = await storeCookiesCheckbox.isChecked(); + + // Toggle the checkbox + await storeCookiesCheckbox.click(); + + // Immediately switch to Themes tab (lighter than Proxy) + await page.getByRole('tab', { name: 'Themes' }).click(); + await page.waitForTimeout(100); + + // Switch back to General tab + await page.getByRole('tab', { name: 'General' }).click(); + await storeCookiesCheckbox.waitFor({ state: 'visible' }); + + // Verify the setting was persisted + const newChecked = await storeCookiesCheckbox.isChecked(); + expect(newChecked).toBe(!initialChecked); + + // Restore original state + await storeCookiesCheckbox.click(); + await page.waitForTimeout(600); + }); + + test('should persist Cache settings when immediately switching tabs', async ({ page }) => { + // Open preferences + await page.locator('.preferences-button').click(); + await page.getByRole('tab', { name: 'Cache' }).waitFor({ state: 'visible' }); + + // Navigate to Cache tab + await page.getByRole('tab', { name: 'Cache' }).click(); + await page.waitForTimeout(300); + + // Get the initial state of SSL session caching checkbox + const sslSessionCheckbox = page.locator('#sslSession\\.enabled'); + await sslSessionCheckbox.waitFor({ state: 'visible' }); + const initialChecked = await sslSessionCheckbox.isChecked(); + + // Toggle the checkbox + await sslSessionCheckbox.click(); + + // Immediately switch to another tab + await page.getByRole('tab', { name: 'Themes' }).click(); + await page.waitForTimeout(100); + + // Switch back to Cache tab + await page.getByRole('tab', { name: 'Cache' }).click(); + await sslSessionCheckbox.waitFor({ state: 'visible' }); + + // Verify the setting was persisted + const newChecked = await sslSessionCheckbox.isChecked(); + expect(newChecked).toBe(!initialChecked); + + // Restore original state + await sslSessionCheckbox.click(); + await page.waitForTimeout(600); + }); + + test('should persist settings after closing and reopening preferences tab', async ({ page }) => { + // Open preferences + await page.locator('.preferences-button').click(); + await page.getByRole('tab', { name: 'General' }).waitFor({ state: 'visible' }); + + // Navigate to General tab + await page.getByRole('tab', { name: 'General' }).click(); + await page.waitForTimeout(300); + + // Get the initial state of SSL verification checkbox + const sslCheckbox = page.locator('#sslVerification'); + await sslCheckbox.waitFor({ state: 'visible' }); + const initialChecked = await sslCheckbox.isChecked(); + + // Toggle the SSL verification checkbox + await sslCheckbox.click(); + + // Immediately close the preferences tab + const preferencesTab = page.locator('.request-tab').filter({ hasText: 'Preferences' }); + await preferencesTab.hover(); + await preferencesTab.locator('.close-icon').click({ force: true }); + + // Wait for preferences tab to close + await preferencesTab.waitFor({ state: 'hidden' }); + + // Reopen preferences + await page.locator('.preferences-button').click(); + await page.getByRole('tab', { name: 'General' }).waitFor({ state: 'visible' }); + + // Navigate to General tab + await page.getByRole('tab', { name: 'General' }).click(); + await sslCheckbox.waitFor({ state: 'visible' }); + + // Verify the setting was persisted + const newChecked = await sslCheckbox.isChecked(); + expect(newChecked).toBe(!initialChecked); + + // Restore original state + await sslCheckbox.click(); + await page.waitForTimeout(600); + }); + + test('should persist Cache settings after closing and reopening preferences', async ({ page }) => { + // Open preferences + await page.locator('.preferences-button').click(); + await page.getByRole('tab', { name: 'Cache' }).waitFor({ state: 'visible' }); + + // Navigate to Cache tab + await page.getByRole('tab', { name: 'Cache' }).click(); + await page.waitForTimeout(300); + + // Get the initial state of SSL session caching checkbox + const sslSessionCheckbox = page.locator('#sslSession\\.enabled'); + await sslSessionCheckbox.waitFor({ state: 'visible' }); + const initialCacheState = await sslSessionCheckbox.isChecked(); + + // Toggle the checkbox + await sslSessionCheckbox.click(); + + // Close preferences tab immediately + const preferencesTab = page.locator('.request-tab').filter({ hasText: 'Preferences' }); + await preferencesTab.hover(); + await preferencesTab.locator('.close-icon').click({ force: true }); + await preferencesTab.waitFor({ state: 'hidden' }); + + // Wait for save to complete + await page.waitForTimeout(300); + + // Reopen preferences + await page.locator('.preferences-button').click(); + await page.getByRole('tab', { name: 'Cache' }).waitFor({ state: 'visible' }); + + // Navigate to Cache tab + await page.getByRole('tab', { name: 'Cache' }).click(); + await page.waitForTimeout(300); + await sslSessionCheckbox.waitFor({ state: 'visible' }); + + // Verify the setting was persisted + expect(await sslSessionCheckbox.isChecked()).toBe(!initialCacheState); + + // Restore original state + await sslSessionCheckbox.click(); + await page.waitForTimeout(600); + }); +});