fix(v-model): handle more edge cases in looseEqual() (#379)

This commit is contained in:
Jacob Müller 2020-07-15 15:37:51 +02:00 committed by GitHub
parent 379a8af288
commit fe1b27b7f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 264 additions and 31 deletions

View File

@ -0,0 +1,221 @@
import { looseEqual } from '../src'
describe('utils/looseEqual', () => {
test('compares booleans correctly', () => {
expect(looseEqual(true, true)).toBe(true)
expect(looseEqual(false, false)).toBe(true)
expect(looseEqual(true, false)).toBe(false)
expect(looseEqual(true, 1)).toBe(false)
expect(looseEqual(false, 0)).toBe(false)
})
test('compares strings correctly', () => {
const text = 'Lorem ipsum'
const number = 1
const bool = true
expect(looseEqual(text, text)).toBe(true)
expect(looseEqual(text, text.slice(0, -1))).toBe(false)
expect(looseEqual(String(number), number)).toBe(true)
expect(looseEqual(String(bool), bool)).toBe(true)
})
test('compares numbers correctly', () => {
const number = 100
const decimal = 2.5
const multiplier = 1.0000001
expect(looseEqual(number, number)).toBe(true)
expect(looseEqual(number, number - 1)).toBe(false)
expect(looseEqual(decimal, decimal)).toBe(true)
expect(looseEqual(decimal, decimal * multiplier)).toBe(false)
expect(looseEqual(number, number * multiplier)).toBe(false)
expect(looseEqual(multiplier, multiplier)).toBe(true)
})
test('compares dates correctly', () => {
const date1 = new Date(2019, 1, 2, 3, 4, 5, 6)
const date2 = new Date(2019, 1, 2, 3, 4, 5, 6)
const date3 = new Date(2019, 1, 2, 3, 4, 5, 7)
const date4 = new Date(2219, 1, 2, 3, 4, 5, 6)
// Identical date object references
expect(looseEqual(date1, date1)).toBe(true)
// Different date references with identical values
expect(looseEqual(date1, date2)).toBe(true)
// Dates with slightly different time (ms)
expect(looseEqual(date1, date3)).toBe(false)
// Dates with different year
expect(looseEqual(date1, date4)).toBe(false)
})
test('compares files correctly', () => {
const date1 = new Date(2019, 1, 2, 3, 4, 5, 6)
const date2 = new Date(2019, 1, 2, 3, 4, 5, 7)
const file1 = new File([''], 'filename.txt', {
type: 'text/plain',
lastModified: date1.getTime(),
})
const file2 = new File([''], 'filename.txt', {
type: 'text/plain',
lastModified: date1.getTime(),
})
const file3 = new File([''], 'filename.txt', {
type: 'text/plain',
lastModified: date2.getTime(),
})
const file4 = new File([''], 'filename.csv', {
type: 'text/csv',
lastModified: date1.getTime(),
})
const file5 = new File(['abcdef'], 'filename.txt', {
type: 'text/plain',
lastModified: date1.getTime(),
})
const file6 = new File(['12345'], 'filename.txt', {
type: 'text/plain',
lastModified: date1.getTime(),
})
// Identical file object references
expect(looseEqual(file1, file1)).toBe(true)
// Different file references with identical values
expect(looseEqual(file1, file2)).toBe(true)
// Files with slightly different dates
expect(looseEqual(file1, file3)).toBe(false)
// Two different file types
expect(looseEqual(file1, file4)).toBe(false)
// Two files with same name, modified date, but different content
expect(looseEqual(file5, file6)).toBe(false)
})
test('compares arrays correctly', () => {
const arr1 = [1, 2, 3, 4]
const arr2 = [1, 2, 3, '4']
const arr3 = [1, 2, 3, 4, 5]
const arr4 = [1, 2, 3, 4, { a: 5 }]
// Identical array references
expect(looseEqual(arr1, arr1)).toBe(true)
// Different array references with identical values
expect(looseEqual(arr1, arr1.slice())).toBe(true)
expect(looseEqual(arr4, arr4.slice())).toBe(true)
// Array with one value different (loose)
expect(looseEqual(arr1, arr2)).toBe(true)
// Array with one value different
expect(looseEqual(arr3, arr4)).toBe(false)
// Arrays with different lengths
expect(looseEqual(arr1, arr3)).toBe(false)
// Arrays with values in different order
expect(looseEqual(arr1, arr1.slice().reverse())).toBe(false)
})
test('compares RegExp correctly', () => {
const rx1 = /^foo$/
const rx2 = /^foo$/
const rx3 = /^bar$/
const rx4 = /^bar$/i
// Identical regex references
expect(looseEqual(rx1, rx1)).toBe(true)
// Different regex references with identical values
expect(looseEqual(rx1, rx2)).toBe(true)
// Different regex
expect(looseEqual(rx1, rx3)).toBe(false)
// Same regex with different options
expect(looseEqual(rx3, rx4)).toBe(false)
})
test('compares objects correctly', () => {
const obj1 = { foo: 'bar' }
const obj2 = { foo: 'bar1' }
const obj3 = { a: 1, b: 2, c: 3 }
const obj4 = { b: 2, c: 3, a: 1 }
const obj5 = { ...obj4, z: 999 }
const nestedObj1 = { ...obj1, bar: [{ ...obj1 }, { ...obj1 }] }
const nestedObj2 = { ...obj1, bar: [{ ...obj1 }, { ...obj2 }] }
// Identical object references
expect(looseEqual(obj1, obj1)).toBe(true)
// Two objects with identical keys/values
expect(looseEqual(obj1, { ...obj1 })).toBe(true)
// Different key values
expect(looseEqual(obj1, obj2)).toBe(false)
// Keys in different orders
expect(looseEqual(obj3, obj4)).toBe(true)
// One object has additional key
expect(looseEqual(obj4, obj5)).toBe(false)
// Identical object references with nested array
expect(looseEqual(nestedObj1, nestedObj1)).toBe(true)
// Identical object definitions with nested array
expect(looseEqual(nestedObj1, { ...nestedObj1 })).toBe(true)
// Object definitions with nested array (which has different order)
expect(looseEqual(nestedObj1, nestedObj2)).toBe(false)
})
test('compares different types correctly', () => {
const obj1 = {}
const obj2 = { a: 1 }
const obj3 = { 0: 0, 1: 1, 2: 2 }
const arr1: any[] = []
const arr2 = [1]
const arr3 = [0, 1, 2]
const date1 = new Date(2019, 1, 2, 3, 4, 5, 6)
const file1 = new File([''], 'filename.txt', {
type: 'text/plain',
lastModified: date1.getTime(),
})
expect(looseEqual(123, '123')).toBe(true)
expect(looseEqual(123, new Date(123))).toBe(false)
expect(looseEqual(`123`, new Date(123))).toBe(false)
expect(looseEqual([1, 2, 3], '1,2,3')).toBe(false)
expect(looseEqual(obj1, arr1)).toBe(false)
expect(looseEqual(obj2, arr2)).toBe(false)
expect(looseEqual(obj1, '[object Object]')).toBe(false)
expect(looseEqual(arr1, '[object Array]')).toBe(false)
expect(looseEqual(obj1, date1)).toBe(false)
expect(looseEqual(obj2, date1)).toBe(false)
expect(looseEqual(arr1, date1)).toBe(false)
expect(looseEqual(arr2, date1)).toBe(false)
expect(looseEqual(obj2, file1)).toBe(false)
expect(looseEqual(arr2, file1)).toBe(false)
expect(looseEqual(date1, file1)).toBe(false)
// Special case where an object's keys are the same as keys (indexes) of an array
expect(looseEqual(obj3, arr3)).toBe(false)
})
test('compares null and undefined values correctly', () => {
expect(looseEqual(null, null)).toBe(true)
expect(looseEqual(undefined, undefined)).toBe(true)
expect(looseEqual(void 0, undefined)).toBe(true)
expect(looseEqual(null, undefined)).toBe(false)
expect(looseEqual(null, void 0)).toBe(false)
expect(looseEqual(null, '')).toBe(false)
expect(looseEqual(null, false)).toBe(false)
expect(looseEqual(undefined, false)).toBe(false)
})
test('compares sparse arrays correctly', () => {
// The following arrays all have a length of 3
// But the first two are "sparse"
const arr1 = []
arr1[2] = true
const arr2 = []
arr2[2] = true
const arr3 = [false, false, true]
const arr4 = [undefined, undefined, true]
// This one is also sparse (missing index 1)
const arr5 = []
arr5[0] = arr5[2] = true
expect(looseEqual(arr1, arr2)).toBe(true)
expect(looseEqual(arr2, arr1)).toBe(true)
expect(looseEqual(arr1, arr3)).toBe(false)
expect(looseEqual(arr3, arr1)).toBe(false)
expect(looseEqual(arr1, arr4)).toBe(true)
expect(looseEqual(arr4, arr1)).toBe(true)
expect(looseEqual(arr1, arr5)).toBe(false)
expect(looseEqual(arr5, arr1)).toBe(false)
})
})

View File

@ -56,6 +56,7 @@ export const hasOwn = (
): key is keyof typeof val => hasOwnProperty.call(val, key) ): key is keyof typeof val => hasOwnProperty.call(val, key)
export const isArray = Array.isArray export const isArray = Array.isArray
export const isDate = (val: unknown): val is Date => val instanceof Date
export const isFunction = (val: unknown): val is Function => export const isFunction = (val: unknown): val is Function =>
typeof val === 'function' typeof val === 'function'
export const isString = (val: unknown): val is string => typeof val === 'string' export const isString = (val: unknown): val is string => typeof val === 'string'

