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.
@@ -3,15 +3,22 @@ import { PropType, isReactive, ref } from 'vue';
3
3
 
4
4
  import {
5
5
  ExtractPropDefaultTypes,
6
- getAllClassDescriptors,
7
- getAllClassProperties,
8
- isClass,
9
- ivue,
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) as unknown as string | number;
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 | string) {
41
+ set width(value: number) {
34
42
  this._width = value;
35
43
  }
36
- _height = iref(5) as unknown as string | number;
44
+ _height = iref(5);
37
45
  get height(): string {
38
46
  return this._height + this.unit;
39
47
  }
40
- set height(value: number | string) {
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 (this._height as number) + (this._width as number);
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 ivue = {
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
- it('getAllClassDescriptors correctly resolves all parent descriptors', () => {
131
- const descriptors = getAllClassDescriptors(Item);
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('undefined');
143
- expect(typeof descriptors.get('testProperty')?.set).toBe('function');
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('getAllClassDescriptors correctly resolves all grand parent descriptors', () => {
147
- const descriptors = getAllClassDescriptors(ProductItem);
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('undefined');
159
- expect(typeof descriptors.get('testProperty')?.set).toBe('function');
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('function');
163
- expect(typeof descriptors.get('productType')?.set).toBe('function');
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('function');
167
- expect(typeof descriptors.get('productFeel')?.set).toBe('undefined');
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('undefined');
171
- expect(typeof descriptors.get('productId')?.set).toBe('function');
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('getAllClassProperties correctly resolves all parent properties', () => {
175
- const item = ivue(Item);
176
- const props = getAllClassProperties(item);
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
- expect(props.has('_width')).toBe(true);
179
- expect(props.has('_height')).toBe(true);
180
- expect(props.has('_testProperty')).toBe(true);
181
- expect(props.has('width')).toBe(true);
182
- expect(props.has('height')).toBe(true);
183
- expect(props.has('testProperty')).toBe(true);
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('getAllClassProperties correctly resolves all grand parent properties', () => {
253
+ it('getClassPropertiesAccessorsMap correctly resolves all grand parent properties', () => {
187
254
  const item = ivue(ProductItem);
188
- const props = getAllClassProperties(item);
189
-
190
- expect(props.has('_width')).toBe(true);
191
- expect(props.has('_height')).toBe(true);
192
- expect(props.has('_testProperty')).toBe(true);
193
- expect(props.has('_productType')).toBe(true);
194
- expect(props.has('_productFit')).toBe(true);
195
- expect(props.has('_productId')).toBe(true);
196
- expect(props.has('width')).toBe(true);
197
- expect(props.has('height')).toBe(true);
198
- expect(props.has('testProperty')).toBe(true);
199
- expect(props.has('productType')).toBe(true);
200
- expect(props.has('productFit')).toBe(true);
201
- expect(props.has('productId')).toBe(true);
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 { _height, _width, height, width, calculateSize } =
287
- ivue(RetailStoreItem).toRefs();
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(calculateSize()).toBe(10);
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, calculateSize } = item.toRefs();
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 } = item.toRefs([
314
- '_width',
315
- '_height',
316
- 'height',
317
- 'width',
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 { calculateSize } = ivue(RetailStoreItem).toRefs(['calculateSize']);
399
+ const { _productType, testProperty } = ivue(RetailStoreItem).toRefs([
400
+ '_productType',
401
+ 'testProperty',
402
+ ]);
348
403
 
349
- expect(calculateSize()).toBe(10);
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
- it('iuse(Class, ...ClassProps) returns standard ref with .value but unwraps type', () => {
462
- const iuseComposable = iuse(Item);
463
- // @ts-expect-error Unwraps type, only to be used within ivue Class or Vue 3 `reactive()` context
464
- expect(iuseComposable._width.value).toBe(5)
465
- // @ts-expect-error Unwraps type, only to be used within ivue Class or Vue 3 `reactive()` context
466
- expect(iuseComposable.unit.value).toBe('px');
467
-
468
- const iuseComposable2 = iuse(Entity, 100);
469
- // @ts-expect-error Unwraps type, only to be used within ivue Class or Vue 3 `reactive()` context
470
- expect(iuseComposable2.height.value).toBe(100)
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
- it('iuse(Function, ...FunctionProps) returns standard ref with .value but unwraps type', () => {
476
-
477
-
478
- const iuseComposable = iuse(useComposable);
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
- // @ts-expect-error Unwraps type, only to be used within ivue Class or Vue 3 `reactive()` context
481
- expect(iuseComposable.x.value).toBe(0)
482
- // @ts-expect-error Unwraps type, only to be used within ivue Class or Vue 3 `reactive()` context
483
- expect(iuseComposable.y.value).toBe(0);
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
- describe('util functions', () => {
489
- it('isClass correctly determines if a value is a class', () => {
490
- expect(isClass('string')).toBe(false);
491
- expect(isClass(10)).toBe(false);
492
- expect(isClass(null)).toBe(false);
493
- expect(isClass(() => {})).toBe(false);
494
- expect(isClass(function () {})).toBe(false);
495
- expect(
496
- isClass(
497
- class {
498
- prop = 1;
499
- }
500
- )
501
- ).toBe(true);
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
  });