1da46f10f4
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.
131 lines
4.2 KiB
TypeScript
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();
|
|
});
|
|
});
|