refactor: update AppTitleBar and SidebarHeader components (#6341)

* refactor: update AppTitleBar and SidebarHeader components to use MenuDropdown and ActionIcon for improved UI consistency

* refactor: update button locators in tests to use data-testid for consistency and improved readability
This commit is contained in:
Abhishek S Lal
2025-12-08 22:06:24 +05:30
committed by GitHub
parent cf969dfcd6
commit 0197ae37c8
42 changed files with 646 additions and 284 deletions

View File

@@ -29,7 +29,6 @@ const Wrapper = styled.div`
.workspace-name-container,
.dropdown-item,
.home-button,
.env-selector-trigger,
.dropdown,
button {
-webkit-app-region: no-drag;
@@ -49,25 +48,6 @@ const Wrapper = styled.div`
margin-left: 0px;
}
/* Home button */
.home-button {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
background: transparent;
border-radius: 6px;
cursor: pointer;
color: ${(props) => props.theme.sidebar.color};
transition: background 0.15s ease;
&:hover {
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
}
}
/* Workspace Name Dropdown Trigger */
.workspace-name-container {
display: flex;
@@ -112,7 +92,7 @@ const Wrapper = styled.div`
.bruno-text {
font-size: 13px;
font-weight: 600;
color: ${(props) => props.theme.sidebar.muted};
color: ${(props) => props.theme.text};
letter-spacing: 0.5px;
}
}
@@ -125,36 +105,6 @@ const Wrapper = styled.div`
flex-shrink: 0;
}
/* Action buttons in right section */
.titlebar-action-button {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
background: transparent;
border-radius: 6px;
cursor: pointer;
color: ${(props) => props.theme.sidebar.color};
transition: background 0.15s ease;
&:hover {
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
}
svg {
color: ${(props) => props.theme.sidebar.color};
}
}
/* Draggable region */
.drag-region {
flex: 1;
height: 100%;
-webkit-app-region: drag;
}
/* Workspace Dropdown Styles */
.workspace-item {
display: flex;

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { IconCheck, IconChevronDown, IconFolder, IconHome, IconLayoutColumns, IconLayoutRows, IconPin, IconPinned, IconPlus } from '@tabler/icons';
import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import { useDispatch, useSelector } from 'react-redux';
@@ -9,7 +10,8 @@ import { openWorkspaceDialog, switchWorkspace } from 'providers/ReduxStore/slice
import { sortWorkspaces, toggleWorkspacePin } from 'utils/workspaces';
import Bruno from 'components/Bruno';
import Dropdown from 'components/Dropdown';
import MenuDropdown from 'ui/MenuDropdown';
import ActionIcon from 'ui/ActionIcon';
import IconSidebarToggle from 'components/Icons/IconSidebarToggle';
import CreateWorkspace from 'components/WorkspaceSidebar/CreateWorkspace';
@@ -53,13 +55,10 @@ const AppTitleBar = () => {
}, [workspaces, preferences]);
const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false);
const [showWorkspaceDropdown, setShowWorkspaceDropdown] = useState(false);
const workspaceDropdownTippyRef = useRef();
const onWorkspaceDropdownCreate = (ref) => (workspaceDropdownTippyRef.current = ref);
const WorkspaceName = forwardRef((props, ref) => {
return (
<div ref={ref} className="workspace-name-container" onClick={() => setShowWorkspaceDropdown(!showWorkspaceDropdown)}>
<div ref={ref} className="workspace-name-container" {...props}>
<span className="workspace-name">{toTitleCase(activeWorkspace?.name) || 'Default Workspace'}</span>
<IconChevronDown size={14} stroke={1.5} className="chevron-icon" />
</div>
@@ -72,12 +71,10 @@ const AppTitleBar = () => {
const handleWorkspaceSwitch = (workspaceUid) => {
dispatch(switchWorkspace(workspaceUid));
setShowWorkspaceDropdown(false);
toast.success(`Switched to ${workspaces.find((w) => w.uid === workspaceUid)?.name}`);
};
const handleOpenWorkspace = async () => {
setShowWorkspaceDropdown(false);
try {
await dispatch(openWorkspaceDialog());
toast.success('Workspace opened successfully');
@@ -87,7 +84,6 @@ const AppTitleBar = () => {
};
const handleCreateWorkspace = () => {
setShowWorkspaceDropdown(false);
setCreateWorkspaceModalOpen(true);
};
@@ -124,6 +120,59 @@ const AppTitleBar = () => {
dispatch(savePreferences(updatedPreferences));
};
// Build workspace menu items
const workspaceMenuItems = useMemo(() => {
const items = sortedWorkspaces.map((workspace) => {
const isActive = workspace.uid === activeWorkspaceUid;
const isPinned = preferences?.workspaces?.pinnedWorkspaceUids?.includes(workspace.uid);
return {
id: workspace.uid,
label: toTitleCase(workspace.name),
onClick: () => handleWorkspaceSwitch(workspace.uid),
className: `workspace-item ${isActive ? 'active' : ''}`,
rightSection: (
<div className="workspace-actions">
{workspace.type !== 'default' && (
<ActionIcon
className={`pin-btn ${isPinned ? 'pinned' : ''}`}
onClick={(e) => handlePinWorkspace(workspace.uid, e)}
label={isPinned ? 'Unpin workspace' : 'Pin workspace'}
size="sm"
>
{isPinned ? (
<IconPinned size={14} stroke={1.5} />
) : (
<IconPin size={14} stroke={1.5} />
)}
</ActionIcon>
)}
{isActive && <IconCheck size={16} stroke={1.5} className="check-icon" />}
</div>
)
};
});
// Add label and action items
items.push(
{ type: 'label', label: 'Workspaces' },
{
id: 'create-workspace',
leftSection: IconPlus,
label: 'Create workspace',
onClick: handleCreateWorkspace
},
{
id: 'open-workspace',
leftSection: IconFolder,
label: 'Open workspace',
onClick: handleOpenWorkspace
}
);
return items;
}, [sortedWorkspaces, activeWorkspaceUid, preferences, handlePinWorkspace]);
return (
<StyledWrapper className={`app-titlebar ${isFullScreen ? 'fullscreen' : ''}`}>
{createWorkspaceModalOpen && (
@@ -133,61 +182,24 @@ const AppTitleBar = () => {
<div className="titlebar-content">
{/* Left section: Home + Workspace */}
<div className="titlebar-left">
<button className="home-button" onClick={handleHomeClick} title="Home">
<ActionIcon
onClick={handleHomeClick}
label="Home"
size="lg"
className="home-button"
>
<IconHome size={16} stroke={1.5} />
</button>
</ActionIcon>
{/* Workspace Dropdown */}
<Dropdown
onCreate={onWorkspaceDropdownCreate}
icon={<WorkspaceName />}
<MenuDropdown
data-testid="workspace-menu"
items={workspaceMenuItems}
placement="bottom-start"
style="new"
visible={showWorkspaceDropdown}
onClickOutside={() => setShowWorkspaceDropdown(false)}
selectedItemId={activeWorkspaceUid}
>
{sortedWorkspaces.map((workspace) => {
const isActive = workspace.uid === activeWorkspaceUid;
const isPinned = preferences?.workspaces?.pinnedWorkspaceUids?.includes(workspace.uid);
return (
<div
key={workspace.uid}
className={`dropdown-item workspace-item ${isActive ? 'active' : ''}`}
onClick={() => handleWorkspaceSwitch(workspace.uid)}
>
<span className="workspace-name">{toTitleCase(workspace.name)}</span>
<div className="workspace-actions">
{workspace.type !== 'default' && (
<button
className={`pin-btn ${isPinned ? 'pinned' : ''}`}
onClick={(e) => handlePinWorkspace(workspace.uid, e)}
title={isPinned ? 'Unpin workspace' : 'Pin workspace'}
>
{isPinned ? (
<IconPinned size={14} stroke={1.5} />
) : (
<IconPin size={14} stroke={1.5} />
)}
</button>
)}
{isActive && <IconCheck size={16} stroke={1.5} className="check-icon" />}
</div>
</div>
);
})}
<div className="label-item border-top">Workspaces</div>
<div className="dropdown-item" onClick={handleCreateWorkspace}>
<IconPlus size={16} stroke={1.5} className="icon" />
Create workspace
</div>
<div className="dropdown-item" onClick={handleOpenWorkspace}>
<IconFolder size={16} stroke={1.5} className="icon" />
Open workspace
</div>
</Dropdown>
<WorkspaceName />
</MenuDropdown>
</div>
{/* Center section: Bruno logo + text */}
@@ -199,33 +211,30 @@ const AppTitleBar = () => {
{/* Right section: Action buttons */}
<div className="titlebar-right">
{/* Toggle sidebar */}
<button
className="titlebar-action-button"
<ActionIcon
onClick={handleToggleSidebar}
title={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}
aria-label="Toggle Sidebar"
label={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}
size="lg"
data-testid="toggle-sidebar-button"
>
<IconSidebarToggle collapsed={sidebarCollapsed} size={16} strokeWidth={1.5} />
</button>
</ActionIcon>
{/* Toggle devtools */}
<button
className="titlebar-action-button"
<ActionIcon
onClick={handleToggleDevtools}
title={isConsoleOpen ? 'Hide devtools' : 'Show devtools'}
aria-label="Toggle Devtools"
label={isConsoleOpen ? 'Hide devtools' : 'Show devtools'}
size="lg"
data-testid="toggle-devtools-button"
>
<IconBottombarToggle collapsed={!isConsoleOpen} size={16} strokeWidth={1.5} />
</button>
</ActionIcon>
{/* Toggle vertical layout */}
<button
className="titlebar-action-button"
<ActionIcon
onClick={handleToggleVerticalLayout}
title={orientation === 'horizontal' ? 'Switch to vertical layout' : 'Switch to horizontal layout'}
aria-label="Toggle Vertical Layout"
label={orientation === 'horizontal' ? 'Switch to vertical layout' : 'Switch to horizontal layout'}
size="lg"
data-testid="toggle-vertical-layout-button"
>
{orientation === 'horizontal' ? (
@@ -233,8 +242,7 @@ const AppTitleBar = () => {
) : (
<IconLayoutRows size={16} stroke={1.5} />
)}
</button>
</ActionIcon>
</div>
</div>
</StyledWrapper>

View File

@@ -25,6 +25,16 @@ const Wrapper = styled.div`
padding-top: 0;
padding-bottom: 0;
[role="menu"] {
outline: none;
&:focus {
outline: none;
}
&:focus-visible {
outline: none;
}
}
.label-item {
display: flex;
align-items: center;
@@ -59,6 +69,10 @@ const Wrapper = styled.div`
}
}
.dropdown-label {
flex: 1;
}
.dropdown-icon {
flex-shrink: 0;
width: 16px;
@@ -70,10 +84,31 @@ const Wrapper = styled.div`
opacity: 0.8;
}
.dropdown-right-section {
margin-left: auto;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
&:hover:not(:disabled) {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&.selected-focused:not(:disabled) {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&:focus-visible:not(:disabled) {
outline: none;
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&:focus:not(:focus-visible) {
outline: none;
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;

View File

@@ -1,7 +1,7 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
padding: 8px 4px 6px 10px;
.sidebar-header {
display: flex;
@@ -9,6 +9,7 @@ const StyledWrapper = styled.div`
justify-content: space-between;
gap: 8px;
border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg};
padding: 6px 4px 6px 10px;
}
/* Section Title (single view mode) */

View File

@@ -13,7 +13,7 @@ import {
IconSquareX,
IconTrash
} from '@tabler/icons';
import { useRef, useState } from 'react';
import { useState } from 'react';
import toast from 'react-hot-toast';
import { useDispatch, useSelector } from 'react-redux';
@@ -22,7 +22,8 @@ import { sortCollections } from 'providers/ReduxStore/slices/collections/index';
import { importCollectionInWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
import { openApiSpec } from 'providers/ReduxStore/slices/apiSpec';
import Dropdown from 'components/Dropdown';
import MenuDropdown from 'ui/MenuDropdown';
import ActionIcon from 'ui/ActionIcon';
import ImportCollection from 'components/Sidebar/ImportCollection';
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
import CreateApiSpec from 'components/Sidebar/ApiSpecs/CreateApiSpec';
@@ -75,12 +76,6 @@ const SidebarHeader = ({ setShowSearch }) => {
});
};
const addDropdownTippyRef = useRef();
const onAddDropdownCreate = (ref) => (addDropdownTippyRef.current = ref);
const actionsDropdownTippyRef = useRef();
const onActionsDropdownCreate = (ref) => (actionsDropdownTippyRef.current = ref);
const handleToggleSearch = () => {
if (setShowSearch) {
setShowSearch((prev) => !prev);
@@ -183,123 +178,112 @@ const SidebarHeader = ({ setShowSearch }) => {
</>
);
// Configuration for Add/Create dropdown items
const addDropdownItems = [
{
id: 'create',
leftSection: IconPlus,
label: 'Create collection',
onClick: () => {
setCreateCollectionModalOpen(true);
}
},
{
id: 'import',
leftSection: IconDownload,
label: 'Import collection',
onClick: () => {
setImportCollectionModalOpen(true);
}
},
{
id: 'open',
leftSection: IconFolder,
label: 'Open collection',
onClick: () => {
handleOpenCollection();
}
},
{
type: 'label',
label: 'API Specs'
},
{
id: 'create-api-spec',
leftSection: IconPlus,
label: 'Create API Spec',
onClick: () => {
setCreateApiSpecModalOpen(true);
}
},
{
id: 'open-api-spec',
leftSection: IconFileCode,
label: 'Open API Spec',
onClick: () => {
handleOpenApiSpec();
}
}
];
// Configuration for Actions dropdown items
const actionsDropdownItems = [
{
id: 'sort',
leftSection: getSortIcon(),
label: getSortLabel(),
onClick: () => {
handleSortCollections();
}
},
{
id: 'close-all',
leftSection: IconSquareX,
label: 'Close all',
onClick: () => {
selectAllCollectionsToClose();
}
}
];
// Render Collections-specific actions
const renderCollectionsActions = () => (
<>
<button
className="action-button"
<ActionIcon
onClick={handleToggleSearch}
title="Search requests"
label="Search requests"
>
<IconSearch size={14} stroke={1.5} />
</button>
{/* Add/Create dropdown */}
<Dropdown
onCreate={onAddDropdownCreate}
icon={(
<button className="action-button plus-icon-button" title="Add new">
<IconPlus size={14} stroke={1.5} />
</button>
)}
placement="bottom-end"
style="new"
>
<div className="label-item">Collections</div>
<div
className="dropdown-item"
onClick={() => {
setCreateCollectionModalOpen(true);
addDropdownTippyRef.current?.hide();
}}
>
<IconPlus size={16} stroke={1.5} className="icon" />
Create collection
</div>
<div
className="dropdown-item"
onClick={() => {
addDropdownTippyRef.current?.hide();
setImportCollectionModalOpen(true);
}}
>
<IconDownload size={16} stroke={1.5} className="icon" />
Import collection
</div>
<div
className="dropdown-item"
onClick={() => {
handleOpenCollection();
addDropdownTippyRef.current?.hide();
}}
>
<IconFolder size={16} stroke={1.5} className="icon" />
Open collection
</div>
<IconSearch size={14} stroke={1.5} aria-hidden="true" />
</ActionIcon>
<div className="label-item mt-2">API Specs</div>
<div
className="dropdown-item"
onClick={() => {
setCreateApiSpecModalOpen(true);
addDropdownTippyRef.current?.hide();
}}
>
<IconPlus size={16} stroke={1.5} className="icon" />
Create API Spec
</div>
<div
className="dropdown-item"
onClick={() => {
handleOpenApiSpec();
addDropdownTippyRef.current?.hide();
}}
>
<IconFileCode size={16} stroke={1.5} className="icon" />
Open API Spec
</div>
</Dropdown>
{/* Actions dropdown (sort, close all, etc.) */}
<Dropdown
onCreate={onActionsDropdownCreate}
icon={(
<button className="action-button" title="More actions">
<IconDotsVertical size={14} stroke={1.5} />
</button>
)}
{/* Add Collection dropdown */}
<MenuDropdown
data-testid="collections-header-add-menu"
items={[
{ type: 'label', label: 'Collections' },
...addDropdownItems
]}
placement="bottom-end"
style="new"
>
<div
className="dropdown-item"
onClick={() => {
handleSortCollections();
actionsDropdownTippyRef.current?.hide();
}}
aria-label="Sort collections"
title="Sort collections"
data-testid="sort-collections-button"
<ActionIcon
label="Add new collection"
>
{(() => {
const SortIcon = getSortIcon();
return <SortIcon size={16} stroke={1.5} className="icon" />;
})()}
{getSortLabel()}
</div>
<div
className="dropdown-item"
onClick={() => {
selectAllCollectionsToClose();
actionsDropdownTippyRef.current?.hide();
}}
aria-label="Close all collections"
title="Close all collections"
data-testid="close-all-collections-button"
<IconPlus size={14} stroke={1.5} aria-hidden="true" />
</ActionIcon>
</MenuDropdown>
{/* More Actions dropdown (sort, close all, etc.) */}
<MenuDropdown
data-testid="collections-header-actions-menu"
items={actionsDropdownItems}
placement="bottom-end"
>
<ActionIcon
label="More actions"
>
<IconSquareX size={16} stroke={1.5} className="icon" />
Close all
</div>
</Dropdown>
<IconDotsVertical size={14} stroke={1.5} aria-hidden="true" />
</ActionIcon>
</MenuDropdown>
{collectionsToClose.length > 0 && (
<RemoveCollectionsModal collectionUids={collectionsToClose} onClose={clearCollectionsToClose} />

View File

@@ -0,0 +1,53 @@
import styled, { css } from 'styled-components';
const sizeMap = {
xs: 20,
sm: 22,
md: 24,
lg: 28,
xl: 32
};
const variants = {
subtle: css`
color: ${(props) => props.theme.colors.text.muted};
background: transparent;
&:hover:not(:disabled) {
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.dropdown.hoverBg};
}
`
};
const StyledWrapper = styled.button`
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s ease;
padding: 0;
width: ${(props) => sizeMap[props.$size] || props.$size}px;
height: ${(props) => sizeMap[props.$size] || props.$size}px;
${(props) => variants[props.$variant] || variants.subtle}
svg {
stroke: currentColor;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
${(props) => props.$colorOnHover && css`
&:hover:not(:disabled) {
color: ${props.$colorOnHover};
}
`}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,52 @@
import React from 'react';
import StyledWrapper from './StyledWrapper';
/**
* ActionIcon - A reusable icon button component
*
* @param {Object} props
* @param {ReactNode} props.children - The icon component to render
* @param {string} props.variant - Visual variant: 'subtle' (default), 'filled', 'outline', etc.
* @param {string} props.size - Size of the button: 'sm', 'md', 'lg', etc. (default: 'md')
* @param {boolean} props.disabled - Whether the button is disabled
* @param {string} props.className - Additional CSS class names
* @param {string} props.component - Polymorphic component (default: 'button')
* @param {string} props.label - Label for both title and aria-label (preferred)
* @param {string} props.title - Title attribute (falls back to label or aria-label)
* @param {string} [props.ariaLabel] - Accessibility label (falls back to label or title)
* @param {string} props.colorOnHover - Color to apply to icon on hover/focus (e.g., 'red', '#ef4444', 'var(--color-danger)')
* @param {Object} props...rest - Other props passed to the underlying element
*/
const ActionIcon = ({
children,
variant = 'subtle',
size = 'md',
disabled = false,
className = '',
component: Component = 'button',
label,
'aria-label': ariaLabel,
colorOnHover,
...rest
}) => {
// Build className array and filter out empty strings
const classNames = ['action-icon', className].filter(Boolean).join(' ');
return (
<StyledWrapper
as={Component}
$variant={variant}
$size={size}
$colorOnHover={colorOnHover}
disabled={disabled}
className={classNames}
title={label}
aria-label={ariaLabel}
{...rest}
>
{children}
</StyledWrapper>
);
};
export default ActionIcon;

View File

@@ -0,0 +1,279 @@
import React from 'react';
import { useRef, useCallback, useState } from 'react';
import Dropdown from 'components/Dropdown';
// Constants
const NAVIGATION_KEYS = ['ArrowDown', 'ArrowUp', 'Home', 'End', 'Escape'];
const ACTION_KEYS = ['Enter', ' '];
// Calculate next index for keyboard navigation
const getNextIndex = (currentIndex, total, key, noFocus) => {
if (key === 'Home') return 0;
if (key === 'End') return total - 1;
if (key === 'ArrowDown') return noFocus ? 0 : (currentIndex + 1) % total;
if (key === 'ArrowUp') return noFocus ? total - 1 : (currentIndex - 1 + total) % total;
return currentIndex;
};
/**
* MenuDropdown - A reusable dropdown menu component with keyboard navigation
*
* @param {Object} props
* @param {Array} props.items - Array of menu items with structure:
* - id: string (unique identifier)
* - type: 'item' | 'label' | 'divider' (default: 'item')
* - leftSection: React component or React element (rendered on the left side, for items only)
* - rightSection: React component or React element (rendered on the right side, for items only)
* - label: string (display text for items, or label text for labels; also used for aria-label and title if not provided)
* - ariaLabel: string (accessibility label, falls back to label or title if not provided)
* - onClick: function (handler when item is clicked, for items only)
* - title: string (tooltip text, falls back to label or ariaLabel if not provided)
* - testId: string (optional, for testing, for items only)
* - disabled: boolean (optional, for items only)
* - className: string (optional, additional CSS classes for the item)
* @param {ReactNode} props.children - The trigger element (button, etc.)
* @param {string} props.placement - Tippy placement (default: 'bottom-end')
* @param {string} props.className - Optional className for the dropdown
* @param {string} props.selectedItemId - Optional ID of the selected/active item to focus on open
* @param {Object} props.dropdownProps - Other props passed to underlying Dropdown component
*/
const MenuDropdown = ({
items = [],
children,
placement = 'bottom-end',
className,
selectedItemId,
'data-testid': testId = 'menu-dropdown',
...dropdownProps
}) => {
const tippyRef = useRef();
const [isOpen, setIsOpen] = useState(false);
// Get all focusable menu items from the menu dropdown
const getMenuItems = useCallback(() => {
const popper = tippyRef.current?.popper;
if (!popper) return [];
const menuContainer = popper.querySelector('[role="menu"]');
if (!menuContainer) return [];
return Array.from(
menuContainer.querySelectorAll('[role="menuitem"]:not([aria-disabled="true"])')
);
}, []);
// Handle item click and close dropdown
const handleItemClick = useCallback((item) => {
if (item.disabled) return;
item.onClick?.();
setIsOpen(false);
}, []);
// Focus a menu item
const focusMenuItem = (item, addSelectedClass = false) => {
if (item) {
// Remove selected class from all items first
const menuContainer = item.closest('[role="menu"]');
if (menuContainer) {
menuContainer.querySelectorAll('.selected-focused').forEach((el) => {
el.classList.remove('selected-focused');
});
}
if (addSelectedClass) {
item.classList.add('selected-focused');
}
item.focus();
item.scrollIntoView({ block: 'nearest' });
}
};
// Keyboard navigation handler (handles all keyboard events at menu level)
const handleMenuKeyDown = useCallback((e) => {
const itemsToNavigate = getMenuItems();
if (itemsToNavigate.length === 0) return;
const currentIndex = itemsToNavigate.findIndex((el) => el === document.activeElement);
const isNoMenuItemFocused = currentIndex === -1;
// Handle Escape
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
setIsOpen(false);
return;
}
// Handle action keys (Enter, Space)
if (ACTION_KEYS.includes(e.key) && !isNoMenuItemFocused) {
e.preventDefault();
e.stopPropagation();
const currentItem = itemsToNavigate[currentIndex];
const itemId = currentItem?.getAttribute('data-item-id');
const item = items.find((i) => i.id === itemId);
if (item && !item.disabled) {
handleItemClick(item);
}
return;
}
// Handle navigation keys
if (NAVIGATION_KEYS.includes(e.key)) {
e.preventDefault();
e.stopPropagation();
const nextIndex = getNextIndex(currentIndex, itemsToNavigate.length, e.key, isNoMenuItemFocused);
focusMenuItem(itemsToNavigate[nextIndex], false);
}
}, [getMenuItems, items, handleItemClick]);
// Toggle dropdown visibility
const handleTriggerClick = useCallback(() => {
setIsOpen((prev) => !prev);
}, []);
// Close dropdown when clicking outside
const handleClickOutside = useCallback(() => {
setIsOpen(false);
}, []);
// Setup Tippy instance
const onDropdownCreate = useCallback((ref) => {
tippyRef.current = ref;
if (ref) {
ref.setProps({
onShow: () => {
// Focus selected item if available, otherwise focus menu container
setTimeout(() => {
const menuContainer = ref.popper?.querySelector('[role="menu"]');
if (!menuContainer) return;
// If selectedItemId is provided, find and focus that item
if (selectedItemId) {
const menuItems = Array.from(
menuContainer.querySelectorAll('[role="menuitem"]:not([aria-disabled="true"])')
);
const selectedItem = menuItems.find(
(item) => item.getAttribute('data-item-id') === selectedItemId
);
if (selectedItem) {
focusMenuItem(selectedItem, true);
return;
}
}
// Fallback: focus menu container
menuContainer.focus();
}, 0);
}
});
}
}, [selectedItemId]);
// Render section (left or right)
const renderSection = (section) => {
if (!section) return null;
// If it's a React component (function), render it with default icon props
if (typeof section === 'function') {
const SectionComponent = section;
return <SectionComponent size={16} stroke={1.5} className="dropdown-icon" aria-hidden="true" />;
}
// If it's already a React element, render it as-is
return section;
};
// Render menu item
const renderMenuItem = (item) => {
return (
<div
key={item.id}
className={`dropdown-item ${item.disabled ? 'disabled' : ''} ${item.className || ''}`.trim()}
role="menuitem"
data-item-id={item.id}
onClick={() => !item.disabled && handleItemClick(item)}
tabIndex={item.disabled ? -1 : 0}
aria-label={item.ariaLabel}
aria-disabled={item.disabled}
title={item.title}
data-testid={`${testId}-${item.id.toLowerCase()}`}
>
{renderSection(item.leftSection)}
<span className="dropdown-label">{item.label}</span>
{item.rightSection && (
<div
className="dropdown-right-section"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{renderSection(item.rightSection)}
</div>
)}
</div>
);
};
// Render label item
const renderLabel = (item) => (
<div key={item.id || `label-${item.label}`} className="label-item" role="presentation" data-testid={`${testId}-label-${item.label.toLowerCase().replace(/ /g, '-')}`}>
{item.label}
</div>
);
// Render divider item
const renderDivider = (item, index) => (
<div key={item.id || `divider-${index}`} className="dropdown-separator" role="separator" />
);
// Render menu content
const renderMenuContent = () => {
let dividerIndex = 0;
return items.map((item) => {
const itemType = item.type || 'item';
if (itemType === 'label') {
return renderLabel(item);
}
if (itemType === 'divider') {
return renderDivider(item, dividerIndex++);
}
return renderMenuItem(item);
});
};
// Clone children to attach click handler
const triggerElement = React.isValidElement(children)
? React.cloneElement(children, {
'onClick': (e) => {
children.props.onClick?.(e);
handleTriggerClick();
},
'data-testid': testId
})
: <div onClick={handleTriggerClick} data-testid={testId}>{children}</div>;
return (
<Dropdown
onCreate={onDropdownCreate}
icon={triggerElement}
placement={placement}
className={className}
visible={isOpen}
onClickOutside={handleClickOutside}
{...dropdownProps}
>
<div role="menu" tabIndex={-1} onKeyDown={handleMenuKeyDown}>
{renderMenuContent()}
</div>
</Dropdown>
);
};
export default MenuDropdown;

View File

@@ -17,7 +17,7 @@ const restartAppAndGetLocators = async (restartApp: (options?: { initUserDataPat
};
// TODO: These tests need to be updated for the new workspace UI
// The CollectionsHeader component (with close-all-collections-button) is not rendered in workspace mode
// The CollectionsHeader component (with collections-header-actions-menu-close-all) is not rendered in workspace mode
// The "Remove from workspace" flow is different from the old "Close collection" flow
test.describe.skip('Close All Collections', () => {
test.afterAll(async () => {

View File

@@ -8,7 +8,7 @@ test.describe('Create collection', () => {
});
test('Create collection and add a simple HTTP request', async ({ page, createTmpDir }) => {
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
await page.getByLabel('Name').click();
await page.getByLabel('Name').fill('test-collection');

View File

@@ -9,7 +9,7 @@ test.describe('Tag persistence', () => {
test('Verify tag persistence while moving requests within a collection', async ({ page, createTmpDir }) => {
// Create first collection - click plus icon button to open dropdown
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
await page.getByLabel('Name').fill('test-collection');
const locationInput = page.locator('.bruno-modal').getByLabel('Location');
@@ -79,7 +79,7 @@ test.describe('Tag persistence', () => {
test('verify tag persistence while moving requests between folders', async ({ page, createTmpDir }) => {
// Create first collection - click plus icon button to open dropdown
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
await page.getByLabel('Name').fill('test-collection');
const locationInput = page.locator('.bruno-modal').getByLabel('Location');

View File

@@ -9,7 +9,7 @@ test.describe('Move tabs', () => {
test('Verify tab move by drag and drop', async ({ page, createTmpDir }) => {
// Create a collection
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
await page.getByLabel('Name').fill('source-collection-drag-drop');
const locationInput = page.locator('.bruno-modal').getByLabel('Location');
@@ -100,7 +100,7 @@ test.describe('Move tabs', () => {
test('Verify tab move by keyboard shortcut', async ({ page, createTmpDir }) => {
// Create a collection
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
await page.getByLabel('Name').fill('source-collection-keyboard-shortcut');
const locationInput2 = page.locator('.bruno-modal').getByLabel('Location');

View File

@@ -57,7 +57,7 @@ test.describe('Open Multiple Collections', () => {
await expect(page.locator('#sidebar-collection-name').getByText('Test Collection 1')).not.toBeVisible();
// Click on plus icon button and then "Open collection" in the dropdown
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Open collection' }).click();
// Wait for both collections to appear in the sidebar
@@ -92,7 +92,7 @@ test.describe('Open Multiple Collections', () => {
},
{ collection1Dir, collection2Dir });
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Open collection' }).click();
// Wait for error toasts to appear

View File

@@ -12,7 +12,7 @@ test.describe('Collection Environment Import Tests', () => {
const envFile = path.join(__dirname, 'fixtures', 'collection-env.json');
// Import test collection
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
const importModal = page.locator('[data-testid="import-collection-modal"]');

View File

@@ -8,7 +8,7 @@ test.describe('Global Environment Import Tests', () => {
const globalEnvFile = path.join(__dirname, 'fixtures', 'global-env.json');
// Import test collection
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
const importModal = page.locator('[data-testid="import-collection-modal"]');

View File

@@ -5,7 +5,7 @@ test.describe('Import Corrupted Bruno Collection - Should Fail', () => {
test('Import Bruno collection with invalid JSON structure should fail', async ({ page }) => {
const brunoFile = path.resolve(__dirname, 'fixtures', 'bruno-malformed.json');
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// Wait for import collection modal to be ready

View File

@@ -5,7 +5,7 @@ test.describe('Import Bruno Collection - Missing Required Schema Fields', () =>
test('Import Bruno collection missing required version field should fail', async ({ page }) => {
const brunoFile = path.resolve(__dirname, 'fixtures', 'bruno-missing-required-fields.json');
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// Wait for import collection modal to be ready

View File

@@ -11,7 +11,7 @@ test.describe('Import Bruno Collection with Examples', () => {
const brunoFile = path.resolve(__dirname, 'fixtures', 'bruno-with-examples.json');
await test.step('Open import collection modal', async () => {
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
});

View File

@@ -2,7 +2,7 @@ import { test, expect } from '../../../playwright';
test.describe('File Input Acceptance', () => {
test('File input accepts expected file types', async ({ page }) => {
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// Check that file input exists (even if hidden)

View File

@@ -5,7 +5,7 @@ test.describe('Invalid File Handling', () => {
test('Handle invalid file without crashing', async ({ page }) => {
const invalidFile = path.resolve(__dirname, 'fixtures', 'invalid.txt');
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// Wait for import collection modal to be ready

View File

@@ -23,7 +23,7 @@ test.describe('Import Insomnia v4 Collection - Environment Import', () => {
const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v4-with-envs.json');
await test.step('Import Insomnia v4 collection with environments', async () => {
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
const importModal = page.getByTestId('import-collection-modal');

View File

@@ -23,7 +23,7 @@ test.describe('Import Insomnia v5 Collection - Environment Import', () => {
const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v5-with-envs.yaml');
await test.step('Import Insomnia v5 collection with environments', async () => {
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
const importModal = page.getByTestId('import-collection-modal');

View File

@@ -5,7 +5,7 @@ test.describe('Invalid Insomnia Collection - Missing Collection Array', () => {
test('Handle Insomnia v5 collection missing collection array', async ({ page }) => {
const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v5-invalid-missing-collection.yaml');
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// Wait for import collection modal to be ready

View File

@@ -5,7 +5,7 @@ test.describe('Invalid Insomnia Collection - Malformed Structure', () => {
test('Handle malformed Insomnia collection structure', async ({ page }) => {
const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-malformed.json');
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// Wait for import collection modal to be ready

View File

@@ -12,7 +12,7 @@ test.describe('OpenAPI Duplicate Names Handling', () => {
const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-duplicate-operation-name.yaml');
// start the import process
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// wait for the import collection modal to appear

View File

@@ -35,7 +35,7 @@ test.describe('Import OpenAPI Collection with Examples', () => {
}, { importDir });
await test.step('Open import collection modal', async () => {
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
});
@@ -152,7 +152,7 @@ test.describe('Import OpenAPI Collection with Examples', () => {
}, { importDir });
await test.step('Open import collection modal', async () => {
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
});

View File

@@ -5,7 +5,7 @@ test.describe('Invalid OpenAPI - Malformed YAML', () => {
test('Handle malformed OpenAPI YAML structure', async ({ page }) => {
const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-malformed.yaml');
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// Wait for import collection modal to be ready

View File

@@ -5,7 +5,7 @@ test.describe('Invalid OpenAPI - Missing Info Section', () => {
test('Handle OpenAPI specification missing required info section', async ({ page }) => {
const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-missing-info.yaml');
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// Wait for import collection modal to be ready

View File

@@ -12,7 +12,7 @@ test.describe('OpenAPI Newline Handling', () => {
const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-newline-in-operation-name.yaml');
// start the import process
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// wait for the import collection modal to appear

View File

@@ -12,7 +12,7 @@ test.describe('OpenAPI Path-Based Grouping', () => {
const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-path-grouping.json');
// Start the import process
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// Wait for import collection modal to be ready

View File

@@ -35,7 +35,7 @@ test.describe('Import Postman Collection with Examples', () => {
}, { importDir });
await test.step('Open import collection modal', async () => {
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
});

View File

@@ -5,7 +5,7 @@ test.describe('Invalid Postman Collection - Invalid JSON', () => {
test('Handle invalid JSON syntax', async ({ page }) => {
const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-invalid-schema.json');
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// Wait for import collection modal to be ready

View File

@@ -5,7 +5,7 @@ test.describe('Invalid Postman Collection - Missing Info', () => {
test('Handle Postman collection missing required info field', async ({ page }) => {
const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-invalid-missing-info.json');
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// Wait for import collection modal to be ready

View File

@@ -5,7 +5,7 @@ test.describe('Invalid Postman Collection - Invalid Schema', () => {
test('Handle Postman collection with invalid schema version', async ({ page }) => {
const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-invalid-schema.json');
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// Wait for import collection modal to be ready

View File

@@ -5,7 +5,7 @@ test.describe('Invalid Postman Collection - Malformed Structure', () => {
test('Handle malformed Postman collection structure', async ({ page }) => {
const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-malformed.json');
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// Wait for import collection modal to be ready

View File

@@ -13,7 +13,7 @@ test.describe('Import WSDL Collection', () => {
const wsdlFile = path.join(testDataDir, 'wsdl.xml');
await test.step('Open import collection modal', async () => {
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// Wait for import collection modal to be ready
@@ -71,7 +71,7 @@ test.describe('Import WSDL Collection', () => {
const wsdlFile = path.join(testDataDir, 'wsdl-bruno.json');
await test.step('Open import collection modal', async () => {
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// Wait for import collection modal to be ready

View File

@@ -56,7 +56,7 @@ test.describe('Default Collection Location Feature', () => {
test('Should use default location in Create Collection modal', async ({ pageWithUserData: page }) => {
// test Create Collection modal
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
// verify the default location is pre-filled (if location input is visible)

View File

@@ -19,7 +19,7 @@ test.describe('Code Generation URL Encoding', () => {
createTmpDir
}) => {
// Use plus icon button in new workspace UI
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
await page.getByLabel('Name').fill('unencoded-test-collection');
const locationInput = page.getByLabel('Location');
@@ -65,7 +65,7 @@ test.describe('Code Generation URL Encoding', () => {
createTmpDir
}) => {
// Use plus icon button in new workspace UI
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
await page.getByLabel('Name').fill('encoded-test-collection');
const locationInput = page.getByLabel('Location');

View File

@@ -10,7 +10,7 @@ test('should persist request with newlines across app restarts', async ({ create
const app1 = await launchElectronApp({ userDataPath });
const page = await app1.firstWindow();
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
await page.locator('.bruno-modal').getByLabel('Name').fill('newlines-persistence');
await page.locator('.bruno-modal').getByLabel('Location').fill(collectionPath);

View File

@@ -9,7 +9,7 @@ const isRequestSaved = async (saveButton: Locator) => {
};
const setup = async (page: Page, createTmpDir: (tag?: string | undefined) => Promise<string>) => {
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
await page.getByLabel('Name').fill('source-collection');
const locationInput = page.getByLabel('Location');

View File

@@ -64,7 +64,7 @@ type CreateCollectionOptions = {
*/
const createCollection = async (page, collectionName: string, collectionLocation: string, options: CreateCollectionOptions = {}) => {
await test.step(`Create collection "${collectionName}"`, async () => {
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
const createCollectionModal = page.locator('.bruno-modal-card').filter({ hasText: 'Create Collection' });
@@ -184,7 +184,7 @@ const importCollection = async (
await test.step(`Import collection from "${filePath}"`, async () => {
const locators = buildCommonLocators(page);
await page.locator('.plus-icon-button').click();
await page.getByTestId('collections-header-add-menu').click();
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();
// Wait for import modal

View File

@@ -17,7 +17,7 @@ export const buildCommonLocators = (page: Page) => ({
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'),
closeAllCollectionsButton: () => page.getByTestId('collections-header-actions-menu-close-all'),
collectionRow: (name: string) => page.locator('.collection-name').filter({
has: page.locator('#sidebar-collection-name', { hasText: name })
})
@@ -81,7 +81,7 @@ export const buildCommonLocators = (page: Page) => ({
copyButton: () => page.locator('button[title="Copy response to clipboard"]')
},
plusMenu: {
button: () => page.locator('.plus-icon-button'),
button: () => page.getByTestId('collections-header-add-menu'),
createCollection: () => page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }),
importCollection: () => page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' })
},