diff --git a/packages/app/e2e/browser/sidebar-title-width.spec.ts b/packages/app/e2e/browser/sidebar-title-width.spec.ts new file mode 100644 index 0000000000..80f96dc68b --- /dev/null +++ b/packages/app/e2e/browser/sidebar-title-width.spec.ts @@ -0,0 +1,153 @@ +import { rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import type { Locator, Page, TestInfo } from "@playwright/test"; +import { expect, test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; +import { + closeSidebarDisplayPreferences, + openMobileAgentSidebar, + openSidebarDisplayPage, + selectSidebarStatusGrouping, +} from "../support/helpers/sidebar"; + +const TITLE = "Linux desktop sandbox launch support"; + +async function seedChangedWorkspace() { + const workspace = await seedWorkspace({ + repoPrefix: "sidebar-title-", + title: TITLE, + repo: { withRemote: true }, + }); + try { + await rm(path.join(workspace.repoPath, "remote.git"), { recursive: true }); + await workspace.client.renameProject(workspace.projectId, "Paseo"); + await writeFile(path.join(workspace.repoPath, "README.md"), "Changed line\n".repeat(12345)); + await workspace.client.checkoutRefresh(workspace.repoPath); + await expect + .poll(async () => { + const { entries } = await workspace.client.fetchWorkspaces(); + return entries.find((entry) => entry.id === workspace.workspaceId)?.diffStat?.additions; + }) + .toBe(12345); + return workspace; + } catch (error) { + await workspace.cleanup(); + throw error; + } +} + +function workspaceRow(page: Page) { + return page.getByRole("button", { name: new RegExp(TITLE) }).filter({ visible: true }); +} + +async function titleWidth(row: Locator) { + return row + .getByText(TITLE, { exact: true }) + .evaluate((element) => element.getBoundingClientRect().width); +} + +async function toggleTrailing(page: Page, label: "Diff stats" | "Last activity", touch = false) { + await openSidebarDisplayPage(page, "sidebar-display-show"); + await page.getByRole("menuitem", { name: label, exact: true }).click(); + if (touch) { + await page + .getByRole("button", { name: "Bottom sheet backdrop", exact: true }) + .click({ position: { x: 5, y: 5 } }); + await expect( + page.getByRole("button", { name: "Bottom sheet backdrop", exact: true }), + ).toHaveCount(0); + } else { + await closeSidebarDisplayPreferences(page); + } + await page.mouse.move(0, 0); +} + +async function expectTouchTitleWidth(page: Page) { + const row = workspaceRow(page); + const withDiff = await titleWidth(row); + await toggleTrailing(page, "Diff stats", true); + const withoutStats = await titleWidth(row); + expect(withDiff).toBeCloseTo(withoutStats, 0); + await toggleTrailing(page, "Last activity", true); + expect(await titleWidth(row)).toBeCloseTo(withoutStats, 0); + await toggleTrailing(page, "Diff stats", true); +} + +let workspace: SeededWorkspace; +test.beforeEach(async () => { + workspace = await seedChangedWorkspace(); +}); +test.afterEach(async () => { + await workspace?.cleanup(); +}); + +async function openTouchWorkspaceList(page: Page, testInfo: TestInfo) { + await page.setViewportSize({ width: 390, height: 844 }); + await page.emulateMedia({ colorScheme: "dark" }); + await gotoAppShell(page); + await openMobileAgentSidebar(page); + await expect(workspaceRow(page)).toBeVisible(); + await page.mouse.move(0, 0); + await page.screenshot({ path: testInfo.outputPath("sidebar.png") }); +} + +async function pinWorkspaceForLayout(page: Page) { + await workspace.client.setWorkspacePinned(workspace.workspaceId, true); + await expect(page.getByTestId("sidebar-pinned-section")).toBeVisible(); +} + +async function moveWorkspaceToStatusGrouping(page: Page) { + await workspace.client.setWorkspacePinned(workspace.workspaceId, false); + await selectSidebarStatusGrouping(page); +} + +async function openDesktopWorkspaceList(page: Page) { + await gotoAppShell(page); + const row = workspaceRow(page); + await expect(row).toBeVisible(); + await page.mouse.move(0, 0); + await expect(row.getByText("+12.3k", { exact: true })).toBeVisible(); + return titleWidth(row); +} + +async function expectHoverKeepsTitleWidth(page: Page, width: number, testInfo: TestInfo) { + const row = workspaceRow(page); + await row.hover(); + await expect(row.getByLabel("Workspace actions", { exact: true })).toBeVisible(); + expect(await titleWidth(row)).toBeCloseTo(width, 0); + await page.screenshot({ path: testInfo.outputPath("desktop-hover.png") }); +} + +async function expectShortcutHintsKeepTitleWidth(page: Page, width: number, testInfo: TestInfo) { + const row = workspaceRow(page); + await page.keyboard.down("Alt"); + await expect(row.getByText("1", { exact: true })).toBeVisible(); + expect(await titleWidth(row)).toBeCloseTo(width, 0); + await page.screenshot({ path: testInfo.outputPath("desktop-shortcuts.png") }); + await page.keyboard.up("Alt"); + await expect(row.getByText("1", { exact: true })).toHaveCount(0); +} + +async function expectDisablingStatsExpandsTitle(page: Page, width: number) { + await toggleTrailing(page, "Diff stats"); + expect(await titleWidth(workspaceRow(page))).toBeGreaterThan(width); +} + +test("touch titles use the space occupied by hidden stats in every grouping", async ({ + page, +}, testInfo) => { + await openTouchWorkspaceList(page, testInfo); + await expectTouchTitleWidth(page); + await pinWorkspaceForLayout(page); + await expectTouchTitleWidth(page); + await moveWorkspaceToStatusGrouping(page); + await expectTouchTitleWidth(page); +}); + +test("desktop hover and shortcut hints preserve title width", async ({ page }, testInfo) => { + const width = await openDesktopWorkspaceList(page); + await expectHoverKeepsTitleWidth(page, width, testInfo); + await expectShortcutHintsKeepTitleWidth(page, width, testInfo); + await expectDisablingStatsExpandsTitle(page, width); +}); diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index 23e4292aeb..4275099cfd 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -645,7 +645,7 @@ function WorkspaceRowRightGroup({ const trailing = useSidebarWorkspaceTrailing(); const showShortcut = showShortcutBadge && shortcutNumber !== null; const { - showTrailing, + trailingPresentation, showKebab: showKebabInSlot, showScrim, renderSlot, @@ -667,7 +667,7 @@ function WorkspaceRowRightGroup({ ) : null} {renderSlot ? ( - + { const showShortcut = showShortcutBadge && shortcutNumber !== null; const { - showTrailing, + trailingPresentation, showKebab: showKebabInSlot, showScrim, renderSlot, @@ -907,7 +908,7 @@ function StatusWorkspaceRowInnerContent({ workspace={workspace} backdrop={backdrop} trailing={trailing} - showBase={showTrailing} + trailingPresentation={trailingPresentation} showKebab={showKebabInSlot} showScrim={showScrim} reserveSlotWidth={reserveSlotWidth} @@ -938,7 +939,7 @@ function StatusWorkspaceActionSlot({ workspace, backdrop, trailing, - showBase, + trailingPresentation, showKebab, showScrim, reserveSlotWidth, @@ -958,7 +959,7 @@ function StatusWorkspaceActionSlot({ workspace: SidebarWorkspaceEntry; backdrop: SidebarSurfaceBackdrop; trailing: SidebarWorkspaceTrailing; - showBase: boolean; + trailingPresentation: SidebarWorkspaceTrailingPresentation; showKebab: boolean; showScrim: boolean; reserveSlotWidth: boolean; @@ -978,7 +979,7 @@ function StatusWorkspaceActionSlot({ const kebab = useOpenKebabMenuVisibility(showKebab); return ( - + {children}; + if (presentation === "absent") return null; + return ( + + {children} + + ); } export function SidebarWorkspaceTrailingActionOverlay({ diff --git a/packages/app/src/components/sidebar/sidebar-workspace-row.tsx b/packages/app/src/components/sidebar/sidebar-workspace-row.tsx index f019158e97..f8dcdebaf5 100644 --- a/packages/app/src/components/sidebar/sidebar-workspace-row.tsx +++ b/packages/app/src/components/sidebar/sidebar-workspace-row.tsx @@ -403,7 +403,7 @@ function WorkspaceRowTrailingActions({ const { t } = useTranslation(); const showShortcut = showShortcutBadge && shortcutNumber !== null; const { - showTrailing, + trailingPresentation, showKebab: showKebabInSlot, showScrim, renderSlot, @@ -425,7 +425,7 @@ function WorkspaceRowTrailingActions({ ) : null} {renderSlot ? ( - +