Files
meteor/packages/diff-sequence/tests.js
2026-04-10 10:46:41 -03:00

345 lines
10 KiB
JavaScript

Tinytest.add("diff-sequence - diff changes ordering", function (test) {
const makeDocs = function (ids) {
return ids.map(function (id) {
return { _id: id };
});
};
const testMutation = function (a, b) {
const aa = makeDocs(a);
const bb = makeDocs(b);
const aaCopy = EJSON.clone(aa);
DiffSequence.diffQueryOrderedChanges(aa, bb, {
addedBefore: function (id, doc, before) {
if (before === null) {
aaCopy.push(Object.assign({ _id: id }, doc));
return;
}
for (let i = 0; i < aaCopy.length; i++) {
if (aaCopy[i]._id === before) {
aaCopy.splice(i, 0, Object.assign({ _id: id }, doc));
return;
}
}
},
movedBefore: function (id, before) {
let found;
for (let i = 0; i < aaCopy.length; i++) {
if (aaCopy[i]._id === id) {
found = aaCopy[i];
aaCopy.splice(i, 1);
}
}
if (before === null) {
aaCopy.push(Object.assign({ _id: id }, found));
return;
}
for (let i = 0; i < aaCopy.length; i++) {
if (aaCopy[i]._id === before) {
aaCopy.splice(i, 0, Object.assign({ _id: id }, found));
return;
}
}
},
removed: function (id) {
for (let i = 0; i < aaCopy.length; i++) {
if (aaCopy[i]._id === id) {
aaCopy.splice(i, 1);
}
}
},
});
test.equal(aaCopy, bb);
};
const testBothWays = function (a, b) {
testMutation(a, b);
testMutation(b, a);
};
testBothWays(["a", "b", "c"], ["c", "b", "a"]);
testBothWays(["a", "b", "c"], []);
testBothWays(["a", "b", "c"], ["e", "f"]);
testBothWays(["a", "b", "c", "d"], ["c", "b", "a"]);
testBothWays(
["A", "B", "C", "D", "E", "F", "G", "H", "I"],
["A", "B", "F", "G", "C", "D", "I", "L", "M", "N", "H"],
);
testBothWays(
["A", "B", "C", "D", "E", "F", "G", "H", "I"],
["A", "B", "C", "D", "F", "G", "H", "E", "I"],
);
});
Tinytest.add("diff-sequence - diff", function (test) {
// test correctness
const diffTest = function (origLen, newOldIdx) {
const oldResults = Array.from({ length: origLen });
for (let i = 1; i <= origLen; i++) oldResults[i - 1] = { _id: i };
const newResults = newOldIdx.map(function (n) {
const doc = { _id: Math.abs(n) };
if (n < 0) doc.changed = true;
return doc;
});
const find = function (arr, id) {
for (let i = 0; i < arr.length; i++) {
if (EJSON.equals(arr[i]._id, id)) return i;
}
return -1;
};
const results = [...oldResults];
const observer = {
addedBefore: function (id, fields, before) {
let before_idx;
if (before === null) before_idx = results.length;
else before_idx = find(results, before);
const doc = Object.assign({ _id: id }, fields);
test.isFalse(before_idx < 0 || before_idx > results.length);
results.splice(before_idx, 0, doc);
},
removed: function (id) {
const at_idx = find(results, id);
test.isFalse(at_idx < 0 || at_idx >= results.length);
results.splice(at_idx, 1);
},
changed: function (id, fields) {
const at_idx = find(results, id);
const oldDoc = results[at_idx];
const doc = EJSON.clone(oldDoc);
DiffSequence.applyChanges(doc, fields);
test.isFalse(at_idx < 0 || at_idx >= results.length);
test.equal(doc._id, oldDoc._id);
results[at_idx] = doc;
},
movedBefore: function (id, before) {
const old_idx = find(results, id);
let new_idx;
if (before === null) new_idx = results.length;
else new_idx = find(results, before);
if (new_idx > old_idx) new_idx--;
test.isFalse(old_idx < 0 || old_idx >= results.length);
test.isFalse(new_idx < 0 || new_idx >= results.length);
results.splice(new_idx, 0, results.splice(old_idx, 1)[0]);
},
};
DiffSequence.diffQueryOrderedChanges(oldResults, newResults, observer);
test.equal(results, newResults);
};
// edge cases and cases run into during debugging
diffTest(5, [5, 1, 2, 3, 4]);
diffTest(0, [1, 2, 3, 4]);
diffTest(4, []);
diffTest(7, [4, 5, 6, 7, 1, 2, 3]);
diffTest(7, [5, 6, 7, 1, 2, 3, 4]);
diffTest(10, [7, 4, 11, 6, 12, 1, 5]);
diffTest(3, [3, 2, 1]);
diffTest(10, [2, 7, 4, 6, 11, 3, 8, 9]);
diffTest(0, []);
diffTest(1, []);
diffTest(0, [1]);
diffTest(1, [1]);
diffTest(5, [1, 2, 3, 4, 5]);
// interaction between "changed" and other ops
diffTest(5, [-5, -1, 2, -3, 4]);
diffTest(7, [-4, -5, 6, 7, -1, 2, 3]);
diffTest(7, [5, 6, -7, 1, 2, -3, 4]);
diffTest(10, [7, -4, 11, 6, 12, -1, 5]);
diffTest(3, [-3, -2, -1]);
diffTest(10, [-2, 7, 4, 6, 11, -3, -8, 9]);
});
Tinytest.add("diff-sequence - diffObjects partitions keys", (test) => {
const left = { a: 1, b: 2, c: 3 };
const right = { b: 2, c: 4, d: 5 };
const leftOnly = [];
const rightOnly = [];
const both = [];
DiffSequence.diffObjects(left, right, {
leftOnly: (key, value) => leftOnly.push([key, value]),
rightOnly: (key, value) => rightOnly.push([key, value]),
both: (key, leftValue, rightValue) => both.push([key, leftValue, rightValue]),
});
test.equal(leftOnly, [["a", 1]]);
test.equal(rightOnly, [["d", 5]]);
// Sort `both` to make the test independent of Object.keys iteration order.
both.sort((x, y) => x[0].localeCompare(y[0]));
test.equal(both, [
["b", 2, 2],
["c", 3, 4],
]);
});
Tinytest.add("diff-sequence - diffObjects omits missing callbacks", (test) => {
const left = { a: 1, b: 2 };
const right = { b: 3, c: 4 };
let bothCount = 0;
// Only `both` is provided — leftOnly and rightOnly are absent and must not throw.
DiffSequence.diffObjects(left, right, {
both: () => {
bothCount++;
},
});
test.equal(bothCount, 1);
test.ok(); // reaching here means no exception was thrown
});
Tinytest.add("diff-sequence - diffMaps partitions keys", (test) => {
const left = new Map([
["a", 1],
["b", 2],
["c", 3],
]);
const right = new Map([
["b", 2],
["c", 4],
["d", 5],
]);
const leftOnly = [];
const rightOnly = [];
const both = [];
DiffSequence.diffMaps(left, right, {
leftOnly: (key, value) => leftOnly.push([key, value]),
rightOnly: (key, value) => rightOnly.push([key, value]),
both: (key, leftValue, rightValue) => both.push([key, leftValue, rightValue]),
});
test.equal(leftOnly, [["a", 1]]);
test.equal(rightOnly, [["d", 5]]);
both.sort((x, y) => x[0].localeCompare(y[0]));
test.equal(both, [
["b", 2, 2],
["c", 3, 4],
]);
});
Tinytest.add("diff-sequence - makeChangedFields detects adds, removes, changes", (test) => {
const oldDoc = { a: 1, b: 2, c: 3 };
const newDoc = { a: 1, b: 99, d: 4 };
const changed = DiffSequence.makeChangedFields(newDoc, oldDoc);
// 'a' unchanged, 'b' changed, 'c' removed (undefined), 'd' added.
test.equal(changed, { b: 99, c: undefined, d: 4 });
});
Tinytest.add("diff-sequence - makeChangedFields uses EJSON.equals for deep compare", (test) => {
const same = DiffSequence.makeChangedFields(
{ a: [1, 2, 3], b: { nested: true } },
{ a: [1, 2, 3], b: { nested: true } },
);
test.equal(same, {}, "deeply equal values should not produce a change");
const changed = DiffSequence.makeChangedFields(
{ a: [1, 2, 4], b: { nested: true } },
{ a: [1, 2, 3], b: { nested: true } },
);
test.equal(changed, { a: [1, 2, 4] });
});
Tinytest.add("diff-sequence - applyChanges adds, replaces, removes fields", (test) => {
const doc = { a: 1, b: 2 };
DiffSequence.applyChanges(doc, { a: 99, c: 3, b: undefined });
test.equal(doc, { a: 99, c: 3 });
});
Tinytest.add("diff-sequence - diffQueryUnorderedChanges detects added/removed/changed", (test) => {
const oldResults = new IdMap();
oldResults.set("x", { _id: "x", v: 1 });
oldResults.set("y", { _id: "y", v: 2 });
const newResults = new IdMap();
newResults.set("y", { _id: "y", v: 99 });
newResults.set("z", { _id: "z", v: 3 });
const added = [];
const removed = [];
const changed = [];
DiffSequence.diffQueryUnorderedChanges(oldResults, newResults, {
added: (id, fields) => added.push([id, fields]),
removed: (id) => removed.push(id),
changed: (id, fields) => changed.push([id, fields]),
});
test.equal(added, [["z", { v: 3 }]]);
test.equal(removed, ["x"]);
test.equal(changed, [["y", { v: 99 }]]);
});
Tinytest.add(
"diff-sequence - diffQueryUnorderedChanges throws with a movedBefore observer",
(test) => {
const oldResults = new IdMap();
const newResults = new IdMap();
test.throws(
() =>
DiffSequence.diffQueryUnorderedChanges(oldResults, newResults, {
movedBefore: () => {},
}),
/movedBefore/,
);
},
);
Tinytest.add("diff-sequence - diffQueryChanges dispatches based on ordered flag", (test) => {
// Unordered path.
const oldUnordered = new IdMap();
oldUnordered.set("a", { _id: "a", v: 1 });
const newUnordered = new IdMap();
newUnordered.set("a", { _id: "a", v: 2 });
const unorderedChanged = [];
DiffSequence.diffQueryChanges(false, oldUnordered, newUnordered, {
changed: (id, fields) => unorderedChanged.push([id, fields]),
});
test.equal(unorderedChanged, [["a", { v: 2 }]]);
// Ordered path — uses addedBefore/movedBefore.
const oldOrdered = [{ _id: "a", v: 1 }];
const newOrdered = [
{ _id: "a", v: 1 },
{ _id: "b", v: 2 },
];
const orderedAddedBefore = [];
DiffSequence.diffQueryChanges(true, oldOrdered, newOrdered, {
addedBefore: (id, fields, before) => orderedAddedBefore.push([id, fields, before]),
});
test.equal(orderedAddedBefore, [["b", { v: 2 }, null]]);
});
Tinytest.add("diff-sequence - projectionFn is applied before change detection", (test) => {
const oldResults = new IdMap();
oldResults.set("a", { _id: "a", visible: 1, hidden: 10 });
const newResults = new IdMap();
// Only the `hidden` field changed.
newResults.set("a", { _id: "a", visible: 1, hidden: 999 });
const changed = [];
DiffSequence.diffQueryUnorderedChanges(
oldResults,
newResults,
{
changed: (id, fields) => changed.push([id, fields]),
},
{
projectionFn: (doc) => ({ _id: doc._id, visible: doc.visible }),
},
);
// The projection drops `hidden`, so no change is reported.
test.equal(changed, []);
});