ivue 1.5.8 → 2.0.0
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 +170 -33
- package/bin/ivue.mjs +152 -0
- package/dist/Reactive.d.ts +131 -0
- package/dist/env.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.es.js +90 -81
- package/dist/index.umd.js +1 -1
- package/lib/Reactive.ts +509 -0
- package/lib/__tests__/Reactive.vitest.spec.ts +1056 -0
- package/lib/__tests__/ReactiveAdversarial.vitest.spec.ts +182 -0
- package/lib/__tests__/coverage-completion.vitest.spec.ts +24 -0
- package/lib/__tests__/ivue.vitest.spec.ts +1460 -150
- package/lib/env.d.ts +1 -0
- package/lib/index.ts +1 -1
- package/lib/ivue.ts +661 -241
- package/lib/kernel.ts +18 -0
- package/package.json +51 -13
- package/skills/ivue/SKILL.md +726 -0
- package/dist/ivue.d.ts +0 -234
|
@@ -3,15 +3,22 @@ import { PropType, isReactive, ref } from 'vue';
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
ExtractPropDefaultTypes,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
IVue,
|
|
7
|
+
__test__,
|
|
8
|
+
accessorsMethodsMaps,
|
|
9
|
+
clearCache,
|
|
10
|
+
deepClone,
|
|
10
11
|
iref,
|
|
11
|
-
propsWithDefaults,
|
|
12
12
|
iuse,
|
|
13
|
+
ivue,
|
|
14
|
+
propertiesAccessorsMaps,
|
|
15
|
+
propsWithDefaults,
|
|
13
16
|
} from '../ivue';
|
|
14
|
-
|
|
17
|
+
const {
|
|
18
|
+
copyOwnProps,
|
|
19
|
+
getClassAccessorsMethodsMap,
|
|
20
|
+
getClassPropertiesAccessorsMap,
|
|
21
|
+
} = __test__;
|
|
15
22
|
class Basic {
|
|
16
23
|
id = iref('id');
|
|
17
24
|
}
|
|
@@ -25,25 +32,29 @@ class Bit {
|
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
class Item extends Bit {
|
|
28
|
-
_width = iref(5)
|
|
35
|
+
_width = iref(5);
|
|
29
36
|
unit = iref('px');
|
|
37
|
+
_anotherTestProperty = iref('another-test-value');
|
|
30
38
|
get width(): string {
|
|
31
39
|
return this._width + this.unit;
|
|
32
40
|
}
|
|
33
|
-
set width(value: number
|
|
41
|
+
set width(value: number) {
|
|
34
42
|
this._width = value;
|
|
35
43
|
}
|
|
36
|
-
_height = iref(5)
|
|
44
|
+
_height = iref(5);
|
|
37
45
|
get height(): string {
|
|
38
46
|
return this._height + this.unit;
|
|
39
47
|
}
|
|
40
|
-
set height(value: number
|
|
48
|
+
set height(value: number) {
|
|
41
49
|
this._height = value;
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
class ProductItem extends Item {
|
|
46
54
|
_productType = iref('generic');
|
|
55
|
+
get anotherTestProperty() {
|
|
56
|
+
return this._anotherTestProperty;
|
|
57
|
+
}
|
|
47
58
|
get productType() {
|
|
48
59
|
return this._productType;
|
|
49
60
|
}
|
|
@@ -83,7 +94,7 @@ class RetailStoreItem extends StoreItem {
|
|
|
83
94
|
return this._testProperty;
|
|
84
95
|
}
|
|
85
96
|
calculateSize() {
|
|
86
|
-
return
|
|
97
|
+
return this._height + this._width;
|
|
87
98
|
}
|
|
88
99
|
}
|
|
89
100
|
|
|
@@ -98,17 +109,16 @@ class Entity {
|
|
|
98
109
|
this.height = height;
|
|
99
110
|
this.width = width;
|
|
100
111
|
}
|
|
101
|
-
init() {
|
|
112
|
+
init(test?: string) {
|
|
102
113
|
this._key = 5;
|
|
103
114
|
}
|
|
104
115
|
get key() {
|
|
105
116
|
return this._key;
|
|
106
117
|
}
|
|
107
|
-
static
|
|
108
|
-
notComputed: false,
|
|
109
|
-
};
|
|
118
|
+
static ivueDisableReactivity = new Set<keyof Entity>(['notComputed']);
|
|
110
119
|
|
|
111
120
|
logNotComputedCalls() {
|
|
121
|
+
console.log('Logging not computed call');
|
|
112
122
|
notComputedCalls++;
|
|
113
123
|
}
|
|
114
124
|
get notComputed() {
|
|
@@ -127,78 +137,135 @@ class Entity {
|
|
|
127
137
|
|
|
128
138
|
describe('ivue', () => {
|
|
129
139
|
describe('base architecture functions', () => {
|
|
130
|
-
|
|
131
|
-
|
|
140
|
+
describe('getClassAccessorsMethodsMap', () => {
|
|
141
|
+
it('should collect only accessor properties', () => {
|
|
142
|
+
class MyClass {
|
|
143
|
+
get prop() {
|
|
144
|
+
return 42;
|
|
145
|
+
}
|
|
146
|
+
set prop(value: number) {}
|
|
147
|
+
method() {}
|
|
148
|
+
}
|
|
149
|
+
const descriptors = getClassAccessorsMethodsMap(MyClass);
|
|
150
|
+
expect(descriptors.accessors.has('prop')).toBe(true);
|
|
151
|
+
expect(descriptors.methods.has('method')).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('getClassAccessorsMethodsMap correctly resolves all parent descriptors', () => {
|
|
156
|
+
const descriptors = getClassAccessorsMethodsMap(Item);
|
|
132
157
|
|
|
133
|
-
expect(descriptors.has('width')).toBe(true);
|
|
134
|
-
expect(typeof descriptors.get('width')?.get).toBe('function');
|
|
135
|
-
expect(typeof descriptors.get('width')?.set).toBe('function');
|
|
158
|
+
expect(descriptors.accessors.has('width')).toBe(true);
|
|
159
|
+
expect(typeof descriptors.accessors.get('width')?.get).toBe('function');
|
|
160
|
+
expect(typeof descriptors.accessors.get('width')?.set).toBe('function');
|
|
136
161
|
|
|
137
|
-
expect(descriptors.has('height')).toBe(true);
|
|
138
|
-
expect(typeof descriptors.get('height')?.get).toBe('function');
|
|
139
|
-
expect(typeof descriptors.get('height')?.set).toBe('function');
|
|
162
|
+
expect(descriptors.accessors.has('height')).toBe(true);
|
|
163
|
+
expect(typeof descriptors.accessors.get('height')?.get).toBe('function');
|
|
164
|
+
expect(typeof descriptors.accessors.get('height')?.set).toBe('function');
|
|
140
165
|
|
|
141
|
-
expect(descriptors.has('testProperty')).toBe(true);
|
|
142
|
-
expect(typeof descriptors.get('testProperty')?.get).toBe(
|
|
143
|
-
|
|
166
|
+
expect(descriptors.accessors.has('testProperty')).toBe(true);
|
|
167
|
+
expect(typeof descriptors.accessors.get('testProperty')?.get).toBe(
|
|
168
|
+
'undefined'
|
|
169
|
+
);
|
|
170
|
+
expect(typeof descriptors.accessors.get('testProperty')?.set).toBe(
|
|
171
|
+
'function'
|
|
172
|
+
);
|
|
144
173
|
});
|
|
145
174
|
|
|
146
|
-
it('
|
|
147
|
-
const descriptors =
|
|
175
|
+
it('getClassAccessorsMethodsMap correctly resolves all grand parent descriptors', () => {
|
|
176
|
+
const descriptors = getClassAccessorsMethodsMap(ProductItem);
|
|
148
177
|
|
|
149
|
-
expect(descriptors.has('width')).toBe(true);
|
|
150
|
-
expect(typeof descriptors.get('width')?.get).toBe('function');
|
|
151
|
-
expect(typeof descriptors.get('width')?.set).toBe('function');
|
|
178
|
+
expect(descriptors.accessors.has('width')).toBe(true);
|
|
179
|
+
expect(typeof descriptors.accessors.get('width')?.get).toBe('function');
|
|
180
|
+
expect(typeof descriptors.accessors.get('width')?.set).toBe('function');
|
|
152
181
|
|
|
153
|
-
expect(descriptors.has('height')).toBe(true);
|
|
154
|
-
expect(typeof descriptors.get('height')?.get).toBe('function');
|
|
155
|
-
expect(typeof descriptors.get('height')?.set).toBe('function');
|
|
182
|
+
expect(descriptors.accessors.has('height')).toBe(true);
|
|
183
|
+
expect(typeof descriptors.accessors.get('height')?.get).toBe('function');
|
|
184
|
+
expect(typeof descriptors.accessors.get('height')?.set).toBe('function');
|
|
156
185
|
|
|
157
|
-
expect(descriptors.has('testProperty')).toBe(true);
|
|
158
|
-
expect(typeof descriptors.get('testProperty')?.get).toBe(
|
|
159
|
-
|
|
186
|
+
expect(descriptors.accessors.has('testProperty')).toBe(true);
|
|
187
|
+
expect(typeof descriptors.accessors.get('testProperty')?.get).toBe(
|
|
188
|
+
'undefined'
|
|
189
|
+
);
|
|
190
|
+
expect(typeof descriptors.accessors.get('testProperty')?.set).toBe(
|
|
191
|
+
'function'
|
|
192
|
+
);
|
|
160
193
|
|
|
161
|
-
expect(descriptors.has('productType')).toBe(true);
|
|
162
|
-
expect(typeof descriptors.get('productType')?.get).toBe(
|
|
163
|
-
|
|
194
|
+
expect(descriptors.accessors.has('productType')).toBe(true);
|
|
195
|
+
expect(typeof descriptors.accessors.get('productType')?.get).toBe(
|
|
196
|
+
'function'
|
|
197
|
+
);
|
|
198
|
+
expect(typeof descriptors.accessors.get('productType')?.set).toBe(
|
|
199
|
+
'function'
|
|
200
|
+
);
|
|
164
201
|
|
|
165
|
-
expect(descriptors.has('productFeel')).toBe(true);
|
|
166
|
-
expect(typeof descriptors.get('productFeel')?.get).toBe(
|
|
167
|
-
|
|
202
|
+
expect(descriptors.accessors.has('productFeel')).toBe(true);
|
|
203
|
+
expect(typeof descriptors.accessors.get('productFeel')?.get).toBe(
|
|
204
|
+
'function'
|
|
205
|
+
);
|
|
206
|
+
expect(typeof descriptors.accessors.get('productFeel')?.set).toBe(
|
|
207
|
+
'undefined'
|
|
208
|
+
);
|
|
168
209
|
|
|
169
|
-
expect(descriptors.has('productId')).toBe(true);
|
|
170
|
-
expect(typeof descriptors.get('productId')?.get).toBe(
|
|
171
|
-
|
|
210
|
+
expect(descriptors.accessors.has('productId')).toBe(true);
|
|
211
|
+
expect(typeof descriptors.accessors.get('productId')?.get).toBe(
|
|
212
|
+
'undefined'
|
|
213
|
+
);
|
|
214
|
+
expect(typeof descriptors.accessors.get('productId')?.set).toBe(
|
|
215
|
+
'function'
|
|
216
|
+
);
|
|
172
217
|
});
|
|
173
218
|
|
|
174
|
-
it('
|
|
175
|
-
|
|
176
|
-
|
|
219
|
+
it('getClassPropertiesAccessorsMap correctly resolves all parent properties for native class', () => {
|
|
220
|
+
class ParentTest {
|
|
221
|
+
_parentWidth = 10;
|
|
222
|
+
get parentWidth() {
|
|
223
|
+
return this._parentWidth + 'px';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
class Test extends ParentTest {
|
|
227
|
+
_width = 5;
|
|
228
|
+
get width() {
|
|
229
|
+
return this._width + 'px';
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const item = new Test();
|
|
233
|
+
const { allProps } = getClassPropertiesAccessorsMap(item);
|
|
234
|
+
|
|
235
|
+
expect(allProps.has('_width')).toBe(true);
|
|
236
|
+
expect(allProps.has('width')).toBe(true);
|
|
237
|
+
expect(allProps.has('_parentWidth')).toBe(true);
|
|
238
|
+
expect(allProps.has('parentWidth')).toBe(true);
|
|
239
|
+
});
|
|
177
240
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
expect(
|
|
183
|
-
expect(
|
|
241
|
+
it('getClassPropertiesAccessorsMap correctly resolves all parent properties for ivue class', () => {
|
|
242
|
+
const item = ivue(Item);
|
|
243
|
+
const { allProps } = getClassPropertiesAccessorsMap(item);
|
|
244
|
+
|
|
245
|
+
expect(allProps.has('_width')).toBe(true);
|
|
246
|
+
expect(allProps.has('_height')).toBe(true);
|
|
247
|
+
expect(allProps.has('_testProperty')).toBe(true);
|
|
248
|
+
expect(allProps.has('width')).toBe(true);
|
|
249
|
+
expect(allProps.has('height')).toBe(true);
|
|
250
|
+
expect(allProps.has('testProperty')).toBe(true);
|
|
184
251
|
});
|
|
185
252
|
|
|
186
|
-
it('
|
|
253
|
+
it('getClassPropertiesAccessorsMap correctly resolves all grand parent properties', () => {
|
|
187
254
|
const item = ivue(ProductItem);
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
expect(
|
|
191
|
-
expect(
|
|
192
|
-
expect(
|
|
193
|
-
expect(
|
|
194
|
-
expect(
|
|
195
|
-
expect(
|
|
196
|
-
expect(
|
|
197
|
-
expect(
|
|
198
|
-
expect(
|
|
199
|
-
expect(
|
|
200
|
-
expect(
|
|
201
|
-
expect(
|
|
255
|
+
const { allProps } = getClassPropertiesAccessorsMap(item);
|
|
256
|
+
|
|
257
|
+
expect(allProps.has('_width')).toBe(true);
|
|
258
|
+
expect(allProps.has('_height')).toBe(true);
|
|
259
|
+
expect(allProps.has('_testProperty')).toBe(true);
|
|
260
|
+
expect(allProps.has('_productType')).toBe(true);
|
|
261
|
+
expect(allProps.has('_productFit')).toBe(true);
|
|
262
|
+
expect(allProps.has('_productId')).toBe(true);
|
|
263
|
+
expect(allProps.has('width')).toBe(true);
|
|
264
|
+
expect(allProps.has('height')).toBe(true);
|
|
265
|
+
expect(allProps.has('testProperty')).toBe(true);
|
|
266
|
+
expect(allProps.has('productType')).toBe(true);
|
|
267
|
+
expect(allProps.has('productFit')).toBe(true);
|
|
268
|
+
expect(allProps.has('productId')).toBe(true);
|
|
202
269
|
});
|
|
203
270
|
});
|
|
204
271
|
|
|
@@ -248,16 +315,6 @@ describe('ivue', () => {
|
|
|
248
315
|
expect(item.productType).toBe('sleek:retail');
|
|
249
316
|
});
|
|
250
317
|
|
|
251
|
-
it('parent setters from the prototype ancestors chain are working', () => {
|
|
252
|
-
const item = ivue(RetailStoreItem);
|
|
253
|
-
|
|
254
|
-
expect(item.productType).toBe('sleek:retail');
|
|
255
|
-
/** SET VIA SETTER */
|
|
256
|
-
// @ts-expect-error ivue supports setting set and get separately in different levels of class prototype ancestors chain
|
|
257
|
-
item.productType = 'new-retail';
|
|
258
|
-
expect(item.productType).toBe('sleek:new-retail');
|
|
259
|
-
});
|
|
260
|
-
|
|
261
318
|
it('parent setters that are set at different levels in the prototype ancestors chain work together in harmony', () => {
|
|
262
319
|
const item = ivue(RetailStoreItem);
|
|
263
320
|
/**
|
|
@@ -269,28 +326,20 @@ describe('ivue', () => {
|
|
|
269
326
|
item.productFeel = 'new-feel';
|
|
270
327
|
expect(item.productType).toBe('new-feel:retail');
|
|
271
328
|
});
|
|
272
|
-
|
|
273
|
-
it('parent setters are working even without getters', () => {
|
|
274
|
-
const item = ivue(RetailStoreItem);
|
|
275
|
-
|
|
276
|
-
expect(item._testProperty).toBe('test-value');
|
|
277
|
-
/** SET VIA SETTER */
|
|
278
|
-
// @ts-expect-error ivue supports setting set and get separately in different levels of class prototype ancestors chain
|
|
279
|
-
item.testProperty = 'new-test-value';
|
|
280
|
-
expect(item._testProperty).toBe('new-test-value');
|
|
281
|
-
});
|
|
282
329
|
});
|
|
283
330
|
|
|
284
331
|
describe('ivue toRefs conversion', () => {
|
|
285
332
|
it('toRefs all properties', () => {
|
|
286
|
-
const
|
|
287
|
-
|
|
333
|
+
const instance = ivue(RetailStoreItem);
|
|
334
|
+
const { _height, _width, height, width, testProperty } =
|
|
335
|
+
instance.toRefs();
|
|
288
336
|
|
|
289
337
|
expect(_height.value).toBe(5);
|
|
290
338
|
expect(_width.value).toBe(5);
|
|
291
339
|
expect(height.value).toBe('5px');
|
|
292
340
|
expect(width.value).toBe('5px');
|
|
293
|
-
expect(
|
|
341
|
+
expect(testProperty.value).toBe('test-value');
|
|
342
|
+
expect(instance.calculateSize()).toBe(10);
|
|
294
343
|
});
|
|
295
344
|
|
|
296
345
|
it('toRefs all properties with pre-calculated computeds', () => {
|
|
@@ -300,27 +349,30 @@ describe('ivue', () => {
|
|
|
300
349
|
item.height;
|
|
301
350
|
item.width;
|
|
302
351
|
|
|
303
|
-
const { height, width
|
|
352
|
+
const { height, width } = item.toRefs();
|
|
304
353
|
|
|
305
354
|
expect(height.value).toBe('5px');
|
|
306
355
|
expect(width.value).toBe('5px');
|
|
307
|
-
expect(calculateSize()).toBe(10);
|
|
356
|
+
expect(item.calculateSize()).toBe(10);
|
|
308
357
|
});
|
|
309
358
|
|
|
310
359
|
it("toRefs(['prop', ...]) specific properties", () => {
|
|
311
360
|
const item = ivue(ProductItem);
|
|
312
361
|
|
|
313
|
-
const { _height, _width, height, width } =
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
362
|
+
const { _height, _width, height, width, anotherTestProperty } =
|
|
363
|
+
item.toRefs([
|
|
364
|
+
'_width',
|
|
365
|
+
'_height',
|
|
366
|
+
'height',
|
|
367
|
+
'width',
|
|
368
|
+
'anotherTestProperty',
|
|
369
|
+
]);
|
|
319
370
|
|
|
320
371
|
expect(_height.value).toBe(5);
|
|
321
372
|
expect(_width.value).toBe(5);
|
|
322
373
|
expect(height.value).toBe('5px');
|
|
323
374
|
expect(width.value).toBe('5px');
|
|
375
|
+
expect(anotherTestProperty.value).toBe('another-test-value');
|
|
324
376
|
});
|
|
325
377
|
|
|
326
378
|
it("toRefs(['prop', ...]) specific properties with pre-calculated computeds", () => {
|
|
@@ -344,9 +396,13 @@ describe('ivue', () => {
|
|
|
344
396
|
});
|
|
345
397
|
|
|
346
398
|
it("toRefs(['prop', ...]) specific functions", () => {
|
|
347
|
-
const {
|
|
399
|
+
const { _productType, testProperty } = ivue(RetailStoreItem).toRefs([
|
|
400
|
+
'_productType',
|
|
401
|
+
'testProperty',
|
|
402
|
+
]);
|
|
348
403
|
|
|
349
|
-
expect(
|
|
404
|
+
expect(_productType.value).toBe('retail');
|
|
405
|
+
expect(testProperty.value).toBe('test-value');
|
|
350
406
|
});
|
|
351
407
|
|
|
352
408
|
it('toRefs() all properties with using a computed as a setter without a getter', () => {
|
|
@@ -436,9 +492,9 @@ describe('ivue', () => {
|
|
|
436
492
|
const useComposable = () => {
|
|
437
493
|
return {
|
|
438
494
|
x: ref(0),
|
|
439
|
-
y: ref(0)
|
|
440
|
-
}
|
|
441
|
-
}
|
|
495
|
+
y: ref(0),
|
|
496
|
+
};
|
|
497
|
+
};
|
|
442
498
|
it('iref returns standard ref with .value but unwraps type', () => {
|
|
443
499
|
const bool = iref(true);
|
|
444
500
|
const str = iref('string');
|
|
@@ -453,52 +509,193 @@ describe('ivue', () => {
|
|
|
453
509
|
const iuseComposable = iuse(useComposable());
|
|
454
510
|
|
|
455
511
|
// @ts-expect-error Unwraps type, only to be used within ivue Class or Vue 3 `reactive()` context
|
|
456
|
-
expect(iuseComposable.x.value).toBe(0)
|
|
512
|
+
expect(iuseComposable.x.value).toBe(0);
|
|
457
513
|
// @ts-expect-error Unwraps type, only to be used within ivue Class or Vue 3 `reactive()` context
|
|
458
514
|
expect(iuseComposable.y.value).toBe(0);
|
|
459
515
|
});
|
|
516
|
+
});
|
|
460
517
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
// @ts-expect-error Unwraps type, only to be used within ivue Class or Vue 3 `reactive()` context
|
|
472
|
-
expect(iuseComposable2.width.value).toBe(5);
|
|
473
|
-
});
|
|
518
|
+
describe('util functions', () => {
|
|
519
|
+
class Animal {
|
|
520
|
+
kind: string;
|
|
521
|
+
constructor(kind: string) {
|
|
522
|
+
this.kind = kind;
|
|
523
|
+
}
|
|
524
|
+
speak() {
|
|
525
|
+
return `I am a ${this.kind}`;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
474
528
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
529
|
+
describe('deepClone', () => {
|
|
530
|
+
it('clones primitives', () => {
|
|
531
|
+
expect(deepClone(1)).toBe(1);
|
|
532
|
+
expect(deepClone('a')).toBe('a');
|
|
533
|
+
expect(deepClone(true)).toBe(true);
|
|
534
|
+
expect(deepClone(null)).toBe(null);
|
|
535
|
+
expect(deepClone(undefined)).toBeUndefined();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('clones arrays deeply', () => {
|
|
539
|
+
const src: any[] = [1, { a: [2, 3] }];
|
|
540
|
+
const out = deepClone(src);
|
|
541
|
+
expect(out).toEqual(src);
|
|
542
|
+
expect(out).not.toBe(src);
|
|
543
|
+
expect(out[1]).not.toBe(src[1]);
|
|
544
|
+
expect(out[1].a).not.toBe(src[1].a);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('clones plain objects deeply and preserves symbols & non-enumerables', () => {
|
|
548
|
+
const sym = Symbol('s');
|
|
549
|
+
const src: any = { a: 1, b: { c: 2 } };
|
|
550
|
+
Object.defineProperty(src, 'hidden', { value: 42, enumerable: false });
|
|
551
|
+
src[sym] = 7;
|
|
552
|
+
|
|
553
|
+
const out: any = deepClone(src);
|
|
554
|
+
|
|
555
|
+
expect(out).toEqual(src);
|
|
556
|
+
expect(out).not.toBe(src);
|
|
557
|
+
// deep
|
|
558
|
+
expect(out.b).not.toBe(src.b);
|
|
559
|
+
// non-enumerable preserved
|
|
560
|
+
const d = Object.getOwnPropertyDescriptor(out, 'hidden');
|
|
561
|
+
expect(d?.enumerable).toBe(false);
|
|
562
|
+
expect(d?.value).toBe(42);
|
|
563
|
+
// symbol preserved
|
|
564
|
+
expect(out[sym]).toBe(7);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('handles circular references', () => {
|
|
568
|
+
const src: any = { name: 'root' };
|
|
569
|
+
src.self = src;
|
|
570
|
+
const out: any = deepClone(src);
|
|
571
|
+
expect(out).not.toBe(src);
|
|
572
|
+
expect(out.name).toBe('root');
|
|
573
|
+
expect(out.self).toBe(out);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('clones Map and Set deeply', () => {
|
|
577
|
+
const m = new Map<any, any>();
|
|
578
|
+
const k = { k: 1 };
|
|
579
|
+
m.set(k, { v: 2 });
|
|
580
|
+
const s = new Set<any>([k, { x: 3 }]);
|
|
581
|
+
|
|
582
|
+
const outM = deepClone(m);
|
|
583
|
+
const outS = deepClone(s);
|
|
584
|
+
|
|
585
|
+
expect(outM).not.toBe(m);
|
|
586
|
+
const [outKey, outVal] = [...outM.entries()][0];
|
|
587
|
+
expect(outKey).not.toBe(k);
|
|
588
|
+
expect(outVal).toEqual({ v: 2 });
|
|
589
|
+
expect(outVal).not.toBe(m.get(k));
|
|
590
|
+
|
|
591
|
+
expect(outS).not.toBe(s);
|
|
592
|
+
const arr = [...outS.values()];
|
|
593
|
+
expect(arr.length).toBe(2);
|
|
594
|
+
expect(typeof arr[0]).toBe('object');
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('clones Date and RegExp', () => {
|
|
598
|
+
const d = new Date();
|
|
599
|
+
const r = /ab+c/gi;
|
|
600
|
+
r.lastIndex = 2;
|
|
601
|
+
|
|
602
|
+
const outD = deepClone(d);
|
|
603
|
+
const outR = deepClone(r);
|
|
604
|
+
|
|
605
|
+
expect(outD).not.toBe(d);
|
|
606
|
+
expect(outD.getTime()).toBe(d.getTime());
|
|
607
|
+
|
|
608
|
+
expect(outR).not.toBe(r);
|
|
609
|
+
expect(outR.source).toBe(r.source);
|
|
610
|
+
expect(outR.flags).toBe(r.flags);
|
|
611
|
+
expect(outR.lastIndex).toBe(2);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('preserves prototype for class instances and clones fields', () => {
|
|
615
|
+
const a = new Animal('cat');
|
|
616
|
+
(a as any).bag = { deep: { v: 1 } };
|
|
617
|
+
|
|
618
|
+
const out = deepClone(a) as Animal & { bag: any };
|
|
619
|
+
expect(out instanceof Animal).toBe(true);
|
|
620
|
+
expect(out.speak()).toBe('I am a cat');
|
|
621
|
+
expect(out).not.toBe(a);
|
|
622
|
+
expect(out.bag).not.toBe((a as any).bag);
|
|
623
|
+
expect(out.bag.deep.v).toBe(1);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('does not invoke getters while cloning', () => {
|
|
627
|
+
let getterCalls = 0;
|
|
628
|
+
const src = Object.defineProperties(
|
|
629
|
+
{},
|
|
630
|
+
{
|
|
631
|
+
x: {
|
|
632
|
+
get() {
|
|
633
|
+
getterCalls++;
|
|
634
|
+
return 123;
|
|
635
|
+
},
|
|
636
|
+
enumerable: true,
|
|
637
|
+
configurable: true,
|
|
638
|
+
},
|
|
639
|
+
}
|
|
640
|
+
);
|
|
479
641
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
642
|
+
const out = deepClone(src);
|
|
643
|
+
const desc = Object.getOwnPropertyDescriptor(out, 'x');
|
|
644
|
+
expect(getterCalls).toBe(0);
|
|
645
|
+
expect(typeof desc?.get).toBe('function');
|
|
646
|
+
});
|
|
484
647
|
});
|
|
485
|
-
});
|
|
486
|
-
|
|
487
648
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
649
|
+
describe('copyOwnProps (used by deepClone)', () => {
|
|
650
|
+
it('copies string & symbol keys, preserves descriptors, avoids invoking getters', () => {
|
|
651
|
+
const sym = Symbol('z');
|
|
652
|
+
let getterCalls = 0;
|
|
653
|
+
|
|
654
|
+
const source: any = {};
|
|
655
|
+
Object.defineProperties(source, {
|
|
656
|
+
a: { value: 1, enumerable: true, configurable: true, writable: true },
|
|
657
|
+
b: {
|
|
658
|
+
value: 2,
|
|
659
|
+
enumerable: false,
|
|
660
|
+
configurable: false,
|
|
661
|
+
writable: false,
|
|
662
|
+
},
|
|
663
|
+
c: {
|
|
664
|
+
get() {
|
|
665
|
+
getterCalls++;
|
|
666
|
+
return 10;
|
|
667
|
+
},
|
|
668
|
+
enumerable: true,
|
|
669
|
+
configurable: true,
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
source[sym] = 99;
|
|
673
|
+
|
|
674
|
+
const target: any = {};
|
|
675
|
+
const seen = new WeakMap();
|
|
676
|
+
// call the helper the way deepClone uses it
|
|
677
|
+
copyOwnProps(source, target, new Map(), seen);
|
|
678
|
+
|
|
679
|
+
// getters preserved (not executed)
|
|
680
|
+
expect(getterCalls).toBe(0);
|
|
681
|
+
|
|
682
|
+
const descA = Object.getOwnPropertyDescriptor(target, 'a')!;
|
|
683
|
+
const descB = Object.getOwnPropertyDescriptor(target, 'b')!;
|
|
684
|
+
const descC = Object.getOwnPropertyDescriptor(target, 'c')!;
|
|
685
|
+
const descZ = Object.getOwnPropertyDescriptor(target, sym)!;
|
|
686
|
+
|
|
687
|
+
expect(descA.value).toBe(1);
|
|
688
|
+
expect(descA.enumerable).toBe(true);
|
|
689
|
+
|
|
690
|
+
expect('value' in descB).toBe(true);
|
|
691
|
+
expect(descB.enumerable).toBe(false);
|
|
692
|
+
expect(descB.configurable).toBe(false);
|
|
693
|
+
expect(descB.writable).toBe(false);
|
|
694
|
+
|
|
695
|
+
expect(typeof descC.get).toBe('function');
|
|
696
|
+
|
|
697
|
+
expect(descZ.value).toBe(99);
|
|
698
|
+
});
|
|
502
699
|
});
|
|
503
700
|
|
|
504
701
|
it('propsWithDefaults correctly assigns defaults to typed props', () => {
|
|
@@ -563,15 +760,1128 @@ describe('ivue', () => {
|
|
|
563
760
|
|
|
564
761
|
// Class
|
|
565
762
|
expect(_propsWithDefaults.runner.default).toBeTypeOf('function');
|
|
566
|
-
expect(isClass((_propsWithDefaults.runner.default as () => any)())).toBe(
|
|
567
|
-
true
|
|
568
|
-
);
|
|
569
763
|
|
|
570
764
|
// Function
|
|
571
765
|
expect(_propsWithDefaults.fn.default).toBeTypeOf('function');
|
|
572
|
-
expect(isClass((_propsWithDefaults.fn.default as () => any)())).toBe(
|
|
573
|
-
false
|
|
574
|
-
);
|
|
575
766
|
});
|
|
576
767
|
});
|
|
768
|
+
|
|
769
|
+
describe('propsWithDefaults', () => {
|
|
770
|
+
it('wraps arrays/objects/class instances in factory functions & leaves primitives', () => {
|
|
771
|
+
class Foo {
|
|
772
|
+
x = 1;
|
|
773
|
+
}
|
|
774
|
+
const fooInstance = new Foo();
|
|
775
|
+
|
|
776
|
+
const typedProps = {
|
|
777
|
+
n: { type: Number as any },
|
|
778
|
+
s: { type: String as any },
|
|
779
|
+
a: { type: Array as any },
|
|
780
|
+
o: { type: Object as any },
|
|
781
|
+
c: { type: Object as any }, // class instance default
|
|
782
|
+
fn: { type: Function as any }, // function-as-value should be returned as value factory
|
|
783
|
+
Ctor: { type: Function as any }, // class constructor as value
|
|
784
|
+
_undefined: { type: String as any },
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
const defaults = {
|
|
788
|
+
n: 5,
|
|
789
|
+
s: 'hi',
|
|
790
|
+
a: [1, 2],
|
|
791
|
+
o: { k: 3 },
|
|
792
|
+
c: fooInstance,
|
|
793
|
+
fn: function valueFn() {
|
|
794
|
+
return 123;
|
|
795
|
+
},
|
|
796
|
+
Ctor: Foo, // constructor itself as default value (intentionally)
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
const out = propsWithDefaults(defaults, { ...typedProps }) as Record<
|
|
800
|
+
string,
|
|
801
|
+
{ default: any }
|
|
802
|
+
>;
|
|
803
|
+
|
|
804
|
+
// primitives copied as-is (no factory)
|
|
805
|
+
expect(out.n.default).toBe(5);
|
|
806
|
+
expect(out.s.default).toBe('hi');
|
|
807
|
+
expect(out._undefined.default).toBe(undefined);
|
|
808
|
+
|
|
809
|
+
// arrays/objects/class instances are factories returning deep clones
|
|
810
|
+
const a1 = out.a.default();
|
|
811
|
+
const a2 = out.a.default();
|
|
812
|
+
expect(Array.isArray(a1)).toBe(true);
|
|
813
|
+
expect(a1).not.toBe(a2);
|
|
814
|
+
a1.push(9);
|
|
815
|
+
expect(a2.includes(9)).toBe(false);
|
|
816
|
+
|
|
817
|
+
const o1 = out.o.default();
|
|
818
|
+
const o2 = out.o.default();
|
|
819
|
+
expect(o1).not.toBe(o2);
|
|
820
|
+
o1.k = 10;
|
|
821
|
+
expect(o2.k).toBe(3);
|
|
822
|
+
|
|
823
|
+
const c1 = out.c.default();
|
|
824
|
+
const c2 = out.c.default();
|
|
825
|
+
expect(c1).not.toBe(c2);
|
|
826
|
+
expect(c1 instanceof Foo).toBe(true);
|
|
827
|
+
c1.x = 7;
|
|
828
|
+
expect(c2.x).toBe(1);
|
|
829
|
+
|
|
830
|
+
// function-as-value and class constructor: both wrapped as "() => def"
|
|
831
|
+
const fnFactory = out.fn.default as any;
|
|
832
|
+
const ctorFactory = out.Ctor.default as any;
|
|
833
|
+
expect(typeof fnFactory).toBe('function');
|
|
834
|
+
expect(typeof ctorFactory).toBe('function');
|
|
835
|
+
expect(fnFactory()).toBe(123); // returns the function value itself
|
|
836
|
+
expect(ctorFactory()).toBe(Foo); // returns the constructor itself
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it('ignores keys not present in typedProps', () => {
|
|
840
|
+
const typedProps = { n: { type: Number as any } };
|
|
841
|
+
const defaults = { n: 1, extra: 2 };
|
|
842
|
+
const out = propsWithDefaults(defaults, { ...typedProps });
|
|
843
|
+
expect(out.n.default).toBe(1);
|
|
844
|
+
// no throw for unknown keys
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it('produces isolated defaults across component instances', () => {
|
|
848
|
+
const typedProps = {
|
|
849
|
+
bag: { type: Object as any },
|
|
850
|
+
list: { type: Array as any },
|
|
851
|
+
};
|
|
852
|
+
const defaults = { bag: { a: 1 }, list: [1, 2, 3] };
|
|
853
|
+
const out = propsWithDefaults(defaults, { ...typedProps }) as Record<
|
|
854
|
+
string,
|
|
855
|
+
{ default: () => any }
|
|
856
|
+
>;
|
|
857
|
+
|
|
858
|
+
const bag1 = out.bag.default();
|
|
859
|
+
const bag2 = out.bag.default();
|
|
860
|
+
expect(bag1).not.toBe(bag2);
|
|
861
|
+
|
|
862
|
+
const list1 = out.list.default();
|
|
863
|
+
const list2 = out.list.default();
|
|
864
|
+
expect(list1).not.toBe(list2);
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
describe('ivue constructor error handling', () => {
|
|
869
|
+
it('does not throw error for invalid constructor args', () => {
|
|
870
|
+
class MyClass {
|
|
871
|
+
constructor(a: number) {
|
|
872
|
+
// No validation, so 'invalid' is accepted
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
expect(() => ivue(MyClass, 'invalid' as any)).not.toThrow();
|
|
876
|
+
const instance = ivue(MyClass, 'invalid' as any);
|
|
877
|
+
expect(isReactive(instance)).toBe(true); // Still creates reactive instance
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
describe('Cache Cleanup', () => {
|
|
882
|
+
it('should clear specific class from accessorsMethodsMaps', () => {
|
|
883
|
+
const MyClass = class {};
|
|
884
|
+
ivue(MyClass);
|
|
885
|
+
expect(accessorsMethodsMaps.has(MyClass)).toBe(true);
|
|
886
|
+
clearCache();
|
|
887
|
+
expect(accessorsMethodsMaps.has(MyClass)).toBe(false);
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
it('should reset accessorsMethodsMaps', () => {
|
|
891
|
+
const MyClass = class {};
|
|
892
|
+
ivue(MyClass);
|
|
893
|
+
clearCache();
|
|
894
|
+
expect(accessorsMethodsMaps.has(MyClass)).toBe(false);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
it('should clear specific class from propertiesAccessorsMaps', () => {
|
|
898
|
+
const MyClass = class {
|
|
899
|
+
prop = 42;
|
|
900
|
+
};
|
|
901
|
+
const instance = ivue(MyClass);
|
|
902
|
+
getClassPropertiesAccessorsMap(instance);
|
|
903
|
+
expect(propertiesAccessorsMaps.has(MyClass)).toBe(true);
|
|
904
|
+
clearCache();
|
|
905
|
+
expect(propertiesAccessorsMaps.has(MyClass)).toBe(false);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it('should reset propertiesAccessorsMaps', () => {
|
|
909
|
+
const MyClass = class {
|
|
910
|
+
prop = 42;
|
|
911
|
+
};
|
|
912
|
+
const instance = ivue(MyClass);
|
|
913
|
+
getClassPropertiesAccessorsMap(instance);
|
|
914
|
+
clearCache();
|
|
915
|
+
expect(propertiesAccessorsMaps.has(MyClass)).toBe(false);
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
// Helpers for readability
|
|
920
|
+
const isEnumerable = (obj: object, key: string) =>
|
|
921
|
+
Object.prototype.propertyIsEnumerable.call(obj, key);
|
|
922
|
+
|
|
923
|
+
describe('descriptor flags (enumerable/configurable)', () => {
|
|
924
|
+
class FlagsBase {
|
|
925
|
+
// Default: enumerable=false, configurable=true
|
|
926
|
+
get hidden() {
|
|
927
|
+
return 1;
|
|
928
|
+
}
|
|
929
|
+
set hidden(_: number) {}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
class Flags extends FlagsBase {
|
|
933
|
+
// no-op field just to have some instance state
|
|
934
|
+
_x = 1;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Make an explicitly enumerable & configurable accessor on the prototype
|
|
938
|
+
Object.defineProperty(Flags.prototype, 'visible', {
|
|
939
|
+
get() {
|
|
940
|
+
return 2;
|
|
941
|
+
},
|
|
942
|
+
set(_: number) {},
|
|
943
|
+
enumerable: true,
|
|
944
|
+
configurable: true,
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
// Make an explicitly non-configurable accessor on the prototype
|
|
948
|
+
Object.defineProperty(Flags.prototype, 'locked', {
|
|
949
|
+
get() {
|
|
950
|
+
return 3;
|
|
951
|
+
},
|
|
952
|
+
set(_: number) {},
|
|
953
|
+
enumerable: true,
|
|
954
|
+
configurable: false,
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
it('mirrors default flags for class accessors: enumerable=false, configurable=true', () => {
|
|
958
|
+
const instance = ivue(Flags);
|
|
959
|
+
const d = Object.getOwnPropertyDescriptor(instance, 'hidden');
|
|
960
|
+
|
|
961
|
+
expect(d?.enumerable).toBe(false);
|
|
962
|
+
expect(d?.configurable).toBe(true);
|
|
963
|
+
expect(isEnumerable(instance, 'hidden')).toBe(false);
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
it('preserves enumerable=true from prototype on the reactive instance', () => {
|
|
967
|
+
const instance = ivue(Flags);
|
|
968
|
+
const d = Object.getOwnPropertyDescriptor(instance, 'visible');
|
|
969
|
+
|
|
970
|
+
expect(d?.enumerable).toBe(true);
|
|
971
|
+
expect(isEnumerable(instance, 'visible')).toBe(true);
|
|
972
|
+
|
|
973
|
+
// sanity check: hidden is not in keys; visible is
|
|
974
|
+
const keys = Object.keys(instance);
|
|
975
|
+
expect(keys.includes('visible')).toBe(true);
|
|
976
|
+
expect(keys.includes('hidden')).toBe(false);
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
it('preserves configurable=false from prototype (cannot redefine)', () => {
|
|
980
|
+
const instance = ivue(Flags);
|
|
981
|
+
const d = Object.getOwnPropertyDescriptor(instance, 'locked');
|
|
982
|
+
|
|
983
|
+
expect(d?.configurable).toBe(false);
|
|
984
|
+
|
|
985
|
+
// Attempting to redefine should throw a TypeError
|
|
986
|
+
expect(() =>
|
|
987
|
+
Object.defineProperty(instance, 'locked', {
|
|
988
|
+
get() {
|
|
989
|
+
return 99;
|
|
990
|
+
},
|
|
991
|
+
})
|
|
992
|
+
).toThrowError(TypeError);
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
it('configurable=true allows redefining accessor on the reactive instance', () => {
|
|
996
|
+
const instance = ivue(Flags);
|
|
997
|
+
|
|
998
|
+
// Before redefine
|
|
999
|
+
// @ts-expect-error anyway
|
|
1000
|
+
expect(instance.visible).toBe(2);
|
|
1001
|
+
|
|
1002
|
+
// Redefine should succeed (configurable=true)
|
|
1003
|
+
Object.defineProperty(instance, 'visible', {
|
|
1004
|
+
get() {
|
|
1005
|
+
return 42;
|
|
1006
|
+
},
|
|
1007
|
+
enumerable: true,
|
|
1008
|
+
configurable: true,
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
// @ts-expect-error anyway
|
|
1012
|
+
expect(instance.visible).toBe(42);
|
|
1013
|
+
|
|
1014
|
+
// And flags remain as set on redefinition
|
|
1015
|
+
const d = Object.getOwnPropertyDescriptor(instance, 'visible');
|
|
1016
|
+
expect(d?.enumerable).toBe(true);
|
|
1017
|
+
expect(d?.configurable).toBe(true);
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
describe('Symbols handling', () => {
|
|
1022
|
+
it('ignores symbol keys in getClassPropertiesAccessorsMap and toRefs', () => {
|
|
1023
|
+
const S = Symbol('secret');
|
|
1024
|
+
|
|
1025
|
+
class WithSymbol {
|
|
1026
|
+
[S] = iref(123); // symbol "data"
|
|
1027
|
+
get [Symbol.for('acc')]() {
|
|
1028
|
+
return 1;
|
|
1029
|
+
} // symbol accessor
|
|
1030
|
+
method() {
|
|
1031
|
+
return 42;
|
|
1032
|
+
} // normal method
|
|
1033
|
+
x = iref(7); // normal data
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const instance = ivue(WithSymbol);
|
|
1037
|
+
|
|
1038
|
+
// sanity: Vue proxy still has the symbol (we won’t surface it)
|
|
1039
|
+
expect(instance[S]).toBe(123);
|
|
1040
|
+
|
|
1041
|
+
// our property collector should not list symbols
|
|
1042
|
+
const { allProps } = getClassPropertiesAccessorsMap(instance);
|
|
1043
|
+
// only string keys
|
|
1044
|
+
for (const k of allProps.keys()) {
|
|
1045
|
+
expect(typeof k).toBe('string');
|
|
1046
|
+
}
|
|
1047
|
+
expect(allProps.has('x')).toBe(true);
|
|
1048
|
+
expect(allProps.has('method')).toBe(false);
|
|
1049
|
+
|
|
1050
|
+
// toRefs() should not expose symbol-backed fields
|
|
1051
|
+
const r = instance.toRefs();
|
|
1052
|
+
expect('x' in r).toBe(true);
|
|
1053
|
+
expect('method' in r).toBe(false);
|
|
1054
|
+
expect(Object.getOwnPropertySymbols(r).length).toBe(0);
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
// Helper: flush microtasks if we depend on Vue computed scheduling, etc.
|
|
1059
|
+
// (We actually shouldn't need it for these sync tests, but it's here if you want.)
|
|
1060
|
+
|
|
1061
|
+
describe('ivue clone()', () => {
|
|
1062
|
+
it('deep clones plain data props and keeps them reactive', async () => {
|
|
1063
|
+
class Store {
|
|
1064
|
+
count = 1;
|
|
1065
|
+
nested = { msg: 'hi' };
|
|
1066
|
+
|
|
1067
|
+
get double() {
|
|
1068
|
+
return this.count * 2;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
inc() {
|
|
1072
|
+
this.count++;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const a = ivue(Store); // IVue<Store>
|
|
1077
|
+
const b = a.clone(); // clone of a
|
|
1078
|
+
|
|
1079
|
+
expect(b.toRefs(['double']).double.value).toBe(2);
|
|
1080
|
+
|
|
1081
|
+
// different object identities for data
|
|
1082
|
+
expect(b).not.toBe(a);
|
|
1083
|
+
expect(b.nested).not.toBe(a.nested);
|
|
1084
|
+
|
|
1085
|
+
// values copied
|
|
1086
|
+
expect(b.count).toBe(1);
|
|
1087
|
+
expect(b.nested.msg).toBe('hi');
|
|
1088
|
+
|
|
1089
|
+
// reactivity preserved: changing b does not change a
|
|
1090
|
+
b.count = 10;
|
|
1091
|
+
b.nested.msg = 'bye';
|
|
1092
|
+
|
|
1093
|
+
expect(a.count).toBe(1);
|
|
1094
|
+
expect(a.nested.msg).toBe('hi');
|
|
1095
|
+
expect(b.count).toBe(10);
|
|
1096
|
+
expect(b.nested.msg).toBe('bye');
|
|
1097
|
+
|
|
1098
|
+
// computed accessors should still work on clone & reflect clone state
|
|
1099
|
+
expect(a.double).toBe(2);
|
|
1100
|
+
expect(b.double).toBe(20);
|
|
1101
|
+
|
|
1102
|
+
// methods should be bound to the clone
|
|
1103
|
+
b.inc();
|
|
1104
|
+
expect(b.count).toBe(11);
|
|
1105
|
+
|
|
1106
|
+
// make sure calling b.inc didn't touch a
|
|
1107
|
+
expect(a.count).toBe(1);
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
it('respects static ivueCloneByReference (shares reference instead of deep cloning)', () => {
|
|
1111
|
+
class RouterMock {
|
|
1112
|
+
name = 'main-router';
|
|
1113
|
+
pushCalls: any[] = [];
|
|
1114
|
+
push(path: string) {
|
|
1115
|
+
this.pushCalls.push(path);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
class StoreWithRouter {
|
|
1120
|
+
static ivueCloneByReference = new Set(['router']);
|
|
1121
|
+
|
|
1122
|
+
router = new RouterMock();
|
|
1123
|
+
localVal = { n: 1 };
|
|
1124
|
+
|
|
1125
|
+
get routeNameUpper() {
|
|
1126
|
+
return this.router.name.toUpperCase();
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const a = ivue(StoreWithRouter);
|
|
1131
|
+
const b = a.clone();
|
|
1132
|
+
|
|
1133
|
+
// router is same reference because ivueCloneByReference has 'router'
|
|
1134
|
+
expect(b.router).toBe(a.router);
|
|
1135
|
+
|
|
1136
|
+
// BUT non-listed props are still deep cloned
|
|
1137
|
+
expect(b.localVal).not.toBe(a.localVal);
|
|
1138
|
+
expect(b.localVal.n).toBe(1);
|
|
1139
|
+
|
|
1140
|
+
// edits to shared ref affect both
|
|
1141
|
+
b.router.name = 'child-router';
|
|
1142
|
+
expect(a.router.name).toBe('child-router');
|
|
1143
|
+
expect(b.router.name).toBe('child-router');
|
|
1144
|
+
|
|
1145
|
+
// edits to deep-cloned prop don't leak
|
|
1146
|
+
b.localVal.n = 999;
|
|
1147
|
+
expect(a.localVal.n).toBe(1);
|
|
1148
|
+
expect(b.localVal.n).toBe(999);
|
|
1149
|
+
|
|
1150
|
+
// computed still uses the clone's proxy as `this`
|
|
1151
|
+
expect(a.routeNameUpper).toBe('CHILD-ROUTER');
|
|
1152
|
+
expect(b.routeNameUpper).toBe('CHILD-ROUTER');
|
|
1153
|
+
});
|
|
1154
|
+
it('covers the else path: standard object fields in ivueDisableReactivity are marked raw', () => {
|
|
1155
|
+
// 1. Define a class with a standard field (NOT a getter)
|
|
1156
|
+
class RawFieldTest {
|
|
1157
|
+
// This is a plain instance field.
|
|
1158
|
+
// It is NOT in the prototype accessors map, so it falls to the 'else' block.
|
|
1159
|
+
data = {
|
|
1160
|
+
secret: 'raw-value',
|
|
1161
|
+
nested: { id: 1 }
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
// We explicitly verify that the code marks this specific field as raw
|
|
1165
|
+
static ivueDisableReactivity = new Set(['data']);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const instance = ivue(RawFieldTest).clone();
|
|
1169
|
+
|
|
1170
|
+
// 2. Access the property
|
|
1171
|
+
const val = instance.data;
|
|
1172
|
+
|
|
1173
|
+
// 3. Verify values are correct
|
|
1174
|
+
expect(val.secret).toBe('raw-value');
|
|
1175
|
+
|
|
1176
|
+
// 4. CRITICAL: The instance itself is reactive...
|
|
1177
|
+
expect(isReactive(instance)).toBe(true);
|
|
1178
|
+
|
|
1179
|
+
// 5. ...but the property content hit the 'else' block and became marked raw.
|
|
1180
|
+
// Because it is marked raw, Vue will NOT create a proxy for it.
|
|
1181
|
+
expect(isReactive(val)).toBe(false);
|
|
1182
|
+
|
|
1183
|
+
// If 'markRaw' was NOT called, accessing instance.data (an object)
|
|
1184
|
+
// from a reactive parent would normally return a reactive proxy.
|
|
1185
|
+
// The fact that it returns a plain object proves the 'else' block executed.
|
|
1186
|
+
expect(val.nested).toBeDefined();
|
|
1187
|
+
expect(isReactive(val.nested)).toBe(false);
|
|
1188
|
+
});
|
|
1189
|
+
it('disables computed conversion for keys in ivueDisableReactivity', () => {
|
|
1190
|
+
let getterCallCountA = 0;
|
|
1191
|
+
let getterCallCountB = 0;
|
|
1192
|
+
|
|
1193
|
+
class StoreNoComputed {
|
|
1194
|
+
static ivueDisableReactivity = new Set(['rawValue']);
|
|
1195
|
+
|
|
1196
|
+
_x = 1;
|
|
1197
|
+
get rawValue() {
|
|
1198
|
+
getterCallCountA++;
|
|
1199
|
+
return this._x;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
get doubled() {
|
|
1203
|
+
getterCallCountB++;
|
|
1204
|
+
return this._x * 2;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
setX(v: number) {
|
|
1208
|
+
this._x = v;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const a = ivue(StoreNoComputed);
|
|
1213
|
+
const b = a.clone();
|
|
1214
|
+
|
|
1215
|
+
// After ivue(), `rawValue` should *not* be converted to computed
|
|
1216
|
+
// so accessing a.rawValue should bump getterCallCountA every time.
|
|
1217
|
+
const beforeA = getterCallCountA;
|
|
1218
|
+
const beforeB = getterCallCountB;
|
|
1219
|
+
|
|
1220
|
+
// access a
|
|
1221
|
+
const v1 = a.rawValue;
|
|
1222
|
+
const v2 = a.rawValue;
|
|
1223
|
+
expect(v1).toBe(1);
|
|
1224
|
+
expect(v2).toBe(1);
|
|
1225
|
+
expect(getterCallCountA).toBe(beforeA + 2); // called each access
|
|
1226
|
+
|
|
1227
|
+
// `doubled` WAS converted to computed, so reading multiple times
|
|
1228
|
+
// should generally not keep calling the original getter over and over
|
|
1229
|
+
// We expect at most +1 here
|
|
1230
|
+
const d1 = a.doubled;
|
|
1231
|
+
const d2 = a.doubled;
|
|
1232
|
+
expect(d1).toBe(2);
|
|
1233
|
+
expect(d2).toBe(2);
|
|
1234
|
+
expect(getterCallCountB).toBeLessThanOrEqual(beforeB + 2);
|
|
1235
|
+
|
|
1236
|
+
// Make sure clone b mirrors behavior
|
|
1237
|
+
const cloneBeforeA = getterCallCountA;
|
|
1238
|
+
const cloneBeforeB = getterCallCountB;
|
|
1239
|
+
const bv1 = b.rawValue;
|
|
1240
|
+
const bv2 = b.rawValue;
|
|
1241
|
+
expect(bv1).toBe(1);
|
|
1242
|
+
expect(bv2).toBe(1);
|
|
1243
|
+
expect(getterCallCountA).toBe(cloneBeforeA + 2);
|
|
1244
|
+
|
|
1245
|
+
const bd1 = b.doubled;
|
|
1246
|
+
const bd2 = b.doubled;
|
|
1247
|
+
expect(bd1).toBe(2);
|
|
1248
|
+
expect(bd2).toBe(2);
|
|
1249
|
+
expect(getterCallCountB).toBeLessThanOrEqual(cloneBeforeB + 2);
|
|
1250
|
+
|
|
1251
|
+
// sanity: mutations on b don't leak into a
|
|
1252
|
+
b.setX(10);
|
|
1253
|
+
expect(a.rawValue).toBe(1);
|
|
1254
|
+
expect(b.rawValue).toBe(10);
|
|
1255
|
+
expect(a.doubled).toBe(2);
|
|
1256
|
+
expect(b.doubled).toBe(20);
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
it('passes ivueCloneArgs to init() on clone', () => {
|
|
1260
|
+
class StoreWithInit {
|
|
1261
|
+
payload: any = null;
|
|
1262
|
+
|
|
1263
|
+
init(isClone?: boolean, arg1?: string, arg2?: number) {
|
|
1264
|
+
this.payload = { arg1, arg2 };
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
const a = ivue(StoreWithInit);
|
|
1269
|
+
const b = a.clone('hello', 123);
|
|
1270
|
+
|
|
1271
|
+
expect(b.payload).toEqual({ arg1: 'hello', arg2: 123 });
|
|
1272
|
+
|
|
1273
|
+
const c = b.clone('hello world', 456); // should re-run init with ivueCloneArgs
|
|
1274
|
+
expect(c.payload).toEqual({ arg1: 'hello world', arg2: 456 });
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
it('deepClone() will not duplicate global stores', () => {
|
|
1278
|
+
class GlobalStore {
|
|
1279
|
+
static ivueGlobalStore = true;
|
|
1280
|
+
value = 42;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
class Wrapper {
|
|
1284
|
+
global = ivue(GlobalStore); // pretend nested store
|
|
1285
|
+
local = { hello: 'world' };
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const w1 = ivue(Wrapper);
|
|
1289
|
+
|
|
1290
|
+
// sanity: Wrapper was made reactive, and inside it we created a GlobalStore instance (reactive)
|
|
1291
|
+
expect(w1.global.value).toBe(42);
|
|
1292
|
+
|
|
1293
|
+
// deep clone Wrapper via deepClone() directly
|
|
1294
|
+
const w2 = deepClone(w1);
|
|
1295
|
+
|
|
1296
|
+
// global store is same reference (because ivueGlobalStore=true)
|
|
1297
|
+
expect(w2.global).toBe(w1.global);
|
|
1298
|
+
|
|
1299
|
+
// local object is NOT same reference
|
|
1300
|
+
expect(w2.local).not.toBe(w1.local);
|
|
1301
|
+
expect(w2.local.hello).toBe('world');
|
|
1302
|
+
|
|
1303
|
+
// editing w2.local doesn't affect w1.local
|
|
1304
|
+
w2.local.hello = 'planet';
|
|
1305
|
+
expect(w1.local.hello).toBe('world');
|
|
1306
|
+
expect(w2.local.hello).toBe('planet');
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
it('clone() of a store containing another ivue store deep-clones that nested store (unless ivueGlobalStore or byReference overrides)', () => {
|
|
1310
|
+
class Child {
|
|
1311
|
+
n = 5;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
class Parent {
|
|
1315
|
+
child = ivue(Child); // nested ivue instance
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
const p1 = ivue(Parent);
|
|
1319
|
+
const p2 = p1.clone();
|
|
1320
|
+
|
|
1321
|
+
// different top-level parents
|
|
1322
|
+
expect(p2).not.toBe(p1);
|
|
1323
|
+
|
|
1324
|
+
// child should be deep cloned as a fresh reactive instance, not same reference
|
|
1325
|
+
expect(p2.child).not.toBe(p1.child);
|
|
1326
|
+
expect(p2.child.n).toBe(5);
|
|
1327
|
+
|
|
1328
|
+
// mutation isolation works
|
|
1329
|
+
p2.child.n = 999;
|
|
1330
|
+
expect(p1.child.n).toBe(5);
|
|
1331
|
+
expect(p2.child.n).toBe(999);
|
|
1332
|
+
});
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
describe('propsWithDefaults()', () => {
|
|
1336
|
+
it('leaves primitive & function defaults as direct values (no factory wrapper)', () => {
|
|
1337
|
+
const defaults = {
|
|
1338
|
+
msg: 'hello',
|
|
1339
|
+
count: 7,
|
|
1340
|
+
truthy: true,
|
|
1341
|
+
fn: () => 'hi',
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
const typedProps = {
|
|
1345
|
+
msg: { type: String },
|
|
1346
|
+
count: { type: Number, required: false },
|
|
1347
|
+
truthy: { type: Boolean },
|
|
1348
|
+
fn: { type: Function },
|
|
1349
|
+
// note: no default for somethingNotProvided
|
|
1350
|
+
somethingNotProvided: { type: String },
|
|
1351
|
+
} as const;
|
|
1352
|
+
|
|
1353
|
+
const result = propsWithDefaults(defaults, { ...typedProps });
|
|
1354
|
+
|
|
1355
|
+
// direct assign
|
|
1356
|
+
expect(result.msg.default).toBe('hello');
|
|
1357
|
+
expect(result.count.default).toBe(7);
|
|
1358
|
+
expect(result.truthy.default).toBe(true);
|
|
1359
|
+
expect(result.fn.default).toBe(defaults.fn);
|
|
1360
|
+
|
|
1361
|
+
// untouched prop that had no default in `defaults`
|
|
1362
|
+
expect('default' in result.somethingNotProvided).toBe(false);
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
it('wraps object and array defaults in a factory that deep clones', () => {
|
|
1366
|
+
const defaults = {
|
|
1367
|
+
obj: { nested: { a: 1 } },
|
|
1368
|
+
list: [1, 2, 3],
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
const typedProps = {
|
|
1372
|
+
obj: { type: Object },
|
|
1373
|
+
list: { type: Array },
|
|
1374
|
+
} as const;
|
|
1375
|
+
|
|
1376
|
+
const result = propsWithDefaults(defaults, { ...typedProps });
|
|
1377
|
+
|
|
1378
|
+
// Should now be a function for Vue-style "default" for objects/arrays
|
|
1379
|
+
expect(typeof result.obj.default).toBe('function');
|
|
1380
|
+
expect(typeof result.list.default).toBe('function');
|
|
1381
|
+
|
|
1382
|
+
const v1 = (result.obj as any).default();
|
|
1383
|
+
const v2 = (result.obj as any).default();
|
|
1384
|
+
|
|
1385
|
+
// Two fresh clones, not the same reference
|
|
1386
|
+
expect(v1).not.toBe(v2);
|
|
1387
|
+
expect(v1.nested).not.toBe(v2.nested);
|
|
1388
|
+
|
|
1389
|
+
// But structurally equal
|
|
1390
|
+
expect(v1).toEqual({ nested: { a: 1 } });
|
|
1391
|
+
expect(v2).toEqual({ nested: { a: 1 } });
|
|
1392
|
+
|
|
1393
|
+
// Mutating one does not affect the other
|
|
1394
|
+
v1.nested.a = 999;
|
|
1395
|
+
expect(v2.nested.a).toBe(1);
|
|
1396
|
+
|
|
1397
|
+
const l1 = (result.list as any).default();
|
|
1398
|
+
const l2 = (result.list as any).default();
|
|
1399
|
+
|
|
1400
|
+
// arrays are also cloned per call
|
|
1401
|
+
expect(l1).toEqual([1, 2, 3]);
|
|
1402
|
+
expect(l2).toEqual([1, 2, 3]);
|
|
1403
|
+
expect(l1).not.toBe(l2);
|
|
1404
|
+
|
|
1405
|
+
l1.push(4);
|
|
1406
|
+
expect(l1).toEqual([1, 2, 3, 4]);
|
|
1407
|
+
expect(l2).toEqual([1, 2, 3]); // unchanged
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
it('wraps class defaults in a factory returning the class itself (NOT deep cloned)', () => {
|
|
1411
|
+
class SomethingCool {
|
|
1412
|
+
x = 10;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
const defaults = {
|
|
1416
|
+
cool: SomethingCool,
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
const typedProps = {
|
|
1420
|
+
cool: { type: SomethingCool },
|
|
1421
|
+
} as const;
|
|
1422
|
+
|
|
1423
|
+
const result = propsWithDefaults(defaults, { ...typedProps });
|
|
1424
|
+
|
|
1425
|
+
// For class defaults, we expect a factory that returns the class,
|
|
1426
|
+
// not an instance and not a POJO deep clone.
|
|
1427
|
+
expect(typeof result.cool.default).toBe('function');
|
|
1428
|
+
|
|
1429
|
+
const out = (result.cool as any).default();
|
|
1430
|
+
|
|
1431
|
+
// should literally be the class constructor we passed in,
|
|
1432
|
+
// because Vue expects default() { return ClassFn } for "type: Class"
|
|
1433
|
+
expect(out).toBe(SomethingCool);
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
describe('deepCloneArgs + ivue() integration', () => {
|
|
1437
|
+
// We'll model a store with ivue semantics.
|
|
1438
|
+
// Important bits:
|
|
1439
|
+
// - .clone() is injected by ivue()
|
|
1440
|
+
// - .toRefs() is injected by ivue()
|
|
1441
|
+
// - instance is reactive (ivue wraps with reactive())
|
|
1442
|
+
// - static ivueCloneArgs / ivueGlobalStore control clone behavior
|
|
1443
|
+
|
|
1444
|
+
it('clones non-global ivue instances using static ivueCloneArgs when used inside defaults', () => {
|
|
1445
|
+
class LocalStore {
|
|
1446
|
+
static ivueCloneInitArgs = [true];
|
|
1447
|
+
|
|
1448
|
+
name = 'ORIGINAL';
|
|
1449
|
+
count = 1;
|
|
1450
|
+
|
|
1451
|
+
set Name(newName: string) {
|
|
1452
|
+
this.name = newName;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// init is optional; not needed for test
|
|
1456
|
+
init(cloned = false) {
|
|
1457
|
+
if (cloned) {
|
|
1458
|
+
this.name = 'CLONED_NAME';
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// make a reactive ivue instance, so it satisfies:
|
|
1464
|
+
// isReactive(source) === true
|
|
1465
|
+
// and has clone()/toRefs()
|
|
1466
|
+
const storeInstance = ivue(LocalStore) as IVue<typeof LocalStore>;
|
|
1467
|
+
// sanity
|
|
1468
|
+
expect(storeInstance.name).toBe('ORIGINAL');
|
|
1469
|
+
expect(typeof storeInstance.clone).toBe('function');
|
|
1470
|
+
expect(typeof storeInstance.toRefs).toBe('function');
|
|
1471
|
+
|
|
1472
|
+
// Now this object becomes a prop default
|
|
1473
|
+
const defaults = {
|
|
1474
|
+
complex: {
|
|
1475
|
+
inner: storeInstance,
|
|
1476
|
+
other: { nested: true },
|
|
1477
|
+
},
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
const typedProps = {
|
|
1481
|
+
complex: { type: Object },
|
|
1482
|
+
} as const;
|
|
1483
|
+
|
|
1484
|
+
// we do NOT provide deepCloneArgs Map here, so deepClone
|
|
1485
|
+
// should fall back to static ivueCloneArgs = ['CLONED_NAME']
|
|
1486
|
+
const result = propsWithDefaults(defaults, { ...typedProps }) as Record<
|
|
1487
|
+
string,
|
|
1488
|
+
{ default: () => any }
|
|
1489
|
+
>;
|
|
1490
|
+
|
|
1491
|
+
expect(typeof result.complex.default).toBe('function');
|
|
1492
|
+
|
|
1493
|
+
const c1 = result.complex.default();
|
|
1494
|
+
const c2 = result.complex.default();
|
|
1495
|
+
|
|
1496
|
+
// Each call should get a deepClone() of defaults.complex
|
|
1497
|
+
expect(c1).not.toBe(c2);
|
|
1498
|
+
|
|
1499
|
+
// inner should NOT be the same reactive instance;
|
|
1500
|
+
// it should be clone()'d with ivueCloneArgs.
|
|
1501
|
+
expect(c1.inner).not.toBe(storeInstance);
|
|
1502
|
+
expect(c2.inner).not.toBe(storeInstance);
|
|
1503
|
+
|
|
1504
|
+
// cloned instances should still be ivue instances (reactive with .clone/.toRefs)
|
|
1505
|
+
expect(typeof c1.inner.clone).toBe('function');
|
|
1506
|
+
expect(typeof c1.inner.toRefs).toBe('function');
|
|
1507
|
+
|
|
1508
|
+
// and their state should reflect ivueCloneArgs override
|
|
1509
|
+
// Our LocalStore constructor sets:
|
|
1510
|
+
// name = 'ORIGINAL', count = 1
|
|
1511
|
+
// But the clone path for ivue instances in deepClone() does:
|
|
1512
|
+
// out = source.clone(...ivueCloneArgs)
|
|
1513
|
+
//
|
|
1514
|
+
// So LocalStore.clone() (auto-generated by ivue) will internally call ivueClone()
|
|
1515
|
+
// which will call LocalStore(...originalCtorArgs) and then copy props.
|
|
1516
|
+
//
|
|
1517
|
+
// Here's the key subtlety:
|
|
1518
|
+
// ivueClone() passes those cloneArgs into vue?.init?.(...cloneArgs)
|
|
1519
|
+
// So we can use init() (or something similar) to mutate after cloneArgs.
|
|
1520
|
+
//
|
|
1521
|
+
// Let's assert that init() got called with 'CLONED_NAME':
|
|
1522
|
+
expect(c1.inner.name).toBe('CLONED_NAME');
|
|
1523
|
+
expect(c2.inner.name).toBe('CLONED_NAME');
|
|
1524
|
+
|
|
1525
|
+
c2.inner.Name = 'MODIFIED_NAME';
|
|
1526
|
+
expect(c2.inner.name).toBe('MODIFIED_NAME');
|
|
1527
|
+
expect(c1.inner.name).toBe('CLONED_NAME'); // unaffected
|
|
1528
|
+
|
|
1529
|
+
// count should be copied from the source
|
|
1530
|
+
expect(c1.inner.count).toBe(1);
|
|
1531
|
+
expect(c2.inner.count).toBe(1);
|
|
1532
|
+
|
|
1533
|
+
// deep nested POJO should also be cloned fresh
|
|
1534
|
+
expect(c1.other).toEqual({ nested: true });
|
|
1535
|
+
expect(c2.other).toEqual({ nested: true });
|
|
1536
|
+
expect(c1.other).not.toBe(c2.other);
|
|
1537
|
+
|
|
1538
|
+
// editing one should not affect the other
|
|
1539
|
+
c1.other.nested = false;
|
|
1540
|
+
expect(c2.other.nested).toBe(true);
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
it('reuses same instance for ivueGlobalStore=true', () => {
|
|
1544
|
+
class GlobalStore {
|
|
1545
|
+
static ivueGlobalStore = true;
|
|
1546
|
+
|
|
1547
|
+
value = 10;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
const globalInstance = ivue(GlobalStore) as IVue<typeof GlobalStore>;
|
|
1551
|
+
|
|
1552
|
+
const defaults = {
|
|
1553
|
+
cfg: {
|
|
1554
|
+
shared: globalInstance,
|
|
1555
|
+
},
|
|
1556
|
+
};
|
|
1557
|
+
|
|
1558
|
+
const typedProps = {
|
|
1559
|
+
cfg: { type: Object },
|
|
1560
|
+
} as const;
|
|
1561
|
+
|
|
1562
|
+
const result = propsWithDefaults(defaults, { ...typedProps }) as Record<
|
|
1563
|
+
string,
|
|
1564
|
+
{ default: () => any }
|
|
1565
|
+
>;
|
|
1566
|
+
|
|
1567
|
+
expect(typeof result.cfg.default).toBe('function');
|
|
1568
|
+
|
|
1569
|
+
const v1 = result.cfg.default();
|
|
1570
|
+
const v2 = result.cfg.default();
|
|
1571
|
+
|
|
1572
|
+
// cfg objects themselves should be unique deep clones
|
|
1573
|
+
expect(v1).not.toBe(v2);
|
|
1574
|
+
|
|
1575
|
+
// BUT the nested global store should be exactly the same reference,
|
|
1576
|
+
// because deepClone() special-cases ivueGlobalStore
|
|
1577
|
+
expect(v1.shared).toBe(globalInstance);
|
|
1578
|
+
expect(v2.shared).toBe(globalInstance);
|
|
1579
|
+
|
|
1580
|
+
// mutating via one should be visible via the other because it's literally shared
|
|
1581
|
+
v1.shared.value = 999;
|
|
1582
|
+
expect(v2.shared.value).toBe(999);
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
it('uses deepCloneArgs Map override when provided', () => {
|
|
1586
|
+
class ServiceStore {
|
|
1587
|
+
value = 1;
|
|
1588
|
+
|
|
1589
|
+
init(isClone?: boolean, newVal = 1) {
|
|
1590
|
+
this.value = newVal;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// make it ivue so it's reactive and has clone()/toRefs()
|
|
1595
|
+
const service = ivue(ServiceStore) as IVue<typeof ServiceStore>;
|
|
1596
|
+
|
|
1597
|
+
const defaults = {
|
|
1598
|
+
wrap: {
|
|
1599
|
+
service,
|
|
1600
|
+
},
|
|
1601
|
+
};
|
|
1602
|
+
|
|
1603
|
+
const typedProps = {
|
|
1604
|
+
wrap: { type: Object },
|
|
1605
|
+
} as const;
|
|
1606
|
+
|
|
1607
|
+
// tell deepClone: when you see ServiceStore, call .clone(1234)
|
|
1608
|
+
const deepCloneArgs = new Map([[ServiceStore, [1234]]]);
|
|
1609
|
+
|
|
1610
|
+
deepClone(service, deepCloneArgs); // warm up cache
|
|
1611
|
+
const result = propsWithDefaults(
|
|
1612
|
+
defaults,
|
|
1613
|
+
{ ...typedProps },
|
|
1614
|
+
deepCloneArgs
|
|
1615
|
+
) as Record<string, { default: () => any }>;
|
|
1616
|
+
|
|
1617
|
+
const w1 = result.wrap.default();
|
|
1618
|
+
const w2 = result.wrap.default();
|
|
1619
|
+
|
|
1620
|
+
// Each wrapper object different
|
|
1621
|
+
expect(w1).not.toBe(w2);
|
|
1622
|
+
|
|
1623
|
+
// cloned service instances different from original AND from each other
|
|
1624
|
+
expect(w1.service).not.toBe(service);
|
|
1625
|
+
expect(w2.service).not.toBe(service);
|
|
1626
|
+
expect(w1.service).not.toBe(w2.service);
|
|
1627
|
+
|
|
1628
|
+
// and init() should have been called with 1234 because of deepCloneArgs map
|
|
1629
|
+
expect(w1.service.value).toBe(1234);
|
|
1630
|
+
expect(w2.service.value).toBe(1234);
|
|
1631
|
+
});
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
it('does not replace prop descriptor objects, only augments them with default', () => {
|
|
1635
|
+
const defaults = { a: 1 };
|
|
1636
|
+
const typedProps = {
|
|
1637
|
+
a: { type: Number },
|
|
1638
|
+
b: { type: String },
|
|
1639
|
+
};
|
|
1640
|
+
|
|
1641
|
+
const aRef = typedProps.a;
|
|
1642
|
+
const bRef = typedProps.b;
|
|
1643
|
+
|
|
1644
|
+
const result = propsWithDefaults(defaults, typedProps);
|
|
1645
|
+
|
|
1646
|
+
expect(result.a).toBe(aRef);
|
|
1647
|
+
expect(result.b).toBe(bRef);
|
|
1648
|
+
|
|
1649
|
+
expect(result.a.default).toBe(1);
|
|
1650
|
+
expect('default' in result.b).toBe(false);
|
|
1651
|
+
});
|
|
1652
|
+
it('does not mutate the original typedProps object shape aside from adding defaults', () => {
|
|
1653
|
+
const defaults = { a: 1 };
|
|
1654
|
+
const typedProps = {
|
|
1655
|
+
a: { type: Number },
|
|
1656
|
+
b: { type: String },
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
// save a reference to ensure we didn't replace the objects
|
|
1660
|
+
const aBefore = typedProps.a;
|
|
1661
|
+
const bBefore = typedProps.b;
|
|
1662
|
+
|
|
1663
|
+
const result = propsWithDefaults(defaults, typedProps);
|
|
1664
|
+
|
|
1665
|
+
// same prop objects, just mutated with `default`
|
|
1666
|
+
expect(result.a).toBe(aBefore);
|
|
1667
|
+
expect(result.b).toBe(bBefore);
|
|
1668
|
+
|
|
1669
|
+
// 'a' now has default
|
|
1670
|
+
expect(result.a.default).toBe(1);
|
|
1671
|
+
|
|
1672
|
+
// 'b' had no default in defaults, so still no "default"
|
|
1673
|
+
expect('default' in result.b).toBe(false);
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1676
|
+
describe('Special Types & Binary Data', () => {
|
|
1677
|
+
describe('Error Handling', () => {
|
|
1678
|
+
it('clones standard Error objects with message and custom props', () => {
|
|
1679
|
+
const err = new Error('Something went wrong');
|
|
1680
|
+
(err as any).code = 500;
|
|
1681
|
+
(err as any).details = { retry: true };
|
|
1682
|
+
|
|
1683
|
+
const clone = deepClone(err);
|
|
1684
|
+
|
|
1685
|
+
expect(clone).not.toBe(err);
|
|
1686
|
+
expect(clone).toBeInstanceOf(Error);
|
|
1687
|
+
expect(clone.message).toBe('Something went wrong');
|
|
1688
|
+
// Custom props should be copied
|
|
1689
|
+
expect(clone.code).toBe(500);
|
|
1690
|
+
expect(clone.details).not.toBe((err as any).details);
|
|
1691
|
+
expect(clone.details.retry).toBe(true);
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
it('clones Custom Error classes', () => {
|
|
1695
|
+
class AppError extends Error {
|
|
1696
|
+
isFatal = true;
|
|
1697
|
+
}
|
|
1698
|
+
const err = new AppError('Fatal crash');
|
|
1699
|
+
const clone = deepClone(err);
|
|
1700
|
+
|
|
1701
|
+
expect(clone).not.toBe(err);
|
|
1702
|
+
expect(clone).toBeInstanceOf(AppError); // Preserves prototype
|
|
1703
|
+
expect(clone.message).toBe('Fatal crash');
|
|
1704
|
+
expect(clone.isFatal).toBe(true);
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
it('handles circular references attached to Errors', () => {
|
|
1708
|
+
const err = new Error('Loop');
|
|
1709
|
+
(err as any).self = err; // Circular ref
|
|
1710
|
+
|
|
1711
|
+
const clone = deepClone(err);
|
|
1712
|
+
|
|
1713
|
+
expect(clone.message).toBe('Loop');
|
|
1714
|
+
expect(clone.self).toBe(clone); // Should point to the clone, not original
|
|
1715
|
+
});
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
describe('Uncloneable Types (Reference Preservation)', () => {
|
|
1719
|
+
it('returns the exact same Promise instance', () => {
|
|
1720
|
+
const p = Promise.resolve(1);
|
|
1721
|
+
const clone = deepClone(p);
|
|
1722
|
+
expect(clone).toBe(p);
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
it('returns the exact same WeakMap instance', () => {
|
|
1726
|
+
const wm = new WeakMap();
|
|
1727
|
+
const clone = deepClone(wm);
|
|
1728
|
+
expect(clone).toBe(wm);
|
|
1729
|
+
});
|
|
1730
|
+
|
|
1731
|
+
it('returns the exact same WeakSet instance', () => {
|
|
1732
|
+
const ws = new WeakSet();
|
|
1733
|
+
const clone = deepClone(ws);
|
|
1734
|
+
expect(clone).toBe(ws);
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
it('preserves references even when nested', () => {
|
|
1738
|
+
const p = Promise.resolve();
|
|
1739
|
+
const wm = new WeakMap();
|
|
1740
|
+
const obj = { p, wm };
|
|
1741
|
+
|
|
1742
|
+
const clone = deepClone(obj);
|
|
1743
|
+
|
|
1744
|
+
expect(clone).not.toBe(obj);
|
|
1745
|
+
expect(clone.p).toBe(p); // Same promise
|
|
1746
|
+
expect(clone.wm).toBe(wm); // Same weakmap
|
|
1747
|
+
});
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
describe('Binary Data', () => {
|
|
1751
|
+
it('clones ArrayBuffer with new memory reference but same content', () => {
|
|
1752
|
+
const buffer = new ArrayBuffer(8);
|
|
1753
|
+
const view = new Int32Array(buffer);
|
|
1754
|
+
view[0] = 123;
|
|
1755
|
+
view[1] = 456;
|
|
1756
|
+
|
|
1757
|
+
const clone = deepClone(buffer);
|
|
1758
|
+
|
|
1759
|
+
expect(clone).toBeInstanceOf(ArrayBuffer);
|
|
1760
|
+
expect(clone).not.toBe(buffer);
|
|
1761
|
+
expect(clone.byteLength).toBe(8);
|
|
1762
|
+
|
|
1763
|
+
// Check content
|
|
1764
|
+
const cloneView = new Int32Array(clone);
|
|
1765
|
+
expect(cloneView[0]).toBe(123);
|
|
1766
|
+
expect(cloneView[1]).toBe(456);
|
|
1767
|
+
|
|
1768
|
+
// Verify independence
|
|
1769
|
+
cloneView[0] = 999;
|
|
1770
|
+
expect(view[0]).toBe(123);
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1773
|
+
it('clones TypedArrays (Int32Array) deeply', () => {
|
|
1774
|
+
const arr = new Int32Array([10, 20, 30]);
|
|
1775
|
+
const clone = deepClone(arr);
|
|
1776
|
+
|
|
1777
|
+
expect(clone).toBeInstanceOf(Int32Array);
|
|
1778
|
+
expect(clone).not.toBe(arr);
|
|
1779
|
+
expect(clone).toEqual(arr);
|
|
1780
|
+
expect(clone.length).toBe(3);
|
|
1781
|
+
expect(clone[0]).toBe(10);
|
|
1782
|
+
|
|
1783
|
+
// Modify clone, original stays same
|
|
1784
|
+
clone[0] = 999;
|
|
1785
|
+
expect(arr[0]).toBe(10);
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
it('clones Uint8Array deeply', () => {
|
|
1789
|
+
const arr = new Uint8Array([255, 0, 128]);
|
|
1790
|
+
const clone = deepClone(arr);
|
|
1791
|
+
|
|
1792
|
+
expect(clone).toBeInstanceOf(Uint8Array);
|
|
1793
|
+
expect(clone).not.toBe(arr);
|
|
1794
|
+
expect(clone[0]).toBe(255);
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1797
|
+
it('clones DataView correctly', () => {
|
|
1798
|
+
const buffer = new ArrayBuffer(4);
|
|
1799
|
+
const view = new DataView(buffer);
|
|
1800
|
+
view.setInt32(0, 123456); // Set value
|
|
1801
|
+
|
|
1802
|
+
const clone = deepClone(view);
|
|
1803
|
+
|
|
1804
|
+
expect(clone).toBeInstanceOf(DataView);
|
|
1805
|
+
expect(clone).not.toBe(view);
|
|
1806
|
+
expect(clone.buffer).not.toBe(view.buffer); // Should clone underlying buffer too
|
|
1807
|
+
expect(clone.getInt32(0)).toBe(123456);
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
it('handles circular refs involving ArrayBuffers (via properties)', () => {
|
|
1811
|
+
// It's rare to attach props to ArrayBuffer, but possible in JS
|
|
1812
|
+
const buf = new ArrayBuffer(2);
|
|
1813
|
+
(buf as any).loop = buf;
|
|
1814
|
+
|
|
1815
|
+
const clone = deepClone(buf);
|
|
1816
|
+
expect(clone).toBeInstanceOf(ArrayBuffer);
|
|
1817
|
+
// Note: The specific implementation for ArrayBuffer usually uses .slice(0)
|
|
1818
|
+
// which creates a NEW buffer. Standard .slice does NOT copy custom properties.
|
|
1819
|
+
// If your deepClone logic for ArrayBuffer looks like:
|
|
1820
|
+
// const out = source.slice(0);
|
|
1821
|
+
// seen.set(source, out);
|
|
1822
|
+
// return out;
|
|
1823
|
+
// Then custom props will be LOST. If you want to keep them, you'd need copyOwnProps.
|
|
1824
|
+
// Based on your snippet, they will be lost. This test asserts that behavior.
|
|
1825
|
+
expect((clone as any).loop).toBeUndefined();
|
|
1826
|
+
});
|
|
1827
|
+
});
|
|
1828
|
+
});
|
|
1829
|
+
});
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
describe('ivue coverage edge cases', () => {
|
|
1833
|
+
it('propsWithDefaults skips a required prop even when a default exists', () => {
|
|
1834
|
+
const typed = {
|
|
1835
|
+
a: { type: String, required: true },
|
|
1836
|
+
b: { type: Number },
|
|
1837
|
+
};
|
|
1838
|
+
const out = propsWithDefaults({ a: 'ignored', b: 2 }, { ...typed }) as Record<
|
|
1839
|
+
string,
|
|
1840
|
+
any
|
|
1841
|
+
>;
|
|
1842
|
+
// required → the `if (typed.required) continue` branch is taken
|
|
1843
|
+
expect('default' in out.a).toBe(false);
|
|
1844
|
+
expect(out.b.default).toBe(2);
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
it('ivueDisableReactivity preserves BOTH getter and setter for an accessor', () => {
|
|
1848
|
+
class WithRW {
|
|
1849
|
+
static ivueDisableReactivity = new Set<keyof WithRW>(['rw']);
|
|
1850
|
+
_v = 1;
|
|
1851
|
+
get rw() {
|
|
1852
|
+
return this._v;
|
|
1853
|
+
}
|
|
1854
|
+
set rw(value: number) {
|
|
1855
|
+
this._v = value;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// ivue() path: descriptor has get AND set → both `?.bind(vue)` branches run
|
|
1860
|
+
const a = ivue(WithRW);
|
|
1861
|
+
expect(a.rw).toBe(1);
|
|
1862
|
+
a.rw = 5;
|
|
1863
|
+
expect(a.rw).toBe(5);
|
|
1864
|
+
|
|
1865
|
+
// clone() path exercises the same branch in ivueClone()
|
|
1866
|
+
const b = a.clone();
|
|
1867
|
+
expect(b.rw).toBe(5);
|
|
1868
|
+
b.rw = 9;
|
|
1869
|
+
expect(b.rw).toBe(9);
|
|
1870
|
+
expect(a.rw).toBe(5); // isolation
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1873
|
+
it('deepClone tolerates a deepCloneArgs entry mapped to undefined (uses [])', () => {
|
|
1874
|
+
class Svc {
|
|
1875
|
+
value = 1;
|
|
1876
|
+
init(_isClone?: boolean, v = 7) {
|
|
1877
|
+
this.value = v;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
const svc = ivue(Svc);
|
|
1881
|
+
// has(Svc) === true but get(Svc) === undefined → the `?? []` fallback runs
|
|
1882
|
+
const deepCloneArgs = new Map<any, any[]>([[Svc, undefined as any]]);
|
|
1883
|
+
const cloned = deepClone(svc, deepCloneArgs);
|
|
1884
|
+
// clone() called with no args → init(true) default → value = 7
|
|
1885
|
+
expect(cloned.value).toBe(7);
|
|
1886
|
+
});
|
|
577
1887
|
});
|