fix(web): recalculate dockview layout after fast-path session switch (#571)

* fix(web): recalculate dockview layout after fast-path session switch

The fast path in tryFastSessionSwitch removed ephemeral panels but
never called api.layout() to redistribute column widths, leaving a
gap on the right side of the screen. Both slow-path branches already
make this call.

* test(web): add regression test for fast-path layout call

Verifies that performSessionSwitch calls api.layout() on both the
fast path and slow path, preventing the squished-layout regression.

* test(web): cover saved-layout fast path branch

* fix(web): replace no-explicit-any cast in test

* fix(web): fix test isolation with mockReturnValueOnce

Use mockReturnValueOnce and clearAllMocks for proper test isolation
between fast-path and slow-path test cases.

* test(web): fix mock shape to match LayoutGroupIds type
This commit is contained in:
José Almeida
2026-04-09 23:07:49 +01:00
committed by GitHub
parent 2a4e2d7c68
commit 2872d072cb
2 changed files with 85 additions and 0 deletions
@@ -0,0 +1,84 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { performSessionSwitch, type SessionSwitchParams } from "./dockview-session-switch";
// Mock dependencies
vi.mock("@/lib/local-storage", () => ({
getSessionLayout: vi.fn(() => null),
}));
vi.mock("./dockview-layout-builders", () => ({
applyLayoutFixups: vi.fn(() => ({
sidebarGroupId: "g1",
centerGroupId: "g2",
rightTopGroupId: "g3",
rightBottomGroupId: "g4",
})),
}));
vi.mock("./layout-manager", () => ({
fromDockviewApi: vi.fn(() => ({ columns: [] })),
savedLayoutMatchesLive: vi.fn(() => false),
layoutStructuresMatch: vi.fn(() => false),
}));
import { getSessionLayout } from "@/lib/local-storage";
import { layoutStructuresMatch, savedLayoutMatchesLive } from "./layout-manager";
function makeMockApi() {
return {
panels: [],
layout: vi.fn(),
fromJSON: vi.fn(),
getPanel: vi.fn(),
} as unknown as SessionSwitchParams["api"];
}
function makeParams(overrides?: Partial<SessionSwitchParams>): SessionSwitchParams {
return {
api: makeMockApi(),
oldSessionId: "old-session",
newSessionId: "new-session",
safeWidth: 800,
safeHeight: 600,
buildDefault: vi.fn(),
getDefaultLayout: vi.fn(() => ({ columns: [] })),
...overrides,
};
}
describe("performSessionSwitch", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("calls api.layout on the fast path when structures match", () => {
vi.mocked(layoutStructuresMatch).mockReturnValueOnce(true);
const params = makeParams();
performSessionSwitch(params);
expect(params.api.layout).toHaveBeenCalledWith(800, 600);
});
it("calls api.layout on the fast path when saved layout matches", () => {
vi.mocked(getSessionLayout).mockReturnValueOnce({ grid: {} } as unknown as ReturnType<
typeof getSessionLayout
>);
vi.mocked(savedLayoutMatchesLive).mockReturnValueOnce(true);
const params = makeParams();
performSessionSwitch(params);
expect(params.api.layout).toHaveBeenCalledWith(800, 600);
expect(params.api.fromJSON).not.toHaveBeenCalled();
});
it("calls api.layout on the slow path (buildDefault fallback)", () => {
const params = makeParams();
performSessionSwitch(params);
expect(params.api.layout).toHaveBeenCalledWith(800, 600);
expect(params.buildDefault).toHaveBeenCalledWith(params.api);
});
});
@@ -83,6 +83,7 @@ function tryFastSessionSwitch(params: SessionSwitchParams): LayoutGroupIds | nul
// Session-scoped portals (terminal, browser, etc.) will be re-acquired
// via usePortalSlot's sessionId dependency change.
removeEphemeralPanels(api, newSessionId);
api.layout(params.safeWidth, params.safeHeight);
return applyLayoutFixups(api);
}