338 lines
11 KiB
TypeScript
338 lines
11 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import type { DockviewApi } from "dockview-react";
|
|
import type { TaskPR } from "@/lib/types/github";
|
|
import type { TaskMR } from "@/lib/types/gitlab";
|
|
import {
|
|
resolveCanonicalReviewPanelState,
|
|
resolveConfiguredReviewPanelPlacement,
|
|
resolveConditionalReviewPanelAction,
|
|
syncCanonicalReviewPanel,
|
|
type CanonicalReviewPanelState,
|
|
type ConditionalReviewPanelOptions,
|
|
} from "./dockview-review-panel-sync";
|
|
|
|
const CENTER_GROUP_ID = "group-center";
|
|
const RIGHT_GROUP_ID = "group-right-top";
|
|
const SESSION_PANEL_ID = "session:session-a";
|
|
const REVIEW_PANEL_ID = "pr-detail";
|
|
const PULL_REQUEST_TITLE = "Pull Request";
|
|
const githubPRKey = "kandev/kandev/42";
|
|
const gitlabMRKey = "https://gitlab.example.test|group/project|7";
|
|
const bitbucketReviewKey = "workspace/repository/42";
|
|
|
|
const DEFAULT_OPTIONS: ConditionalReviewPanelOptions = {
|
|
sessionId: "session-a",
|
|
centerGroupId: CENTER_GROUP_ID,
|
|
reviewsLoaded: true,
|
|
isRestoringLayout: false,
|
|
isMaximized: false,
|
|
wasOffered: false,
|
|
};
|
|
|
|
const githubPR = {
|
|
owner: "kandev",
|
|
repo: "kandev",
|
|
pr_number: 42,
|
|
pr_url: "https://github.com/kandev/kandev/pull/42",
|
|
} as TaskPR;
|
|
const gitlabMR = {
|
|
host: "https://gitlab.example.test",
|
|
project_path: "group/project",
|
|
mr_iid: 7,
|
|
mr_url: "https://gitlab.example.test/group/project/-/merge_requests/7",
|
|
} as TaskMR;
|
|
const bitbucketReview = {
|
|
providerId: "bitbucket",
|
|
reviewKey: bitbucketReviewKey,
|
|
title: "Bitbucket Pull Request #42",
|
|
url: "https://bitbucket.example/workspace/repository/pull-requests/42",
|
|
connectionScope: "https://bitbucket.example",
|
|
repositoryId: "repository-1",
|
|
changeRequestNumber: 42,
|
|
state: "OPEN",
|
|
};
|
|
|
|
function makeApi(
|
|
panel?: { params?: Record<string, unknown>; groupId?: string; title?: string },
|
|
sessionGroupId = CENTER_GROUP_ID,
|
|
extraGroupIds: string[] = [],
|
|
) {
|
|
const updateParameters = vi.fn((next: Record<string, unknown>) => {
|
|
Object.assign(panel?.params ?? {}, next);
|
|
});
|
|
const setTitle = vi.fn();
|
|
const close = vi.fn();
|
|
const addPanel = vi.fn();
|
|
const reviewPanel = panel
|
|
? {
|
|
id: REVIEW_PANEL_ID,
|
|
params: panel.params ?? {},
|
|
group: { id: panel.groupId ?? RIGHT_GROUP_ID },
|
|
api: { title: panel.title, updateParameters, setTitle, close },
|
|
}
|
|
: undefined;
|
|
const api = {
|
|
getPanel: (id: string) => {
|
|
if (id === REVIEW_PANEL_ID) return reviewPanel;
|
|
if (id === SESSION_PANEL_ID) return { id, group: { id: sessionGroupId } };
|
|
return undefined;
|
|
},
|
|
panels: [
|
|
...(reviewPanel ? [reviewPanel] : []),
|
|
{ id: SESSION_PANEL_ID, group: { id: sessionGroupId } },
|
|
],
|
|
groups: [
|
|
{ id: sessionGroupId },
|
|
...(sessionGroupId === CENTER_GROUP_ID ? [] : [{ id: CENTER_GROUP_ID }]),
|
|
...extraGroupIds.map((id) => ({ id })),
|
|
],
|
|
addPanel,
|
|
removePanel: vi.fn(),
|
|
} as unknown as DockviewApi;
|
|
return { api, updateParameters, setTitle, close, addPanel };
|
|
}
|
|
|
|
describe("resolveCanonicalReviewPanelState", () => {
|
|
it("normalizes GitHub and GitLab identities", () => {
|
|
expect(resolveCanonicalReviewPanelState([githubPR], [])).toEqual({
|
|
kind: "github",
|
|
params: {
|
|
providerId: "github",
|
|
provider: "github",
|
|
reviewKey: githubPRKey,
|
|
connectionScope: "https://github.com",
|
|
repositoryId: "kandev/kandev",
|
|
changeRequestNumber: 42,
|
|
prKey: githubPRKey,
|
|
mrKey: undefined,
|
|
},
|
|
title: PULL_REQUEST_TITLE,
|
|
});
|
|
expect(resolveCanonicalReviewPanelState([], [gitlabMR])).toEqual({
|
|
kind: "gitlab",
|
|
params: {
|
|
providerId: "gitlab",
|
|
provider: "gitlab",
|
|
reviewKey: gitlabMRKey,
|
|
connectionScope: "https://gitlab.example.test",
|
|
repositoryId: "group/project",
|
|
changeRequestNumber: 7,
|
|
prKey: undefined,
|
|
mrKey: gitlabMRKey,
|
|
},
|
|
title: "Merge Request",
|
|
});
|
|
});
|
|
|
|
it("normalizes a registered provider review", () => {
|
|
expect(resolveCanonicalReviewPanelState([], [], [bitbucketReview])).toEqual({
|
|
kind: "registered",
|
|
params: {
|
|
providerId: "bitbucket",
|
|
provider: undefined,
|
|
reviewKey: bitbucketReviewKey,
|
|
connectionScope: "https://bitbucket.example",
|
|
repositoryId: "repository-1",
|
|
changeRequestNumber: 42,
|
|
prKey: undefined,
|
|
mrKey: undefined,
|
|
},
|
|
title: "Bitbucket Pull Request #42",
|
|
});
|
|
});
|
|
|
|
it("requires selection when providers coexist", () => {
|
|
expect(resolveCanonicalReviewPanelState([githubPR], [gitlabMR], [bitbucketReview])).toEqual({
|
|
kind: "multiple",
|
|
params: {
|
|
providerId: undefined,
|
|
provider: undefined,
|
|
reviewKey: undefined,
|
|
connectionScope: undefined,
|
|
repositoryId: undefined,
|
|
changeRequestNumber: undefined,
|
|
prKey: undefined,
|
|
mrKey: undefined,
|
|
},
|
|
title: "Reviews",
|
|
});
|
|
});
|
|
|
|
it("returns an empty canonical panel state when no review is linked", () => {
|
|
expect(resolveCanonicalReviewPanelState([], [])).toEqual({
|
|
kind: "empty",
|
|
params: {
|
|
providerId: undefined,
|
|
provider: undefined,
|
|
reviewKey: undefined,
|
|
connectionScope: undefined,
|
|
repositoryId: undefined,
|
|
changeRequestNumber: undefined,
|
|
prKey: undefined,
|
|
mrKey: undefined,
|
|
},
|
|
title: "PR Details",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("resolveConditionalReviewPanelAction", () => {
|
|
it.each([
|
|
["add", { hasReview: true, panelExists: false }],
|
|
["sync", { hasReview: true, panelExists: true }],
|
|
["remove", { hasReview: false, panelExists: true }],
|
|
["none", { hasReview: false, panelExists: false }],
|
|
])("returns %s for the ordinary lifecycle", (expected, input) => {
|
|
expect(
|
|
resolveConditionalReviewPanelAction({
|
|
reviewsLoaded: true,
|
|
isRestoringLayout: false,
|
|
isMaximized: false,
|
|
wasOffered: false,
|
|
...input,
|
|
}),
|
|
).toBe(expected);
|
|
});
|
|
|
|
it("does not add during restore, maximize, or after dismissal", () => {
|
|
for (const blocked of [
|
|
{ isRestoringLayout: true },
|
|
{ isMaximized: true },
|
|
{ wasOffered: true },
|
|
]) {
|
|
expect(
|
|
resolveConditionalReviewPanelAction({
|
|
hasReview: true,
|
|
panelExists: false,
|
|
reviewsLoaded: true,
|
|
isRestoringLayout: false,
|
|
isMaximized: false,
|
|
wasOffered: false,
|
|
...blocked,
|
|
}),
|
|
).toBe("none");
|
|
}
|
|
});
|
|
|
|
it("waits for review hydration before removing the panel", () => {
|
|
expect(
|
|
resolveConditionalReviewPanelAction({
|
|
hasReview: false,
|
|
panelExists: true,
|
|
reviewsLoaded: false,
|
|
isRestoringLayout: false,
|
|
isMaximized: false,
|
|
wasOffered: false,
|
|
}),
|
|
).toBe("none");
|
|
});
|
|
});
|
|
|
|
describe("resolveConfiguredReviewPanelPlacement", () => {
|
|
it("returns the saved group and tab index", () => {
|
|
expect(
|
|
resolveConfiguredReviewPanelPlacement({
|
|
columns: [
|
|
{
|
|
id: "right",
|
|
groups: [
|
|
{
|
|
id: RIGHT_GROUP_ID,
|
|
panels: [
|
|
{ id: "files", component: "files", title: "Files" },
|
|
{ id: REVIEW_PANEL_ID, component: REVIEW_PANEL_ID, title: "PR Details" },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}),
|
|
).toEqual({ groupId: RIGHT_GROUP_ID, index: 1 });
|
|
});
|
|
});
|
|
|
|
describe("syncCanonicalReviewPanel", () => {
|
|
function sync(api: DockviewApi, next: CanonicalReviewPanelState, options = DEFAULT_OPTIONS) {
|
|
return syncCanonicalReviewPanel(api, next, options);
|
|
}
|
|
|
|
it("adds a GitHub review beside the live Agent without activating it", () => {
|
|
const { api, addPanel } = makeApi();
|
|
expect(sync(api, resolveCanonicalReviewPanelState([githubPR], []))).toBe(true);
|
|
expect(addPanel).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
id: REVIEW_PANEL_ID,
|
|
title: PULL_REQUEST_TITLE,
|
|
inactive: true,
|
|
position: { referenceGroup: CENTER_GROUP_ID },
|
|
params: expect.objectContaining({ providerId: "github", reviewKey: githubPRKey }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("adds registered and mixed-provider review surfaces", () => {
|
|
for (const next of [
|
|
resolveCanonicalReviewPanelState([], [], [bitbucketReview]),
|
|
resolveCanonicalReviewPanelState([githubPR], [], [bitbucketReview]),
|
|
]) {
|
|
const { api, addPanel } = makeApi();
|
|
expect(sync(api, next)).toBe(true);
|
|
expect(addPanel).toHaveBeenCalledWith(
|
|
expect.objectContaining({ id: REVIEW_PANEL_ID, title: next.title, params: next.params }),
|
|
);
|
|
}
|
|
});
|
|
|
|
it("honors the saved layout placement", () => {
|
|
const { api, addPanel } = makeApi(undefined, CENTER_GROUP_ID, [RIGHT_GROUP_ID]);
|
|
expect(
|
|
sync(api, resolveCanonicalReviewPanelState([githubPR], []), {
|
|
...DEFAULT_OPTIONS,
|
|
configuredPlacement: { groupId: RIGHT_GROUP_ID, index: 2 },
|
|
}),
|
|
).toBe(true);
|
|
expect(addPanel).toHaveBeenCalledWith(
|
|
expect.objectContaining({ position: { referenceGroup: RIGHT_GROUP_ID, index: 2 } }),
|
|
);
|
|
});
|
|
|
|
it("closes the panel only after empty review data is loaded", () => {
|
|
const { api, close } = makeApi({ params: { providerId: "github" } });
|
|
expect(
|
|
sync(api, resolveCanonicalReviewPanelState([], []), {
|
|
...DEFAULT_OPTIONS,
|
|
reviewsLoaded: false,
|
|
}),
|
|
).toBe(false);
|
|
expect(close).not.toHaveBeenCalled();
|
|
expect(sync(api, resolveCanonicalReviewPanelState([], []))).toBe(true);
|
|
expect(close).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("updates identity and title without moving the existing panel", () => {
|
|
const params: Record<string, unknown> = { providerId: "gitlab", reviewKey: "old" };
|
|
const { api, updateParameters, setTitle } = makeApi({
|
|
params,
|
|
groupId: "custom-review-group",
|
|
title: "Merge Request",
|
|
});
|
|
expect(sync(api, resolveCanonicalReviewPanelState([githubPR], []))).toBe(true);
|
|
expect(updateParameters).toHaveBeenCalledWith(
|
|
expect.objectContaining({ providerId: "github", reviewKey: githubPRKey }),
|
|
);
|
|
expect(setTitle).toHaveBeenCalledWith(PULL_REQUEST_TITLE);
|
|
expect(api.getPanel(REVIEW_PANEL_ID)?.group.id).toBe("custom-review-group");
|
|
});
|
|
|
|
it("updates a registered review title without churning its identity", () => {
|
|
const next = resolveCanonicalReviewPanelState([], [], [bitbucketReview]);
|
|
const { api, updateParameters, setTitle } = makeApi({
|
|
params: { ...next.params },
|
|
title: PULL_REQUEST_TITLE,
|
|
});
|
|
expect(sync(api, next)).toBe(true);
|
|
expect(updateParameters).not.toHaveBeenCalled();
|
|
expect(setTitle).toHaveBeenCalledWith(bitbucketReview.title);
|
|
});
|
|
});
|