meocord 2.0.0-beta.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
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.
|
|
@@ -582,6 +582,104 @@ function findPrototypeMethod(instance, name) {
|
|
|
582
582
|
return stubDeep(instance, stubs);
|
|
583
583
|
}
|
|
584
584
|
// ---------------------------------------------------------------------------
|
|
585
|
+
// createMock — class-free mock for services and interfaces
|
|
586
|
+
// ---------------------------------------------------------------------------
|
|
587
|
+
/**
|
|
588
|
+
* A mock fn that also answers property access with another one, so a nested call
|
|
589
|
+
* like `cache.store.flush()` works without declaring `store` up front.
|
|
590
|
+
*
|
|
591
|
+
* stubDeep cannot serve this: it decides between a mock fn and a nested object by
|
|
592
|
+
* looking up the prototype chain, and a service double has no prototype to read.
|
|
593
|
+
* Everything here is callable instead, which is the right default when the shape
|
|
594
|
+
* being mocked is an interface that erases at runtime.
|
|
595
|
+
*/ function stubCallable() {
|
|
596
|
+
const fn = createMockFn();
|
|
597
|
+
const nested = new Map();
|
|
598
|
+
return new Proxy(fn, {
|
|
599
|
+
get (target, prop) {
|
|
600
|
+
if (typeof prop === 'symbol') return Reflect.get(target, prop, target);
|
|
601
|
+
const key = prop;
|
|
602
|
+
// Never thenable — otherwise awaiting a mock hangs on itself
|
|
603
|
+
if (key === 'then') return undefined;
|
|
604
|
+
// The mock's own API (`mock`, `mockReturnValue`, `_isMockFunction`, …) and
|
|
605
|
+
// the function intrinsics pass straight through.
|
|
606
|
+
if (key in target) return Reflect.get(target, prop, target);
|
|
607
|
+
if (!nested.has(key)) nested.set(key, stubCallable());
|
|
608
|
+
return nested.get(key);
|
|
609
|
+
},
|
|
610
|
+
set (target, prop, value) {
|
|
611
|
+
Object.defineProperty(target, prop, {
|
|
612
|
+
value,
|
|
613
|
+
writable: true,
|
|
614
|
+
enumerable: true,
|
|
615
|
+
configurable: true
|
|
616
|
+
});
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Creates a mock of any type, with no runtime class required.
|
|
623
|
+
*
|
|
624
|
+
* `createMockInteraction` needs a class to build a prototype chain from, which is
|
|
625
|
+
* what makes `instanceof` and the real type guards work. A service double needs
|
|
626
|
+
* none of that, and frequently has no class to pass at all — an injected
|
|
627
|
+
* dependency may be an interface, which does not exist at runtime.
|
|
628
|
+
*
|
|
629
|
+
* Every property is a mock fn, created on first access and cached, so a double
|
|
630
|
+
* only has to declare what the test actually cares about. The result is assignable
|
|
631
|
+
* to `T`, so it can be handed to `useValue` or a constructor without a cast.
|
|
632
|
+
*
|
|
633
|
+
* Properties supplied through {@link MockProps} are used as given rather than
|
|
634
|
+
* wrapped, so call assertions do not apply to them.
|
|
635
|
+
*
|
|
636
|
+
* @example
|
|
637
|
+
* ```ts
|
|
638
|
+
* const notifications = createMock<NotificationService>()
|
|
639
|
+
* notifications.notify.mockResolvedValue('sent')
|
|
640
|
+
*
|
|
641
|
+
* const module = MeoCordTestingModule.create({
|
|
642
|
+
* controllers: [AlertController],
|
|
643
|
+
* providers: [{ provide: NotificationService, useValue: notifications }],
|
|
644
|
+
* }).compile()
|
|
645
|
+
*
|
|
646
|
+
* expect(notifications.notify).toHaveBeenCalledWith('hello')
|
|
647
|
+
* ```
|
|
648
|
+
*/ function createMock(props) {
|
|
649
|
+
const target = {};
|
|
650
|
+
if (props !== undefined) {
|
|
651
|
+
for (const [key, value] of Object.entries(props)){
|
|
652
|
+
Object.defineProperty(target, key, {
|
|
653
|
+
value,
|
|
654
|
+
writable: true,
|
|
655
|
+
enumerable: true,
|
|
656
|
+
configurable: true
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const stubs = new Map();
|
|
661
|
+
return new Proxy(target, {
|
|
662
|
+
get (instance, prop) {
|
|
663
|
+
if (typeof prop === 'symbol') return Reflect.get(instance, prop, instance);
|
|
664
|
+
const key = prop;
|
|
665
|
+
if (key === 'then') return undefined;
|
|
666
|
+
// An explicitly supplied prop wins over the auto-stub.
|
|
667
|
+
if (Object.prototype.hasOwnProperty.call(instance, key)) return Reflect.get(instance, prop, instance);
|
|
668
|
+
if (!stubs.has(key)) stubs.set(key, stubCallable());
|
|
669
|
+
return stubs.get(key);
|
|
670
|
+
},
|
|
671
|
+
set (instance, prop, value) {
|
|
672
|
+
Object.defineProperty(instance, prop, {
|
|
673
|
+
value,
|
|
674
|
+
writable: true,
|
|
675
|
+
enumerable: true,
|
|
676
|
+
configurable: true
|
|
677
|
+
});
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
// ---------------------------------------------------------------------------
|
|
585
683
|
// Convenience wrappers for common discord.js classes
|
|
586
684
|
// ---------------------------------------------------------------------------
|
|
587
685
|
/** Creates a mock {@link User}. All methods are auto-stubbed as a mock fn. */ const createMockUser = ()=>createMockInteraction(discord_js.User);
|
|
@@ -793,6 +891,7 @@ exports.MeoCordTestingModule = MeoCordTestingModule;
|
|
|
793
891
|
exports.TestingModule = TestingModule;
|
|
794
892
|
exports.TestingModuleBuilder = TestingModuleBuilder;
|
|
795
893
|
exports.createChatInputOptions = createChatInputOptions;
|
|
894
|
+
exports.createMock = createMock;
|
|
796
895
|
exports.createMockChannel = createMockChannel;
|
|
797
896
|
exports.createMockClient = createMockClient;
|
|
798
897
|
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';
|
|
@@ -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.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22"
|