/** * @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ĩ */ 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(); 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(); 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(); await screen.findByText(/markers do not line up/); await user.click(screen.getByRole("button", { name: "Edit by hand" })); expect(onEdit).toHaveBeenCalled(); }); });