Skip to content

Commit fe1b27b

Browse files
authored
fix(v-model): handle more edge cases in looseEqual() (#379)
1 parent 379a8af commit fe1b27b

File tree

3 files changed

+263
-30
lines changed

3 files changed

+263
-30
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { looseEqual } from '../src'
2+
3+
describe('utils/looseEqual', () => {
4+
test('compares booleans correctly', () => {
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, 1)).toBe(false)
9+
expect(looseEqual(false, 0)).toBe(false)
10+
})
11+
12+
test('compares strings correctly', () => {
13+
const text = 'Lorem ipsum'
14+
const number = 1
15+
const bool = true
16+
17+
expect(looseEqual(text, text)).toBe(true)
18+
expect(looseEqual(text, text.slice(0, -1))).toBe(false)
19+
expect(looseEqual(String(number), number)).toBe(true)
20+
expect(looseEqual(String(bool), bool)).toBe(true)
21+
})
22+
23+
test('compares numbers correctly', () => {
24+
const number = 100
25+
const decimal = 2.5
26+
const multiplier = 1.0000001
27+
28+
expect(looseEqual(number, number)).toBe(true)
29+
expect(looseEqual(number, number - 1)).toBe(false)
30+
expect(looseEqual(decimal, decimal)).toBe(true)
31+
expect(looseEqual(decimal, decimal * multiplier)).toBe(false)
32+
expect(looseEqual(number, number * multiplier)).toBe(false)
33+
expect(looseEqual(multiplier, multiplier)).toBe(true)
34+
})
35+
36+
test('compares dates correctly', () => {
37+
const date1 = new Date(2019, 1, 2, 3, 4, 5, 6)
38+
const date2 = new Date(2019, 1, 2, 3, 4, 5, 6)
39+
const date3 = new Date(2019, 1, 2, 3, 4, 5, 7)
40+
const date4 = new Date(2219, 1, 2, 3, 4, 5, 6)
41+
42+
// Identical date object references
43+
expect(looseEqual(date1, date1)).toBe(true)
44+
// Different date references with identical values
45+
expect(looseEqual(date1, date2)).toBe(true)
46+
// Dates with slightly different time (ms)
47+
expect(looseEqual(date1, date3)).toBe(false)
48+
// Dates with different year
49+
expect(looseEqual(date1, date4)).toBe(false)
50+
})
51+
52+
test('compares files correctly', () => {
53+
const date1 = new Date(2019, 1, 2, 3, 4, 5, 6)
54+
const date2 = new Date(2019, 1, 2, 3, 4, 5, 7)
55+
const file1 = new File([''], 'filename.txt', {
56+
type: 'text/plain',
57+
lastModified: date1.getTime(),
58+
})
59+
const file2 = new File([''], 'filename.txt', {
60+
type: 'text/plain',
61+
lastModified: date1.getTime(),
62+
})
63+
const file3 = new File([''], 'filename.txt', {
64+
type: 'text/plain',
65+
lastModified: date2.getTime(),
66+
})
67+
const file4 = new File([''], 'filename.csv', {
68+
type: 'text/csv',
69+
lastModified: date1.getTime(),
70+
})
71+
const file5 = new File(['abcdef'], 'filename.txt', {
72+
type: 'text/plain',
73+
lastModified: date1.getTime(),
74+
})
75+
const file6 = new File(['12345'], 'filename.txt', {
76+
type: 'text/plain',
77+
lastModified: date1.getTime(),
78+
})
79+
80+
// Identical file object references
81+
expect(looseEqual(file1, file1)).toBe(true)
82+
// Different file references with identical values
83+
expect(looseEqual(file1, file2)).toBe(true)
84+
// Files with slightly different dates
85+
expect(looseEqual(file1, file3)).toBe(false)
86+
// Two different file types
87+
expect(looseEqual(file1, file4)).toBe(false)
88+
// Two files with same name, modified date, but different content
89+
expect(looseEqual(file5, file6)).toBe(false)
90+
})
91+
92+
test('compares arrays correctly', () => {
93+
const arr1 = [1, 2, 3, 4]
94+
const arr2 = [1, 2, 3, '4']
95+
const arr3 = [1, 2, 3, 4, 5]
96+
const arr4 = [1, 2, 3, 4, { a: 5 }]
97+
98+
// Identical array references
99+
expect(looseEqual(arr1, arr1)).toBe(true)
100+
// Different array references with identical values
101+
expect(looseEqual(arr1, arr1.slice())).toBe(true)
102+
expect(looseEqual(arr4, arr4.slice())).toBe(true)
103+
// Array with one value different (loose)
104+
expect(looseEqual(arr1, arr2)).toBe(true)
105+
// Array with one value different
106+
expect(looseEqual(arr3, arr4)).toBe(false)
107+
// Arrays with different lengths
108+
expect(looseEqual(arr1, arr3)).toBe(false)
109+
// Arrays with values in different order
110+
expect(looseEqual(arr1, arr1.slice().reverse())).toBe(false)
111+
})
112+
113+
test('compares RegExp correctly', () => {
114+
const rx1 = /^foo$/
115+
const rx2 = /^foo$/
116+
const rx3 = /^bar$/
117+
const rx4 = /^bar$/i
118+
119+
// Identical regex references
120+
expect(looseEqual(rx1, rx1)).toBe(true)
121+
// Different regex references with identical values
122+
expect(looseEqual(rx1, rx2)).toBe(true)
123+
// Different regex
124+
expect(looseEqual(rx1, rx3)).toBe(false)
125+
// Same regex with different options
126+
expect(looseEqual(rx3, rx4)).toBe(false)
127+
})
128+
129+
test('compares objects correctly', () => {
130+
const obj1 = { foo: 'bar' }
131+
const obj2 = { foo: 'bar1' }
132+
const obj3 = { a: 1, b: 2, c: 3 }
133+
const obj4 = { b: 2, c: 3, a: 1 }
134+
const obj5 = { ...obj4, z: 999 }
135+
const nestedObj1 = { ...obj1, bar: [{ ...obj1 }, { ...obj1 }] }
136+
const nestedObj2 = { ...obj1, bar: [{ ...obj1 }, { ...obj2 }] }
137+
138+
// Identical object references
139+
expect(looseEqual(obj1, obj1)).toBe(true)
140+
// Two objects with identical keys/values
141+
expect(looseEqual(obj1, { ...obj1 })).toBe(true)
142+
// Different key values
143+
expect(looseEqual(obj1, obj2)).toBe(false)
144+
// Keys in different orders
145+
expect(looseEqual(obj3, obj4)).toBe(true)
146+
// One object has additional key
147+
expect(looseEqual(obj4, obj5)).toBe(false)
148+
// Identical object references with nested array
149+
expect(looseEqual(nestedObj1, nestedObj1)).toBe(true)
150+
// Identical object definitions with nested array
151+
expect(looseEqual(nestedObj1, { ...nestedObj1 })).toBe(true)
152+
// Object definitions with nested array (which has different order)
153+
expect(looseEqual(nestedObj1, nestedObj2)).toBe(false)
154+
})
155+
156+
test('compares different types correctly', () => {
157+
const obj1 = {}
158+
const obj2 = { a: 1 }
159+
const obj3 = { 0: 0, 1: 1, 2: 2 }
160+
const arr1: any[] = []
161+
const arr2 = [1]
162+
const arr3 = [0, 1, 2]
163+
const date1 = new Date(2019, 1, 2, 3, 4, 5, 6)
164+
const file1 = new File([''], 'filename.txt', {
165+
type: 'text/plain',
166+
lastModified: date1.getTime(),
167+
})
168+
169+
expect(looseEqual(123, '123')).toBe(true)
170+
expect(looseEqual(123, new Date(123))).toBe(false)
171+
expect(looseEqual(`123`, new Date(123))).toBe(false)
172+
expect(looseEqual([1, 2, 3], '1,2,3')).toBe(false)
173+
expect(looseEqual(obj1, arr1)).toBe(false)
174+
expect(looseEqual(obj2, arr2)).toBe(false)
175+
expect(looseEqual(obj1, '[object Object]')).toBe(false)
176+
expect(looseEqual(arr1, '[object Array]')).toBe(false)
177+
expect(looseEqual(obj1, date1)).toBe(false)
178+
expect(looseEqual(obj2, date1)).toBe(false)
179+
expect(looseEqual(arr1, date1)).toBe(false)
180+
expect(looseEqual(arr2, date1)).toBe(false)
181+
expect(looseEqual(obj2, file1)).toBe(false)
182+
expect(looseEqual(arr2, file1)).toBe(false)
183+
expect(looseEqual(date1, file1)).toBe(false)
184+
// Special case where an object's keys are the same as keys (indexes) of an array
185+
expect(looseEqual(obj3, arr3)).toBe(false)
186+
})
187+
188+
test('compares null and undefined values correctly', () => {
189+
expect(looseEqual(null, null)).toBe(true)
190+
expect(looseEqual(undefined, undefined)).toBe(true)
191+
expect(looseEqual(void 0, undefined)).toBe(true)
192+
expect(looseEqual(null, undefined)).toBe(false)
193+
expect(looseEqual(null, void 0)).toBe(false)
194+
expect(looseEqual(null, '')).toBe(false)
195+
expect(looseEqual(null, false)).toBe(false)
196+
expect(looseEqual(undefined, false)).toBe(false)
197+
})
198+
199+
test('compares sparse arrays correctly', () => {
200+
// The following arrays all have a length of 3
201+
// But the first two are "sparse"
202+
const arr1 = []
203+
arr1[2] = true
204+
const arr2 = []
205+
arr2[2] = true
206+
const arr3 = [false, false, true]
207+
const arr4 = [undefined, undefined, true]
208+
// This one is also sparse (missing index 1)
209+
const arr5 = []
210+
arr5[0] = arr5[2] = true
211+
212+
expect(looseEqual(arr1, arr2)).toBe(true)
213+
expect(looseEqual(arr2, arr1)).toBe(true)
214+
expect(looseEqual(arr1, arr3)).toBe(false)
215+
expect(looseEqual(arr3, arr1)).toBe(false)
216+
expect(looseEqual(arr1, arr4)).toBe(true)
217+
expect(looseEqual(arr4, arr1)).toBe(true)
218+
expect(looseEqual(arr1, arr5)).toBe(false)
219+
expect(looseEqual(arr5, arr1)).toBe(false)
220+
})
221+
})

