From f05bb9c49d2be0fa75ed2cb459be8dbaba4695a3 Mon Sep 17 00:00:00 2001 From: Sid Date: Fri, 12 Jun 2026 19:13:34 +0530 Subject: [PATCH] fix(snapshot): folder nested script tab interactivity and tests (#8225) * fix(snapshot): folder script interactivity * fix: add tests for collection scripts --- .../components/FolderSettings/Script/index.js | 6 +- .../RequestTabs/RequestTab/index.js | 3 +- .../src/providers/ReduxStore/slices/tabs.js | 3 + tests/snapshots/collection.spec.ts | 135 ++++++++++++++++++ tests/snapshots/folder.spec.ts | 82 +++++++---- tests/utils/page/actions.ts | 101 +++++++++++++ tests/utils/page/index.ts | 1 + tests/utils/page/locators.ts | 3 + tests/utils/snapshot.ts | 43 ++++++ 9 files changed, 343 insertions(+), 34 deletions(-) create mode 100644 tests/snapshots/collection.spec.ts create mode 100644 tests/utils/snapshot.ts diff --git a/packages/bruno-app/src/components/FolderSettings/Script/index.js b/packages/bruno-app/src/components/FolderSettings/Script/index.js index 61b0af6e8..3458fe442 100644 --- a/packages/bruno-app/src/components/FolderSettings/Script/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Script/index.js @@ -22,7 +22,9 @@ const Script = ({ collection, folder }) => { const responseScript = folder.draft ? get(folder, 'draft.request.script.res', '') : get(folder, 'root.request.script.res', ''); const tabs = useSelector((state) => state.tabs.tabs); - const focusedTab = find(tabs, (t) => t.uid === folder.uid); + const focusedTab = find(tabs, (tab) => tab.type === 'folder-settings' && (tab.uid === folder.uid || tab.folderUid === folder.uid)) + || find(tabs, (tab) => tab.type === 'folder-settings' && tab.pathname === folder.pathname); + const tabUid = focusedTab?.uid || folder.uid; const scriptPaneTab = focusedTab?.scriptPaneTab; // Default to post-response if pre-request script is empty (only when scriptPaneTab is null/undefined) @@ -34,7 +36,7 @@ const Script = ({ collection, folder }) => { const activeTab = scriptPaneTab || getDefaultTab(); const setActiveTab = (tab) => { - dispatch(updateScriptPaneTab({ uid: folder.uid, scriptPaneTab: tab })); + dispatch(updateScriptPaneTab({ uid: tabUid, scriptPaneTab: tab })); }; const { displayedTheme } = useTheme(); diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js index bd00be092..81ed90e12 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -52,8 +52,9 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi || tab.type === 'graphql-request' || tab.type === 'grpc-request' || tab.type === 'ws-request'; + const shouldSyncUid = isRequestType || tab.type === 'folder-settings'; - if (!isRequestType || !tab.pathname || !item?.uid || tab.uid === item.uid) { + if (!shouldSyncUid || !tab.pathname || !item?.uid || tab.uid === item.uid) { return; } diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js index afac7998a..3c38b277f 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js @@ -446,6 +446,9 @@ export const tabsSlice = createSlice({ const tab = find(state.tabs, (t) => t.uid === oldUid); if (tab) { tab.uid = newUid; + if (tab.type === 'folder-settings') { + tab.folderUid = newUid; + } if (state.activeTabUid === oldUid) { state.activeTabUid = newUid; } diff --git a/tests/snapshots/collection.spec.ts b/tests/snapshots/collection.spec.ts new file mode 100644 index 000000000..38bf2e5d4 --- /dev/null +++ b/tests/snapshots/collection.spec.ts @@ -0,0 +1,135 @@ +import { test, expect, closeElectronApp } from '../../playwright'; +import { + createCollection, + createWorkspace, + focusCollectionSettingsTab, + openCollectionSettings, + readSnapshot, + findSnapshotCollectionTab, + selectCollectionPaneTab, + selectCollectionScriptPaneTab, + switchWorkspace, + waitForReadyPage, + waitForSnapshotFile +} from '../utils/page'; + +test.describe('Snapshot: collection Pane Interactivity', () => { + test('collection pane tab interactivity is preserved after workspace switch', async ({ launchElectronApp, createTmpDir }) => { + const userDataPath = await createTmpDir('snap-collection-workspace-switch'); + const colPath = await createTmpDir('col'); + + const app = await launchElectronApp({ userDataPath }); + const page = await waitForReadyPage(app); + + await test.step('Create collection and open collection settings', async () => { + await createCollection(page, 'TestCol', colPath); + await openCollectionSettings(page, 'TestCol', { persist: true }); + await selectCollectionPaneTab(page, 'auth'); + }); + + await test.step('Switch to a new workspace', async () => { + // Background flushing takes about 2 seconds to complete + await page.waitForTimeout(2000); + await createWorkspace(page, 'SecondWorkspace'); + await expect(page.getByTestId('workspace-name')).toHaveText('SecondWorkspace', { timeout: 5000 }); + }); + + await test.step('Switch back to original workspace and verify collection pane interactivity', async () => { + await switchWorkspace(page, 'My Workspace'); + await openCollectionSettings(page, 'TestCol', { persist: true }); + + await focusCollectionSettingsTab(page); + + await selectCollectionPaneTab(page, 'auth'); + await selectCollectionPaneTab(page, 'headers'); + await selectCollectionPaneTab(page, 'overview'); + await selectCollectionPaneTab(page, 'script'); + await selectCollectionPaneTab(page, 'vars'); + }); + + await closeElectronApp(app); + }); + + test('collection pane tab interactivity is preserved after app restart', async ({ launchElectronApp, createTmpDir }) => { + const userDataPath = await createTmpDir('snap-collection-restart'); + const colPath = await createTmpDir('col'); + + const app = await launchElectronApp({ userDataPath }); + const page = await waitForReadyPage(app); + + await test.step('Create collection and open collection settings on auth tab', async () => { + await createCollection(page, 'TestCol', colPath); + await openCollectionSettings(page, 'TestCol', { persist: true }); + await selectCollectionPaneTab(page, 'auth'); + }); + + await test.step('Close app and verify snapshot stores collection-settings tab', async () => { + await page.waitForTimeout(2000); + await closeElectronApp(app); + + await waitForSnapshotFile(userDataPath); + + const snapshot = readSnapshot(userDataPath); + const tab = findSnapshotCollectionTab(snapshot, colPath); + expect(tab).toBeTruthy(); + expect(tab.type).toBe('collection-settings'); + expect(tab.permanent).toBe(true); + }); + + await test.step('Restart app and verify collection pane interactivity is restored', async () => { + const app2 = await launchElectronApp({ userDataPath }); + const page2 = await waitForReadyPage(app2); + + await focusCollectionSettingsTab(page2, { timeout: 15000 }); + + await selectCollectionPaneTab(page2, 'auth'); + await selectCollectionPaneTab(page2, 'headers'); + await selectCollectionPaneTab(page2, 'overview'); + await selectCollectionPaneTab(page2, 'script'); + await selectCollectionPaneTab(page2, 'vars'); + + await closeElectronApp(app2); + }); + }); + + test('collection script\'s tabs need to be interactive after app restart', async ({ launchElectronApp, createTmpDir }) => { + const userDataPath = await createTmpDir('snap-collection-restart'); + const colPath = await createTmpDir('col'); + + const app = await launchElectronApp({ userDataPath }); + const page = await waitForReadyPage(app); + + await test.step('Create collection and open collection settings on script tab', async () => { + await createCollection(page, 'TestCol', colPath); + await openCollectionSettings(page, 'TestCol', { persist: true }); + await selectCollectionPaneTab(page, 'script'); + }); + + await test.step('Close app and verify snapshot stores collection-settings tab', async () => { + await page.waitForTimeout(2000); + await closeElectronApp(app); + + await waitForSnapshotFile(userDataPath); + + const snapshot = readSnapshot(userDataPath); + const tab = findSnapshotCollectionTab(snapshot, colPath); + expect(tab).toBeTruthy(); + expect(tab.type).toBe('collection-settings'); + expect(tab.permanent).toBe(true); + }); + + await test.step('Restart app and verify collection script pane is interactive', async () => { + const app2 = await launchElectronApp({ userDataPath }); + const page2 = await waitForReadyPage(app2); + + await focusCollectionSettingsTab(page2, { timeout: 15000 }); + + await selectCollectionPaneTab(page2, 'script'); + + await selectCollectionScriptPaneTab(page2, 'pre-request'); + await selectCollectionScriptPaneTab(page2, 'post-response'); + + await closeElectronApp(app2); + }); + }); +}); diff --git a/tests/snapshots/folder.spec.ts b/tests/snapshots/folder.spec.ts index c45ed9a2d..4a4faf392 100644 --- a/tests/snapshots/folder.spec.ts +++ b/tests/snapshots/folder.spec.ts @@ -1,34 +1,18 @@ -import path from 'path'; -import fs from 'fs'; import { test, expect, closeElectronApp } from '../../playwright'; import { createCollection, createFolder, createWorkspace, + focusFolderSettingsTab, openfolder, + readSnapshot, + findSnapshotFolderTab, selectfolderPaneTab, + selectFolderScriptPaneTab, switchWorkspace, - waitForReadyPage + waitForReadyPage, + waitForSnapshotFile } from '../utils/page'; -import { buildCommonLocators } from '../utils/page/locators'; - -const readSnapshot = (userDataPath: string) => { - const snapshotPath = path.join(userDataPath, 'ui-state-snapshot.json'); - if (!fs.existsSync(snapshotPath)) return null; - return JSON.parse(fs.readFileSync(snapshotPath, 'utf-8')); -}; - -const findSnapshotFolderTab = (snapshot: any, folderName: string) => { - if (!snapshot || !Array.isArray(snapshot.collections)) return null; - for (const collection of snapshot.collections) { - if (!Array.isArray(collection?.tabs)) continue; - const tab = collection.tabs.find( - (t: any) => t?.type === 'folder-settings' && typeof t?.pathname === 'string' && t.pathname.includes(folderName) - ); - if (tab) return tab; - } - return null; -}; test.describe('Snapshot: folder Pane Interactivity', () => { test('folder pane tab interactivity is preserved after workspace switch', async ({ launchElectronApp, createTmpDir }) => { @@ -55,10 +39,7 @@ test.describe('Snapshot: folder Pane Interactivity', () => { await switchWorkspace(page, 'My Workspace'); await openfolder(page, 'TestCol', 'TestFolder', { persist: true }); - const locators = buildCommonLocators(page); - - await expect(locators.tabs.folderTab('TestFolder')).toBeVisible({ timeout: 10000 }); - await locators.tabs.folderTab('TestFolder').click({ force: true }); + await focusFolderSettingsTab(page, 'TestFolder'); await selectfolderPaneTab(page, 'auth'); await selectfolderPaneTab(page, 'headers'); @@ -88,8 +69,7 @@ test.describe('Snapshot: folder Pane Interactivity', () => { await page.waitForTimeout(2000); await closeElectronApp(app); - const snapshotPath = path.join(userDataPath, 'ui-state-snapshot.json'); - await expect.poll(() => fs.existsSync(snapshotPath)).toBe(true); + await waitForSnapshotFile(userDataPath); const snapshot = readSnapshot(userDataPath); const tab = findSnapshotFolderTab(snapshot, 'TestFolder'); @@ -102,9 +82,7 @@ test.describe('Snapshot: folder Pane Interactivity', () => { const app2 = await launchElectronApp({ userDataPath }); const page2 = await waitForReadyPage(app2); - const locators = buildCommonLocators(page2); - await expect(locators.tabs.folderTab('TestFolder')).toBeVisible({ timeout: 15000 }); - await locators.tabs.folderTab('TestFolder').click({ force: true }); + await focusFolderSettingsTab(page2, 'TestFolder', { timeout: 15000 }); await selectfolderPaneTab(page2, 'auth'); await selectfolderPaneTab(page2, 'headers'); @@ -115,4 +93,46 @@ test.describe('Snapshot: folder Pane Interactivity', () => { await closeElectronApp(app2); }); }); + + test('folder script\'s tabs need to be interactive after app restart', async ({ launchElectronApp, createTmpDir }) => { + const userDataPath = await createTmpDir('snap-folder-restart'); + const colPath = await createTmpDir('col'); + + const app = await launchElectronApp({ userDataPath }); + const page = await waitForReadyPage(app); + + await test.step('Create collection and folder, open folder settings on auth tab', async () => { + await createCollection(page, 'TestCol', colPath); + await createFolder(page, 'TestFolder', 'TestCol'); + await openfolder(page, 'TestCol', 'TestFolder', { persist: true }); + await selectfolderPaneTab(page, 'script'); + }); + + await test.step('Close app and verify snapshot stores folder-settings tab', async () => { + await page.waitForTimeout(2000); + await closeElectronApp(app); + + await waitForSnapshotFile(userDataPath); + + const snapshot = readSnapshot(userDataPath); + const tab = findSnapshotFolderTab(snapshot, 'TestFolder'); + expect(tab).toBeTruthy(); + expect(tab.type).toBe('folder-settings'); + expect(tab.permanent).toBe(true); + }); + + await test.step('Restart app and verify folder script pane is interactive', async () => { + const app2 = await launchElectronApp({ userDataPath }); + const page2 = await waitForReadyPage(app2); + + await focusFolderSettingsTab(page2, 'TestFolder', { timeout: 15000 }); + + await selectfolderPaneTab(page2, 'script'); + + await selectFolderScriptPaneTab(page2, 'pre-request'); + await selectFolderScriptPaneTab(page2, 'post-response'); + + await closeElectronApp(app2); + }); + }); }); diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts index f9874f88d..5e476f357 100644 --- a/tests/utils/page/actions.ts +++ b/tests/utils/page/actions.ts @@ -857,6 +857,101 @@ const selectfolderPaneTab = async (page: Page, tabName: string) => { }); }; +/** + * Select a sub-tab in the folder script pane (Pre Request or Post Response) + * @param page - The page object + * @param tabName - 'pre-request' or 'post-response' + * @returns void + */ +const selectFolderScriptPaneTab = async (page: Page, tabName: 'pre-request' | 'post-response') => { + await test.step(`Select folder script pane tab "${tabName}"`, async () => { + const locators = buildCommonLocators(page); + const tab = locators.paneTabs.folderScriptTab(tabName); + await tab.click(); + await expect(tab).toContainClass('active'); + }); +}; + +/** + * Open a collection's settings tab by clicking on it in the sidebar + * @param page - The page object + * @param collectionName - The name of the collection + * @param options - Optional settings (persist: double-click to make tab permanent) + * @returns void + */ +const openCollectionSettings = async (page: Page, collectionName: string, { persist = false } = {}) => { + await test.step(`Open collection settings for "${collectionName}"`, async () => { + const locators = buildCommonLocators(page); + const collection = locators.sidebar.collection(collectionName); + if (!persist) { + await collection.click(); + } else { + await collection.dblclick(); + } + }); +}; + +/** + * Select a tab in the collection settings pane + * @param page - The page object + * @param tabName - The tab name key (e.g. 'auth', 'headers', 'overview', 'script', 'vars') + * @returns void + */ +const selectCollectionPaneTab = async (page: Page, tabName: string) => { + await test.step(`Select collection pane tab "${tabName}"`, async () => { + const locators = buildCommonLocators(page); + const tab = locators.paneTabs.collectionSettingsTab(tabName.toLowerCase()); + await tab.click(); + await expect(tab).toContainClass('active'); + }); +}; + +/** + * Select a sub-tab in the collection script pane (Pre Request or Post Response) + * @param page - The page object + * @param tabName - 'pre-request' or 'post-response' + * @returns void + */ +const selectCollectionScriptPaneTab = async (page: Page, tabName: 'pre-request' | 'post-response') => { + await test.step(`Select collection script pane tab "${tabName}"`, async () => { + const locators = buildCommonLocators(page); + const tab = locators.paneTabs.tabTrigger(tabName); + await tab.click(); + await expect(tab).toContainClass('active'); + }); +}; + +/** + * Focus the folder settings tab in the tab bar after restore + * @param page - The page object + * @param folderName - The name of the folder + * @param options - Optional timeout in milliseconds + * @returns void + */ +const focusFolderSettingsTab = async (page: Page, folderName: string, { timeout = 10000 } = {}) => { + await test.step(`Focus folder settings tab "${folderName}"`, async () => { + const locators = buildCommonLocators(page); + const tab = locators.tabs.folderTab(folderName); + await expect(tab).toBeVisible({ timeout }); + await tab.click({ force: true }); + }); +}; + +/** + * Focus the collection settings tab in the tab bar after restore + * @param page - The page object + * @param options - Optional timeout in milliseconds + * @returns void + */ +const focusCollectionSettingsTab = async (page: Page, { timeout = 10000 } = {}) => { + await test.step('Focus collection settings tab', async () => { + const locators = buildCommonLocators(page); + const tab = locators.tabs.collectionSettingsTab(); + await expect(tab).toBeVisible({ timeout }); + await tab.click({ force: true }); + }); +}; + /** * Open a request within a folder * @param page - The page object @@ -1667,6 +1762,12 @@ export { openfolder, openFolderRequest, selectfolderPaneTab, + selectFolderScriptPaneTab, + openCollectionSettings, + selectCollectionPaneTab, + selectCollectionScriptPaneTab, + focusFolderSettingsTab, + focusCollectionSettingsTab, getResponseBody, expectResponseContains, selectRequestPaneTab, diff --git a/tests/utils/page/index.ts b/tests/utils/page/index.ts index 5eecb558f..bf315f9f9 100644 --- a/tests/utils/page/index.ts +++ b/tests/utils/page/index.ts @@ -1,3 +1,4 @@ export * from './actions'; export * from './runner'; export * from './locators'; +export * from '../snapshot'; diff --git a/tests/utils/page/locators.ts b/tests/utils/page/locators.ts index 7a2d4c4c1..c385f39da 100644 --- a/tests/utils/page/locators.ts +++ b/tests/utils/page/locators.ts @@ -38,6 +38,8 @@ export const buildCommonLocators = (page: Page) => ({ tabs: { requestTab: (requestName: string) => page.locator('.request-tab .tab-label').filter({ hasText: requestName }), folderTab: (folderName: string) => page.locator('.request-tab .tab-label').filter({ hasText: folderName }), + collectionSettingsTab: () => + page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) }), activeRequestTab: () => page.locator('.request-tab.active'), closeTab: (requestName: string) => page.locator('.request-tab').filter({ hasText: requestName }).getByTestId('request-tab-close-icon'), draftIndicator: () => page.locator('.request-tab.active .has-changes-icon') @@ -46,6 +48,7 @@ export const buildCommonLocators = (page: Page) => ({ responsiveTab: (key: string) => page.getByTestId(`responsive-tab-${key}`), collectionSettingsTab: (key: string) => page.getByTestId(`collection-settings-tab-${key}`), folderSettingsTab: (key: string) => page.getByTestId(`folder-settings-tab-${key}`), + folderScriptTab: (key: 'pre-request' | 'post-response') => page.getByTestId(`tab-trigger-${key}`), tabTrigger: (key: string) => page.getByTestId(`tab-trigger-${key}`) }, folder: { diff --git a/tests/utils/snapshot.ts b/tests/utils/snapshot.ts new file mode 100644 index 000000000..64df1f401 --- /dev/null +++ b/tests/utils/snapshot.ts @@ -0,0 +1,43 @@ +import path from 'path'; +import fs from 'fs'; +import { expect } from '../../playwright'; + +export const getSnapshotPath = (userDataPath: string) => + path.join(userDataPath, 'ui-state-snapshot.json'); + +/** + * Read the snapshot JSON from the user data directory. + * electron-store saves it as `ui-state-snapshot.json`. + */ +export const readSnapshot = (userDataPath: string) => { + const snapshotPath = getSnapshotPath(userDataPath); + if (!fs.existsSync(snapshotPath)) return null; + return JSON.parse(fs.readFileSync(snapshotPath, 'utf-8')); +}; + +export const waitForSnapshotFile = async (userDataPath: string) => { + await expect.poll(() => fs.existsSync(getSnapshotPath(userDataPath))).toBe(true); +}; + +export const findSnapshotFolderTab = (snapshot: any, folderName: string) => { + if (!snapshot || !Array.isArray(snapshot.collections)) return null; + for (const collection of snapshot.collections) { + if (!Array.isArray(collection?.tabs)) continue; + const tab = collection.tabs.find( + (t: any) => t?.type === 'folder-settings' && typeof t?.pathname === 'string' && t.pathname.includes(folderName) + ); + if (tab) return tab; + } + return null; +}; + +export const findSnapshotCollectionTab = (snapshot: any, collectionPath: string) => { + if (!snapshot || !Array.isArray(snapshot.collections)) return null; + for (const collection of snapshot.collections) { + if (collectionPath && collection?.pathname && !collection.pathname.includes(collectionPath)) continue; + if (!Array.isArray(collection?.tabs)) continue; + const tab = collection.tabs.find((t: any) => t?.type === 'collection-settings'); + if (tab) return tab; + } + return null; +};