@zeix/cause-effect 0.14.2 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +256 -35
- package/index.d.ts +31 -6
- package/index.dev.js +383 -47
- package/index.js +1 -1
- package/index.ts +33 -4
- package/package.json +2 -2
- package/src/computed.ts +15 -6
- package/src/diff.ts +148 -0
- package/src/effect.ts +57 -50
- package/src/match.ts +52 -0
- package/src/resolve.ts +48 -0
- package/src/signal.ts +60 -14
- package/src/state.ts +4 -3
- package/src/store.ts +324 -0
- package/src/util.ts +54 -4
- package/test/batch.test.ts +23 -19
- package/test/benchmark.test.ts +8 -8
- package/test/computed.test.ts +15 -11
- package/test/diff.test.ts +638 -0
- package/test/effect.test.ts +656 -48
- package/test/match.test.ts +378 -0
- package/test/resolve.test.ts +156 -0
- package/test/signal.test.ts +451 -0
- package/test/store.test.ts +746 -0
- package/tsconfig.json +9 -10
- package/types/index.d.ts +15 -0
- package/types/src/diff.d.ts +30 -0
- package/types/src/effect.d.ts +16 -0
- package/types/src/match.d.ts +21 -0
- package/types/src/resolve.d.ts +29 -0
- package/types/src/signal.d.ts +44 -0
- package/{src → types/src}/state.d.ts +1 -1
- package/types/src/store.d.ts +62 -0
- package/types/src/util.d.ts +14 -0
- package/types/test-new-effect.d.ts +1 -0
- package/src/effect.d.ts +0 -17
- package/src/signal.d.ts +0 -26
- package/src/util.d.ts +0 -7
- /package/{src → types/src}/computed.d.ts +0 -0
- /package/{src → types/src}/scheduler.d.ts +0 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
CircularDependencyError,
|
|
4
|
+
diff,
|
|
5
|
+
isEqual,
|
|
6
|
+
UNSET,
|
|
7
|
+
type UnknownRecord,
|
|
8
|
+
} from '..'
|
|
9
|
+
|
|
10
|
+
describe('diff', () => {
|
|
11
|
+
describe('basic object diffing', () => {
|
|
12
|
+
test('should detect no changes for identical objects', () => {
|
|
13
|
+
const obj1 = { a: 1, b: 'hello' }
|
|
14
|
+
const obj2 = { a: 1, b: 'hello' }
|
|
15
|
+
const result = diff(obj1, obj2)
|
|
16
|
+
|
|
17
|
+
expect(result.changed).toBe(false)
|
|
18
|
+
expect(Object.keys(result.add)).toHaveLength(0)
|
|
19
|
+
expect(Object.keys(result.change)).toHaveLength(0)
|
|
20
|
+
expect(Object.keys(result.remove)).toHaveLength(0)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('should detect additions', () => {
|
|
24
|
+
const obj1 = { a: 1 }
|
|
25
|
+
const obj2 = { a: 1, b: 'new' }
|
|
26
|
+
const result = diff(obj1 as UnknownRecord, obj2 as UnknownRecord)
|
|
27
|
+
|
|
28
|
+
expect(result.changed).toBe(true)
|
|
29
|
+
expect(result.add).toEqual({ b: 'new' })
|
|
30
|
+
expect(Object.keys(result.change)).toHaveLength(0)
|
|
31
|
+
expect(Object.keys(result.remove)).toHaveLength(0)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('should detect removals', () => {
|
|
35
|
+
const obj1 = { a: 1, b: 'hello' }
|
|
36
|
+
const obj2 = { a: 1 }
|
|
37
|
+
const result = diff(obj1 as UnknownRecord, obj2 as UnknownRecord)
|
|
38
|
+
|
|
39
|
+
expect(result.changed).toBe(true)
|
|
40
|
+
expect(Object.keys(result.add)).toHaveLength(0)
|
|
41
|
+
expect(Object.keys(result.change)).toHaveLength(0)
|
|
42
|
+
expect(result.remove).toEqual({ b: UNSET })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('should detect changes', () => {
|
|
46
|
+
const obj1 = { a: 1, b: 'hello' }
|
|
47
|
+
const obj2 = { a: 2, b: 'hello' }
|
|
48
|
+
const result = diff(obj1, obj2)
|
|
49
|
+
|
|
50
|
+
expect(result.changed).toBe(true)
|
|
51
|
+
expect(Object.keys(result.add)).toHaveLength(0)
|
|
52
|
+
expect(result.change).toEqual({ a: 2 })
|
|
53
|
+
expect(Object.keys(result.remove)).toHaveLength(0)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('should detect multiple changes', () => {
|
|
57
|
+
const obj1 = { a: 1, b: 'hello', c: true }
|
|
58
|
+
const obj2 = { a: 2, d: 'new', c: true }
|
|
59
|
+
const result = diff(obj1 as UnknownRecord, obj2 as UnknownRecord)
|
|
60
|
+
|
|
61
|
+
expect(result.changed).toBe(true)
|
|
62
|
+
expect(result.add).toEqual({ d: 'new' })
|
|
63
|
+
expect(result.change).toEqual({ a: 2 })
|
|
64
|
+
expect(result.remove).toEqual({ b: UNSET })
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('primitive value handling', () => {
|
|
69
|
+
test('should handle string changes', () => {
|
|
70
|
+
const obj1 = { text: 'hello' }
|
|
71
|
+
const obj2 = { text: 'world' }
|
|
72
|
+
const result = diff(obj1, obj2)
|
|
73
|
+
|
|
74
|
+
expect(result.changed).toBe(true)
|
|
75
|
+
expect(result.change).toEqual({ text: 'world' })
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('should handle number changes including special values', () => {
|
|
79
|
+
const obj1 = { num: 42, nan: NaN, zero: -0 }
|
|
80
|
+
const obj2 = { num: 43, nan: NaN, zero: +0 }
|
|
81
|
+
const result = diff(obj1, obj2)
|
|
82
|
+
|
|
83
|
+
expect(result.changed).toBe(true)
|
|
84
|
+
expect(result.change).toEqual({ num: 43, zero: +0 })
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('should handle boolean changes', () => {
|
|
88
|
+
const obj1 = { flag: true }
|
|
89
|
+
const obj2 = { flag: false }
|
|
90
|
+
const result = diff(obj1, obj2)
|
|
91
|
+
|
|
92
|
+
expect(result.changed).toBe(true)
|
|
93
|
+
expect(result.change).toEqual({ flag: false })
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('array handling', () => {
|
|
98
|
+
test('should detect no changes in identical arrays', () => {
|
|
99
|
+
const obj1 = { arr: [1, 2, 3] }
|
|
100
|
+
const obj2 = { arr: [1, 2, 3] }
|
|
101
|
+
const result = diff(obj1, obj2)
|
|
102
|
+
|
|
103
|
+
expect(result.changed).toBe(false)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('should detect changes in arrays', () => {
|
|
107
|
+
const obj1 = { arr: [1, 2, 3] }
|
|
108
|
+
const obj2 = { arr: [1, 2, 4] }
|
|
109
|
+
const result = diff(obj1, obj2)
|
|
110
|
+
|
|
111
|
+
expect(result.changed).toBe(true)
|
|
112
|
+
expect(result.change).toEqual({ arr: [1, 2, 4] })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('should detect length changes in arrays', () => {
|
|
116
|
+
const obj1 = { arr: [1, 2, 3] }
|
|
117
|
+
const obj2 = { arr: [1, 2] }
|
|
118
|
+
const result = diff(obj1, obj2)
|
|
119
|
+
|
|
120
|
+
expect(result.changed).toBe(true)
|
|
121
|
+
expect(result.change).toEqual({ arr: [1, 2] })
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('should handle empty arrays', () => {
|
|
125
|
+
const obj1 = { arr: [] as number[] }
|
|
126
|
+
const obj2 = { arr: [1] }
|
|
127
|
+
const result = diff(obj1, obj2)
|
|
128
|
+
|
|
129
|
+
expect(result.changed).toBe(true)
|
|
130
|
+
expect(result.change).toEqual({ arr: [1] })
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('should handle arrays with complex objects', () => {
|
|
134
|
+
const obj1 = { arr: [{ id: 1, name: 'a' }] }
|
|
135
|
+
const obj2 = { arr: [{ id: 1, name: 'b' }] }
|
|
136
|
+
const result = diff(obj1, obj2)
|
|
137
|
+
|
|
138
|
+
expect(result.changed).toBe(true)
|
|
139
|
+
expect(result.change).toEqual({ arr: [{ id: 1, name: 'b' }] })
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('should handle nested arrays', () => {
|
|
143
|
+
const obj1 = {
|
|
144
|
+
matrix: [
|
|
145
|
+
[1, 2],
|
|
146
|
+
[3, 4],
|
|
147
|
+
],
|
|
148
|
+
}
|
|
149
|
+
const obj2 = {
|
|
150
|
+
matrix: [
|
|
151
|
+
[1, 2],
|
|
152
|
+
[3, 5],
|
|
153
|
+
],
|
|
154
|
+
}
|
|
155
|
+
const result = diff(obj1, obj2)
|
|
156
|
+
|
|
157
|
+
expect(result.changed).toBe(true)
|
|
158
|
+
expect(result.change).toEqual({
|
|
159
|
+
matrix: [
|
|
160
|
+
[1, 2],
|
|
161
|
+
[3, 5],
|
|
162
|
+
],
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('nested object handling', () => {
|
|
168
|
+
test('should detect no changes in nested objects', () => {
|
|
169
|
+
const obj1 = {
|
|
170
|
+
user: { id: 1, profile: { name: 'John', age: 30 } },
|
|
171
|
+
}
|
|
172
|
+
const obj2 = {
|
|
173
|
+
user: { id: 1, profile: { name: 'John', age: 30 } },
|
|
174
|
+
}
|
|
175
|
+
const result = diff(obj1, obj2)
|
|
176
|
+
|
|
177
|
+
expect(result.changed).toBe(false)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('should detect changes in nested objects', () => {
|
|
181
|
+
const obj1 = {
|
|
182
|
+
user: { id: 1, profile: { name: 'John', age: 30 } },
|
|
183
|
+
}
|
|
184
|
+
const obj2 = {
|
|
185
|
+
user: { id: 1, profile: { name: 'Jane', age: 30 } },
|
|
186
|
+
}
|
|
187
|
+
const result = diff(obj1, obj2)
|
|
188
|
+
|
|
189
|
+
expect(result.changed).toBe(true)
|
|
190
|
+
expect(result.change).toEqual({
|
|
191
|
+
user: { id: 1, profile: { name: 'Jane', age: 30 } },
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('should handle deeply nested structures', () => {
|
|
196
|
+
const obj1 = {
|
|
197
|
+
a: { b: { c: { d: { e: 'deep' } } } },
|
|
198
|
+
}
|
|
199
|
+
const obj2 = {
|
|
200
|
+
a: { b: { c: { d: { e: 'deeper' } } } },
|
|
201
|
+
}
|
|
202
|
+
const result = diff(obj1, obj2)
|
|
203
|
+
|
|
204
|
+
expect(result.changed).toBe(true)
|
|
205
|
+
expect(result.change).toEqual({
|
|
206
|
+
a: { b: { c: { d: { e: 'deeper' } } } },
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
describe('type change handling', () => {
|
|
212
|
+
test('should handle changes from primitive to object', () => {
|
|
213
|
+
const obj1 = { value: 'string' }
|
|
214
|
+
const obj2 = { value: { type: 'object' } }
|
|
215
|
+
const result = diff(obj1 as UnknownRecord, obj2 as UnknownRecord)
|
|
216
|
+
|
|
217
|
+
expect(result.changed).toBe(true)
|
|
218
|
+
expect(result.change).toEqual({ value: { type: 'object' } })
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('should handle changes from array to object', () => {
|
|
222
|
+
const obj1 = { data: [1, 2, 3] }
|
|
223
|
+
const obj2 = { data: { 0: 1, 1: 2, 2: 3 } }
|
|
224
|
+
const result = diff(obj1 as UnknownRecord, obj2 as UnknownRecord)
|
|
225
|
+
|
|
226
|
+
expect(result.changed).toBe(true)
|
|
227
|
+
expect(result.change).toEqual({ data: { 0: 1, 1: 2, 2: 3 } })
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test('should handle changes from object to array', () => {
|
|
231
|
+
const obj1 = { data: { a: 1, b: 2 } }
|
|
232
|
+
const obj2 = { data: [1, 2] }
|
|
233
|
+
const result = diff(obj1 as UnknownRecord, obj2 as UnknownRecord)
|
|
234
|
+
|
|
235
|
+
expect(result.changed).toBe(true)
|
|
236
|
+
expect(result.change).toEqual({ data: [1, 2] })
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
describe('special object types', () => {
|
|
241
|
+
test('should handle Date objects', () => {
|
|
242
|
+
const date1 = new Date('2023-01-01')
|
|
243
|
+
const date2 = new Date('2023-01-02')
|
|
244
|
+
const obj1 = { timestamp: date1 }
|
|
245
|
+
const obj2 = { timestamp: date2 }
|
|
246
|
+
const result = diff(obj1, obj2)
|
|
247
|
+
|
|
248
|
+
expect(result.changed).toBe(true)
|
|
249
|
+
expect(result.change).toEqual({ timestamp: date2 })
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test('should handle RegExp objects', () => {
|
|
253
|
+
const regex1 = /hello/g
|
|
254
|
+
const regex2 = /world/g
|
|
255
|
+
const obj1 = { pattern: regex1 }
|
|
256
|
+
const obj2 = { pattern: regex2 }
|
|
257
|
+
const result = diff(obj1, obj2)
|
|
258
|
+
|
|
259
|
+
expect(result.changed).toBe(true)
|
|
260
|
+
expect(result.change).toEqual({ pattern: regex2 })
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test('should handle identical special objects', () => {
|
|
264
|
+
const date = new Date('2023-01-01')
|
|
265
|
+
const obj1 = { timestamp: date }
|
|
266
|
+
const obj2 = { timestamp: date }
|
|
267
|
+
const result = diff(obj1, obj2)
|
|
268
|
+
|
|
269
|
+
expect(result.changed).toBe(false)
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
describe('edge cases and error handling', () => {
|
|
274
|
+
test('should handle empty objects', () => {
|
|
275
|
+
const result = diff({}, {})
|
|
276
|
+
expect(result.changed).toBe(false)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test('should detect circular references and throw error', () => {
|
|
280
|
+
const circular1: UnknownRecord = { a: 1 }
|
|
281
|
+
circular1.self = circular1
|
|
282
|
+
|
|
283
|
+
const circular2: UnknownRecord = { a: 1 }
|
|
284
|
+
circular2.self = circular2
|
|
285
|
+
|
|
286
|
+
expect(() => diff(circular1, circular2)).toThrow(
|
|
287
|
+
CircularDependencyError,
|
|
288
|
+
)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
test('should handle objects with Symbol keys', () => {
|
|
292
|
+
const sym = Symbol('test')
|
|
293
|
+
const obj1 = {
|
|
294
|
+
[sym]: 'value1',
|
|
295
|
+
normal: 'prop',
|
|
296
|
+
}
|
|
297
|
+
const obj2 = {
|
|
298
|
+
[sym]: 'value2',
|
|
299
|
+
normal: 'prop',
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Since Object.keys() doesn't include symbols,
|
|
303
|
+
// the diff should not detect the symbol property change
|
|
304
|
+
const result = diff(obj1, obj2)
|
|
305
|
+
expect(result.changed).toBe(false)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test('should handle objects with non-enumerable properties', () => {
|
|
309
|
+
const obj1 = { a: 1 }
|
|
310
|
+
const obj2 = { a: 1 }
|
|
311
|
+
|
|
312
|
+
Object.defineProperty(obj1, 'hidden', {
|
|
313
|
+
value: 'secret1',
|
|
314
|
+
enumerable: false,
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
Object.defineProperty(obj2, 'hidden', {
|
|
318
|
+
value: 'secret2',
|
|
319
|
+
enumerable: false,
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
// Since Object.keys() doesn't include non-enumerable properties,
|
|
323
|
+
// the diff should not detect the hidden property change
|
|
324
|
+
const result = diff(obj1, obj2)
|
|
325
|
+
expect(result.changed).toBe(false)
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
describe('performance edge cases', () => {
|
|
330
|
+
test('should handle large objects efficiently', () => {
|
|
331
|
+
const createLargeObject = (size: number, seed: number = 0) => {
|
|
332
|
+
const obj: Record<string, number> = {}
|
|
333
|
+
for (let i = 0; i < size; i++) {
|
|
334
|
+
obj[`prop${i}`] = i + seed
|
|
335
|
+
}
|
|
336
|
+
return obj
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const obj1 = createLargeObject(1000)
|
|
340
|
+
const obj2 = createLargeObject(1000, 1) // Same structure, different values
|
|
341
|
+
|
|
342
|
+
const start = performance.now()
|
|
343
|
+
const result = diff(obj1, obj2)
|
|
344
|
+
const duration = performance.now() - start
|
|
345
|
+
|
|
346
|
+
expect(result.changed).toBe(true)
|
|
347
|
+
expect(Object.keys(result.change)).toHaveLength(1000)
|
|
348
|
+
expect(duration).toBeLessThan(100) // Should complete within 100ms
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test('should handle deeply nested structures without stack overflow', () => {
|
|
352
|
+
// biome-ignore lint/suspicious/noExplicitAny: testing purposes
|
|
353
|
+
const createDeepObject = (depth: number): any => {
|
|
354
|
+
let obj: UnknownRecord = { value: 'leaf' }
|
|
355
|
+
for (let i = 0; i < depth; i++) {
|
|
356
|
+
obj = { [`level${i}`]: obj }
|
|
357
|
+
}
|
|
358
|
+
return obj
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const obj1 = createDeepObject(100)
|
|
362
|
+
const obj2 = createDeepObject(100)
|
|
363
|
+
obj2.level99.level98.level97.value = 'changed'
|
|
364
|
+
|
|
365
|
+
const result = diff(obj1, obj2)
|
|
366
|
+
expect(result.changed).toBe(true)
|
|
367
|
+
})
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
describe('optional keys handling', () => {
|
|
371
|
+
type OptionalKeysType = {
|
|
372
|
+
required: string
|
|
373
|
+
optional?: number
|
|
374
|
+
maybeUndefined?: string | undefined
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
test('should handle optional keys correctly', () => {
|
|
378
|
+
const obj1: OptionalKeysType = {
|
|
379
|
+
required: 'test',
|
|
380
|
+
}
|
|
381
|
+
const obj2: OptionalKeysType = {
|
|
382
|
+
required: 'test',
|
|
383
|
+
optional: 42,
|
|
384
|
+
}
|
|
385
|
+
const result = diff(obj1, obj2)
|
|
386
|
+
|
|
387
|
+
expect(result.changed).toBe(true)
|
|
388
|
+
expect(result.add).toEqual({ optional: 42 })
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
test('should handle undefined optional keys', () => {
|
|
392
|
+
const obj1: OptionalKeysType = {
|
|
393
|
+
required: 'test',
|
|
394
|
+
maybeUndefined: 'defined',
|
|
395
|
+
}
|
|
396
|
+
const obj2: OptionalKeysType = {
|
|
397
|
+
required: 'test',
|
|
398
|
+
maybeUndefined: undefined,
|
|
399
|
+
}
|
|
400
|
+
const result = diff(obj1, obj2)
|
|
401
|
+
|
|
402
|
+
expect(result.changed).toBe(true)
|
|
403
|
+
expect(result.change).toEqual({ maybeUndefined: undefined })
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
describe('array normalization to records', () => {
|
|
408
|
+
test('should correctly normalize arrays to records for comparison', () => {
|
|
409
|
+
const obj1 = { items: ['a', 'b', 'c'] }
|
|
410
|
+
const obj2 = { items: ['a', 'x', 'c'] }
|
|
411
|
+
const result = diff(obj1, obj2)
|
|
412
|
+
|
|
413
|
+
expect(result.changed).toBe(true)
|
|
414
|
+
expect(result.change).toEqual({ items: ['a', 'x', 'c'] })
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
test('should handle sparse arrays correctly', () => {
|
|
418
|
+
const sparse1: string[] = []
|
|
419
|
+
sparse1[0] = 'a'
|
|
420
|
+
sparse1[2] = 'c'
|
|
421
|
+
|
|
422
|
+
const sparse2: string[] = []
|
|
423
|
+
sparse2[0] = 'a'
|
|
424
|
+
sparse2[1] = 'b'
|
|
425
|
+
sparse2[2] = 'c'
|
|
426
|
+
|
|
427
|
+
const obj1 = { sparse: sparse1 }
|
|
428
|
+
const obj2 = { sparse: sparse2 }
|
|
429
|
+
const result = diff(obj1, obj2)
|
|
430
|
+
|
|
431
|
+
expect(result.changed).toBe(true)
|
|
432
|
+
expect(result.change).toEqual({ sparse: sparse2 })
|
|
433
|
+
})
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
describe('isEqual function', () => {
|
|
437
|
+
describe('primitives and fast paths', () => {
|
|
438
|
+
test('should handle identical values', () => {
|
|
439
|
+
expect(isEqual(1, 1)).toBe(true)
|
|
440
|
+
expect(isEqual('hello', 'hello')).toBe(true)
|
|
441
|
+
expect(isEqual(true, true)).toBe(true)
|
|
442
|
+
expect(isEqual(null, null)).toBe(true)
|
|
443
|
+
expect(isEqual(undefined, undefined)).toBe(true)
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
test('should handle different primitives', () => {
|
|
447
|
+
expect(isEqual(1, 2)).toBe(false)
|
|
448
|
+
expect(isEqual('hello', 'world')).toBe(false)
|
|
449
|
+
expect(isEqual(true, false)).toBe(false)
|
|
450
|
+
expect(isEqual(null, undefined)).toBe(false)
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
test('should handle special number values', () => {
|
|
454
|
+
expect(isEqual(NaN, NaN)).toBe(true)
|
|
455
|
+
expect(isEqual(-0, +0)).toBe(false)
|
|
456
|
+
expect(isEqual(Infinity, Infinity)).toBe(true)
|
|
457
|
+
expect(isEqual(-Infinity, Infinity)).toBe(false)
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
test('should handle type mismatches', () => {
|
|
461
|
+
// @ts-expect-error deliberate type mismatch
|
|
462
|
+
expect(isEqual(1, '1')).toBe(false)
|
|
463
|
+
// @ts-expect-error deliberate type mismatch
|
|
464
|
+
expect(isEqual(true, 1)).toBe(false)
|
|
465
|
+
expect(isEqual(null, 0)).toBe(false)
|
|
466
|
+
expect(isEqual(undefined, '')).toBe(false)
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
test('should handle same object reference', () => {
|
|
470
|
+
const obj = { a: 1 }
|
|
471
|
+
expect(isEqual(obj, obj)).toBe(true)
|
|
472
|
+
|
|
473
|
+
const arr = [1, 2, 3]
|
|
474
|
+
expect(isEqual(arr, arr)).toBe(true)
|
|
475
|
+
})
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
describe('objects', () => {
|
|
479
|
+
test('should compare objects with same content', () => {
|
|
480
|
+
expect(isEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true)
|
|
481
|
+
expect(isEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true)
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
test('should detect different object content', () => {
|
|
485
|
+
expect(isEqual({ a: 1 }, { a: 2 })).toBe(false)
|
|
486
|
+
expect(isEqual({ a: 1 }, { b: 1 })).toBe(false)
|
|
487
|
+
expect(isEqual({ a: 1, b: 2 }, { a: 1 })).toBe(false)
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
test('should handle nested objects', () => {
|
|
491
|
+
const obj1 = { user: { name: 'John', age: 30 } }
|
|
492
|
+
const obj2 = { user: { name: 'John', age: 30 } }
|
|
493
|
+
const obj3 = { user: { name: 'Jane', age: 30 } }
|
|
494
|
+
|
|
495
|
+
expect(isEqual(obj1, obj2)).toBe(true)
|
|
496
|
+
expect(isEqual(obj1, obj3)).toBe(false)
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
test('should handle empty objects', () => {
|
|
500
|
+
expect(isEqual({}, {})).toBe(true)
|
|
501
|
+
expect(isEqual({}, { a: 1 })).toBe(false)
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
describe('arrays', () => {
|
|
506
|
+
test('should compare arrays with same content', () => {
|
|
507
|
+
expect(isEqual([1, 2, 3], [1, 2, 3])).toBe(true)
|
|
508
|
+
expect(isEqual([], [])).toBe(true)
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
test('should detect different array content', () => {
|
|
512
|
+
expect(isEqual([1, 2, 3], [1, 2, 4])).toBe(false)
|
|
513
|
+
expect(isEqual([1, 2], [1, 2, 3])).toBe(false)
|
|
514
|
+
expect(isEqual([1, 2, 3], [3, 2, 1])).toBe(false)
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
test('should handle nested arrays', () => {
|
|
518
|
+
const arr1 = [
|
|
519
|
+
[1, 2],
|
|
520
|
+
[3, 4],
|
|
521
|
+
]
|
|
522
|
+
const arr2 = [
|
|
523
|
+
[1, 2],
|
|
524
|
+
[3, 4],
|
|
525
|
+
]
|
|
526
|
+
const arr3 = [
|
|
527
|
+
[1, 2],
|
|
528
|
+
[3, 5],
|
|
529
|
+
]
|
|
530
|
+
|
|
531
|
+
expect(isEqual(arr1, arr2)).toBe(true)
|
|
532
|
+
expect(isEqual(arr1, arr3)).toBe(false)
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
test('should handle arrays with objects', () => {
|
|
536
|
+
const arr1 = [{ a: 1 }, { b: 2 }]
|
|
537
|
+
const arr2 = [{ a: 1 }, { b: 2 }]
|
|
538
|
+
const arr3 = [{ a: 2 }, { b: 2 }]
|
|
539
|
+
|
|
540
|
+
expect(isEqual(arr1, arr2)).toBe(true)
|
|
541
|
+
expect(isEqual(arr1, arr3)).toBe(false)
|
|
542
|
+
})
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
describe('mixed types', () => {
|
|
546
|
+
test('should handle array vs object', () => {
|
|
547
|
+
expect(isEqual([1, 2], { 0: 1, 1: 2 })).toBe(false)
|
|
548
|
+
expect(isEqual({ length: 2 }, [1, 2])).toBe(false)
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
test('should handle object vs primitive', () => {
|
|
552
|
+
// @ts-expect-error deliberate type mismatch
|
|
553
|
+
expect(isEqual({ a: 1 }, 'object')).toBe(false)
|
|
554
|
+
// @ts-expect-error deliberate type mismatch
|
|
555
|
+
expect(isEqual(42, { value: 42 })).toBe(false)
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
test('should handle complex mixed structures', () => {
|
|
559
|
+
const obj1 = {
|
|
560
|
+
data: [1, 2, { nested: true }],
|
|
561
|
+
meta: { count: 3 },
|
|
562
|
+
}
|
|
563
|
+
const obj2 = {
|
|
564
|
+
data: [1, 2, { nested: true }],
|
|
565
|
+
meta: { count: 3 },
|
|
566
|
+
}
|
|
567
|
+
const obj3 = {
|
|
568
|
+
data: [1, 2, { nested: false }],
|
|
569
|
+
meta: { count: 3 },
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
expect(isEqual(obj1, obj2)).toBe(true)
|
|
573
|
+
expect(isEqual(obj1, obj3)).toBe(false)
|
|
574
|
+
})
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
describe('edge cases', () => {
|
|
578
|
+
test('should handle circular references', () => {
|
|
579
|
+
const circular1: UnknownRecord = { a: 1 }
|
|
580
|
+
circular1.self = circular1
|
|
581
|
+
|
|
582
|
+
const circular2: UnknownRecord = { a: 1 }
|
|
583
|
+
circular2.self = circular2
|
|
584
|
+
|
|
585
|
+
expect(() => isEqual(circular1, circular2)).toThrow(
|
|
586
|
+
CircularDependencyError,
|
|
587
|
+
)
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
test('should handle special objects', () => {
|
|
591
|
+
const date1 = new Date('2023-01-01')
|
|
592
|
+
const date2 = new Date('2023-01-01')
|
|
593
|
+
const date3 = new Date('2023-01-02')
|
|
594
|
+
|
|
595
|
+
// Different Date objects with same time should be false (reference equality for special objects)
|
|
596
|
+
expect(isEqual(date1, date1)).toBe(true) // same reference
|
|
597
|
+
expect(isEqual(date1, date2)).toBe(false) // different references
|
|
598
|
+
expect(isEqual(date1, date3)).toBe(false)
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
test('should handle null and undefined edge cases', () => {
|
|
602
|
+
expect(isEqual(null, null)).toBe(true)
|
|
603
|
+
expect(isEqual(undefined, undefined)).toBe(true)
|
|
604
|
+
expect(isEqual(null, undefined)).toBe(false)
|
|
605
|
+
expect(isEqual({}, null)).toBe(false)
|
|
606
|
+
expect(isEqual([], undefined)).toBe(false)
|
|
607
|
+
})
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
describe('performance comparison', () => {
|
|
611
|
+
test('should demonstrate isEqual vs Object.is difference', () => {
|
|
612
|
+
// Objects with same content but different references
|
|
613
|
+
const obj1 = {
|
|
614
|
+
user: { name: 'John', age: 30 },
|
|
615
|
+
items: [1, 2, 3],
|
|
616
|
+
}
|
|
617
|
+
const obj2 = {
|
|
618
|
+
user: { name: 'John', age: 30 },
|
|
619
|
+
items: [1, 2, 3],
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Object.is fails for content equality
|
|
623
|
+
expect(Object.is(obj1, obj2)).toBe(false)
|
|
624
|
+
|
|
625
|
+
// isEqual succeeds for content equality
|
|
626
|
+
expect(isEqual(obj1, obj2)).toBe(true)
|
|
627
|
+
|
|
628
|
+
// Both work for reference equality
|
|
629
|
+
expect(Object.is(obj1, obj1)).toBe(true)
|
|
630
|
+
expect(isEqual(obj1, obj1)).toBe(true)
|
|
631
|
+
|
|
632
|
+
// Both work for primitive equality
|
|
633
|
+
expect(Object.is(42, 42)).toBe(true)
|
|
634
|
+
expect(isEqual(42, 42)).toBe(true)
|
|
635
|
+
})
|
|
636
|
+
})
|
|
637
|
+
})
|
|
638
|
+
})
|