UNPKG

@plugjs/expect5

Version:

Unit Testing for the PlugJS Build System ========================================

251 lines (250 loc) 9.79 kB
// 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