1279 lines
45 KiB
TypeScript
1279 lines
45 KiB
TypeScript
import * as Equal from "effect/Equal"
|
|
import * as Hash from "effect/Hash"
|
|
import * as HashMap from "effect/HashMap"
|
|
import * as Option from "effect/Option"
|
|
import { describe, expect, it } from "vitest"
|
|
|
|
describe("Equal.equals", () => {
|
|
describe("plain objects", () => {
|
|
it("should return true for structurally identical objects (structural equality)", () => {
|
|
const obj1 = { a: 1, b: 2 }
|
|
const obj2 = { a: 1, b: 2 }
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
})
|
|
|
|
it("should return true for same reference", () => {
|
|
const obj = { a: 1, b: 2 }
|
|
expect(Equal.equals(obj, obj)).toBe(true)
|
|
})
|
|
|
|
it("should return true for empty objects", () => {
|
|
expect(Equal.equals({}, {})).toBe(true)
|
|
})
|
|
|
|
it("should return false for objects with different values", () => {
|
|
const obj1 = { a: 1, b: 2 }
|
|
const obj2 = { a: 1, b: 3 }
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
|
|
it("should return false for objects with different keys", () => {
|
|
const obj1 = { a: 1, b: 2 }
|
|
const obj2 = { a: 1, c: 2 }
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
|
|
describe("symbol properties", () => {
|
|
it("should return true for objects with identical symbol keys and values", () => {
|
|
const sym1 = Symbol("test")
|
|
const sym2 = Symbol("other")
|
|
const obj1 = { [sym1]: 1, [sym2]: 2 }
|
|
const obj2 = { [sym1]: 1, [sym2]: 2 }
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
})
|
|
|
|
it("should return false for objects with different symbol keys", () => {
|
|
const sym1 = Symbol("test")
|
|
const sym2 = Symbol("test") // Different symbol with same description
|
|
const obj1 = { [sym1]: 1 }
|
|
const obj2 = { [sym2]: 1 }
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
|
|
it("should return false for objects with different symbol values", () => {
|
|
const sym = Symbol("test")
|
|
const obj1 = { [sym]: 1 }
|
|
const obj2 = { [sym]: 2 }
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
|
|
it("should return true for objects with mixed string and symbol keys", () => {
|
|
const sym = Symbol("test")
|
|
const obj1 = { a: 1, [sym]: 2 }
|
|
const obj2 = { a: 1, [sym]: 2 }
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
})
|
|
|
|
it("should return false when one object has symbol keys and the other doesn't", () => {
|
|
const sym = Symbol("test")
|
|
const obj1 = { a: 1, [sym]: 2 }
|
|
const obj2 = { a: 1 }
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
|
|
it("should consider non-enumerable symbol properties", () => {
|
|
const sym1 = Symbol("enumerable")
|
|
const sym2 = Symbol("non-enumerable")
|
|
const obj1: any = { a: 1 }
|
|
const obj2: any = { a: 1 }
|
|
|
|
// Add enumerable symbol property
|
|
obj1[sym1] = "visible"
|
|
obj2[sym1] = "visible"
|
|
|
|
// Add non-enumerable symbol property with same value
|
|
Object.defineProperty(obj1, sym2, { value: "hidden", enumerable: false })
|
|
Object.defineProperty(obj2, sym2, { value: "hidden", enumerable: false })
|
|
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
})
|
|
|
|
it("should consider non-enumerable symbol properties even when they differ", () => {
|
|
const sym1 = Symbol("enumerable")
|
|
const sym2 = Symbol("non-enumerable")
|
|
const obj1: any = { a: 1 }
|
|
const obj2: any = { a: 1 }
|
|
|
|
// Add same enumerable symbol property
|
|
obj1[sym1] = "visible"
|
|
obj2[sym1] = "visible"
|
|
|
|
// Add non-enumerable symbol properties with different values
|
|
Object.defineProperty(obj1, sym2, { value: "hidden1", enumerable: false })
|
|
Object.defineProperty(obj2, sym2, { value: "hidden2", enumerable: false })
|
|
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
|
|
it("should consider enumerable symbol properties even when string properties are the same", () => {
|
|
const sym = Symbol("test")
|
|
const obj1: any = { a: 1, b: 2 }
|
|
const obj2: any = { a: 1, b: 2 }
|
|
|
|
obj1[sym] = "different"
|
|
obj2[sym] = "values"
|
|
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("non-enumerable properties", () => {
|
|
it("should handle non-enumerable string properties correctly", () => {
|
|
const obj1 = { a: 1 }
|
|
const obj2 = { a: 1 }
|
|
|
|
Object.defineProperty(obj1, "hidden", { value: "secret", enumerable: false })
|
|
Object.defineProperty(obj2, "hidden", { value: "secret", enumerable: false })
|
|
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
})
|
|
|
|
it("should consider non-enumerable string properties even when they differ", () => {
|
|
const obj1 = { a: 1 }
|
|
const obj2 = { a: 1 }
|
|
|
|
Object.defineProperty(obj1, "hidden", { value: "secret1", enumerable: false })
|
|
Object.defineProperty(obj2, "hidden", { value: "secret2", enumerable: false })
|
|
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
|
|
it("should consider enumerable properties and non-enumerable ones", () => {
|
|
const obj1: any = { a: 1 }
|
|
const obj2: any = { a: 1 }
|
|
|
|
// Add same enumerable property
|
|
obj1.b = 2
|
|
obj2.b = 2
|
|
|
|
// Add different non-enumerable properties (should be considered)
|
|
Object.defineProperty(obj1, "hidden", { value: "secret1", enumerable: false })
|
|
Object.defineProperty(obj2, "hidden", { value: "secret2", enumerable: false })
|
|
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("hash consistency with symbol properties", () => {
|
|
it("should produce same hash for objects with identical symbol keys and values", () => {
|
|
const sym1 = Symbol("test")
|
|
const sym2 = Symbol("other")
|
|
const obj1 = { [sym1]: 1, [sym2]: 2 }
|
|
const obj2 = { [sym1]: 1, [sym2]: 2 }
|
|
|
|
expect(Hash.hash(obj1)).toBe(Hash.hash(obj2))
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
})
|
|
|
|
it("should return false for equality even if symbols have same hash", () => {
|
|
const sym1 = Symbol("test")
|
|
const sym2 = Symbol("test") // Different symbol with same description
|
|
const obj1 = { [sym1]: 1 }
|
|
const obj2 = { [sym2]: 1 }
|
|
|
|
// Note: symbols with same description may have same hash (acceptable hash collision)
|
|
// but equality should still work correctly
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
|
|
it("should produce different hash for symbols with different descriptions", () => {
|
|
const sym1 = Symbol("first")
|
|
const sym2 = Symbol("second")
|
|
const obj1 = { [sym1]: 1 }
|
|
const obj2 = { [sym2]: 1 }
|
|
|
|
expect(Hash.hash(obj1)).not.toBe(Hash.hash(obj2))
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
|
|
it("should produce different hash for objects with different symbol values", () => {
|
|
const sym = Symbol("test")
|
|
const obj1 = { [sym]: 1 }
|
|
const obj2 = { [sym]: 2 }
|
|
|
|
expect(Hash.hash(obj1)).not.toBe(Hash.hash(obj2))
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
|
|
it("should produce same hash for objects with mixed string and symbol keys", () => {
|
|
const sym = Symbol("test")
|
|
const obj1 = { a: 1, [sym]: 2 }
|
|
const obj2 = { a: 1, [sym]: 2 }
|
|
|
|
expect(Hash.hash(obj1)).toBe(Hash.hash(obj2))
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
})
|
|
|
|
it("should consider non-enumerable symbol properties in hash calculation", () => {
|
|
const sym1 = Symbol("enumerable")
|
|
const sym2 = Symbol("non-enumerable")
|
|
const obj1: any = { a: 1 }
|
|
const obj2: any = { a: 1 }
|
|
|
|
// Add same enumerable symbol property
|
|
obj1[sym1] = "visible"
|
|
obj2[sym1] = "visible"
|
|
|
|
// Add different non-enumerable symbol properties
|
|
Object.defineProperty(obj1, sym2, { value: "hidden1", enumerable: false })
|
|
Object.defineProperty(obj2, sym2, { value: "hidden2", enumerable: false })
|
|
|
|
expect(Hash.hash(obj1)).not.toBe(Hash.hash(obj2))
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("plain arrays", () => {
|
|
it("should return true for structurally identical arrays (structural equality)", () => {
|
|
const arr1 = [1, 2, 3]
|
|
const arr2 = [1, 2, 3]
|
|
expect(Equal.equals(arr1, arr2)).toBe(true)
|
|
})
|
|
|
|
it("should return true for same reference", () => {
|
|
const arr = [1, 2, 3]
|
|
expect(Equal.equals(arr, arr)).toBe(true)
|
|
})
|
|
|
|
it("should return true for empty arrays", () => {
|
|
expect(Equal.equals([], [])).toBe(true)
|
|
})
|
|
|
|
it("should return false for arrays with different elements", () => {
|
|
const arr1 = [1, 2, 3]
|
|
const arr2 = [1, 2, 4]
|
|
expect(Equal.equals(arr1, arr2)).toBe(false)
|
|
})
|
|
|
|
it("should return false for arrays with different lengths", () => {
|
|
const arr1 = [1, 2, 3]
|
|
const arr2 = [1, 2]
|
|
expect(Equal.equals(arr1, arr2)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("primitives", () => {
|
|
it("should work correctly for numbers", () => {
|
|
expect(Equal.equals(42, 42)).toBe(true)
|
|
expect(Equal.equals(42, 43)).toBe(false)
|
|
})
|
|
|
|
it("should work correctly for strings", () => {
|
|
expect(Equal.equals("hello", "hello")).toBe(true)
|
|
expect(Equal.equals("hello", "world")).toBe(false)
|
|
})
|
|
|
|
it("should work correctly for booleans", () => {
|
|
expect(Equal.equals(true, true)).toBe(true)
|
|
expect(Equal.equals(false, false)).toBe(true)
|
|
expect(Equal.equals(true, false)).toBe(false)
|
|
})
|
|
|
|
it("should work correctly for null and undefined", () => {
|
|
expect(Equal.equals(null, null)).toBe(true)
|
|
expect(Equal.equals(undefined, undefined)).toBe(true)
|
|
expect(Equal.equals(null, undefined)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("Date objects", () => {
|
|
it("should compare dates by ISO string", () => {
|
|
const date1 = new Date("2023-01-01T00:00:00.000Z")
|
|
const date2 = new Date("2023-01-01T00:00:00.000Z")
|
|
expect(Equal.equals(date1, date2)).toBe(true)
|
|
})
|
|
|
|
it("should return false for different dates", () => {
|
|
const date1 = new Date("2023-01-01T00:00:00.000Z")
|
|
const date2 = new Date("2023-01-02T00:00:00.000Z")
|
|
expect(Equal.equals(date1, date2)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("Effect data structures", () => {
|
|
it("should work with HashMap (implements Equal interface)", () => {
|
|
const map1 = HashMap.make(["a", 1], ["b", 2])
|
|
const map2 = HashMap.make(["a", 1], ["b", 2])
|
|
expect(Equal.equals(map1, map2)).toBe(true)
|
|
})
|
|
|
|
it("should work with Option (implements Equal interface)", () => {
|
|
const opt1 = Option.some(42)
|
|
const opt2 = Option.some(42)
|
|
expect(Equal.equals(opt1, opt2)).toBe(true)
|
|
})
|
|
|
|
it("HashMap should not equal plain objects", () => {
|
|
const map = HashMap.make(["a", 1], ["b", 2])
|
|
const obj = { a: 1, b: 2 }
|
|
expect(Equal.equals(map, obj)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("custom Equal implementations", () => {
|
|
class CustomPoint implements Equal.Equal {
|
|
constructor(readonly x: number, readonly y: number) {}
|
|
|
|
[Equal.symbol](that: Equal.Equal): boolean {
|
|
return that instanceof CustomPoint &&
|
|
this.x === that.x &&
|
|
this.y === that.y
|
|
}
|
|
|
|
[Hash.symbol](): number {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
it("should work with custom Equal implementation", () => {
|
|
const point1 = new CustomPoint(1, 2)
|
|
const point2 = new CustomPoint(1, 2)
|
|
expect(Equal.equals(point1, point2)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe("nested structures", () => {
|
|
it("should return true for nested objects (deep structural equality)", () => {
|
|
const obj1 = { a: { b: 1 }, c: [1, 2] }
|
|
const obj2 = { a: { b: 1 }, c: [1, 2] }
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
})
|
|
|
|
it("should return true for nested arrays (deep structural equality)", () => {
|
|
const arr1 = [[1, 2], [3, 4]]
|
|
const arr2 = [[1, 2], [3, 4]]
|
|
expect(Equal.equals(arr1, arr2)).toBe(true)
|
|
})
|
|
|
|
it("should return false for nested objects with different values", () => {
|
|
const obj1 = { a: { b: 1 }, c: [1, 2] }
|
|
const obj2 = { a: { b: 2 }, c: [1, 2] }
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
|
|
it("should return false for nested arrays with different values", () => {
|
|
const arr1 = [[1, 2], [3, 4]]
|
|
const arr2 = [[1, 3], [3, 4]]
|
|
expect(Equal.equals(arr1, arr2)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("special values", () => {
|
|
describe("NaN", () => {
|
|
it("should handle NaN correctly (structural equality - NaN equals NaN)", () => {
|
|
expect(Equal.equals(NaN, NaN)).toBe(true)
|
|
})
|
|
|
|
it("should return false when comparing NaN with regular numbers", () => {
|
|
expect(Equal.equals(NaN, 0)).toBe(false)
|
|
expect(Equal.equals(NaN, 42)).toBe(false)
|
|
expect(Equal.equals(42, NaN)).toBe(false)
|
|
})
|
|
|
|
it("should return false when comparing NaN with Infinity", () => {
|
|
expect(Equal.equals(NaN, Infinity)).toBe(false)
|
|
expect(Equal.equals(NaN, -Infinity)).toBe(false)
|
|
expect(Equal.equals(Infinity, NaN)).toBe(false)
|
|
expect(Equal.equals(-Infinity, NaN)).toBe(false)
|
|
})
|
|
|
|
it("should handle NaN in arrays", () => {
|
|
expect(Equal.equals([NaN], [NaN])).toBe(true)
|
|
expect(Equal.equals([NaN, 1], [NaN, 1])).toBe(true)
|
|
expect(Equal.equals([NaN, 1], [NaN, 2])).toBe(false)
|
|
expect(Equal.equals([NaN], [Infinity])).toBe(false)
|
|
})
|
|
|
|
it("should handle NaN in objects", () => {
|
|
expect(Equal.equals({ a: NaN }, { a: NaN })).toBe(true)
|
|
expect(Equal.equals({ a: NaN, b: 1 }, { a: NaN, b: 1 })).toBe(true)
|
|
expect(Equal.equals({ a: NaN }, { a: Infinity })).toBe(false)
|
|
expect(Equal.equals({ a: NaN }, { a: 42 })).toBe(false)
|
|
})
|
|
|
|
it("should handle NaN in nested structures", () => {
|
|
expect(Equal.equals({ arr: [NaN] }, { arr: [NaN] })).toBe(true)
|
|
expect(Equal.equals([{ val: NaN }], [{ val: NaN }])).toBe(true)
|
|
expect(Equal.equals({ nested: { deep: NaN } }, { nested: { deep: NaN } })).toBe(true)
|
|
expect(Equal.equals({ arr: [NaN] }, { arr: [Infinity] })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("Infinity", () => {
|
|
it("should handle Infinity correctly (structural equality)", () => {
|
|
expect(Equal.equals(Infinity, Infinity)).toBe(true)
|
|
})
|
|
|
|
it("should return false when comparing Infinity with regular numbers", () => {
|
|
expect(Equal.equals(Infinity, 0)).toBe(false)
|
|
expect(Equal.equals(Infinity, 42)).toBe(false)
|
|
expect(Equal.equals(42, Infinity)).toBe(false)
|
|
expect(Equal.equals(Infinity, Number.MAX_VALUE)).toBe(false)
|
|
})
|
|
|
|
it("should return false when comparing Infinity with -Infinity", () => {
|
|
expect(Equal.equals(Infinity, -Infinity)).toBe(false)
|
|
expect(Equal.equals(-Infinity, Infinity)).toBe(false)
|
|
})
|
|
|
|
it("should handle Infinity in arrays", () => {
|
|
expect(Equal.equals([Infinity], [Infinity])).toBe(true)
|
|
expect(Equal.equals([Infinity, 1], [Infinity, 1])).toBe(true)
|
|
expect(Equal.equals([Infinity, 1], [Infinity, 2])).toBe(false)
|
|
expect(Equal.equals([Infinity], [-Infinity])).toBe(false)
|
|
})
|
|
|
|
it("should handle Infinity in objects", () => {
|
|
expect(Equal.equals({ a: Infinity }, { a: Infinity })).toBe(true)
|
|
expect(Equal.equals({ a: Infinity, b: 1 }, { a: Infinity, b: 1 })).toBe(true)
|
|
expect(Equal.equals({ a: Infinity }, { a: -Infinity })).toBe(false)
|
|
expect(Equal.equals({ a: Infinity }, { a: 42 })).toBe(false)
|
|
})
|
|
|
|
it("should handle Infinity in nested structures", () => {
|
|
expect(Equal.equals({ arr: [Infinity] }, { arr: [Infinity] })).toBe(true)
|
|
expect(Equal.equals([{ val: Infinity }], [{ val: Infinity }])).toBe(true)
|
|
expect(Equal.equals({ nested: { deep: Infinity } }, { nested: { deep: Infinity } })).toBe(true)
|
|
expect(Equal.equals({ arr: [Infinity] }, { arr: [-Infinity] })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("-Infinity", () => {
|
|
it("should handle -Infinity correctly (structural equality)", () => {
|
|
expect(Equal.equals(-Infinity, -Infinity)).toBe(true)
|
|
})
|
|
|
|
it("should return false when comparing -Infinity with regular numbers", () => {
|
|
expect(Equal.equals(-Infinity, 0)).toBe(false)
|
|
expect(Equal.equals(-Infinity, -42)).toBe(false)
|
|
expect(Equal.equals(-42, -Infinity)).toBe(false)
|
|
expect(Equal.equals(-Infinity, Number.MIN_VALUE)).toBe(false)
|
|
})
|
|
|
|
it("should handle -Infinity in arrays", () => {
|
|
expect(Equal.equals([-Infinity], [-Infinity])).toBe(true)
|
|
expect(Equal.equals([-Infinity, 1], [-Infinity, 1])).toBe(true)
|
|
expect(Equal.equals([-Infinity, 1], [-Infinity, 2])).toBe(false)
|
|
expect(Equal.equals([-Infinity], [Infinity])).toBe(false)
|
|
})
|
|
|
|
it("should handle -Infinity in objects", () => {
|
|
expect(Equal.equals({ a: -Infinity }, { a: -Infinity })).toBe(true)
|
|
expect(Equal.equals({ a: -Infinity, b: 1 }, { a: -Infinity, b: 1 })).toBe(true)
|
|
expect(Equal.equals({ a: -Infinity }, { a: Infinity })).toBe(false)
|
|
expect(Equal.equals({ a: -Infinity }, { a: -42 })).toBe(false)
|
|
})
|
|
|
|
it("should handle -Infinity in nested structures", () => {
|
|
expect(Equal.equals({ arr: [-Infinity] }, { arr: [-Infinity] })).toBe(true)
|
|
expect(Equal.equals([{ val: -Infinity }], [{ val: -Infinity }])).toBe(true)
|
|
expect(Equal.equals({ nested: { deep: -Infinity } }, { nested: { deep: -Infinity } })).toBe(true)
|
|
expect(Equal.equals({ arr: [-Infinity] }, { arr: [Infinity] })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("mixed special values", () => {
|
|
it("should handle arrays with multiple special values", () => {
|
|
expect(Equal.equals([NaN, Infinity, -Infinity], [NaN, Infinity, -Infinity])).toBe(true)
|
|
expect(Equal.equals([NaN, Infinity], [Infinity, NaN])).toBe(false) // order matters
|
|
expect(Equal.equals([NaN, Infinity, 0], [NaN, Infinity, 0])).toBe(true)
|
|
})
|
|
|
|
it("should handle objects with multiple special values", () => {
|
|
expect(Equal.equals(
|
|
{ nan: NaN, inf: Infinity, negInf: -Infinity },
|
|
{ nan: NaN, inf: Infinity, negInf: -Infinity }
|
|
)).toBe(true)
|
|
expect(Equal.equals(
|
|
{ nan: NaN, inf: Infinity },
|
|
{ nan: NaN, inf: -Infinity }
|
|
)).toBe(false)
|
|
})
|
|
|
|
it("should handle complex nested structures with special values", () => {
|
|
const obj1 = {
|
|
data: [
|
|
{ value: NaN, meta: { type: "special" } },
|
|
{ value: Infinity, meta: { type: "infinite" } },
|
|
{ value: -Infinity, meta: { type: "negative-infinite" } }
|
|
]
|
|
}
|
|
const obj2 = {
|
|
data: [
|
|
{ value: NaN, meta: { type: "special" } },
|
|
{ value: Infinity, meta: { type: "infinite" } },
|
|
{ value: -Infinity, meta: { type: "negative-infinite" } }
|
|
]
|
|
}
|
|
const obj3 = {
|
|
data: [
|
|
{ value: NaN, meta: { type: "special" } },
|
|
{ value: -Infinity, meta: { type: "infinite" } }, // swapped values
|
|
{ value: Infinity, meta: { type: "negative-infinite" } }
|
|
]
|
|
}
|
|
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
expect(Equal.equals(obj1, obj3)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("zero values", () => {
|
|
it("should handle -0 and +0 correctly", () => {
|
|
expect(Equal.equals(-0, +0)).toBe(true)
|
|
expect(Equal.equals(0, -0)).toBe(true)
|
|
expect(Equal.equals(+0, -0)).toBe(true)
|
|
})
|
|
|
|
it("should handle zero values in arrays", () => {
|
|
expect(Equal.equals([-0], [+0])).toBe(true)
|
|
expect(Equal.equals([0], [-0])).toBe(true)
|
|
expect(Equal.equals([-0, 1], [+0, 1])).toBe(true)
|
|
})
|
|
|
|
it("should handle zero values in objects", () => {
|
|
expect(Equal.equals({ a: -0 }, { a: +0 })).toBe(true)
|
|
expect(Equal.equals({ a: 0 }, { a: -0 })).toBe(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("JavaScript Map", () => {
|
|
it("should return true for structurally identical maps", () => {
|
|
const map1 = new Map([["a", 1], ["b", 2]])
|
|
const map2 = new Map([["a", 1], ["b", 2]])
|
|
const map3 = new Map([["b", 2], ["a", 1]])
|
|
expect(Equal.equals(map1, map2)).toBe(true)
|
|
expect(Equal.equals(map1, map3)).toBe(true)
|
|
})
|
|
|
|
it("should return true for same reference", () => {
|
|
const map = new Map([["a", 1]])
|
|
expect(Equal.equals(map, map)).toBe(true)
|
|
})
|
|
|
|
it("should return true for empty maps", () => {
|
|
expect(Equal.equals(new Map(), new Map())).toBe(true)
|
|
})
|
|
|
|
it("should return false for maps with different values", () => {
|
|
const map1 = new Map([["a", 1], ["b", 2]])
|
|
const map2 = new Map([["a", 1], ["b", 3]])
|
|
expect(Equal.equals(map1, map2)).toBe(false)
|
|
})
|
|
|
|
it("should return false for maps with different keys", () => {
|
|
const map1 = new Map([["a", 1], ["b", 2]])
|
|
const map2 = new Map([["a", 1], ["c", 2]])
|
|
expect(Equal.equals(map1, map2)).toBe(false)
|
|
})
|
|
|
|
it("should return false for maps with different sizes", () => {
|
|
const map1 = new Map([["a", 1], ["b", 2]])
|
|
const map2 = new Map([["a", 1]])
|
|
expect(Equal.equals(map1, map2)).toBe(false)
|
|
})
|
|
|
|
it("should handle maps with object keys", () => {
|
|
const key1 = { id: 1 }
|
|
const key2 = { id: 1 }
|
|
const map1 = new Map()
|
|
map1.set(key1, "value1")
|
|
map1.set({ nested: { x: 1 } }, "value2")
|
|
|
|
const map2 = new Map()
|
|
map2.set(key2, "value1")
|
|
map2.set({ nested: { x: 1 } }, "value2")
|
|
|
|
expect(Equal.equals(map1, map2)).toBe(true)
|
|
})
|
|
|
|
it("should handle maps with object values", () => {
|
|
const map1 = new Map([["a", { x: 1 }], ["b", { y: [1, 2] }]])
|
|
const map2 = new Map([["a", { x: 1 }], ["b", { y: [1, 2] }]])
|
|
expect(Equal.equals(map1, map2)).toBe(true)
|
|
})
|
|
|
|
it("should handle nested maps", () => {
|
|
const inner1 = new Map([["x", 1]])
|
|
const inner2 = new Map([["x", 1]])
|
|
const map1 = new Map([["nested", inner1]])
|
|
const map2 = new Map([["nested", inner2]])
|
|
expect(Equal.equals(map1, map2)).toBe(true)
|
|
})
|
|
|
|
it("should handle maps with special values", () => {
|
|
const map1 = new Map([[NaN, "nan"], [Infinity, "inf"], [-Infinity, "neginf"]])
|
|
const map2 = new Map([[NaN, "nan"], [Infinity, "inf"], [-Infinity, "neginf"]])
|
|
expect(Equal.equals(map1, map2)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe("JavaScript Set", () => {
|
|
it("should return true for structurally identical sets", () => {
|
|
const set1 = new Set([1, 2, 3])
|
|
const set2 = new Set([1, 2, 3])
|
|
expect(Equal.equals(set1, set2)).toBe(true)
|
|
})
|
|
|
|
it("should return true for same reference", () => {
|
|
const set = new Set([1, 2, 3])
|
|
expect(Equal.equals(set, set)).toBe(true)
|
|
})
|
|
|
|
it("should return true for empty sets", () => {
|
|
expect(Equal.equals(new Set(), new Set())).toBe(true)
|
|
})
|
|
|
|
it("should return false for sets with different elements", () => {
|
|
const set1 = new Set([1, 2, 3])
|
|
const set2 = new Set([1, 2, 4])
|
|
expect(Equal.equals(set1, set2)).toBe(false)
|
|
})
|
|
|
|
it("should return false for sets with different sizes", () => {
|
|
const set1 = new Set([1, 2, 3])
|
|
const set2 = new Set([1, 2])
|
|
expect(Equal.equals(set1, set2)).toBe(false)
|
|
})
|
|
|
|
it("should return true for sets with same elements in different insertion order", () => {
|
|
const set1 = new Set([1, 2, 3])
|
|
const set2 = new Set([3, 1, 2])
|
|
expect(Equal.equals(set1, set2)).toBe(true)
|
|
})
|
|
|
|
it("should handle sets with object elements", () => {
|
|
const set1 = new Set([{ x: 1 }, { y: 2 }])
|
|
const set2 = new Set([{ x: 1 }, { y: 2 }])
|
|
expect(Equal.equals(set1, set2)).toBe(true)
|
|
})
|
|
|
|
it("should handle nested sets", () => {
|
|
const inner1 = new Set([1, 2])
|
|
const inner2 = new Set([1, 2])
|
|
const set1 = new Set([inner1, "other"])
|
|
const set2 = new Set([inner2, "other"])
|
|
expect(Equal.equals(set1, set2)).toBe(true)
|
|
})
|
|
|
|
it("should handle sets with special values", () => {
|
|
const set1 = new Set([NaN, Infinity, -Infinity])
|
|
const set2 = new Set([NaN, Infinity, -Infinity])
|
|
expect(Equal.equals(set1, set2)).toBe(true)
|
|
})
|
|
|
|
it("should handle sets with array elements", () => {
|
|
const set1 = new Set([[1, 2], [3, 4]])
|
|
const set2 = new Set([[1, 2], [3, 4]])
|
|
expect(Equal.equals(set1, set2)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe("Map and Set mixed", () => {
|
|
it("should handle objects containing maps and sets", () => {
|
|
const obj1 = {
|
|
map: new Map([["a", 1], ["b", 2]]),
|
|
set: new Set([1, 2, 3]),
|
|
array: [new Map([["x", 1]]), new Set([4, 5])]
|
|
}
|
|
const obj2 = {
|
|
map: new Map([["a", 1], ["b", 2]]),
|
|
set: new Set([1, 2, 3]),
|
|
array: [new Map([["x", 1]]), new Set([4, 5])]
|
|
}
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
})
|
|
|
|
it("should return false when Map/Set differ in nested structures", () => {
|
|
const obj1 = {
|
|
map: new Map([["a", 1], ["b", 2]]),
|
|
set: new Set([1, 2, 3])
|
|
}
|
|
const obj2 = {
|
|
map: new Map([["a", 1], ["b", 3]]), // different value
|
|
set: new Set([1, 2, 3])
|
|
}
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("byReference", () => {
|
|
it("should allow objects to opt out of structural equality", () => {
|
|
const obj1 = { a: 1, b: 2 }
|
|
const obj2 = { a: 1, b: 2 }
|
|
|
|
// Normal structural equality
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
|
|
// Create instance equality version of obj1
|
|
const obj1Instance = Equal.byReference(obj1)
|
|
expect(Equal.equals(obj1Instance, obj2)).toBe(false)
|
|
expect(Equal.equals(obj1Instance, obj1Instance)).toBe(true)
|
|
})
|
|
|
|
it("should work with arrays", () => {
|
|
const arr1 = [1, 2, 3]
|
|
const arr2 = [1, 2, 3]
|
|
|
|
// Normal structural equality
|
|
expect(Equal.equals(arr1, arr2)).toBe(true)
|
|
|
|
// Create instance equality version of arr1
|
|
const arr1Instance = Equal.byReference(arr1)
|
|
expect(Equal.equals(arr1Instance, arr2)).toBe(false)
|
|
expect(Equal.equals(arr1Instance, arr1Instance)).toBe(true)
|
|
})
|
|
|
|
it("should work with Maps", () => {
|
|
const map1 = new Map([["a", 1]])
|
|
const map2 = new Map([["a", 1]])
|
|
|
|
// Normal structural equality
|
|
expect(Equal.equals(map1, map2)).toBe(true)
|
|
|
|
// Create instance equality version of map1
|
|
const map1Instance = Equal.byReference(map1)
|
|
expect(Equal.equals(map1Instance, map2)).toBe(false)
|
|
expect(Equal.equals(map1Instance, map1Instance)).toBe(true)
|
|
})
|
|
|
|
it("should work with Sets", () => {
|
|
const set1 = new Set([1, 2])
|
|
const set2 = new Set([1, 2])
|
|
|
|
// Normal structural equality
|
|
expect(Equal.equals(set1, set2)).toBe(true)
|
|
|
|
// Create instance equality version of set1
|
|
const set1Instance = Equal.byReference(set1)
|
|
expect(Equal.equals(set1Instance, set2)).toBe(false)
|
|
expect(Equal.equals(set1Instance, set1Instance)).toBe(true)
|
|
})
|
|
|
|
it("should return a proxy that behaves like the original object", () => {
|
|
const obj = { a: 1 }
|
|
const result = Equal.byReference(obj)
|
|
|
|
// The result should not be the same reference (it's a proxy)
|
|
expect(result).not.toBe(obj)
|
|
|
|
// But it should behave the same way
|
|
expect(result.a).toBe(1)
|
|
|
|
// And should support property access
|
|
expect(result.a).toBe(obj.a)
|
|
})
|
|
|
|
it("should work when either object is marked for instance equality", () => {
|
|
const obj1 = { a: 1, b: 2 }
|
|
const obj2 = { a: 1, b: 2 }
|
|
|
|
// Create instance equality version of obj2
|
|
const obj2Instance = Equal.byReference(obj2)
|
|
expect(Equal.equals(obj1, obj2Instance)).toBe(false)
|
|
expect(Equal.equals(obj2Instance, obj1)).toBe(false)
|
|
})
|
|
|
|
it("should work with nested structures", () => {
|
|
const obj1 = { a: { b: 1 }, c: [1, 2] }
|
|
const obj2 = { a: { b: 1 }, c: [1, 2] }
|
|
|
|
// Normal structural equality
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
|
|
// Create instance equality version of obj1
|
|
const obj1Instance = Equal.byReference(obj1)
|
|
expect(Equal.equals(obj1Instance, obj2)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("byReferenceUnsafe", () => {
|
|
it("should allow objects to opt out of structural equality without proxy", () => {
|
|
const obj1 = { a: 1, b: 2 }
|
|
const obj2 = { a: 1, b: 2 }
|
|
|
|
// Mark obj1 for reference equality (modifies obj1 directly)
|
|
const obj1ByRef = Equal.byReferenceUnsafe(obj1)
|
|
expect(obj1ByRef).toBe(obj1) // Same object, no proxy created
|
|
expect(Equal.equals(obj1ByRef, obj2)).toBe(false) // uses reference equality
|
|
expect(Equal.equals(obj1ByRef, obj1ByRef)).toBe(true) // same reference
|
|
|
|
// The original obj1 now uses reference equality
|
|
expect(Equal.equals(obj1, obj2)).toBe(false) // obj1 uses reference equality
|
|
})
|
|
|
|
it("should work with arrays", () => {
|
|
const arr1 = [1, 2, 3]
|
|
const arr2 = [1, 2, 3]
|
|
|
|
// Mark arr1 for reference equality
|
|
const arr1ByRef = Equal.byReferenceUnsafe(arr1)
|
|
expect(arr1ByRef).toBe(arr1) // Same array, no proxy
|
|
expect(Equal.equals(arr1ByRef, arr2)).toBe(false)
|
|
expect(Equal.equals(arr1ByRef, arr1ByRef)).toBe(true)
|
|
})
|
|
|
|
it("should work with Maps", () => {
|
|
const map1 = new Map([["a", 1]])
|
|
const map2 = new Map([["a", 1]])
|
|
|
|
// Mark map1 for reference equality
|
|
const map1ByRef = Equal.byReferenceUnsafe(map1)
|
|
expect(map1ByRef).toBe(map1) // Same map, no proxy
|
|
expect(Equal.equals(map1ByRef, map2)).toBe(false)
|
|
expect(Equal.equals(map1ByRef, map1ByRef)).toBe(true)
|
|
})
|
|
|
|
it("should work with Sets", () => {
|
|
const set1 = new Set([1, 2])
|
|
const set2 = new Set([1, 2])
|
|
|
|
// Mark set1 for reference equality
|
|
const set1ByRef = Equal.byReferenceUnsafe(set1)
|
|
expect(set1ByRef).toBe(set1) // Same set, no proxy
|
|
expect(Equal.equals(set1ByRef, set2)).toBe(false)
|
|
expect(Equal.equals(set1ByRef, set1ByRef)).toBe(true)
|
|
})
|
|
|
|
it("should permanently change object equality behavior", () => {
|
|
const obj1 = { a: 1, b: 2 }
|
|
const obj2 = { a: 1, b: 2 }
|
|
|
|
// Mark obj1 for reference equality
|
|
Equal.byReferenceUnsafe(obj1)
|
|
|
|
// Now obj1 permanently uses reference equality
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
expect(Equal.equals(obj1, obj1)).toBe(true)
|
|
})
|
|
|
|
it("should work when either object is marked for reference equality", () => {
|
|
const obj1 = { a: 1, b: 2 }
|
|
const obj2 = { a: 1, b: 2 }
|
|
|
|
// Mark obj2 for reference equality
|
|
Equal.byReferenceUnsafe(obj2)
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
expect(Equal.equals(obj2, obj1)).toBe(false)
|
|
})
|
|
|
|
it("should work with nested structures", () => {
|
|
const obj1 = { a: { b: 1 }, c: [1, 2] }
|
|
const obj2 = { a: { b: 1 }, c: [1, 2] }
|
|
|
|
// Mark obj1 for reference equality
|
|
Equal.byReferenceUnsafe(obj1)
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("Error objects", () => {
|
|
it("should return true for Error objects with same message and additional properties", () => {
|
|
const error1 = new Error("test message")
|
|
;(error1 as any).code = "TEST_CODE"
|
|
;(error1 as any).details = { info: "additional details" }
|
|
|
|
const error2 = new Error("test message")
|
|
;(error2 as any).code = "TEST_CODE"
|
|
;(error2 as any).details = { info: "additional details" }
|
|
|
|
// Error objects are now structurally comparable (stack is excluded)
|
|
expect(Equal.equals(error1, error2)).toBe(true)
|
|
})
|
|
|
|
it("should return false for Error objects with different messages", () => {
|
|
const error1 = new Error("message 1")
|
|
;(error1 as any).code = "TEST_CODE"
|
|
|
|
const error2 = new Error("message 2")
|
|
;(error2 as any).code = "TEST_CODE"
|
|
|
|
expect(Equal.equals(error1, error2)).toBe(false)
|
|
})
|
|
|
|
it("should work with custom Error classes", () => {
|
|
class CustomError extends Error {
|
|
readonly code: string
|
|
constructor(message: string, code: string) {
|
|
super(message)
|
|
this.name = "CustomError"
|
|
this.code = code
|
|
}
|
|
}
|
|
|
|
const error1 = new CustomError("test", "CUSTOM")
|
|
const error2 = new CustomError("test", "CUSTOM")
|
|
|
|
// Custom Error objects are now structurally comparable (stack is excluded)
|
|
expect(Equal.equals(error1, error2)).toBe(true)
|
|
})
|
|
|
|
it("should ignore stack traces when comparing Error objects", () => {
|
|
function createErrorAtDifferentLocation1() {
|
|
return new Error("same message")
|
|
}
|
|
|
|
function createErrorAtDifferentLocation2() {
|
|
return new Error("same message")
|
|
}
|
|
|
|
const error1 = createErrorAtDifferentLocation1()
|
|
const error2 = createErrorAtDifferentLocation2()
|
|
|
|
// Even though stack traces are different, errors should be equal
|
|
expect(error1.stack).not.toBe(error2.stack)
|
|
expect(Equal.equals(error1, error2)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe("objects with different constructors", () => {
|
|
it("should return true for objects with different constructors but same shape", () => {
|
|
class CustomClass {
|
|
readonly a: number
|
|
readonly b: number
|
|
constructor() {
|
|
this.a = 1
|
|
this.b = 2
|
|
}
|
|
}
|
|
|
|
const plainObject = { a: 1, b: 2 }
|
|
const customObject = new CustomClass()
|
|
expect(Equal.equals(plainObject, customObject)).toBe(true)
|
|
})
|
|
|
|
it("should return true for objects with null prototype", () => {
|
|
const nullProtoObject: any = Object.create(null)
|
|
nullProtoObject.a = 1
|
|
nullProtoObject.b = 2
|
|
|
|
const plainObject = { a: 1, b: 2 }
|
|
expect(Equal.equals(nullProtoObject, plainObject)).toBe(true)
|
|
})
|
|
|
|
it("should return true for objects with different function constructors", () => {
|
|
function Constructor1(this: any) {
|
|
this.a = 1
|
|
this.b = 2
|
|
}
|
|
|
|
function Constructor2(this: any) {
|
|
this.a = 1
|
|
this.b = 2
|
|
}
|
|
|
|
const obj1 = new (Constructor1 as any)()
|
|
const obj2 = new (Constructor2 as any)()
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
})
|
|
|
|
it("should return false when properties differ regardless of constructor", () => {
|
|
class CustomClass1 {
|
|
readonly a: number
|
|
readonly b: number
|
|
constructor() {
|
|
this.a = 1
|
|
this.b = 2
|
|
}
|
|
}
|
|
|
|
class CustomClass2 {
|
|
readonly a: number
|
|
readonly b: number
|
|
constructor() {
|
|
this.a = 1
|
|
this.b = 3 // Different value
|
|
}
|
|
}
|
|
|
|
const obj1 = new CustomClass1()
|
|
const obj2 = new CustomClass2()
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
|
|
it("should handle mixed constructor types in nested structures", () => {
|
|
class CustomClass {
|
|
readonly inner: { x: number }
|
|
constructor() {
|
|
this.inner = { x: 1 }
|
|
}
|
|
}
|
|
|
|
const obj1 = { data: new CustomClass() }
|
|
const obj2 = { data: { inner: { x: 1 } } }
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
})
|
|
|
|
it("should include constructor property when it has a meaningful user-defined value", () => {
|
|
const obj1 = { constructor: 10, a: 1 }
|
|
const obj2 = { constructor: 10, a: 1 }
|
|
const obj3 = { constructor: 20, a: 1 }
|
|
|
|
// Objects with same constructor value should be equal
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
|
|
// Objects with different constructor values should not be equal
|
|
expect(Equal.equals(obj1, obj3)).toBe(false)
|
|
expect(Equal.equals(obj2, obj3)).toBe(false)
|
|
})
|
|
|
|
it("should include constructor property when it has a meaningful user-defined value from a prototype", () => {
|
|
const obj1 = Object.setPrototypeOf({ a: 1 }, { constructor: 10 })
|
|
const obj2 = Object.setPrototypeOf({ a: 1 }, { constructor: 10 })
|
|
const obj3 = Object.setPrototypeOf({ a: 1 }, { constructor: 20 })
|
|
|
|
// Objects with same constructor value should be equal
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
|
|
// Objects with different constructor values should not be equal
|
|
expect(Equal.equals(obj1, obj3)).toBe(false)
|
|
expect(Equal.equals(obj2, obj3)).toBe(false)
|
|
})
|
|
|
|
it("should include constructor property when it has a meaningful user-defined value from a prototype when it is a function", () => {
|
|
const f = () => 10
|
|
const g = () => 10
|
|
const obj1 = Object.setPrototypeOf({ a: 1 }, { constructor: f })
|
|
const obj2 = Object.setPrototypeOf({ a: 1 }, { constructor: f })
|
|
const obj3 = Object.setPrototypeOf({ a: 1 }, { constructor: g })
|
|
|
|
// Objects with same constructor value should be equal
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
|
|
// Objects with different constructor values should not be equal
|
|
expect(Equal.equals(obj1, obj3)).toBe(false)
|
|
expect(Equal.equals(obj2, obj3)).toBe(false)
|
|
})
|
|
|
|
it("should still ignore default constructor property", () => {
|
|
const obj1 = { a: 1, b: 2 }
|
|
const obj2 = { a: 1, b: 2 }
|
|
|
|
// Normal objects should still be equal (constructor property ignored when it's the default)
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
|
|
// Verify that obj1.constructor and obj2.constructor are the default Object constructor
|
|
expect(obj1.constructor).toBe(Object)
|
|
expect(obj2.constructor).toBe(Object)
|
|
})
|
|
})
|
|
|
|
describe("recursive objects", () => {
|
|
it("should handle circular references in Equal implementations without infinite recursion", () => {
|
|
class CircularEqualTest implements Equal.Equal {
|
|
constructor(readonly value: string, public child: CircularEqualTest | null = null) {}
|
|
|
|
[Equal.symbol](that: Equal.Equal): boolean {
|
|
if (!(that instanceof CircularEqualTest)) {
|
|
return false
|
|
}
|
|
|
|
// Circular references are handled automatically by the system
|
|
// This implementation can safely call Equal.equals without manual tracking
|
|
return this.value === that.value && Equal.equals(this.child, that.child)
|
|
}
|
|
|
|
[Hash.symbol](): number {
|
|
return Hash.string(this.value)
|
|
}
|
|
}
|
|
|
|
// Test case 1: Self-referencing objects
|
|
const obj1 = new CircularEqualTest("test")
|
|
obj1.child = obj1
|
|
|
|
const obj2 = new CircularEqualTest("test")
|
|
obj2.child = obj2
|
|
|
|
// This should not cause infinite recursion
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
|
|
// Test case 2: Different values should not be equal even with same structure
|
|
const obj3 = new CircularEqualTest("different")
|
|
obj3.child = obj3
|
|
|
|
expect(Equal.equals(obj1, obj3)).toBe(false)
|
|
})
|
|
|
|
it("should handle complex circular references in Equal implementations", () => {
|
|
class ComplexEqualTest implements Equal.Equal {
|
|
constructor(readonly id: number, public ref: ComplexEqualTest | null = null) {}
|
|
|
|
[Equal.symbol](that: Equal.Equal): boolean {
|
|
if (!(that instanceof ComplexEqualTest)) {
|
|
return false
|
|
}
|
|
|
|
// Circular references are handled automatically by the system
|
|
return this.id === that.id && Equal.equals(this.ref, that.ref)
|
|
}
|
|
|
|
[Hash.symbol](): number {
|
|
return Hash.number(this.id)
|
|
}
|
|
}
|
|
|
|
// Create cycle: obj1 -> obj2 -> obj1
|
|
const obj1 = new ComplexEqualTest(1)
|
|
const obj2 = new ComplexEqualTest(2)
|
|
obj1.ref = obj2
|
|
obj2.ref = obj1
|
|
|
|
// Create identical cycle: obj3 -> obj4 -> obj3
|
|
const obj3 = new ComplexEqualTest(1)
|
|
const obj4 = new ComplexEqualTest(2)
|
|
obj3.ref = obj4
|
|
obj4.ref = obj3
|
|
|
|
// Should not cause infinite recursion and should be equal
|
|
expect(Equal.equals(obj1, obj3)).toBe(true)
|
|
expect(Equal.equals(obj2, obj4)).toBe(true)
|
|
})
|
|
|
|
it("should expose visited sets for advanced use cases", () => {
|
|
// Most implementations don't need manual tracking, but the sets are exposed
|
|
// for advanced scenarios where integration with the tracking system is needed
|
|
|
|
class SimpleContainer implements Equal.Equal {
|
|
constructor(readonly name: string, readonly items: Array<SimpleContainer> = []) {}
|
|
|
|
[Equal.symbol](that: Equal.Equal): boolean {
|
|
if (!(that instanceof SimpleContainer)) {
|
|
return false
|
|
}
|
|
|
|
// Simple implementation - no manual tracking needed
|
|
// Circular references are handled automatically
|
|
return this.name === that.name &&
|
|
this.items.length === that.items.length &&
|
|
this.items.every((item, i) => Equal.equals(item, that.items[i]))
|
|
}
|
|
|
|
[Hash.symbol](): number {
|
|
// Simple hash implementation - circular references are handled automatically
|
|
let result = Hash.string(this.name)
|
|
for (const item of this.items) {
|
|
result = Hash.combine(result, Hash.hash(item))
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
// Test that automatic circular reference handling works
|
|
const container1 = new SimpleContainer("A")
|
|
container1.items.push(container1) // Self-reference
|
|
|
|
const container2 = new SimpleContainer("A")
|
|
container2.items.push(container2) // Self-reference
|
|
|
|
// Should not cause infinite recursion due to automatic tracking
|
|
expect(Equal.equals(container1, container2)).toBe(true)
|
|
})
|
|
|
|
it("should handle objects with circular references without infinite recursion", () => {
|
|
// Create objects that reference themselves
|
|
const obj1: any = { a: 1, b: 2 }
|
|
obj1.self = obj1
|
|
|
|
const obj2: any = { a: 1, b: 2 }
|
|
obj2.self = obj2
|
|
|
|
// This should not throw due to stack overflow
|
|
expect(() => Equal.equals(obj1, obj2)).not.toThrow()
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
})
|
|
|
|
it("should handle arrays with circular references without infinite recursion", () => {
|
|
// Create arrays that reference themselves
|
|
const arr1: any = [1, 2, 3]
|
|
arr1.push(arr1)
|
|
|
|
const arr2: any = [1, 2, 3]
|
|
arr2.push(arr2)
|
|
|
|
// This should not throw due to stack overflow
|
|
expect(() => Equal.equals(arr1, arr2)).not.toThrow()
|
|
expect(Equal.equals(arr1, arr2)).toBe(true)
|
|
})
|
|
|
|
it("should handle Uint8Array", () => {
|
|
const arr1: Uint8Array = new Uint8Array([1, 2, 3])
|
|
const arr2: Uint8Array = new Uint8Array([1, 2, 3])
|
|
expect(Equal.equals(arr1, arr2)).toBe(true)
|
|
})
|
|
|
|
it("should handle mixed circular references between objects and arrays", () => {
|
|
// Create complex circular structure
|
|
const obj1: any = { type: "object", items: [] }
|
|
const arr1: any = [1, 2, obj1]
|
|
obj1.items = arr1
|
|
|
|
const obj2: any = { type: "object", items: [] }
|
|
const arr2: any = [1, 2, obj2]
|
|
obj2.items = arr2
|
|
|
|
// This should not throw due to stack overflow
|
|
expect(() => Equal.equals(obj1, obj2)).not.toThrow()
|
|
expect(Equal.equals(obj1, obj2)).toBe(true)
|
|
})
|
|
|
|
it("should handle hashing objects with circular references without infinite recursion", () => {
|
|
// Create object that references itself
|
|
const obj: any = { a: 1, b: 2 }
|
|
obj.self = obj
|
|
|
|
// This should not throw due to stack overflow
|
|
expect(() => Hash.hash(obj)).not.toThrow()
|
|
|
|
// Hash should be consistent
|
|
const hash1 = Hash.hash(obj)
|
|
const hash2 = Hash.hash(obj)
|
|
expect(hash1).toBe(hash2)
|
|
})
|
|
|
|
it("should handle hashing arrays with circular references without infinite recursion", () => {
|
|
// Create array that references itself
|
|
const arr: any = [1, 2, 3]
|
|
arr.push(arr)
|
|
|
|
// This should not throw due to stack overflow
|
|
expect(() => Hash.hash(arr)).not.toThrow()
|
|
|
|
// Hash should be consistent
|
|
const hash1 = Hash.hash(arr)
|
|
const hash2 = Hash.hash(arr)
|
|
expect(hash1).toBe(hash2)
|
|
})
|
|
|
|
it("should produce different hashes for objects with different circular structures", () => {
|
|
const obj1: any = { a: 1, b: 2 }
|
|
obj1.self = obj1
|
|
|
|
const obj2: any = { a: 1, b: 3 } // Different value
|
|
obj2.self = obj2
|
|
|
|
// Should not throw and should produce different hashes
|
|
expect(() => Hash.hash(obj1)).not.toThrow()
|
|
expect(() => Hash.hash(obj2)).not.toThrow()
|
|
expect(Hash.hash(obj1)).not.toBe(Hash.hash(obj2))
|
|
})
|
|
|
|
it("should return false for objects with different circular reference patterns", () => {
|
|
// Object that references itself
|
|
const obj1: any = { a: 1, b: 2 }
|
|
obj1.self = obj1
|
|
|
|
// Object that references a different object
|
|
const obj2: any = { a: 1, b: 2 }
|
|
const obj3: any = { a: 1, b: 2 }
|
|
obj2.self = obj3
|
|
|
|
// These should be considered different
|
|
expect(() => Equal.equals(obj1, obj2)).not.toThrow()
|
|
expect(Equal.equals(obj1, obj2)).toBe(false)
|
|
})
|
|
})
|
|
})
|