From 14966f6e6c1a965879e991b0aec0b45a033881f2 Mon Sep 17 00:00:00 2001 From: Jayakrishnan C N Date: Tue, 30 Sep 2025 13:27:20 +0530 Subject: [PATCH] feat: import multiple collections from a parent folder (#5431) * feat: import multiple collections from a parent folder * feat: open collections in parallel, revert plural labels, and update playwright tests --------- Co-authored-by: Bijin Bruno --- eslint.config.js | 1 + .../CreateOrOpenCollection/index.js | 5 +- .../src/components/Sidebar/TitleBar/index.js | 5 +- .../bruno-electron/src/app/collections.js | 39 +++++-- .../moving-requests/tag-persistence.spec.ts | 6 + .../open/open-multiple-collections.spec.ts | 108 ++++++++++++++++++ tests/utils/page/actions.ts | 11 ++ tests/utils/page/index.ts | 1 + .../actions.js => page/navigation.ts} | 0 tests/utils/pageUtils/index.js | 0 tests/utils/pageUtils/navigation.js | 0 11 files changed, 165 insertions(+), 11 deletions(-) create mode 100644 tests/collection/open/open-multiple-collections.spec.ts create mode 100644 tests/utils/page/actions.ts create mode 100644 tests/utils/page/index.ts rename tests/utils/{pageUtils/actions.js => page/navigation.ts} (100%) delete mode 100644 tests/utils/pageUtils/index.js delete mode 100644 tests/utils/pageUtils/navigation.js diff --git a/eslint.config.js b/eslint.config.js index 1e5a352b3..671b6f363 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -60,6 +60,7 @@ module.exports = runESMImports().then(() => defineConfig([ '@stylistic/function-call-spacing': ['error', 'never'], '@stylistic/multiline-ternary': ['off'], '@stylistic/padding-line-between-statements': ['off'], + '@stylistic/jsx-one-expression-per-line': ['off'], '@stylistic/semi-style': ['error', 'last'], '@stylistic/max-len': ['off'], }, diff --git a/packages/bruno-app/src/components/Sidebar/Collections/CreateOrOpenCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/CreateOrOpenCollection/index.js index 0feaa45c8..1de9084ab 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/CreateOrOpenCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/CreateOrOpenCollection/index.js @@ -19,7 +19,10 @@ const CreateOrOpenCollection = () => { const handleOpenCollection = () => { dispatch(openCollection()).catch( - (err) => console.log(err) && toast.error('An error occurred while opening the collection') + (err) => { + console.log(err); + toast.error('An error occurred while opening the collection'); + } ); }; const CreateLink = () => ( diff --git a/packages/bruno-app/src/components/Sidebar/TitleBar/index.js b/packages/bruno-app/src/components/Sidebar/TitleBar/index.js index f3ec920a2..4eb5dadec 100644 --- a/packages/bruno-app/src/components/Sidebar/TitleBar/index.js +++ b/packages/bruno-app/src/components/Sidebar/TitleBar/index.js @@ -55,7 +55,10 @@ const TitleBar = () => { const handleOpenCollection = () => { dispatch(openCollection()).catch( - (err) => console.log(err) && toast.error('An error occurred while opening the collection') + (err) => { + console.log(err); + toast.error('An error occurred while opening the collection'); + } ); }; diff --git a/packages/bruno-electron/src/app/collections.js b/packages/bruno-electron/src/app/collections.js index a6b7a178c..46ea976ef 100644 --- a/packages/bruno-electron/src/app/collections.js +++ b/packages/bruno-electron/src/app/collections.js @@ -42,15 +42,36 @@ const getCollectionConfigFile = async (pathname) => { }; const openCollectionDialog = async (win, watcher) => { - const { filePaths } = await dialog.showOpenDialog(win, { - properties: ['openDirectory', 'createDirectory'] + const { canceled, filePaths } = await dialog.showOpenDialog(win, { + properties: ['openDirectory', 'createDirectory', 'multiSelections'] }); - if (filePaths && filePaths[0]) { - const resolvedPath = path.resolve(filePaths[0]); - if (isDirectory(resolvedPath)) { - openCollection(win, watcher, resolvedPath); - } else { - console.error(`[ERROR] Cannot open unknown folder: "${resolvedPath}"`); + + if (!canceled && filePaths?.length > 0) { + // Using Set to remove duplicates + const { openCollectionPromises, invalidPaths } = [...new Set(filePaths)].reduce((acc, filePath) => { + const resolvedPath = path.resolve(filePath); + + if (isDirectory(resolvedPath)) { + // Open each valid collection in parallel + acc.openCollectionPromises.push(openCollection(win, watcher, resolvedPath).catch((err) => { + console.error(`[ERROR] Failed to open collection at "${resolvedPath}":`, err.message); + return { error: err, path: resolvedPath }; + })); + } else { + acc.invalidPaths.push(resolvedPath); + console.error(`[ERROR] Cannot open unknown folder: "${resolvedPath}"`); + } + + return acc; + }, + { openCollectionPromises: [], invalidPaths: [] }); + + // Wait for all valid collections to be opened + await Promise.all(openCollectionPromises); + + // Notify about any invalid paths + if (invalidPaths.length > 0) { + win.webContents.send('main:display-error', `Some selected folders could not be opened: ${invalidPaths.join(', ')}`); } } }; @@ -78,7 +99,7 @@ const openCollection = async (win, watcher, collectionPath, options = {}) => { } catch (err) { if (!options.dontSendDisplayErrors) { win.webContents.send('main:display-error', { - error: err.message || 'An error occurred while opening the local collection' + message: err.message || 'An error occurred while opening the local collection' }); } } diff --git a/tests/collection/moving-requests/tag-persistence.spec.ts b/tests/collection/moving-requests/tag-persistence.spec.ts index 94ae26526..c96a6f50b 100644 --- a/tests/collection/moving-requests/tag-persistence.spec.ts +++ b/tests/collection/moving-requests/tag-persistence.spec.ts @@ -1,6 +1,12 @@ import { test, expect } from '../../../playwright'; +import { closeAllCollections } from '../../utils/page'; test.describe('Tag persistence', () => { + test.afterAll(async ({ pageWithUserData: page }) => { + // cleanup: close all collections + await closeAllCollections(page); + }); + test('Verify tag persistence while moving requests within a collection', async ({ pageWithUserData: page, createTmpDir }) => { // Create first collection - click dropdown menu first await page.getByLabel('Create Collection').click(); diff --git a/tests/collection/open/open-multiple-collections.spec.ts b/tests/collection/open/open-multiple-collections.spec.ts new file mode 100644 index 000000000..2647d6ef3 --- /dev/null +++ b/tests/collection/open/open-multiple-collections.spec.ts @@ -0,0 +1,108 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; +import * as fs from 'fs'; + +import { closeAllCollections } from '../../utils/page'; + +test.describe('Open Multiple Collections', () => { + let originalShowOpenDialog; + + test.beforeAll(async ({ electronApp }) => { + // save the original showOpenDialog function + await electronApp.evaluate(({ dialog }) => { + originalShowOpenDialog = dialog.showOpenDialog; + }); + }); + + test.afterAll(async ({ electronApp }) => { + // restore the original showOpenDialog function + await electronApp.evaluate(({ dialog }) => { + dialog.showOpenDialog = originalShowOpenDialog; + }); + }); + + test('Should open multiple collections using Open Collection feature', async ({ + page, + electronApp, + createTmpDir + }) => { + // Create two test collections with proper bruno.json files + const collection1Dir = await createTmpDir('collection-1'); + const collection2Dir = await createTmpDir('collection-2'); + + // Create bruno.json for first collection + const collection1Config = { + version: '1', + name: 'Test Collection 1', + type: 'collection' + }; + // Create bruno.json for second collection + const collection2Config = { + version: '1', + name: 'Test Collection 2', + type: 'collection' + }; + + fs.writeFileSync(path.join(collection1Dir, 'bruno.json'), JSON.stringify(collection1Config, null, 2)); + fs.writeFileSync(path.join(collection2Dir, 'bruno.json'), JSON.stringify(collection2Config, null, 2)); + + // Mock the electron dialog to return multiple folder selections + await electronApp.evaluate(({ dialog }, { collection1Dir, collection2Dir }) => { + dialog.showOpenDialog = async () => ({ + canceled: false, + filePaths: [collection1Dir, collection2Dir] + }); + }, + { collection1Dir, collection2Dir }); + + await expect(page.locator('#sidebar-collection-name').getByText('Test Collection 1')).not.toBeVisible(); + + // Click on Open Collection(s) button + await page.getByRole('button', { name: 'Open Collection' }).click(); + + // Wait for both collections to appear in the sidebar + const collection1Element = page.locator('#sidebar-collection-name').getByText('Test Collection 1'); + const collection2Element = page.locator('#sidebar-collection-name').getByText('Test Collection 2'); + + await expect(collection1Element).toBeVisible(); + await expect(collection2Element).toBeVisible(); + + // cleanup: close all collections + await closeAllCollections(page); + }); + + test('Should handle invalid collection path and display error', async ({ + page, + electronApp, + createTmpDir + }) => { + // Directory without bruno.json file + const collection1Dir = await createTmpDir('collection-1'); + const collection2Dir = 'invalid-collection-path'; + + // Mock the electron dialog to return multiple folder selections + await electronApp.evaluate(({ dialog }, { collection1Dir, collection2Dir }) => { + dialog.showOpenDialog = async () => ({ + canceled: false, + filePaths: [collection1Dir, collection2Dir] + }); + }, + { collection1Dir, collection2Dir }); + + await expect(page.locator('#sidebar-collection-name').getByText('Test Collection 1')).not.toBeVisible(); + + // Click on Open Collection(s) button + await page.getByRole('button', { name: 'Open Collection' }).click(); + + // Verify no collections were opened + await expect(page.locator('#sidebar-collection-name')).toHaveCount(0); + + // Verify invalid collection error + const invalidCollectionError = page.getByText('The collection is not valid (bruno.json not found)').first(); + await expect(invalidCollectionError).toBeVisible(); + + // Verify invalid path error + const invalidPathError = page.getByText('Some selected folders could not be opened').getByText('invalid-collection-path').first(); + await expect(invalidPathError).toBeVisible(); + }); +}); diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts new file mode 100644 index 000000000..e9f970488 --- /dev/null +++ b/tests/utils/page/actions.ts @@ -0,0 +1,11 @@ +const closeAllCollections = async (page) => { + const numberOfCollections = await page.locator('.collection-name').count(); + + for (let i = 0; i < numberOfCollections; i++) { + await page.locator('.collection-name').first().locator('.collection-actions').click(); + await page.locator('.dropdown-item').getByText('Close').click(); + await page.getByRole('button', { name: 'Close' }).click(); + } +}; + +export { closeAllCollections }; diff --git a/tests/utils/page/index.ts b/tests/utils/page/index.ts new file mode 100644 index 000000000..485f1b10a --- /dev/null +++ b/tests/utils/page/index.ts @@ -0,0 +1 @@ +export * from './actions'; diff --git a/tests/utils/pageUtils/actions.js b/tests/utils/page/navigation.ts similarity index 100% rename from tests/utils/pageUtils/actions.js rename to tests/utils/page/navigation.ts diff --git a/tests/utils/pageUtils/index.js b/tests/utils/pageUtils/index.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/utils/pageUtils/navigation.js b/tests/utils/pageUtils/navigation.js deleted file mode 100644 index e69de29bb..000000000