diff --git a/packages/app/e2e/browser/workspace-relative-navigation.spec.ts b/packages/app/e2e/browser/workspace-relative-navigation.spec.ts new file mode 100644 index 0000000000..4853e9daa4 --- /dev/null +++ b/packages/app/e2e/browser/workspace-relative-navigation.spec.ts @@ -0,0 +1,74 @@ +import type { Page } from "@playwright/test"; +import { test, expect } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; +import { + expectSidebarWorkspaceSelected, + switchWorkspaceViaSidebar, +} from "../support/helpers/workspace-ui"; + +async function readVisibleWorkspaceOrder(page: Page): Promise { + const rows = page.getByRole("button", { name: /^Workspace \d{2}$/ }); + await expect(rows).toHaveCount(12); + return rows.evaluateAll( + (elements, prefix) => + elements.map((element) => element.getAttribute("data-testid")!.slice(prefix.length)), + `sidebar-workspace-row-${getServerId()}:`, + ); +} + +async function navigateWorkspace( + page: Page, + direction: "next" | "previous", + workspaceId: string, +): Promise { + await page.keyboard.press(direction === "next" ? "Alt+]" : "Alt+["); + await expectSidebarWorkspaceSelected({ page, serverId: getServerId(), workspaceId }); + await expect(page).toHaveURL(new RegExp(`/workspace/${workspaceId}$`)); +} + +test("previous and next traverse every visible workspace while digit shortcuts stop at nine", async ({ + page, +}) => { + test.setTimeout(120_000); + const project = await seedWorkspace({ + repoPrefix: "relative-navigation-", + title: "Workspace 01", + }); + try { + for (let index = 2; index <= 12; index += 1) { + const created = await project.client.createWorkspace({ + source: { kind: "directory", path: project.repoPath }, + title: `Workspace ${String(index).padStart(2, "0")}`, + }); + expect(created.error).toBeNull(); + } + await page.setViewportSize({ width: 1440, height: 1100 }); + await gotoAppShell(page); + const order = await readVisibleWorkspaceOrder(page); + + await test.step("next crosses the ninth workspace and previous returns to it", async () => { + await switchWorkspaceViaSidebar({ page, serverId: getServerId(), workspaceId: order[8] }); + await navigateWorkspace(page, "next", order[9]); + await navigateWorkspace(page, "previous", order[8]); + }); + + await test.step("wrapping uses the true first and last visible workspace", async () => { + await switchWorkspaceViaSidebar({ page, serverId: getServerId(), workspaceId: order[0] }); + await navigateWorkspace(page, "previous", order[11]); + await navigateWorkspace(page, "next", order[0]); + }); + + await test.step("the ninth digit shortcut still selects the ninth workspace", async () => { + await page.keyboard.press("Alt+9"); + await expectSidebarWorkspaceSelected({ + page, + serverId: getServerId(), + workspaceId: order[8], + }); + }); + } finally { + await project.cleanup(); + } +}); diff --git a/packages/app/src/utils/sidebar-shortcuts.test.ts b/packages/app/src/utils/sidebar-shortcuts.test.ts index ac9663c6da..b05c930123 100644 --- a/packages/app/src/utils/sidebar-shortcuts.test.ts +++ b/packages/app/src/utils/sidebar-shortcuts.test.ts @@ -5,6 +5,7 @@ import type { SidebarWorkspaceEntry, } from "@/hooks/use-sidebar-workspaces-list"; import { buildStatusGroups } from "@/hooks/sidebar-status-view-model"; +import { routeKeyboardShortcut } from "@/keyboard/route-shortcut"; import { buildSidebarShortcutModel, @@ -65,6 +66,46 @@ function project(projectKey: string, workspaces: SidebarWorkspaceEntry[]): Sideb } describe("buildSidebarShortcutModel", () => { + it.each([ + [9, 1, 10], + [10, -1, 9], + [12, 1, 1], + [1, -1, 12], + ] as const)( + "navigates from workspace %i by %i to workspace %i across the full visible sidebar", + (current, delta, next) => { + const workspaces = Array.from({ length: 12 }, (_, index) => + workspace({ + serverId: "s", + workspaceId: `ws-${index + 1}`, + workspaceDirectory: `/repo/w${index + 1}`, + name: `w${index + 1}`, + }), + ); + const model = buildSidebarShortcutModel({ + projects: [project("p", workspaces)], + collapsedProjectKeys: new Set(), + }); + + expect( + routeKeyboardShortcut( + { action: "workspace.navigate.relative", payload: { delta } }, + { + pathname: `/h/s/workspace/ws-${current}`, + isMobile: false, + sidebarShortcutTargets: model.shortcutTargets, + navigationActiveWorkspace: null, + commandCenterOpen: false, + shortcutsDialogOpen: false, + }, + ), + ).toEqual({ kind: "navigate-workspace", serverId: "s", workspaceId: `ws-${next}` }); + expect(model.shortcutIndexByWorkspaceKey.size).toBe(9); + expect(model.shortcutIndexByWorkspaceKey.get("s:ws-9")).toBe(9); + expect(model.shortcutIndexByWorkspaceKey.has("s:ws-10")).toBe(false); + }, + ); + it("builds shortcut targets in visual order and excludes collapsed projects", () => { const projects = [ project("p1", [ @@ -111,7 +152,7 @@ describe("buildSidebarShortcutModel", () => { expect(model.shortcutIndexByWorkspaceKey.get("s1:ws-repo2-main")).toBeUndefined(); }); - it("limits shortcuts to 9", () => { + it("keeps all traversal targets while numbering only the first 9", () => { const workspaces = Array.from({ length: 20 }, (_, index) => workspace({ serverId: "s", @@ -127,9 +168,11 @@ describe("buildSidebarShortcutModel", () => { collapsedProjectKeys: new Set(), }); - expect(model.shortcutTargets).toHaveLength(9); - expect(model.shortcutTargets[0]).toEqual({ serverId: "s", workspaceId: "ws-1" }); + expect(model.shortcutTargets).toHaveLength(20); expect(model.shortcutTargets[8]).toEqual({ serverId: "s", workspaceId: "ws-9" }); + expect(model.shortcutTargets[9]).toEqual({ serverId: "s", workspaceId: "ws-10" }); + expect(model.shortcutIndexByWorkspaceKey.get("s:ws-9")).toBe(9); + expect(model.shortcutIndexByWorkspaceKey.get("s:ws-10")).toBeUndefined(); }); it("excludes a collapsed project's workspaces regardless of project kind", () => { diff --git a/packages/app/src/utils/sidebar-shortcuts.ts b/packages/app/src/utils/sidebar-shortcuts.ts index 86540357e6..d08cba927c 100644 --- a/packages/app/src/utils/sidebar-shortcuts.ts +++ b/packages/app/src/utils/sidebar-shortcuts.ts @@ -70,13 +70,10 @@ export function buildSidebarShortcutSections(input: { } for (const workspace of section.workspaces) { - if (shortcutTargets.length >= maxShortcuts) { - break; - } - - const shortcutNumber = shortcutTargets.length + 1; shortcutTargets.push(createShortcutTarget(workspace)); - shortcutIndexByWorkspaceKey.set(workspace.workspaceKey, shortcutNumber); + if (shortcutTargets.length <= maxShortcuts) { + shortcutIndexByWorkspaceKey.set(workspace.workspaceKey, shortcutTargets.length); + } } }