@pyreon/ui-core 0.11.4 → 0.11.6
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 +15 -13
- package/lib/index.d.ts +19 -12
- package/lib/index.js +29 -19
- package/package.json +24 -24
- package/src/PyreonUI.tsx +28 -34
- package/src/__tests__/PyreonUI.test.tsx +40 -37
- package/src/__tests__/compose.test.ts +8 -8
- package/src/__tests__/config.test.ts +37 -37
- package/src/__tests__/context.test.tsx +28 -27
- package/src/__tests__/hoistNonReactStatics.test.tsx +44 -44
- package/src/__tests__/isEmpty.test.ts +15 -15
- package/src/__tests__/isEqual.test.ts +28 -28
- package/src/__tests__/render.test.tsx +28 -28
- package/src/__tests__/useStableValue.test.ts +23 -23
- package/src/__tests__/utils.test.ts +149 -149
- package/src/config.ts +7 -7
- package/src/context.tsx +43 -8
- package/src/hoistNonReactStatics.ts +1 -1
- package/src/html/htmlTags.ts +142 -142
- package/src/html/index.ts +3 -3
- package/src/index.ts +19 -17
- package/src/isEmpty.ts +1 -1
- package/src/isEqual.ts +1 -1
- package/src/render.tsx +6 -6
- package/src/useStableValue.ts +2 -2
- package/src/utils.ts +3 -3
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from
|
|
2
|
-
import { get, merge, omit, pick, set, throttle } from
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { get, merge, omit, pick, set, throttle } from '../utils'
|
|
3
3
|
|
|
4
4
|
// --------------------------------------------------------
|
|
5
5
|
// omit
|
|
6
6
|
// --------------------------------------------------------
|
|
7
|
-
describe(
|
|
8
|
-
it(
|
|
7
|
+
describe('omit', () => {
|
|
8
|
+
it('should return object without specified keys', () => {
|
|
9
9
|
const obj = { a: 1, b: 2, c: 3 }
|
|
10
|
-
expect(omit(obj, [
|
|
10
|
+
expect(omit(obj, ['b'])).toEqual({ a: 1, c: 3 })
|
|
11
11
|
})
|
|
12
12
|
|
|
13
|
-
it(
|
|
13
|
+
it('should return shallow copy when no keys specified', () => {
|
|
14
14
|
const obj = { a: 1, b: 2 }
|
|
15
15
|
const result = omit(obj)
|
|
16
16
|
expect(result).toEqual({ a: 1, b: 2 })
|
|
17
17
|
expect(result).not.toBe(obj)
|
|
18
18
|
})
|
|
19
19
|
|
|
20
|
-
it(
|
|
20
|
+
it('should return shallow copy when keys is empty array', () => {
|
|
21
21
|
const obj = { a: 1 }
|
|
22
22
|
expect(omit(obj, [])).toEqual({ a: 1 })
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
it(
|
|
25
|
+
it('should return empty object for null', () => {
|
|
26
26
|
expect(omit(null)).toEqual({})
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
-
it(
|
|
29
|
+
it('should return empty object for undefined', () => {
|
|
30
30
|
expect(omit(undefined)).toEqual({})
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
-
it(
|
|
33
|
+
it('should handle multiple keys', () => {
|
|
34
34
|
const obj = { a: 1, b: 2, c: 3, d: 4 }
|
|
35
|
-
expect(omit(obj, [
|
|
35
|
+
expect(omit(obj, ['a', 'c'])).toEqual({ b: 2, d: 4 })
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
it(
|
|
38
|
+
it('should ignore keys not present in object', () => {
|
|
39
39
|
const obj = { a: 1 }
|
|
40
|
-
expect(omit(obj, [
|
|
40
|
+
expect(omit(obj, ['b', 'c'])).toEqual({ a: 1 })
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
-
it(
|
|
43
|
+
it('should only include own properties', () => {
|
|
44
44
|
const proto = { inherited: true }
|
|
45
45
|
const obj = Object.create(proto)
|
|
46
46
|
obj.own = 1
|
|
@@ -51,230 +51,230 @@ describe("omit", () => {
|
|
|
51
51
|
// --------------------------------------------------------
|
|
52
52
|
// pick
|
|
53
53
|
// --------------------------------------------------------
|
|
54
|
-
describe(
|
|
55
|
-
it(
|
|
54
|
+
describe('pick', () => {
|
|
55
|
+
it('should return object with only specified keys', () => {
|
|
56
56
|
const obj = { a: 1, b: 2, c: 3 }
|
|
57
|
-
expect(pick(obj, [
|
|
57
|
+
expect(pick(obj, ['a', 'c'])).toEqual({ a: 1, c: 3 })
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
-
it(
|
|
60
|
+
it('should return shallow copy when no keys specified', () => {
|
|
61
61
|
const obj = { a: 1, b: 2 }
|
|
62
62
|
const result = pick(obj)
|
|
63
63
|
expect(result).toEqual({ a: 1, b: 2 })
|
|
64
64
|
expect(result).not.toBe(obj)
|
|
65
65
|
})
|
|
66
66
|
|
|
67
|
-
it(
|
|
67
|
+
it('should return shallow copy when keys is empty array', () => {
|
|
68
68
|
const obj = { a: 1 }
|
|
69
69
|
expect(pick(obj, [])).toEqual({ a: 1 })
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
it(
|
|
72
|
+
it('should return empty object for null', () => {
|
|
73
73
|
expect(pick(null)).toEqual({})
|
|
74
74
|
})
|
|
75
75
|
|
|
76
|
-
it(
|
|
76
|
+
it('should return empty object for undefined', () => {
|
|
77
77
|
expect(pick(undefined)).toEqual({})
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
-
it(
|
|
80
|
+
it('should ignore keys not present in object', () => {
|
|
81
81
|
const obj = { a: 1 }
|
|
82
|
-
expect(pick(obj, [
|
|
82
|
+
expect(pick(obj, ['a', 'b'])).toEqual({ a: 1 })
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
-
it(
|
|
85
|
+
it('should only pick own properties', () => {
|
|
86
86
|
const proto = { inherited: true }
|
|
87
87
|
const obj = Object.create(proto)
|
|
88
88
|
obj.own = 1
|
|
89
|
-
expect(pick(obj, [
|
|
89
|
+
expect(pick(obj, ['own', 'inherited'])).toEqual({ own: 1 })
|
|
90
90
|
})
|
|
91
91
|
})
|
|
92
92
|
|
|
93
93
|
// --------------------------------------------------------
|
|
94
94
|
// get
|
|
95
95
|
// --------------------------------------------------------
|
|
96
|
-
describe(
|
|
97
|
-
it(
|
|
96
|
+
describe('get', () => {
|
|
97
|
+
it('should get nested value by dot path', () => {
|
|
98
98
|
const obj = { a: { b: { c: 42 } } }
|
|
99
|
-
expect(get(obj,
|
|
99
|
+
expect(get(obj, 'a.b.c')).toBe(42)
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
-
it(
|
|
102
|
+
it('should get value by array path', () => {
|
|
103
103
|
const obj = { a: { b: 10 } }
|
|
104
|
-
expect(get(obj, [
|
|
104
|
+
expect(get(obj, ['a', 'b'])).toBe(10)
|
|
105
105
|
})
|
|
106
106
|
|
|
107
|
-
it(
|
|
107
|
+
it('should get array element by bracket notation', () => {
|
|
108
108
|
const obj = { items: [10, 20, 30] }
|
|
109
|
-
expect(get(obj,
|
|
109
|
+
expect(get(obj, 'items[1]')).toBe(20)
|
|
110
110
|
})
|
|
111
111
|
|
|
112
|
-
it(
|
|
112
|
+
it('should return defaultValue when path does not exist', () => {
|
|
113
113
|
const obj = { a: 1 }
|
|
114
|
-
expect(get(obj,
|
|
114
|
+
expect(get(obj, 'b.c', 'default')).toBe('default')
|
|
115
115
|
})
|
|
116
116
|
|
|
117
|
-
it(
|
|
117
|
+
it('should return defaultValue when intermediate is null', () => {
|
|
118
118
|
const obj = { a: null }
|
|
119
|
-
expect(get(obj,
|
|
119
|
+
expect(get(obj, 'a.b', 'fallback')).toBe('fallback')
|
|
120
120
|
})
|
|
121
121
|
|
|
122
|
-
it(
|
|
122
|
+
it('should return defaultValue when intermediate is undefined', () => {
|
|
123
123
|
const obj = { a: undefined }
|
|
124
|
-
expect(get(obj,
|
|
124
|
+
expect(get(obj, 'a.b', 'fallback')).toBe('fallback')
|
|
125
125
|
})
|
|
126
126
|
|
|
127
|
-
it(
|
|
128
|
-
expect(get({},
|
|
127
|
+
it('should return undefined when path does not exist and no default', () => {
|
|
128
|
+
expect(get({}, 'a.b.c')).toBeUndefined()
|
|
129
129
|
})
|
|
130
130
|
|
|
131
|
-
it(
|
|
131
|
+
it('should return the root value for empty array path', () => {
|
|
132
132
|
const obj = { a: 1 }
|
|
133
133
|
expect(get(obj, [])).toEqual({ a: 1 })
|
|
134
134
|
})
|
|
135
135
|
|
|
136
|
-
it(
|
|
137
|
-
expect(get({ x: 5 },
|
|
136
|
+
it('should handle top-level key', () => {
|
|
137
|
+
expect(get({ x: 5 }, 'x')).toBe(5)
|
|
138
138
|
})
|
|
139
139
|
|
|
140
|
-
it(
|
|
141
|
-
expect(get({ a: 0 },
|
|
142
|
-
expect(get({ a: false },
|
|
143
|
-
expect(get({ a:
|
|
144
|
-
expect(get({ a: null },
|
|
140
|
+
it('should return actual value even if it is falsy', () => {
|
|
141
|
+
expect(get({ a: 0 }, 'a', 'default')).toBe(0)
|
|
142
|
+
expect(get({ a: false }, 'a', 'default')).toBe(false)
|
|
143
|
+
expect(get({ a: '' }, 'a', 'default')).toBe('')
|
|
144
|
+
expect(get({ a: null }, 'a', 'default')).toBeNull()
|
|
145
145
|
})
|
|
146
146
|
|
|
147
|
-
it(
|
|
148
|
-
expect(get({ a: undefined },
|
|
147
|
+
it('should use defaultValue only when result is undefined', () => {
|
|
148
|
+
expect(get({ a: undefined }, 'a', 'default')).toBe('default')
|
|
149
149
|
})
|
|
150
150
|
|
|
151
151
|
// UNSAFE_KEYS guard
|
|
152
|
-
it(
|
|
152
|
+
it('should return defaultValue for __proto__ key', () => {
|
|
153
153
|
const obj = { a: 1 }
|
|
154
|
-
expect(get(obj,
|
|
154
|
+
expect(get(obj, '__proto__', 'safe')).toBe('safe')
|
|
155
155
|
})
|
|
156
156
|
|
|
157
|
-
it(
|
|
157
|
+
it('should return defaultValue for prototype key', () => {
|
|
158
158
|
const obj = { a: 1 }
|
|
159
|
-
expect(get(obj,
|
|
159
|
+
expect(get(obj, 'prototype', 'safe')).toBe('safe')
|
|
160
160
|
})
|
|
161
161
|
|
|
162
|
-
it(
|
|
162
|
+
it('should return defaultValue for constructor key', () => {
|
|
163
163
|
const obj = { a: 1 }
|
|
164
|
-
expect(get(obj,
|
|
164
|
+
expect(get(obj, 'constructor', 'safe')).toBe('safe')
|
|
165
165
|
})
|
|
166
166
|
|
|
167
|
-
it(
|
|
167
|
+
it('should return defaultValue for __proto__ in nested path', () => {
|
|
168
168
|
const obj = { a: { b: 1 } }
|
|
169
|
-
expect(get(obj,
|
|
169
|
+
expect(get(obj, 'a.__proto__.c', 'safe')).toBe('safe')
|
|
170
170
|
})
|
|
171
171
|
|
|
172
|
-
it(
|
|
172
|
+
it('should return obj itself for empty string path (no keys parsed)', () => {
|
|
173
173
|
// parsePath('') returns [] — no iteration — result stays as obj
|
|
174
174
|
// obj is not undefined — returned as-is
|
|
175
|
-
expect(get({ a: 1 },
|
|
175
|
+
expect(get({ a: 1 }, '')).toEqual({ a: 1 })
|
|
176
176
|
})
|
|
177
177
|
})
|
|
178
178
|
|
|
179
179
|
// --------------------------------------------------------
|
|
180
180
|
// set
|
|
181
181
|
// --------------------------------------------------------
|
|
182
|
-
describe(
|
|
183
|
-
it(
|
|
182
|
+
describe('set', () => {
|
|
183
|
+
it('should set nested value by dot path', () => {
|
|
184
184
|
const obj: any = {}
|
|
185
|
-
set(obj,
|
|
185
|
+
set(obj, 'a.b.c', 42)
|
|
186
186
|
expect(obj.a.b.c).toBe(42)
|
|
187
187
|
})
|
|
188
188
|
|
|
189
|
-
it(
|
|
189
|
+
it('should set value by array path', () => {
|
|
190
190
|
const obj: any = {}
|
|
191
|
-
set(obj, [
|
|
191
|
+
set(obj, ['x', 'y'], 10)
|
|
192
192
|
expect(obj.x.y).toBe(10)
|
|
193
193
|
})
|
|
194
194
|
|
|
195
|
-
it(
|
|
195
|
+
it('should create arrays for numeric keys', () => {
|
|
196
196
|
const obj: any = {}
|
|
197
|
-
set(obj,
|
|
197
|
+
set(obj, 'items.0', 'first')
|
|
198
198
|
expect(Array.isArray(obj.items)).toBe(true)
|
|
199
|
-
expect(obj.items[0]).toBe(
|
|
199
|
+
expect(obj.items[0]).toBe('first')
|
|
200
200
|
})
|
|
201
201
|
|
|
202
|
-
it(
|
|
202
|
+
it('should overwrite existing values', () => {
|
|
203
203
|
const obj = { a: { b: 1 } }
|
|
204
|
-
set(obj,
|
|
204
|
+
set(obj, 'a.b', 2)
|
|
205
205
|
expect(obj.a.b).toBe(2)
|
|
206
206
|
})
|
|
207
207
|
|
|
208
|
-
it(
|
|
208
|
+
it('should return the mutated object', () => {
|
|
209
209
|
const obj = {}
|
|
210
|
-
const result = set(obj,
|
|
210
|
+
const result = set(obj, 'a', 1)
|
|
211
211
|
expect(result).toBe(obj)
|
|
212
212
|
})
|
|
213
213
|
|
|
214
|
-
it(
|
|
214
|
+
it('should handle single key path', () => {
|
|
215
215
|
const obj: any = {}
|
|
216
|
-
set(obj,
|
|
217
|
-
expect(obj.key).toBe(
|
|
216
|
+
set(obj, 'key', 'value')
|
|
217
|
+
expect(obj.key).toBe('value')
|
|
218
218
|
})
|
|
219
219
|
|
|
220
|
-
it(
|
|
220
|
+
it('should not set anything for empty path', () => {
|
|
221
221
|
const obj = { a: 1 }
|
|
222
|
-
set(obj,
|
|
222
|
+
set(obj, '', 'value')
|
|
223
223
|
expect(obj).toEqual({ a: 1 })
|
|
224
224
|
})
|
|
225
225
|
|
|
226
226
|
// Security: prototype pollution protection
|
|
227
|
-
it(
|
|
227
|
+
it('should not pollute Object.prototype via __proto__', () => {
|
|
228
228
|
const obj = {}
|
|
229
|
-
set(obj,
|
|
229
|
+
set(obj, '__proto__.polluted', true)
|
|
230
230
|
expect(({} as any).polluted).toBeUndefined()
|
|
231
231
|
})
|
|
232
232
|
|
|
233
|
-
it(
|
|
233
|
+
it('should not pollute via constructor.prototype', () => {
|
|
234
234
|
const obj = {}
|
|
235
|
-
set(obj,
|
|
235
|
+
set(obj, 'constructor.prototype.polluted', true)
|
|
236
236
|
expect(({} as any).polluted).toBeUndefined()
|
|
237
237
|
})
|
|
238
238
|
|
|
239
|
-
it(
|
|
239
|
+
it('should bail out when intermediate key is __proto__', () => {
|
|
240
240
|
const obj: any = { a: 1 }
|
|
241
|
-
const result = set(obj,
|
|
241
|
+
const result = set(obj, '__proto__.polluted', true)
|
|
242
242
|
expect(result).toBe(obj)
|
|
243
243
|
expect(({} as any).polluted).toBeUndefined()
|
|
244
244
|
})
|
|
245
245
|
|
|
246
|
-
it(
|
|
246
|
+
it('should bail out when next key in path is unsafe', () => {
|
|
247
247
|
const obj: any = {}
|
|
248
|
-
const result = set(obj,
|
|
248
|
+
const result = set(obj, 'a.__proto__', 'bad')
|
|
249
249
|
expect(result).toBe(obj)
|
|
250
250
|
expect(obj.a).toBeUndefined()
|
|
251
251
|
})
|
|
252
252
|
|
|
253
|
-
it(
|
|
253
|
+
it('should bail out when last key is unsafe', () => {
|
|
254
254
|
const obj: any = {}
|
|
255
|
-
set(obj,
|
|
255
|
+
set(obj, 'prototype', 'bad')
|
|
256
256
|
// prototype is in UNSAFE_KEYS, so the set should be blocked
|
|
257
257
|
expect(obj.prototype).toBeUndefined()
|
|
258
258
|
})
|
|
259
259
|
|
|
260
|
-
it(
|
|
260
|
+
it('should handle bracket notation in paths', () => {
|
|
261
261
|
const obj: any = {}
|
|
262
|
-
set(obj,
|
|
263
|
-
expect(obj.items[0].name).toBe(
|
|
262
|
+
set(obj, 'items[0].name', 'first')
|
|
263
|
+
expect(obj.items[0].name).toBe('first')
|
|
264
264
|
})
|
|
265
265
|
|
|
266
|
-
it(
|
|
266
|
+
it('should not overwrite existing intermediate objects', () => {
|
|
267
267
|
const obj: any = { a: { existing: true } }
|
|
268
|
-
set(obj,
|
|
268
|
+
set(obj, 'a.b', 'new')
|
|
269
269
|
expect(obj.a.existing).toBe(true)
|
|
270
|
-
expect(obj.a.b).toBe(
|
|
270
|
+
expect(obj.a.b).toBe('new')
|
|
271
271
|
})
|
|
272
272
|
})
|
|
273
273
|
|
|
274
274
|
// --------------------------------------------------------
|
|
275
275
|
// throttle
|
|
276
276
|
// --------------------------------------------------------
|
|
277
|
-
describe(
|
|
277
|
+
describe('throttle', () => {
|
|
278
278
|
beforeEach(() => {
|
|
279
279
|
vi.useFakeTimers()
|
|
280
280
|
})
|
|
@@ -283,15 +283,15 @@ describe("throttle", () => {
|
|
|
283
283
|
vi.useRealTimers()
|
|
284
284
|
})
|
|
285
285
|
|
|
286
|
-
it(
|
|
286
|
+
it('should call function immediately on first invocation', () => {
|
|
287
287
|
const fn = vi.fn()
|
|
288
288
|
const throttled = throttle(fn, 100)
|
|
289
|
-
throttled(
|
|
290
|
-
expect(fn).toHaveBeenCalledWith(
|
|
289
|
+
throttled('a')
|
|
290
|
+
expect(fn).toHaveBeenCalledWith('a')
|
|
291
291
|
expect(fn).toHaveBeenCalledTimes(1)
|
|
292
292
|
})
|
|
293
293
|
|
|
294
|
-
it(
|
|
294
|
+
it('should not call again within wait period', () => {
|
|
295
295
|
const fn = vi.fn()
|
|
296
296
|
const throttled = throttle(fn, 100)
|
|
297
297
|
throttled()
|
|
@@ -300,24 +300,24 @@ describe("throttle", () => {
|
|
|
300
300
|
expect(fn).toHaveBeenCalledTimes(1)
|
|
301
301
|
})
|
|
302
302
|
|
|
303
|
-
it(
|
|
303
|
+
it('should call with latest args after wait period', () => {
|
|
304
304
|
const fn = vi.fn()
|
|
305
305
|
const throttled = throttle(fn, 100)
|
|
306
306
|
|
|
307
|
-
throttled(
|
|
308
|
-
throttled(
|
|
309
|
-
throttled(
|
|
307
|
+
throttled('first')
|
|
308
|
+
throttled('second')
|
|
309
|
+
throttled('third')
|
|
310
310
|
|
|
311
311
|
expect(fn).toHaveBeenCalledTimes(1)
|
|
312
|
-
expect(fn).toHaveBeenCalledWith(
|
|
312
|
+
expect(fn).toHaveBeenCalledWith('first')
|
|
313
313
|
|
|
314
314
|
vi.advanceTimersByTime(100)
|
|
315
315
|
|
|
316
316
|
expect(fn).toHaveBeenCalledTimes(2)
|
|
317
|
-
expect(fn).toHaveBeenLastCalledWith(
|
|
317
|
+
expect(fn).toHaveBeenLastCalledWith('third')
|
|
318
318
|
})
|
|
319
319
|
|
|
320
|
-
it(
|
|
320
|
+
it('should allow immediate call after wait period elapses', () => {
|
|
321
321
|
const fn = vi.fn()
|
|
322
322
|
const throttled = throttle(fn, 100)
|
|
323
323
|
|
|
@@ -327,88 +327,88 @@ describe("throttle", () => {
|
|
|
327
327
|
expect(fn).toHaveBeenCalledTimes(2)
|
|
328
328
|
})
|
|
329
329
|
|
|
330
|
-
it(
|
|
330
|
+
it('should cancel pending invocations', () => {
|
|
331
331
|
const fn = vi.fn()
|
|
332
332
|
const throttled = throttle(fn, 100)
|
|
333
333
|
|
|
334
|
-
throttled(
|
|
335
|
-
throttled(
|
|
334
|
+
throttled('first')
|
|
335
|
+
throttled('second')
|
|
336
336
|
throttled.cancel()
|
|
337
337
|
|
|
338
338
|
vi.advanceTimersByTime(200)
|
|
339
339
|
expect(fn).toHaveBeenCalledTimes(1)
|
|
340
340
|
})
|
|
341
341
|
|
|
342
|
-
it(
|
|
342
|
+
it('should work with default wait of 0', () => {
|
|
343
343
|
const fn = vi.fn()
|
|
344
344
|
const throttled = throttle(fn)
|
|
345
345
|
throttled()
|
|
346
346
|
expect(fn).toHaveBeenCalledTimes(1)
|
|
347
347
|
})
|
|
348
348
|
|
|
349
|
-
it(
|
|
349
|
+
it('should skip trailing call when trailing: false', () => {
|
|
350
350
|
const fn = vi.fn()
|
|
351
351
|
const throttled = throttle(fn, 100, { trailing: false })
|
|
352
352
|
|
|
353
|
-
throttled(
|
|
354
|
-
throttled(
|
|
355
|
-
throttled(
|
|
353
|
+
throttled('first')
|
|
354
|
+
throttled('second')
|
|
355
|
+
throttled('third')
|
|
356
356
|
|
|
357
357
|
expect(fn).toHaveBeenCalledTimes(1)
|
|
358
|
-
expect(fn).toHaveBeenCalledWith(
|
|
358
|
+
expect(fn).toHaveBeenCalledWith('first')
|
|
359
359
|
|
|
360
360
|
vi.advanceTimersByTime(200)
|
|
361
361
|
expect(fn).toHaveBeenCalledTimes(1)
|
|
362
362
|
})
|
|
363
363
|
|
|
364
|
-
it(
|
|
364
|
+
it('should skip leading call when leading: false', () => {
|
|
365
365
|
const fn = vi.fn()
|
|
366
366
|
const throttled = throttle(fn, 100, { leading: false })
|
|
367
367
|
|
|
368
|
-
throttled(
|
|
368
|
+
throttled('first')
|
|
369
369
|
expect(fn).toHaveBeenCalledTimes(0)
|
|
370
370
|
|
|
371
371
|
vi.advanceTimersByTime(100)
|
|
372
372
|
expect(fn).toHaveBeenCalledTimes(1)
|
|
373
|
-
expect(fn).toHaveBeenCalledWith(
|
|
373
|
+
expect(fn).toHaveBeenCalledWith('first')
|
|
374
374
|
})
|
|
375
375
|
|
|
376
|
-
it(
|
|
376
|
+
it('should support leading: false with trailing: true (default)', () => {
|
|
377
377
|
const fn = vi.fn()
|
|
378
378
|
const throttled = throttle(fn, 100, { leading: false })
|
|
379
379
|
|
|
380
|
-
throttled(
|
|
381
|
-
throttled(
|
|
380
|
+
throttled('a')
|
|
381
|
+
throttled('b')
|
|
382
382
|
expect(fn).toHaveBeenCalledTimes(0)
|
|
383
383
|
|
|
384
384
|
vi.advanceTimersByTime(100)
|
|
385
385
|
expect(fn).toHaveBeenCalledTimes(1)
|
|
386
|
-
expect(fn).toHaveBeenCalledWith(
|
|
386
|
+
expect(fn).toHaveBeenCalledWith('b')
|
|
387
387
|
})
|
|
388
388
|
|
|
389
|
-
it(
|
|
389
|
+
it('should still fire leading call after cooldown with trailing: false', () => {
|
|
390
390
|
const fn = vi.fn()
|
|
391
391
|
const throttled = throttle(fn, 100, { trailing: false })
|
|
392
392
|
|
|
393
|
-
throttled(
|
|
393
|
+
throttled('first')
|
|
394
394
|
expect(fn).toHaveBeenCalledTimes(1)
|
|
395
395
|
|
|
396
396
|
vi.advanceTimersByTime(100)
|
|
397
|
-
throttled(
|
|
397
|
+
throttled('second')
|
|
398
398
|
expect(fn).toHaveBeenCalledTimes(2)
|
|
399
|
-
expect(fn).toHaveBeenLastCalledWith(
|
|
399
|
+
expect(fn).toHaveBeenLastCalledWith('second')
|
|
400
400
|
})
|
|
401
401
|
|
|
402
|
-
it(
|
|
402
|
+
it('should not fire trailing call when cancelled before timer fires', () => {
|
|
403
403
|
const fn = vi.fn()
|
|
404
404
|
const throttled = throttle(fn, 100)
|
|
405
405
|
|
|
406
406
|
// Leading call fires immediately
|
|
407
|
-
throttled(
|
|
407
|
+
throttled('first')
|
|
408
408
|
expect(fn).toHaveBeenCalledTimes(1)
|
|
409
409
|
|
|
410
410
|
// Call again within wait period — queues trailing
|
|
411
|
-
throttled(
|
|
411
|
+
throttled('second')
|
|
412
412
|
|
|
413
413
|
// Cancel before trailing timer fires
|
|
414
414
|
throttled.cancel()
|
|
@@ -418,11 +418,11 @@ describe("throttle", () => {
|
|
|
418
418
|
expect(fn).toHaveBeenCalledTimes(1)
|
|
419
419
|
})
|
|
420
420
|
|
|
421
|
-
it(
|
|
421
|
+
it('should handle leading: false with trailing: false (no calls fire)', () => {
|
|
422
422
|
const fn = vi.fn()
|
|
423
423
|
const throttled = throttle(fn, 100, { leading: false, trailing: false })
|
|
424
424
|
|
|
425
|
-
throttled(
|
|
425
|
+
throttled('a')
|
|
426
426
|
expect(fn).toHaveBeenCalledTimes(0)
|
|
427
427
|
|
|
428
428
|
vi.advanceTimersByTime(200)
|
|
@@ -433,51 +433,51 @@ describe("throttle", () => {
|
|
|
433
433
|
// --------------------------------------------------------
|
|
434
434
|
// merge
|
|
435
435
|
// --------------------------------------------------------
|
|
436
|
-
describe(
|
|
437
|
-
it(
|
|
436
|
+
describe('merge', () => {
|
|
437
|
+
it('should deep merge two objects', () => {
|
|
438
438
|
const target = { a: { b: 1, c: 2 } }
|
|
439
439
|
const source = { a: { c: 3, d: 4 } }
|
|
440
440
|
expect(merge({ ...target }, source)).toEqual({ a: { b: 1, c: 3, d: 4 } })
|
|
441
441
|
})
|
|
442
442
|
|
|
443
|
-
it(
|
|
443
|
+
it('should replace arrays instead of merging them', () => {
|
|
444
444
|
const target = { items: [1, 2, 3] }
|
|
445
445
|
const source = { items: [4, 5] }
|
|
446
446
|
expect(merge({ ...target }, source)).toEqual({ items: [4, 5] })
|
|
447
447
|
})
|
|
448
448
|
|
|
449
|
-
it(
|
|
449
|
+
it('should handle multiple sources', () => {
|
|
450
450
|
const result = merge({ a: 1 }, { b: 2 }, { c: 3 })
|
|
451
451
|
expect(result).toEqual({ a: 1, b: 2, c: 3 })
|
|
452
452
|
})
|
|
453
453
|
|
|
454
|
-
it(
|
|
454
|
+
it('should overwrite primitive values', () => {
|
|
455
455
|
expect(merge({ a: 1 }, { a: 2 })).toEqual({ a: 2 })
|
|
456
456
|
})
|
|
457
457
|
|
|
458
|
-
it(
|
|
458
|
+
it('should skip null sources', () => {
|
|
459
459
|
const target = { a: 1 }
|
|
460
460
|
expect(merge(target, null as any)).toEqual({ a: 1 })
|
|
461
461
|
})
|
|
462
462
|
|
|
463
|
-
it(
|
|
463
|
+
it('should skip undefined sources', () => {
|
|
464
464
|
const target = { a: 1 }
|
|
465
465
|
expect(merge(target, undefined as any)).toEqual({ a: 1 })
|
|
466
466
|
})
|
|
467
467
|
|
|
468
|
-
it(
|
|
468
|
+
it('should not merge non-plain objects deeply', () => {
|
|
469
469
|
const date = new Date()
|
|
470
470
|
const result = merge({} as any, { d: date })
|
|
471
471
|
expect(result.d).toBe(date)
|
|
472
472
|
})
|
|
473
473
|
|
|
474
|
-
it(
|
|
474
|
+
it('should return the target object (mutates)', () => {
|
|
475
475
|
const target = { a: 1 }
|
|
476
476
|
const result = merge(target, { b: 2 })
|
|
477
477
|
expect(result).toBe(target)
|
|
478
478
|
})
|
|
479
479
|
|
|
480
|
-
it(
|
|
480
|
+
it('should deeply merge nested objects', () => {
|
|
481
481
|
const target = { a: { b: { c: 1 } } }
|
|
482
482
|
const source = { a: { b: { d: 2 } } }
|
|
483
483
|
expect(merge({ ...target }, source)).toEqual({
|
|
@@ -486,39 +486,39 @@ describe("merge", () => {
|
|
|
486
486
|
})
|
|
487
487
|
|
|
488
488
|
// Security: prototype pollution protection
|
|
489
|
-
it(
|
|
489
|
+
it('should not pollute Object.prototype via __proto__', () => {
|
|
490
490
|
const malicious = JSON.parse('{"__proto__": {"polluted": true}}')
|
|
491
491
|
merge({}, malicious)
|
|
492
492
|
expect(({} as any).polluted).toBeUndefined()
|
|
493
493
|
})
|
|
494
494
|
|
|
495
|
-
it(
|
|
495
|
+
it('should not pollute via constructor.prototype', () => {
|
|
496
496
|
const malicious = JSON.parse('{"constructor": {"prototype": {"polluted": true}}}')
|
|
497
497
|
merge({}, malicious)
|
|
498
498
|
expect(({} as any).polluted).toBeUndefined()
|
|
499
499
|
})
|
|
500
500
|
|
|
501
|
-
it(
|
|
501
|
+
it('should not pollute via prototype key', () => {
|
|
502
502
|
const malicious = { prototype: { polluted: true } }
|
|
503
503
|
merge({}, malicious)
|
|
504
504
|
expect(({} as any).polluted).toBeUndefined()
|
|
505
505
|
})
|
|
506
506
|
|
|
507
|
-
it(
|
|
507
|
+
it('should overwrite target plain object with source array', () => {
|
|
508
508
|
const target = { a: { b: 1 } }
|
|
509
509
|
const source = { a: [1, 2, 3] }
|
|
510
510
|
const result = merge({ ...target }, source as any)
|
|
511
511
|
expect(result.a).toEqual([1, 2, 3])
|
|
512
512
|
})
|
|
513
513
|
|
|
514
|
-
it(
|
|
514
|
+
it('should overwrite target array with source plain object', () => {
|
|
515
515
|
const target = { a: [1, 2] } as any
|
|
516
|
-
const source = { a: { key:
|
|
516
|
+
const source = { a: { key: 'value' } }
|
|
517
517
|
const result = merge({ ...target }, source)
|
|
518
|
-
expect(result.a).toEqual({ key:
|
|
518
|
+
expect(result.a).toEqual({ key: 'value' })
|
|
519
519
|
})
|
|
520
520
|
|
|
521
|
-
it(
|
|
521
|
+
it('should handle source with class instances (non-plain objects)', () => {
|
|
522
522
|
class MyClass {
|
|
523
523
|
x = 1
|
|
524
524
|
}
|
|
@@ -529,7 +529,7 @@ describe("merge", () => {
|
|
|
529
529
|
expect(result.a).toBe(instance)
|
|
530
530
|
})
|
|
531
531
|
|
|
532
|
-
it(
|
|
532
|
+
it('should handle empty sources array', () => {
|
|
533
533
|
const target = { a: 1 }
|
|
534
534
|
const result = merge(target)
|
|
535
535
|
expect(result).toEqual({ a: 1 })
|