Files
AgentHub/client/src/components/lanes/__tests__/LaneConflictView.test.tsx
T
nntrivi2001 1da46f10f4 feat(git): per-block conflict view with mine/theirs choices (#45)
Opening a conflicted file in the Changes panel now shows each conflict
block as its two sides, labelled from git's markers and tinted apart,
with Keep mine / Take theirs / Keep both per block (undoable, ^/v to
jump). Save and mark resolved writes the chosen text through the
whole-file editor's PUT /git/file (baseSha stale check) then runs
resolve done. The Merge conflicts group's buttons are text, and a
conflicted file is marked with a red ! in the Files tree.

Fix: ls-files --cached lists an unmerged path once per stage, so the
tree showed a conflicted file three times; lsFiles now dedupes.
2026-09-25 13:35:50 +07:00

131 lines
4.2 KiB
TypeScript

/**
* @file Tests for the merge-conflict view: both sides of each block shown
* under their own labels, one choice per block with undo, Save only once
* every block is chosen — and Save writes the chosen text over the version
* opened, then marks the file resolved. Whole-file ours/theirs reuse
* `resolve`.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LaneConflictView } from "../LaneConflictView";
import { api } from "../../../lib/api";
vi.mock("../../../lib/api", () => ({
api: { lanes: { gitFile: vi.fn(), gitSaveFile: vi.fn(), gitAction: vi.fn() } },
}));
const LINES = [
"top",
"<<<<<<< HEAD",
"mine",
"=======",
"theirs",
">>>>>>> origin/main",
"middle",
"<<<<<<< HEAD",
"a",
"=======",
"b",
">>>>>>> origin/main",
];
beforeEach(() => {
vi.mocked(api.lanes.gitFile)
.mockReset()
.mockResolvedValue({
available: true,
path: "a.txt",
rev: "work",
lines: LINES,
truncated: false,
binary: false,
sha: "s1",
} as never);
vi.mocked(api.lanes.gitSaveFile)
.mockReset()
.mockResolvedValue({ available: true } as never);
vi.mocked(api.lanes.gitAction)
.mockReset()
.mockResolvedValue({ action: "resolve", ok: true, output: "" });
});
const blocks = () => screen.getAllByTestId("conflict-block");
describe("LaneConflictView", () => {
it("shows both sides under their labels and saves once every block is chosen", async () => {
const user = userEvent.setup();
const onResolved = vi.fn();
render(<LaneConflictView laneId={3} path="a.txt" onResolved={onResolved} onEdit={vi.fn()} />);
const [first, second] = (await screen.findAllByTestId("conflict-block")) as [
HTMLElement,
HTMLElement,
];
expect(first).toHaveTextContent("Mine (HEAD)");
expect(first).toHaveTextContent("Theirs (origin/main)");
const save = screen.getByRole("button", { name: "Save and mark resolved" });
expect(save).toBeDisabled();
await user.click(within(first).getByRole("button", { name: "Keep mine" }));
// Chosen: the block is plain text now, with an undo.
await user.click(within(blocks()[0]!).getByRole("button", { name: "Undo" }));
await user.click(within(blocks()[0]!).getByRole("button", { name: "Take theirs" }));
await user.click(within(second).getByRole("button", { name: "Keep both" }));
expect(save).toBeEnabled();
await user.click(save);
await waitFor(() =>
expect(api.lanes.gitSaveFile).toHaveBeenCalledWith(
3,
"a.txt",
"top\ntheirs\nmiddle\na\nb",
"s1"
)
);
await waitFor(() =>
expect(api.lanes.gitAction).toHaveBeenCalledWith(3, "resolve", {
path: "a.txt",
side: "done",
})
);
expect(onResolved).toHaveBeenCalled();
});
it("takes a whole side for the file through resolve", async () => {
const user = userEvent.setup();
const onResolved = vi.fn();
render(<LaneConflictView laneId={3} path="a.txt" onResolved={onResolved} onEdit={vi.fn()} />);
await screen.findAllByTestId("conflict-block");
await user.click(screen.getByRole("button", { name: "Take all theirs" }));
await waitFor(() =>
expect(api.lanes.gitAction).toHaveBeenCalledWith(3, "resolve", {
path: "a.txt",
side: "theirs",
})
);
expect(onResolved).toHaveBeenCalled();
expect(api.lanes.gitSaveFile).not.toHaveBeenCalled();
});
it("offers the plain editor when the markers do not balance", async () => {
vi.mocked(api.lanes.gitFile).mockResolvedValue({
available: true,
path: "a.txt",
rev: "work",
lines: ["<<<<<<< HEAD", "x"],
truncated: false,
binary: false,
sha: "s1",
} as never);
const user = userEvent.setup();
const onEdit = vi.fn();
render(<LaneConflictView laneId={3} path="a.txt" onResolved={vi.fn()} onEdit={onEdit} />);
await screen.findByText(/markers do not line up/);
await user.click(screen.getByRole("button", { name: "Edit by hand" }));
expect(onEdit).toHaveBeenCalled();
});
});