]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(v-model): handle more edge cases in `looseEqual()` (#379)
authorJacob Müller <jacob.mueller.elz@gmail.com>
Wed, 15 Jul 2020 13:37:51 +0000 (15:37 +0200)
committerGitHub <noreply@github.com>
Wed, 15 Jul 2020 13:37:51 +0000 (09:37 -0400)
packages/shared/__tests__/looseEqual.spec.ts [new file with mode: 0644]
packages/shared/src/index.ts
packages/shared/src/looseEqual.ts

diff --git a/packages/shared/__tests__/looseEqual.spec.ts b/packages/shared/__tests__/looseEqual.spec.ts
new file mode 100644 (file)
index 0000000..fe321cd
--- /dev/null
@@ -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)
+  })
+})
index 0119f11591f3e497cc56ccfd4f5c2eb0446ab54a..7caf7d7867969c27448fbc2e4f77f32ef87733b9 100644 (file)
@@ -56,6 +56,7 @@ export const hasOwn = (
 ): key is keyof typeof val => hasOwnProperty.call(val, key)
 
 export const isArray = Array.isArray
+export const isDate = (val: unknown): val is Date => val instanceof Date
 export const isFunction = (val: unknown): val is Function =>
   typeof val === 'function'
 export const isString = (val: unknown): val is string => typeof val === 'string'
index bd58332ddd6362e4759b2c427723f347d8a3fdd9..030f0338b30e395e0496ba0dff42546e05c27e1a 100644 (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 {
   if (a === b) return true
-  const isObjectA = isObject(a)
-  const isObjectB = isObject(b)
-  if (isObjectA && isObjectB) {
-    try {
-      const isArrayA = isArray(a)
-      const isArrayB = isArray(b)
-      if (isArrayA && isArrayB) {
-        return (
-          a.length === b.length &&
-          a.every((e: any, i: any) => looseEqual(e, b[i]))
-        )
-      } else if (a instanceof Date && b instanceof Date) {
-        return a.getTime() === b.getTime()
-      } else if (!isArrayA && !isArrayB) {
-        const keysA = Object.keys(a)
-        const keysB = Object.keys(b)
-        return (
-          keysA.length === keysB.length &&
-          keysA.every(key => looseEqual(a[key], b[key]))
-        )
-      } else {
-        /* istanbul ignore next */
+  let aValidType = isDate(a)
+  let bValidType = isDate(b)
+  if (aValidType || bValidType) {
+    return aValidType && bValidType ? a.getTime() === b.getTime() : false
+  }
+  aValidType = isArray(a)
+  bValidType = isArray(b)
+  if (aValidType || bValidType) {
+    return aValidType && bValidType ? looseCompareArrays(a, b) : false
+  }
+  aValidType = isObject(a)
+  bValidType = isObject(b)
+  if (aValidType || bValidType) {
+    /* istanbul ignore if: this if will probably never be called */
+    if (!aValidType || !bValidType) {
+      return false
+    }
+    const aKeysCount = Object.keys(a).length
+    const bKeysCount = Object.keys(b).length
+    if (aKeysCount !== bKeysCount) {
+      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
       }
-    } catch (e) {
-      /* istanbul ignore next */
-      return false
     }
-  } else if (!isObjectA && !isObjectB) {
-    return String(a) === String(b)
-  } else {
-    return false
   }
+  return String(a) === String(b)
 }
 
 export function looseIndexOf(arr: any[], val: any): number {