/** * @file Tests for the Changes panel's CodeMirror editor: which changed lines * count as edited in place (blue) rather than added or removed whole, and the * editor's own diff classes on a real mount. * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect, vi } from "vitest"; import { render, waitFor } from "@testing-library/react"; import DiffEditor, { keepsText } from "../DiffEditor"; // jsdom lays nothing out; CodeMirror's measuring asks a Range for its boxes. const EMPTY = Object.assign([], { item: () => null }) as unknown as DOMRectList; Range.prototype.getClientRects ??= () => EMPTY; Range.prototype.getBoundingClientRect ??= () => new DOMRect(); describe("keepsText", () => { it("is false for a line whose every non-blank character changed", () => { expect(keepsText(" added()", 10, [[12, 19]])).toBe(false); expect(keepsText("", 0, [])).toBe(false); // Only a `;` survived: not the same line. expect(keepsText("export x;", 0, [[0, 8]])).toBe(false); }); it("is true when some text outside the changes survives", () => { expect(keepsText("const x = 2;", 0, [[10, 11]])).toBe(true); }); }); describe("DiffEditor", () => { it("marks a whole new line green and a line edited in place blue", async () => { const { container } = render( ); await waitFor(() => expect(container.querySelectorAll(".cm-changedLine")).toHaveLength(2)); const lines = [...container.querySelectorAll(".cm-changedLine")]; expect(lines.map((l) => l.classList.contains("cm-ah-mod"))).toEqual([true, false]); await waitFor(() => expect(container.querySelector(".cm-deletedLine")).toHaveAttribute("data-ah", "mod") ); }); it("side by side: the original read-only on the left, its edited line blue", async () => { const { container } = render( ); await waitFor(() => expect(container.querySelectorAll(".cm-editor")).toHaveLength(2)); const [left, right] = [...container.querySelectorAll(".cm-content")]; expect(left).toHaveAttribute("contenteditable", "false"); expect(right).toHaveAttribute("contenteditable", "true"); expect(container.querySelector(".cm-merge-a .cm-changedLine.cm-ah-mod")).not.toBeNull(); expect(container.querySelector(".cm-merge-b .cm-changedLine.cm-ah-mod")).not.toBeNull(); // The revert column is drawn in CodeMirror's next measure pass, not at mount. await waitFor(() => expect(container.querySelector(".cm-merge-revert button")).toHaveAttribute("title", "Revert") ); }); it("folds unchanged lines away when collapsed, and offers Revert on a block", async () => { const same = Array.from({ length: 30 }, (_, i) => `line ${i}`).join("\n"); const { container } = render( ); await waitFor(() => expect(container.querySelector(".cm-collapsedLines")).not.toBeNull()); const buttons = [...container.querySelectorAll(".cm-chunkButtons button")]; expect(buttons.filter((b) => !b.hidden).map((b) => b.textContent)).toEqual(["Revert"]); }); });