Files
kandev/apps/web/components/gitlab/mr-topbar-button.layout-settle.test.tsx
T

197 lines
7.2 KiB
TypeScript

/**
* Regression cover for the MR detail panel being silently discarded when the
* user clicks the topbar button while a task's session is still loading.
*
* Navigating straight to /t/:id runs dockview's `onReady` before the
* session→env mapping hydrates, so it builds a throwaway DEFAULT layout with a
* null env; `switchEnvLayout` then replaces that layout wholesale via
* `api.fromJSON` once the env arrives. A panel added in that gap is thrown
* away and the click appears to do nothing. `useMRDesktopReviewOpener` must
* therefore re-apply the open once the env's layout has settled.
*/
import { createElement, useSyncExternalStore } from "react";
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { MRTopbarButton } from "./mr-topbar-button";
import type { TaskMR } from "@/lib/types/gitlab";
const TRIGGER_TESTID = "mr-topbar-button";
const gitlabMocks = vi.hoisted(() => ({ mrs: [] as TaskMR[] }));
// A genuinely reactive stand-in for the dockview store: MRTopbarButton is
// memo()-wrapped, so a plain rerender with identical props would never re-read
// module-level mock values. Subscribers make store changes drive the re-render
// the same way zustand does in the app.
const dockviewMocks = vi.hoisted(() => {
const listeners = new Set<() => void>();
const state = {
addMRPanel: vi.fn(),
api: {} as object | null,
currentLayoutEnvId: null as string | null,
isRestoringLayout: false,
};
return {
state,
listeners,
get addMRPanel() {
return state.addMRPanel;
},
set(patch: Partial<typeof state>) {
Object.assign(state, patch);
listeners.forEach((l) => l());
},
};
});
vi.mock("@/lib/state/dockview-store", () => ({
useDockviewStore: (selector: (state: unknown) => unknown) =>
useSyncExternalStore(
(onChange: () => void) => {
dockviewMocks.listeners.add(onChange);
return () => dockviewMocks.listeners.delete(onChange);
},
() => selector(dockviewMocks.state),
),
}));
vi.mock("@/components/state-provider", () => ({
useAppStore: (selector: (state: unknown) => unknown) =>
selector({
tasks: { activeTaskId: "task-1", activeSessionId: "session-1" },
workspaces: { activeId: "workspace-1" },
repositories: { itemsByWorkspaceId: { "workspace-1": [] } },
removeTaskMR: vi.fn(),
setMobileSessionReview: vi.fn(),
}),
}));
vi.mock("@/hooks/domains/gitlab/use-task-mr", () => ({
useGitLabAvailable: () => true,
useTaskMRs: () => gitlabMocks.mrs,
useUnlinkTaskMR: () => vi.fn(async () => {}),
useWorkspaceMRs: vi.fn(),
}));
vi.mock("@/hooks/domains/kanban/use-task-by-id", () => ({
useTaskById: () => ({ id: "task-1", repositories: [] }),
}));
vi.mock("@/components/toast-provider", () => ({ useToast: () => ({ toast: vi.fn() }) }));
vi.mock("./task-mr-link-dialog", () => ({ TaskMRLinkDialog: () => null }));
vi.mock("./mr-automation-controls", () => ({ MRAutomationControls: () => null }));
vi.mock("./mr-ci-popover", () => ({ MRCIPopover: () => null }));
vi.mock("@/hooks/use-compact-task-chrome", () => ({ useTouchDrawer: () => false }));
function singleMR(): TaskMR {
return {
id: "association-1",
task_id: "task-1",
host: "https://gitlab.example",
project_path: "group/project",
mr_iid: 81,
mr_url: "https://gitlab.example/group/project/-/merge_requests/81",
mr_title: "Test MR",
head_branch: "feature",
base_branch: "main",
author_username: "alice",
state: "opened",
approval_state: "",
pipeline_state: "",
merge_status: "",
draft: false,
approval_count: 0,
required_approvals: 0,
pipeline_jobs_total: 0,
pipeline_jobs_pass: 0,
reviewer_count: 0,
unapproved_reviewers: 0,
unresolved_discussions: 0,
created_at: "",
updated_at: "",
} as TaskMR;
}
describe("MRTopbarButton desktop review opener — layout settling", () => {
beforeEach(() => {
gitlabMocks.mrs = [singleMR()];
dockviewMocks.addMRPanel.mockClear();
dockviewMocks.set({ api: {}, currentLayoutEnvId: null, isRestoringLayout: false });
});
afterEach(cleanup);
it("re-opens the panel after the env-switch restore that would have discarded it", () => {
// Session still loading: dockview api exists, but no env has been adopted.
render(createElement(MRTopbarButton));
act(() => {
fireEvent.click(screen.getByTestId(TRIGGER_TESTID));
});
// Opened immediately so the click gives instant feedback...
expect(dockviewMocks.addMRPanel).toHaveBeenCalled();
const callsBeforeRestore = dockviewMocks.addMRPanel.mock.calls.length;
// ...then the env hydrates and switchEnvLayout replaces the layout,
// discarding that panel. Once it settles, the open must be re-applied.
act(() => {
dockviewMocks.set({ currentLayoutEnvId: "env-1", isRestoringLayout: false });
});
expect(dockviewMocks.addMRPanel.mock.calls.length).toBeGreaterThan(callsBeforeRestore);
});
it("does not re-open the panel on a later layout change once the env has settled", () => {
// Env already adopted at click time — the common case for an established
// session. The intent must not be retained, or a later preset switch or
// maximize would resurrect a panel the user has since closed.
dockviewMocks.set({ currentLayoutEnvId: "env-1" });
render(createElement(MRTopbarButton));
act(() => {
fireEvent.click(screen.getByTestId(TRIGGER_TESTID));
});
const callsAfterClick = dockviewMocks.addMRPanel.mock.calls.length;
expect(callsAfterClick).toBeGreaterThan(0);
// A later user-initiated layout restore (preset switch / maximize).
act(() => {
dockviewMocks.set({ isRestoringLayout: true });
});
act(() => {
dockviewMocks.set({ isRestoringLayout: false });
});
expect(dockviewMocks.addMRPanel.mock.calls.length).toBe(callsAfterClick);
});
it("retains the intent when a stale env id makes the layout look settled mid-remount", () => {
// `setApi(null)` does not reset `currentLayoutEnvId`, so navigating between
// tasks leaves the previous env id in place while the new dockview mounts.
// "Settled" must not be trusted on its own — without the dockviewReady
// check the click would be dropped and no later API init could open it.
dockviewMocks.set({ api: null, currentLayoutEnvId: "stale-env", isRestoringLayout: false });
render(createElement(MRTopbarButton));
act(() => {
fireEvent.click(screen.getByTestId(TRIGGER_TESTID));
});
expect(dockviewMocks.addMRPanel).not.toHaveBeenCalled();
act(() => {
dockviewMocks.set({ api: {}, currentLayoutEnvId: "env-1" });
});
expect(dockviewMocks.addMRPanel).toHaveBeenCalled();
});
it("defers the open until dockview exists when clicked before onReady", () => {
dockviewMocks.set({ api: null });
render(createElement(MRTopbarButton));
act(() => {
fireEvent.click(screen.getByTestId(TRIGGER_TESTID));
});
expect(dockviewMocks.addMRPanel).not.toHaveBeenCalled();
act(() => {
dockviewMocks.set({ api: {}, currentLayoutEnvId: "env-1" });
});
expect(dockviewMocks.addMRPanel).toHaveBeenCalled();
});
});