@plugjs/expect5
Version:
Unit Testing for the PlugJS Build System ========================================
251 lines (250 loc) • 9.79 kB
JavaScript
// expectation/diff.ts
import {
ExpectationError,
isMatcher,
stringifyConstructor,
stringifyValue
} from "./types.mjs";
function findEnumerableKeys(...objects) {
const keys = /* @__PURE__ */ new Set();
for (const object of objects) {
for (const key in object) {
keys.add(key);
}
}
return keys;
}
function isSameNumberOrBothNaN(a, b) {
return a === b ? true : isNaN(a) ? isNaN(b) : false;
}
function errorDiff(value, message) {
const error = `Expected ${stringifyValue(value)} ${message}`;
return { diff: true, value, error };
}
function objectDiff(actual, expected, remarks, keys) {
if (!keys) keys = findEnumerableKeys(actual, expected);
if (!keys.size) return { diff: false, value: actual };
let diff2 = false;
const props = {};
for (const key of keys) {
const act = actual[key];
const exp = expected[key];
let result;
if (act === void 0 && exp === void 0 && !remarks.strict) {
result = { diff: false, value: void 0 };
} else if (key in expected && !(key in actual)) {
result = { diff: true, missing: exp };
} else if (key in actual && !(key in expected)) {
result = { diff: true, extra: act };
} else {
result = diffValues(act, exp, remarks);
}
props[key] = result;
diff2 ||= result.diff;
}
return { diff: diff2, value: actual, props };
}
function arrayDiff(actual, expected, remarks) {
if (actual.length !== expected.length) {
return errorDiff(actual, `to have length ${expected.length} (length=${actual.length})`);
}
const keys = findEnumerableKeys(actual, expected);
let valuesDiff = false;
const values = new Array(expected.length);
for (let i = 0; i < expected.length; i++) {
const result2 = values[i] = diffValues(actual[i], expected[i], remarks);
valuesDiff = valuesDiff || result2.diff;
keys.delete(String(i));
}
const result = objectDiff(actual, expected, remarks, keys);
const diff2 = result.diff || valuesDiff;
return { ...result, diff: diff2, values };
}
function setDiff(actual, expected, remarks) {
const error = actual.size === expected.size ? {} : errorDiff(actual, `to have size ${expected.size} (size=${actual.size})`);
const values = [];
const missing = new Set(expected);
const extra = new Set(actual);
for (const act of extra) {
for (const exp of missing) {
const diff3 = diffValues(act, exp, remarks);
if (diff3.diff) continue;
values.push(diff3);
extra.delete(act);
missing.delete(exp);
}
}
const result = objectDiff(actual, expected, remarks);
const diff2 = !!(missing.size || extra.size || result.diff);
extra.forEach((value) => values.push({ diff: true, extra: value }));
missing.forEach((value) => values.push({ diff: true, missing: value }));
return { ...error, ...result, diff: diff2, values };
}
function mapDiff(actual, expected, remarks) {
let diff2 = false;
const mappings = [];
for (const key of /* @__PURE__ */ new Set([...actual.keys(), ...expected.keys()])) {
const act = actual.get(key);
const exp = expected.get(key);
if (!actual.has(key)) {
mappings.push([key, { diff: true, missing: exp }]);
diff2 = true;
} else if (!expected.has(key)) {
mappings.push([key, { diff: true, extra: act }]);
diff2 = true;
} else {
const result2 = diffValues(act, exp, remarks);
mappings.push([key, result2]);
diff2 = diff2 || result2.diff;
}
}
const result = objectDiff(actual, expected, remarks);
diff2 = diff2 || result.diff;
return { ...result, diff: diff2, mappings };
}
function binaryDiff(actual, expected, actualData, expectedData, remarks) {
if (actualData.length !== expectedData.length) {
return errorDiff(actual, `to have length ${expectedData.length} (length=${actualData.length})`);
}
const keys = findEnumerableKeys(actual, expected);
if (actual instanceof Buffer) {
for (const key in Buffer.prototype) {
keys.delete(key);
}
}
const length = expectedData.length;
for (let i = 0; i < length; i++) {
keys.delete(String(i));
if (actualData[i] === expectedData[i]) continue;
let act = actualData.toString("hex", i, i + 6);
let exp = expectedData.toString("hex", i, i + 6);
if (act.length > 10) {
act = act.substring(0, 10) + "\u2026";
exp = exp.substring(0, 10) + "\u2026";
}
if (i > 0) {
act = "\u2026" + act;
exp = "\u2026" + exp;
}
return errorDiff(actual, `to equal at index ${i} (actual=${act}, expected=${exp})`);
}
return objectDiff(actual, expected, remarks, keys);
}
function primitiveDiff(actual, expected, remarks) {
if (actual.valueOf() !== expected.valueOf()) {
return {
diff: true,
value: actual,
expected
};
}
const keys = findEnumerableKeys(actual, expected);
if (actual instanceof String) {
const length = actual.valueOf().length;
for (let i = 0; i < length; i++) {
keys.delete(String(i));
}
}
return keys.size ? objectDiff(actual, expected, remarks, keys) : {
diff: false,
value: actual
};
}
function diffValues(actual, expected, remarks) {
if (expected === actual) return { diff: false, value: expected };
if (expected === null) {
return {
diff: true,
value: actual,
expected: null
};
}
if (isMatcher(expected)) {
try {
expected.expect(actual);
return { diff: false, value: actual };
} catch (error) {
if (error instanceof ExpectationError) {
const { message, diff: diff2 } = error;
return diff2?.diff ? diff2 : { diff: true, value: actual, error: message };
} else {
throw error;
}
}
}
const actualType = typeof actual;
const expectedType = typeof expected;
if (actualType !== expectedType) {
return {
diff: true,
value: actual,
expected
};
}
switch (actualType) {
// numbers are one-of-a-kind as NaN !== NaN
case "number":
if (isSameNumberOrBothNaN(actual, expected)) {
return { diff: false, value: NaN };
}
// primitives always must be strict ===
case "bigint":
case "boolean":
case "function":
case "string":
case "symbol":
case "undefined":
return {
diff: true,
value: actual,
expected
};
}
const actualIndex = remarks.actualMemos.indexOf(actual);
if (actualIndex !== -1) {
if (actualIndex === remarks.expectedMemos.indexOf(expected)) {
return { diff: false, value: actual };
}
}
remarks.actualMemos.push(actual);
remarks.expectedMemos.push(expected);
const prototype = Object.getPrototypeOf(expected);
if (prototype && prototype.constructor) {
if (!(actual instanceof prototype.constructor)) {
return {
...errorDiff(actual, `to be instance of ${stringifyConstructor(prototype.constructor)}`),
diff: true,
expected
};
}
}
const checkInstance = (ctor, callback) => expected instanceof ctor ? callback(actual, expected, remarks) : actual instanceof ctor ? { diff: true, value: actual, expected } : void 0;
return (
/* == ARRAYS ============================================================ */
checkInstance(Array, arrayDiff) || /* == SETS ============================================================== */
checkInstance(Set, (act, exp) => setDiff(act, exp, remarks)) || /* == MAPS ============================================================== */
checkInstance(Map, (act, exp) => mapDiff(act, exp, remarks)) || /* == BOXED PRIMITIVES ================================================== */
checkInstance(Boolean, primitiveDiff) || checkInstance(String, primitiveDiff) || checkInstance(Number, primitiveDiff) || /* == PROMISES (always error, must be ===) ============================== */
checkInstance(Promise, (act, exp) => errorDiff(act, `to strictly equal ${stringifyValue(exp)}`)) || /* == DATES ============================================================= */
checkInstance(Date, (act, exp) => !isSameNumberOrBothNaN(act.getTime(), exp.getTime()) ? {
diff: true,
value: act,
expected: exp
} : objectDiff(act, exp, remarks)) || /* == REGULAR EXPRESSIONS =============================================== */
checkInstance(RegExp, (act, exp) => act.source !== exp.source || act.flags !== exp.flags ? {
diff: true,
value: act,
expected: exp
} : objectDiff(act, exp, remarks)) || /* == BINARY ARRAYS ===================================================== */
checkInstance(Buffer, (act, exp) => binaryDiff(act, exp, act, exp, remarks)) || checkInstance(Uint8Array, (act, exp) => binaryDiff(act, exp, Buffer.from(act), Buffer.from(exp), remarks)) || checkInstance(ArrayBuffer, (act, exp) => binaryDiff(act, exp, Buffer.from(act), Buffer.from(exp), remarks)) || checkInstance(SharedArrayBuffer, (act, exp) => binaryDiff(act, exp, Buffer.from(act), Buffer.from(exp), remarks)) || /* == OTHER TYPED ARRAYS ================================================ */
checkInstance(BigInt64Array, arrayDiff) || checkInstance(BigUint64Array, arrayDiff) || checkInstance(Float32Array, arrayDiff) || checkInstance(Float64Array, arrayDiff) || checkInstance(Int16Array, arrayDiff) || checkInstance(Int32Array, arrayDiff) || checkInstance(Int8Array, arrayDiff) || checkInstance(Uint16Array, arrayDiff) || checkInstance(Uint32Array, arrayDiff) || checkInstance(Uint8ClampedArray, arrayDiff) || /* == DONE ============================================================== */
objectDiff(actual, expected, remarks)
);
}
function diff(actual, expected, strict = false) {
return diffValues(actual, expected, { actualMemos: [], expectedMemos: [], strict });
}
export {
diff
};
//# sourceMappingURL=diff.mjs.map