Files
kandev/apps/web/lib/state/dockview-session-switch.test.ts
T
José Almeida 7f9cff0dcb fix(web): fix dock view task-switching regressions (#595)
* fix(web): fix dock view task-switching regressions

Three bugs caused broken UI when switching tasks in the dock view:

1. Environment key migration was dead code — data stored under fallback
   sessionId keys became invisible when the environmentIdBySessionId
   mapping was later set, because migrateEnvKeyedData was never called
   in production (only in tests via registerSessionEnvironment).

2. Orphaned portals leaked on every fast-path session switch — the
   isRestoringLayout guard in setupPortalCleanup blocked portal release
   for panels removed during the fast path, accumulating invisible React
   trees with live WS subscriptions.

3. Chat panel gap during fast-path transition — old session tab was
   removed synchronously but new tab was deferred to a useEffect,
   leaving a brief layout gap.

* fix(web): remove superfluous args in test helper

Address CodeQL finding — createSessionSlice only takes `set`, not
`get` and `api`.

* fix(web): add addPanel test assertions and doc comment

Address claude-review blocker: assert that fast-path creates the
session panel with correct id/params, and skips creation when it
already exists. Add doc comment noting syncEnvironmentMapping
requires the combined store draft.

* test(web): add PanelPortalManager.reconcile tests

Export PanelPortalManager class for testability and add tests covering
reconcile removal, no-op, and listener notification paths.
2026-04-15 21:40:16 +01:00

116 lines
3.4 KiB
TypeScript

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(() => null),
addPanel: 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("creates session panel inline on the fast path when it does not exist", () => {
vi.mocked(layoutStructuresMatch).mockReturnValueOnce(true);
const params = makeParams();
performSessionSwitch(params);
expect(params.api.addPanel).toHaveBeenCalledWith(
expect.objectContaining({
id: "session:new-session",
component: "chat",
params: { sessionId: "new-session" },
}),
);
});
it("skips addPanel on the fast path when the session panel already exists", () => {
vi.mocked(layoutStructuresMatch).mockReturnValueOnce(true);
const panel = { id: "session:new-session", api: { component: "chat" }, group: { id: "g1" } };
const params = makeParams({
api: {
...makeMockApi(),
getPanel: vi.fn((id: string) => (id === "session:new-session" ? panel : null)),
} as unknown as SessionSwitchParams["api"],
});
performSessionSwitch(params);
expect(params.api.addPanel).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);
});
});