diff --git a/packages/bruno-app/src/components/Icons/CloseAll/index.js b/packages/bruno-app/src/components/Icons/CloseAll/index.js new file mode 100644 index 000000000..83669ee31 --- /dev/null +++ b/packages/bruno-app/src/components/Icons/CloseAll/index.js @@ -0,0 +1,24 @@ +import React from 'react'; + +const CloseAllIcon = ({ size = 18, strokeWidth = 1.5, className = '', ...props }) => { + return ( + + + + + ); +}; + +export default CloseAllIcon; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/CollectionsHeader/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/CollectionsHeader/StyledWrapper.js new file mode 100644 index 000000000..ef11bec64 --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/CollectionsHeader/StyledWrapper.js @@ -0,0 +1,24 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + .collections-badge { + margin-inline: 0.5rem; + background-color: ${(props) => props.theme.sidebar.badge.bg}; + border-radius: 5px; + + .caret { + margin-left: 0.25rem; + color: rgb(140, 140, 140); + fill: rgb(140, 140, 140); + } + + .collections-header-actions { + .collection-action-button { + opacity: 0; + transition: opacity 0.2s ease-in-out; + } + } + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/CollectionsHeader/index.js b/packages/bruno-app/src/components/Sidebar/Collections/CollectionsHeader/index.js new file mode 100644 index 000000000..935239723 --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/CollectionsHeader/index.js @@ -0,0 +1,86 @@ +import { useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { IconArrowsSort, IconFolders, IconSortAscendingLetters, IconSortDescendingLetters } from '@tabler/icons'; +import CloseAllIcon from 'components/Icons/CloseAll'; +import { sortCollections } from 'providers/ReduxStore/slices/collections/index'; +import RemoveCollectionsModal from '../RemoveCollectionsModal'; +import StyledWrapper from './StyledWrapper'; + +const CollectionsHeader = () => { + const dispatch = useDispatch(); + const { collections } = useSelector((state) => state.collections); + const { collectionSortOrder } = useSelector((state) => state.collections); + const [collectionsToClose, setCollectionsToClose] = useState([]); + + const sortCollectionOrder = () => { + let order; + switch (collectionSortOrder) { + case 'default': + order = 'alphabetical'; + break; + case 'alphabetical': + order = 'reverseAlphabetical'; + break; + case 'reverseAlphabetical': + order = 'default'; + break; + } + dispatch(sortCollections({ order })); + }; + + let sortIcon; + if (collectionSortOrder === 'default') { + sortIcon = ; + } else if (collectionSortOrder === 'alphabetical') { + sortIcon = ; + } else { + sortIcon = ; + } + + const selectAllCollectionsToClose = () => { + setCollectionsToClose(collections.map((c) => c.uid)); + }; + + const clearCollectionsToClose = () => { + setCollectionsToClose([]); + }; + + return ( + +
+
+ + + + Collections +
+ {collections.length >= 1 && ( +
+ + + {collectionsToClose.length > 0 && ( + + )} +
+ )} +
+
+ ); +}; + +export default CollectionsHeader; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/RemoveCollectionsModal/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/RemoveCollectionsModal/StyledWrapper.js new file mode 100644 index 000000000..54ef650cb --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/RemoveCollectionsModal/StyledWrapper.js @@ -0,0 +1,60 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + width: 600px; + overflow: hidden; + box-sizing: border-box; + + .collections-list-container { + width: 100%; + max-height: 150px; + overflow-y: auto; + overflow-x: hidden; + padding: 4px 0; + box-sizing: border-box; + } + + .collections-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + width: 100%; + } + + .collection-tag { + display: inline-flex; + align-items: center; + padding: 6px 12px; + background-color: ${(props) => props.theme.requestTabs.active.bg}; + border: 1px solid ${(props) => props.theme.requestTabs.bottomBorder}; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + color: ${(props) => props.theme.text}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .collection-tag-text { + display: inline-block; + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .show-more-link, + .show-less-link { + display: inline-flex; + align-items: center; + + &:hover { + span { + text-decoration: underline; + } + } + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/RemoveCollectionsModal/index.js b/packages/bruno-app/src/components/Sidebar/Collections/RemoveCollectionsModal/index.js new file mode 100644 index 000000000..ba12cc6e4 --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/RemoveCollectionsModal/index.js @@ -0,0 +1,273 @@ +import React, { useState, useMemo } from 'react'; +import toast from 'react-hot-toast'; +import { useDispatch, useSelector } from 'react-redux'; +import { filter, groupBy } from 'lodash'; +import Modal from 'components/Modal'; +import Portal from 'components/Portal'; +import { + removeCollection, + saveMultipleRequests, + saveMultipleCollections, + saveMultipleFolders +} from 'providers/ReduxStore/slices/collections/actions'; +import { + findCollectionByUid, + flattenItems, + isItemARequest, + isItemAFolder, + hasRequestChanges +} from 'utils/collections/index'; +import { IconAlertTriangle } from '@tabler/icons'; +import StyledWrapper from './StyledWrapper'; + +const MAX_COLLECTIONS_WIDTH = 530; +const CHARACTER_WIDTH = 8; +const COLLECTION_PADDING = 24; +const COLLECTION_GAP = 12; + +const getDisplayItems = (items, maxWidth = MAX_COLLECTIONS_WIDTH) => { + const visibleItems = []; + let totalWidth = 0; + + for (let i = 0; i < items.length; i += 1) { + const currentItem = items[i]; + const name = typeof currentItem === 'string' ? currentItem : currentItem?.name || ''; + const width = name.length * CHARACTER_WIDTH + COLLECTION_PADDING + COLLECTION_GAP; + + if (i === 0 || totalWidth + width <= maxWidth) { + totalWidth += width; + visibleItems.push(currentItem); + } else { + break; + } + } + + return visibleItems; +}; + +const RemoveCollectionsModal = ({ collectionUids, onClose }) => { + const dispatch = useDispatch(); + const allCollections = useSelector((state) => state.collections.collections || []); + const [showAllCollections, setShowAllCollections] = useState(false); + + const allDrafts = useMemo(() => { + const requestDrafts = []; + const collectionDrafts = []; + const folderDrafts = []; + + collectionUids.forEach((collectionUid) => { + const collection = findCollectionByUid(allCollections, collectionUid); + if (!collection) { + return; + } + + // Check for collection draft + if (collection.draft) { + collectionDrafts.push({ + name: collection.name, + collectionUid: collectionUid + }); + } + + // Check for request and folder drafts + const items = flattenItems(collection.items); + + // Request drafts + const unsavedRequests = filter(items, (item) => isItemARequest(item) && hasRequestChanges(item)); + unsavedRequests.forEach((request) => { + requestDrafts.push({ + ...request, + collectionUid: collectionUid + }); + }); + + // Folder drafts + const unsavedFolders = filter(items, (item) => isItemAFolder(item) && item.draft); + unsavedFolders.forEach((folder) => { + folderDrafts.push({ + name: folder.name, + folderUid: folder.uid, + collectionUid: collectionUid + }); + }); + }); + + return { requestDrafts, collectionDrafts, folderDrafts }; + }, [collectionUids, allCollections]); + + const collectionsWithUnsavedChanges = useMemo(() => { + const allDraftTypes = [...allDrafts.collectionDrafts, ...allDrafts.folderDrafts, ...allDrafts.requestDrafts]; + const draftsByCollection = groupBy(allDraftTypes, 'collectionUid'); + return Object.keys(draftsByCollection) + .map((collectionUid) => { + const collection = findCollectionByUid(allCollections, collectionUid); + return collection ? { uid: collectionUid, name: collection.name } : null; + }) + .filter(Boolean); + }, [allDrafts, allCollections]); + + const hasUnsavedChanges + = allDrafts.collectionDrafts.length > 0 || allDrafts.folderDrafts.length > 0 || allDrafts.requestDrafts.length > 0; + + const handleCloseAllCollections = () => { + const removalPromises = collectionUids.map((uid) => dispatch(removeCollection(uid))); + + Promise.all(removalPromises) + .then(() => { + toast.success('Closed all collections'); + }) + .catch((error) => { + console.error('Error closing collections:', error); + toast.error('An error occurred while closing collections'); + }) + .finally(() => { + onClose(); + }); + }; + + const handleDiscard = () => { + handleCloseAllCollections(); + }; + + const handleCancel = () => { + onClose(); + }; + + const handleSave = async () => { + try { + const savePromises = []; + + // Save all collection drafts + if (allDrafts.collectionDrafts.length > 0) { + savePromises.push(dispatch(saveMultipleCollections(allDrafts.collectionDrafts))); + } + + // Save all folder drafts + if (allDrafts.folderDrafts.length > 0) { + savePromises.push(dispatch(saveMultipleFolders(allDrafts.folderDrafts))); + } + + // Save all request drafts + if (allDrafts.requestDrafts.length > 0) { + savePromises.push(dispatch(saveMultipleRequests(allDrafts.requestDrafts))); + } + + await Promise.all(savePromises); + handleCloseAllCollections(); + } catch (error) { + console.error('Error saving drafts:', error); + toast.error('An error occurred while saving changes'); + handleCancel(); + } + }; + + if (collectionUids.length === 0) { + return null; + } + + const hasMultipleCollections = collectionUids.length > 1; + const singleCollectionName = hasMultipleCollections + ? null + : findCollectionByUid(allCollections, collectionUids[0])?.name; + + const displayedCollections = useMemo(() => showAllCollections ? collectionsWithUnsavedChanges : getDisplayItems(collectionsWithUnsavedChanges), + [collectionsWithUnsavedChanges, showAllCollections]); + const hasMoreCollections = collectionsWithUnsavedChanges.length > displayedCollections.length; + const hiddenCollectionsCount = collectionsWithUnsavedChanges.length - displayedCollections.length; + + const toggleButton = hasMoreCollections || showAllCollections ? ( + setShowAllCollections(!showAllCollections)} + > + + {showAllCollections ? 'Show less' : `Show ${hiddenCollectionsCount} more`} + + + ) : null; + + return ( + + + + {hasUnsavedChanges ? ( + <> +
+ +

Hold on..

+
+
+ Do you want to save changes you made to the following{' '} + {collectionsWithUnsavedChanges.length === 1 ? 'collection' : 'collections'}? +
+
+ Collections will still be available in the file system and can be re-opened later. +
+ +
+
+
+ {displayedCollections.map(({ uid, name }) => ( + + {name} + + ))} + {toggleButton} +
+
+
+ +
+
+ +
+
+ + +
+
+ + ) : ( + <> +
+ {hasMultipleCollections ? ( + `Are you sure you want to close all ${collectionUids.length} collections in Bruno?` + ) : ( + <> + Are you sure you want to close the collection {singleCollectionName} in Bruno? + + )} +
+
+ Collections will still be available in the file system and can be re-opened later. +
+
+ + +
+ + )} +
+
+
+ ); +}; + +export default RemoveCollectionsModal; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/StyledWrapper.js index 18058e6aa..2854eeb09 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/StyledWrapper.js @@ -1,21 +1,13 @@ import styled from 'styled-components'; const Wrapper = styled.div` - .collections-badge { - margin-inline: 0.5rem; - background-color: ${(props) => props.theme.sidebar.badge.bg}; - border-radius: 5px; - - .caret { - margin-left: 0.25rem; - color: rgb(140, 140, 140); - fill: rgb(140, 140, 140); - } - } - span.close-icon { color: ${(props) => props.theme.colors.text.muted}; } + + &:hover .collections-badge .collections-header-actions .collection-action-button { + opacity: 1; + } `; export default Wrapper; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/index.js b/packages/bruno-app/src/components/Sidebar/Collections/index.js index 119318819..5bc8e8754 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/index.js @@ -1,64 +1,14 @@ -import React, { useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import { IconSearch, - IconFolders, - IconArrowsSort, - IconSortAscendingLetters, - IconSortDescendingLetters, IconX } from '@tabler/icons'; -import Collection from './Collection'; +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; import CreateCollection from '../CreateCollection'; -import StyledWrapper from './StyledWrapper'; +import Collection from './Collection'; +import CollectionsHeader from './CollectionsHeader'; import CreateOrOpenCollection from './CreateOrOpenCollection'; -import { sortCollections } from 'providers/ReduxStore/slices/collections/actions'; - -// todo: move this to a separate folder -// the coding convention is to keep all the components in a folder named after the component -const CollectionsBadge = () => { - const dispatch = useDispatch(); - const { collections } = useSelector((state) => state.collections); - const { collectionSortOrder } = useSelector((state) => state.collections); - const sortCollectionOrder = () => { - let order; - switch (collectionSortOrder) { - case 'default': - order = 'alphabetical'; - break; - case 'alphabetical': - order = 'reverseAlphabetical'; - break; - case 'reverseAlphabetical': - order = 'default'; - break; - } - dispatch(sortCollections({ order })); - }; - return ( -
-
-
- - - - Collections -
- {collections.length >= 1 && ( - - )} -
-
- ); -}; +import StyledWrapper from './StyledWrapper'; const Collections = () => { const [searchText, setSearchText] = useState(''); @@ -67,18 +17,18 @@ const Collections = () => { if (!collections || !collections.length) { return ( - - + + ); } return ( - + {createCollectionModalOpen ? setCreateCollectionModalOpen(false)} /> : null} - +
diff --git a/tests/collection/close-all-collections/close-all-collections.spec.ts b/tests/collection/close-all-collections/close-all-collections.spec.ts new file mode 100644 index 000000000..2f458de8b --- /dev/null +++ b/tests/collection/close-all-collections/close-all-collections.spec.ts @@ -0,0 +1,186 @@ +import { execSync } from 'child_process'; +import { test, expect } from '../../../playwright'; +import { Page, ElectronApplication } from '@playwright/test'; +import path from 'path'; +import { openCollectionAndAcceptSandbox } from '../../utils/page/actions'; +import { buildCommonLocators } from '../../utils/page/locators'; + +/** + * Helper function to restart app and get fresh state with locators + */ +const restartAppAndGetLocators = async (restartApp: (options?: { initUserDataPath?: string }) => Promise): Promise<{ app: ElectronApplication; page: Page; locators: ReturnType }> => { + const app = await restartApp(); + const page = await app.firstWindow(); + await page.locator('[data-app-state="loaded"]').waitFor(); + const locators = buildCommonLocators(page); + return { app, page, locators }; +}; + +test.describe('Close All Collections', () => { + test.afterAll(async () => { + // Reset the request file to the original state after saving changes + execSync(`git checkout -- "${path.join(__dirname, 'fixtures', 'collections', 'collection 1', 'test-request.bru')}"`); + }); + + test('should show/hide close all icon based on hover state', async ({ pageWithUserData: page }) => { + const locators = buildCommonLocators(page); + + await test.step('Verify initial state', async () => { + await expect(locators.sidebar.collection('collection 1')).toBeVisible(); + const closeAllButton = locators.sidebar.closeAllCollectionsButton(); + await expect(closeAllButton).toHaveCSS('opacity', '0'); + }); + + await test.step('Hover to show icon', async () => { + const closeAllButton = locators.sidebar.closeAllCollectionsButton(); + await locators.sidebar.collectionsContainer().hover(); + await expect(closeAllButton).toHaveCSS('opacity', '1'); + }); + + await test.step('Move mouse away to hide icon', async () => { + const closeAllButton = locators.sidebar.closeAllCollectionsButton(); + await page.mouse.move(0, 0); + await expect(closeAllButton).toHaveCSS('opacity', '0'); + }); + }); + + test('should handle closing all collections without unsaved changes', async ({ restartApp }) => { + const { page, locators } = await restartAppAndGetLocators(restartApp); + + await test.step('Verify collections are visible', async () => { + await expect(locators.sidebar.collection('collection 1')).toBeVisible(); + await expect(locators.sidebar.collection('collection 2')).toBeVisible(); + }); + + await test.step('Cancel closing collections', async () => { + // Hover and click close all icon + await locators.sidebar.collectionsContainer().hover(); + await locators.sidebar.closeAllCollectionsButton().click(); + + // Verify confirmation modal appears + const confirmModal = locators.modal.byTitle('Close all collections'); + await expect(confirmModal).toBeVisible(); + + // Click "Cancel" to dismiss the modal + await locators.modal.closeButton().click(); + + // Verify collections are still visible + await expect(locators.sidebar.collection('collection 1')).toBeVisible(); + await expect(locators.sidebar.collection('collection 2')).toBeVisible(); + }); + + await test.step('Confirm closing collections', async () => { + // Hover and click close all icon again + await locators.sidebar.collectionsContainer().hover(); + await locators.sidebar.closeAllCollectionsButton().click(); + + // Verify confirmation modal appears + const confirmModal = locators.modal.byTitle('Close all collections'); + await expect(confirmModal).toBeVisible(); + + // Click "Close All" to confirm + await locators.modal.button('Close All').click(); + + // Verify collections are closed + await expect(locators.sidebar.collection('collection 1')).not.toBeVisible(); + await expect(locators.sidebar.collection('collection 2')).not.toBeVisible(); + }); + }); + + test('should discard changes and close collections when Discard and Close is clicked', async ({ restartApp }) => { + const { page, locators: newLocators } = await restartAppAndGetLocators(restartApp); + + await test.step('Verify collections are visible', async () => { + await expect(newLocators.sidebar.collection('collection 1')).toBeVisible(); + await expect(newLocators.sidebar.collection('collection 2')).toBeVisible(); + }); + + await test.step('Create unsaved changes', async () => { + await openCollectionAndAcceptSandbox(page, 'collection 1'); + await newLocators.sidebar.request('test-request').click(); + + const urlContainer = page.locator('#request-url'); + await expect(urlContainer).toBeVisible(); + + const codeMirrorEditor = urlContainer.locator('.CodeMirror'); + await codeMirrorEditor.click(); + await page.keyboard.type('modified'); + }); + + await test.step('Trigger close all and discard changes', async () => { + await newLocators.sidebar.collectionsContainer().hover(); + await newLocators.sidebar.closeAllCollectionsButton().click(); + + const unsavedChangesModal = newLocators.modal.byTitle('Close all collections'); + await expect(unsavedChangesModal).toBeVisible(); + await expect(unsavedChangesModal.getByText('Do you want to save')).toBeVisible(); + + await newLocators.modal.button('Discard and Close').click(); + + await expect(page.getByText('Closed all collections')).toBeVisible(); + await expect(newLocators.sidebar.collection('collection 1')).not.toBeVisible(); + await expect(newLocators.sidebar.collection('collection 2')).not.toBeVisible(); + }); + + await test.step('Restart app to verify changes were discarded', async () => { + const { page: restartedPage, locators: restartedLocators } = await restartAppAndGetLocators(restartApp); + + await expect(restartedLocators.sidebar.collection('collection 1')).toBeVisible(); + await openCollectionAndAcceptSandbox(restartedPage, 'collection 1'); + await restartedLocators.sidebar.request('test-request').click(); + + const urlContainerAfterReopen = restartedPage.locator('#request-url'); + await expect(urlContainerAfterReopen).toBeVisible(); + const urlAfterReopen = await urlContainerAfterReopen.locator('.CodeMirror').textContent(); + expect(urlAfterReopen).not.toContain('modified'); + }); + }); + + test('should save changes and close collections when Save and Close is clicked', async ({ restartApp }) => { + const { page, locators: newLocators } = await restartAppAndGetLocators(restartApp); + + await test.step('Verify collections are visible', async () => { + await expect(newLocators.sidebar.collection('collection 1')).toBeVisible(); + await expect(newLocators.sidebar.collection('collection 2')).toBeVisible(); + }); + + await test.step('Create unsaved changes', async () => { + await openCollectionAndAcceptSandbox(page, 'collection 1'); + await newLocators.sidebar.request('test-request').click(); + + const urlContainer = page.locator('#request-url'); + await expect(urlContainer).toBeVisible(); + + const codeMirrorEditor = urlContainer.locator('.CodeMirror'); + await codeMirrorEditor.click(); + await page.keyboard.type('modified'); + }); + + await test.step('Trigger close all and save changes', async () => { + await newLocators.sidebar.collectionsContainer().hover(); + await newLocators.sidebar.closeAllCollectionsButton().click(); + + const unsavedChangesModal = newLocators.modal.byTitle('Close all collections'); + await expect(unsavedChangesModal).toBeVisible(); + await expect(unsavedChangesModal.getByText('Do you want to save')).toBeVisible(); + + await newLocators.modal.button('Save and Close').click(); + + await expect(newLocators.sidebar.collection('collection 1')).not.toBeVisible(); + await expect(newLocators.sidebar.collection('collection 2')).not.toBeVisible(); + }); + + await test.step('Restart app to verify changes were saved', async () => { + const { page: restartedPage, locators: restartedLocators } = await restartAppAndGetLocators(restartApp); + + await expect(restartedLocators.sidebar.collection('collection 1')).toBeVisible(); + await openCollectionAndAcceptSandbox(restartedPage, 'collection 1'); + await restartedLocators.sidebar.request('test-request').click(); + + const urlContainerAfterReopen = restartedPage.locator('#request-url'); + await expect(urlContainerAfterReopen).toBeVisible(); + const urlAfterReopen = await urlContainerAfterReopen.locator('.CodeMirror').textContent(); + expect(urlAfterReopen).toContain('modified'); + }); + }); +}); diff --git a/tests/collection/close-all-collections/fixtures/collections/collection 1/bruno.json b/tests/collection/close-all-collections/fixtures/collections/collection 1/bruno.json new file mode 100644 index 000000000..9296360cd --- /dev/null +++ b/tests/collection/close-all-collections/fixtures/collections/collection 1/bruno.json @@ -0,0 +1,6 @@ +{ + "version": "1", + "name": "collection 1", + "type": "collection" +} + diff --git a/tests/collection/close-all-collections/fixtures/collections/collection 1/test-request.bru b/tests/collection/close-all-collections/fixtures/collections/collection 1/test-request.bru new file mode 100644 index 000000000..4b0a586e9 --- /dev/null +++ b/tests/collection/close-all-collections/fixtures/collections/collection 1/test-request.bru @@ -0,0 +1,12 @@ +meta { + name: test-request + type: http + seq: 1 +} + +get { + url: https://jsonplaceholder.typicode.com/posts/1 + body: none + auth: none +} + diff --git a/tests/collection/close-all-collections/fixtures/collections/collection 2/bruno.json b/tests/collection/close-all-collections/fixtures/collections/collection 2/bruno.json new file mode 100644 index 000000000..61e871772 --- /dev/null +++ b/tests/collection/close-all-collections/fixtures/collections/collection 2/bruno.json @@ -0,0 +1,6 @@ +{ + "version": "1", + "name": "collection 2", + "type": "collection" +} + diff --git a/tests/collection/close-all-collections/fixtures/collections/collection 2/test-request.bru b/tests/collection/close-all-collections/fixtures/collections/collection 2/test-request.bru new file mode 100644 index 000000000..5a64399b6 --- /dev/null +++ b/tests/collection/close-all-collections/fixtures/collections/collection 2/test-request.bru @@ -0,0 +1,12 @@ +meta { + name: test-request + type: http + seq: 1 +} + +get { + url: https://jsonplaceholder.typicode.com/users/1 + body: none + auth: none +} + diff --git a/tests/collection/close-all-collections/init-user-data/preferences.json b/tests/collection/close-all-collections/init-user-data/preferences.json new file mode 100644 index 000000000..e818ec80e --- /dev/null +++ b/tests/collection/close-all-collections/init-user-data/preferences.json @@ -0,0 +1,7 @@ +{ + "lastOpenedCollections": [ + "{{projectRoot}}/tests/collection/close-all-collections/fixtures/collections/collection 1", + "{{projectRoot}}/tests/collection/close-all-collections/fixtures/collections/collection 2" + ] +} + diff --git a/tests/environments/import-environment/global-env-import.spec.ts b/tests/environments/import-environment/global-env-import.spec.ts index 0327ae830..8e5205296 100644 --- a/tests/environments/import-environment/global-env-import.spec.ts +++ b/tests/environments/import-environment/global-env-import.spec.ts @@ -88,6 +88,7 @@ test.describe('Global Environment Import Tests', () => { .click(); await page.locator('.dropdown-item').filter({ hasText: 'Close' }).click(); await page.locator('.dropdown-item').filter({ hasText: 'Close' }).waitFor({ state: 'detached' }); - await page.getByRole('button', { name: 'Close' }).click(); + const closeModal = page.getByRole('dialog').filter({ has: page.getByText('Close Collection') }); + await closeModal.getByRole('button', { name: 'Close' }).click(); }); }); diff --git a/tests/onboarding/sample-collection.spec.ts b/tests/onboarding/sample-collection.spec.ts index 78d89a38b..5c842c90e 100644 --- a/tests/onboarding/sample-collection.spec.ts +++ b/tests/onboarding/sample-collection.spec.ts @@ -101,7 +101,7 @@ test.describe('Onboarding', () => { await closeOption.click(); // Handle the confirmation dialog - click the 'Close' button to confirm - const confirmCloseButton = page.getByRole('button', { name: 'Close' }); + const confirmCloseButton = page.locator('.bruno-modal').getByRole('button', { name: 'Close' }); await expect(confirmCloseButton).toBeVisible(); await confirmCloseButton.click(); diff --git a/tests/utils/page/locators.ts b/tests/utils/page/locators.ts index dde7bf971..7ee6e660a 100644 --- a/tests/utils/page/locators.ts +++ b/tests/utils/page/locators.ts @@ -6,6 +6,7 @@ export const buildCommonLocators = (page: Page) => ({ .locator('.infotip') .filter({ hasText: /^Save/ }), sidebar: { + collectionsContainer: () => page.getByTestId('collections'), collection: (name: string) => page.locator('#sidebar-collection-name').filter({ hasText: name }), folder: (name: string) => page.locator('.collection-item-name').filter({ hasText: name }), request: (name: string) => page.locator('.collection-item-name').filter({ hasText: name }), @@ -15,7 +16,8 @@ export const buildCommonLocators = (page: Page) => ({ // Using .locator('..') gets the parent element of the folder's collection-item-name div. const folderWrapper = page.locator('.collection-item-name').filter({ hasText: folderName }).locator('..'); return folderWrapper.locator('.collection-item-name').filter({ hasText: requestName }); - } + }, + closeAllCollectionsButton: () => page.getByTestId('close-all-collections-button') }, actions: { collectionActions: (collectionName: string) => @@ -40,7 +42,9 @@ export const buildCommonLocators = (page: Page) => ({ }, modal: { title: (title: string) => page.locator('.bruno-modal-header-title').filter({ hasText: title }), - button: (name: string) => page.getByRole('button', { name: name, exact: true }) + byTitle: (title: string) => page.locator('.bruno-modal').filter({ has: page.locator('.bruno-modal-header-title').filter({ hasText: title }) }), + button: (name: string) => page.getByRole('button', { name: name, exact: true }), + closeButton: () => page.locator('.bruno-modal').getByTestId('modal-close-button') }, environment: { selector: () => page.getByTestId('environment-selector-trigger'),