Release Diff Tools 0.1.0

This commit is contained in:
2026-09-01 02:45:20 +02:00
commit a1380cfba2
65 changed files with 11442 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
import { expect, test, type Page, type Response } from "@playwright/test";
const ORIGIN = "http://127.0.0.1:4173";
const APP_PATH = "/deep/nested/diff/";
function auditPage(page: Page) {
const external: string[] = [];
const errors: string[] = [];
const responses: Response[] = [];
page.on("pageerror", (error) => errors.push(error.message));
page.on("console", (message) => {
if (message.type() === "error") errors.push(message.text());
});
page.on("response", (response) => responses.push(response));
void page.route("**/*", async (route) => {
const url = new URL(route.request().url());
if (url.origin !== ORIGIN) {
external.push(url.href);
await route.abort();
} else await route.continue();
});
return { external, errors, responses };
}
async function openApp(page: Page) {
await page.goto(APP_PATH);
await expect(
page.getByRole("heading", { name: "Diff Tools", exact: true }),
).toBeVisible();
await expect(page.getByText(/substantive change row/u)).toBeVisible();
}
test("runs from a nested path in a same-origin module worker", async ({
page,
}) => {
const audit = auditPage(page);
await openApp(page);
await expect(
page.getByRole("region", { name: "Comparison result" }),
).toBeVisible();
await expect(page.getByText("CRLF", { exact: true }).first()).toBeVisible();
const worker = audit.responses.find((response) =>
/\/deep\/nested\/diff\/assets\/diff\.worker-[\w-]+\.js$/u.test(
new URL(response.url()).pathname,
),
);
expect(worker, "the comparator worker was requested").toBeTruthy();
expect(new URL(worker!.url()).origin).toBe(ORIGIN);
expect(worker!.headers()["content-type"]).toContain("text/javascript");
expect(worker!.headers()["cache-control"]).toBe(
"public, max-age=31536000, immutable",
);
expect(audit.external).toEqual([]);
expect(audit.errors).toEqual([]);
});
test("exports exact-decimal JSON Patch without losing the prior result", async ({
page,
}) => {
const audit = auditPage(page);
await openApp(page);
await page.getByRole("tab", { name: /JSON Semantic/u }).click();
await page.getByTestId("left-editor").fill('{"amount":1,"keep":true}');
await page
.getByTestId("right-editor")
.fill('{"amount":9007199254740993123456789,"keep":true}');
await page.getByRole("button", { name: "Compare now" }).click();
await expect(page.getByText(/1 substantive change row/u)).toBeVisible();
await page.getByRole("tab", { name: "Patches" }).click();
await expect(page.getByTestId("json-patch")).toContainText(
"9007199254740993123456789",
);
await page.getByRole("tab", { name: /XML Namespace-aware/u }).click();
await expect(page.getByText(/substantive change row/u)).toBeVisible();
const previousVerdict = page.getByText("Different", { exact: true }).first();
await expect(previousVerdict).toBeVisible();
await page.getByTestId("left-editor").fill("<!DOCTYPE x><x/>");
await page.getByRole("button", { name: "Compare now" }).click();
await expect(page.getByText("xml.doctype-rejected")).toBeVisible();
await expect(previousVerdict).toBeVisible();
expect(audit.external).toEqual([]);
expect(audit.errors).toEqual([]);
});
test("opens local keyed CSV and reports duplicate keys", async ({ page }) => {
const audit = auditPage(page);
await openApp(page);
await page.getByRole("tab", { name: /CSV \/ TSV Keyed rows/u }).click();
await page.getByTestId("left-file-input").setInputFiles({
name: "duplicates.csv",
mimeType: "text/csv",
buffer: Buffer.from("id,name\n1,Ada\n1,Grace", "utf8"),
});
await expect(
page.getByRole("heading", { name: "duplicates.csv" }),
).toBeVisible();
await page.getByRole("button", { name: "Compare now" }).click();
await expect(page.getByText("csv.duplicate-key")).toBeVisible();
await expect(page.getByText("Duplicate key", { exact: false })).toBeVisible();
expect(audit.external).toEqual([]);
expect(audit.errors).toEqual([]);
});
test("serves the release identity and hardened headers", async ({
request,
}) => {
const index = await request.get(APP_PATH);
expect(index.ok()).toBe(true);
expect(index.headers()["content-security-policy"]).toContain(
"default-src 'self'",
);
expect(index.headers()["content-security-policy"]).toContain(
"worker-src 'self' blob:",
);
expect(await index.text()).not.toMatch(/\b(?:src|href)=["']\//u);
const manifest = await request.get(`${APP_PATH}toolbox-app.json`);
await expect(manifest.json()).resolves.toMatchObject({
id: "de.add-ideas.diff-tools",
version: "0.1.0",
entry: "./",
privacy: { processing: "local", fileUploads: true, telemetry: false },
});
});
+52
View File
@@ -0,0 +1,52 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { Workbench } from "../../src/components/Workbench";
describe("Diff Tools workbench", () => {
it("renders exact newline metadata and both result layouts", async () => {
const user = userEvent.setup();
render(<Workbench />);
expect(screen.getByText("Browser-local")).toBeVisible();
expect(await screen.findByText(/substantive change row/u)).toBeVisible();
expect(screen.getAllByText("CRLF").length).toBeGreaterThan(0);
await user.click(screen.getByRole("tab", { name: "Side by side" }));
expect(
screen.getByRole("table", { name: "Side-by-side differences" }),
).toBeVisible();
});
it("keeps exact large numbers in the RFC 6902 artifact", async () => {
const user = userEvent.setup();
render(<Workbench />);
await user.click(screen.getByRole("tab", { name: /JSON Semantic/u }));
fireEvent.change(screen.getByTestId("left-editor"), {
target: { value: '{"n":1}' },
});
fireEvent.change(screen.getByTestId("right-editor"), {
target: { value: '{"n":9007199254740993123456789}' },
});
await user.click(screen.getByRole("button", { name: "Compare now" }));
await waitFor(() =>
expect(screen.getByText(/1 substantive change row/u)).toBeVisible(),
);
await user.click(screen.getByRole("tab", { name: "Patches" }));
expect(
(screen.getByTestId("json-patch") as HTMLTextAreaElement).value,
).toContain("9007199254740993123456789");
});
it("keeps the last successful XML result visible beside an error", async () => {
const user = userEvent.setup();
render(<Workbench />);
await user.click(screen.getByRole("tab", { name: /XML Namespace-aware/u }));
expect(await screen.findByText(/substantive change row/u)).toBeVisible();
const verdict = screen.getByText("Different", { exact: true });
fireEvent.change(screen.getByTestId("left-editor"), {
target: { value: "<!DOCTYPE x><x/>" },
});
await user.click(screen.getByRole("button", { name: "Compare now" }));
expect(await screen.findByText("xml.doctype-rejected")).toBeVisible();
expect(verdict).toBeVisible();
});
});
+253
View File
@@ -0,0 +1,253 @@
import { describe, expect, it } from "vitest";
import { compareInputs } from "../../src/core/compare";
import { DEFAULT_OPTIONS, type CompareRequest } from "../../src/core/types";
function request(
mode: CompareRequest["mode"],
left: string,
right: string,
overrides: Partial<CompareRequest["options"]> = {},
): CompareRequest {
return {
mode,
left,
right,
options: {
text: { ...DEFAULT_OPTIONS.text, ...overrides.text },
json: { ...DEFAULT_OPTIONS.json, ...overrides.json },
xml: { ...DEFAULT_OPTIONS.xml, ...overrides.xml },
csv: { ...DEFAULT_OPTIONS.csv, ...overrides.csv },
},
};
}
describe("text comparison", () => {
it("preserves exact final-newline and line-ending state", () => {
const result = compareInputs(request("text", "one\r\ntwo", "one\ntwo\n"));
expect(result.left.newlines).toEqual({
lf: 0,
crlf: 1,
cr: 0,
final: "none",
});
expect(result.right.newlines).toEqual({
lf: 2,
crlf: 0,
cr: 0,
final: "lf",
});
expect(result.exactlyEqual).toBe(false);
expect(result.unifiedPatch).toContain("\\ No newline at end of file");
});
it("shows source differences that compare equal after normalization", () => {
const result = compareInputs(
request("text", "Cafe\u0301\r\n", "CAFÉ\n", {
text: {
...DEFAULT_OPTIONS.text,
unicodeNormalization: "NFC",
ignoreCase: true,
ignoreLineEndingStyle: true,
},
}),
);
expect(result.semanticallyEqual).toBe(true);
expect(result.rows).toEqual(
expect.arrayContaining([expect.objectContaining({ kind: "normalized" })]),
);
});
it.each(["line", "word", "code-point", "grapheme"] as const)(
"supports %s granularity",
(granularity) => {
const result = compareInputs(
request("text", "A 👨‍👩‍👧", "A 👨‍👩‍👦", {
text: { ...DEFAULT_OPTIONS.text, granularity },
}),
);
expect(
result.stats.modified + result.stats.added + result.stats.removed,
).toBeGreaterThan(0);
},
);
it.each(["line", "word", "code-point", "grapheme"] as const)(
"normalizes line-ending style at %s granularity",
(granularity) => {
const result = compareInputs(
request("text", "A\r\nB", "A\nB", {
text: {
...DEFAULT_OPTIONS.text,
granularity,
ignoreLineEndingStyle: true,
},
}),
);
expect(result.semanticallyEqual).toBe(true);
expect(result.stats.normalized).toBeGreaterThan(0);
},
);
});
describe("semantic JSON", () => {
it("compares object order and exact decimal value semantically", () => {
const result = compareInputs(
request(
"json",
'{"amount":1.00,"large":9007199254740993123456789,"name":"Ada"}',
'{"name":"Ada","large":9007199254740993123456789,"amount":1e0}',
),
);
expect(result.semanticallyEqual).toBe(true);
expect(result.exactlyEqual).toBe(false);
expect(result.stats.normalized).toBeGreaterThanOrEqual(2);
expect(result.jsonPatch).toBe("[]\n");
});
it("reports common-member reordering alongside additions", () => {
const result = compareInputs(
request("json", '{"a":1,"b":2}', '{"b":2,"a":1,"c":3}'),
);
expect(result.rows).toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "normalized", path: "" }),
expect.objectContaining({ kind: "added", path: "/c" }),
]),
);
});
it("emits an exact RFC 6902 patch", () => {
const result = compareInputs(
request(
"json",
'{"amount":1,"items":["a","b"]}',
'{"amount":9007199254740993123456789,"items":["a","c","d"]}',
),
);
expect(result.jsonPatch).toContain("9007199254740993123456789");
expect(result.jsonPatch).toContain('"op": "replace"');
expect(result.jsonPatch).toContain('"op": "add"');
});
it("rejects duplicates and prototype-affecting keys", () => {
expect(() =>
compareInputs(request("json", '{"a":1,"a":2}', '{"a":2}')),
).toThrow(/Duplicate JSON key/iu);
expect(() =>
compareInputs(request("json", '{"__proto__":1}', "{}")),
).toThrow(/prototype-affecting/iu);
});
});
describe("namespace-aware XML", () => {
it("ignores prefixes and attribute order while keeping them visible", () => {
const result = compareInputs(
request(
"xml",
'<a:root xmlns:a="urn:x" id="1" role="test"><![CDATA[ hello ]]></a:root>',
'<b:root role="test" id="1" xmlns:b="urn:x"> hello </b:root>',
),
);
expect(result.semanticallyEqual).toBe(true);
expect(result.stats.normalized).toBeGreaterThan(0);
});
it("rejects DOCTYPE and XInclude before comparison", () => {
expect(() =>
compareInputs(request("xml", "<!DOCTYPE x><x/>", "<x/>")),
).toThrow(/rejected/iu);
expect(() =>
compareInputs(
request(
"xml",
'<x xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="x"/></x>',
"<x/>",
),
),
).toThrow(/XInclude/iu);
});
it("rejects excessive XML nesting before DOM construction", () => {
const deeplyNested = `${"<x>".repeat(129)}${"</x>".repeat(129)}`;
expect(() => compareInputs(request("xml", deeplyNested, "<x/>"))).toThrow(
/XML depth/iu,
);
});
it("treats attribute order as substantive when normalization is disabled", () => {
const result = compareInputs(
request("xml", '<x a="1" b="2"/>', '<x b="2" a="1"/>', {
xml: { ...DEFAULT_OPTIONS.xml, ignoreAttributeOrder: false },
}),
);
expect(result.semanticallyEqual).toBe(false);
expect(result.rows).toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "modified", path: "/x[1]/@*" }),
]),
);
});
});
describe("keyed CSV", () => {
it("matches rows by key and compares fields as strings", () => {
const result = compareInputs(
request(
"csv",
"id,name,value\n2,Grace,001\n1,Ada,10",
"value,id,name\n10,1,Ada\n002,2,Grace",
{ csv: { ...DEFAULT_OPTIONS.csv, keyColumns: "id" } },
),
);
expect(result.rows).toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "normalized", path: "$columns" }),
expect.objectContaining({
kind: "modified",
left: "001",
right: "002",
}),
]),
);
});
it("rejects duplicate keys with a row diagnostic", () => {
expect(() =>
compareInputs(
request("csv", "id,name\n1,A\n1,B", "id,name\n1,A", {
csv: { ...DEFAULT_OPTIONS.csv, keyColumns: "id" },
}),
),
).toThrow(/Duplicate key/iu);
});
it("keeps common row and column reordering visible when items are added", () => {
const result = compareInputs(
request(
"csv",
"id,a,b\n1,A,B\n2,C,D",
"b,id,a,extra\nD,2,C,x\nB,1,A,y\nZ,3,E,z",
{ csv: { ...DEFAULT_OPTIONS.csv, keyColumns: "id" } },
),
);
expect(result.rows).toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "normalized", path: "$columns" }),
expect.objectContaining({ kind: "normalized", path: "$rows" }),
expect.objectContaining({ kind: "added", path: "$columns/extra" }),
]),
);
});
});
describe("portable artifacts", () => {
it("produces a schema-labelled JSON report without library HTML", () => {
const result = compareInputs(request("text", "before\n", "after\n"));
expect(JSON.parse(result.report)).toMatchObject({
schema: "de.add-ideas.diff-tools.report.v1",
generatedLocally: true,
});
expect(result.report).not.toContain("<ins");
expect(result.unifiedPatch).toContain("--- before.txt");
});
});