lil-mocky 1.4.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.
@@ -0,0 +1,867 @@
1
+ const { expect } = require('chai');
2
+ const mocky = require('../src/lil-mocky.js');
3
+
4
+
5
+ describe('lil-mocky', () => {
6
+ describe('function', () => {
7
+ it('will build directly without mocky.create', async () => {
8
+ const mock = mocky.function().args('x', 'y').build();
9
+ mock.ret('result');
10
+
11
+ expect(mock(1, 2)).to.equal('result');
12
+ expect(mock.calls(0)).to.deep.equal({ x: 1, y: 2 });
13
+ });
14
+ it('will be a function with args', async () => {
15
+ const mock = mocky.create(mocky.function().args('firstArg', { secondArg: 'testDefault' }));
16
+ mock.ret('testRet');
17
+
18
+ expect(mock('testArg')).to.equal('testRet');
19
+ expect(mock.calls(0)).to.deep.equal({
20
+ firstArg: 'testArg',
21
+ secondArg: 'testDefault'
22
+ });
23
+ expect(mock.calls().length).to.equal(1);
24
+ });
25
+ it('will be a function with custom return', async () => {
26
+ const mock = mocky.create(mocky.function((context) => {
27
+ return context.ret;
28
+ }).args('firstArg'));
29
+ mock.ret('testRet');
30
+
31
+ expect(mock('testArg')).to.equal('testRet');
32
+ expect(mock.calls(0)).to.deep.equal({ firstArg: 'testArg' });
33
+ expect(mock.calls().length).to.equal(1);
34
+ });
35
+ it('will use argSelect with single index to return raw argument', async () => {
36
+ const mock = mocky.create(mocky.function().argSelect(1));
37
+ mock.ret('testRet');
38
+
39
+ const result = mock('first', { test: 'data' }, 'third');
40
+
41
+ expect(result).to.equal('testRet');
42
+ // When argSelect has single index, calls returns the raw argument
43
+ expect(mock.calls(0)).to.deep.equal({ test: 'data' });
44
+ });
45
+ it('will use argSelect with multiple indexes to filter named args', async () => {
46
+ const mock = mocky.create(mocky.function().args('first', 'second', 'third').argSelect(0, 2));
47
+
48
+ mock('value1', 'value2', 'value3');
49
+
50
+ // Only first and third are captured
51
+ expect(mock.calls(0)).to.deep.equal({
52
+ first: 'value1',
53
+ third: 'value3'
54
+ });
55
+ });
56
+ it('will deep clone arguments to prevent mutation', async () => {
57
+ const mock = mocky.create(mocky.function().args('data'));
58
+
59
+ const obj = { nested: { value: 'original' } };
60
+ mock(obj);
61
+
62
+ // Mutate the original
63
+ obj.nested.value = 'mutated';
64
+
65
+ // Stored call should be unchanged
66
+ expect(mock.calls(0).data.nested.value).to.equal('original');
67
+ });
68
+ it('will reset function state including calls, returns and data', async () => {
69
+ const mock = mocky.create(mocky.function((context) => {
70
+ context.state.data.counter = (context.state.data.counter || 0) + 1;
71
+ return context.ret;
72
+ }).args('arg'));
73
+ mock.ret('test-ret');
74
+
75
+ mock('call1');
76
+ mock('call2');
77
+
78
+ expect(mock.calls().length).to.equal(2);
79
+ expect(mock.data('counter')).to.equal(2);
80
+
81
+ mock.reset();
82
+
83
+ expect(mock.calls().length).to.equal(0);
84
+ expect(mock('call3')).to.equal(undefined); // Return values cleared
85
+ expect(mock.data('counter')).to.equal(1); // Restarted from scratch
86
+ expect(mock.data()).to.deep.equal({ counter: 1 });
87
+ });
88
+ it('will provide context with call, args, ret, state and data', async () => {
89
+ const mock = mocky.create(mocky.function((context) => {
90
+ context.state.data.allItems = context.state.data.allItems || [];
91
+ context.state.data.allItems.push(...context.args.items);
92
+ return {
93
+ call: context.call,
94
+ args: context.args,
95
+ ret: context.ret,
96
+ hasState: typeof context.state === 'object',
97
+ hasData: typeof context.state.data === 'object'
98
+ };
99
+ }).args('items'));
100
+ mock.ret('custom-ret');
101
+
102
+ const result = mock(['a', 'b']);
103
+
104
+ expect(result.call).to.equal(1);
105
+ expect(result.args).to.deep.equal({ items: ['a', 'b'] });
106
+ expect(result.ret).to.equal('custom-ret');
107
+ expect(result.hasState).to.equal(true);
108
+ expect(result.hasData).to.equal(true);
109
+
110
+ // Data accumulates across calls
111
+ mock(['c']);
112
+ expect(mock.data('allItems')).to.deep.equal(['a', 'b', 'c']);
113
+ });
114
+ it('will be async function', async () => {
115
+ const mock = mocky.function().args('x').async().build();
116
+ mock.ret('async-result');
117
+
118
+ const result = mock('test');
119
+
120
+ expect(result).to.be.a('promise');
121
+ expect(await result).to.equal('async-result');
122
+ expect(mock.calls(0)).to.deep.equal({ x: 'test' });
123
+ });
124
+ it('will return different values per call index', () => {
125
+ const mock = mocky.function().build();
126
+
127
+ mock.ret('default');
128
+ mock.ret('first', 1);
129
+ mock.ret('second', 2);
130
+
131
+ expect(mock()).to.equal('first');
132
+ expect(mock()).to.equal('second');
133
+ expect(mock()).to.equal('default');
134
+ expect(mock()).to.equal('default');
135
+ });
136
+ it('will use onRet handler for return value', () => {
137
+ const mock = mocky.function().args('x', 'y').build();
138
+
139
+ mock.onRet((args) => args.x + args.y);
140
+
141
+ expect(mock(2, 3)).to.equal(5);
142
+ expect(mock(10, 20)).to.equal(30);
143
+ });
144
+ it('will use onRet handler per call index', () => {
145
+ const mock = mocky.function().args('x').build();
146
+
147
+ mock.onRet((args) => args.x * 10);
148
+ mock.onRet((args) => args.x * 100, 1);
149
+
150
+ expect(mock(5)).to.equal(500); // call 1 handler
151
+ expect(mock(5)).to.equal(50); // default handler
152
+ });
153
+ it('will throw error when ret is an Error', () => {
154
+ const mock = mocky.function().build();
155
+
156
+ mock.ret(new Error('test error'));
157
+
158
+ expect(() => mock()).to.throw('test error');
159
+ expect(mock.calls().length).to.equal(1);
160
+ });
161
+ it('will provide rawArgs in context', () => {
162
+ const mock = mocky.function((context) => {
163
+ return context.rawArgs;
164
+ }).args('first').build();
165
+
166
+ const result = mock('a', 'b', 'c');
167
+
168
+ expect(result).to.deep.equal(['a', 'b', 'c']);
169
+ });
170
+ it('will provide original function in context', () => {
171
+ const original = (x) => x * 2;
172
+ const mock = mocky.function((context) => {
173
+ return context.original(context.args.x) + 100;
174
+ }).args('x').original(original).build();
175
+
176
+ expect(mock(5)).to.equal(110); // (5 * 2) + 100
177
+ });
178
+ });
179
+
180
+ describe('object', () => {
181
+ it('will be object with function', async () => {
182
+ const mock = mocky.create(mocky.object({
183
+ run: mocky.function().args('firstArg')
184
+ }));
185
+
186
+ mock.run.ret('testRet');
187
+ expect(mock.run('testArg')).to.equal('testRet');
188
+ expect(mock.run.calls(0)).to.deep.equal({ firstArg: 'testArg' });
189
+ expect(mock.run.calls().length).to.equal(1);
190
+ });
191
+ it('will be object with nested object and function', async () => {
192
+ const mock = mocky.create(mocky.object({
193
+ sub: mocky.object({
194
+ run: mocky.function().args('firstArg')
195
+ })
196
+ }));
197
+ mock.sub.run.ret('testRet');
198
+
199
+ expect(mock.sub.run('testArg')).to.equal('testRet');
200
+ expect(mock.sub.run.calls(0)).to.deep.equal({ firstArg: 'testArg' });
201
+ expect(mock.sub.run.calls().length).to.equal(1);
202
+ });
203
+ it('will reset all nested mocks when object is reset', async () => {
204
+ const mock = mocky.create(mocky.object({
205
+ method1: mocky.function().args('arg'),
206
+ nested: mocky.object({
207
+ method2: mocky.function().args('arg')
208
+ })
209
+ }));
210
+
211
+ mock.method1.ret('ret1');
212
+ mock.nested.method2.ret('ret2');
213
+
214
+ mock.method1('call1');
215
+ mock.nested.method2('call2');
216
+
217
+ expect(mock.method1.calls().length).to.equal(1);
218
+ expect(mock.nested.method2.calls().length).to.equal(1);
219
+
220
+ // Reset should propagate to all nested mocks
221
+ mock.reset();
222
+
223
+ expect(mock.method1.calls().length).to.equal(0);
224
+ expect(mock.nested.method2.calls().length).to.equal(0);
225
+ expect(mock.method1('test')).to.equal(undefined);
226
+ expect(mock.nested.method2('test')).to.equal(undefined);
227
+ });
228
+ it('will be object with property getter and setter', async () => {
229
+ const mock = mocky.create(mocky.object({
230
+ accessor: mocky.property()
231
+ }));
232
+
233
+ // Initial value should be undefined
234
+ expect(mock.accessor).to.equal(undefined);
235
+
236
+ // Set a value
237
+ mock.accessor = 'testValue';
238
+ expect(mock.accessor).to.equal('testValue');
239
+
240
+ // Change the value
241
+ mock.accessor = 'newValue';
242
+ expect(mock.accessor).to.equal('newValue');
243
+ });
244
+ it('will reset all property types to initial values', async () => {
245
+ const mock = mocky.create(mocky.object({
246
+ counter: 42,
247
+ name: 'Alice',
248
+ method: mocky.function(),
249
+ accessor: mocky.property()
250
+ }));
251
+
252
+ // Modify all properties
253
+ mock.counter = 100;
254
+ mock.name = 'Bob';
255
+ mock.method.ret('test');
256
+ mock.method();
257
+ mock.accessor = 'value';
258
+
259
+ // Add dynamic property
260
+ mock.added = 'new';
261
+
262
+ mock.reset();
263
+
264
+ // Plain values reset
265
+ expect(mock.counter).to.equal(42);
266
+ expect(mock.name).to.equal('Alice');
267
+
268
+ // Methods reset
269
+ expect(mock.method.calls().length).to.equal(0);
270
+
271
+ // mocky.property() resets to undefined
272
+ expect(mock.accessor).to.equal(undefined);
273
+
274
+ // Dynamic properties deleted
275
+ expect(mock.added).to.equal(undefined);
276
+ expect('added' in mock).to.equal(false);
277
+ });
278
+ it('will support Symbol properties and reset them correctly', async () => {
279
+ const customSymbol = Symbol('custom');
280
+ const addedSymbol = Symbol('added');
281
+
282
+ const mock = mocky.create(mocky.object({
283
+ [Symbol.iterator]: mocky.function(),
284
+ [customSymbol]: 'initialValue'
285
+ }));
286
+
287
+ // Verify Symbol properties work
288
+ expect(typeof mock[Symbol.iterator]).to.equal('function');
289
+ expect(mock[customSymbol]).to.equal('initialValue');
290
+
291
+ // Configure and use Symbol function
292
+ mock[Symbol.iterator].ret('iteratorResult');
293
+ expect(mock[Symbol.iterator]()).to.equal('iteratorResult');
294
+ expect(mock[Symbol.iterator].calls().length).to.equal(1);
295
+
296
+ // Modify Symbol value and add dynamic Symbol
297
+ mock[customSymbol] = 'modifiedValue';
298
+ mock[addedSymbol] = 'added';
299
+
300
+ // Reset
301
+ mock.reset();
302
+
303
+ // Symbol function mock was reset
304
+ expect(mock[Symbol.iterator].calls().length).to.equal(0);
305
+ expect(mock[Symbol.iterator]()).to.equal(undefined);
306
+
307
+ // Symbol plain property was restored
308
+ expect(mock[customSymbol]).to.equal('initialValue');
309
+
310
+ // Dynamic Symbol property was deleted
311
+ expect(mock[addedSymbol]).to.equal(undefined);
312
+ expect(Object.getOwnPropertySymbols(mock).includes(addedSymbol)).to.equal(false);
313
+ });
314
+ it('will handle null and undefined property values', async () => {
315
+ const mock = mocky.create(mocky.object({
316
+ nullProp: null,
317
+ undefinedProp: undefined,
318
+ zeroProp: 0,
319
+ falseProp: false,
320
+ emptyStringProp: ''
321
+ }));
322
+
323
+ // Verify initial values
324
+ expect(mock.nullProp).to.equal(null);
325
+ expect(mock.undefinedProp).to.equal(undefined);
326
+ expect(mock.zeroProp).to.equal(0);
327
+ expect(mock.falseProp).to.equal(false);
328
+ expect(mock.emptyStringProp).to.equal('');
329
+
330
+ // Modify values
331
+ mock.nullProp = 'changed';
332
+ mock.undefinedProp = 'changed';
333
+ mock.zeroProp = 100;
334
+ mock.falseProp = true;
335
+ mock.emptyStringProp = 'changed';
336
+
337
+ // Reset
338
+ mock.reset();
339
+
340
+ // Verify restored to initial values
341
+ expect(mock.nullProp).to.equal(null);
342
+ expect(mock.undefinedProp).to.equal(undefined);
343
+ expect(mock.zeroProp).to.equal(0);
344
+ expect(mock.falseProp).to.equal(false);
345
+ expect(mock.emptyStringProp).to.equal('');
346
+ });
347
+ });
348
+
349
+ describe('class', () => {
350
+ it('will be class with constructor and method', async () => {
351
+ const Mock = mocky.create(mocky.class({
352
+ constructor: mocky.function().args('initArg'),
353
+ run: mocky.function().args('firstArg')
354
+ }));
355
+ Mock.inst().run.ret('testRet');
356
+
357
+ const mockInstance = new Mock('testOption');
358
+ expect(Mock.inst().constructor.calls(0)).to.deep.equal({ initArg: 'testOption' });
359
+ expect(Mock.inst().constructor.calls().length).to.equal(1);
360
+
361
+ expect(mockInstance.run('testArg')).to.equal('testRet');
362
+ expect(Mock.inst().run.calls(0)).to.deep.equal({ firstArg: 'testArg' });
363
+ expect(Mock.inst().run.calls().length).to.equal(1);
364
+ });
365
+
366
+ it('will be class with constructor and builder methods', async () => {
367
+ const Mock = mocky.create(mocky.class({
368
+ index: mocky.function((context) => {
369
+ return context.self;
370
+ }).args('index'),
371
+ namespace: mocky.function((context) => {
372
+ return context.self;
373
+ }).args('namespace'),
374
+ query: mocky.function((context) => {
375
+ return context.self;
376
+ }).args('query')
377
+ }));
378
+
379
+ const mockInstance = new Mock();
380
+ mockInstance.index('test-index').namespace('test-namespace').query({ test: 'query' });
381
+ expect(Mock.inst().index.calls(0)).to.deep.equal({ index: 'test-index' });
382
+ expect(Mock.inst().index.calls().length).to.equal(1);
383
+ expect(Mock.inst().namespace.calls(0)).to.deep.equal({ namespace: 'test-namespace' });
384
+ expect(Mock.inst().namespace.calls().length).to.equal(1);
385
+ expect(Mock.inst().query.calls(0)).to.deep.equal({ query: { test: 'query' } });
386
+ expect(Mock.inst().query.calls().length).to.equal(1);
387
+ });
388
+
389
+ it('will demonstrate that state.data is per-method, not shared', async () => {
390
+ const Counter = mocky.create(mocky.class({
391
+ constructor: mocky.function((context) => {
392
+ // state.data here is separate from other methods
393
+ context.state.data.constructorData = 'set in constructor';
394
+ }).args('startValue'),
395
+ getConstructorData: mocky.function((context) => {
396
+ // This state.data is different from constructor's state.data
397
+ return context.state.data.constructorData; // Will be undefined
398
+ })
399
+ }));
400
+
401
+ const counter = new Counter(10);
402
+ // This will be undefined because each method has its own state.data
403
+ expect(counter.getConstructorData()).to.equal(undefined);
404
+ });
405
+
406
+ it('will store data on instance via context.self in constructor', async () => {
407
+ const Counter = mocky.create(mocky.class({
408
+ constructor: mocky.function((context) => {
409
+ // Store data directly on the instance
410
+ context.self._count = context.args.startValue || 0;
411
+ context.self._initialized = true;
412
+ }).args('startValue'),
413
+ increment: mocky.function((context) => {
414
+ context.self._count++;
415
+ return context.self._count;
416
+ }),
417
+ getCount: mocky.function((context) => {
418
+ return context.self._count;
419
+ }),
420
+ isInitialized: mocky.function((context) => {
421
+ return context.self._initialized;
422
+ })
423
+ }));
424
+
425
+ const counter1 = new Counter(10);
426
+ expect(counter1._initialized).to.equal(true);
427
+ expect(counter1.isInitialized()).to.equal(true);
428
+ expect(counter1.getCount()).to.equal(10);
429
+ expect(counter1.increment()).to.equal(11);
430
+ expect(counter1.increment()).to.equal(12);
431
+ expect(counter1.getCount()).to.equal(12);
432
+
433
+ // Second instance should have separate data
434
+ const counter2 = new Counter(5);
435
+ expect(counter2.getCount()).to.equal(5);
436
+ expect(counter2.increment()).to.equal(6);
437
+
438
+ // First instance should be unaffected
439
+ expect(counter1.getCount()).to.equal(12);
440
+ });
441
+
442
+ it('will track number of instances created with numInsts', async () => {
443
+ const Mock = mocky.create(mocky.class({
444
+ constructor: mocky.function().args('value')
445
+ }));
446
+
447
+ expect(Mock.numInsts()).to.equal(0);
448
+
449
+ const inst1 = new Mock('first');
450
+ expect(Mock.numInsts()).to.equal(1);
451
+
452
+ const inst2 = new Mock('second');
453
+ expect(Mock.numInsts()).to.equal(2);
454
+
455
+ const inst3 = new Mock('third');
456
+ expect(Mock.numInsts()).to.equal(3);
457
+ });
458
+
459
+ it('will reset all instances and counters', async () => {
460
+ const Mock = mocky.create(mocky.class({
461
+ constructor: mocky.function().args('value'),
462
+ run: mocky.function().args('arg')
463
+ }));
464
+
465
+ Mock.inst(0).run.ret('first-ret');
466
+ Mock.inst(1).run.ret('second-ret');
467
+
468
+ const inst1 = new Mock('first');
469
+ const inst2 = new Mock('second');
470
+
471
+ expect(inst1.run('test')).to.equal('first-ret');
472
+ expect(inst2.run('test')).to.equal('second-ret');
473
+ expect(Mock.numInsts()).to.equal(2);
474
+ expect(Mock.inst(0).run.calls().length).to.equal(1);
475
+
476
+ // Reset should clear everything
477
+ Mock.reset();
478
+
479
+ expect(Mock.numInsts()).to.equal(0);
480
+
481
+ // After reset, new instances start from index 0 again
482
+ const inst3 = new Mock('third');
483
+ expect(Mock.numInsts()).to.equal(1);
484
+ // Returns should be cleared
485
+ expect(inst3.run('test')).to.equal(undefined);
486
+ // Calls should be cleared
487
+ expect(Mock.inst(0).run.calls().length).to.equal(1); // Just this one call
488
+ });
489
+
490
+ it('will reset plain value properties to initial values', async () => {
491
+ const Mock = mocky.create(mocky.class({
492
+ name: null,
493
+ count: 0
494
+ }));
495
+
496
+ // Create and modify instances
497
+ const inst1 = new Mock();
498
+ inst1.name = 'first';
499
+ inst1.count = 10;
500
+
501
+ const inst2 = new Mock();
502
+ inst2.name = 'second';
503
+ inst2.count = 20;
504
+
505
+ expect(Mock.numInsts()).to.equal(2);
506
+ expect(Mock.inst(0).name).to.equal('first');
507
+ expect(Mock.inst(1).count).to.equal(20);
508
+
509
+ // Reset
510
+ Mock.reset();
511
+
512
+ expect(Mock.numInsts()).to.equal(0);
513
+
514
+ // After reset, descriptions are fresh with initial values
515
+ expect(Mock.inst(0).name).to.equal(null);
516
+ expect(Mock.inst(0).count).to.equal(0);
517
+
518
+ // New instance gets initial values
519
+ const inst3 = new Mock();
520
+ expect(inst3.name).to.equal(null);
521
+ expect(inst3.count).to.equal(0);
522
+ });
523
+
524
+ it('will pre-configure instances before instantiation', async () => {
525
+ const Mock = mocky.create(mocky.class({
526
+ run: mocky.function().args('arg')
527
+ }));
528
+
529
+ // Configure instances before they're created
530
+ Mock.inst(0).run.ret('first-value');
531
+ Mock.inst(1).run.ret('second-value');
532
+ Mock.inst(2).run.ret('third-value');
533
+
534
+ const inst1 = new Mock();
535
+ const inst2 = new Mock();
536
+ const inst3 = new Mock();
537
+
538
+ expect(inst1.run('test')).to.equal('first-value');
539
+ expect(inst2.run('test')).to.equal('second-value');
540
+ expect(inst3.run('test')).to.equal('third-value');
541
+ });
542
+
543
+ it('will support async methods', async () => {
544
+ const Mock = mocky.create(mocky.class({
545
+ fetchData: mocky.function().args('id').async()
546
+ }));
547
+
548
+ Mock.inst().fetchData.ret('async-result');
549
+
550
+ const inst = new Mock();
551
+ const result = await inst.fetchData('test-id');
552
+
553
+ expect(result).to.equal('async-result');
554
+ expect(Mock.inst().fetchData.calls(0)).to.deep.equal({ id: 'test-id' });
555
+ });
556
+
557
+ it('will throw errors from class methods', async () => {
558
+ const Mock = mocky.create(mocky.class({
559
+ throwError: mocky.function().args('message')
560
+ }));
561
+
562
+ const testError = new Error('Test error');
563
+ Mock.inst().throwError.ret(testError);
564
+
565
+ const inst = new Mock();
566
+
567
+ expect(() => inst.throwError('fail')).to.throw('Test error');
568
+ expect(Mock.inst().throwError.calls(0)).to.deep.equal({ message: 'fail' });
569
+ });
570
+
571
+ it('will handle onRet handlers in class methods', async () => {
572
+ const Mock = mocky.create(mocky.class({
573
+ calculate: mocky.function().args('x', 'y')
574
+ }));
575
+
576
+ Mock.inst().calculate.onRet((args) => {
577
+ return args.x + args.y;
578
+ });
579
+
580
+ const inst = new Mock();
581
+
582
+ expect(inst.calculate(5, 3)).to.equal(8);
583
+ expect(inst.calculate(10, 20)).to.equal(30);
584
+ });
585
+
586
+ it('will handle different return values per call', async () => {
587
+ const Mock = mocky.create(mocky.class({
588
+ getValue: mocky.function()
589
+ }));
590
+
591
+ Mock.inst().getValue.ret('default');
592
+ Mock.inst().getValue.ret('first-call', 1);
593
+ Mock.inst().getValue.ret('second-call', 2);
594
+
595
+ const inst = new Mock();
596
+
597
+ expect(inst.getValue()).to.equal('first-call');
598
+ expect(inst.getValue()).to.equal('second-call');
599
+ expect(inst.getValue()).to.equal('default'); // Falls back to call 0
600
+ });
601
+
602
+ it('will support class inheritance with super calls', () => {
603
+ const Mock = mocky.create(mocky.class({
604
+ constructor: mocky.function((context) => {
605
+ context.self.mockInitialized = true;
606
+ context.self.mockValue = context.args.value;
607
+ }).args('value'),
608
+ getValue: mocky.function().args('x')
609
+ }));
610
+
611
+ Mock.inst().getValue.ret('mock-value');
612
+
613
+ class Child extends Mock {
614
+ constructor(value) {
615
+ super(value);
616
+ this.childInitialized = true;
617
+ this.childValue = value * 2;
618
+ }
619
+ getValue(x) {
620
+ const parentResult = super.getValue(x);
621
+ return `child-${parentResult}`;
622
+ }
623
+ }
624
+
625
+ const child = new Child(10);
626
+
627
+ // Mock constructor should have run
628
+ expect(child.mockInitialized).to.equal(true);
629
+ expect(child.mockValue).to.equal(10);
630
+ expect(Mock.inst().constructor.calls(0)).to.deep.equal({ value: 10 });
631
+
632
+ // Child constructor should have run after
633
+ expect(child.childInitialized).to.equal(true);
634
+ expect(child.childValue).to.equal(20);
635
+
636
+ // Child's overridden method calls super and wraps result
637
+ const result = child.getValue('test');
638
+ expect(result).to.equal('child-mock-value');
639
+ expect(Mock.inst().getValue.calls(0)).to.deep.equal({ x: 'test' });
640
+ });
641
+
642
+ it('will not expose __mockyInst in deep equal comparisons', () => {
643
+ const Mock = mocky.create(mocky.class({
644
+ constructor: mocky.function((context) => {
645
+ context.self.name = context.args.name;
646
+ }).args('name')
647
+ }));
648
+
649
+ const instance = new Mock('test');
650
+
651
+ expect(instance).to.deep.equal({ name: 'test' });
652
+ });
653
+
654
+ it('will sync plain value members between instance and Mock.inst()', () => {
655
+ const Mock = mocky.create(mocky.class({
656
+ name: null,
657
+ count: 0
658
+ }));
659
+
660
+ const inst = new Mock();
661
+
662
+ // Initial values from definition
663
+ expect(inst.name).to.equal(null);
664
+ expect(inst.count).to.equal(0);
665
+
666
+ // Setting on instance syncs to Mock.inst()
667
+ inst.name = 'test';
668
+ inst.count = 42;
669
+ expect(Mock.inst(0).name).to.equal('test');
670
+ expect(Mock.inst(0).count).to.equal(42);
671
+
672
+ // Setting on Mock.inst() syncs to instance
673
+ Mock.inst(0).name = 'changed';
674
+ expect(inst.name).to.equal('changed');
675
+ });
676
+
677
+ it('will isolate plain value members between class instances', () => {
678
+ const Mock = mocky.create(mocky.class({
679
+ value: null
680
+ }));
681
+
682
+ const inst1 = new Mock();
683
+ const inst2 = new Mock();
684
+
685
+ inst1.value = 'first';
686
+ inst2.value = 'second';
687
+
688
+ expect(inst1.value).to.equal('first');
689
+ expect(inst2.value).to.equal('second');
690
+ expect(Mock.inst(0).value).to.equal('first');
691
+ expect(Mock.inst(1).value).to.equal('second');
692
+ });
693
+
694
+ it('will allow pre-configuring plain value members before instantiation', () => {
695
+ const Mock = mocky.create(mocky.class({
696
+ config: null
697
+ }));
698
+
699
+ // Pre-configure before instantiation
700
+ Mock.inst(0).config = 'preset-value';
701
+
702
+ const inst = new Mock();
703
+ expect(inst.config).to.equal('preset-value');
704
+ });
705
+
706
+ it('will support mixing plain values and methods in class', () => {
707
+ const Mock = mocky.create(mocky.class({
708
+ name: null,
709
+ greet: mocky.function().args('greeting')
710
+ }));
711
+
712
+ Mock.inst().greet.ret('Hello!');
713
+
714
+ const inst = new Mock();
715
+ inst.name = 'World';
716
+
717
+ expect(inst.name).to.equal('World');
718
+ expect(Mock.inst(0).name).to.equal('World');
719
+ expect(inst.greet('Hi')).to.equal('Hello!');
720
+ expect(Mock.inst().greet.calls(0)).to.deep.equal({ greeting: 'Hi' });
721
+ });
722
+
723
+ it('will support nested objects with methods', () => {
724
+ const Mock = mocky.create(mocky.class({
725
+ db: mocky.object({
726
+ query: mocky.function().args('sql'),
727
+ close: mocky.function()
728
+ })
729
+ }));
730
+
731
+ Mock.inst(0).db.query.ret({ rows: ['a', 'b'] });
732
+ Mock.inst(0).db.close.ret(true);
733
+
734
+ const inst = new Mock();
735
+
736
+ expect(inst.db.query('SELECT *')).to.deep.equal({ rows: ['a', 'b'] });
737
+ expect(inst.db.close()).to.equal(true);
738
+ expect(Mock.inst(0).db.query.calls(0)).to.deep.equal({ sql: 'SELECT *' });
739
+ });
740
+
741
+ it('will access mock helpers directly on instance methods', () => {
742
+ const Mock = mocky.create(mocky.class({
743
+ run: mocky.function().args('arg')
744
+ }));
745
+
746
+ const inst = new Mock();
747
+
748
+ // Configure via instance
749
+ inst.run.ret('test-ret');
750
+ expect(inst.run('hello')).to.equal('test-ret');
751
+
752
+ // Access calls via instance
753
+ expect(inst.run.calls(0)).to.deep.equal({ arg: 'hello' });
754
+ expect(inst.run.calls().length).to.equal(1);
755
+
756
+ // Reset via instance
757
+ inst.run.reset();
758
+ expect(inst.run.calls().length).to.equal(0);
759
+ });
760
+ });
761
+
762
+ describe('spy', () => {
763
+ it('will spy on method, call through, track calls and preserve this', () => {
764
+ const obj = {
765
+ value: 42,
766
+ greet: function(name) {
767
+ return `Hello, ${name}!`;
768
+ },
769
+ getValue: function() {
770
+ return this.value;
771
+ }
772
+ };
773
+
774
+ const greetSpy = mocky.spy(obj, 'greet');
775
+ const valueSpy = mocky.spy(obj, 'getValue');
776
+
777
+ // Calls through to original
778
+ expect(obj.greet('World')).to.equal('Hello, World!');
779
+ expect(obj.greet('Test')).to.equal('Hello, Test!');
780
+
781
+ // Tracks multiple calls
782
+ expect(greetSpy.calls().length).to.equal(2);
783
+ expect(greetSpy.calls(0)).to.deep.equal(['World']);
784
+ expect(greetSpy.calls(1)).to.deep.equal(['Test']);
785
+
786
+ // Preserves this context
787
+ expect(obj.getValue()).to.equal(42);
788
+ expect(valueSpy.calls().length).to.equal(1);
789
+
790
+ greetSpy.restore();
791
+ valueSpy.restore();
792
+ });
793
+
794
+ it('will override return value with ret() while still tracking', () => {
795
+ const obj = {
796
+ getValue: function() {
797
+ return 'original';
798
+ }
799
+ };
800
+
801
+ const spy = mocky.spy(obj, 'getValue');
802
+ spy.ret('overridden');
803
+
804
+ const result = obj.getValue();
805
+
806
+ expect(result).to.equal('overridden');
807
+ expect(spy.calls().length).to.equal(1);
808
+ });
809
+
810
+ it('will restore original method', () => {
811
+ const obj = {
812
+ doThing: function() {
813
+ return 'original';
814
+ }
815
+ };
816
+
817
+ const spy = mocky.spy(obj, 'doThing');
818
+ spy.ret('spied');
819
+
820
+ expect(obj.doThing()).to.equal('spied');
821
+
822
+ spy.restore();
823
+
824
+ expect(obj.doThing()).to.equal('original');
825
+ });
826
+
827
+ it('will spy on class prototype method', () => {
828
+ class MyClass {
829
+ greet(name) {
830
+ return `Hello, ${name}`;
831
+ }
832
+ }
833
+
834
+ const spy = mocky.spy(MyClass.prototype, 'greet');
835
+
836
+ const instance = new MyClass();
837
+ const result = instance.greet('Test');
838
+
839
+ expect(result).to.equal('Hello, Test');
840
+ expect(spy.calls().length).to.equal(1);
841
+ expect(spy.calls(0)).to.deep.equal(['Test']);
842
+
843
+ spy.restore();
844
+ });
845
+
846
+ it('will spy with replacement function and provide context.original', () => {
847
+ const obj = {
848
+ add: function(a, b) {
849
+ return a + b;
850
+ }
851
+ };
852
+
853
+ // Replacement with args and context.original
854
+ const spy = mocky.spy(obj, 'add', mocky.function((context) => {
855
+ const originalResult = context.original.apply(context.self, context.rawArgs);
856
+ return originalResult * 10;
857
+ }).args('x', 'y'));
858
+
859
+ const result = obj.add(3, 4);
860
+
861
+ expect(result).to.equal(70); // (3 + 4) * 10
862
+ expect(spy.calls(0)).to.deep.equal({ x: 3, y: 4 });
863
+
864
+ spy.restore();
865
+ });
866
+ });
867
+ });