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.0.0-beta.2",
4
+ "version": "2.1.0",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=22"