3e676e3bb2
* feat(web): double-click tab to toggle maximize Adds a dblclick gesture on dockview tabs that toggles maximize/restore on the containing group, mirroring the existing maximize button. Preview tabs keep their promote-to-pin shortcut: first dblclick pins, then dblclick maximizes. Sidebar group is excluded. * fix(web): add cursor-pointer to tab wrappers for consistency * fix(web): add cursor-pointer to plan-tab wrapper * fix(web): make tab dblclick maximize resilient to layout rebuild Refactor the dblclick-to-maximize handler so it reads the panel's group ID and store state at call time instead of from a render-time closure, and guards against firing while the dockview store is mid-layout-restore. This addresses oscillation when double-clicking a session tab on a task that has multiple sessions: the maximize call now stops event propagation, respects isRestoringLayout, and always uses the live api.group.id, so it remains correct even after dockview rebuilds groups via fromJSON.
107 lines
3.7 KiB
TypeScript
107 lines
3.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { renderHook } from "@testing-library/react";
|
|
|
|
const SIDEBAR_GROUP_ID = "sidebar-group";
|
|
|
|
const mockMaximizeGroup = vi.fn();
|
|
const mockExitMaximizedLayout = vi.fn();
|
|
|
|
const storeState: {
|
|
preMaximizeLayout: object | null;
|
|
sidebarGroupId: string;
|
|
isRestoringLayout: boolean;
|
|
maximizeGroup: typeof mockMaximizeGroup;
|
|
exitMaximizedLayout: typeof mockExitMaximizedLayout;
|
|
} = {
|
|
preMaximizeLayout: null,
|
|
sidebarGroupId: SIDEBAR_GROUP_ID,
|
|
isRestoringLayout: false,
|
|
maximizeGroup: mockMaximizeGroup,
|
|
exitMaximizedLayout: mockExitMaximizedLayout,
|
|
};
|
|
|
|
vi.mock("@/lib/state/dockview-store", () => ({
|
|
useDockviewStore: Object.assign(
|
|
(selector: (state: typeof storeState) => unknown) => selector(storeState),
|
|
{ getState: () => storeState },
|
|
),
|
|
}));
|
|
|
|
import { useTabMaximizeOnDoubleClick } from "../use-tab-maximize";
|
|
|
|
function makeApi(groupId: string | undefined) {
|
|
return { group: groupId === undefined ? undefined : { id: groupId } } as Parameters<
|
|
typeof useTabMaximizeOnDoubleClick
|
|
>[0];
|
|
}
|
|
|
|
function makeEvent() {
|
|
return {
|
|
stopPropagation: vi.fn(),
|
|
preventDefault: vi.fn(),
|
|
} as unknown as React.MouseEvent;
|
|
}
|
|
|
|
describe("useTabMaximizeOnDoubleClick", () => {
|
|
beforeEach(() => {
|
|
mockMaximizeGroup.mockClear();
|
|
mockExitMaximizedLayout.mockClear();
|
|
storeState.preMaximizeLayout = null;
|
|
storeState.sidebarGroupId = SIDEBAR_GROUP_ID;
|
|
storeState.isRestoringLayout = false;
|
|
});
|
|
|
|
it("calls maximizeGroup when not currently maximized", () => {
|
|
const { result } = renderHook(() => useTabMaximizeOnDoubleClick(makeApi("group-a")));
|
|
result.current(makeEvent());
|
|
expect(mockMaximizeGroup).toHaveBeenCalledWith("group-a");
|
|
expect(mockExitMaximizedLayout).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("calls exitMaximizedLayout when currently maximized", () => {
|
|
storeState.preMaximizeLayout = { columns: [] };
|
|
const { result } = renderHook(() => useTabMaximizeOnDoubleClick(makeApi("group-a")));
|
|
result.current(makeEvent());
|
|
expect(mockExitMaximizedLayout).toHaveBeenCalledTimes(1);
|
|
expect(mockMaximizeGroup).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("no-ops when groupId is the sidebar group", () => {
|
|
const { result } = renderHook(() => useTabMaximizeOnDoubleClick(makeApi(SIDEBAR_GROUP_ID)));
|
|
result.current(makeEvent());
|
|
expect(mockMaximizeGroup).not.toHaveBeenCalled();
|
|
expect(mockExitMaximizedLayout).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("no-ops while a layout restore is in progress", () => {
|
|
storeState.isRestoringLayout = true;
|
|
const { result } = renderHook(() => useTabMaximizeOnDoubleClick(makeApi("group-a")));
|
|
result.current(makeEvent());
|
|
expect(mockMaximizeGroup).not.toHaveBeenCalled();
|
|
expect(mockExitMaximizedLayout).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("no-ops when api.group is missing", () => {
|
|
const { result } = renderHook(() => useTabMaximizeOnDoubleClick(makeApi(undefined)));
|
|
result.current(makeEvent());
|
|
expect(mockMaximizeGroup).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reads groupId fresh at call time, not from render-time closure", () => {
|
|
const api = makeApi("group-a");
|
|
const { result } = renderHook(() => useTabMaximizeOnDoubleClick(api));
|
|
// Simulate dockview reassigning the panel's group after layout rebuild.
|
|
(api.group as { id: string }).id = "group-b";
|
|
result.current(makeEvent());
|
|
expect(mockMaximizeGroup).toHaveBeenCalledWith("group-b");
|
|
});
|
|
|
|
it("stops the event from propagating to the underlying tab", () => {
|
|
const event = makeEvent();
|
|
const { result } = renderHook(() => useTabMaximizeOnDoubleClick(makeApi("group-a")));
|
|
result.current(event);
|
|
expect(event.stopPropagation).toHaveBeenCalled();
|
|
expect(event.preventDefault).toHaveBeenCalled();
|
|
});
|
|
});
|