packages/shared/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export const hasOwn = (
5656
): key is keyof typeof val => hasOwnProperty.call(val, key)
5757

5858
export const isArray = Array.isArray
59+
export const isDate = (val: unknown): val is Date => val instanceof Date
5960
export const isFunction = (val: unknown): val is Function =>
6061
typeof val === 'function'
6162
export const isString = (val: unknown): val is string => typeof val === 'string'

packages/shared/src/looseEqual.ts

+41-30
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,51 @@
1-
import { isObject, isArray } from './'
1+
import { isArray, isDate, isObject } from './'
2+
3+
function looseCompareArrays(a: any[], b: any[]) {
4+
if (a.length !== b.length) return false
5+
let equal = true
6+
for (let i = 0; equal && i < a.length; i++) {
7+
equal = looseEqual(a[i], b[i])
8+
}
9+
return equal
10+
}
211

312
export function looseEqual(a: any, b: any): boolean {
413
if (a === b) return true
5-
const isObjectA = isObject(a)
6-
const isObjectB = isObject(b)
7-
if (isObjectA && isObjectB) {
8-
try {
9-
const isArrayA = isArray(a)
10-
const isArrayB = isArray(b)
11-
if (isArrayA && isArrayB) {
12-
return (
13-
a.length === b.length &&
14-
a.every((e: any, i: any) => looseEqual(e, b[i]))
15-
)
16-
} else if (a instanceof Date && b instanceof Date) {
17-
return a.getTime() === b.getTime()
18-
} else if (!isArrayA && !isArrayB) {
19-
const keysA = Object.keys(a)
20-
const keysB = Object.keys(b)
21-
return (
22-
keysA.length === keysB.length &&
23-
keysA.every(key => looseEqual(a[key], b[key]))
24-
)
25-
} else {
26-
/* istanbul ignore next */
14+
let aValidType = isDate(a)
15+
let bValidType = isDate(b)
16+
if (aValidType || bValidType) {
17+
return aValidType && bValidType ? a.getTime() === b.getTime() : false
18+
}
19+
aValidType = isArray(a)
20+
bValidType = isArray(b)
21+
if (aValidType || bValidType) {
22+
return aValidType && bValidType ? looseCompareArrays(a, b) : false
23+
}
24+
aValidType = isObject(a)
25+
bValidType = isObject(b)
26+
if (aValidType || bValidType) {
27+
/* istanbul ignore if: this if will probably never be called */
28+
if (!aValidType || !bValidType) {
29+
return false
30+
}
31+
const aKeysCount = Object.keys(a).length
32+
const bKeysCount = Object.keys(b).length
33+
if (aKeysCount !== bKeysCount) {
34+
return false
35+
}
36+
for (const key in a) {
37+
const aHasKey = a.hasOwnProperty(key)
38+
const bHasKey = b.hasOwnProperty(key)
39+
if (
40+
(aHasKey && !bHasKey) ||
41+
(!aHasKey && bHasKey) ||
42+
!looseEqual(a[key], b[key])
43+
) {
2744
return false
2845
}
29-
} catch (e) {
30-
/* istanbul ignore next */
31-
return false
3246
}
33-
} else if (!isObjectA && !isObjectB) {
34-
return String(a) === String(b)
35-
} else {
36-
return false
3747
}
48+
return String(a) === String(b)
3849
}
3950

4051
export function looseIndexOf(arr: any[], val: any): number {

0 commit comments

Comments
 (0)