diff --git a/docs/reference/functions/compare.md b/docs/reference/functions/compare.md new file mode 100644 index 00000000..6d91cd43 --- /dev/null +++ b/docs/reference/functions/compare.md @@ -0,0 +1,41 @@ +--- +id: compare +title: compare +--- + +# Function: compare() + +```ts +function compare( + objA, + objB, + config): boolean; +``` + +Defined in: [compare.ts:1](https://github.com/TanStack/store/blob/main/packages/store/src/compare.ts#L1) + +## Type Parameters + +### T + +`T` + +## Parameters + +### objA + +`T` + +### objB + +`T` + +### config + +#### mode + +`"shallow"` \| `"deep"` + +## Returns + +`boolean` diff --git a/docs/reference/index.md b/docs/reference/index.md index afc8e5dd..baae1f07 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -35,6 +35,7 @@ title: "@tanstack/store" ## Functions - [batch](functions/batch.md) +- [compare](functions/compare.md) - [createAsyncAtom](functions/createAsyncAtom.md) - [createAtom](functions/createAtom.md) - [createStore](functions/createStore.md) diff --git a/packages/react-store/tests/index.test.tsx b/packages/react-store/tests/index.test.tsx index 60369d3f..e9bc94a7 100644 --- a/packages/react-store/tests/index.test.tsx +++ b/packages/react-store/tests/index.test.tsx @@ -4,8 +4,8 @@ import { describe, expect, it, test, vi } from 'vitest' import { createAtom, createStore } from '@tanstack/store' import { _useStore, + compare, createStoreContext, - shallow, useAtom, useCreateAtom, useCreateStore, @@ -686,66 +686,66 @@ describe('shallow', () => { test('should return true for shallowly equal objects', () => { const objA = { a: 1, b: 'hello' } const objB = { a: 1, b: 'hello' } - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) test('should return false for objects with different values', () => { const objA = { a: 1, b: 'hello' } const objB = { a: 2, b: 'world' } - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for objects with different keys', () => { const objA = { a: 1, b: 'hello' } const objB = { a: 1, c: 'world' } // @ts-expect-error - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for objects with different structures', () => { const objA = { a: 1, b: 'hello' } const objB = [1, 'hello'] // @ts-expect-error - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for one object being null', () => { const objA = { a: 1, b: 'hello' } const objB = null - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for one object being undefined', () => { const objA = { a: 1, b: 'hello' } const objB = undefined - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return true for two null objects', () => { const objA = null const objB = null - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) test('should return false for objects with different types', () => { const objA = { a: 1, b: 'hello' } const objB = { a: '1', b: 'hello' } // @ts-expect-error - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return true for shallow equal objects with symbol keys', () => { const sym = Symbol.for('key') const objA = { [sym]: 1 } const objB = { [sym]: 1 } - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) test('should return false for shallow different values for symbol keys', () => { const sym = Symbol.for('key') const objA = { [sym]: 1 } const objB = { [sym]: 2 } - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return true for non-enumerable keys', () => { @@ -762,6 +762,6 @@ describe('shallow', () => { value: 2, }) - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) }) diff --git a/packages/solid-store/tests/index.test.tsx b/packages/solid-store/tests/index.test.tsx index de9895d0..564913d0 100644 --- a/packages/solid-store/tests/index.test.tsx +++ b/packages/solid-store/tests/index.test.tsx @@ -3,8 +3,8 @@ import { render, renderHook } from '@solidjs/testing-library' import { createAtom, createStore } from '@tanstack/store' import { _useStore, + compare, createStoreContext, - shallow, useAtom, useSelector, useStore, @@ -263,101 +263,101 @@ describe('shallow', () => { test('should return true for shallowly equal objects', () => { const objA = { a: 1, b: 'hello' } const objB = { a: 1, b: 'hello' } - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) test('should return false for objects with different values', () => { const objA = { a: 1, b: 'hello' } const objB = { a: 2, b: 'world' } - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for objects with different keys', () => { const objA = { a: 1, b: 'hello' } const objB = { a: 1, c: 'world' } // @ts-expect-error - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for objects with different structures', () => { const objA = { a: 1, b: 'hello' } const objB = [1, 'hello'] // @ts-expect-error - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for one object being null', () => { const objA = { a: 1, b: 'hello' } const objB = null - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for one object being undefined', () => { const objA = { a: 1, b: 'hello' } const objB = undefined - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return true for two null objects', () => { const objA = null const objB = null - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) test('should return false for objects with different types', () => { const objA = { a: 1, b: 'hello' } const objB = { a: '1', b: 'hello' } // @ts-expect-error - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return true for shallow equal objects with symbol keys', () => { const sym = Symbol.for('key') const objA = { [sym]: 1 } const objB = { [sym]: 1 } - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) test('should return false for shallow different values for symbol keys', () => { const sym = Symbol.for('key') const objA = { [sym]: 1 } const objB = { [sym]: 2 } - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return true for shallowly equal maps', () => { const objA = new Map([['1', 'hello']]) const objB = new Map([['1', 'hello']]) - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) test('should return false for maps with different values', () => { const objA = new Map([['1', 'hello']]) const objB = new Map([['1', 'world']]) - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return true for shallowly equal sets', () => { const objA = new Set([1]) const objB = new Set([1]) - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) test('should return false for sets with different values', () => { const objA = new Set([1]) const objB = new Set([2]) - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for dates with different values', () => { const objA = new Date('2025-04-10T14:48:00') const objB = new Date('2025-04-10T14:58:00') - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return true for equal dates', () => { const objA = new Date('2025-02-10') const objB = new Date('2025-02-10') - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) }) diff --git a/packages/store/src/compare.ts b/packages/store/src/compare.ts new file mode 100644 index 00000000..b4825c68 --- /dev/null +++ b/packages/store/src/compare.ts @@ -0,0 +1,138 @@ +export function compare( + objA: T, + objB: T, + config: { mode: 'shallow' | 'deep' } = { mode: 'shallow' }, +): boolean { + return _evaluate(objA, objB, config, new WeakMap()) +} + +function _evaluate( + objA: T, + objB: T, + config: { mode: 'shallow' | 'deep' }, + seen: WeakMap>, +): boolean { + if (Object.is(objA, objB)) { + return true + } + + if ( + typeof objA !== 'object' || + objA === null || + typeof objB !== 'object' || + objB === null + ) { + return false + } + + // guards against circular references + if (config.mode === 'deep') { + let seenB = seen.get(objA as object) + + if (seenB?.has(objB as object)) return true + + if (!seenB) { + seenB = new WeakSet() + seen.set(objA as object, seenB) + } + + seenB.add(objB as object) + } + + // guards against runtime cross type evaluation + if (Object.getPrototypeOf(objA) !== Object.getPrototypeOf(objB)) { + return false + } + + if (objA instanceof Date && objB instanceof Date) { + return objA.getTime() === objB.getTime() + } + + if ( + typeof File !== 'undefined' && + objA instanceof File && + objB instanceof File + ) { + return ( + objA.name === objB.name && + objA.size === objB.size && + objA.type === objB.type && + objA.lastModified === objB.lastModified + ) + } + + if (objA instanceof Map && objB instanceof Map) { + if (objA.size !== objB.size) return false + + if (config.mode === 'deep') { + for (const [k, v] of objA) { + if (!objB.has(k) || !_evaluate(v, objB.get(k), config, seen)) + return false + } + } + + if (config.mode === 'shallow') { + for (const [k, v] of objA) { + if (!objB.has(k) || !Object.is(v, objB.get(k))) return false + } + } + + return true + } + + if (objA instanceof Set && objB instanceof Set) { + if (objA.size !== objB.size) return false + + if (config.mode === 'deep') { + for (const v of objA) { + if (![...objB].some((bv) => _evaluate(v, bv, config, seen))) + return false + } + } + + if (config.mode === 'shallow') { + for (const v of objA) { + if (!objB.has(v)) return false + } + } + + return true + } + + const keysA = getOwnKeys(objA as object) + const keysB = getOwnKeys(objB as object) + + if (keysA.length !== keysB.length) { + return false + } + + if (config.mode === 'deep') { + for (const key of keysA) { + if ( + !Object.prototype.hasOwnProperty.call(objB, key) || + !_evaluate(objA[key as keyof T], objB[key as keyof T], config, seen) + ) { + return false + } + } + } + + if (config.mode === 'shallow') { + for (const key of keysA) { + if ( + !Object.prototype.hasOwnProperty.call(objB, key) || + !Object.is(objA[key as keyof T], objB[key as keyof T]) + ) { + return false + } + } + } + + return true +} + +function getOwnKeys(obj: object): Array { + return (Object.keys(obj) as Array).concat( + Object.getOwnPropertySymbols(obj), + ) +} diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index 71215a5e..80123c95 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -2,3 +2,4 @@ export * from './types' export * from './atom' export * from './store' export * from './shallow' +export * from './compare' diff --git a/packages/store/tests/evaluate.spec.ts b/packages/store/tests/evaluate.spec.ts new file mode 100644 index 00000000..d8a9eae2 --- /dev/null +++ b/packages/store/tests/evaluate.spec.ts @@ -0,0 +1,533 @@ +import { describe, expect, it, vi } from 'vitest' +import { compare } from '../src/compare' + +describe('evaluate', () => { + it('should test equality between primitives', () => { + const numbersTrue = compare(1, 1) + expect(numbersTrue).toEqual(true) + + const stringFalse = compare('uh oh', '') + expect(stringFalse).toEqual(false) + + const boolTrue = compare(true, true) + expect(boolTrue).toEqual(true) + + const nullFalse = compare(null, {}) + expect(nullFalse).toEqual(false) + + const undefinedFalse = compare(undefined, null) + expect(undefinedFalse).toEqual(false) + }) + + it('should return false for runtime cross-type comparisons', () => { + expect(compare(new Date(), new Map())).toEqual(false) + expect(compare(new Date(), new Set())).toEqual(false) + expect(compare(new Map(), new Set())).toEqual(false) + expect(compare(new Date(), {})).toEqual(false) + expect(compare(new Map(), {})).toEqual(false) + expect(compare(new Set(), {})).toEqual(false) + }) + + it('should return false when comparing a subclass instance to a base class instance', () => { + class ExtendedMap extends Map {} + class ExtendedSet extends Set {} + class ExtendedDate extends Date {} + + // subclass vs base class, different prototypes, never equal + expect(compare(new ExtendedMap(), new Map())).toEqual(false) + expect(compare(new Map(), new ExtendedMap())).toEqual(false) + expect(compare(new ExtendedSet(), new Set())).toEqual(false) + expect(compare(new Set(), new ExtendedSet())).toEqual(false) + expect(compare(new ExtendedDate(), new Date())).toEqual(false) + expect(compare(new Date(), new ExtendedDate())).toEqual(false) + + // two instances of the same subclass with equal contents are equal + expect(compare(new ExtendedMap(), new ExtendedMap())).toEqual(true) + expect(compare(new ExtendedSet(), new ExtendedSet())).toEqual(true) + + // same checks hold in deep mode + expect( + compare(new ExtendedMap(), new Map(), { mode: 'deep' }), + ).toEqual(false) + expect( + compare(new ExtendedSet(), new Set(), { mode: 'deep' }), + ).toEqual(false) + expect( + compare(new ExtendedDate(), new Date(), { mode: 'deep' }), + ).toEqual(false) + expect( + compare(new ExtendedMap(), new ExtendedMap(), { mode: 'deep' }), + ).toEqual(true) + expect( + compare(new ExtendedSet(), new ExtendedSet(), { mode: 'deep' }), + ).toEqual(true) + }) + + it('should not throw a runtime error when File is undefined in the environment', () => { + vi.stubGlobal('File', undefined) + + const file1 = { + name: 'hello.txt', + size: 5, + type: 'text/plain', + lastModified: 0, + } + const file2 = { + name: 'hello.txt', + size: 5, + type: 'text/plain', + lastModified: 0, + } + + try { + expect(() => compare(file1, file2)).not.toThrow() + } finally { + vi.unstubAllGlobals() + } + }) + + describe('shallow', () => { + it('should test equality between arrays', () => { + expect(compare([], [])).toEqual(true) + + const arrayFalse = compare([], ['']) + expect(arrayFalse).toEqual(false) + + const arrayDeepFalse = compare([[1]], []) + expect(arrayDeepFalse).toEqual(false) + + const arrayNestedFalse = compare([[1]], [[1]]) + expect(arrayNestedFalse).toEqual(false) + + const arrayComplexFalse = compare([[{ test: 'true' }], null], [[1], {}]) + expect(arrayComplexFalse).toEqual(false) + + const arrayComplexFalse2 = compare( + [[{ test: 'true' }], null], + [[{ test: 'true' }], null], + ) + expect(arrayComplexFalse2).toEqual(false) + }) + + it('should test equality between objects', () => { + const objTrue = compare({ test: 'same' }, { test: 'same' }) + expect(objTrue).toEqual(true) + + const objFalse = compare({ test: 'not' }, { test: 'same' }) + expect(objFalse).toEqual(false) + + const objDeepFalse = compare({ test: 'not' }, { test: { test: 'same' } }) + expect(objDeepFalse).toEqual(false) + + const objDeepArrFalse = compare({ test: [] }, { test: [[]] }) + expect(objDeepArrFalse).toEqual(false) + + const objNullFalse = compare({ test: '' }, null) + expect(objNullFalse).toEqual(false) + + const objComplexFalse = compare( + { test: { testTwo: '' }, arr: [[1]] }, + { test: { testTwo: false }, arr: [[1], [0]] }, + ) + expect(objComplexFalse).toEqual(false) + + const objComplexShallowFalse = compare( + { test: { testTwo: '' }, arr: [[1]] }, + { test: { testTwo: '' }, arr: [[1]] }, + ) + expect(objComplexShallowFalse).toEqual(false) + }) + + it('should test equality between Date objects', () => { + const date1 = new Date('2025-01-01T00:00:00.000Z') + const date2 = new Date('2025-01-01T00:00:00.000Z') + const date3 = new Date('2025-01-02T00:00:00.000Z') + + expect(compare(date1, date2)).toEqual(true) + expect(compare(date1, date3)).toEqual(false) + + const dateObjectShallowFalse = compare({ date: date1 }, { date: date2 }) + expect(dateObjectShallowFalse).toEqual(false) + + const dateObjectFalse = compare({ date: date1 }, { date: date3 }) + expect(dateObjectFalse).toEqual(false) + }) + + it('should test equality between Map objects', () => { + expect( + compare( + new Map([ + ['a', 1], + ['b', 2], + ]), + new Map([ + ['a', 1], + ['b', 2], + ]), + ), + ).toEqual(true) + expect(compare(new Map(), new Map())).toEqual(true) + expect( + compare( + new Map([['a', 1]]), + new Map([ + ['a', 1], + ['b', 2], + ]), + ), + ).toEqual(false) + expect( + compare( + new Map([ + ['a', 1], + ['b', 2], + ]), + new Map([ + ['a', 1], + ['c', 2], + ]), + ), + ).toEqual(false) + expect( + compare( + new Map([ + ['a', 1], + ['b', 2], + ]), + new Map([ + ['a', 1], + ['b', 3], + ]), + ), + ).toEqual(false) + + const obj = { x: 1 } + expect(compare(new Map([['a', obj]]), new Map([['a', obj]]))).toEqual( + true, + ) + expect( + compare(new Map([['a', { x: 1 }]]), new Map([['a', { x: 1 }]])), + ).toEqual(false) + }) + + it('should test equality between Set objects', () => { + expect(compare(new Set([1, 2, 3]), new Set([1, 2, 3]))).toEqual(true) + expect(compare(new Set(), new Set())).toEqual(true) + expect(compare(new Set([1, 2]), new Set([1, 2, 3]))).toEqual(false) + expect(compare(new Set([1, 2, 3]), new Set([1, 2, 4]))).toEqual(false) + + const obj = { x: 1 } + expect(compare(new Set([obj]), new Set([obj]))).toEqual(true) + expect(compare(new Set([{ x: 1 }]), new Set([{ x: 1 }]))).toEqual(false) + }) + + it('should test equality between File objects', () => { + const file1 = new File(['hello'], 'hello.txt', { + type: 'text/plain', + lastModified: 0, + }) + const file2 = new File(['hello'], 'hello.txt', { + type: 'text/plain', + lastModified: 0, + }) + const fileDiffName = new File(['hello'], 'world.txt', { + type: 'text/plain', + lastModified: 0, + }) + const fileDiffType = new File(['hello'], 'hello.txt', { + type: 'text/html', + lastModified: 0, + }) + const fileDiffSize = new File(['hello world'], 'hello.txt', { + type: 'text/plain', + lastModified: 0, + }) + + expect(compare(file1, file2)).toEqual(true) + expect(compare(file1, fileDiffName)).toEqual(false) + expect(compare(file1, fileDiffType)).toEqual(false) + expect(compare(file1, fileDiffSize)).toEqual(false) + + expect(compare({ file: file1 }, { file: file2 })).toEqual(false) + expect(compare({ file: file1 }, { file: fileDiffName })).toEqual(false) + }) + + it('should test equality between objects with Symbol keys', () => { + const sym = Symbol('id') + + expect( + compare({ [sym]: 1, name: 'foo' }, { [sym]: 1, name: 'foo' }), + ).toEqual(true) + expect( + compare({ [sym]: 1, name: 'foo' }, { [sym]: 2, name: 'foo' }), + ).toEqual(false) + expect(compare({ [sym]: 1 } as any, {} as any)).toEqual(false) + expect(compare({} as any, { [sym]: 1 } as any)).toEqual(false) + }) + }) + + describe('deep', () => { + it('should test equality between arrays', () => { + expect(compare([], [], { mode: 'deep' })).toEqual(true) + + const arrayFalse = compare([], [''], { mode: 'deep' }) + expect(arrayFalse).toEqual(false) + + const arrayDeepFalse = compare([[1]], [], { mode: 'deep' }) + expect(arrayDeepFalse).toEqual(false) + + const arrayDeepSearchTrue = compare([[1]], [[1]], { mode: 'deep' }) + expect(arrayDeepSearchTrue).toEqual(true) + + const arrayComplexFalse = compare([[{ test: 'true' }], null], [[1], {}], { + mode: 'deep', + }) + expect(arrayComplexFalse).toEqual(false) + + const arrayComplexTrue = compare( + [[{ test: 'true' }], null], + [[{ test: 'true' }], null], + { mode: 'deep' }, + ) + expect(arrayComplexTrue).toEqual(true) + }) + + it('should test equality between objects', () => { + const objTrue = compare( + { test: 'same' }, + { test: 'same' }, + { mode: 'deep' }, + ) + expect(objTrue).toEqual(true) + + const objFalse = compare( + { test: 'not' }, + { test: 'same' }, + { mode: 'deep' }, + ) + expect(objFalse).toEqual(false) + + const objDeepFalse = compare( + { test: 'not' }, + { test: { test: 'same' } }, + { mode: 'deep' }, + ) + expect(objDeepFalse).toEqual(false) + + const objDeepArrFalse = compare( + { test: [] }, + { test: [[]] }, + { mode: 'deep' }, + ) + expect(objDeepArrFalse).toEqual(false) + + const objNullFalse = compare({ test: '' }, null, { mode: 'deep' }) + expect(objNullFalse).toEqual(false) + + const objComplexFalse = compare( + { test: { testTwo: '' }, arr: [[1]] }, + { test: { testTwo: false }, arr: [[1], [0]] }, + { mode: 'deep' }, + ) + expect(objComplexFalse).toEqual(false) + + const objComplexTrue = compare( + { test: { testTwo: '' }, arr: [[1]] }, + { test: { testTwo: '' }, arr: [[1]] }, + { mode: 'deep' }, + ) + expect(objComplexTrue).toEqual(true) + }) + + it('should test equality between Date objects', () => { + const date1 = new Date('2025-01-01T00:00:00.000Z') + const date2 = new Date('2025-01-01T00:00:00.000Z') + const date3 = new Date('2025-01-02T00:00:00.000Z') + + expect(compare(date1, date2, { mode: 'deep' })).toEqual(true) + expect(compare(date1, date3, { mode: 'deep' })).toEqual(false) + + const dateObjectTrue = compare( + { date: date1 }, + { date: date2 }, + { mode: 'deep' }, + ) + expect(dateObjectTrue).toEqual(true) + + const dateObjectFalse = compare( + { date: date1 }, + { date: date3 }, + { mode: 'deep' }, + ) + expect(dateObjectFalse).toEqual(false) + }) + + it('should test equality between Map objects', () => { + expect( + compare( + new Map([ + ['a', 1], + ['b', 2], + ]), + new Map([ + ['a', 1], + ['b', 2], + ]), + { mode: 'deep' }, + ), + ).toEqual(true) + expect(compare(new Map(), new Map(), { mode: 'deep' })).toEqual(true) + expect( + compare( + new Map([['a', 1]]), + new Map([ + ['a', 1], + ['b', 2], + ]), + { mode: 'deep' }, + ), + ).toEqual(false) + expect( + compare( + new Map([ + ['a', 1], + ['b', 2], + ]), + new Map([ + ['a', 1], + ['b', 3], + ]), + { mode: 'deep' }, + ), + ).toEqual(false) + + expect( + compare(new Map([['a', { x: 1 }]]), new Map([['a', { x: 1 }]]), { + mode: 'deep', + }), + ).toEqual(true) + expect( + compare(new Map([['a', { x: 1 }]]), new Map([['a', { x: 2 }]]), { + mode: 'deep', + }), + ).toEqual(false) + expect( + compare( + new Map([['a', { nested: { x: 1 } }]]), + new Map([['a', { nested: { x: 1 } }]]), + { mode: 'deep' }, + ), + ).toEqual(true) + }) + + it('should test equality between Set objects', () => { + expect( + compare(new Set([1, 2, 3]), new Set([1, 2, 3]), { mode: 'deep' }), + ).toEqual(true) + expect(compare(new Set(), new Set(), { mode: 'deep' })).toEqual(true) + expect( + compare(new Set([1, 2]), new Set([1, 2, 3]), { mode: 'deep' }), + ).toEqual(false) + expect( + compare(new Set([1, 2, 3]), new Set([1, 2, 4]), { mode: 'deep' }), + ).toEqual(false) + + expect( + compare(new Set([{ x: 1 }]), new Set([{ x: 1 }]), { mode: 'deep' }), + ).toEqual(true) + expect( + compare(new Set([{ x: 1 }]), new Set([{ x: 2 }]), { mode: 'deep' }), + ).toEqual(false) + expect( + compare( + new Set([{ nested: { x: 1 } }]), + new Set([{ nested: { x: 1 } }]), + { mode: 'deep' }, + ), + ).toEqual(true) + }) + + it('should test equality between File objects', () => { + const file1 = new File(['hello'], 'hello.txt', { + type: 'text/plain', + lastModified: 0, + }) + const file2 = new File(['hello'], 'hello.txt', { + type: 'text/plain', + lastModified: 0, + }) + const fileDiffName = new File(['hello'], 'world.txt', { + type: 'text/plain', + lastModified: 0, + }) + const fileDiffType = new File(['hello'], 'hello.txt', { + type: 'text/html', + lastModified: 0, + }) + const fileDiffSize = new File(['hello world'], 'hello.txt', { + type: 'text/plain', + lastModified: 0, + }) + + expect(compare(file1, file2, { mode: 'deep' })).toEqual(true) + expect(compare(file1, fileDiffName, { mode: 'deep' })).toEqual(false) + expect(compare(file1, fileDiffType, { mode: 'deep' })).toEqual(false) + expect(compare(file1, fileDiffSize, { mode: 'deep' })).toEqual(false) + + expect( + compare({ file: file1 }, { file: file2 }, { mode: 'deep' }), + ).toEqual(true) + expect( + compare({ file: file1 }, { file: fileDiffName }, { mode: 'deep' }), + ).toEqual(false) + }) + + it('should test equality between objects with Symbol keys', () => { + const sym = Symbol('id') + + expect( + compare( + { [sym]: 1, name: 'foo' }, + { [sym]: 1, name: 'foo' }, + { mode: 'deep' }, + ), + ).toEqual(true) + expect( + compare( + { [sym]: 1, name: 'foo' }, + { [sym]: 2, name: 'foo' }, + { mode: 'deep' }, + ), + ).toEqual(false) + expect( + compare( + { [sym]: { nested: true } } as any, + { [sym]: { nested: true } } as any, + { mode: 'deep' }, + ), + ).toEqual(true) + expect( + compare( + { [sym]: { nested: true } } as any, + { [sym]: { nested: false } } as any, + { mode: 'deep' }, + ), + ).toEqual(false) + }) + + it('should handle circular references', () => { + const a: any = { x: 1 } + a.self = a + + const b: any = { x: 1 } + b.self = b + + expect(() => compare(a, b, { mode: 'deep' })).not.toThrow() + expect(compare(a, b, { mode: 'deep' })).toEqual(true) + + const c: any = { x: 2 } + c.self = c + expect(compare(a, c, { mode: 'deep' })).toEqual(false) + }) + }) +}) diff --git a/packages/svelte-store/tests/index.test.ts b/packages/svelte-store/tests/index.test.ts index dc304f01..ede62c00 100644 --- a/packages/svelte-store/tests/index.test.ts +++ b/packages/svelte-store/tests/index.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, test } from 'vitest' import { render, waitFor } from '@testing-library/svelte' import { userEvent } from '@testing-library/user-event' -import { shallow } from '../src/index.svelte.js' +import { compare } from '@tanstack/store' import TestBaseStore from './BaseStore.test.svelte' import TestRerender from './Render.test.svelte' import TestValue from './Value.test.svelte' @@ -44,63 +44,63 @@ describe('shallow', () => { test('should return true for shallowly equal objects', () => { const objA = { a: 1, b: 'hello' } const objB = { a: 1, b: 'hello' } - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) test('should return false for objects with different values', () => { const objA = { a: 1, b: 'hello' } const objB = { a: 2, b: 'world' } - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for objects with different keys', () => { const objA = { a: 1, b: 'hello' } const objB = { a: 1, c: 'world' } // @ts-expect-error testing invalid input - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for objects with different structures', () => { const objA = { a: 1, b: 'hello' } const objB = [1, 'hello'] // @ts-expect-error testing invalid input - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for one object being null', () => { const objA = { a: 1, b: 'hello' } const objB = null - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for one object being undefined', () => { const objA = { a: 1, b: 'hello' } const objB = undefined - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return true for two null objects', () => { const objA = null const objB = null - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) test('should return false for objects with different types', () => { const objA = { a: 1, b: 'hello' } const objB = { a: '1', b: 'hello' } // @ts-expect-error testing invalid input - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for dates with different values', () => { const objA = new Date('2025-04-10T14:48:00') const objB = new Date('2025-04-10T14:58:00') - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return true for equal dates', () => { const objA = new Date('2025-02-10') const objB = new Date('2025-02-10') - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) }) diff --git a/packages/vue-store/tests/index.test.tsx b/packages/vue-store/tests/index.test.tsx index f9a2d0ce..000d6871 100644 --- a/packages/vue-store/tests/index.test.tsx +++ b/packages/vue-store/tests/index.test.tsx @@ -5,7 +5,7 @@ import { createAtom, createStore } from '@tanstack/store' import { userEvent } from '@testing-library/user-event' import { _useStore, - shallow, + compare, useAtom, useSelector, useStore, @@ -378,101 +378,101 @@ describe('shallow', () => { test('should return true for shallowly equal objects', () => { const objA = { a: 1, b: 'hello' } const objB = { a: 1, b: 'hello' } - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) test('should return false for objects with different values', () => { const objA = { a: 1, b: 'hello' } const objB = { a: 2, b: 'world' } - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for objects with different keys', () => { const objA = { a: 1, b: 'hello' } const objB = { a: 1, c: 'world' } // @ts-expect-error - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for objects with different structures', () => { const objA = { a: 1, b: 'hello' } const objB = [1, 'hello'] // @ts-expect-error - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for one object being null', () => { const objA = { a: 1, b: 'hello' } const objB = null - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for one object being undefined', () => { const objA = { a: 1, b: 'hello' } const objB = undefined - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return true for two null objects', () => { const objA = null const objB = null - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) test('should return false for objects with different types', () => { const objA = { a: 1, b: 'hello' } const objB = { a: '1', b: 'hello' } // @ts-expect-error - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return true for shallow equal objects with symbol keys', () => { const sym = Symbol.for('key') const objA = { [sym]: 1 } const objB = { [sym]: 1 } - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) test('should return false for shallow different values for symbol keys', () => { const sym = Symbol.for('key') const objA = { [sym]: 1 } const objB = { [sym]: 2 } - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return true for shallowly equal maps', () => { const objA = new Map([['1', 'hello']]) const objB = new Map([['1', 'hello']]) - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) test('should return false for maps with different values', () => { const objA = new Map([['1', 'hello']]) const objB = new Map([['1', 'world']]) - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return true for shallowly equal sets', () => { const objA = new Set([1]) const objB = new Set([1]) - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) test('should return false for sets with different values', () => { const objA = new Set([1]) const objB = new Set([2]) - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return false for dates with different values', () => { const objA = new Date('2025-04-10T14:48:00') const objB = new Date('2025-04-10T14:58:00') - expect(shallow(objA, objB)).toBe(false) + expect(compare(objA, objB)).toBe(false) }) test('should return true for equal dates', () => { const objA = new Date('2025-02-10') const objB = new Date('2025-02-10') - expect(shallow(objA, objB)).toBe(true) + expect(compare(objA, objB)).toBe(true) }) })