import { replaceEqualDeep, partialDeepEqual, isPlainObject, parseMutationArgs, matchMutation, scheduleMicrotask, sleep, isPlainArray, } from '../utils' import { Mutation } from '../mutation' import { createQueryClient } from './utils' describe('core/utils', () => { describe('isPlainObject', () => { it('should return `true` for a plain object', () => { expect(isPlainObject({})).toEqual(true) }) it('should return `false` for an array', () => { expect(isPlainObject([])).toEqual(false) }) it('should return `false` for null', () => { expect(isPlainObject(null)).toEqual(false) }) it('should return `false` for undefined', () => { expect(isPlainObject(undefined)).toEqual(false) }) it('should return `true` for object with an undefined constructor', () => { expect(isPlainObject(Object.create(null))).toBeTruthy() }) it('should return `false` if constructor does not have an Object-specific method', () => { class Foo { abc: any constructor() { this.abc = {} } } expect(isPlainObject(new Foo())).toBeFalsy() }) it('should return `false` if the object has a modified prototype', () => { function Graph(this: any) { this.vertices = [] this.edges = [] } Graph.prototype.addVertex = function (v: any) { this.vertices.push(v) } expect(isPlainObject(Object.create(Graph))).toBeFalsy() }) }) describe('isPlainArray', () => { it('should return `true` for plain arrays', () => { expect(isPlainArray([1, 2])).toEqual(true) }) it('should return `false` for non plain arrays', () => { expect(isPlainArray(Object.assign([1, 2], { a: 'b' }))).toEqual(false) }) }) describe('partialDeepEqual', () => { it('should return `true` if a includes b', () => { const a = { a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] } const b = { a: { b: 'b' }, c: 'c', d: [] } expect(partialDeepEqual(a, b)).toEqual(true) }) it('should return `false` if a does not include b', () => { const a = { a: { b: 'b' }, c: 'c', d: [] } const b = { a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] } expect(partialDeepEqual(a, b)).toEqual(false) }) it('should return `true` if array a includes array b', () => { const a = [1, 2, 3] const b = [1, 2] expect(partialDeepEqual(a, b)).toEqual(true) }) it('should return `false` if a is null and b is not', () => { const a = null const b = { a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] } expect(partialDeepEqual(a, b)).toEqual(false) }) it('should return `false` if a contains null and b is not', () => { const a = { a: null, c: 'c', d: [] } const b = { a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] } expect(partialDeepEqual(a, b)).toEqual(false) }) it('should return `false` if b is null and a is not', () => { const a = { a: { b: 'b' }, c: 'c', d: [] } const b = null expect(partialDeepEqual(a, b)).toEqual(false) }) it('should return `false` if b contains null and a is not', () => { const a = { a: { b: 'b' }, c: 'c', d: [] } const b = { a: null, c: 'c', d: [{ d: 'd ' }] } expect(partialDeepEqual(a, b)).toEqual(false) }) }) describe('replaceEqualDeep', () => { it('should return the previous value when the next value is an equal primitive', () => { expect(replaceEqualDeep(1, 1)).toBe(1) expect(replaceEqualDeep('1', '1')).toBe('1') expect(replaceEqualDeep(true, true)).toBe(true) expect(replaceEqualDeep(false, false)).toBe(false) expect(replaceEqualDeep(null, null)).toBe(null) expect(replaceEqualDeep(undefined, undefined)).toBe(undefined) }) it('should return the next value when the previous value is a different value', () => { const date1 = new Date() const date2 = new Date() expect(replaceEqualDeep(1, 0)).toBe(0) expect(replaceEqualDeep(1, 2)).toBe(2) expect(replaceEqualDeep('1', '2')).toBe('2') expect(replaceEqualDeep(true, false)).toBe(false) expect(replaceEqualDeep(false, true)).toBe(true) expect(replaceEqualDeep(date1, date2)).toBe(date2) }) it('should return the next value when the previous value is a different type', () => { const array = [1] const object = { a: 'a' } expect(replaceEqualDeep(0, undefined)).toBe(undefined) expect(replaceEqualDeep(undefined, 0)).toBe(0) expect(replaceEqualDeep(2, undefined)).toBe(undefined) expect(replaceEqualDeep(undefined, 2)).toBe(2) expect(replaceEqualDeep(undefined, null)).toBe(null) expect(replaceEqualDeep(null, undefined)).toBe(undefined) expect(replaceEqualDeep({}, undefined)).toBe(undefined) expect(replaceEqualDeep([], undefined)).toBe(undefined) expect(replaceEqualDeep(array, object)).toBe(object) expect(replaceEqualDeep(object, array)).toBe(array) }) it('should return the previous value when the next value is an equal array', () => { const prev = [1, 2] const next = [1, 2] expect(replaceEqualDeep(prev, next)).toBe(prev) }) it('should return a copy when the previous value is a different array subset', () => { const prev = [1, 2] const next = [1, 2, 3] const result = replaceEqualDeep(prev, next) expect(result).toEqual(next) expect(result).not.toBe(prev) expect(result).not.toBe(next) }) it('should return a copy when the previous value is a different array superset', () => { const prev = [1, 2, 3] const next = [1, 2] const result = replaceEqualDeep(prev, next) expect(result).toEqual(next) expect(result).not.toBe(prev) expect(result).not.toBe(next) }) it('should return the previous value when the next value is an equal empty array', () => { const prev: any[] = [] const next: any[] = [] expect(replaceEqualDeep(prev, next)).toBe(prev) }) it('should return the previous value when the next value is an equal empty object', () => { const prev = {} const next = {} expect(replaceEqualDeep(prev, next)).toBe(prev) }) it('should return the previous value when the next value is an equal object', () => { const prev = { a: 'a' } const next = { a: 'a' } expect(replaceEqualDeep(prev, next)).toBe(prev) }) it('should replace different values in objects', () => { const prev = { a: { b: 'b' }, c: 'c' } const next = { a: { b: 'b' }, c: 'd' } const result = replaceEqualDeep(prev, next) expect(result).toEqual(next) expect(result).not.toBe(prev) expect(result).not.toBe(next) expect(result.a).toBe(prev.a) expect(result.c).toBe(next.c) }) it('should replace different values in arrays', () => { const prev = [1, { a: 'a' }, { b: { b: 'b' } }, [1]] as const const next = [1, { a: 'a' }, { b: { b: 'c' } }, [1]] as const const result = replaceEqualDeep(prev, next) expect(result).toEqual(next) expect(result).not.toBe(prev) expect(result).not.toBe(next) expect(result[0]).toBe(prev[0]) expect(result[1]).toBe(prev[1]) expect(result[2]).not.toBe(next[2]) expect(result[2].b.b).toBe(next[2].b.b) expect(result[3]).toBe(prev[3]) }) it('should replace different values in arrays when the next value is a subset', () => { const prev = [{ a: 'a' }, { b: 'b' }, { c: 'c' }] const next = [{ a: 'a' }, { b: 'b' }] const result = replaceEqualDeep(prev, next) expect(result).toEqual(next) expect(result).not.toBe(prev) expect(result).not.toBe(next) expect(result[0]).toBe(prev[0]) expect(result[1]).toBe(prev[1]) expect(result[2]).toBeUndefined() }) it('should replace different values in arrays when the next value is a superset', () => { const prev = [{ a: 'a' }, { b: 'b' }] const next = [{ a: 'a' }, { b: 'b' }, { c: 'c' }] const result = replaceEqualDeep(prev, next) expect(result).toEqual(next) expect(result).not.toBe(prev) expect(result).not.toBe(next) expect(result[0]).toBe(prev[0]) expect(result[1]).toBe(prev[1]) expect(result[2]).toBe(next[2]) }) it('should copy objects which are not arrays or objects', () => { const prev = [{ a: 'a' }, { b: 'b' }, { c: 'c' }, 1] const next = [{ a: 'a' }, new Map(), { c: 'c' }, 2] const result = replaceEqualDeep(prev, next) expect(result).not.toBe(prev) expect(result).not.toBe(next) expect(result[0]).toBe(prev[0]) expect(result[1]).toBe(next[1]) expect(result[2]).toBe(prev[2]) expect(result[3]).toBe(next[3]) }) it('should support equal objects which are not arrays or objects', () => { const map = new Map() const prev = [map, [1]] const next = [map, [1]] const result = replaceEqualDeep(prev, next) expect(result).toBe(prev) }) it('should support non equal objects which are not arrays or objects', () => { const map1 = new Map() const map2 = new Map() const prev = [map1, [1]] const next = [map2, [1]] const result = replaceEqualDeep(prev, next) expect(result).not.toBe(prev) expect(result).not.toBe(next) expect(result[0]).toBe(next[0]) expect(result[1]).toBe(prev[1]) }) it('should support objects which are not plain arrays', () => { const prev = Object.assign([1, 2], { a: { b: 'b' }, c: 'c' }) const next = Object.assign([1, 2], { a: { b: 'b' }, c: 'c' }) const result = replaceEqualDeep(prev, next) expect(result).toBe(next) }) it('should replace all parent objects if some nested value changes', () => { const prev = { todo: { id: '1', meta: { createdAt: 0 }, state: { done: false } }, otherTodo: { id: '2', meta: { createdAt: 0 }, state: { done: true } }, } const next = { todo: { id: '1', meta: { createdAt: 0 }, state: { done: true } }, otherTodo: { id: '2', meta: { createdAt: 0 }, state: { done: true } }, } const result = replaceEqualDeep(prev, next) expect(result).toEqual(next) expect(result).not.toBe(prev) expect(result).not.toBe(next) expect(result.todo).not.toBe(prev.todo) expect(result.todo).not.toBe(next.todo) expect(result.todo.id).toBe(next.todo.id) expect(result.todo.meta).toBe(prev.todo.meta) expect(result.todo.state).not.toBe(next.todo.state) expect(result.todo.state.done).toBe(next.todo.state.done) expect(result.otherTodo).toBe(prev.otherTodo) }) it('should replace all parent arrays if some nested value changes', () => { const prev = { todos: [ { id: '1', meta: { createdAt: 0 }, state: { done: false } }, { id: '2', meta: { createdAt: 0 }, state: { done: true } }, ], } const next = { todos: [ { id: '1', meta: { createdAt: 0 }, state: { done: true } }, { id: '2', meta: { createdAt: 0 }, state: { done: true } }, ], } const result = replaceEqualDeep(prev, next) expect(result).toEqual(next) expect(result).not.toBe(prev) expect(result).not.toBe(next) expect(result.todos).not.toBe(prev.todos) expect(result.todos).not.toBe(next.todos) expect(result.todos[0]).not.toBe(prev.todos[0]) expect(result.todos[0]).not.toBe(next.todos[0]) expect(result.todos[0]?.id).toBe(next.todos[0]?.id) expect(result.todos[0]?.meta).toBe(prev.todos[0]?.meta) expect(result.todos[0]?.state).not.toBe(next.todos[0]?.state) expect(result.todos[0]?.state.done).toBe(next.todos[0]?.state.done) expect(result.todos[1]).toBe(prev.todos[1]) }) }) describe('parseMutationArgs', () => { it('should return mutation options', () => { const options = { mutationKey: ['key'] } expect(parseMutationArgs(options)).toMatchObject(options) }) }) describe('matchMutation', () => { it('should return false if mutationKey options is undefined', () => { const filters = { mutationKey: ['key1'] } const queryClient = createQueryClient() const mutation = new Mutation({ mutationId: 1, mutationCache: queryClient.getMutationCache(), options: {}, }) expect(matchMutation(filters, mutation)).toBeFalsy() }) }) describe('scheduleMicrotask', () => { it('should defer execution of callback', async () => { const callback = jest.fn() scheduleMicrotask(callback) expect(callback).not.toHaveBeenCalled() await sleep(0) expect(callback).toHaveBeenCalledTimes(1) }) }) })