diff --git a/apps/ui/src/components/layout/sidebar/components/collapse-toggle-button.tsx b/apps/ui/src/components/layout/sidebar/components/collapse-toggle-button.tsx index 29a716444..2a503fc57 100644 --- a/apps/ui/src/components/layout/sidebar/components/collapse-toggle-button.tsx +++ b/apps/ui/src/components/layout/sidebar/components/collapse-toggle-button.tsx @@ -25,7 +25,7 @@ export function CollapseToggleButton({ + + + Running Agents + {runningAgentsCount > 0 && ( + + {runningAgentsCount} + + )} + + + + )} + + {/* Settings */} + + + + + + + Global Settings + + {formatShortcut(shortcuts.settings, true)} + + + + + + {/* Documentation */} + {!hideWiki && ( + + + + + + + Documentation + + + + )} + + {/* Feedback */} + + + + + + + Feedback + + + + + + ); + } + + // Expanded state return ( -
+
{/* Running Agents Link */} {!hideRunningAgents && ( -
+
)} + {/* Settings Link */} -
+
+
+ + {/* Separator */} +
+ + {/* Documentation Link */} + {!hideWiki && ( +
+ +
+ )} + + {/* Feedback Link */} +
+
+ + {/* Version */} +
+ + v{appVersion} {versionSuffix} + +
); } diff --git a/apps/ui/src/components/layout/sidebar/components/sidebar-header.tsx b/apps/ui/src/components/layout/sidebar/components/sidebar-header.tsx index 8f3d921e8..afca3e9c2 100644 --- a/apps/ui/src/components/layout/sidebar/components/sidebar-header.tsx +++ b/apps/ui/src/components/layout/sidebar/components/sidebar-header.tsx @@ -1,179 +1,411 @@ -import { useState } from 'react'; -import { Folder, LucideIcon, X, Menu, Check } from 'lucide-react'; +import { useState, useCallback } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { ChevronsUpDown, Folder, Plus, FolderOpen } from 'lucide-react'; import * as LucideIcons from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; import { cn, isMac } from '@/lib/utils'; -import { getAuthenticatedImageUrl } from '@/lib/api-fetch'; +import { formatShortcut } from '@/store/app-store'; import { isElectron, type Project } from '@/lib/electron'; -import { useIsCompact } from '@/hooks/use-media-query'; -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { getAuthenticatedImageUrl } from '@/lib/api-fetch'; import { useAppStore } from '@/store/app-store'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; interface SidebarHeaderProps { sidebarOpen: boolean; currentProject: Project | null; - onClose?: () => void; - onExpand?: () => void; + onNewProject: () => void; + onOpenFolder: () => void; + onProjectContextMenu: (project: Project, event: React.MouseEvent) => void; } export function SidebarHeader({ sidebarOpen, currentProject, - onClose, - onExpand, + onNewProject, + onOpenFolder, + onProjectContextMenu, }: SidebarHeaderProps) { - const isCompact = useIsCompact(); - const [projectListOpen, setProjectListOpen] = useState(false); + const navigate = useNavigate(); const { projects, setCurrentProject } = useAppStore(); - // Get the icon component from lucide-react - const getIconComponent = (): LucideIcon => { - if (currentProject?.icon && currentProject.icon in LucideIcons) { - return (LucideIcons as unknown as Record)[currentProject.icon]; + const [dropdownOpen, setDropdownOpen] = useState(false); + + const handleLogoClick = useCallback(() => { + navigate({ to: '/dashboard' }); + }, [navigate]); + + const handleProjectSelect = useCallback( + (project: Project) => { + setCurrentProject(project); + setDropdownOpen(false); + navigate({ to: '/board' }); + }, + [setCurrentProject, navigate] + ); + + const getIconComponent = (project: Project): LucideIcon => { + if (project.icon && project.icon in LucideIcons) { + return (LucideIcons as unknown as Record)[project.icon]; } return Folder; }; - const IconComponent = getIconComponent(); - const hasCustomIcon = !!currentProject?.customIconPath; + const renderProjectIcon = (project: Project, size: 'sm' | 'md' = 'md') => { + const IconComponent = getIconComponent(project); + const sizeClasses = size === 'sm' ? 'w-6 h-6' : 'w-8 h-8'; + const iconSizeClasses = size === 'sm' ? 'w-4 h-4' : 'w-5 h-5'; + + if (project.customIconPath) { + return ( + {project.name} + ); + } + + return ( +
+ +
+ ); + }; + + // Collapsed state - show logo only + if (!sidebarOpen) { + return ( +
+ + + + + + + Go to Dashboard + + + + {/* Collapsed project icon with dropdown */} + {currentProject && ( + <> +
+ + + + + + + + + + {currentProject.name} + + + + +
+ Projects +
+ {projects.map((project, index) => { + const isActive = currentProject?.id === project.id; + const hotkeyLabel = index < 9 ? `${index + 1}` : index === 9 ? '0' : undefined; + + return ( + handleProjectSelect(project)} + onContextMenu={(e) => { + e.preventDefault(); + e.stopPropagation(); + setDropdownOpen(false); + onProjectContextMenu(project, e); + }} + className="flex items-center gap-3 cursor-pointer" + data-testid={`collapsed-project-item-${project.id}`} + > + {renderProjectIcon(project, 'sm')} + + {project.name} + + {hotkeyLabel && ( + + {formatShortcut(`Cmd+${hotkeyLabel}`, true)} + + )} + + ); + })} + + { + setDropdownOpen(false); + onNewProject(); + }} + className="cursor-pointer" + data-testid="collapsed-new-project-dropdown-item" + > + + New Project + + { + setDropdownOpen(false); + onOpenFolder(); + }} + className="cursor-pointer" + data-testid="collapsed-open-project-dropdown-item" + > + + Open Project + +
+
+ + )} +
+ ); + } + + // Expanded state - show logo + project dropdown return (
- {/* Mobile close button - only visible on mobile when sidebar is open */} - {sidebarOpen && onClose && ( + {/* Header with logo and project dropdown */} +
+ {/* Logo */} - )} - {/* Mobile expand button - hamburger menu to expand sidebar when collapsed on mobile */} - {!sidebarOpen && isCompact && onExpand && ( - - )} - {/* Project name and icon display - entire element clickable on mobile */} - {currentProject && ( - - - + + {/* Project Dropdown */} + {currentProject ? ( + + + + + +
+ Projects
- - {/* Project Name - only show when sidebar is open */} - {sidebarOpen && ( -
-

- {currentProject.name} -

-
- )} - -
- -
-

Switch Project

- {projects.map((project) => { - const ProjectIcon = - project.icon && project.icon in LucideIcons - ? (LucideIcons as unknown as Record)[project.icon] - : Folder; + {projects.map((project, index) => { const isActive = currentProject?.id === project.id; + const hotkeyLabel = index < 9 ? `${index + 1}` : index === 9 ? '0' : undefined; return ( - + ); })} -
-
-
- )} + + { + setDropdownOpen(false); + onNewProject(); + }} + className="cursor-pointer" + data-testid="new-project-dropdown-item" + > + + New Project + + { + setDropdownOpen(false); + onOpenFolder(); + }} + className="cursor-pointer" + data-testid="open-project-dropdown-item" + > + + Open Project + + + + ) : ( +
+ + +
+ )} +
); } diff --git a/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx b/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx index c4956159e..4a1ab1fc0 100644 --- a/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx +++ b/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx @@ -1,9 +1,24 @@ +import { useState, useCallback, useEffect, useRef } from 'react'; import type { NavigateOptions } from '@tanstack/react-router'; +import { ChevronDown, Wrench, Github } from 'lucide-react'; import { cn } from '@/lib/utils'; import { formatShortcut } from '@/store/app-store'; import type { NavSection } from '../types'; import type { Project } from '@/lib/electron'; import { Spinner } from '@/components/ui/spinner'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; + +// Map section labels to icons +const sectionIcons: Record> = { + Tools: Wrench, + GitHub: Github, +}; interface SidebarNavigationProps { currentProject: Project | null; @@ -11,6 +26,7 @@ interface SidebarNavigationProps { navSections: NavSection[]; isActiveRoute: (id: string) => boolean; navigate: (opts: NavigateOptions) => void; + onScrollStateChange?: (canScrollDown: boolean) => void; } export function SidebarNavigation({ @@ -19,174 +35,299 @@ export function SidebarNavigation({ navSections, isActiveRoute, navigate, + onScrollStateChange, }: SidebarNavigationProps) { + const navRef = useRef(null); + + // Track collapsed state for each collapsible section + const [collapsedSections, setCollapsedSections] = useState>({}); + + // Initialize collapsed state when sections change (e.g., GitHub section appears) + useEffect(() => { + setCollapsedSections((prev) => { + const updated = { ...prev }; + navSections.forEach((section) => { + if (section.collapsible && section.label && !(section.label in updated)) { + updated[section.label] = section.defaultCollapsed ?? false; + } + }); + return updated; + }); + }, [navSections]); + + // Check scroll state + const checkScrollState = useCallback(() => { + if (!navRef.current || !onScrollStateChange) return; + const { scrollTop, scrollHeight, clientHeight } = navRef.current; + const canScrollDown = scrollTop + clientHeight < scrollHeight - 10; + onScrollStateChange(canScrollDown); + }, [onScrollStateChange]); + + // Monitor scroll state + useEffect(() => { + checkScrollState(); + const nav = navRef.current; + if (!nav) return; + + nav.addEventListener('scroll', checkScrollState); + const resizeObserver = new ResizeObserver(checkScrollState); + resizeObserver.observe(nav); + + return () => { + nav.removeEventListener('scroll', checkScrollState); + resizeObserver.disconnect(); + }; + }, [checkScrollState, collapsedSections]); + + const toggleSection = useCallback((label: string) => { + setCollapsedSections((prev) => ({ + ...prev, + [label]: !prev[label], + })); + }, []); + + // Filter sections: always show non-project sections, only show project sections when project exists + const visibleSections = navSections.filter((section) => { + // Always show Dashboard (first section with no label) + if (!section.label && section.items.some((item) => item.id === 'dashboard')) { + return true; + } + // Show other sections only when project is selected + return !!currentProject; + }); + return ( -
- )) - ) : null} + ); + })} + + {/* Placeholder when no project is selected */} + {!currentProject && sidebarOpen && ( +
+

+ Select or create a project to continue +

+
+ )} ); } diff --git a/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts b/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts index 91b40e4ac..df5d033f5 100644 --- a/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts +++ b/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts @@ -13,6 +13,7 @@ import { Network, Bell, Settings, + Home, } from 'lucide-react'; import type { NavSection, NavItem } from '../types'; import type { KeyboardShortcut } from '@/hooks/use-keyboard-shortcuts'; @@ -174,13 +175,30 @@ export function useNavigation({ } const sections: NavSection[] = [ + // Dashboard - standalone at top + { + label: '', + items: [ + { + id: 'dashboard', + label: 'Dashboard', + icon: Home, + }, + ], + }, + // Project section - expanded by default { label: 'Project', items: projectItems, + collapsible: true, + defaultCollapsed: false, }, + // Tools section - collapsed by default { label: 'Tools', items: visibleToolsItems, + collapsible: true, + defaultCollapsed: true, }, ]; @@ -203,6 +221,8 @@ export function useNavigation({ shortcut: shortcuts.githubPrs, }, ], + collapsible: true, + defaultCollapsed: true, }); } diff --git a/apps/ui/src/components/layout/sidebar/index.ts b/apps/ui/src/components/layout/sidebar/index.ts new file mode 100644 index 000000000..bfed62466 --- /dev/null +++ b/apps/ui/src/components/layout/sidebar/index.ts @@ -0,0 +1 @@ +export { Sidebar } from './sidebar'; diff --git a/apps/ui/src/components/layout/sidebar.tsx b/apps/ui/src/components/layout/sidebar/sidebar.tsx similarity index 73% rename from apps/ui/src/components/layout/sidebar.tsx rename to apps/ui/src/components/layout/sidebar/sidebar.tsx index 05ff1328c..5b63921f7 100644 --- a/apps/ui/src/components/layout/sidebar.tsx +++ b/apps/ui/src/components/layout/sidebar/sidebar.tsx @@ -1,8 +1,7 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { createLogger } from '@automaker/utils/logger'; import { useNavigate, useLocation } from '@tanstack/react-router'; - -const logger = createLogger('Sidebar'); +import { PanelLeftClose, ChevronDown } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useAppStore } from '@/store/app-store'; import { useNotificationsStore } from '@/store/notifications-store'; @@ -10,22 +9,18 @@ import { useKeyboardShortcuts, useKeyboardShortcutsConfig } from '@/hooks/use-ke import { getElectronAPI } from '@/lib/electron'; import { initializeProject, hasAppSpec, hasAutomakerDir } from '@/lib/project-init'; import { toast } from 'sonner'; -import { DeleteProjectDialog } from '@/components/views/settings-view/components/delete-project-dialog'; -import { NewProjectModal } from '@/components/dialogs/new-project-modal'; -import { CreateSpecDialog } from '@/components/views/spec-view/dialogs'; +import { useIsCompact } from '@/hooks/use-media-query'; +import type { Project } from '@/lib/electron'; -// Local imports from subfolder +// Sidebar components import { + SidebarNavigation, CollapseToggleButton, + MobileSidebarToggle, SidebarHeader, - SidebarNavigation, SidebarFooter, - MobileSidebarToggle, -} from './sidebar/components'; -import { useIsCompact } from '@/hooks/use-media-query'; -import { PanelLeftClose } from 'lucide-react'; -import { TrashDialog, OnboardingDialog } from './sidebar/dialogs'; -import { SIDEBAR_FEATURE_FLAGS } from './sidebar/constants'; +} from './components'; +import { SIDEBAR_FEATURE_FLAGS } from './constants'; import { useSidebarAutoCollapse, useRunningAgents, @@ -35,7 +30,19 @@ import { useSetupDialog, useTrashOperations, useUnviewedValidations, -} from './sidebar/hooks'; +} from './hooks'; +import { TrashDialog, OnboardingDialog } from './dialogs'; + +// Reuse dialogs from project-switcher +import { ProjectContextMenu } from '../project-switcher/components/project-context-menu'; +import { EditProjectDialog } from '../project-switcher/components/edit-project-dialog'; + +// Import shared dialogs +import { DeleteProjectDialog } from '@/components/views/settings-view/components/delete-project-dialog'; +import { NewProjectModal } from '@/components/dialogs/new-project-modal'; +import { CreateSpecDialog } from '@/components/views/spec-view/dialogs'; + +const logger = createLogger('Sidebar'); export function Sidebar() { const navigate = useNavigate(); @@ -59,12 +66,14 @@ export function Sidebar() { moveProjectToTrash, specCreatingForProject, setSpecCreatingForProject, + setCurrentProject, } = useAppStore(); const isCompact = useIsCompact(); // Environment variable flags for hiding sidebar items - const { hideTerminal, hideRunningAgents, hideContext, hideSpecEditor } = SIDEBAR_FEATURE_FLAGS; + const { hideTerminal, hideRunningAgents, hideContext, hideSpecEditor, hideWiki } = + SIDEBAR_FEATURE_FLAGS; // Get customizable keyboard shortcuts const shortcuts = useKeyboardShortcutsConfig(); @@ -72,6 +81,13 @@ export function Sidebar() { // Get unread notifications count const unreadNotificationsCount = useNotificationsStore((s) => s.unreadCount); + // State for context menu + const [contextMenuProject, setContextMenuProject] = useState(null); + const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>( + null + ); + const [editDialogProject, setEditDialogProject] = useState(null); + // State for delete project confirmation dialog const [showDeleteProjectDialog, setShowDeleteProjectDialog] = useState(false); @@ -129,7 +145,7 @@ export function Sidebar() { const isCurrentProjectGeneratingSpec = specCreatingForProject !== null && specCreatingForProject === currentProject?.path; - // Auto-collapse sidebar on small screens and update Electron window minWidth + // Auto-collapse sidebar on small screens useSidebarAutoCollapse({ sidebarOpen, toggleSidebar }); // Running agents count @@ -163,9 +179,28 @@ export function Sidebar() { setNewProjectPath, }); + // Context menu handlers + const handleContextMenu = useCallback((project: Project, event: React.MouseEvent) => { + event.preventDefault(); + setContextMenuProject(project); + setContextMenuPosition({ x: event.clientX, y: event.clientY }); + }, []); + + const handleCloseContextMenu = useCallback(() => { + setContextMenuProject(null); + setContextMenuPosition(null); + }, []); + + const handleEditProject = useCallback( + (project: Project) => { + setEditDialogProject(project); + handleCloseContextMenu(); + }, + [handleCloseContextMenu] + ); + /** * Opens the system folder selection dialog and initializes the selected project. - * Used by both the 'O' keyboard shortcut and the folder icon button. */ const handleOpenFolder = useCallback(async () => { const api = getElectronAPI(); @@ -173,14 +208,10 @@ export function Sidebar() { if (!result.canceled && result.filePaths[0]) { const path = result.filePaths[0]; - // Extract folder name from path (works on both Windows and Mac/Linux) const name = path.split(/[/\\]/).filter(Boolean).pop() || 'Untitled Project'; try { - // Check if this is a brand new project (no .automaker directory) const hadAutomakerDir = await hasAutomakerDir(path); - - // Initialize the .automaker directory structure const initResult = await initializeProject(path); if (!initResult.success) { @@ -190,15 +221,10 @@ export function Sidebar() { return; } - // Upsert project and set as current (handles both create and update cases) - // Theme handling (trashed project recovery or undefined for global) is done by the store upsertAndSetCurrentProject(path, name); - - // Check if app_spec.txt exists const specExists = await hasAppSpec(path); if (!hadAutomakerDir && !specExists) { - // This is a brand new project - show setup dialog setSetupProjectPath(path); setShowSetupDialog(true); toast.success('Project opened', { @@ -213,6 +239,8 @@ export function Sidebar() { description: `Opened ${name}`, }); } + + navigate({ to: '/board' }); } catch (error) { logger.error('Failed to open project:', error); toast.error('Failed to open project', { @@ -220,9 +248,13 @@ export function Sidebar() { }); } } - }, [upsertAndSetCurrentProject]); + }, [upsertAndSetCurrentProject, navigate, setSetupProjectPath, setShowSetupDialog]); + + const handleNewProject = useCallback(() => { + setShowNewProjectModal(true); + }, [setShowNewProjectModal]); - // Navigation sections and keyboard shortcuts (defined after handlers) + // Navigation sections and keyboard shortcuts const { navSections, navigationShortcuts } = useNavigation({ shortcuts, hideSpecEditor, @@ -244,12 +276,48 @@ export function Sidebar() { // Register keyboard shortcuts useKeyboardShortcuts(navigationShortcuts); + // Keyboard shortcuts for project switching (1-9, 0) + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + const target = event.target as HTMLElement; + if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { + return; + } + + if (event.ctrlKey || event.metaKey || event.altKey) { + return; + } + + const key = event.key; + let projectIndex: number | null = null; + + if (key >= '1' && key <= '9') { + projectIndex = parseInt(key, 10) - 1; + } else if (key === '0') { + projectIndex = 9; + } + + if (projectIndex !== null && projectIndex < projects.length) { + const targetProject = projects[projectIndex]; + if (targetProject && targetProject.id !== currentProject?.id) { + setCurrentProject(targetProject); + navigate({ to: '/board' }); + } + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [projects, currentProject, setCurrentProject, navigate]); + const isActiveRoute = (id: string) => { - // Map view IDs to route paths const routePath = id === 'welcome' ? '/' : `/${id}`; return location.pathname === routePath; }; + // Track if nav can scroll down + const [canScrollDown, setCanScrollDown] = useState(false); + // Check if sidebar should be completely hidden on mobile const shouldHideSidebar = isCompact && mobileSidebarHidden; @@ -266,6 +334,7 @@ export function Sidebar() { data-testid="sidebar-backdrop" /> )} +
+ {/* Scroll indicator - shows there's more content below */} + {canScrollDown && sidebarOpen && ( +
+ +
+ )} + + + + {/* Context Menu */} + {contextMenuProject && contextMenuPosition && ( + + )} + + {/* Edit Project Dialog */} + {editDialogProject && ( + !open && setEditDialogProject(null)} + /> + )} ); } diff --git a/apps/ui/src/components/layout/sidebar/types.ts b/apps/ui/src/components/layout/sidebar/types.ts index c86a0334e..a7486f052 100644 --- a/apps/ui/src/components/layout/sidebar/types.ts +++ b/apps/ui/src/components/layout/sidebar/types.ts @@ -4,6 +4,10 @@ import type React from 'react'; export interface NavSection { label?: string; items: NavItem[]; + /** Whether this section can be collapsed */ + collapsible?: boolean; + /** Whether this section should start collapsed */ + defaultCollapsed?: boolean; } export interface NavItem { diff --git a/apps/ui/src/routes/__root.tsx b/apps/ui/src/routes/__root.tsx index 907d2b196..f374b7dd0 100644 --- a/apps/ui/src/routes/__root.tsx +++ b/apps/ui/src/routes/__root.tsx @@ -4,7 +4,6 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { createLogger } from '@automaker/utils/logger'; import { Sidebar } from '@/components/layout/sidebar'; -import { ProjectSwitcher } from '@/components/layout/project-switcher'; import { FileBrowserProvider, useFileBrowser, @@ -171,8 +170,6 @@ function RootLayoutContent() { skipSandboxWarning, setSkipSandboxWarning, fetchCodexModels, - sidebarOpen, - toggleSidebar, } = useAppStore(); const { setupComplete, codexCliStatus } = useSetupStore(); const navigate = useNavigate(); @@ -186,7 +183,7 @@ function RootLayoutContent() { // Load project settings when switching projects useProjectSettingsLoader(); - // Check if we're in compact mode (< 1240px) to hide project switcher + // Check if we're in compact mode (< 1240px) const isCompact = useIsCompact(); const isSetupRoute = location.pathname === '/setup'; @@ -853,11 +850,6 @@ function RootLayoutContent() { ); } - // Show project switcher on all app pages (not on dashboard, setup, or login) - // Also hide on compact screens (< 1240px) - the sidebar will show a logo instead - const showProjectSwitcher = - !isDashboardRoute && !isSetupRoute && !isLoginRoute && !isLoggedOutRoute && !isCompact; - return ( <>
@@ -868,7 +860,6 @@ function RootLayoutContent() { aria-hidden="true" /> )} - {showProjectSwitcher && }
{ await page.waitForTimeout(300); } - // Verify we're on the correct project (project switcher button shows project name) - // Use ends-with selector since data-testid format is: project-switcher-{id}-{sanitizedName} - const sanitizedProjectName = sanitizeForTestId(projectName); - await expect(page.locator(`[data-testid$="-${sanitizedProjectName}"]`)).toBeVisible({ + // Verify we're on the correct project (project dropdown trigger shows project name) + await expect( + page.locator('[data-testid="project-dropdown-trigger"]').getByText(projectName) + ).toBeVisible({ timeout: 10000, }); diff --git a/apps/ui/tests/projects/new-project-creation.spec.ts b/apps/ui/tests/projects/new-project-creation.spec.ts index 4599e8feb..07d5bc3bc 100644 --- a/apps/ui/tests/projects/new-project-creation.spec.ts +++ b/apps/ui/tests/projects/new-project-creation.spec.ts @@ -14,7 +14,6 @@ import { authenticateForTests, handleLoginScreenIfPresent, waitForNetworkIdle, - sanitizeForTestId, } from '../utils'; const TEST_TEMP_DIR = createTempDirPath('project-creation-test'); @@ -78,10 +77,10 @@ test.describe('Project Creation', () => { } // Wait for project to be set as current and visible on the page - // The project name appears in the project switcher button - // Use ends-with selector since data-testid format is: project-switcher-{id}-{sanitizedName} - const sanitizedProjectName = sanitizeForTestId(projectName); - await expect(page.locator(`[data-testid$="-${sanitizedProjectName}"]`)).toBeVisible({ + // The project name appears in the project dropdown trigger + await expect( + page.locator('[data-testid="project-dropdown-trigger"]').getByText(projectName) + ).toBeVisible({ timeout: 15000, }); diff --git a/apps/ui/tests/projects/open-existing-project.spec.ts b/apps/ui/tests/projects/open-existing-project.spec.ts index 0e3cb7891..4d8db61f3 100644 --- a/apps/ui/tests/projects/open-existing-project.spec.ts +++ b/apps/ui/tests/projects/open-existing-project.spec.ts @@ -18,7 +18,6 @@ import { authenticateForTests, handleLoginScreenIfPresent, waitForNetworkIdle, - sanitizeForTestId, } from '../utils'; // Create unique temp dir for this test run @@ -169,11 +168,11 @@ test.describe('Open Project', () => { } // Wait for a project to be set as current and visible on the page - // The project name appears in the project switcher button - // Use ends-with selector since data-testid format is: project-switcher-{id}-{sanitizedName} + // The project name appears in the project dropdown trigger if (targetProjectName) { - const sanitizedName = sanitizeForTestId(targetProjectName); - await expect(page.locator(`[data-testid$="-${sanitizedName}"]`)).toBeVisible({ + await expect( + page.locator('[data-testid="project-dropdown-trigger"]').getByText(targetProjectName) + ).toBeVisible({ timeout: 15000, }); }