feat: add copy paste feature for folder (#6097)

* feat: add copy paste feature for folder

* add: playwright test

* fix

* fix

* add: keyboardFocusBg in light mode

* fix: copy paste in yaml collection

* improvement
This commit is contained in:
Pooja
2025-12-02 15:29:32 +05:30
committed by GitHub
parent bc4062b950
commit 06a024a1d9
12 changed files with 383 additions and 68 deletions

View File

@@ -131,6 +131,15 @@ const Wrapper = styled.div`
}
}
&.item-keyboard-focused {
background: ${(props) => props.theme.sidebar.collection.item.keyboardFocusBg};
outline: none;
&:hover {
background: ${(props) => props.theme.sidebar.collection.item.keyboardFocusBg} !important;
}
}
div.tippy-box {
position: relative;
top: -0.625rem;

View File

@@ -77,6 +77,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
const [itemInfoModalOpen, setItemInfoModalOpen] = useState(false);
const [examplesExpanded, setExamplesExpanded] = useState(false);
const [isKeyboardFocused, setIsKeyboardFocused] = useState(false);
const hasSearchText = searchText && searchText?.trim()?.length;
const itemIsCollapsed = hasSearchText ? false : item.collapsed;
const isFolder = isItemAFolder(item);
@@ -186,10 +187,11 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
});
const itemRowClassName = classnames('flex collection-item-name relative items-center', {
'item-focused-in-tab': isTabForItemActive,
'item-focused-in-tab': isTabForItemActive && !isKeyboardFocused,
'item-hovered': isOver && canDrop,
'drop-target': isOver && dropType === 'inside',
'drop-target-above': isOver && dropType === 'adjacent'
'drop-target-above': isOver && dropType === 'adjacent',
'item-keyboard-focused': isKeyboardFocused
});
const handleRun = async () => {
@@ -393,23 +395,56 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
}
};
const handleCopyRequest = () => {
const handleCopyItem = () => {
dropdownTippyRef.current.hide();
dispatch(copyRequest(item));
toast.success('Request copied to clipboard');
const itemType = isFolder ? 'Folder' : 'Request';
toast.success(`${itemType} copied to clipboard`);
};
const handlePasteRequest = () => {
const handlePasteItem = () => {
dropdownTippyRef.current.hide();
// Only allow paste into folders
if (!isFolder) {
toast.error('Paste is only available for folders');
return;
}
dispatch(pasteItem(collectionUid, item.uid))
.then(() => {
toast.success('Request pasted successfully');
toast.success('Item pasted successfully');
})
.catch((err) => {
toast.error(err ? err.message : 'An error occurred while pasting the request');
toast.error(err ? err.message : 'An error occurred while pasting the item');
});
};
// Keyboard shortcuts handler
const handleKeyDown = (e) => {
// Detect Mac by checking both metaKey and platform
const isMac = navigator.userAgent?.includes('Mac') || navigator.platform?.startsWith('Mac');
const isModifierPressed = isMac ? e.metaKey : e.ctrlKey;
if (isModifierPressed && e.key.toLowerCase() === 'c') {
e.preventDefault();
e.stopPropagation();
handleCopyItem();
} else if (isModifierPressed && e.key.toLowerCase() === 'v') {
e.preventDefault();
e.stopPropagation();
handlePasteItem();
}
};
const handleFocus = () => {
setIsKeyboardFocused(true);
};
const handleBlur = () => {
setIsKeyboardFocused(false);
};
return (
<StyledWrapper className={className}>
{renameItemModalOpen && (
@@ -449,6 +484,10 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
ref.current = node;
drag(drop(node));
}}
tabIndex={0}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
onBlur={handleBlur}
>
<div className="flex items-center h-full w-full">
{indents && indents.length
@@ -568,21 +607,19 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
</span>
Clone
</div>
{!isFolder && (
<div
className="dropdown-item"
onClick={handleCopyRequest}
>
<span className="dropdown-icon">
<div
className="dropdown-item"
onClick={handleCopyItem}
>
<span className="dropdown-icon">
<IconCopy size={16} strokeWidth={2} />
</span>
Copy
</div>
)}
</span>
Copy
</div>
{isFolder && hasCopiedItems && (
<div
className="dropdown-item"
onClick={handlePasteRequest}
onClick={handlePasteItem}
>
<span className="dropdown-icon">
<IconClipboard size={16} strokeWidth={2} />

View File

@@ -94,6 +94,15 @@ const Wrapper = styled.div`
background: ${(props) => props.theme.sidebar.collection.item.bg} !important;
}
}
&.collection-keyboard-focused {
background: ${(props) => props.theme.sidebar.collection.item.keyboardFocusBg};
outline: none;
&:hover {
background: ${(props) => props.theme.sidebar.collection.item.keyboardFocusBg} !important;
}
}
}
#sidebar-collection-name {

