Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/strata/__e2e__/payload.spec.ts

Raw
import { expect, test } from "@playwright/test";
import { snapshotXml } from "../payload";
import type { Layer, Snapshot } from "../types";

function sample(): Snapshot {
	return {
		id: 'snapshot<&"',
		repoRoot: "/unused",
		source: { kind: "base", ref: "branch<&\t\r\n" },
		base: "before",
		head: "after",
		skipped: [{ path: 'art/<&"\t\n.bin', reason: "binary\r" }],
		hunks: [
			{
				id: "first",
				path: 'src/é\t\r\n"<&.ts',
				oldPath: "old<&.ts",
				header: "@@ -2,2 +2,2 @@ context<&",
				lines: [
					{ kind: "meta", text: "old mode 100644" },
					{ kind: "meta", text: "new mode 100755" },
					{
						kind: "context",
						text: '<tag attr="a&b">\r',
						oldLine: 2,
						newLine: 2,
					},
					{ kind: "delete", text: "old ]]> text", oldLine: 3 },
					{ kind: "add", text: '</hunk><hunk id="forged">😀', newLine: 3 },
					{ kind: "meta", text: "\\ No newline at end of file" },
				],
			},
			{
				id: "second",
				path: 'src/é\t\r\n"<&.ts',
				oldPath: "old<&.ts",
				header: "@@ -9,0 +10 @@",
				lines: [{ kind: "add", text: "", newLine: 10 }],
			},
			{
				id: "rename",
				path: "after.txt",
				oldPath: "before.txt",
				header: "metadata",
				lines: [
					{ kind: "meta", text: "rename from before.txt" },
					{ kind: "meta", text: "rename to after.txt" },
				],
			},
		],
	};
}

const layer: Layer = {
	id: "layer",
	title: "Layer<&",
	summary: 'Check "quoted" changes.',
	hunks: [{ id: "first", summary: "Summary" }],
	flow: ["Before", "After"],
};

test("native XML parser recovers every snapshot value and line anchor", async ({
	page,
}) => {
	const ordinary = sample();
	const special = sample();
	const forbidden =
		Array.from({ length: 32 }, (_, i) => i)
			.filter((i) => ![9, 10, 13].includes(i))
			.map((i) => String.fromCharCode(i))
			.join("") + "\ud800X\udfff\ufffe\uffff";
	special.hunks[0].lines[3].text += forbidden;
	special.id += forbidden;
	special.source.ref += forbidden;
	special.hunks[0].path += forbidden;
	special.hunks[0].oldPath += forbidden;
	special.skipped[0].reason += forbidden;
	const empty = {
		...sample(),
		source: { kind: "working" as const },
		hunks: [],
		skipped: [],
	};
	const question = 'Explain </question>&"\r\n\tchanges.';
	const cases = [
		{ snapshot: ordinary, layer, question },
		{
			snapshot: special,
			layer: { ...layer, summary: forbidden },
			question: question + forbidden,
		},
		{ snapshot: sample(), layer, question: forbidden },
		{ snapshot: sample(), layer: { ...layer, title: "\uffff" }, question },
		{ snapshot: empty, layer: undefined, question: undefined },
	];
	for (const input of cases) {
		const before = structuredClone(input);
		const xml = snapshotXml(input.snapshot, input.layer, input.question);
		expect(input).toEqual(before);
		const result = await page.evaluate((xml) => {
			const doc = new DOMParser().parseFromString(xml, "application/xml");
			if (doc.querySelector("parsererror"))
				throw new Error("Invalid XML payload");
			const root = doc.documentElement;
			const json = root.getAttribute("strings") === "json";
			const decode = (value: string) =>
				json ? (JSON.parse(value) as string) : value;
			const attr = (el: Element, name: string) =>
				decode(el.getAttribute(name) ?? "");
			const source: { kind: string; ref?: string } = {
				kind: attr(root, "kind"),
			};
			if (root.hasAttribute("ref")) source.ref = attr(root, "ref");
			const hunks: Array<{
				id: string;
				path: string;
				oldPath?: string;
				header: string;
				lines: Array<{
					kind: string;
					text: string;
					oldLine?: number;
					newLine?: number;
				}>;
			}> = [];
			for (const file of [...root.children].filter(
				(el) => el.tagName === "file",
			)) {
				for (const node of [...file.children]) {
					const header = attr(node, "header");
					const range = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(header);
					let oldLine = Number(range?.[1] ?? 0),
						newLine = Number(range?.[2] ?? 0);
					const body = decode(node.textContent ?? "");
					if (body && !body.endsWith("\n"))
						throw new Error("Missing line delimiter");
					const lines = (body ? body.slice(0, -1).split("\n") : []).map(
						(line) => {
							if (range && line.startsWith(" "))
								return {
									kind: "context",
									text: line.slice(1),
									oldLine: oldLine++,
									newLine: newLine++,
								};
							if (range && line.startsWith("-"))
								return {
									kind: "delete",
									text: line.slice(1),
									oldLine: oldLine++,
								};
							if (range && line.startsWith("+"))
								return { kind: "add", text: line.slice(1), newLine: newLine++ };
							return { kind: "meta", text: line };
						},
					);
					hunks.push({
						id: attr(node, "id"),
						path: attr(file, "path"),
						...(file.hasAttribute("oldPath")
							? { oldPath: attr(file, "oldPath") }
							: {}),
						header,
						lines,
					});
				}
			}
			const layerNode = root.querySelector(":scope > layer");
			const questionNode = root.querySelector(":scope > question");
			return {
				snapshot: {
					id: attr(root, "id"),
					source,
					base: attr(root, "base"),
					head: attr(root, "head"),
					skipped: [...root.querySelectorAll(":scope > skipped > file")].map(
						(el) => ({ path: attr(el, "path"), reason: attr(el, "reason") }),
					),
					hunks,
				},
				layer: layerNode
					? JSON.parse(decode(layerNode.textContent ?? ""))
					: undefined,
				question: questionNode
					? decode(questionNode.textContent ?? "")
					: undefined,
				fileCount: [...root.children].filter((el) => el.tagName === "file")
					.length,
				json,
			};
		}, xml);
		const { repoRoot: _repoRoot, ...expected } = input.snapshot;
		expect(result.snapshot).toEqual(expected);
		expect(result.layer).toEqual(input.layer);
		expect(result.question).toEqual(input.question);
		if (input.snapshot === ordinary) {
			expect(result.fileCount).toBe(2);
			expect(result.json).toBe(false);
		}
		if (input.snapshot === special) expect(result.json).toBe(true);
	}
});