Skip to content

Commit

Permalink
feat: handleEdit
Browse files Browse the repository at this point in the history
  • Loading branch information
JakobVogelsang committed Oct 2, 2024
1 parent 1619630 commit f7fcaf5
Show file tree
Hide file tree
Showing 4 changed files with 705 additions and 0 deletions.
280 changes: 280 additions & 0 deletions handleEdit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
import { expect } from "chai";

import {
insert,
isValidInsert,
remove,
sclDocString,
setAttribute,
setTextContent,
testDocs,
UndoRedoTestCase,
undoRedoTestCases,
xmlAttributeName,
} from "./testHelpers.js";

import { EditV2, handleEdit, Insert } from "./handleEdit.js";

import { assert, property } from "fast-check";

describe("handleEdit", () => {
let sclDoc: XMLDocument;

beforeEach(async () => {
sclDoc = new DOMParser().parseFromString(sclDocString, "application/xml");
});

it("does nothing given invalid input", () => {
const sclDocStringBefore = new XMLSerializer().serializeToString(sclDoc);

const parent = sclDoc.documentElement;
const node = sclDoc.createElement("test");
const reference = sclDoc.querySelector("Substation");
const invalidedit = {
parent,
someinvalidkey: node,
reference,
} as unknown as Insert;

const undoEdit = handleEdit(invalidedit);

const sclDocStringAfter = new XMLSerializer().serializeToString(sclDoc);

expect(undoEdit).to.be.an("array").that.is.empty;
expect(sclDocStringBefore).to.equal(sclDocStringAfter);
});

it("inserts an element given an Insert", () => {
const parent = sclDoc.documentElement;
const node = sclDoc.createElement("test");
const reference = sclDoc.querySelector("Substation");
handleEdit({ parent, node, reference });
expect(sclDoc.documentElement.querySelector("test")).to.have.property(
"nextSibling",
reference,
);
});

it("removes an element given a Remove", () => {
const node = sclDoc.querySelector("Substation")!;
handleEdit({ node });
expect(sclDoc.querySelector("Substation")).to.not.exist;
});

it("updates an element's attributes given a SetAttributes", () => {
const element = sclDoc.querySelector("Substation")!;
handleEdit({
element,
attributes: {
name: "A2",
desc: null,
["__proto__"]: "a string", // covers a rare edge case branch
},
attributesNS: {
"http://example.org/myns": {
"myns:attr": "value1",
"myns:attr2": "value1",
},
"http://example.org/myns2": {
attr: "value2",
attr2: "value2",
},
"http://example.org/myns3": {
attr: "value3",
attr2: "value3",
},
},
});

expect(element.getAttribute("name")).to.equal("A2");
expect(element.getAttribute("desc")).to.be.null;
expect(element.getAttribute("__proto__")).to.equal("a string");
expect(element.getAttribute("myns:attr")).to.equal("value1");
expect(element.getAttribute("myns:attr2")).to.equal("value1");
expect(element.getAttribute("ens2:attr")).to.equal("value2");
expect(element.getAttribute("ens2:attr2")).to.equal("value2");
expect(element.getAttribute("ens3:attr")).to.equal("value3");
expect(element.getAttribute("ens3:attr2")).to.equal("value3");
});

it("sets an element's textContent given a SetTextContent", () => {
const element = sclDoc.querySelector("SCL")!;

const newTextContent = "someNewTextContent";
handleEdit({
element,
textContent: newTextContent,
});

expect(element.textContent).to.equal(newTextContent);
});

it("processes complex edits in the given order", () => {
const parent = sclDoc.documentElement;
const reference = sclDoc.querySelector("Substation");
const node1 = sclDoc.createElement("test1");
const node2 = sclDoc.createElement("test2");
handleEdit([
{ parent, node: node1, reference },
{ parent, node: node2, reference },
]);
expect(sclDoc.documentElement.querySelector("test1")).to.have.property(
"nextSibling",
node2,
);
expect(sclDoc.documentElement.querySelector("test2")).to.have.property(
"nextSibling",
reference,
);
});

it("returns an edit that undoes the original edit", () => {
const node = sclDoc.querySelector("Substation")!;
const undoEdit = handleEdit({ node }); // do edit
handleEdit(undoEdit); // undo edit
expect(sclDoc.querySelector("Substation")).to.exist;
});

it("returns an undo the return value of which is a redo", () => {
const node = sclDoc.querySelector("Substation")!;
const undoEdit = handleEdit({ node });
const redoEdit = handleEdit(undoEdit);
handleEdit(redoEdit);
expect(sclDoc.querySelector("Substation")).to.not.exist;
});

describe("generally", () => {
it("inserts elements given Inserts", () =>
assert(
property(
testDocs.chain(([doc1, doc2]) => {
const nodes = doc1.nodes.concat(doc2.nodes);
return insert(nodes);
}),
(edit) => {
handleEdit(edit);
if (isValidInsert(edit))
return (
edit.node.parentElement === edit.parent &&
edit.node.nextSibling === edit.reference
);
return true;
},
),
));

it("set's an element's textContent given SetTextContents", () =>
assert(
property(
testDocs.chain(([doc1, doc2]) => {
const nodes = doc1.nodes.concat(doc2.nodes);
return setTextContent(nodes);
}),
(edit) => {
handleEdit(edit);

return edit.element.textContent === edit.textContent;
},
),
));

it("updates attributes given SetAttributes", () =>
assert(
property(
testDocs.chain(([{ nodes }]) => setAttribute(nodes)),
(edit) => {
handleEdit(edit);
return (
Object.entries(edit.attributes)
.filter(([name]) => xmlAttributeName.test(name))
.map((entry) => entry as [string, string | null])
.every(
([name, value]) => edit.element.getAttribute(name) === value,
) &&
Object.entries(edit.attributesNS)
.map(
(entry) => entry as [string, Record<string, string | null>],
)
.every(([ns, attributes]) =>
Object.entries(attributes)
.filter(([name]) => xmlAttributeName.test(name))
.map((entry) => entry as [string, string | null])
.every(
([name, value]) =>
edit.element.getAttributeNS(
ns,
name.includes(":")
? <string>name.split(":", 2)[1]
: name,
) === value,
),
)
);
},
),
)).timeout(20000);

it("removes elements given Removes", () =>
assert(
property(
testDocs.chain(([{ nodes }]) => remove(nodes)),
({ node }) => {
handleEdit({ node });
return !node.parentNode;
},
),
));

it("leaves the document unchanged after undoing all edits", () =>
assert(
property(
testDocs.chain((docs) => undoRedoTestCases(...docs)),
({ doc1, doc2, edits }: UndoRedoTestCase) => {
const [oldDoc1, oldDoc2] = [doc1, doc2].map((doc) =>
doc.cloneNode(true),
);
const undoEdits: EditV2[] = [];
edits.forEach((a: EditV2) => {
const ed = handleEdit(a);
undoEdits.unshift(ed);
});
if (edits.length) handleEdit(undoEdits);
expect(doc1).to.satisfy((doc: XMLDocument) =>
doc.isEqualNode(oldDoc1),
);
expect(doc2).to.satisfy((doc: XMLDocument) =>
doc.isEqualNode(oldDoc2),
);
return true;
},
),
)).timeout(20000);

it("changes the document the same way when redoing undone edits", () =>
assert(
property(
testDocs.chain((docs) => undoRedoTestCases(...docs)),
({ doc1, doc2, edits }: UndoRedoTestCase) => {
const undoEdits: EditV2[] = [];
edits.forEach((a: EditV2) => {
undoEdits.unshift(handleEdit(a));
});
const [oldDoc1, oldDoc2] = [doc1, doc2].map((doc) =>
new XMLSerializer().serializeToString(doc),
);
const redoEdits: EditV2[] = [];

if (edits.length) {
redoEdits.unshift(handleEdit(undoEdits));
handleEdit(redoEdits);
}
const [newDoc1, newDoc2] = [doc1, doc2].map((doc) =>
new XMLSerializer().serializeToString(doc),
);
return oldDoc1 === newDoc1 && oldDoc2 === newDoc2;
},
),
)).timeout(20000);
});
});
Loading

0 comments on commit f7fcaf5

Please sign in to comment.