View File

@ -1,40 +1,51 @@
import { isObject, isArray } from './' import { isArray, isDate, isObject } from './'
function looseCompareArrays(a: any[], b: any[]) {
if (a.length !== b.length) return false
let equal = true
for (let i = 0; equal && i < a.length; i++) {
equal = looseEqual(a[i], b[i])
}
return equal
}
export function looseEqual(a: any, b: any): boolean { export function looseEqual(a: any, b: any): boolean {
if (a === b) return true if (a === b) return true
const isObjectA = isObject(a) let aValidType = isDate(a)
const isObjectB = isObject(b) let bValidType = isDate(b)
if (isObjectA && isObjectB) { if (aValidType || bValidType) {
try { return aValidType && bValidType ? a.getTime() === b.getTime() : false
const isArrayA = isArray(a) }
const isArrayB = isArray(b) aValidType = isArray(a)
if (isArrayA && isArrayB) { bValidType = isArray(b)
return ( if (aValidType || bValidType) {
a.length === b.length && return aValidType && bValidType ? looseCompareArrays(a, b) : false
a.every((e: any, i: any) => looseEqual(e, b[i])) }
) aValidType = isObject(a)
} else if (a instanceof Date && b instanceof Date) { bValidType = isObject(b)
return a.getTime() === b.getTime() if (aValidType || bValidType) {
} else if (!isArrayA && !isArrayB) { /* istanbul ignore if: this if will probably never be called */
const keysA = Object.keys(a) if (!aValidType || !bValidType) {
const keysB = Object.keys(b)
return (
keysA.length === keysB.length &&
keysA.every(key => looseEqual(a[key], b[key]))
)
} else {
/* istanbul ignore next */
return false
}
} catch (e) {
/* istanbul ignore next */
return false return false
} }
} else if (!isObjectA && !isObjectB) { const aKeysCount = Object.keys(a).length
return String(a) === String(b) const bKeysCount = Object.keys(b).length
} else { if (aKeysCount !== bKeysCount) {
return false return false
}
for (const key in a) {
const aHasKey = a.hasOwnProperty(key)
const bHasKey = b.hasOwnProperty(key)
if (
(aHasKey && !bHasKey) ||
(!aHasKey && bHasKey) ||
!looseEqual(a[key], b[key])
) {
return false
}
}
} }
return String(a) === String(b)
} }
export function looseIndexOf(arr: any[], val: any): number { export function looseIndexOf(arr: any[], val: any): number {