meocord 1.8.3 → 1.8.4

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
@@ -124,7 +124,7 @@ export class App {}
124
124
  .
125
125
  ├── meocord.config.ts
126
126
  ├── eslint.config.ts
127
- ├── jest.config.ts
127
+ ├── vitest.config.ts
128
128
  ├── tsconfig.json
129
129
  ├── tsconfig.eslint.json
130
130
  ├── tsconfig.test.json
@@ -337,7 +337,21 @@ async ban(interaction: ChatInputCommandInteraction) { ... }
337
337
 
338
338
  ## Testing
339
339
 
340
- MeoCord ships a `meocord/testing` entry point with utilities for testing controllers in isolation — no real Discord connection required.
340
+ MeoCord ships a `meocord/testing` entry point with utilities for testing controllers in isolation — no real Discord connection required. The framework repo runs tests with [Vitest](https://vitest.dev/); the mocks themselves are **framework-agnostic** and work with Vitest or Jest assertions (see below).
341
+
342
+ ### Running tests
343
+
344
+ From the MeoCord repo root:
345
+
346
+ ```shell
347
+ bun run test # run once
348
+ bun run test:watch # watch mode
349
+ bun run test:coverage # coverage report
350
+ bun run test:typecheck # tsc -p tsconfig.test.json
351
+ bun run lint # eslint --fix + tsc
352
+ ```
353
+
354
+ In your own bot project, add Vitest (or keep Jest) plus a `vitest.config.ts` with SWC if you use decorator metadata — see this repo's config for reference. Generated apps include `*.spec.ts` stubs but not a test runner config yet.
341
355
 
342
356
  ### `MeoCordTestingModule`
343
357
 
@@ -360,9 +374,11 @@ const controller = module.get(GreetingSlashController)
360
374
 
361
375
  Creates a smart mock instance of any discord.js class. The full prototype chain is preserved so `instanceof` checks pass at every level.
362
376
 
363
- **Type guards run real logic** — `isButton()`, `isRepliable()`, `isChatInputCommand()`, etc. are backed by the actual discord.js prototype methods. The right fields (`type`, `componentType`, `commandType`) are set based on the class you pass in, so no manual `.mockReturnValue(true)` setup is needed. All type guard methods are still `jest.fn()` and can be overridden per test.
377
+ **Type guards run real logic** — `isButton()`, `isRepliable()`, `isChatInputCommand()`, etc. are backed by the actual discord.js prototype methods. The right fields (`type`, `componentType`, `commandType`) are set based on the class you pass in, so no manual `.mockReturnValue(true)` setup is needed. All type guard methods are still mock functions and can be overridden per test.
378
+
379
+ **Reply state machine** — for repliable interactions, `replied` and `deferred` start as `false`. Calling `reply()` or `deferReply()` twice throws, just like a real interaction. `followUp()`, `editReply()`, and `deleteReply()` throw if called before any reply. The ephemeral flag is tracked on `interaction.ephemeral`. All reply methods are still mock functions so call assertions work normally.
364
380
 
365
- **Reply state machine** — for repliable interactions, `replied` and `deferred` start as `false`. Calling `reply()` or `deferReply()` twice throws, just like a real interaction. `followUp()`, `editReply()`, and `deleteReply()` throw if called before any reply. The ephemeral flag is tracked on `interaction.ephemeral`. All reply methods are still `jest.fn()` so call assertions work normally.
381
+ > **Framework-agnostic**the mocks returned here are plain mock functions that stamp `_isMockFunction` and expose `.mock.calls`, the exact contract both `jest` and `vitest` check. Use them with either framework's `expect(...).toHaveBeenCalledWith(...)` / `toHaveBeenCalledTimes(...)` no jest or vitest import is required to produce them. For typed stubs in your own code, import `MockedFunction`, `createMockFn`, and `DeepMocked` from `meocord/testing`.
366
382
 
367
383
  ```typescript
368
384
  import { createMockInteraction } from 'meocord/testing'
@@ -385,7 +401,7 @@ await interaction.reply({ content: 'hi' })
385
401
  interaction.replied // → true
386
402
  await interaction.reply({ content: 'again' }) // → throws (already replied)
387
403
 
388
- // still jest.fn() — call assertions work normally
404
+ // still a mock fn — call assertions work normally
389
405
  expect(interaction.reply).toHaveBeenCalledWith({ content: 'hi' })
390
406
 
391
407
  // direct property writes work normally
@@ -420,14 +436,20 @@ interaction.options.getString('duration') // → null (wrong type)
420
436
  interaction.options.getNumber('x', true) // → throws (absent + required)
421
437
  ```
422
438
 
423
- All methods are `jest.fn()` — override any per test with `.mockReturnValue()`.
439
+ All methods are mock functions — override any per test with `.mockReturnValue()`.
424
440
 
425
441
  ### `createMockUser` / `createMockClient` / `createMockGuild` / `createMockChannel`
426
442
 
427
- Convenience wrappers for common discord.js classes. All methods are auto-stubbed as `jest.fn()`. Nested managers (`client.users`, `guild.members`, etc.) are independent nested stubs.
443
+ Convenience wrappers for common discord.js classes. All methods are auto-stubbed as mock functions. Nested managers (`client.users`, `guild.members`, etc.) are independent nested stubs.
428
444
 
429
445
  ```typescript
430
- import { createMockUser, createMockClient, createMockGuild, createMockChannel } from 'meocord/testing'
446
+ import {
447
+ createMockFn,
448
+ createMockUser,
449
+ createMockClient,
450
+ createMockGuild,
451
+ createMockChannel,
452
+ } from 'meocord/testing'
431
453
  import { TextChannel } from 'discord.js'
432
454
 
433
455
  const user = createMockUser()
@@ -435,15 +457,15 @@ const client = createMockClient()
435
457
  const guild = createMockGuild()
436
458
  const channel = createMockChannel(TextChannel)
437
459
 
438
- // override nested manager methods per test
439
- ;(client.users as any).fetch = jest.fn(() => Promise.resolve(user))
460
+ // override nested manager methods per test (createMockFn works with vitest and jest matchers)
461
+ ;(client.users as any).fetch = createMockFn(() => Promise.resolve(user))
440
462
  await (client.users as any).fetch('user-123')
441
463
  expect((client.users as any).fetch).toHaveBeenCalledWith('user-123')
442
464
  ```
443
465
 
444
466
  ### `createMockMessage`
445
467
 
446
- Creates a smart mock `Message`. Tracks a `deleted` boolean — `delete()`, `edit()`, `reply()`, `react()`, `pin()`, and `unpin()` throw if the message has already been deleted. `edit()` and `reply()` resolve to a new mock `Message` instance. All methods are `jest.fn()`.
468
+ Creates a smart mock `Message`. Tracks a `deleted` boolean — `delete()`, `edit()`, `reply()`, `react()`, `pin()`, and `unpin()` throw if the message has already been deleted. `edit()` and `reply()` resolve to a new mock `Message` instance. All methods are mock functions.
447
469
 
448
470
  ```typescript
449
471
  import { createMockMessage } from 'meocord/testing'
@@ -458,9 +480,9 @@ await msg.edit({ content: 'x' }) // → throws (already deleted)
458
480
 
459
481
  // edit() and reply() resolve to a new Message mock
460
482
  const edited = await createMockMessage().edit({ content: 'updated' })
461
- edited.delete // → jest.fn()
483
+ edited.delete // → a mock fn
462
484
 
463
- // still jest.fn() — assertions work
485
+ // still a mock fn — assertions work
464
486
  expect(msg.delete).toHaveBeenCalledTimes(1)
465
487
  ```
466
488
 
@@ -483,8 +505,13 @@ const module = MeoCordTestingModule.create({
483
505
  ### Full example
484
506
 
485
507
  ```typescript
486
- import { jest } from '@jest/globals'
487
- import { MeoCordTestingModule, createMockInteraction, createChatInputOptions } from 'meocord/testing'
508
+ import {
509
+ MeoCordTestingModule,
510
+ createMockFn,
511
+ createMockInteraction,
512
+ createChatInputOptions,
513
+ type MockedFunction,
514
+ } from 'meocord/testing'
488
515
  import { ChatInputCommandInteraction } from 'discord.js'
489
516
  import { GreetingSlashController } from '@src/controllers/slash/greeting.slash.controller.js'
490
517
  import { GreetingService } from '@src/services/greeting.service.js'
@@ -492,10 +519,10 @@ import { RateLimiterGuard } from '@src/guards/rate-limiter.guard.js'
492
519
 
493
520
  describe('GreetingSlashController', () => {
494
521
  let controller: GreetingSlashController
495
- let greetingService: { buildGreeting: jest.MockedFunction<GreetingService['buildGreeting']> }
522
+ let greetingService: { buildGreeting: MockedFunction<GreetingService['buildGreeting']> }
496
523
 
497
524
  beforeEach(() => {
498
- greetingService = { buildGreeting: jest.fn() }
525
+ greetingService = { buildGreeting: createMockFn() }
499
526
 
500
527
  const module = MeoCordTestingModule.create({
501
528
  controllers: [GreetingSlashController],
@@ -508,7 +535,7 @@ describe('GreetingSlashController', () => {
508
535
  })
509
536
 
510
537
  it('replies with a greeting for the provided name', async () => {
511
- jest.mocked(greetingService.buildGreeting).mockResolvedValue('Hello, Alice!')
538
+ greetingService.buildGreeting.mockResolvedValue('Hello, Alice!')
512
539
 
513
540
  const interaction = createMockInteraction(ChatInputCommandInteraction)
514
541
  interaction.options = createChatInputOptions({ name: 'Alice' })
@@ -563,9 +590,10 @@ npx meocord start --prod
563
590
  1. Fork the repository
564
591
  2. Create a feature branch: `git checkout -b feat/your-feature`
565
592
  3. Commit with conventional commits: `git commit -m "feat: add X"`
566
- 4. Push and open a pull request against `main`
593
+ 4. Run `bun run lint` and `bun run test` before pushing
594
+ 5. Push and open a pull request against `main`
567
595
 
568
- Include a description of what changed and why, and add tests for any new behaviour.
596
+ Include a description of what changed and why, and add tests for any new behaviour. Use `fix:` / `feat:` / `docs:` prefixes so [semantic-release](https://semantic-release.gitbook.io/) can version correctly — `test:` commits do not trigger a release.
569
597
 
570
598
  ---
571
599
 
@@ -3,7 +3,6 @@
3
3
  require('reflect-metadata');
4
4
  var inversify = require('inversify');
5
5
  var metadataKey_enum = require('../_shared/metadata-key.enum-BzzvGUId.cjs');
6
- var globals = require('@jest/globals');
7
6
  var discord_js = require('discord.js');
8
7
 
9
8
  function isValueProvider(p) {
@@ -98,10 +97,12 @@ function isValueProvider(p) {
98
97
  *
99
98
  * @example
100
99
  * ```typescript
100
+ * import { MeoCordTestingModule, createMockFn } from 'meocord/testing'
101
+ *
101
102
  * const module = MeoCordTestingModule.create({
102
103
  * controllers: [PingController],
103
104
  * providers: [
104
- * { provide: PingService, useValue: { handlePing: jest.fn().mockResolvedValue('pong') } },
105
+ * { provide: PingService, useValue: { handlePing: createMockFn().mockResolvedValue('pong') } },
105
106
  * ],
106
107
  * }).compile()
107
108
  *
@@ -113,8 +114,203 @@ function isValueProvider(p) {
113
114
  }
114
115
  }
115
116
 
117
+ /**
118
+ * MeoCord Framework
119
+ * Copyright (c) 2025 Ukasyah Rahmatullah Zada
120
+ * SPDX-License-Identifier: MIT
121
+ */ // ---------------------------------------------------------------------------
122
+ // Framework-agnostic mock function
123
+ //
124
+ // A minimal mock-fn implementation used internally by the testing utilities so
125
+ // that `meocord/testing` does not depend on jest OR vitest. Both jest and
126
+ // vitest detect a mock via the `_isMockFunction` flag, and both
127
+ // `expect(fn).toHaveBeenCalledWith(...)` / `toHaveBeenCalledTimes(...)` read
128
+ // `fn.mock.calls` — so a mock produced here is recognized by assertions in
129
+ // either framework.
130
+ // ---------------------------------------------------------------------------
131
+ /**
132
+ * Type guard that recognises mocks produced by `createMockFn` as well as
133
+ * native `jest.fn()` / `vi.fn()` mocks — all stamp `_isMockFunction = true`.
134
+ */ function isMockFunction(fn) {
135
+ return typeof fn === 'function' && '_isMockFunction' in fn && fn._isMockFunction === true;
136
+ }
137
+ /**
138
+ * Creates a framework-agnostic mock function. Callable; records calls on
139
+ * `.mock.calls`; supports the mock-return/resolved/rejected/implementation
140
+ * API used by the testing utilities and by user assertions.
141
+ *
142
+ * @example
143
+ * const fn = createMockFn<(x: number) => number>((x) => x + 1)
144
+ * fn(2) // → 3
145
+ * fn.mockReturnValue(99)
146
+ * fn(2) // → 99
147
+ * fn.mock.calls // → [[2], [2]]
148
+ */ function createMockFn(impl) {
149
+ let currentImpl = impl;
150
+ let returnOnce = [];
151
+ let returnValue = {
152
+ set: false,
153
+ value: undefined
154
+ };
155
+ let resolvedOnce = [];
156
+ let resolvedValue = {
157
+ set: false,
158
+ value: undefined
159
+ };
160
+ let rejectedOnce = [];
161
+ let rejectedValue = {
162
+ set: false,
163
+ value: undefined
164
+ };
165
+ const implOnce = [];
166
+ let name = 'vi.fn';
167
+ const calls = [];
168
+ const results = [];
169
+ const instances = [];
170
+ const mockFn = function(...args) {
171
+ calls.push(args);
172
+ instances.push(this);
173
+ let type = 'return';
174
+ let value;
175
+ try {
176
+ if (implOnce.length > 0) {
177
+ value = implOnce.shift().apply(this, args);
178
+ } else if (returnOnce.length > 0) {
179
+ value = returnOnce.shift();
180
+ } else if (returnValue.set) {
181
+ value = returnValue.value;
182
+ } else if (resolvedOnce.length > 0) {
183
+ value = Promise.resolve(resolvedOnce.shift());
184
+ } else if (resolvedValue.set) {
185
+ value = Promise.resolve(resolvedValue.value);
186
+ } else if (rejectedOnce.length > 0) {
187
+ value = Promise.reject(rejectedOnce.shift());
188
+ } else if (rejectedValue.set) {
189
+ value = Promise.reject(rejectedValue.value);
190
+ } else if (currentImpl !== undefined) {
191
+ value = currentImpl.apply(this, args);
192
+ } else {
193
+ value = undefined;
194
+ }
195
+ return value;
196
+ } catch (err) {
197
+ type = 'throw';
198
+ value = err;
199
+ throw err;
200
+ } finally{
201
+ results.push({
202
+ type,
203
+ value
204
+ });
205
+ }
206
+ };
207
+ // Stamp the marker both jest and vitest check via isMockFunction.
208
+ Object.defineProperty(mockFn, '_isMockFunction', {
209
+ value: true,
210
+ enumerable: false
211
+ });
212
+ const mock = {
213
+ get calls () {
214
+ return calls;
215
+ },
216
+ get results () {
217
+ return results;
218
+ },
219
+ get instances () {
220
+ return instances;
221
+ },
222
+ get lastCall () {
223
+ return calls.length > 0 ? calls[calls.length - 1] : undefined;
224
+ }
225
+ };
226
+ Object.defineProperty(mockFn, 'mock', {
227
+ value: mock,
228
+ enumerable: false
229
+ });
230
+ mockFn.mockReturnValue = (v)=>{
231
+ returnValue = {
232
+ set: true,
233
+ value: v
234
+ };
235
+ return mockFn;
236
+ };
237
+ mockFn.mockReturnValueOnce = (v)=>{
238
+ returnOnce.push(v);
239
+ return mockFn;
240
+ };
241
+ mockFn.mockResolvedValue = (v)=>{
242
+ resolvedValue = {
243
+ set: true,
244
+ value: v
245
+ };
246
+ return mockFn;
247
+ };
248
+ mockFn.mockResolvedValueOnce = (v)=>{
249
+ resolvedOnce.push(v);
250
+ return mockFn;
251
+ };
252
+ mockFn.mockRejectedValue = (v)=>{
253
+ rejectedValue = {
254
+ set: true,
255
+ value: v
256
+ };
257
+ return mockFn;
258
+ };
259
+ mockFn.mockRejectedValueOnce = (v)=>{
260
+ rejectedOnce.push(v);
261
+ return mockFn;
262
+ };
263
+ mockFn.mockImplementation = (fn)=>{
264
+ currentImpl = fn;
265
+ return mockFn;
266
+ };
267
+ mockFn.mockImplementationOnce = (fn)=>{
268
+ implOnce.push(fn);
269
+ return mockFn;
270
+ };
271
+ mockFn.mockClear = ()=>{
272
+ calls.length = 0;
273
+ results.length = 0;
274
+ instances.length = 0;
275
+ return mockFn;
276
+ };
277
+ mockFn.mockReset = ()=>{
278
+ calls.length = 0;
279
+ results.length = 0;
280
+ instances.length = 0;
281
+ returnOnce = [];
282
+ returnValue = {
283
+ set: false,
284
+ value: undefined
285
+ };
286
+ resolvedOnce = [];
287
+ resolvedValue = {
288
+ set: false,
289
+ value: undefined
290
+ };
291
+ rejectedOnce = [];
292
+ rejectedValue = {
293
+ set: false,
294
+ value: undefined
295
+ };
296
+ implOnce.length = 0;
297
+ currentImpl = impl;
298
+ return mockFn;
299
+ };
300
+ mockFn.mockRestore = ()=>{
301
+ mockFn.mockReset();
302
+ return mockFn;
303
+ };
304
+ mockFn.getMockName = ()=>name;
305
+ mockFn.mockName = (n)=>{
306
+ name = n;
307
+ return mockFn;
308
+ };
309
+ return mockFn;
310
+ }
311
+
116
312
  // ---------------------------------------------------------------------------
117
- // stubDeep — Proxy that auto-creates jest.fn() on any property access
313
+ // stubDeep — Proxy that auto-creates a mock fn on any property access
118
314
  // ---------------------------------------------------------------------------
119
315
  const SKIP = new Set([
120
316
  'constructor',
@@ -132,7 +328,7 @@ function stubDeep(instance, externalStubs) {
132
328
  return Reflect.get(target, prop, target);
133
329
  }
134
330
  const key = prop;
135
- // 'then' must be undefined — prevents jest treating the mock as a Promise
331
+ // 'then' must be undefined — prevents frameworks treating the mock as a Promise
136
332
  if (key === 'then') return undefined;
137
333
  // Own property writes take precedence (e.g. interaction.guildId = 'abc')
138
334
  if (Object.prototype.hasOwnProperty.call(target, key)) {
@@ -155,7 +351,7 @@ function stubDeep(instance, externalStubs) {
155
351
  }
156
352
  proto = Object.getPrototypeOf(proto);
157
353
  }
158
- const stub = typeof protoValue === 'function' ? globals.jest.fn() : stubDeep({});
354
+ const stub = typeof protoValue === 'function' ? createMockFn() : stubDeep({});
159
355
  stubs.set(key, stub);
160
356
  return stub;
161
357
  },
@@ -225,7 +421,7 @@ const CLASS_TYPE_FIELDS = {
225
421
  }
226
422
  };
227
423
  // All known pure type-guard methods on BaseInteraction and its subclasses.
228
- // These are wired as jest.fn() wrapping the real prototype logic so they return
424
+ // These are wired as a mock fn wrapping the real prototype logic so they return
229
425
  // correct values by default and can still be overridden per test.
230
426
  const TYPE_GUARD_METHODS = [
231
427
  'isCommand',
@@ -265,14 +461,14 @@ function findPrototypeMethod(instance, name) {
265
461
  *
266
462
  * **Type guards** (`isButton()`, `isRepliable()`, etc.) run the real discord.js
267
463
  * prototype logic — no manual `.mockReturnValue(true)` setup needed. They are
268
- * still `jest.fn()` so you can override them per test.
464
+ * still mock functions so you can override them per test.
269
465
  *
270
466
  * **Reply state machine** — `replied` and `deferred` start as `false`. Calling
271
467
  * `reply()` or `deferReply()` twice throws, just like a real interaction would.
272
468
  * `followUp()`, `editReply()`, and `deleteReply()` throw if called before any
273
- * reply. These are still `jest.fn()` so call assertions work normally.
469
+ * reply. These are still mock functions so call assertions work normally.
274
470
  *
275
- * All other methods are auto-stubbed as `jest.fn()` via Proxy.
471
+ * All other methods are auto-stubbed as a mock fn via Proxy.
276
472
  *
277
473
  * @example
278
474
  * ```ts
@@ -295,12 +491,12 @@ function findPrototypeMethod(instance, name) {
295
491
  instance[key] = value;
296
492
  }
297
493
  }
298
- // Wire each type guard as jest.fn() calling the real prototype implementation.
494
+ // Wire each type guard as a mock fn calling the real prototype implementation.
299
495
  // Correct by default; overridable per test via .mockReturnValue().
300
496
  for (const name of TYPE_GUARD_METHODS){
301
497
  const method = findPrototypeMethod(instance, name);
302
498
  if (method !== null) {
303
- stubs.set(name, globals.jest.fn().mockImplementation(()=>method.call(instance)));
499
+ stubs.set(name, createMockFn().mockImplementation(()=>method.call(instance)));
304
500
  }
305
501
  }
306
502
  // Set up reply state machine for repliable interactions
@@ -320,36 +516,36 @@ function findPrototypeMethod(instance, name) {
320
516
  if (typeof flags === 'bigint') return (flags & 64n) !== 0n;
321
517
  return false;
322
518
  };
323
- stubs.set('reply', globals.jest.fn(async (...args)=>{
519
+ stubs.set('reply', createMockFn(async (...args)=>{
324
520
  if (instance.deferred || instance.replied) throw alreadyReplied();
325
521
  instance.replied = true;
326
522
  if (hasEphemeralFlag(args[0])) instance.ephemeral = true;
327
523
  }));
328
- stubs.set('deferReply', globals.jest.fn(async (...args)=>{
524
+ stubs.set('deferReply', createMockFn(async (...args)=>{
329
525
  if (instance.deferred || instance.replied) throw alreadyReplied();
330
526
  instance.deferred = true;
331
527
  if (hasEphemeralFlag(args[0])) instance.ephemeral = true;
332
528
  }));
333
- stubs.set('followUp', globals.jest.fn(async ()=>{
529
+ stubs.set('followUp', createMockFn(async ()=>{
334
530
  if (!instance.deferred && !instance.replied) throw notYetReplied('followUp');
335
531
  instance.replied = true;
336
532
  return createMockMessage();
337
533
  }));
338
- stubs.set('editReply', globals.jest.fn(async ()=>{
534
+ stubs.set('editReply', createMockFn(async ()=>{
339
535
  if (!instance.deferred && !instance.replied) throw notYetReplied('editReply');
340
536
  instance.replied = true;
341
537
  return createMockMessage();
342
538
  }));
343
- stubs.set('deleteReply', globals.jest.fn(async ()=>{
539
+ stubs.set('deleteReply', createMockFn(async ()=>{
344
540
  if (!instance.deferred && !instance.replied) throw notYetReplied('deleteReply');
345
541
  }));
346
542
  // deferUpdate / update — MessageComponentInteraction only (type === MessageComponent)
347
543
  if (instance.type === discord_js.InteractionType.MessageComponent) {
348
- stubs.set('update', globals.jest.fn(async ()=>{
544
+ stubs.set('update', createMockFn(async ()=>{
349
545
  if (instance.deferred || instance.replied) throw alreadyReplied();
350
546
  instance.replied = true;
351
547
  }));
352
- stubs.set('deferUpdate', globals.jest.fn(async ()=>{
548
+ stubs.set('deferUpdate', createMockFn(async ()=>{
353
549
  if (instance.deferred || instance.replied) throw alreadyReplied();
354
550
  instance.deferred = true;
355
551
  }));
@@ -360,18 +556,18 @@ function findPrototypeMethod(instance, name) {
360
556
  // ---------------------------------------------------------------------------
361
557
  // Convenience wrappers for common discord.js classes
362
558
  // ---------------------------------------------------------------------------
363
- /** Creates a mock {@link User}. All methods are auto-stubbed as `jest.fn()`. */ const createMockUser = ()=>createMockInteraction(discord_js.User);
559
+ /** Creates a mock {@link User}. All methods are auto-stubbed as a mock fn. */ const createMockUser = ()=>createMockInteraction(discord_js.User);
364
560
  /**
365
561
  * Creates a mock {@link Client}.
366
562
  *
367
563
  * Manager methods that are constructor-assigned (not on the prototype) are
368
- * pre-initialized as `jest.fn()` so they work out of the box without manual
564
+ * pre-initialized as a mock fn so they work out of the box without manual
369
565
  * setup: `users.fetch`, `channels.fetch`, `guilds.fetch`, and
370
566
  * `application.commands.fetch`.
371
567
  */ function createMockClient() {
372
568
  const instance = Object.create(discord_js.Client.prototype);
373
569
  // Manager properties are constructor-assigned — pre-initialize as prototype-based
374
- // stubs so ALL manager methods (not just fetch) are auto-stubbed as jest.fn().
570
+ // stubs so ALL manager methods (not just fetch) are auto-stubbed as a mock fn.
375
571
  const appInstance = Object.create(null);
376
572
  appInstance.commands = stubDeep(Object.create(discord_js.ApplicationCommandManager.prototype));
377
573
  instance.users = stubDeep(Object.create(discord_js.UserManager.prototype));
@@ -386,7 +582,7 @@ function findPrototypeMethod(instance, name) {
386
582
  *
387
583
  * Manager properties are constructor-assigned in discord.js. This factory
388
584
  * pre-initializes each as a prototype-based stub so all methods are
389
- * auto-stubbed as `jest.fn()`.
585
+ * auto-stubbed as a mock fn.
390
586
  *
391
587
  * Pre-initialized: `members`, `channels`, `roles`, `bans`.
392
588
  */ function createMockGuild() {
@@ -402,7 +598,7 @@ function findPrototypeMethod(instance, name) {
402
598
  *
403
599
  * Manager properties that are constructor-assigned in discord.js are
404
600
  * pre-initialized as prototype-based stubs so all methods are auto-stubbed
405
- * as `jest.fn()`:
601
+ * as a mock fn:
406
602
  * - Guild text channels (`TextChannel`, `NewsChannel`): `messages`, `threads`
407
603
  * - `DMChannel`: `messages`
408
604
  * - `ThreadChannel`: `messages`, `members`
@@ -437,7 +633,7 @@ function findPrototypeMethod(instance, name) {
437
633
  * `msg.channel.send`, `msg.guild.members.fetch`, `msg.thread.fetch`,
438
634
  * and `msg.mentions.has` all work out of the box.
439
635
  *
440
- * All methods remain `jest.fn()` — overridable per test.
636
+ * All methods remain mock functions — overridable per test.
441
637
  *
442
638
  * Note: `createMockInteraction`'s `followUp()` and `editReply()` stubs return
443
639
  * a `createMockMessage()` by default, matching the official return types.
@@ -460,7 +656,7 @@ function createMockMessage() {
460
656
  // Constructor-assigned — set as prototype-based stubs
461
657
  instance.author = stubDeep(Object.create(discord_js.User.prototype));
462
658
  // Getters on the prototype — the proxy sees them as functions and returns
463
- // jest.fn(), which is wrong. Pre-initialize as own properties to shadow
659
+ // a mock fn, which is wrong. Pre-initialize as own properties to shadow
464
660
  // the prototype getters.
465
661
  Object.defineProperty(instance, 'member', {
466
662
  value: stubDeep(Object.create(discord_js.GuildMember.prototype)),
@@ -481,25 +677,25 @@ function createMockMessage() {
481
677
  // MessageMentions — constructor-assigned, has methods like .has(), .members
482
678
  instance.mentions = stubDeep(Object.create(discord_js.MessageMentions.prototype));
483
679
  const alreadyDeleted = ()=>new Error('This message has already been deleted.');
484
- stubs.set('delete', globals.jest.fn(async ()=>{
680
+ stubs.set('delete', createMockFn(async ()=>{
485
681
  if (instance.deleted) throw alreadyDeleted();
486
682
  instance.deleted = true;
487
683
  }));
488
- stubs.set('edit', globals.jest.fn(async ()=>{
684
+ stubs.set('edit', createMockFn(async ()=>{
489
685
  if (instance.deleted) throw alreadyDeleted();
490
686
  return createMockMessage();
491
687
  }));
492
- stubs.set('reply', globals.jest.fn(async ()=>{
688
+ stubs.set('reply', createMockFn(async ()=>{
493
689
  if (instance.deleted) throw alreadyDeleted();
494
690
  return createMockMessage();
495
691
  }));
496
- stubs.set('react', globals.jest.fn(async ()=>{
692
+ stubs.set('react', createMockFn(async ()=>{
497
693
  if (instance.deleted) throw alreadyDeleted();
498
694
  }));
499
- stubs.set('pin', globals.jest.fn(async ()=>{
695
+ stubs.set('pin', createMockFn(async ()=>{
500
696
  if (instance.deleted) throw alreadyDeleted();
501
697
  }));
502
- stubs.set('unpin', globals.jest.fn(async ()=>{
698
+ stubs.set('unpin', createMockFn(async ()=>{
503
699
  if (instance.deleted) throw alreadyDeleted();
504
700
  }));
505
701
  return stubDeep(instance, stubs);
@@ -509,7 +705,7 @@ function createMockMessage() {
509
705
  * `CommandInteractionOptionResolver` works: declare what options the command
510
706
  * was invoked with, and the resolver finds them by name.
511
707
  *
512
- * All explicit methods are `jest.fn()` — override per test with `.mockReturnValue()`.
708
+ * All explicit methods are mock functions — override per test with `.mockReturnValue()`.
513
709
  * Methods not listed (e.g. `getAttachment`) are auto-stubbed by the Proxy.
514
710
  *
515
711
  * @example
@@ -541,20 +737,20 @@ function createMockMessage() {
541
737
  }
542
738
  const isObjectOption = (v)=>typeof v === 'object' && v !== null && 'id' in v;
543
739
  // Use a real prototype instance so unlisted methods (e.g. getAttachment)
544
- // are found on the prototype chain and auto-stubbed as jest.fn()
740
+ // are found on the prototype chain and auto-stubbed as a mock fn
545
741
  const base = Object.create(discord_js.CommandInteractionOptionResolver.prototype);
546
- base.getSubcommandGroup = globals.jest.fn((required)=>resolveSubEntry(subcommandGroup, 'subcommand group', required));
547
- base.getSubcommand = globals.jest.fn((required)=>resolveSubEntry(subcommand, 'subcommand', required));
548
- base.getString = globals.jest.fn((name, required)=>resolveOrThrow(name, typeof values[name] === 'string' ? values[name] : null, required));
549
- base.getNumber = globals.jest.fn((name, required)=>resolveOrThrow(name, typeof values[name] === 'number' ? values[name] : null, required));
550
- base.getInteger = globals.jest.fn((name, required)=>resolveOrThrow(name, typeof values[name] === 'number' ? values[name] : null, required));
551
- base.getBoolean = globals.jest.fn((name, required)=>resolveOrThrow(name, typeof values[name] === 'boolean' ? values[name] : null, required));
742
+ base.getSubcommandGroup = createMockFn((required)=>resolveSubEntry(subcommandGroup, 'subcommand group', required));
743
+ base.getSubcommand = createMockFn((required)=>resolveSubEntry(subcommand, 'subcommand', required));
744
+ base.getString = createMockFn((name, required)=>resolveOrThrow(name, typeof values[name] === 'string' ? values[name] : null, required));
745
+ base.getNumber = createMockFn((name, required)=>resolveOrThrow(name, typeof values[name] === 'number' ? values[name] : null, required));
746
+ base.getInteger = createMockFn((name, required)=>resolveOrThrow(name, typeof values[name] === 'number' ? values[name] : null, required));
747
+ base.getBoolean = createMockFn((name, required)=>resolveOrThrow(name, typeof values[name] === 'boolean' ? values[name] : null, required));
552
748
  const getObjectOption = (name, required)=>resolveOrThrow(name, isObjectOption(values[name]) ? values[name] : null, required);
553
- base.getUser = globals.jest.fn(getObjectOption);
554
- base.getRole = globals.jest.fn(getObjectOption);
555
- base.getChannel = globals.jest.fn(getObjectOption);
556
- base.getMember = globals.jest.fn(getObjectOption);
557
- base.getMentionable = globals.jest.fn(getObjectOption);
749
+ base.getUser = createMockFn(getObjectOption);
750
+ base.getRole = createMockFn(getObjectOption);
751
+ base.getChannel = createMockFn(getObjectOption);
752
+ base.getMember = createMockFn(getObjectOption);
753
+ base.getMentionable = createMockFn(getObjectOption);
558
754
  return stubDeep(base);
559
755
  }
560
756
 
@@ -564,7 +760,9 @@ exports.TestingModuleBuilder = TestingModuleBuilder;
564
760
  exports.createChatInputOptions = createChatInputOptions;
565
761
  exports.createMockChannel = createMockChannel;
566
762
  exports.createMockClient = createMockClient;
763
+ exports.createMockFn = createMockFn;
567
764
  exports.createMockGuild = createMockGuild;
568
765
  exports.createMockInteraction = createMockInteraction;
569
766
  exports.createMockMessage = createMockMessage;
570
767
  exports.createMockUser = createMockUser;
768
+ exports.isMockFunction = isMockFunction;
@@ -1,2 +1,3 @@
1
1
  export { MeoCordTestingModule, TestingModule, TestingModuleBuilder } from './meocord-testing-module.js';
2
2
  export { createChatInputOptions, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMessage, createMockUser } from './mock-interaction.js';
3
+ export { createMockFn, isMockFunction } from './mock-fn.js';
@@ -94,10 +94,12 @@ function isValueProvider(p) {
94
94
  *
95
95
  * @example
96
96
  * ```typescript
97
+ * import { MeoCordTestingModule, createMockFn } from 'meocord/testing'
98
+ *
97
99
  * const module = MeoCordTestingModule.create({
98
100
  * controllers: [PingController],
99
101
  * providers: [
100
- * { provide: PingService, useValue: { handlePing: jest.fn().mockResolvedValue('pong') } },
102
+ * { provide: PingService, useValue: { handlePing: createMockFn().mockResolvedValue('pong') } },
101
103
  * ],
102
104
  * }).compile()
103
105
  *