1
0
mirror of synced 2024-11-28 16:16:09 +03:00

refactor: Deep cloning of state, deep equal checker

This commit is contained in:
Zaytsev Kirill 2021-06-11 20:32:46 +03:00
parent 7c2a9e8110
commit b37040d2d3
16 changed files with 395 additions and 353 deletions

View File

@ -7,10 +7,10 @@ import {
ValidationRuleFn,
ValidationMessageFn,
ValidationMessageI18NFn,
ViolationsRecord,
Violation,
} from '@/validation/validator'
import { FormularioFormInterface } from '@/types'
import { FormularioForm } from '@/types'
export interface FormularioOptions {
validationRules?: Record<string, ValidationRuleFn>;
@ -24,7 +24,7 @@ export default class Formulario {
public validationRules: Record<string, ValidationRuleFn> = {}
public validationMessages: Record<string, ValidationMessageI18NFn|string> = {}
private readonly registry: Map<string, FormularioFormInterface>
private readonly registry: Map<string, FormularioForm>
public constructor (options?: FormularioOptions) {
this.registry = new Map()
@ -47,12 +47,12 @@ export default class Formulario {
throw new Error(`[Formulario]: Formulario.extend(): should be passed an object (was ${typeof extendWith})`)
}
public runValidation (id: string): Promise<ViolationsRecord> {
public runValidation (id: string): Promise<Record<string, Violation[]>> {
if (!this.registry.has(id)) {
throw new Error(`[Formulario]: Formulario.runValidation(): no forms with id "${id}"`)
}
const form = this.registry.get(id) as FormularioFormInterface
const form = this.registry.get(id) as FormularioForm
return form.runValidation()
}
@ -62,7 +62,7 @@ export default class Formulario {
return
}
const form = this.registry.get(id) as FormularioFormInterface
const form = this.registry.get(id) as FormularioForm
form.resetValidation()
}
@ -71,7 +71,7 @@ export default class Formulario {
* Used by forms instances to add themselves into a registry
* @internal
*/
public register (id: string, form: FormularioFormInterface): void {
public register (id: string, form: FormularioForm): void {
if (this.registry.has(id)) {
throw new Error(`[Formulario]: Formulario.register(): id "${id}" is already in use`)
}

View File

@ -13,7 +13,7 @@ import {
Prop,
Watch,
} from 'vue-property-decorator'
import { has, shallowEquals, snakeToCamel } from './utils'
import { deepEquals, has, snakeToCamel } from './utils'
import {
processConstraints,
validate,
@ -87,35 +87,18 @@ export default class FormularioField extends Vue {
return has(this.$options.propsData || {}, 'value')
}
public get model (): unknown {
return this.modelGetConverter(this.hasModel ? this.value : this.proxy)
}
public set model (value: unknown) {
value = this.modelSetConverter(value, this.proxy)
if (!shallowEquals(value, this.proxy)) {
this.proxy = value
this.$emit('input', value)
if (typeof this.__FormularioForm_set === 'function') {
this.__FormularioForm_set(this.fullPath, value)
this.__FormularioForm_emitInput()
}
}
}
private get context (): FormularioFieldContext<unknown> {
return Object.defineProperty({
name: this.fullPath,
path: this.fullPath,
runValidation: this.runValidation.bind(this),
violations: this.violations,
errors: this.localErrors,
allErrors: [...this.localErrors, ...this.violations.map(v => v.message)],
}, 'model', {
get: () => this.model,
set: (value: unknown) => {
this.model = value
get: () => this.modelGetConverter(this.proxy),
set: (value: unknown): void => {
this.syncProxy(this.modelSetConverter(value, this.proxy))
},
})
}
@ -137,18 +120,12 @@ export default class FormularioField extends Vue {
}
@Watch('value')
private onValueChange (newValue: unknown, oldValue: unknown): void {
if (this.hasModel && !shallowEquals(newValue, oldValue)) {
this.model = newValue
}
private onValueChange (): void {
this.syncProxy(this.value)
}
@Watch('proxy')
private onProxyChange (newValue: unknown, oldValue: unknown): void {
if (!this.hasModel && !shallowEquals(newValue, oldValue)) {
this.model = newValue
}
private onProxyChange (): void {
if (this.validationBehavior === VALIDATION_BEHAVIOR.LIVE) {
this.runValidation()
} else {
@ -160,10 +137,6 @@ export default class FormularioField extends Vue {
* @internal
*/
public created (): void {
if (!shallowEquals(this.model, this.proxy)) {
this.model = this.proxy
}
if (typeof this.__FormularioForm_register === 'function') {
this.__FormularioForm_register(this.fullPath, this)
}
@ -182,13 +155,22 @@ export default class FormularioField extends Vue {
}
}
public runValidation (): Promise<Violation[]> {
this.validationRun = this.validate().then(violations => {
if (!shallowEquals(this.violations, violations)) {
this.emitValidation(this.fullPath, violations)
private syncProxy (value: unknown): void {
if (!deepEquals(value, this.proxy)) {
this.proxy = value
this.$emit('input', value)
if (typeof this.__FormularioForm_set === 'function') {
this.__FormularioForm_set(this.fullPath, value)
this.__FormularioForm_emitInput()
}
}
}
public runValidation (): Promise<Violation[]> {
this.validationRun = this.validate().then(violations => {
this.violations = violations
this.emitValidation(this.fullPath, violations)
return this.violations
})
@ -202,8 +184,8 @@ export default class FormularioField extends Vue {
this.$formulario.getRules(this.normalizedValidationRules),
this.$formulario.getMessages(this, this.normalizedValidationMessages),
), {
value: this.context.model,
name: this.context.name,
value: this.proxy,
name: this.fullPath,
formValues: this.__FormularioForm_getState(),
})
}

View File

@ -22,17 +22,17 @@ export default class FormularioFieldGroup extends Vue {
@Provide('__Formulario_path')
get fullPath (): string {
const name = `${this.name}`
const path = `${this.name}`
if (parseInt(name).toString() === name) {
return `${this.__Formulario_path}[${name}]`
if (parseInt(path).toString() === path) {
return `${this.__Formulario_path}[${path}]`
}
if (this.__Formulario_path === '') {
return name
return path
}
return `${this.__Formulario_path}.${name}`
return `${this.__Formulario_path}.${path}`
}
}
</script>

View File

@ -14,67 +14,51 @@ import {
Watch,
} from 'vue-property-decorator'
import {
id,
clone,
deepEquals,
get,
has,
merge,
set,
shallowEquals,
unset,
} from '@/utils'
import PathRegistry from '@/PathRegistry'
import { FormularioFieldInterface } from '@/types'
import {
Violation,
ViolationsRecord,
} from '@/validation/validator'
type ErrorsRecord = Record<string, string[]>
import { FormularioField } from '@/types'
import { Violation } from '@/validation/validator'
type ValidationEventPayload = {
name: string;
violations: Violation[];
}
let counter = 0
const update = (state: Record<string, unknown>, path: string, value: unknown): Record<string, unknown> => {
if (value === undefined) {
return unset(state, path) as Record<string, unknown>
}
return set(state, path, value) as Record<string, unknown>
}
@Component({ name: 'FormularioForm' })
export default class FormularioForm extends Vue {
@Model('input', { default: () => ({}) })
public readonly state!: Record<string, unknown>
@Prop({ default: () => `formulario-form-${++counter}` })
@Prop({ default: () => id('formulario-form') })
public readonly id!: string
// Describes validation errors of whole form
@Prop({ default: () => ({}) }) readonly fieldsErrors!: ErrorsRecord
@Prop({ default: () => ({}) }) readonly fieldsErrors!: Record<string, string[]>
// Only used on FormularioForm default slot
@Prop({ default: () => ([]) }) readonly formErrors!: string[]
private proxy: Record<string, unknown> = {}
private registry: PathRegistry<FormularioFieldInterface> = new PathRegistry()
private registry: Map<string, FormularioField> = new Map()
// Local error messages are temporal, they wiped each resetValidation call
private localFieldsErrors: ErrorsRecord = {}
private localFieldsErrors: Record<string, string[]> = {}
private localFormErrors: string[] = []
private get hasModel (): boolean {
return has(this.$options.propsData || {}, 'state')
}
private get modelIsDefined (): boolean {
return this.state && typeof this.state === 'object'
}
private get modelCopy (): Record<string, unknown> {
if (this.hasModel && typeof this.state === 'object') {
return { ...this.state } // @todo - use a deep clone to detach reference types
}
return {}
}
private get fieldsErrorsComputed (): Record<string, string[]> {
return merge(this.fieldsErrors || {}, this.localFieldsErrors)
}
@ -84,19 +68,21 @@ export default class FormularioForm extends Vue {
}
@Provide('__FormularioForm_register')
private register (path: string, field: FormularioFieldInterface): void {
this.registry.add(path, field)
private register (path: string, field: FormularioField): void {
if (!this.registry.has(path)) {
this.registry.set(path, field)
}
const value = get(this.modelCopy, path)
const value = get(this.proxy, path)
if (!field.hasModel && this.modelIsDefined) {
if (!field.hasModel) {
if (value !== undefined) {
field.model = value
field.proxy = value
} else {
this.setFieldValue(path, null)
this.emitInput()
}
} else if (field.hasModel && !shallowEquals(field.proxy, value)) {
} else if (!deepEquals(field.proxy, value)) {
this.setFieldValue(path, field.proxy)
this.emitInput()
}
@ -109,10 +95,9 @@ export default class FormularioForm extends Vue {
@Provide('__FormularioForm_unregister')
private unregister (path: string): void {
if (this.registry.has(path)) {
this.registry.remove(path)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [path]: _, ...newProxy } = this.proxy
this.proxy = newProxy
this.registry.delete(path)
this.proxy = unset(this.proxy, path) as Record<string, unknown>
this.emitInput()
}
}
@ -123,16 +108,12 @@ export default class FormularioForm extends Vue {
@Provide('__FormularioForm_set')
private setFieldValue (path: string, value: unknown): void {
if (value === undefined) {
this.proxy = unset(this.proxy, path) as Record<string, unknown>
} else {
this.proxy = set(this.proxy, path, value) as Record<string, unknown>
}
this.proxy = update(this.proxy, path, value)
}
@Provide('__FormularioForm_emitInput')
private emitInput (): void {
this.$emit('input', { ...this.proxy })
this.$emit('input', clone(this.proxy))
}
@Provide('__FormularioForm_emitValidation')
@ -141,9 +122,29 @@ export default class FormularioForm extends Vue {
}
@Watch('state', { deep: true })
private onStateChange (state: Record<string, unknown>): void {
if (this.hasModel && state && typeof state === 'object') {
this.loadState(state)
private onStateChange (newState: Record<string, unknown>): void {
const newProxy = clone(newState)
const oldProxy = this.proxy
let proxyHasChanges = false
this.registry.forEach((field, path) => {
const newValue = get(newState, path, null)
const oldValue = get(oldProxy, path, null)
field.proxy = newValue
if (!deepEquals(newValue, oldValue)) {
field.$emit('input', newValue)
update(newProxy, path, newValue)
proxyHasChanges = true
}
})
this.proxy = newProxy
if (proxyHasChanges) {
this.emitInput()
}
}
@ -155,18 +156,22 @@ export default class FormularioForm extends Vue {
}
public created (): void {
this.syncProxy()
this.$formulario.register(this.id, this)
if (typeof this.state === 'object') {
this.proxy = clone(this.state)
}
}
public beforeDestroy (): void {
this.$formulario.unregister(this.id)
}
public runValidation (): Promise<ViolationsRecord> {
const violations: ViolationsRecord = {}
const runs = this.registry.map((field: FormularioFieldInterface, path: string) => {
return field.runValidation().then(v => { violations[path] = v })
public runValidation (): Promise<Record<string, Violation[]>> {
const runs: Promise<void>[] = []
const violations: Record<string, Violation[]> = {}
this.registry.forEach((field, path) => {
runs.push(field.runValidation().then(v => { violations[path] = v }))
})
return Promise.all(runs).then(() => violations)
@ -178,7 +183,10 @@ export default class FormularioForm extends Vue {
})
}
public setErrors ({ fieldsErrors, formErrors }: { fieldsErrors?: ErrorsRecord; formErrors?: string[] }): void {
public setErrors ({ fieldsErrors, formErrors }: {
fieldsErrors?: Record<string, string[]>;
formErrors?: string[];
}): void {
this.localFieldsErrors = fieldsErrors || {}
this.localFormErrors = formErrors || []
}
@ -186,14 +194,13 @@ export default class FormularioForm extends Vue {
public resetValidation (): void {
this.localFieldsErrors = {}
this.localFormErrors = []
this.registry.forEach((field: FormularioFieldInterface) => {
this.registry.forEach((field: FormularioField) => {
field.resetValidation()
})
}
private onSubmit (): Promise<void> {
return this.runValidation()
.then(violations => {
return this.runValidation().then(violations => {
const hasErrors = Object.keys(violations).some(path => violations[path].length > 0)
if (!hasErrors) {
@ -203,46 +210,5 @@ export default class FormularioForm extends Vue {
}
})
}
private loadState (state: Record<string, unknown>): void {
const paths = Array.from(new Set([
...Object.keys(state),
...Object.keys(this.proxy),
]))
let proxyHasChanges = false
paths.forEach(path => {
if (!this.registry.hasSubset(path)) {
return
}
this.registry.getSubset(path).forEach((field, path) => {
const oldValue = get(this.proxy, path, null)
const newValue = get(state, path, null)
if (!shallowEquals(newValue, oldValue)) {
this.setFieldValue(path, newValue)
proxyHasChanges = true
}
if (!shallowEquals(newValue, field.proxy)) {
field.model = newValue
}
})
})
this.syncProxy()
if (proxyHasChanges) {
this.$emit('input', { ...this.proxy })
}
}
private syncProxy (): void {
if (this.modelIsDefined) {
this.proxy = this.modelCopy
}
}
}
</script>

View File

@ -1,71 +0,0 @@
/**
* @internal
*/
export default class PathRegistry<T> {
private registry: Map<string, T>
constructor () {
this.registry = new Map()
}
has (path: string): boolean {
return this.registry.has(path)
}
hasSubset (path: string): boolean {
for (const itemPath of this.registry.keys()) {
if (itemPath === path || itemPath.includes(path + '.')) {
return true
}
}
return false
}
get (path: string): T | undefined {
return this.registry.get(path)
}
/**
* Returns registry subset by given path - field & descendants
*/
getSubset (path: string): PathRegistry<T> {
const subset: PathRegistry<T> = new PathRegistry()
for (const itemPath of this.registry.keys()) {
if (
itemPath === path ||
itemPath.startsWith(path + '.') ||
itemPath.startsWith(path + '[')
) {
subset.add(itemPath, this.registry.get(itemPath) as T)
}
}
return subset
}
add (path: string, item: T): void {
if (!this.registry.has(path)) {
this.registry.set(path, item)
}
}
remove (path: string): void {
this.registry.delete(path)
}
paths (): IterableIterator<string> {
return this.registry.keys()
}
forEach (callback: (field: T, path: string) => void): void {
this.registry.forEach((field, path) => {
callback(field, path)
})
}
map<U> (mapper: (item: T, path: string) => U): U[] {
return Array.from(this.registry.keys()).map(path => mapper(this.get(path) as T, path))
}
}

View File

@ -1,13 +1,13 @@
import { Violation, ViolationsRecord } from '@/validation/validator'
import Vue from 'vue'
import { Violation } from '@/validation/validator'
export interface FormularioFormInterface {
runValidation(): Promise<ViolationsRecord>;
export interface FormularioForm extends Vue {
runValidation(): Promise<Record<string, Violation[]>>;
resetValidation(): void;
}
export interface FormularioFieldInterface {
export interface FormularioField extends Vue {
hasModel: boolean;
model: unknown;
proxy: unknown;
setErrors(errors: string[]): void;
runValidation(): Promise<Violation[]>;
@ -33,10 +33,58 @@ export interface FormularioFieldModelSetConverter {
export type Empty = undefined | null
export type RecordKey = string | number
export type RecordLike<T> = T[] | Record<RecordKey, T>
export enum TYPE {
ARRAY = 'ARRAY',
BIGINT = 'BIGINT',
BOOLEAN = 'BOOLEAN',
DATE = 'DATE',
FUNCTION = 'FUNCTION',
NUMBER = 'NUMBER',
RECORD = 'RECORD',
STRING = 'STRING',
SYMBOL = 'SYMBOL',
UNDEFINED = 'UNDEFINED',
NULL = 'NULL',
}
export type Scalar = boolean | number | string | symbol | Empty
export function typeOf (value: unknown): string {
switch (typeof value) {
case 'bigint':
return TYPE.BIGINT
case 'boolean':
return TYPE.BOOLEAN
case 'function':
return TYPE.FUNCTION
case 'number':
return TYPE.NUMBER
case 'string':
return TYPE.STRING
case 'symbol':
return TYPE.SYMBOL
case 'undefined':
return TYPE.UNDEFINED
case 'object':
if (value === null) {
return TYPE.NULL
}
if (value instanceof Date) {
return TYPE.DATE
}
if (Array.isArray(value)) {
return TYPE.ARRAY
}
if (value.constructor.name === 'Object') {
return TYPE.RECORD
}
return 'InstanceOf<' + value.constructor.name + '>'
}
throw new Error()
}
export function isRecordLike (value: unknown): boolean {
return typeof value === 'object' && value !== null && ['Array', 'Object'].includes(value.constructor.name)

View File

@ -1,10 +1,4 @@
import has from '@/utils/has'
import {
RecordLike,
Scalar,
isRecordLike,
isScalar,
} from '@/types'
import { isRecordLike, isScalar } from '@/types'
const cloneInstance = <T>(original: T): T => {
return Object.assign(Object.create(Object.getPrototypeOf(original)), original)
@ -14,27 +8,27 @@ const cloneInstance = <T>(original: T): T => {
* A simple (somewhat non-comprehensive) clone function, valid for our use
* case of needing to unbind reactive watchers.
*/
export default function clone (value: unknown): unknown {
export default function clone<T = unknown> (value: T): T {
if (isScalar(value)) {
return value as Scalar
return value
}
if (value instanceof Date) {
return new Date(value)
return new Date(value) as unknown as T
}
if (!isRecordLike(value)) {
return cloneInstance(value)
}
const source: RecordLike<unknown> = value as RecordLike<unknown>
const copy: RecordLike<unknown> = Array.isArray(source) ? [] : {}
for (const key in source) {
if (has(source, key)) {
copy[key] = clone(source[key])
}
if (Array.isArray(value)) {
return value.slice().map(clone) as unknown as T
}
return copy
const source: Record<string, unknown> = value as Record<string, unknown>
return Object.keys(source).reduce((copy, key) => ({
...copy,
[key]: clone(source[key])
}), {}) as unknown as T
}

82
src/utils/compare.ts Normal file
View File

@ -0,0 +1,82 @@
import has from './has'
import { typeOf, TYPE } from '@/types'
export interface EqualPredicate {
(a: unknown, b: unknown): boolean;
}
export function datesEquals (a: Date, b: Date): boolean {
return a.getTime() === b.getTime()
}
export function arraysEquals (
a: unknown[],
b: unknown[],
predicate: EqualPredicate
): boolean {
if (a.length !== b.length) {
return false
}
for (let i = 0; i < a.length; i++) {
if (!predicate(a[i], b[i])) {
return false
}
}
return true
}
export function recordsEquals (
a: Record<string, unknown>,
b: Record<string, unknown>,
predicate: EqualPredicate
): boolean {
if (Object.keys(a).length !== Object.keys(b).length) {
return false
}
for (const prop in a as object) {
if (!has(b, prop) || !predicate(a[prop], b[prop])) {
return false
}
}
return true
}
export function strictEquals (a: unknown, b: unknown): boolean {
return a === b
}
export function equals (a: unknown, b: unknown, predicate: EqualPredicate): boolean {
const typeOfA = typeOf(a)
const typeOfB = typeOf(b)
return typeOfA === typeOfB && (
(typeOfA === TYPE.ARRAY && arraysEquals(
a as unknown[],
b as unknown[],
predicate
)) ||
(typeOfA === TYPE.DATE && datesEquals(a as Date, b as Date)) ||
(typeOfA === TYPE.RECORD && recordsEquals(
a as Record<string, unknown>,
b as Record<string, unknown>,
predicate
)) ||
(typeOfA.includes('InstanceOf') && equals(
Object.entries(a as object),
Object.entries(b as object),
predicate,
))
)
}
export function deepEquals (a: unknown, b: unknown): boolean {
return a === b || equals(a, b, deepEquals)
}
export function shallowEquals (a: unknown, b: unknown): boolean {
return a === b || equals(a, b, strictEquals)
}

10
src/utils/id.ts Normal file
View File

@ -0,0 +1,10 @@
const registry: Map<string, number> = new Map()
export default (prefix: string): string => {
const current = registry.get(prefix) || 0
const next = current + 1
registry.set(prefix, next)
return `${prefix}-${next}`
}

View File

@ -1,7 +1,8 @@
export { default as id } from './id'
export { default as clone } from './clone'
export { default as has } from './has'
export { default as merge } from './merge'
export { get, set, unset } from './access'
export { default as regexForFormat } from './regexForFormat'
export { default as shallowEquals } from './shallowEquals'
export { deepEquals, shallowEquals } from './compare'
export { default as snakeToCamel } from './snakeToCamel'

View File

@ -1,42 +0,0 @@
export function equalsDates (a: Date, b: Date): boolean {
return a.getTime() === b.getTime()
}
export function shallowEqualsRecords (
a: Record<string, unknown>,
b: Record<string, unknown>
): boolean {
const aKeys = Object.keys(a)
const bKeys = Object.keys(b)
if (aKeys.length !== bKeys.length) {
return false
}
if (aKeys.length === 0) {
return a === b
}
return aKeys.reduce((equals: boolean, key: string): boolean => {
return equals && a[key] === b[key]
}, true)
}
export default function shallowEquals (a: unknown, b: unknown): boolean {
if (a === b) {
return true
}
if (!a || !b) {
return false
}
if (a instanceof Date && b instanceof Date) {
return equalsDates(a, b)
}
return shallowEqualsRecords(
a as Record<string, unknown>,
b as Record<string, unknown>
)
}

View File

@ -130,7 +130,7 @@ const rules: Record<string, ValidationRuleFn> = {
* Rule: Value is in an array (stack).
*/
in ({ value }: ValidationContext, ...stack: any[]): boolean {
return stack.some(item => typeof item === 'object' ? shallowEquals(item, value) : item === value)
return stack.some(item => shallowEquals(item, value))
},
/**
@ -198,7 +198,7 @@ const rules: Record<string, ValidationRuleFn> = {
* Rule: Value is not in stack.
*/
not ({ value }: ValidationContext, ...stack: any[]): boolean {
return !stack.some(item => typeof item === 'object' ? shallowEquals(item, value) : item === value)
return !stack.some(item => shallowEquals(item, value))
},
/**

View File

@ -11,8 +11,6 @@ export interface Violation {
context: ValidationContext|null;
}
export type ViolationsRecord = Record<string, Violation[]>
export interface ValidationRuleFn {
(context: ValidationContext, ...args: any[]): Promise<boolean>|boolean;
}

View File

@ -283,7 +283,6 @@ describe('FormularioField', () => {
const form = wrapper.findComponent(FormularioForm)
expect(form.emitted('input')).toEqual([
[{}],
[{ date: new Date('2001-05-12') }],
])
})

View File

@ -1,50 +0,0 @@
import PathRegistry from '@/PathRegistry'
describe('PathRegistry', () => {
test('subset structure', () => {
const registry = new PathRegistry()
const paths = path => Array.from(registry.getSubset(path).paths())
registry.add('name', null)
registry.add('address', [])
registry.add('address[0]', {})
registry.add('address[0].street', 'Baker Street')
registry.add('address[0].building', '221b')
registry.add('address[1]', {})
registry.add('address[1].street', '')
registry.add('address[1].building', '')
expect(paths('name')).toEqual(['name'])
expect(paths('address')).toEqual([
'address',
'address[0]',
'address[0].street',
'address[0].building',
'address[1]',
'address[1].street',
'address[1].building',
])
expect(paths('address[1]')).toEqual([
'address[1]',
'address[1].street',
'address[1].building',
])
registry.remove('address[1]')
expect(paths('address')).toEqual([
'address',
'address[0]',
'address[0].street',
'address[0].building',
'address[1].street',
'address[1].building',
])
expect(paths('address[1]')).toEqual([
'address[1].street',
'address[1].building',
])
})
})

View File

@ -0,0 +1,125 @@
import { deepEquals, shallowEquals } from '@/utils/compare'
class Sample {
constructor() {
this.fieldA = 'fieldA'
this.fieldB = 'fieldB'
}
doSomething () {}
}
describe('compare', () => {
describe('deepEquals', () => {
test.each`
type | a
${'booleans'} | ${false}
${'numbers'} | ${123}
${'strings'} | ${'hello'}
${'symbols'} | ${Symbol(123)}
${'undefined'} | ${undefined}
${'null'} | ${null}
${'array'} | ${[1, 2, 3]}
${'pojo'} | ${{ a: 1, b: 2 }}
${'empty array'} | ${[]}
${'empty pojo'} | ${{}}
${'date'} | ${new Date()}
`('A=A check on $type', ({ a }) => {
expect(deepEquals(a, a)).toBe(true)
})
test.each`
a | b | expected
${[]} | ${[]} | ${true}
${[1, 2, 3]} | ${[1, 2, 3]} | ${true}
${[1, 2, { a: 1 }]} | ${[1, 2, { a: 1 }]} | ${true}
${[1, 2, { a: 1 }]} | ${[1, 2, { a: 2 }]} | ${false}
${[1, 2, 3]} | ${[1, 2, 4]} | ${false}
${[1, 2, 3]} | ${[1, 2]} | ${false}
${[]} | ${[1, 2]} | ${false}
${{}} | ${{}} | ${true}
${{ a: 1, b: 2 }} | ${{ a: 1, b: 2 }} | ${true}
${{ a: 1, b: 2 }} | ${{ a: 1 }} | ${false}
${{ a: {} }} | ${{ a: {} }} | ${true}
${{ a: { b: 1 } }} | ${{ a: { b: 1 } }} | ${true}
${{ a: { b: 1 } }} | ${{ a: { b: 2 } }} | ${false}
${new Date()} | ${new Date()} | ${true}
`('A=B & B=A check: A=$a, B=$b, expected: $expected', ({ a, b, expected }) => {
expect(deepEquals(a, b)).toBe(expected)
expect(deepEquals(b, a)).toBe(expected)
})
test('A=B & B=A check for instances', () => {
const a = new Sample()
const b = new Sample()
expect(deepEquals(a, b)).toBe(true)
expect(deepEquals(b, a)).toBe(true)
b.fieldA += '~'
expect(deepEquals(a, b)).toBe(false)
expect(deepEquals(b, a)).toBe(false)
})
test('A=B & B=A check for instances with nesting', () => {
const a = new Sample()
const b = new Sample()
a.fieldA = new Sample()
b.fieldA = new Sample()
expect(deepEquals(a, b)).toBe(true)
expect(deepEquals(b, a)).toBe(true)
b.fieldA.fieldA += '~'
expect(deepEquals(a, b)).toBe(false)
expect(deepEquals(b, a)).toBe(false)
})
})
describe('shallowEquals', () => {
test.each`
type | a
${'booleans'} | ${false}
${'numbers'} | ${123}
${'strings'} | ${'hello'}
${'symbols'} | ${Symbol(123)}
${'undefined'} | ${undefined}
${'null'} | ${null}
${'array'} | ${[1, 2, 3]}
${'pojo'} | ${{ a: 1, b: 2 }}
${'empty array'} | ${[]}
${'empty pojo'} | ${{}}
${'date'} | ${new Date()}
`('A=A check on $type', ({ a }) => {
expect(shallowEquals(a, a)).toBe(true)
})
test.each`
a | b | expected
${[]} | ${[]} | ${true}
${[1, 2, 3]} | ${[1, 2, 3]} | ${true}
${[1, 2, 3]} | ${[1, 2, 4]} | ${false}
${[1, 2, 3]} | ${[1, 2]} | ${false}
${[]} | ${[1, 2]} | ${false}
${{}} | ${{}} | ${true}
${{ a: 1, b: 2 }} | ${{ a: 1, b: 2 }} | ${true}
${{ a: 1, b: 2 }} | ${{ a: 1 }} | ${false}
${{ a: {} }} | ${{ a: {} }} | ${false}
${new Date()} | ${new Date()} | ${true}
`('A=B & B=A check: A=$a, B=$b, expected: $expected', ({ a, b, expected }) => {
expect(shallowEquals(a, b)).toBe(expected)
expect(shallowEquals(b, a)).toBe(expected)
})
test('A=B & B=A check for instances', () => {
const a = new Sample()
const b = new Sample()
expect(shallowEquals(a, b)).toBe(false)
expect(shallowEquals(b, a)).toBe(false)
})
})
})