import { expect, type Page } from "@playwright/test"; import { getServerId } from "./server-id"; export async function selectWorkspaceInSidebar(page: Page, workspaceId: string): Promise { const row = page.getByTestId(`sidebar-workspace-row-${getServerId()}:${workspaceId}`); await expect(row).toBeVisible({ timeout: 30_000 }); await row.click(); } export async function expectWorkspaceListed(page: Page, name: string): Promise { await expect( page.locator('[data-testid^="sidebar-workspace-row-"]').filter({ hasText: name }).first(), ).toBeVisible({ timeout: 30_000 }); } // The workspace row kebab and its menu items carry no web ARIA role, so the sidebar // suite addresses them by the stable test ids the app assigns per workspace — the same // convention the rename flow uses. The kebab only reveals on hover. export async function archiveWorktreeFromSidebar(page: Page, workspaceId: string): Promise { const serverId = getServerId(); const row = page.getByTestId(`sidebar-workspace-row-${serverId}:${workspaceId}`); await expect(row).toBeVisible({ timeout: 30_000 }); await row.hover(); const kebab = page.getByTestId(`sidebar-workspace-kebab-${serverId}:${workspaceId}`); await expect(kebab).toBeVisible({ timeout: 10_000 }); await kebab.click(); // A clean worktree archives with no prompt; if the host reports unsynced work the app // raises a browser confirm. Accept it so the user-confirmed archive stays deterministic // either way. page.once("dialog", (dialog) => void dialog.accept()); const archiveItem = page.getByTestId(`sidebar-workspace-menu-archive-${serverId}:${workspaceId}`); await expect(archiveItem).toBeVisible({ timeout: 10_000 }); await archiveItem.click(); // Archiving the last reference to a worktree opens the keep/delete prompt. // This helper deletes the worktree from disk; callers that want to keep it use // openWorktreeDeletePrompt directly. const deleteButton = page.getByTestId("worktree-delete-confirm-delete"); await expect(deleteButton).toBeVisible({ timeout: 10_000 }); await deleteButton.click(); } // Opens the archive flow for a last-reference worktree and stops at the inline // keep/delete prompt, which the caller resolves by clicking keep or delete. export async function openWorktreeDeletePrompt(page: Page, workspaceId: string): Promise { const serverId = getServerId(); const row = page.getByTestId(`sidebar-workspace-row-${serverId}:${workspaceId}`); await expect(row).toBeVisible({ timeout: 30_000 }); await row.hover(); const kebab = page.getByTestId(`sidebar-workspace-kebab-${serverId}:${workspaceId}`); await expect(kebab).toBeVisible({ timeout: 10_000 }); await kebab.click(); // A dirty/unsynced worktree raises a browser confirm before the prompt; accept // it so the prompt opens deterministically either way. page.once("dialog", (dialog) => void dialog.accept()); const archiveItem = page.getByTestId(`sidebar-workspace-menu-archive-${serverId}:${workspaceId}`); await expect(archiveItem).toBeVisible({ timeout: 10_000 }); await archiveItem.click(); await expect(page.getByTestId("worktree-delete-confirm-keep")).toBeVisible({ timeout: 10_000 }); } export async function expectWorkspaceAbsentFromSidebar( page: Page, workspaceId: string, ): Promise { await expect( page.getByTestId(`sidebar-workspace-row-${getServerId()}:${workspaceId}`), ).toHaveCount(0, { timeout: 30_000 }); } export async function openMobileAgentSidebar(page: Page): Promise { await page.getByRole("button", { name: "Open menu" }).click(); } export async function closeMobileAgentSidebar(page: Page): Promise { const closeButton = page.getByTestId("sidebar-close"); await expect(closeButton).toBeInViewport({ timeout: 5_000 }); await closeButton.click({ force: true }); } // The mobile sidebar panel animates via translateX; toBeInViewport reflects the rendered position. export async function expectMobileAgentSidebarVisible(page: Page): Promise { await expect(page.getByTestId("sidebar-sessions")).toBeInViewport({ timeout: 5_000 }); } export async function expectMobileAgentSidebarHidden(page: Page): Promise { await expect(page.getByTestId("sidebar-sessions")).not.toBeInViewport({ timeout: 5_000 }); }