Files
paseo/packages/app/src/utils/sidebar-shortcuts.test.ts
T
paseo-bot[bot]andRaisal P Wardana cafa04d3fb fix(app): traverse workspaces beyond nine (#6586)
Relative navigation shared the list capped for digit shortcuts, so it wrapped at the ninth workspace. Keep all sidebar targets and cap only numbered badges. Carry forward #4128 with keyboard-routing and browser regressions.

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: Raisal P Wardana <IEatCodeDaily@users.noreply.github.com>
2026-10-14 13:14:08 +02:00

366 lines
11 KiB
TypeScript

import { i18n } from "@/i18n/i18next";
import { describe, expect, it } from "vitest";
import type {
SidebarProjectEntry,
SidebarWorkspaceEntry,
} from "@/hooks/use-sidebar-workspaces-list";
import { buildStatusGroups } from "@/hooks/sidebar-status-view-model";
import { routeKeyboardShortcut } from "@/keyboard/route-shortcut";
import {
buildSidebarShortcutModel,
buildStatusSidebarShortcutModel,
getRelativeSidebarShortcutTarget,
} from "./sidebar-shortcuts";
function workspace(input: {
serverId: string;
workspaceId: string;
workspaceDirectory: string;
name: string;
projectKey?: string;
statusBucket?: SidebarWorkspaceEntry["statusBucket"];
statusEnteredAt?: Date | null;
}): SidebarWorkspaceEntry {
return {
workspaceKey: `${input.serverId}:${input.workspaceId}`,
serverId: input.serverId,
workspaceId: input.workspaceId,
projectViewKey: input.projectKey ?? "project-default",
projectName: input.projectKey ?? "Project",
workspaceDirectory: input.workspaceDirectory,
workspaceDirectoryLabel: input.workspaceDirectory,
projectKind: "git",
workspaceKind: "checkout",
name: input.name,
title: null,
currentBranch: null,
statusBucket: input.statusBucket ?? "done",
archivingAt: null,
statusEnteredAt: input.statusEnteredAt ?? null,
diffStat: null,
prHint: null,
archiveHasUncommittedChanges: null,
archiveUnpushedCommitCount: null,
scripts: [],
hasRunningScripts: false,
};
}
function project(projectKey: string, workspaces: SidebarWorkspaceEntry[]): SidebarProjectEntry {
return {
viewKey: projectKey,
projectName: projectKey,
projectKind: "git",
iconWorkingDir: workspaces[0]?.workspaceDirectory ?? "",
hosts: [
{
serverId: workspaces[0]?.serverId ?? "s1",
projectId: projectKey,
iconWorkingDir: workspaces[0]?.workspaceDirectory ?? "",
worktreeSupport: "supported" as const,
},
],
workspaces,
};
}
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<string>(),
});
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", [
workspace({
serverId: "s1",
workspaceId: "ws-main",
workspaceDirectory: "/repo/main",
name: "main",
}),
workspace({
serverId: "s1",
workspaceId: "ws-feat-a",
workspaceDirectory: "/repo/feat-a",
name: "feat-a",
}),
]),
project("p2", [
workspace({
serverId: "s1",
workspaceId: "ws-repo2-main",
workspaceDirectory: "/repo2/main",
name: "main",
}),
workspace({
serverId: "s1",
workspaceId: "ws-repo2-feat-a",
workspaceDirectory: "/repo2/feat-a",
name: "feat-a",
}),
]),
];
const model = buildSidebarShortcutModel({
projects,
collapsedProjectKeys: new Set<string>(["p2"]),
});
expect(model.shortcutTargets).toEqual([
{ serverId: "s1", workspaceId: "ws-main" },
{ serverId: "s1", workspaceId: "ws-feat-a" },
]);
expect(model.shortcutIndexByWorkspaceKey.get("s1:ws-main")).toBe(1);
expect(model.shortcutIndexByWorkspaceKey.get("s1:ws-feat-a")).toBe(2);
expect(model.shortcutIndexByWorkspaceKey.get("s1:ws-repo2-main")).toBeUndefined();
});
it("keeps all traversal targets while numbering only the first 9", () => {
const workspaces = Array.from({ length: 20 }, (_, index) =>
workspace({
serverId: "s",
workspaceId: `ws-${index + 1}`,
workspaceDirectory: `/repo/w${index + 1}`,
name: `w${index + 1}`,
}),
);
const projects = [project("p", workspaces)];
const model = buildSidebarShortcutModel({
projects,
collapsedProjectKeys: new Set<string>(),
});
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", () => {
const gitProject = project("p1", [
workspace({
serverId: "s1",
workspaceId: "ws-main",
workspaceDirectory: "/repo/main",
name: "main",
}),
]);
const directoryProject = project("p2", [
workspace({
serverId: "s1",
workspaceId: "ws-script",
workspaceDirectory: "/scripts",
name: "scripts",
}),
]);
directoryProject.projectKind = "directory";
directoryProject.hosts = directoryProject.hosts.map((host) => ({
...host,
worktreeSupport: "unsupported" as const,
}));
const model = buildSidebarShortcutModel({
projects: [gitProject, directoryProject],
collapsedProjectKeys: new Set<string>(["p1", "p2"]),
});
expect(model.shortcutTargets).toEqual([]);
});
});
describe("buildStatusSidebarShortcutModel", () => {
it("builds shortcut targets in status visual order", () => {
const workspaces = [
workspace({
serverId: "s1",
workspaceId: "done-old",
workspaceDirectory: "/repo/done-old",
name: "done old",
projectKey: "p1",
statusBucket: "done",
statusEnteredAt: new Date("2026-01-01T00:00:00.000Z"),
}),
workspace({
serverId: "s1",
workspaceId: "running-new",
workspaceDirectory: "/repo/running-new",
name: "running new",
projectKey: "p2",
statusBucket: "running",
statusEnteredAt: new Date("2026-03-01T00:00:00.000Z"),
}),
workspace({
serverId: "s1",
workspaceId: "needs-input",
workspaceDirectory: "/repo/needs-input",
name: "needs input",
projectKey: "p1",
statusBucket: "needs_input",
statusEnteredAt: new Date("2026-02-01T00:00:00.000Z"),
}),
workspace({
serverId: "s1",
workspaceId: "running-old",
workspaceDirectory: "/repo/running-old",
name: "running old",
projectKey: "p2",
statusBucket: "running",
statusEnteredAt: new Date("2026-01-15T00:00:00.000Z"),
}),
];
const model = buildStatusSidebarShortcutModel({
groups: buildStatusGroups(
workspaces,
new Map([
["p1", "Project 1"],
["p2", "Project 2"],
]),
i18n.t,
),
});
expect(model.shortcutTargets).toEqual([
{ serverId: "s1", workspaceId: "needs-input" },
{ serverId: "s1", workspaceId: "running-new" },
{ serverId: "s1", workspaceId: "running-old" },
{ serverId: "s1", workspaceId: "done-old" },
]);
expect(model.shortcutIndexByWorkspaceKey.get("s1:needs-input")).toBe(1);
expect(model.shortcutIndexByWorkspaceKey.get("s1:running-new")).toBe(2);
expect(model.shortcutIndexByWorkspaceKey.get("s1:running-old")).toBe(3);
expect(model.shortcutIndexByWorkspaceKey.get("s1:done-old")).toBe(4);
});
it("excludes collapsed status groups from shortcut targets", () => {
const workspaces = [
workspace({
serverId: "s1",
workspaceId: "needs-input",
workspaceDirectory: "/repo/needs-input",
name: "needs input",
projectKey: "p1",
statusBucket: "needs_input",
}),
workspace({
serverId: "s1",
workspaceId: "running",
workspaceDirectory: "/repo/running",
name: "running",
projectKey: "p1",
statusBucket: "running",
}),
];
const model = buildStatusSidebarShortcutModel({
groups: buildStatusGroups(workspaces, new Map([["p1", "Project 1"]]), i18n.t),
collapsedStatusGroupKeys: new Set(["needs_input"]),
});
expect(model.shortcutTargets).toEqual([{ serverId: "s1", workspaceId: "running" }]);
expect(model.shortcutIndexByWorkspaceKey.get("s1:needs-input")).toBeUndefined();
expect(model.shortcutIndexByWorkspaceKey.get("s1:running")).toBe(1);
});
});
describe("getRelativeSidebarShortcutTarget", () => {
const targets = [
{ serverId: "s1", workspaceId: "ws-1" },
{ serverId: "s1", workspaceId: "ws-2" },
{ serverId: "s1", workspaceId: "ws-3" },
];
it("moves backward and forward through the numbered shortcut target list", () => {
expect(
getRelativeSidebarShortcutTarget({
targets,
currentTarget: { serverId: "s1", workspaceId: "ws-2" },
delta: -1,
}),
).toEqual({ serverId: "s1", workspaceId: "ws-1" });
expect(
getRelativeSidebarShortcutTarget({
targets,
currentTarget: { serverId: "s1", workspaceId: "ws-2" },
delta: 1,
}),
).toEqual({ serverId: "s1", workspaceId: "ws-3" });
});
it("wraps around the numbered shortcut target list", () => {
expect(
getRelativeSidebarShortcutTarget({
targets,
currentTarget: { serverId: "s1", workspaceId: "ws-1" },
delta: -1,
}),
).toEqual({ serverId: "s1", workspaceId: "ws-3" });
expect(
getRelativeSidebarShortcutTarget({
targets,
currentTarget: { serverId: "s1", workspaceId: "ws-3" },
delta: 1,
}),
).toEqual({ serverId: "s1", workspaceId: "ws-1" });
});
it("falls back to the nearest edge when the current route is not in the numbered list", () => {
expect(
getRelativeSidebarShortcutTarget({
targets,
currentTarget: { serverId: "s1", workspaceId: "ws-hidden" },
delta: 1,
}),
).toEqual({ serverId: "s1", workspaceId: "ws-1" });
expect(
getRelativeSidebarShortcutTarget({
targets,
currentTarget: { serverId: "s1", workspaceId: "ws-hidden" },
delta: -1,
}),
).toEqual({ serverId: "s1", workspaceId: "ws-3" });
});
});