View File

@@ -51,6 +51,7 @@ const Collection = ({ collection, searchText }) => {
const [showShareCollectionModal, setShowShareCollectionModal] = useState(false);
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
const [dropType, setDropType] = useState(null);
const [isKeyboardFocused, setIsKeyboardFocused] = useState(false);
const dispatch = useDispatch();
const isLoading = areItemsLoading(collection);
const collectionRef = useRef(null);
@@ -161,17 +162,38 @@ const Collection = ({ collection, searchText }) => {
);
};
const handlePasteRequest = () => {
const handlePasteItem = () => {
menuDropdownTippyRef.current.hide();
dispatch(pasteItem(collection.uid, null))
.then(() => {
toast.success('Request pasted successfully');
toast.success('Item pasted successfully');
})
.catch((err) => {
toast.error(err ? err.message : 'An error occurred while pasting the request');
toast.error(err ? err.message : 'An error occurred while pasting the item');
});
};
// Keyboard shortcuts handler for collection
const handleKeyDown = (e) => {
// Detect Mac by checking both metaKey and platform
const isMac = navigator.userAgent?.includes('Mac') || navigator.platform?.startsWith('Mac');
const isModifierPressed = isMac ? e.metaKey : e.ctrlKey;
if (isModifierPressed && e.key.toLowerCase() === 'v') {
e.preventDefault();
e.stopPropagation();
handlePasteItem();
}
};
const handleFocus = () => {
setIsKeyboardFocused(true);
};
const handleBlur = () => {
setIsKeyboardFocused(false);
};
const isCollectionItem = (itemType) => {
return itemType === 'collection-item';
};
@@ -227,9 +249,10 @@ const Collection = ({ collection, searchText }) => {
}
const collectionRowClassName = classnames('flex py-1 collection-name items-center', {
'item-hovered': isOver && dropType === 'adjacent', // For collection-to-collection moves (show line)
'drop-target': isOver && dropType === 'inside', // For collection-item drops (highlight full area)
'collection-focused-in-tab': isCollectionFocused
'item-hovered': isOver && dropType === 'adjacent', // For collection-to-collection moves (show line)
'drop-target': isOver && dropType === 'inside', // For collection-item drops (highlight full area)
'collection-focused-in-tab': isCollectionFocused && !isKeyboardFocused,
'collection-keyboard-focused': isKeyboardFocused
});
// we need to sort request items by seq property
@@ -262,6 +285,10 @@ const Collection = ({ collection, searchText }) => {
collectionRef.current = node;
drag(drop(node));
}}
tabIndex={0}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
onBlur={handleBlur}
>
<div
className="flex flex-grow items-center overflow-hidden"
@@ -337,7 +364,7 @@ const Collection = ({ collection, searchText }) => {
{hasCopiedItems && (
<div
className="dropdown-item"
onClick={handlePasteRequest}
onClick={handlePasteItem}
>
<span className="dropdown-icon">
<IconClipboard size={16} strokeWidth={2} />

View File

@@ -81,6 +81,42 @@ import { addTab } from 'providers/ReduxStore/slices/tabs';
import { updateSettingsSelectedTab } from './index';
import { saveGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
// generate a unique names
const generateUniqueName = (originalName, existingItems, isFolder) => {
// Extract base name by removing any existing " (number)" suffix
const baseName = originalName.replace(/\s*\(\d+\)$/, '');
const baseFilename = sanitizeName(baseName);
// Get normalized filenames for items of the same type
const existingFilenames = existingItems
.filter((item) => isFolder ? item.type === 'folder' : item.type !== 'folder')
.map((item) => {
let filename = trim(item.filename);
// For requests, remove file extension (.bru, .yml, .yaml)
return isFolder ? filename : filename.replace(/\.(bru|yml|yaml)$/, '');
});
// Check if base name conflicts with existing items
if (!existingFilenames.includes(baseFilename)) {
return { newName: baseName, newFilename: baseFilename };
}
// Find highest counter among conflicting names
const counters = existingFilenames
.filter((filename) => filename === baseFilename || filename.startsWith(`${baseFilename} (`))
.map((filename) => {
if (filename === baseFilename) return 0;
const match = filename.match(/\((\d+)\)$/);
return match ? parseInt(match[1], 10) : 0;
});
const nextCounter = Math.max(0, ...counters) + 1;
return {
newName: `${baseName} (${nextCounter})`,
newFilename: `${baseFilename} (${nextCounter})`
};
};
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@@ -853,7 +889,7 @@ export const cloneItem = (newName, newFilename, itemUid, collectionUid) => (disp
const collectionPath = path.join(parentFolder.pathname, newFilename);
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:clone-folder', item, collectionPath).then(resolve).catch(reject);
ipcRenderer.invoke('renderer:clone-folder', item, collectionPath, collection.pathname).then(resolve).catch(reject);
return;
}
@@ -943,11 +979,6 @@ export const pasteItem = (targetCollectionUid, targetItemUid = null) => (dispatc
for (const clipboardItem of clipboardResult.items) {
const copiedItem = cloneDeep(clipboardItem);
// Only allow pasting requests (not folders)
if (isItemAFolder(copiedItem)) {
return reject(new Error('Pasting folders is not supported'));
}
const targetCollectionCopy = cloneDeep(targetCollection);
let targetItem = null;
let targetParentPathname = targetCollection.pathname;
@@ -964,39 +995,47 @@ export const pasteItem = (targetCollectionUid, targetItemUid = null) => (dispatc
targetParentPathname = targetItem.pathname;
}
// Generate a unique filename for the pasted item
let newName = copiedItem.name;
let newFilename = sanitizeName(copiedItem.name);
let counter = 1;
const existingItems = targetItem ? targetItem.items : targetCollection.items;
// Check for duplicate names and append counter if needed
while (find(existingItems, (i) => i.type !== 'folder' && trim(i.filename) === trim(resolveRequestFilename(newFilename, targetCollection.format)))) {
newName = `${copiedItem.name} (${counter})`;
newFilename = `${sanitizeName(copiedItem.name)} (${counter})`;
counter++;
// Handle folder pasting
if (isItemAFolder(copiedItem)) {
// Generate unique name for folder
const { newName, newFilename } = generateUniqueName(copiedItem.name, existingItems, true);
set(copiedItem, 'name', newName);
set(copiedItem, 'filename', newFilename);
set(copiedItem, 'root.meta.name', newName);
set(copiedItem, 'root.meta.seq', (existingItems?.length ?? 0) + 1);
const fullPathname = path.join(targetParentPathname, newFilename);
const { ipcRenderer } = window;
await ipcRenderer.invoke('renderer:clone-folder', copiedItem, fullPathname, targetCollection.pathname);
} else {
// Handle request pasting
// Generate unique name for request
const { newName, newFilename } = generateUniqueName(copiedItem.name, existingItems, false);
const filename = resolveRequestFilename(newFilename, targetCollection.format);
const itemToSave = refreshUidsInItem(transformRequestToSaveToFilesystem(copiedItem));
set(itemToSave, 'name', trim(newName));
set(itemToSave, 'filename', trim(filename));
const fullPathname = path.join(targetParentPathname, filename);
const { ipcRenderer } = window;
const requestItems = filter(existingItems, (i) => i.type !== 'folder');
itemToSave.seq = requestItems ? requestItems.length + 1 : 1;
await itemSchema.validate(itemToSave);
await ipcRenderer.invoke('renderer:new-request', fullPathname, itemToSave, targetCollection.format);
dispatch(insertTaskIntoQueue({
uid: uuid(),
type: 'OPEN_REQUEST',
collectionUid: targetCollectionUid,
itemPathname: fullPathname
}));
}
const filename = resolveRequestFilename(newFilename, targetCollection.format);
const itemToSave = refreshUidsInItem(transformRequestToSaveToFilesystem(copiedItem));
set(itemToSave, 'name', trim(newName));
set(itemToSave, 'filename', trim(filename));
const fullPathname = path.join(targetParentPathname, filename);
const { ipcRenderer } = window;
const requestItems = filter(existingItems, (i) => i.type !== 'folder');
itemToSave.seq = requestItems ? requestItems.length + 1 : 1;
await itemSchema.validate(itemToSave);
await ipcRenderer.invoke('renderer:new-request', fullPathname, itemToSave, targetCollection.format);
dispatch(insertTaskIntoQueue({
uid: uuid(),
type: 'OPEN_REQUEST',
collectionUid: targetCollectionUid,
itemPathname: fullPathname
}));
}
resolve();
@@ -1021,7 +1060,7 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
const { ipcRenderer } = window;
ipcRenderer
.invoke('renderer:delete-item', item.pathname, item.type)
.invoke('renderer:delete-item', item.pathname, item.type, collection.pathname)
.then(async () => {
// Reorder items in parent directory after deletion
if (parentDirectoryItem.items) {

View File

@@ -115,6 +115,7 @@ const darkTheme = {
item: {
bg: '#37373D',
hoverBg: '#2A2D2F',
keyboardFocusBg: 'rgba(10, 132, 255, 0.2)',
indentBorder: 'solid 1px #585858',
active: {
indentBorder: 'solid 1px #4c4c4c'

View File

@@ -118,6 +118,7 @@ const lightTheme = {
item: {
bg: colors.GRAY_2,
hoverBg: colors.GRAY_2,
keyboardFocusBg: 'rgba(10, 132, 255, 0.2)',
indentBorder: `solid 1px ${colors.GRAY_3}`,
active: {
indentBorder: `solid 1px ${colors.GRAY_3}`

View File

@@ -773,7 +773,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
});
// delete file/folder
ipcMain.handle('renderer:delete-item', async (event, pathname, type) => {
ipcMain.handle('renderer:delete-item', async (event, pathname, type, collectionPathname) => {
try {
if (type === 'folder') {
if (!fs.existsSync(pathname)) {
@@ -781,7 +781,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
// delete the request uid mappings
const requestFilesAtSource = await searchForRequestFiles(pathname);
const requestFilesAtSource = await searchForRequestFiles(pathname, collectionPathname);
for (let requestFile of requestFilesAtSource) {
deleteRequestUid(requestFile);
}
@@ -947,7 +947,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
items.forEach(async (item) => {
if (['http-request', 'graphql-request', 'grpc-request'].includes(item.type)) {
const content = await stringifyRequestViaWorker(item, { format });
const filePath = path.join(currentPath, item.filename);
// Use the correct file extension based on target format
const baseName = path.parse(item.filename).name;
const newFilename = format === 'yml' ? `${baseName}.yml` : `${baseName}.bru`;
const filePath = path.join(currentPath, newFilename);
safeWriteFileSync(filePath, content);
}
if (item.type === 'folder') {

View File

@@ -9,14 +9,17 @@ export const toOpenCollectionVariables = (variables: BrunoFolderRequest['vars']
const reqVars = hasReqRes ? variables.req : variables as BrunoVariables;
const resVars = hasReqRes && 'res' in variables ? variables.res : [];
const allVars = [...(reqVars || []), ...(resVars || [])];
const reqVarsArray = Array.isArray(reqVars) ? reqVars : [];
const resVarsArray = Array.isArray(resVars) ? resVars : [];
const allVars = [...reqVarsArray, ...resVarsArray];
if (!allVars.length) {
return undefined;
}
const ocVariables: Variable[] = allVars.map((v: BrunoVariable, index: number): Variable => {
const isResVar = reqVars && index >= (reqVars?.length || 0);
const isResVar = index >= reqVarsArray.length;
const variable: Variable = {
name: v.name || '',
value: v.value || ''

View File

@@ -0,0 +1,92 @@
import { test, expect } from '../../../playwright';
import { closeAllCollections, createCollection } from '../../utils/page';
test.describe('Copy and Paste Folders', () => {
test.afterAll(async ({ page }) => {
await closeAllCollections(page);
});
test('should copy and paste a folder within the same collection', async ({ page, createTmpDir }) => {
await createCollection(page, 'test-collection', await createTmpDir('test-collection'), { openWithSandboxMode: 'safe' });
const collection = page.locator('.collection-name').filter({ hasText: 'test-collection' });
// Create a new folder with a request inside
await collection.locator('.collection-actions').hover();
await collection.locator('.collection-actions .icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();
await page.locator('#folder-name').fill('folder-to-copy');
await page.getByRole('button', { name: 'Create' }).click();
const folder = page.locator('.collection-item-name').filter({ hasText: 'folder-to-copy' });
await expect(folder).toBeVisible();
// Add a request to the folder
await folder.hover();
await folder.locator('.menu-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
await page.getByPlaceholder('Request Name').fill('request-in-folder');
await page.locator('#new-request-url .CodeMirror').click();
await page.locator('textarea').fill('https://echo.usebruno.com/test');
await page.getByRole('button', { name: 'Create' }).click();
await folder.click();
await expect(page.locator('.collection-item-name').filter({ hasText: 'request-in-folder' })).toBeVisible();
// Copy the folder
await folder.hover();
await folder.locator('.menu-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'Copy' }).click();
// Paste into the collection root
await collection.hover();
await collection.locator('.collection-actions .icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'Paste' }).click();
// Verify the pasted folder appears
await expect(page.locator('.collection-item-name').filter({ hasText: 'folder-to-copy' })).toHaveCount(2);
});
test('should copy and paste a folder into a different collection', async ({ page, createTmpDir }) => {
// Create second collection
await createCollection(page, 'test-collection-2', await createTmpDir('test-collection-2'), { openWithSandboxMode: 'safe' });
const collection2 = page.locator('.collection-name').filter({ hasText: 'test-collection-2' });
// Paste the folder from clipboard into the new collection
await collection2.hover();
await collection2.locator('.collection-actions .icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'Paste' }).click();
// Verify the pasted folder appears in the new collection
await expect(page.locator('.collection-item-name').filter({ hasText: 'folder-to-copy' })).toHaveCount(3);
});
test('should paste folder into another folder', async ({ page }) => {
const collection = page.locator('.collection-name').filter({ hasText: 'test-collection-2' });
const folderToCopy = page.locator('.collection-item-name').filter({ hasText: 'folder-to-copy' }).first();
// Create a target folder
await collection.locator('.collection-actions').hover();
await collection.locator('.collection-actions .icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();
await page.locator('#folder-name').fill('target-folder');
await page.getByRole('button', { name: 'Create' }).click();
const targetFolder = page.locator('.collection-item-name').filter({ hasText: 'target-folder' });
await expect(targetFolder).toBeVisible();
await targetFolder.click();
// Copy folder-to-copy
await folderToCopy.hover();
await folderToCopy.locator('.menu-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'Copy' }).click();
await folderToCopy.click();
// Paste into target folder
await targetFolder.hover();
await targetFolder.locator('.menu-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'Paste' }).click();
// Verify folder was pasted inside target folder
await expect(page.locator('.collection-item-name').filter({ hasText: 'folder-to-copy' })).toHaveCount(4);
});
});

View File

@@ -50,7 +50,6 @@ test.describe('Copy and Paste Requests', () => {
await folder.locator('.menu-icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'Paste' }).click();
await page.waitForTimeout(2000);
await expect(page.locator('.collection-item-name').filter({ hasText: 'original-request' })).toHaveCount(3);
});

View File

@@ -0,0 +1,93 @@
import { test, expect } from '../../../playwright';
import { closeAllCollections, createCollection } from '../../utils/page';
test.describe('Copy and Paste with Keyboard Shortcuts', () => {
test.afterAll(async ({ page }) => {
await closeAllCollections(page);
});
test('should copy and paste request using keyboard shortcuts', async ({ page, createTmpDir }) => {
await createCollection(page, 'keyboard-test', await createTmpDir('keyboard-test'), { openWithSandboxMode: 'safe' });
const collection = page.locator('.collection-name').filter({ hasText: 'keyboard-test' });
// Create a request
await collection.locator('.collection-actions').hover();
await collection.locator('.collection-actions .icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
await page.getByPlaceholder('Request Name').fill('test-request');
await page.locator('#new-request-url .CodeMirror').click();
await page.locator('textarea').fill('https://echo.usebruno.com');
await page.getByRole('button', { name: 'Create' }).click();
const requestItem = page.locator('.collection-item-name').filter({ hasText: 'test-request' });
await expect(requestItem).toBeVisible();
// Focus the request item
await requestItem.click();
await requestItem.focus();
// Wait for keyboard focus indicator
await expect(requestItem).toHaveClass(/item-keyboard-focused/);
// Use Cmd+C on Mac, Ctrl+C on Windows/Linux
const modifier = process.platform === 'darwin' ? 'Meta' : 'Control';
await page.keyboard.press(`${modifier}+KeyC`);
// Verify copy success (toast message)
await expect(page.getByText(/copied to clipboard/i).first()).toBeVisible();
// Focus the collection to paste
await collection.click();
await collection.focus();
// Use Cmd+V on Mac, Ctrl+V on Windows/Linux
await page.keyboard.press(`${modifier}+KeyV`);
// Verify paste success
await expect(page.getByText(/pasted successfully/i).first()).toBeVisible();
// Verify the pasted request appears
await expect(page.locator('.collection-item-name').filter({ hasText: 'test-request' })).toHaveCount(2);
});
test('should copy and paste folder using keyboard shortcuts', async ({ page }) => {
const collection = page.locator('.collection-name').filter({ hasText: 'keyboard-test' });
// Create a folder
await collection.locator('.collection-actions').hover();
await collection.locator('.collection-actions .icon').click();
await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();
await page.locator('#folder-name').fill('test-folder');
await page.getByRole('button', { name: 'Create' }).click();
const folder = page.locator('.collection-item-name').filter({ hasText: 'test-folder' });
await expect(folder).toBeVisible();
// Focus the folder
await folder.click();
await folder.focus();
// Wait for keyboard focus indicator
await expect(folder).toHaveClass(/item-keyboard-focused/);
// Use keyboard shortcut to copy
const modifier = process.platform === 'darwin' ? 'Meta' : 'Control';
await page.keyboard.press(`${modifier}+KeyC`);
// Verify copy success
await expect(page.getByText(/copied to clipboard/i).first()).toBeVisible();
// Focus the collection to paste
await collection.click();
await collection.focus();
// Use keyboard shortcut to paste
await page.keyboard.press(`${modifier}+KeyV`);
// Verify paste success
await expect(page.getByText(/pasted successfully/i).first()).toBeVisible();
// Verify the pasted folder appears
await expect(page.locator('.collection-item-name').filter({ hasText: 'test-folder' })).toHaveCount(2);
});
});