Файли
openidea/src/features/vcs/__tests__/logOptions.test.ts
T
l0ng-ai b6a371559a feat(vcs): Preview the selected file's diff in the Git Log (#175)
* feat(vcs): Preview the selected file's diff in the Git Log

Selecting a single changed file in the Git Log's changes pane shows its diff
in the editor's preview tab without taking focus, so arrow keys walk through
the files. View Options › Show Diff Preview turns it off; double-click and
Show Diff still open a regular diff tab.

* fix(vcs): Skip the Git Log diff preview when it cannot open
2026-10-29 09:25:01 +08:00

183 рядки
8.2 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const vcsLog = vi.fn();
const vcsConfigGet = vi.fn();
vi.mock("@/platform/ipc", async (importOriginal) => {
const real = await importOriginal<typeof import("@/platform/ipc")>();
return {
...real,
commands: {
...real.commands,
vcsLog: (repo: unknown, query: unknown) => vcsLog(repo, query),
vcsConfigGet: (repo: unknown, key: string) => vcsConfigGet(repo, key),
},
};
});
import { collapseLinear, computeGraph, hideLongEdges, reachableFrom } from "../graph";
import { DEFAULT_LOG_OPTIONS, parseLogOptions, refLabels, setLogOptions, useLogOptions } from "../logOptions";
import { emptyFilters, goToRef, isMine, reloadLog, updateLogOptions, useGitLog } from "../logStore";
import { defaultWorktreePath, disabledMergeFlags, remoteBranchesOf, validateRemoteName } from "../gitMenuDialogs";
import type { Branch, Commit } from "@/bindings";
const c = (id: string, parents: string[] = [], refs: string[] = []) => ({ id, parents, refs });
describe("Collapse Linear Branches", () => {
it("hides linear commits and links the remaining ones with dashed edges", () => {
// m merges f3 into main; f1..f3 is a linear branch from base, b a commit on main.
const commits = [
c("t", ["m"], ["main"]),
c("m", ["b", "f3"]),
c("f3", ["f2"]),
c("f2", ["f1"]),
c("f1", ["base"]),
c("b", ["base"]),
c("base"),
];
const { commits: shown, graph } = collapseLinear(commits, new Set());
expect(shown.map((x) => x.id)).toEqual(["t", "m", "base"]);
expect(graph.find((g) => g.id === "m")).toEqual({ id: "m", parents: ["base"], dashed: [true] });
expect(graph.find((g) => g.id === "t")).toEqual({ id: "t", parents: ["m"] });
const { rows } = computeGraph(graph);
expect(rows[1]!.down.some((s) => s.dashed)).toBe(true);
expect(rows[2]!.up.some((s) => s.dashed)).toBe(true);
});
it("keeps referenced and kept commits", () => {
const commits = [c("d", ["c"]), c("c", ["b"], ["tag: v1"]), c("b", ["a"]), c("a")];
expect(collapseLinear(commits, new Set()).commits.map((x) => x.id)).toEqual(["d", "c", "a"]);
const chain = [c("e", ["d"]), c("d", ["c"]), c("c", ["b"]), c("b", ["a"]), c("a")];
expect(collapseLinear(chain, new Set(["c"])).commits.map((x) => x.id)).toEqual(["e", "c", "a"]);
expect(collapseLinear(chain, new Set()).commits.map((x) => x.id)).toEqual(["e", "a"]);
});
});
describe("long edges", () => {
it("hides the middle of a long straight lane and puts arrows on the stubs", () => {
// t's parent p is 40 rows below; the commits in between sit in lane 0.
const between = Array.from({ length: 40 }, (_, i) => c(`x${i}`, i < 39 ? [`x${i + 1}`] : []));
const { rows } = computeGraph([c("t", ["p"]), ...between, c("p")]);
const hidden = hideLongEdges(rows);
// The x chain sits in lane 1; the edge t → p passes through lane 0.
const lane0 = (r: (typeof rows)[number]) => r.up.concat(r.down).filter((s) => s.from === 0 && s.to === 0);
expect(lane0(rows[20]!)).toHaveLength(2);
expect(lane0(hidden[20]!)).toHaveLength(0);
expect(hidden[20]!.lane).toBe(1);
expect(hidden.some((r) => r.down.some((s) => s.arrow === "down"))).toBe(true);
expect(hidden.some((r) => r.up.some((s) => s.arrow === "up"))).toBe(true);
expect(hideLongEdges(rows.slice(0, 10))).toEqual(rows.slice(0, 10));
});
});
describe("highlighting", () => {
it("finds the loaded commits of the current branch", () => {
const commits = [c("s", ["a"]), c("h", ["a"]), c("a", ["r"]), c("r")];
expect([...reachableFrom(commits, "h")].sort()).toEqual(["a", "h", "r"]);
expect(reachableFrom(commits, null).size).toBe(0);
});
it("matches my commits by email or name", () => {
const commit = { email: "[email protected]", author: "Me" } as Commit;
expect(isMine(commit, "[email protected]")).toBe(true);
expect(isMine(commit, "me")).toBe(true);
expect(isMine(commit, null)).toBe(false);
});
});
describe("reference labels", () => {
const remotes = new Set(["origin/main", "origin/feat/x"]);
const refs = ["HEAD", "main", "origin/main", "feat/x", "tag: v1", "tag: v2"];
it("merges a branch with its remote-tracking branches in the compact view", () => {
const labels = refLabels(refs, { compact: true, tagNames: false }, remotes);
expect(labels.map((l) => [l.kind, l.text])).toEqual([
["local", "origin & main"],
["local", "feat/x"],
["tag", ""],
]);
expect(labels[2]!.title).toBe("v1, v2");
});
it("lists every reference otherwise", () => {
const labels = refLabels(refs, { compact: false, tagNames: true }, remotes);
expect(labels.map((l) => l.text)).toEqual(["HEAD", "main", "feat/x", "origin/main", "v1", "v2"]);
});
});
describe("log options", () => {
beforeEach(() => {
vcsLog.mockReset().mockResolvedValue({ status: "ok", data: [] });
vcsConfigGet.mockReset().mockResolvedValue({ status: "ok", data: null });
useLogOptions.setState(DEFAULT_LOG_OPTIONS);
});
it("parses stored options with per-field defaults", () => {
expect(parseLogOptions(null)).toEqual(DEFAULT_LOG_OPTIONS);
expect(parseLogOptions({ sort: "topo", longEdges: true, showHash: "yes" })).toEqual({
...DEFAULT_LOG_OPTIONS,
sort: "topo",
longEdges: true,
});
});
it("previews the selected file's diff by default and remembers turning it off", () => {
expect(DEFAULT_LOG_OPTIONS.diffPreview).toBe(true);
expect(parseLogOptions({ diffPreview: false }).diffPreview).toBe(false);
expect(setLogOptions({ diffPreview: false })).toBe(false);
});
it("reloads only for options that change the query", () => {
expect(setLogOptions({ compactRefs: false })).toBe(false);
expect(setLogOptions({ noMerges: true })).toBe(true);
expect(JSON.parse(localStorage.getItem("openidea.vcs.logOptions")!).noMerges).toBe(true);
});
it("sends the graph options and the date range to the backend", async () => {
useGitLog.setState({ filters: { ...emptyFilters(), date: { after: 1000, before: 5000, label: "x" } } });
updateLogOptions({ sort: "topo", firstParent: true });
await reloadLog();
const q = vcsLog.mock.calls.map((x) => x[1] as Record<string, unknown>).find((x) => x.range === null)!;
expect(q).toMatchObject({ order: "topo", firstParent: true, noMerges: false, sinceMs: 1000, untilMs: 5000 });
});
it("goes to a reference by resolving it and selecting the commit", async () => {
const commit = { id: "abc123", parents: [], refs: [], summary: "s", message: "s" } as unknown as Commit;
vcsLog.mockImplementation(async (_r: unknown, q: { branch: string | null }) => ({
status: "ok",
data: q.branch === "v1" || q.branch === null ? [commit] : [],
}));
useGitLog.setState({ filters: emptyFilters(), commits: [], done: false });
expect(await goToRef("v1")).toBe(true);
expect(useGitLog.getState().selected).toBe("abc123");
expect(await goToRef("nope")).toBe(false);
});
});
describe("Git menu dialogs", () => {
it("disables merge flags git refuses together", () => {
expect([...disabledMergeFlags(new Set(["ffOnly"]))].sort()).toEqual(["noCommit", "noFf", "squash"]);
expect(disabledMergeFlags(new Set(), true).size).toBe(4);
expect(disabledMergeFlags(new Set()).size).toBe(0);
});
it("lists a remote's branches without its prefix", () => {
const b = (name: string, remote: boolean) => ({ name, remote, current: false }) as Branch;
const branches = [b("main", false), b("origin/main", true), b("origin/HEAD", true), b("fork/dev", true)];
expect(remoteBranchesOf(branches, "origin")).toEqual(["main"]);
});
it("validates remote names", () => {
expect(validateRemoteName("origin", ["origin"])).toMatch(/already exists/);
expect(validateRemoteName("my remote", [])).toBe("Invalid remote name");
expect(validateRemoteName("", [])).toBe("Enter a remote name");
expect(validateRemoteName("upstream", ["origin"])).toBeNull();
});
it("suggests a worktree folder next to the repository", () => {
expect(defaultWorktreePath("/src/app", "feature/x")).toBe("/src/app-feature-x");
expect(defaultWorktreePath("/src/app/", "main")).toBe("/src/app-main");
expect(defaultWorktreePath("C:\\src\\app", "fix")).toBe("C:\\src\\app-fix");
});
});