Skip to content

Commit 401d3e9

Browse files
jacobmllr95tmorehouse
authored andcommitted
fix(utils): Make looseEqual() util handle File comparison correctly (bootstrap-vue#2640)
1 parent 00f5a7f commit 401d3e9

File tree

2 files changed

+225
-34
lines changed

2 files changed

+225
-34
lines changed

src/utils/loose-equal.js

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import { isArray } from './array'
22
import { keys } from './object'
33

4+
function isDate(obj) {
5+
return obj instanceof Date
6+
}
7+
8+
function isFile(obj) {
9+
return obj instanceof File
10+
}
11+
412
/**
513
* Quick object check - this is primarily used to tell
614
* Objects from primitive values when we know the value
715
* is a JSON-compliant type.
16+
* Note object could be a complex type like array, date, etc.
817
*/
918
function isObject(obj) {
1019
return obj !== null && typeof obj === 'object'
@@ -16,44 +25,53 @@ function isObject(obj) {
1625
* Returns boolean true or false
1726
*/
1827
function looseEqual(a, b) {
19-
if (a === b) return true
20-
const isObjectA = isObject(a)
21-
const isObjectB = isObject(b)
22-
if (isObjectA && isObjectB) {
23-
try {
24-
const isArrayA = isArray(a)
25-
const isArrayB = isArray(b)
26-
if (isArrayA && isArrayB) {
27-
return (
28-
a.length === b.length &&
29-
a.every((e, i) => {
30-
return looseEqual(e, b[i])
31-
})
32-
)
33-
} else if (a instanceof Date && b instanceof Date) {
34-
return a.getTime() === b.getTime()
35-
} else if (!isArrayA && !isArrayB) {
36-
const keysA = keys(a)
37-
const keysB = keys(b)
38-
return (
39-
keysA.length === keysB.length &&
40-
keysA.every(key => {
41-
return looseEqual(a[key], b[key])
42-
})
43-
)
44-
} else {
45-
/* istanbul ignore next */
28+
if (a === b) {
29+
return true
30+
}
31+
if (typeof a !== typeof b) {
32+
return false
33+
}
34+
let validTypesCount = [isDate(a), isDate(b)].filter(Boolean).length
35+
if (validTypesCount > 0) {
36+
return validTypesCount === 2 ? a.getTime() === b.getTime() : false
37+
}
38+
validTypesCount = [isFile(a), isFile(b)].filter(Boolean).length
39+
if (validTypesCount > 0) {
40+
return validTypesCount === 2 ? a === b : false
41+
}
42+
validTypesCount = [isArray(a), isArray(b)].filter(Boolean).length
43+
if (validTypesCount > 0) {
44+
return validTypesCount === 2
45+
? a.length === b.length && a.every((e, i) => looseEqual(e, b[i]))
46+
: false
47+
}
48+
validTypesCount = [isObject(a), isObject(b)].filter(Boolean).length
49+
if (validTypesCount > 0) {
50+
/* istanbul ignore if: this if will probably never be called */
51+
if (validTypesCount === 1) {
52+
return false
53+
}
54+
const aKeysCount = keys(a).length
55+
const bKeysCount = keys(b).length
56+
if (aKeysCount !== bKeysCount) {
57+
return false
58+
}
59+
if (aKeysCount === 0 && bKeysCount === 0) {
60+
return String(a) === String(b)
61+
}
62+
// Using for loop over `Object.keys()` here since some class
63+
// keys are not handled correctly otherwise
64+
for (const key in a) {
65+
if (
66+
[a.hasOwnProperty(key), b.hasOwnProperty(key)].filter(Boolean).length === 1 ||
67+
!looseEqual(a[key], b[key])
68+
) {
4669
return false
4770
}
48-
} catch (e) {
49-
/* istanbul ignore next */
50-
return false
5171
}
52-
} else if (!isObjectA && !isObjectB) {
53-
return String(a) === String(b)
54-
} else {
55-
return false
72+
return true
5673
}
74+
return false
5775
}
5876

5977
export default looseEqual

src/utils/loose-equal.spec.js

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import looseEqual from './loose-equal'
2+
3+
describe('looseEqual', async () => {
4+
it('compares booleans correctly', async () => {
5+
expect(looseEqual(true, true)).toBe(true)
6+
expect(looseEqual(false, false)).toBe(true)
7+
expect(looseEqual(true, false)).toBe(false)
8+
expect(looseEqual(true, true)).toBe(true)
9+
expect(looseEqual(true, 1)).toBe(false)
10+
expect(looseEqual(false, 0)).toBe(false)
11+
})
12+
13+
it('compares strings correctly', async () => {
14+
const text = 'Lorem ipsum'
15+
const number = 1
16+
const bool = true
17+
18+
expect(looseEqual(text, text)).toBe(true)
19+
expect(looseEqual(text, text.slice(0, -1))).toBe(false)
20+
expect(looseEqual(String(number), number)).toBe(false)
21+
expect(looseEqual(String(bool), bool)).toBe(false)
22+
})
23+
24+
it('compares numbers correctly', async () => {
25+
const number = 100
26+
const decimal = 2.5
27+
const multiplier = 1.0000001
28+
29+
expect(looseEqual(number, number)).toBe(true)
30+
expect(looseEqual(number, number - 1)).toBe(false)
31+
expect(looseEqual(decimal, decimal)).toBe(true)
32+
expect(looseEqual(decimal, decimal * multiplier)).toBe(false)
33+
expect(looseEqual(number, number * multiplier)).toBe(false)
34+
expect(looseEqual(multiplier, multiplier)).toBe(true)
35+
})
36+
37+
it('compares dates correctly', async () => {
38+
const date1 = new Date(2019, 1, 2, 3, 4, 5, 6)
39+
const date2 = new Date(2019, 1, 2, 3, 4, 5, 6)
40+
const date3 = new Date(2019, 1, 2, 3, 4, 5, 7)
41+
const date4 = new Date(2219, 1, 2, 3, 4, 5, 6)
42+
43+
// Identical date object references
44+
expect(looseEqual(date1, date1)).toBe(true)
45+
// Different date references with identical values
46+
expect(looseEqual(date1, date2)).toBe(true)
47+
// Dates with slightly different time (ms)
48+
expect(looseEqual(date1, date3)).toBe(false)
49+
// Dates with different year
50+
expect(looseEqual(date1, date4)).toBe(false)
51+
})
52+
53+
it('compares files correctly', async () => {
54+
const date1 = new Date(2019, 1, 2, 3, 4, 5, 6)
55+
const date2 = new Date(2019, 1, 2, 3, 4, 5, 7)
56+
const file1 = new File([''], 'filename.txt', { type: 'text/plain', lastModified: date1 })
57+
const file2 = new File([''], 'filename.txt', { type: 'text/plain', lastModified: date1 })
58+
const file3 = new File([''], 'filename.txt', { type: 'text/plain', lastModified: date2 })
59+
const file4 = new File([''], 'filename.csv', { type: 'text/csv', lastModified: date1 })
60+
61+
// Identical file object references
62+
expect(looseEqual(file1, file1)).toBe(true)
63+
// Different file references with identical values
64+
expect(looseEqual(file1, file2)).toBe(false)
65+
// Files with slightly different dates
66+
expect(looseEqual(file1, file3)).toBe(false)
67+
// Two different file types
68+
expect(looseEqual(file1, file4)).toBe(false)
69+
})
70+
71+
it('compares arrays correctly', async () => {
72+
const arr1 = [1, 2, 3, 4]
73+
const arr2 = [1, 2, 3, '4']
74+
const arr3 = [1, 2, 3, 4, 5]
75+
const arr4 = [1, 2, 3, 4, { a: 5 }]
76+
77+
// Identical array references
78+
expect(looseEqual(arr1, arr1)).toBe(true)
79+
// Different array references with identical values
80+
expect(looseEqual(arr1, arr1.slice())).toBe(true)
81+
expect(looseEqual(arr4, arr4.slice())).toBe(true)
82+
// Array with one value different
83+
expect(looseEqual(arr1, arr2)).toBe(false)
84+
expect(looseEqual(arr3, arr4)).toBe(false)
85+
// Arrays with different lengths
86+
expect(looseEqual(arr1, arr3)).toBe(false)
87+
// Arrays with values in different order
88+
expect(looseEqual(arr1, arr1.slice().reverse())).toBe(false)
89+
})
90+
91+
it('compares RegExp correctly', async () => {
92+
const rx1 = /^foo$/
93+
const rx2 = /^foo$/
94+
const rx3 = /^bar$/
95+
const rx4 = /^bar$/i
96+
97+
// Identical regex references
98+
expect(looseEqual(rx1, rx1)).toBe(true)
99+
// Different regex references with identical values
100+
expect(looseEqual(rx1, rx2)).toBe(true)
101+
// Different regex
102+
expect(looseEqual(rx1, rx3)).toBe(false)
103+
// Same regex with different options
104+
expect(looseEqual(rx3, rx4)).toBe(false)
105+
})
106+
107+
it('compares objects correctly', async () => {
108+
const obj1 = { foo: 'bar' }
109+
const obj2 = { foo: 'bar1' }
110+
const obj3 = { a: 1, b: 2, c: 3 }
111+
const obj4 = { b: 2, c: 3, a: 1 }
112+
const obj5 = { ...obj4, z: 999 }
113+
const nestedObj1 = { ...obj1, bar: [{ ...obj1 }, { ...obj1 }] }
114+
const nestedObj2 = { ...obj1, bar: [{ ...obj1 }, { ...obj2 }] }
115+
116+
// Identical object references
117+
expect(looseEqual(obj1, obj1)).toBe(true)
118+
// Two objects with identical keys/values
119+
expect(looseEqual(obj1, { ...obj1 })).toBe(true)
120+
// Different key values
121+
expect(looseEqual(obj1, obj2)).toBe(false)
122+
// Keys in different orders
123+
expect(looseEqual(obj3, obj4)).toBe(true)
124+
// One object has additional key
125+
expect(looseEqual(obj4, obj5)).toBe(false)
126+
// Identical object references with nested array
127+
expect(looseEqual(nestedObj1, nestedObj1)).toBe(true)
128+
// Identical object definitions with nested array
129+
expect(looseEqual(nestedObj1, { ...nestedObj1 })).toBe(true)
130+
// Object definitions with nested array (which has different order)
131+
expect(looseEqual(nestedObj1, nestedObj2)).toBe(false)
132+
})
133+
134+
it('compares different types correctly', async () => {
135+
const obj1 = {}
136+
const obj2 = { a: 1 }
137+
const obj3 = { 0: 0, 1: 1, 2: 2 }
138+
const arr1 = []
139+
const arr2 = [1]
140+
const arr3 = [0, 1, 2]
141+
const date1 = new Date(2019, 1, 2, 3, 4, 5, 6)
142+
const file1 = new File([''], 'filename.txt', { type: 'text/plain', lastModified: date1 })
143+
144+
expect(looseEqual(123, '123')).toBe(false)
145+
expect(looseEqual(123, new Date(123))).toBe(false)
146+
expect(looseEqual(`123`, new Date(123))).toBe(false)
147+
expect(looseEqual([1, 2, 3], '1,2,3')).toBe(false)
148+
expect(looseEqual(obj1, arr1)).toBe(false)
149+
expect(looseEqual(obj2, arr2)).toBe(false)
150+
expect(looseEqual(obj1, '[object Object]')).toBe(false)
151+
expect(looseEqual(arr1, '[object Array]')).toBe(false)
152+
expect(looseEqual(obj1, date1)).toBe(false)
153+
expect(looseEqual(obj2, date1)).toBe(false)
154+
expect(looseEqual(arr1, date1)).toBe(false)
155+
expect(looseEqual(arr2, date1)).toBe(false)
156+
expect(looseEqual(obj2, file1)).toBe(false)
157+
expect(looseEqual(arr2, file1)).toBe(false)
158+
expect(looseEqual(date1, file1)).toBe(false)
159+
// Special case where an object's keys are the same as keys (indexes) of an array
160+
expect(looseEqual(obj3, arr3)).toBe(false)
161+
})
162+
163+
it('compares null and undefs correctly', async () => {
164+
expect(looseEqual(null, null)).toBe(true)
165+
expect(looseEqual(undefined, undefined)).toBe(true)
166+
expect(looseEqual(void 0, undefined)).toBe(true)
167+
expect(looseEqual(null, undefined)).toBe(false)
168+
expect(looseEqual(null, void 0)).toBe(false)
169+
expect(looseEqual(null, '')).toBe(false)
170+
expect(looseEqual(null, false)).toBe(false)
171+
expect(looseEqual(undefined, false)).toBe(false)
172+
})
173+
})

0 commit comments

Comments
 (0)