test.rules/codemods/selectors/edit.selector.test.ts
test.rules/codemods/selectors/edit.selector.test.ts is a file in Codebase Testing. 124 lines of code and 0 definitions.
import {
applyByFile,
applyEdits,
disjoint,
parsesAsJson,
splice,
} from "@ssot/govlab/codemods/selectors/edit.selector.ts";
import { describe, expect, it } from "vitest";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import type { Edit } from "@ssot/govlab/types/codemod.types.ts";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { writeVerbatim } from "@govlab/canonical-write";
const SOURCE = "const a = 1;";
const withFile = function withFile(content: string, run: (fileName: string) => void, name = "probe.ts"): void {
const dir = mkdtempSync(join(tmpdir(), "codemod-edit-"));
const fileName = join(dir, name);
writeVerbatim(fileName, content);
try {
run(fileName);
} finally {
rmSync(dir, { force: true, recursive: true });
}
};
describe("applyEdits", () => {
it("writes nothing and reports zero when there is no edit", () => {
withFile(SOURCE, (fileName) => {
expect(applyEdits(fileName, [])).toBe(0);
expect(readFileSync(fileName, "utf8")).toBe(SOURCE);
});
});
it("applies a single replacement", () => {
withFile(SOURCE, (fileName) => {
const edits: Edit[] = [{ end: 11, replacement: "2", start: 10 }];
expect(applyEdits(fileName, edits)).toBe(1);
expect(readFileSync(fileName, "utf8")).toBe("const a = 2;");
});
});
it("applies several edits without letting an earlier one shift a later one", () => {
withFile("const a = 1; const b = 2;", (fileName) => {
const edits: Edit[] = [
{ end: 11, replacement: "9", start: 10 },
{ end: 24, replacement: "8", start: 23 },
];
expect(applyEdits(fileName, edits)).toBe(2);
expect(readFileSync(fileName, "utf8")).toBe("const a = 9; const b = 8;");
});
});
it("keeps only the first of two overlapping edits, so a rewrite never half-applies", () => {
withFile(SOURCE, (fileName) => {
const edits: Edit[] = [
{ end: 11, replacement: "b = 2", start: 6 },
{ end: 11, replacement: "3", start: 10 },
];
expect(applyEdits(fileName, edits)).toBe(1);
});
});
it("refuses to write a rewrite that introduces a syntax error, leaving the file untouched", () => {
withFile(SOURCE, (fileName) => {
const edits: Edit[] = [{ end: 12, replacement: "const = ;", start: 0 }];
expect(() => applyEdits(fileName, edits)).toThrow("refused to write");
expect(readFileSync(fileName, "utf8")).toBe(SOURCE);
});
});
});
describe("applyEdits on JSON", () => {
it("writes a rewrite that still parses", () => {
withFile(
'{"a": "x"}',
(fileName) => {
expect(applyEdits(fileName, [{ end: 8, replacement: "y", start: 7 }])).toBe(1);
expect(readFileSync(fileName, "utf8")).toBe('{"a": "y"}');
},
"probe.json",
);
});
it("refuses a rewrite that no longer parses, leaving the file untouched", () => {
withFile(
'{"a": 1}',
(fileName) => {
expect(() => applyEdits(fileName, [{ end: 7, replacement: "1,", start: 6 }])).toThrow(
"refused to write",
);
expect(readFileSync(fileName, "utf8")).toBe('{"a": 1}');
},
"probe.json",
);
});
});
describe("parsesAsJson", () => {
it("answers whether the text parses as JSON", () => {
expect(parsesAsJson('{"a": [1, 2]}')).toBe(true);
expect(parsesAsJson('{"a": 1,}')).toBe(false);
});
});
describe("disjoint", () => {
it("keeps each edit that overlaps none kept before it, in the given order", () => {
const first: Edit = { end: 8, replacement: "x", start: 4 };
const overlapping: Edit = { end: 6, replacement: "y", start: 2 };
const apart: Edit = { end: 2, replacement: "z", start: 0 };
expect(disjoint([first, overlapping, apart])).toStrictEqual([first, apart]);
});
});
describe("splice", () => {
it("applies edits ordered from the end of the text back to the start", () => {
const edits: Edit[] = [
{ end: 24, replacement: "8", start: 23 },
{ end: 11, replacement: "9", start: 10 },
];
expect(splice("const a = 1; const b = 2;", edits)).toBe("const a = 9; const b = 8;");
});
});
describe("applyByFile", () => {
it("totals the edits applied across every file", () => {
withFile(SOURCE, (fileName) => {
const byFile = new Map<string, Edit[]>([[fileName, [{ end: 11, replacement: "4", start: 10 }]]]);
expect(applyByFile(byFile)).toBe(1);
expect(readFileSync(fileName, "utf8")).toBe("const a = 4;");
});
});
it("reports zero for an empty plan", () => {
expect(applyByFile(new Map<string, Edit[]>())).toBe(0);
});
});