meocord 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/cjs/testing/index.cjs +118 -62
- package/dist/esm/testing/index.js +1 -1
- package/dist/esm/testing/mock-fn.js +19 -62
- package/dist/esm/testing/mock-interaction.js +99 -1
- package/dist/types/testing/index.d.ts +30 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -513,6 +513,29 @@ edited.delete // → a mock fn
|
|
|
513
513
|
expect(msg.delete).toHaveBeenCalledTimes(1)
|
|
514
514
|
```
|
|
515
515
|
|
|
516
|
+
### `createMock`
|
|
517
|
+
|
|
518
|
+
Mocks any type without a runtime class — use it for the services a controller depends on. `createMockInteraction` needs a class to build a prototype chain from, which is what makes `instanceof` and the real type guards work; a service double needs none of that, and an injected dependency may be an interface that does not exist at runtime at all.
|
|
519
|
+
|
|
520
|
+
Every property is a mock fn, created on first access, so a double only declares what the test cares about. The result is assignable to `T`, so it goes straight into `useValue` with no cast — which matters because a class holding a `private` member (a logger, say) can never be satisfied by an object literal.
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
import { createMock, MeoCordTestingModule } from 'meocord/testing'
|
|
524
|
+
import { GreetingService } from '@src/services/greeting.service.js'
|
|
525
|
+
|
|
526
|
+
const greetingService = createMock<GreetingService>()
|
|
527
|
+
greetingService.buildGreeting.mockResolvedValue('Hello, Alice!')
|
|
528
|
+
|
|
529
|
+
const module = MeoCordTestingModule.create({
|
|
530
|
+
controllers: [GreetingSlashController],
|
|
531
|
+
providers: [{ provide: GreetingService, useValue: greetingService }],
|
|
532
|
+
}).compile()
|
|
533
|
+
|
|
534
|
+
expect(greetingService.buildGreeting).toHaveBeenCalledWith('Alice')
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Nested access works without declaring the shape first — `cache.store.flush()` is a mock fn on a mock fn. Properties passed as `createMock<T>({ ... })` are used exactly as given rather than wrapped, so call assertions do not apply to those.
|
|
538
|
+
|
|
516
539
|
### `overrideGuard`
|
|
517
540
|
|
|
518
541
|
Replaces a guard class in the DI container with a stub. No guard dependencies need to be provided.
|
|
@@ -152,23 +152,16 @@ function isValueProvider(p) {
|
|
|
152
152
|
* fn(2) // → 99
|
|
153
153
|
* fn.mock.calls // → [[2], [2]]
|
|
154
154
|
*/ function createMockFn(impl) {
|
|
155
|
+
// One persistent implementation and one queue of single-use ones, which is how
|
|
156
|
+
// jest and vitest model this. mockReturnValue, mockResolvedValue,
|
|
157
|
+
// mockRejectedValue and mockImplementation all write the same slot, so the last
|
|
158
|
+
// call wins; the four `*Once` variants all push onto the same queue, so they are
|
|
159
|
+
// consumed in the order they were declared regardless of which kind they are.
|
|
160
|
+
// Keeping a slot per kind instead gave a fixed precedence, where a stored
|
|
161
|
+
// resolved value beat a later mockRejectedValue and the override was silently
|
|
162
|
+
// dropped.
|
|
155
163
|
let currentImpl = impl;
|
|
156
|
-
let
|
|
157
|
-
let returnValue = {
|
|
158
|
-
set: false,
|
|
159
|
-
value: undefined
|
|
160
|
-
};
|
|
161
|
-
let resolvedOnce = [];
|
|
162
|
-
let resolvedValue = {
|
|
163
|
-
set: false,
|
|
164
|
-
value: undefined
|
|
165
|
-
};
|
|
166
|
-
let rejectedOnce = [];
|
|
167
|
-
let rejectedValue = {
|
|
168
|
-
set: false,
|
|
169
|
-
value: undefined
|
|
170
|
-
};
|
|
171
|
-
const implOnce = [];
|
|
164
|
+
let onceQueue = [];
|
|
172
165
|
let name = 'vi.fn';
|
|
173
166
|
const calls = [];
|
|
174
167
|
const results = [];
|
|
@@ -179,20 +172,8 @@ function isValueProvider(p) {
|
|
|
179
172
|
let type = 'return';
|
|
180
173
|
let value;
|
|
181
174
|
try {
|
|
182
|
-
if (
|
|
183
|
-
value =
|
|
184
|
-
} else if (returnOnce.length > 0) {
|
|
185
|
-
value = returnOnce.shift();
|
|
186
|
-
} else if (returnValue.set) {
|
|
187
|
-
value = returnValue.value;
|
|
188
|
-
} else if (resolvedOnce.length > 0) {
|
|
189
|
-
value = Promise.resolve(resolvedOnce.shift());
|
|
190
|
-
} else if (resolvedValue.set) {
|
|
191
|
-
value = Promise.resolve(resolvedValue.value);
|
|
192
|
-
} else if (rejectedOnce.length > 0) {
|
|
193
|
-
value = Promise.reject(rejectedOnce.shift());
|
|
194
|
-
} else if (rejectedValue.set) {
|
|
195
|
-
value = Promise.reject(rejectedValue.value);
|
|
175
|
+
if (onceQueue.length > 0) {
|
|
176
|
+
value = onceQueue.shift().apply(this, args);
|
|
196
177
|
} else if (currentImpl !== undefined) {
|
|
197
178
|
value = currentImpl.apply(this, args);
|
|
198
179
|
} else {
|
|
@@ -234,36 +215,27 @@ function isValueProvider(p) {
|
|
|
234
215
|
enumerable: false
|
|
235
216
|
});
|
|
236
217
|
mockFn.mockReturnValue = (v)=>{
|
|
237
|
-
|
|
238
|
-
set: true,
|
|
239
|
-
value: v
|
|
240
|
-
};
|
|
218
|
+
currentImpl = ()=>v;
|
|
241
219
|
return mockFn;
|
|
242
220
|
};
|
|
243
221
|
mockFn.mockReturnValueOnce = (v)=>{
|
|
244
|
-
|
|
222
|
+
onceQueue.push(()=>v);
|
|
245
223
|
return mockFn;
|
|
246
224
|
};
|
|
247
225
|
mockFn.mockResolvedValue = (v)=>{
|
|
248
|
-
|
|
249
|
-
set: true,
|
|
250
|
-
value: v
|
|
251
|
-
};
|
|
226
|
+
currentImpl = ()=>Promise.resolve(v);
|
|
252
227
|
return mockFn;
|
|
253
228
|
};
|
|
254
229
|
mockFn.mockResolvedValueOnce = (v)=>{
|
|
255
|
-
|
|
230
|
+
onceQueue.push(()=>Promise.resolve(v));
|
|
256
231
|
return mockFn;
|
|
257
232
|
};
|
|
258
233
|
mockFn.mockRejectedValue = (v)=>{
|
|
259
|
-
|
|
260
|
-
set: true,
|
|
261
|
-
value: v
|
|
262
|
-
};
|
|
234
|
+
currentImpl = ()=>Promise.reject(v);
|
|
263
235
|
return mockFn;
|
|
264
236
|
};
|
|
265
237
|
mockFn.mockRejectedValueOnce = (v)=>{
|
|
266
|
-
|
|
238
|
+
onceQueue.push(()=>Promise.reject(v));
|
|
267
239
|
return mockFn;
|
|
268
240
|
};
|
|
269
241
|
mockFn.mockImplementation = (fn)=>{
|
|
@@ -271,7 +243,7 @@ function isValueProvider(p) {
|
|
|
271
243
|
return mockFn;
|
|
272
244
|
};
|
|
273
245
|
mockFn.mockImplementationOnce = (fn)=>{
|
|
274
|
-
|
|
246
|
+
onceQueue.push(fn);
|
|
275
247
|
return mockFn;
|
|
276
248
|
};
|
|
277
249
|
mockFn.mockClear = ()=>{
|
|
@@ -284,22 +256,7 @@ function isValueProvider(p) {
|
|
|
284
256
|
calls.length = 0;
|
|
285
257
|
results.length = 0;
|
|
286
258
|
instances.length = 0;
|
|
287
|
-
|
|
288
|
-
returnValue = {
|
|
289
|
-
set: false,
|
|
290
|
-
value: undefined
|
|
291
|
-
};
|
|
292
|
-
resolvedOnce = [];
|
|
293
|
-
resolvedValue = {
|
|
294
|
-
set: false,
|
|
295
|
-
value: undefined
|
|
296
|
-
};
|
|
297
|
-
rejectedOnce = [];
|
|
298
|
-
rejectedValue = {
|
|
299
|
-
set: false,
|
|
300
|
-
value: undefined
|
|
301
|
-
};
|
|
302
|
-
implOnce.length = 0;
|
|
259
|
+
onceQueue = [];
|
|
303
260
|
currentImpl = impl;
|
|
304
261
|
return mockFn;
|
|
305
262
|
};
|
|
@@ -582,6 +539,104 @@ function findPrototypeMethod(instance, name) {
|
|
|
582
539
|
return stubDeep(instance, stubs);
|
|
583
540
|
}
|
|
584
541
|
// ---------------------------------------------------------------------------
|
|
542
|
+
// createMock — class-free mock for services and interfaces
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
544
|
+
/**
|
|
545
|
+
* A mock fn that also answers property access with another one, so a nested call
|
|
546
|
+
* like `cache.store.flush()` works without declaring `store` up front.
|
|
547
|
+
*
|
|
548
|
+
* stubDeep cannot serve this: it decides between a mock fn and a nested object by
|
|
549
|
+
* looking up the prototype chain, and a service double has no prototype to read.
|
|
550
|
+
* Everything here is callable instead, which is the right default when the shape
|
|
551
|
+
* being mocked is an interface that erases at runtime.
|
|
552
|
+
*/ function stubCallable() {
|
|
553
|
+
const fn = createMockFn();
|
|
554
|
+
const nested = new Map();
|
|
555
|
+
return new Proxy(fn, {
|
|
556
|
+
get (target, prop) {
|
|
557
|
+
if (typeof prop === 'symbol') return Reflect.get(target, prop, target);
|
|
558
|
+
const key = prop;
|
|
559
|
+
// Never thenable — otherwise awaiting a mock hangs on itself
|
|
560
|
+
if (key === 'then') return undefined;
|
|
561
|
+
// The mock's own API (`mock`, `mockReturnValue`, `_isMockFunction`, …) and
|
|
562
|
+
// the function intrinsics pass straight through.
|
|
563
|
+
if (key in target) return Reflect.get(target, prop, target);
|
|
564
|
+
if (!nested.has(key)) nested.set(key, stubCallable());
|
|
565
|
+
return nested.get(key);
|
|
566
|
+
},
|
|
567
|
+
set (target, prop, value) {
|
|
568
|
+
Object.defineProperty(target, prop, {
|
|
569
|
+
value,
|
|
570
|
+
writable: true,
|
|
571
|
+
enumerable: true,
|
|
572
|
+
configurable: true
|
|
573
|
+
});
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Creates a mock of any type, with no runtime class required.
|
|
580
|
+
*
|
|
581
|
+
* `createMockInteraction` needs a class to build a prototype chain from, which is
|
|
582
|
+
* what makes `instanceof` and the real type guards work. A service double needs
|
|
583
|
+
* none of that, and frequently has no class to pass at all — an injected
|
|
584
|
+
* dependency may be an interface, which does not exist at runtime.
|
|
585
|
+
*
|
|
586
|
+
* Every property is a mock fn, created on first access and cached, so a double
|
|
587
|
+
* only has to declare what the test actually cares about. The result is assignable
|
|
588
|
+
* to `T`, so it can be handed to `useValue` or a constructor without a cast.
|
|
589
|
+
*
|
|
590
|
+
* Properties supplied through {@link MockProps} are used as given rather than
|
|
591
|
+
* wrapped, so call assertions do not apply to them.
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* ```ts
|
|
595
|
+
* const notifications = createMock<NotificationService>()
|
|
596
|
+
* notifications.notify.mockResolvedValue('sent')
|
|
597
|
+
*
|
|
598
|
+
* const module = MeoCordTestingModule.create({
|
|
599
|
+
* controllers: [AlertController],
|
|
600
|
+
* providers: [{ provide: NotificationService, useValue: notifications }],
|
|
601
|
+
* }).compile()
|
|
602
|
+
*
|
|
603
|
+
* expect(notifications.notify).toHaveBeenCalledWith('hello')
|
|
604
|
+
* ```
|
|
605
|
+
*/ function createMock(props) {
|
|
606
|
+
const target = {};
|
|
607
|
+
if (props !== undefined) {
|
|
608
|
+
for (const [key, value] of Object.entries(props)){
|
|
609
|
+
Object.defineProperty(target, key, {
|
|
610
|
+
value,
|
|
611
|
+
writable: true,
|
|
612
|
+
enumerable: true,
|
|
613
|
+
configurable: true
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const stubs = new Map();
|
|
618
|
+
return new Proxy(target, {
|
|
619
|
+
get (instance, prop) {
|
|
620
|
+
if (typeof prop === 'symbol') return Reflect.get(instance, prop, instance);
|
|
621
|
+
const key = prop;
|
|
622
|
+
if (key === 'then') return undefined;
|
|
623
|
+
// An explicitly supplied prop wins over the auto-stub.
|
|
624
|
+
if (Object.prototype.hasOwnProperty.call(instance, key)) return Reflect.get(instance, prop, instance);
|
|
625
|
+
if (!stubs.has(key)) stubs.set(key, stubCallable());
|
|
626
|
+
return stubs.get(key);
|
|
627
|
+
},
|
|
628
|
+
set (instance, prop, value) {
|
|
629
|
+
Object.defineProperty(instance, prop, {
|
|
630
|
+
value,
|
|
631
|
+
writable: true,
|
|
632
|
+
enumerable: true,
|
|
633
|
+
configurable: true
|
|
634
|
+
});
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
// ---------------------------------------------------------------------------
|
|
585
640
|
// Convenience wrappers for common discord.js classes
|
|
586
641
|
// ---------------------------------------------------------------------------
|
|
587
642
|
/** Creates a mock {@link User}. All methods are auto-stubbed as a mock fn. */ const createMockUser = ()=>createMockInteraction(discord_js.User);
|
|
@@ -793,6 +848,7 @@ exports.MeoCordTestingModule = MeoCordTestingModule;
|
|
|
793
848
|
exports.TestingModule = TestingModule;
|
|
794
849
|
exports.TestingModuleBuilder = TestingModuleBuilder;
|
|
795
850
|
exports.createChatInputOptions = createChatInputOptions;
|
|
851
|
+
exports.createMock = createMock;
|
|
796
852
|
exports.createMockChannel = createMockChannel;
|
|
797
853
|
exports.createMockClient = createMockClient;
|
|
798
854
|
exports.createMockFn = createMockFn;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { MeoCordTestingModule, TestingModule, TestingModuleBuilder } from './meocord-testing-module.js';
|
|
2
|
-
export { createChatInputOptions, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMessage, createMockUser } from './mock-interaction.js';
|
|
2
|
+
export { createChatInputOptions, createMock, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMessage, createMockUser } from './mock-interaction.js';
|
|
3
3
|
export { createMockFn, isMockFunction } from './mock-fn.js';
|
|
@@ -30,23 +30,16 @@
|
|
|
30
30
|
* fn(2) // → 99
|
|
31
31
|
* fn.mock.calls // → [[2], [2]]
|
|
32
32
|
*/ function createMockFn(impl) {
|
|
33
|
+
// One persistent implementation and one queue of single-use ones, which is how
|
|
34
|
+
// jest and vitest model this. mockReturnValue, mockResolvedValue,
|
|
35
|
+
// mockRejectedValue and mockImplementation all write the same slot, so the last
|
|
36
|
+
// call wins; the four `*Once` variants all push onto the same queue, so they are
|
|
37
|
+
// consumed in the order they were declared regardless of which kind they are.
|
|
38
|
+
// Keeping a slot per kind instead gave a fixed precedence, where a stored
|
|
39
|
+
// resolved value beat a later mockRejectedValue and the override was silently
|
|
40
|
+
// dropped.
|
|
33
41
|
let currentImpl = impl;
|
|
34
|
-
let
|
|
35
|
-
let returnValue = {
|
|
36
|
-
set: false,
|
|
37
|
-
value: undefined
|
|
38
|
-
};
|
|
39
|
-
let resolvedOnce = [];
|
|
40
|
-
let resolvedValue = {
|
|
41
|
-
set: false,
|
|
42
|
-
value: undefined
|
|
43
|
-
};
|
|
44
|
-
let rejectedOnce = [];
|
|
45
|
-
let rejectedValue = {
|
|
46
|
-
set: false,
|
|
47
|
-
value: undefined
|
|
48
|
-
};
|
|
49
|
-
const implOnce = [];
|
|
42
|
+
let onceQueue = [];
|
|
50
43
|
let name = 'vi.fn';
|
|
51
44
|
const calls = [];
|
|
52
45
|
const results = [];
|
|
@@ -57,20 +50,8 @@
|
|
|
57
50
|
let type = 'return';
|
|
58
51
|
let value;
|
|
59
52
|
try {
|
|
60
|
-
if (
|
|
61
|
-
value =
|
|
62
|
-
} else if (returnOnce.length > 0) {
|
|
63
|
-
value = returnOnce.shift();
|
|
64
|
-
} else if (returnValue.set) {
|
|
65
|
-
value = returnValue.value;
|
|
66
|
-
} else if (resolvedOnce.length > 0) {
|
|
67
|
-
value = Promise.resolve(resolvedOnce.shift());
|
|
68
|
-
} else if (resolvedValue.set) {
|
|
69
|
-
value = Promise.resolve(resolvedValue.value);
|
|
70
|
-
} else if (rejectedOnce.length > 0) {
|
|
71
|
-
value = Promise.reject(rejectedOnce.shift());
|
|
72
|
-
} else if (rejectedValue.set) {
|
|
73
|
-
value = Promise.reject(rejectedValue.value);
|
|
53
|
+
if (onceQueue.length > 0) {
|
|
54
|
+
value = onceQueue.shift().apply(this, args);
|
|
74
55
|
} else if (currentImpl !== undefined) {
|
|
75
56
|
value = currentImpl.apply(this, args);
|
|
76
57
|
} else {
|
|
@@ -112,36 +93,27 @@
|
|
|
112
93
|
enumerable: false
|
|
113
94
|
});
|
|
114
95
|
mockFn.mockReturnValue = (v)=>{
|
|
115
|
-
|
|
116
|
-
set: true,
|
|
117
|
-
value: v
|
|
118
|
-
};
|
|
96
|
+
currentImpl = ()=>v;
|
|
119
97
|
return mockFn;
|
|
120
98
|
};
|
|
121
99
|
mockFn.mockReturnValueOnce = (v)=>{
|
|
122
|
-
|
|
100
|
+
onceQueue.push(()=>v);
|
|
123
101
|
return mockFn;
|
|
124
102
|
};
|
|
125
103
|
mockFn.mockResolvedValue = (v)=>{
|
|
126
|
-
|
|
127
|
-
set: true,
|
|
128
|
-
value: v
|
|
129
|
-
};
|
|
104
|
+
currentImpl = ()=>Promise.resolve(v);
|
|
130
105
|
return mockFn;
|
|
131
106
|
};
|
|
132
107
|
mockFn.mockResolvedValueOnce = (v)=>{
|
|
133
|
-
|
|
108
|
+
onceQueue.push(()=>Promise.resolve(v));
|
|
134
109
|
return mockFn;
|
|
135
110
|
};
|
|
136
111
|
mockFn.mockRejectedValue = (v)=>{
|
|
137
|
-
|
|
138
|
-
set: true,
|
|
139
|
-
value: v
|
|
140
|
-
};
|
|
112
|
+
currentImpl = ()=>Promise.reject(v);
|
|
141
113
|
return mockFn;
|
|
142
114
|
};
|
|
143
115
|
mockFn.mockRejectedValueOnce = (v)=>{
|
|
144
|
-
|
|
116
|
+
onceQueue.push(()=>Promise.reject(v));
|
|
145
117
|
return mockFn;
|
|
146
118
|
};
|
|
147
119
|
mockFn.mockImplementation = (fn)=>{
|
|
@@ -149,7 +121,7 @@
|
|
|
149
121
|
return mockFn;
|
|
150
122
|
};
|
|
151
123
|
mockFn.mockImplementationOnce = (fn)=>{
|
|
152
|
-
|
|
124
|
+
onceQueue.push(fn);
|
|
153
125
|
return mockFn;
|
|
154
126
|
};
|
|
155
127
|
mockFn.mockClear = ()=>{
|
|
@@ -162,22 +134,7 @@
|
|
|
162
134
|
calls.length = 0;
|
|
163
135
|
results.length = 0;
|
|
164
136
|
instances.length = 0;
|
|
165
|
-
|
|
166
|
-
returnValue = {
|
|
167
|
-
set: false,
|
|
168
|
-
value: undefined
|
|
169
|
-
};
|
|
170
|
-
resolvedOnce = [];
|
|
171
|
-
resolvedValue = {
|
|
172
|
-
set: false,
|
|
173
|
-
value: undefined
|
|
174
|
-
};
|
|
175
|
-
rejectedOnce = [];
|
|
176
|
-
rejectedValue = {
|
|
177
|
-
set: false,
|
|
178
|
-
value: undefined
|
|
179
|
-
};
|
|
180
|
-
implOnce.length = 0;
|
|
137
|
+
onceQueue = [];
|
|
181
138
|
currentImpl = impl;
|
|
182
139
|
return mockFn;
|
|
183
140
|
};
|
|
@@ -269,6 +269,104 @@ function findPrototypeMethod(instance, name) {
|
|
|
269
269
|
return stubDeep(instance, stubs);
|
|
270
270
|
}
|
|
271
271
|
// ---------------------------------------------------------------------------
|
|
272
|
+
// createMock — class-free mock for services and interfaces
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
/**
|
|
275
|
+
* A mock fn that also answers property access with another one, so a nested call
|
|
276
|
+
* like `cache.store.flush()` works without declaring `store` up front.
|
|
277
|
+
*
|
|
278
|
+
* stubDeep cannot serve this: it decides between a mock fn and a nested object by
|
|
279
|
+
* looking up the prototype chain, and a service double has no prototype to read.
|
|
280
|
+
* Everything here is callable instead, which is the right default when the shape
|
|
281
|
+
* being mocked is an interface that erases at runtime.
|
|
282
|
+
*/ function stubCallable() {
|
|
283
|
+
const fn = createMockFn();
|
|
284
|
+
const nested = new Map();
|
|
285
|
+
return new Proxy(fn, {
|
|
286
|
+
get (target, prop) {
|
|
287
|
+
if (typeof prop === 'symbol') return Reflect.get(target, prop, target);
|
|
288
|
+
const key = prop;
|
|
289
|
+
// Never thenable — otherwise awaiting a mock hangs on itself
|
|
290
|
+
if (key === 'then') return undefined;
|
|
291
|
+
// The mock's own API (`mock`, `mockReturnValue`, `_isMockFunction`, …) and
|
|
292
|
+
// the function intrinsics pass straight through.
|
|
293
|
+
if (key in target) return Reflect.get(target, prop, target);
|
|
294
|
+
if (!nested.has(key)) nested.set(key, stubCallable());
|
|
295
|
+
return nested.get(key);
|
|
296
|
+
},
|
|
297
|
+
set (target, prop, value) {
|
|
298
|
+
Object.defineProperty(target, prop, {
|
|
299
|
+
value,
|
|
300
|
+
writable: true,
|
|
301
|
+
enumerable: true,
|
|
302
|
+
configurable: true
|
|
303
|
+
});
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Creates a mock of any type, with no runtime class required.
|
|
310
|
+
*
|
|
311
|
+
* `createMockInteraction` needs a class to build a prototype chain from, which is
|
|
312
|
+
* what makes `instanceof` and the real type guards work. A service double needs
|
|
313
|
+
* none of that, and frequently has no class to pass at all — an injected
|
|
314
|
+
* dependency may be an interface, which does not exist at runtime.
|
|
315
|
+
*
|
|
316
|
+
* Every property is a mock fn, created on first access and cached, so a double
|
|
317
|
+
* only has to declare what the test actually cares about. The result is assignable
|
|
318
|
+
* to `T`, so it can be handed to `useValue` or a constructor without a cast.
|
|
319
|
+
*
|
|
320
|
+
* Properties supplied through {@link MockProps} are used as given rather than
|
|
321
|
+
* wrapped, so call assertions do not apply to them.
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```ts
|
|
325
|
+
* const notifications = createMock<NotificationService>()
|
|
326
|
+
* notifications.notify.mockResolvedValue('sent')
|
|
327
|
+
*
|
|
328
|
+
* const module = MeoCordTestingModule.create({
|
|
329
|
+
* controllers: [AlertController],
|
|
330
|
+
* providers: [{ provide: NotificationService, useValue: notifications }],
|
|
331
|
+
* }).compile()
|
|
332
|
+
*
|
|
333
|
+
* expect(notifications.notify).toHaveBeenCalledWith('hello')
|
|
334
|
+
* ```
|
|
335
|
+
*/ function createMock(props) {
|
|
336
|
+
const target = {};
|
|
337
|
+
if (props !== undefined) {
|
|
338
|
+
for (const [key, value] of Object.entries(props)){
|
|
339
|
+
Object.defineProperty(target, key, {
|
|
340
|
+
value,
|
|
341
|
+
writable: true,
|
|
342
|
+
enumerable: true,
|
|
343
|
+
configurable: true
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const stubs = new Map();
|
|
348
|
+
return new Proxy(target, {
|
|
349
|
+
get (instance, prop) {
|
|
350
|
+
if (typeof prop === 'symbol') return Reflect.get(instance, prop, instance);
|
|
351
|
+
const key = prop;
|
|
352
|
+
if (key === 'then') return undefined;
|
|
353
|
+
// An explicitly supplied prop wins over the auto-stub.
|
|
354
|
+
if (Object.prototype.hasOwnProperty.call(instance, key)) return Reflect.get(instance, prop, instance);
|
|
355
|
+
if (!stubs.has(key)) stubs.set(key, stubCallable());
|
|
356
|
+
return stubs.get(key);
|
|
357
|
+
},
|
|
358
|
+
set (instance, prop, value) {
|
|
359
|
+
Object.defineProperty(instance, prop, {
|
|
360
|
+
value,
|
|
361
|
+
writable: true,
|
|
362
|
+
enumerable: true,
|
|
363
|
+
configurable: true
|
|
364
|
+
});
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
272
370
|
// Convenience wrappers for common discord.js classes
|
|
273
371
|
// ---------------------------------------------------------------------------
|
|
274
372
|
/** Creates a mock {@link User}. All methods are auto-stubbed as a mock fn. */ const createMockUser = ()=>createMockInteraction(User);
|
|
@@ -476,4 +574,4 @@ function createChatInputOptions(opts = {}) {
|
|
|
476
574
|
return stubDeep(base);
|
|
477
575
|
}
|
|
478
576
|
|
|
479
|
-
export { createChatInputOptions, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMessage, createMockUser };
|
|
577
|
+
export { createChatInputOptions, createMock, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMessage, createMockUser };
|
|
@@ -212,6 +212,35 @@ interface InteractionClass<T> {
|
|
|
212
212
|
* ```
|
|
213
213
|
*/
|
|
214
214
|
declare function createMockInteraction<T extends object>(Class: InteractionClass<T>, props?: MockProps<T>): DeepMocked<T>;
|
|
215
|
+
/**
|
|
216
|
+
* Creates a mock of any type, with no runtime class required.
|
|
217
|
+
*
|
|
218
|
+
* `createMockInteraction` needs a class to build a prototype chain from, which is
|
|
219
|
+
* what makes `instanceof` and the real type guards work. A service double needs
|
|
220
|
+
* none of that, and frequently has no class to pass at all — an injected
|
|
221
|
+
* dependency may be an interface, which does not exist at runtime.
|
|
222
|
+
*
|
|
223
|
+
* Every property is a mock fn, created on first access and cached, so a double
|
|
224
|
+
* only has to declare what the test actually cares about. The result is assignable
|
|
225
|
+
* to `T`, so it can be handed to `useValue` or a constructor without a cast.
|
|
226
|
+
*
|
|
227
|
+
* Properties supplied through {@link MockProps} are used as given rather than
|
|
228
|
+
* wrapped, so call assertions do not apply to them.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```ts
|
|
232
|
+
* const notifications = createMock<NotificationService>()
|
|
233
|
+
* notifications.notify.mockResolvedValue('sent')
|
|
234
|
+
*
|
|
235
|
+
* const module = MeoCordTestingModule.create({
|
|
236
|
+
* controllers: [AlertController],
|
|
237
|
+
* providers: [{ provide: NotificationService, useValue: notifications }],
|
|
238
|
+
* }).compile()
|
|
239
|
+
*
|
|
240
|
+
* expect(notifications.notify).toHaveBeenCalledWith('hello')
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
declare function createMock<T extends object>(props?: MockProps<T>): DeepMocked<T>;
|
|
215
244
|
/** Creates a mock {@link User}. All methods are auto-stubbed as a mock fn. */
|
|
216
245
|
declare const createMockUser: () => DeepMocked<User>;
|
|
217
246
|
/**
|
|
@@ -274,5 +303,5 @@ interface ChatInputOptions {
|
|
|
274
303
|
*/
|
|
275
304
|
declare function createChatInputOptions<Cached extends CacheType = any>(opts?: ChatInputOptions): DeepMocked<CommandInteractionOptionResolver<Cached>>;
|
|
276
305
|
|
|
277
|
-
export { MeoCordTestingModule, TestingModule, TestingModuleBuilder, createChatInputOptions, createMockChannel, createMockClient, createMockFn, createMockGuild, createMockInteraction, createMockMessage, createMockUser, isMockFunction };
|
|
306
|
+
export { MeoCordTestingModule, TestingModule, TestingModuleBuilder, createChatInputOptions, createMock, createMockChannel, createMockClient, createMockFn, createMockGuild, createMockInteraction, createMockMessage, createMockUser, isMockFunction };
|
|
278
307
|
export type { ChatInputOptions, ClassProvider, DeepMocked, Mock, MockInstance, MockProps, MockResult, MockState, MockedFunction, Provider, TestingModuleOptions, ValueProvider };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meocord",
|
|
3
3
|
"description": "Decorator-based Discord bot framework built on discord.js. Brings NestJS-style controllers, dependency injection, guards, and testing utilities to bot development — with a full CLI and TypeScript-first design.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.1.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22"
|