diff --git a/packages/app/e2e/browser/split-pane-resize.spec.ts b/packages/app/e2e/browser/split-pane-resize.spec.ts new file mode 100644 index 0000000000..75414ee321 --- /dev/null +++ b/packages/app/e2e/browser/split-pane-resize.spec.ts @@ -0,0 +1,88 @@ +import { expect, type Locator, type Page } from "@playwright/test"; +import { test } from "../support/fixtures"; +import { runWorkspaceActionFromCommandCenter } from "../support/helpers/command-center-workspace-actions"; +import { clickNewChat, gotoWorkspace } from "../support/helpers/launcher"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs"; + +const DRAG_DISTANCE = -40; + +function splitDividers(page: Page): Locator { + return page.getByTestId("workspace-split-resize-handle").filter({ visible: true }); +} + +async function dividerX(divider: Locator): Promise { + const box = await divider.boundingBox(); + if (!box) throw new Error("Divider has no bounding box"); + return box.x; +} + +async function dragDivider(page: Page, divider: Locator, deltaX: number): Promise { + const box = await divider.boundingBox(); + if (!box) throw new Error("Divider has no bounding box"); + const x = box.x + box.width / 2; + const y = box.y + box.height / 2; + await page.mouse.move(x, y); + await page.mouse.down(); + await page.mouse.move(x + deltaX / 2, y, { steps: 4 }); + await page.mouse.move(x + deltaX, y, { steps: 4 }); + await page.mouse.up(); +} + +async function expectDividerFollowsDrag(page: Page, index: number): Promise { + const divider = splitDividers(page).nth(index); + const before = await dividerX(divider); + await dragDivider(page, divider, DRAG_DISTANCE); + await expect + .poll(async () => (await dividerX(divider)) - before, { + message: `divider ${index + 1} should follow the drag`, + }) + .toBeLessThan(DRAG_DISTANCE * 0.75); +} + +async function splitRight(page: Page, expectedDividers: number): Promise { + await runWorkspaceActionFromCommandCenter(page, "Split pane right"); + await expect(splitDividers(page)).toHaveCount(expectedDividers); +} + +test.describe("Split pane resize", () => { + test("every divider drags in a four-pane row", async ({ page }) => { + await page.setViewportSize({ width: 1600, height: 900 }); + const workspace = await seedWorkspace({ repoPrefix: "split-resize-fresh-" }); + try { + await gotoWorkspace(page, workspace.workspaceId); + await waitForWorkspaceTabsVisible(page); + await clickNewChat(page); + await splitRight(page, 1); + await splitRight(page, 2); + await splitRight(page, 3); + + for (const index of [0, 1, 2]) { + await expectDividerFollowsDrag(page, index); + } + } finally { + await workspace.cleanup(); + } + }); + + test("every divider drags after panes are added to a resized row", async ({ page }) => { + await page.setViewportSize({ width: 1600, height: 900 }); + const workspace = await seedWorkspace({ repoPrefix: "split-resize-grown-" }); + try { + await gotoWorkspace(page, workspace.workspaceId); + await waitForWorkspaceTabsVisible(page); + await clickNewChat(page); + await splitRight(page, 1); + await expectDividerFollowsDrag(page, 0); + + await splitRight(page, 2); + await splitRight(page, 3); + + for (const index of [0, 1, 2]) { + await expectDividerFollowsDrag(page, index); + } + } finally { + await workspace.cleanup(); + } + }); +}); diff --git a/packages/app/src/components/split-container.tsx b/packages/app/src/components/split-container.tsx index 9705f3be15..7f627a8c72 100644 --- a/packages/app/src/components/split-container.tsx +++ b/packages/app/src/components/split-container.tsx @@ -872,6 +872,19 @@ function SplitGroupChild({ ); } +/** + * Sizes from a divider drag are kept per group id, apart from the layout tree. A group keeps its + * id when panes are added or removed, so the dragged sizes only apply while they still have one + * entry per child; otherwise the dividers past the end of the list could not move. + */ +function resolveGroupSizes( + layoutSizes: number[], + draggedSizes: number[] | undefined, + childCount: number, +): number[] { + return draggedSizes?.length === childCount ? draggedSizes : layoutSizes; +} + /** * Flex grow per child, renormalized so the visible ones always sum to 1. * @@ -968,8 +981,11 @@ function SplitNodeView({ groupId ? state.splitSizesByWorkspace[workspaceKey]?.[groupId] : undefined, ); const groupChildren = node.kind === "group" ? node.group.children : EMPTY_SPLIT_NODES; - const groupSizes = - storedGroupSizes ?? (node.kind === "group" ? node.group.sizes : EMPTY_SPLIT_SIZES); + const groupSizes = resolveGroupSizes( + node.kind === "group" ? node.group.sizes : EMPTY_SPLIT_SIZES, + storedGroupSizes, + groupChildren.length, + ); const visibleFlex = useMemo( () => resolveVisibleGroupFlex(groupChildren, groupSizes, maximizedPaneId), [groupChildren, groupSizes, maximizedPaneId],