meocord 1.8.3 → 1.8.4-beta.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 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
@@ -360,9 +360,11 @@ const controller = module.get(GreetingSlashController)
360
360
 
361
361
  Creates a smart mock instance of any discord.js class. The full prototype chain is preserved so `instanceof` checks pass at every level.
362
362
 
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.
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 mock functions and can be overridden per test.
364
364
 
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.
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 mock functions so call assertions work normally.
366
+
367
+ > **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.
366
368
 
367
369
  ```typescript
368
370
  import { createMockInteraction } from 'meocord/testing'
@@ -385,7 +387,7 @@ await interaction.reply({ content: 'hi' })
385
387
  interaction.replied // → true
386
388
  await interaction.reply({ content: 'again' }) // → throws (already replied)
387
389
 
388
- // still jest.fn() — call assertions work normally
390
+ // still a mock fn — call assertions work normally
389
391
  expect(interaction.reply).toHaveBeenCalledWith({ content: 'hi' })
390
392
 
391
393
  // direct property writes work normally
@@ -420,13 +422,14 @@ interaction.options.getString('duration') // → null (wrong type)
420
422
  interaction.options.getNumber('x', true) // → throws (absent + required)
421
423
  ```
422
424
 
423
- All methods are `jest.fn()` — override any per test with `.mockReturnValue()`.
425
+ All methods are mock functions — override any per test with `.mockReturnValue()`.
424
426
 
425
427
  ### `createMockUser` / `createMockClient` / `createMockGuild` / `createMockChannel`
426
428
 
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.
429
+ 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
430
 
429
431
  ```typescript
432
+ import { vi } from 'vitest'
430
433
  import { createMockUser, createMockClient, createMockGuild, createMockChannel } from 'meocord/testing'
431
434
  import { TextChannel } from 'discord.js'
432
435
 
@@ -436,14 +439,14 @@ const guild = createMockGuild()
436
439
  const channel = createMockChannel(TextChannel)
437
440
 
438
441
  // override nested manager methods per test
439
- ;(client.users as any).fetch = jest.fn(() => Promise.resolve(user))
442
+ ;(client.users as any).fetch = vi.fn(() => Promise.resolve(user))
440
443
  await (client.users as any).fetch('user-123')
441
444
  expect((client.users as any).fetch).toHaveBeenCalledWith('user-123')
442
445
  ```
443
446
 
444
447
  ### `createMockMessage`
445
448
 
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()`.
449
+ 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
450
 
448
451
  ```typescript
449
452
  import { createMockMessage } from 'meocord/testing'
@@ -458,9 +461,9 @@ await msg.edit({ content: 'x' }) // → throws (already deleted)
458
461
 
459
462
  // edit() and reply() resolve to a new Message mock
460
463
  const edited = await createMockMessage().edit({ content: 'updated' })
461
- edited.delete // → jest.fn()
464
+ edited.delete // → a mock fn
462
465
 
463
- // still jest.fn() — assertions work
466
+ // still a mock fn — assertions work
464
467
  expect(msg.delete).toHaveBeenCalledTimes(1)
465
468
  ```
466
469
 
@@ -483,7 +486,7 @@ const module = MeoCordTestingModule.create({
483
486
  ### Full example
484
487
 
485
488
  ```typescript
486
- import { jest } from '@jest/globals'
489
+ import { vi, type MockedFunction } from 'vitest'
487
490
  import { MeoCordTestingModule, createMockInteraction, createChatInputOptions } from 'meocord/testing'
488
491
  import { ChatInputCommandInteraction } from 'discord.js'
489
492
  import { GreetingSlashController } from '@src/controllers/slash/greeting.slash.controller.js'
@@ -492,10 +495,10 @@ import { RateLimiterGuard } from '@src/guards/rate-limiter.guard.js'
492
495
 
493
496
  describe('GreetingSlashController', () => {
494
497
  let controller: GreetingSlashController
495
- let greetingService: { buildGreeting: jest.MockedFunction<GreetingService['buildGreeting']> }
498
+ let greetingService: { buildGreeting: MockedFunction<GreetingService['buildGreeting']> }
496
499
 
497
500
  beforeEach(() => {
498
- greetingService = { buildGreeting: jest.fn() }
501
+ greetingService = { buildGreeting: vi.fn() }
499
502
 
500
503
  const module = MeoCordTestingModule.create({
501
504
  controllers: [GreetingSlashController],
@@ -508,7 +511,7 @@ describe('GreetingSlashController', () => {
508
511
  })
509
512
 
510
513
  it('replies with a greeting for the provided name', async () => {
511
- jest.mocked(greetingService.buildGreeting).mockResolvedValue('Hello, Alice!')
514
+ greetingService.buildGreeting.mockResolvedValue('Hello, Alice!')
512
515
 
513
516
  const interaction = createMockInteraction(ChatInputCommandInteraction)
514
517
  interaction.options = createChatInputOptions({ name: 'Alice' })
@@ -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) {
@@ -101,7 +100,7 @@ function isValueProvider(p) {
101
100
  * const module = MeoCordTestingModule.create({
102
101
  * controllers: [PingController],
103
102
  * providers: [
104
- * { provide: PingService, useValue: { handlePing: jest.fn().mockResolvedValue('pong') } },
103
+ * { provide: PingService, useValue: { handlePing: vi.fn().mockResolvedValue('pong') } },
105
104
  * ],
106
105
  * }).compile()
107
106
  *
@@ -113,8 +112,197 @@ function isValueProvider(p) {
113
112
  }
114
113
  }
115
114
 
115
+ /**
116
+ * MeoCord Framework
117
+ * Copyright (c) 2025 Ukasyah Rahmatullah Zada
118
+ * SPDX-License-Identifier: MIT
119
+ */ // ---------------------------------------------------------------------------
120
+ // Framework-agnostic mock function
121
+ //
122
+ // A minimal mock-fn implementation used internally by the testing utilities so
123
+ // that `meocord/testing` does not depend on jest OR vitest. Both jest and
124
+ // vitest detect a mock via the `_isMockFunction` flag, and both
125
+ // `expect(fn).toHaveBeenCalledWith(...)` / `toHaveBeenCalledTimes(...)` read
126
+ // `fn.mock.calls` — so a mock produced here is recognized by assertions in
127
+ // either framework.
128
+ // ---------------------------------------------------------------------------
129
+ /**
130
+ * Creates a framework-agnostic mock function. Callable; records calls on
131
+ * `.mock.calls`; supports the mock-return/resolved/rejected/implementation
132
+ * API used by the testing utilities and by user assertions.
133
+ *
134
+ * @example
135
+ * const fn = createMockFn<(x: number) => number>((x) => x + 1)
136
+ * fn(2) // → 3
137
+ * fn.mockReturnValue(99)
138
+ * fn(2) // → 99
139
+ * fn.mock.calls // → [[2], [2]]
140
+ */ function createMockFn(impl) {
141
+ let currentImpl = impl;
142
+ let returnOnce = [];
143
+ let returnValue = {
144
+ set: false,
145
+ value: undefined
146
+ };
147
+ let resolvedOnce = [];
148
+ let resolvedValue = {
149
+ set: false,
150
+ value: undefined
151
+ };
152
+ let rejectedOnce = [];
153
+ let rejectedValue = {
154
+ set: false,
155
+ value: undefined
156
+ };
157
+ const implOnce = [];
158
+ let name = 'vi.fn';
159
+ const calls = [];
160
+ const results = [];
161
+ const instances = [];
162
+ const mockFn = function(...args) {
163
+ calls.push(args);
164
+ instances.push(this);
165
+ let type = 'return';
166
+ let value;
167
+ try {
168
+ if (implOnce.length > 0) {
169
+ value = implOnce.shift().apply(this, args);
170
+ } else if (returnOnce.length > 0) {
171
+ value = returnOnce.shift();
172
+ } else if (returnValue.set) {
173
+ value = returnValue.value;
174
+ } else if (resolvedOnce.length > 0) {
175
+ value = Promise.resolve(resolvedOnce.shift());
176
+ } else if (resolvedValue.set) {
177
+ value = Promise.resolve(resolvedValue.value);
178
+ } else if (rejectedOnce.length > 0) {
179
+ value = Promise.reject(rejectedOnce.shift());
180
+ } else if (rejectedValue.set) {
181
+ value = Promise.reject(rejectedValue.value);
182
+ } else if (currentImpl !== undefined) {
183
+ value = currentImpl.apply(this, args);
184
+ } else {
185
+ value = undefined;
186
+ }
187
+ return value;
188
+ } catch (err) {
189
+ type = 'throw';
190
+ value = err;
191
+ throw err;
192
+ } finally{
193
+ results.push({
194
+ type,
195
+ value
196
+ });
197
+ }
198
+ };
199
+ // Stamp the marker both jest and vitest check via isMockFunction.
200
+ Object.defineProperty(mockFn, '_isMockFunction', {
201
+ value: true,
202
+ enumerable: false
203
+ });
204
+ const mock = {
205
+ get calls () {
206
+ return calls;
207
+ },
208
+ get results () {
209
+ return results;
210
+ },
211
+ get instances () {
212
+ return instances;
213
+ },
214
+ get lastCall () {
215
+ return calls.length > 0 ? calls[calls.length - 1] : undefined;
216
+ }
217
+ };
218
+ Object.defineProperty(mockFn, 'mock', {
219
+ value: mock,
220
+ enumerable: false
221
+ });
222
+ mockFn.mockReturnValue = (v)=>{
223
+ returnValue = {
224
+ set: true,
225
+ value: v
226
+ };
227
+ return mockFn;
228
+ };
229
+ mockFn.mockReturnValueOnce = (v)=>{
230
+ returnOnce.push(v);
231
+ return mockFn;
232
+ };
233
+ mockFn.mockResolvedValue = (v)=>{
234
+ resolvedValue = {
235
+ set: true,
236
+ value: v
237
+ };
238
+ return mockFn;
239
+ };
240
+ mockFn.mockResolvedValueOnce = (v)=>{
241
+ resolvedOnce.push(v);
242
+ return mockFn;
243
+ };
244
+ mockFn.mockRejectedValue = (v)=>{
245
+ rejectedValue = {
246
+ set: true,
247
+ value: v
248
+ };
249
+ return mockFn;
250
+ };
251
+ mockFn.mockRejectedValueOnce = (v)=>{
252
+ rejectedOnce.push(v);
253
+ return mockFn;
254
+ };
255
+ mockFn.mockImplementation = (fn)=>{
256
+ currentImpl = fn;
257
+ return mockFn;
258
+ };
259
+ mockFn.mockImplementationOnce = (fn)=>{
260
+ implOnce.push(fn);
261
+ return mockFn;
262
+ };
263
+ mockFn.mockClear = ()=>{
264
+ calls.length = 0;
265
+ results.length = 0;
266
+ instances.length = 0;
267
+ return mockFn;
268
+ };
269
+ mockFn.mockReset = ()=>{
270
+ calls.length = 0;
271
+ results.length = 0;
272
+ instances.length = 0;
273
+ returnOnce = [];
274
+ returnValue = {
275
+ set: false,
276
+ value: undefined
277
+ };
278
+ resolvedOnce = [];
279
+ resolvedValue = {
280
+ set: false,
281
+ value: undefined
282
+ };
283
+ rejectedOnce = [];
284
+ rejectedValue = {
285
+ set: false,
286
+ value: undefined
287
+ };
288
+ implOnce.length = 0;
289
+ currentImpl = impl;
290
+ return mockFn;
291
+ };
292
+ mockFn.mockRestore = ()=>{
293
+ mockFn.mockReset();
294
+ return mockFn;
295
+ };
296
+ mockFn.getMockName = ()=>name;
297
+ mockFn.mockName = (n)=>{
298
+ name = n;
299
+ return mockFn;
300
+ };
301
+ return mockFn;
302
+ }
303
+
116
304
  // ---------------------------------------------------------------------------
117
- // stubDeep — Proxy that auto-creates jest.fn() on any property access
305
+ // stubDeep — Proxy that auto-creates a mock fn on any property access
118
306
  // ---------------------------------------------------------------------------
119
307
  const SKIP = new Set([
120
308
  'constructor',
@@ -132,7 +320,7 @@ function stubDeep(instance, externalStubs) {
132
320
  return Reflect.get(target, prop, target);
133
321
  }
134
322
  const key = prop;
135
- // 'then' must be undefined — prevents jest treating the mock as a Promise
323
+ // 'then' must be undefined — prevents frameworks treating the mock as a Promise
136
324
  if (key === 'then') return undefined;
137
325
  // Own property writes take precedence (e.g. interaction.guildId = 'abc')
138
326
  if (Object.prototype.hasOwnProperty.call(target, key)) {
@@ -155,7 +343,7 @@ function stubDeep(instance, externalStubs) {
155
343
  }
156
344
  proto = Object.getPrototypeOf(proto);
157
345
  }
158
- const stub = typeof protoValue === 'function' ? globals.jest.fn() : stubDeep({});
346
+ const stub = typeof protoValue === 'function' ? createMockFn() : stubDeep({});
159
347
  stubs.set(key, stub);
160
348
  return stub;
161
349
  },
@@ -225,7 +413,7 @@ const CLASS_TYPE_FIELDS = {
225
413
  }
226
414
  };
227
415
  // 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
416
+ // These are wired as a mock fn wrapping the real prototype logic so they return
229
417
  // correct values by default and can still be overridden per test.
230
418
  const TYPE_GUARD_METHODS = [
231
419
  'isCommand',
@@ -265,14 +453,14 @@ function findPrototypeMethod(instance, name) {
265
453
  *
266
454
  * **Type guards** (`isButton()`, `isRepliable()`, etc.) run the real discord.js
267
455
  * prototype logic — no manual `.mockReturnValue(true)` setup needed. They are
268
- * still `jest.fn()` so you can override them per test.
456
+ * still mock functions so you can override them per test.
269
457
  *
270
458
  * **Reply state machine** — `replied` and `deferred` start as `false`. Calling
271
459
  * `reply()` or `deferReply()` twice throws, just like a real interaction would.
272
460
  * `followUp()`, `editReply()`, and `deleteReply()` throw if called before any
273
- * reply. These are still `jest.fn()` so call assertions work normally.
461
+ * reply. These are still mock functions so call assertions work normally.
274
462
  *
275
- * All other methods are auto-stubbed as `jest.fn()` via Proxy.
463
+ * All other methods are auto-stubbed as a mock fn via Proxy.
276
464
  *
277
465
  * @example
278
466
  * ```ts
@@ -295,12 +483,12 @@ function findPrototypeMethod(instance, name) {
295
483
  instance[key] = value;
296
484
  }
297
485
  }
298
- // Wire each type guard as jest.fn() calling the real prototype implementation.
486
+ // Wire each type guard as a mock fn calling the real prototype implementation.
299
487
  // Correct by default; overridable per test via .mockReturnValue().
300
488
  for (const name of TYPE_GUARD_METHODS){
301
489
  const method = findPrototypeMethod(instance, name);
302
490
  if (method !== null) {
303
- stubs.set(name, globals.jest.fn().mockImplementation(()=>method.call(instance)));
491
+ stubs.set(name, createMockFn().mockImplementation(()=>method.call(instance)));
304
492
  }
305
493
  }
306
494
  // Set up reply state machine for repliable interactions
@@ -320,36 +508,36 @@ function findPrototypeMethod(instance, name) {
320
508
  if (typeof flags === 'bigint') return (flags & 64n) !== 0n;
321
509
  return false;
322
510
  };
323
- stubs.set('reply', globals.jest.fn(async (...args)=>{
511
+ stubs.set('reply', createMockFn(async (...args)=>{
324
512
  if (instance.deferred || instance.replied) throw alreadyReplied();
325
513
  instance.replied = true;
326
514
  if (hasEphemeralFlag(args[0])) instance.ephemeral = true;
327
515
  }));
328
- stubs.set('deferReply', globals.jest.fn(async (...args)=>{
516
+ stubs.set('deferReply', createMockFn(async (...args)=>{
329
517
  if (instance.deferred || instance.replied) throw alreadyReplied();
330
518
  instance.deferred = true;
331
519
  if (hasEphemeralFlag(args[0])) instance.ephemeral = true;
332
520
  }));
333
- stubs.set('followUp', globals.jest.fn(async ()=>{
521
+ stubs.set('followUp', createMockFn(async ()=>{
334
522
  if (!instance.deferred && !instance.replied) throw notYetReplied('followUp');
335
523
  instance.replied = true;
336
524
  return createMockMessage();
337
525
  }));
338
- stubs.set('editReply', globals.jest.fn(async ()=>{
526
+ stubs.set('editReply', createMockFn(async ()=>{
339
527
  if (!instance.deferred && !instance.replied) throw notYetReplied('editReply');
340
528
  instance.replied = true;
341
529
  return createMockMessage();
342
530
  }));
343
- stubs.set('deleteReply', globals.jest.fn(async ()=>{
531
+ stubs.set('deleteReply', createMockFn(async ()=>{
344
532
  if (!instance.deferred && !instance.replied) throw notYetReplied('deleteReply');
345
533
  }));
346
534
  // deferUpdate / update — MessageComponentInteraction only (type === MessageComponent)
347
535
  if (instance.type === discord_js.InteractionType.MessageComponent) {
348
- stubs.set('update', globals.jest.fn(async ()=>{
536
+ stubs.set('update', createMockFn(async ()=>{
349
537
  if (instance.deferred || instance.replied) throw alreadyReplied();
350
538
  instance.replied = true;
351
539
  }));
352
- stubs.set('deferUpdate', globals.jest.fn(async ()=>{
540
+ stubs.set('deferUpdate', createMockFn(async ()=>{
353
541
  if (instance.deferred || instance.replied) throw alreadyReplied();
354
542
  instance.deferred = true;
355
543
  }));
@@ -360,18 +548,18 @@ function findPrototypeMethod(instance, name) {
360
548
  // ---------------------------------------------------------------------------
361
549
  // Convenience wrappers for common discord.js classes
362
550
  // ---------------------------------------------------------------------------
363
- /** Creates a mock {@link User}. All methods are auto-stubbed as `jest.fn()`. */ const createMockUser = ()=>createMockInteraction(discord_js.User);
551
+ /** Creates a mock {@link User}. All methods are auto-stubbed as a mock fn. */ const createMockUser = ()=>createMockInteraction(discord_js.User);
364
552
  /**
365
553
  * Creates a mock {@link Client}.
366
554
  *
367
555
  * 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
556
+ * pre-initialized as a mock fn so they work out of the box without manual
369
557
  * setup: `users.fetch`, `channels.fetch`, `guilds.fetch`, and
370
558
  * `application.commands.fetch`.
371
559
  */ function createMockClient() {
372
560
  const instance = Object.create(discord_js.Client.prototype);
373
561
  // 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().
562
+ // stubs so ALL manager methods (not just fetch) are auto-stubbed as a mock fn.
375
563
  const appInstance = Object.create(null);
376
564
  appInstance.commands = stubDeep(Object.create(discord_js.ApplicationCommandManager.prototype));
377
565
  instance.users = stubDeep(Object.create(discord_js.UserManager.prototype));
@@ -386,7 +574,7 @@ function findPrototypeMethod(instance, name) {
386
574
  *
387
575
  * Manager properties are constructor-assigned in discord.js. This factory
388
576
  * pre-initializes each as a prototype-based stub so all methods are
389
- * auto-stubbed as `jest.fn()`.
577
+ * auto-stubbed as a mock fn.
390
578
  *
391
579
  * Pre-initialized: `members`, `channels`, `roles`, `bans`.
392
580
  */ function createMockGuild() {
@@ -402,7 +590,7 @@ function findPrototypeMethod(instance, name) {
402
590
  *
403
591
  * Manager properties that are constructor-assigned in discord.js are
404
592
  * pre-initialized as prototype-based stubs so all methods are auto-stubbed
405
- * as `jest.fn()`:
593
+ * as a mock fn:
406
594
  * - Guild text channels (`TextChannel`, `NewsChannel`): `messages`, `threads`
407
595
  * - `DMChannel`: `messages`
408
596
  * - `ThreadChannel`: `messages`, `members`
@@ -437,7 +625,7 @@ function findPrototypeMethod(instance, name) {
437
625
  * `msg.channel.send`, `msg.guild.members.fetch`, `msg.thread.fetch`,
438
626
  * and `msg.mentions.has` all work out of the box.
439
627
  *
440
- * All methods remain `jest.fn()` — overridable per test.
628
+ * All methods remain mock functions — overridable per test.
441
629
  *
442
630
  * Note: `createMockInteraction`'s `followUp()` and `editReply()` stubs return
443
631
  * a `createMockMessage()` by default, matching the official return types.
@@ -460,7 +648,7 @@ function createMockMessage() {
460
648
  // Constructor-assigned — set as prototype-based stubs
461
649
  instance.author = stubDeep(Object.create(discord_js.User.prototype));
462
650
  // 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
651
+ // a mock fn, which is wrong. Pre-initialize as own properties to shadow
464
652
  // the prototype getters.
465
653
  Object.defineProperty(instance, 'member', {
466
654
  value: stubDeep(Object.create(discord_js.GuildMember.prototype)),
@@ -481,25 +669,25 @@ function createMockMessage() {
481
669
  // MessageMentions — constructor-assigned, has methods like .has(), .members
482
670
  instance.mentions = stubDeep(Object.create(discord_js.MessageMentions.prototype));
483
671
  const alreadyDeleted = ()=>new Error('This message has already been deleted.');
484
- stubs.set('delete', globals.jest.fn(async ()=>{
672
+ stubs.set('delete', createMockFn(async ()=>{
485
673
  if (instance.deleted) throw alreadyDeleted();
486
674
  instance.deleted = true;
487
675
  }));
488
- stubs.set('edit', globals.jest.fn(async ()=>{
676
+ stubs.set('edit', createMockFn(async ()=>{
489
677
  if (instance.deleted) throw alreadyDeleted();
490
678
  return createMockMessage();
491
679
  }));
492
- stubs.set('reply', globals.jest.fn(async ()=>{
680
+ stubs.set('reply', createMockFn(async ()=>{
493
681
  if (instance.deleted) throw alreadyDeleted();
494
682
  return createMockMessage();
495
683
  }));
496
- stubs.set('react', globals.jest.fn(async ()=>{
684
+ stubs.set('react', createMockFn(async ()=>{
497
685
  if (instance.deleted) throw alreadyDeleted();
498
686
  }));
499
- stubs.set('pin', globals.jest.fn(async ()=>{
687
+ stubs.set('pin', createMockFn(async ()=>{
500
688
  if (instance.deleted) throw alreadyDeleted();
501
689
  }));
502
- stubs.set('unpin', globals.jest.fn(async ()=>{
690
+ stubs.set('unpin', createMockFn(async ()=>{
503
691
  if (instance.deleted) throw alreadyDeleted();
504
692
  }));
505
693
  return stubDeep(instance, stubs);
@@ -509,7 +697,7 @@ function createMockMessage() {
509
697
  * `CommandInteractionOptionResolver` works: declare what options the command
510
698
  * was invoked with, and the resolver finds them by name.
511
699
  *
512
- * All explicit methods are `jest.fn()` — override per test with `.mockReturnValue()`.
700
+ * All explicit methods are mock functions — override per test with `.mockReturnValue()`.
513
701
  * Methods not listed (e.g. `getAttachment`) are auto-stubbed by the Proxy.
514
702
  *
515
703
  * @example
@@ -541,20 +729,20 @@ function createMockMessage() {
541
729
  }
542
730
  const isObjectOption = (v)=>typeof v === 'object' && v !== null && 'id' in v;
543
731
  // Use a real prototype instance so unlisted methods (e.g. getAttachment)
544
- // are found on the prototype chain and auto-stubbed as jest.fn()
732
+ // are found on the prototype chain and auto-stubbed as a mock fn
545
733
  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));
734
+ base.getSubcommandGroup = createMockFn((required)=>resolveSubEntry(subcommandGroup, 'subcommand group', required));
735
+ base.getSubcommand = createMockFn((required)=>resolveSubEntry(subcommand, 'subcommand', required));
736
+ base.getString = createMockFn((name, required)=>resolveOrThrow(name, typeof values[name] === 'string' ? values[name] : null, required));
737
+ base.getNumber = createMockFn((name, required)=>resolveOrThrow(name, typeof values[name] === 'number' ? values[name] : null, required));
738
+ base.getInteger = createMockFn((name, required)=>resolveOrThrow(name, typeof values[name] === 'number' ? values[name] : null, required));
739
+ base.getBoolean = createMockFn((name, required)=>resolveOrThrow(name, typeof values[name] === 'boolean' ? values[name] : null, required));
552
740
  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);
741
+ base.getUser = createMockFn(getObjectOption);
742
+ base.getRole = createMockFn(getObjectOption);
743
+ base.getChannel = createMockFn(getObjectOption);
744
+ base.getMember = createMockFn(getObjectOption);
745
+ base.getMentionable = createMockFn(getObjectOption);
558
746
  return stubDeep(base);
559
747
  }
560
748
 
@@ -97,7 +97,7 @@ function isValueProvider(p) {
97
97
  * const module = MeoCordTestingModule.create({
98
98
  * controllers: [PingController],
99
99
  * providers: [
100
- * { provide: PingService, useValue: { handlePing: jest.fn().mockResolvedValue('pong') } },
100
+ * { provide: PingService, useValue: { handlePing: vi.fn().mockResolvedValue('pong') } },
101
101
  * ],
102
102
  * }).compile()
103
103
  *
@@ -0,0 +1,190 @@
1
+ /**
2
+ * MeoCord Framework
3
+ * Copyright (c) 2025 Ukasyah Rahmatullah Zada
4
+ * SPDX-License-Identifier: MIT
5
+ */ // ---------------------------------------------------------------------------
6
+ // Framework-agnostic mock function
7
+ //
8
+ // A minimal mock-fn implementation used internally by the testing utilities so
9
+ // that `meocord/testing` does not depend on jest OR vitest. Both jest and
10
+ // vitest detect a mock via the `_isMockFunction` flag, and both
11
+ // `expect(fn).toHaveBeenCalledWith(...)` / `toHaveBeenCalledTimes(...)` read
12
+ // `fn.mock.calls` — so a mock produced here is recognized by assertions in
13
+ // either framework.
14
+ // ---------------------------------------------------------------------------
15
+ /**
16
+ * Creates a framework-agnostic mock function. Callable; records calls on
17
+ * `.mock.calls`; supports the mock-return/resolved/rejected/implementation
18
+ * API used by the testing utilities and by user assertions.
19
+ *
20
+ * @example
21
+ * const fn = createMockFn<(x: number) => number>((x) => x + 1)
22
+ * fn(2) // → 3
23
+ * fn.mockReturnValue(99)
24
+ * fn(2) // → 99
25
+ * fn.mock.calls // → [[2], [2]]
26
+ */ function createMockFn(impl) {
27
+ let currentImpl = impl;
28
+ let returnOnce = [];
29
+ let returnValue = {
30
+ set: false,
31
+ value: undefined
32
+ };
33
+ let resolvedOnce = [];
34
+ let resolvedValue = {
35
+ set: false,
36
+ value: undefined
37
+ };
38
+ let rejectedOnce = [];
39
+ let rejectedValue = {
40
+ set: false,
41
+ value: undefined
42
+ };
43
+ const implOnce = [];
44
+ let name = 'vi.fn';
45
+ const calls = [];
46
+ const results = [];
47
+ const instances = [];
48
+ const mockFn = function(...args) {
49
+ calls.push(args);
50
+ instances.push(this);
51
+ let type = 'return';
52
+ let value;
53
+ try {
54
+ if (implOnce.length > 0) {
55
+ value = implOnce.shift().apply(this, args);
56
+ } else if (returnOnce.length > 0) {
57
+ value = returnOnce.shift();
58
+ } else if (returnValue.set) {
59
+ value = returnValue.value;
60
+ } else if (resolvedOnce.length > 0) {
61
+ value = Promise.resolve(resolvedOnce.shift());
62
+ } else if (resolvedValue.set) {
63
+ value = Promise.resolve(resolvedValue.value);
64
+ } else if (rejectedOnce.length > 0) {
65
+ value = Promise.reject(rejectedOnce.shift());
66
+ } else if (rejectedValue.set) {
67
+ value = Promise.reject(rejectedValue.value);
68
+ } else if (currentImpl !== undefined) {
69
+ value = currentImpl.apply(this, args);
70
+ } else {
71
+ value = undefined;
72
+ }
73
+ return value;
74
+ } catch (err) {
75
+ type = 'throw';
76
+ value = err;
77
+ throw err;
78
+ } finally{
79
+ results.push({
80
+ type,
81
+ value
82
+ });
83
+ }
84
+ };
85
+ // Stamp the marker both jest and vitest check via isMockFunction.
86
+ Object.defineProperty(mockFn, '_isMockFunction', {
87
+ value: true,
88
+ enumerable: false
89
+ });
90
+ const mock = {
91
+ get calls () {
92
+ return calls;
93
+ },
94
+ get results () {
95
+ return results;
96
+ },
97
+ get instances () {
98
+ return instances;
99
+ },
100
+ get lastCall () {
101
+ return calls.length > 0 ? calls[calls.length - 1] : undefined;
102
+ }
103
+ };
104
+ Object.defineProperty(mockFn, 'mock', {
105
+ value: mock,
106
+ enumerable: false
107
+ });
108
+ mockFn.mockReturnValue = (v)=>{
109
+ returnValue = {
110
+ set: true,
111
+ value: v
112
+ };
113
+ return mockFn;
114
+ };
115
+ mockFn.mockReturnValueOnce = (v)=>{
116
+ returnOnce.push(v);
117
+ return mockFn;
118
+ };
119
+ mockFn.mockResolvedValue = (v)=>{
120
+ resolvedValue = {
121
+ set: true,
122
+ value: v
123
+ };
124
+ return mockFn;
125
+ };
126
+ mockFn.mockResolvedValueOnce = (v)=>{
127
+ resolvedOnce.push(v);
128
+ return mockFn;
129
+ };
130
+ mockFn.mockRejectedValue = (v)=>{
131
+ rejectedValue = {
132
+ set: true,
133
+ value: v
134
+ };
135
+ return mockFn;
136
+ };
137
+ mockFn.mockRejectedValueOnce = (v)=>{
138
+ rejectedOnce.push(v);
139
+ return mockFn;
140
+ };
141
+ mockFn.mockImplementation = (fn)=>{
142
+ currentImpl = fn;
143
+ return mockFn;
144
+ };
145
+ mockFn.mockImplementationOnce = (fn)=>{
146
+ implOnce.push(fn);
147
+ return mockFn;
148
+ };
149
+ mockFn.mockClear = ()=>{
150
+ calls.length = 0;
151
+ results.length = 0;
152
+ instances.length = 0;
153
+ return mockFn;
154
+ };
155
+ mockFn.mockReset = ()=>{
156
+ calls.length = 0;
157
+ results.length = 0;
158
+ instances.length = 0;
159
+ returnOnce = [];
160
+ returnValue = {
161
+ set: false,
162
+ value: undefined
163
+ };
164
+ resolvedOnce = [];
165
+ resolvedValue = {
166
+ set: false,
167
+ value: undefined
168
+ };
169
+ rejectedOnce = [];
170
+ rejectedValue = {
171
+ set: false,
172
+ value: undefined
173
+ };
174
+ implOnce.length = 0;
175
+ currentImpl = impl;
176
+ return mockFn;
177
+ };
178
+ mockFn.mockRestore = ()=>{
179
+ mockFn.mockReset();
180
+ return mockFn;
181
+ };
182
+ mockFn.getMockName = ()=>name;
183
+ mockFn.mockName = (n)=>{
184
+ name = n;
185
+ return mockFn;
186
+ };
187
+ return mockFn;
188
+ }
189
+
190
+ export { createMockFn };
@@ -1,9 +1,9 @@
1
1
  import 'reflect-metadata';
2
- import { jest } from '@jest/globals';
2
+ import { createMockFn } from './mock-fn.js';
3
3
  import { InteractionType, ComponentType, ApplicationCommandType, CommandInteractionOptionResolver, GuildMessageManager, ThreadManager, DMMessageManager, MessageManager, ThreadMemberManager, Client, ApplicationCommandManager, UserManager, ChannelManager, GuildManager, ClientUser, Guild, GuildMemberManager, GuildChannelManager, RoleManager, GuildBanManager, Message, User, GuildMember, TextChannel, ThreadChannel, MessageMentions } from 'discord.js';
4
4
 
5
5
  // ---------------------------------------------------------------------------
6
- // stubDeep — Proxy that auto-creates jest.fn() on any property access
6
+ // stubDeep — Proxy that auto-creates a mock fn on any property access
7
7
  // ---------------------------------------------------------------------------
8
8
  const SKIP = new Set([
9
9
  'constructor',
@@ -21,7 +21,7 @@ function stubDeep(instance, externalStubs) {
21
21
  return Reflect.get(target, prop, target);
22
22
  }
23
23
  const key = prop;
24
- // 'then' must be undefined — prevents jest treating the mock as a Promise
24
+ // 'then' must be undefined — prevents frameworks treating the mock as a Promise
25
25
  if (key === 'then') return undefined;
26
26
  // Own property writes take precedence (e.g. interaction.guildId = 'abc')
27
27
  if (Object.prototype.hasOwnProperty.call(target, key)) {
@@ -44,7 +44,7 @@ function stubDeep(instance, externalStubs) {
44
44
  }
45
45
  proto = Object.getPrototypeOf(proto);
46
46
  }
47
- const stub = typeof protoValue === 'function' ? jest.fn() : stubDeep({});
47
+ const stub = typeof protoValue === 'function' ? createMockFn() : stubDeep({});
48
48
  stubs.set(key, stub);
49
49
  return stub;
50
50
  },
@@ -114,7 +114,7 @@ const CLASS_TYPE_FIELDS = {
114
114
  }
115
115
  };
116
116
  // All known pure type-guard methods on BaseInteraction and its subclasses.
117
- // These are wired as jest.fn() wrapping the real prototype logic so they return
117
+ // These are wired as a mock fn wrapping the real prototype logic so they return
118
118
  // correct values by default and can still be overridden per test.
119
119
  const TYPE_GUARD_METHODS = [
120
120
  'isCommand',
@@ -154,14 +154,14 @@ function findPrototypeMethod(instance, name) {
154
154
  *
155
155
  * **Type guards** (`isButton()`, `isRepliable()`, etc.) run the real discord.js
156
156
  * prototype logic — no manual `.mockReturnValue(true)` setup needed. They are
157
- * still `jest.fn()` so you can override them per test.
157
+ * still mock functions so you can override them per test.
158
158
  *
159
159
  * **Reply state machine** — `replied` and `deferred` start as `false`. Calling
160
160
  * `reply()` or `deferReply()` twice throws, just like a real interaction would.
161
161
  * `followUp()`, `editReply()`, and `deleteReply()` throw if called before any
162
- * reply. These are still `jest.fn()` so call assertions work normally.
162
+ * reply. These are still mock functions so call assertions work normally.
163
163
  *
164
- * All other methods are auto-stubbed as `jest.fn()` via Proxy.
164
+ * All other methods are auto-stubbed as a mock fn via Proxy.
165
165
  *
166
166
  * @example
167
167
  * ```ts
@@ -184,12 +184,12 @@ function findPrototypeMethod(instance, name) {
184
184
  instance[key] = value;
185
185
  }
186
186
  }
187
- // Wire each type guard as jest.fn() calling the real prototype implementation.
187
+ // Wire each type guard as a mock fn calling the real prototype implementation.
188
188
  // Correct by default; overridable per test via .mockReturnValue().
189
189
  for (const name of TYPE_GUARD_METHODS){
190
190
  const method = findPrototypeMethod(instance, name);
191
191
  if (method !== null) {
192
- stubs.set(name, jest.fn().mockImplementation(()=>method.call(instance)));
192
+ stubs.set(name, createMockFn().mockImplementation(()=>method.call(instance)));
193
193
  }
194
194
  }
195
195
  // Set up reply state machine for repliable interactions
@@ -209,36 +209,36 @@ function findPrototypeMethod(instance, name) {
209
209
  if (typeof flags === 'bigint') return (flags & 64n) !== 0n;
210
210
  return false;
211
211
  };
212
- stubs.set('reply', jest.fn(async (...args)=>{
212
+ stubs.set('reply', createMockFn(async (...args)=>{
213
213
  if (instance.deferred || instance.replied) throw alreadyReplied();
214
214
  instance.replied = true;
215
215
  if (hasEphemeralFlag(args[0])) instance.ephemeral = true;
216
216
  }));
217
- stubs.set('deferReply', jest.fn(async (...args)=>{
217
+ stubs.set('deferReply', createMockFn(async (...args)=>{
218
218
  if (instance.deferred || instance.replied) throw alreadyReplied();
219
219
  instance.deferred = true;
220
220
  if (hasEphemeralFlag(args[0])) instance.ephemeral = true;
221
221
  }));
222
- stubs.set('followUp', jest.fn(async ()=>{
222
+ stubs.set('followUp', createMockFn(async ()=>{
223
223
  if (!instance.deferred && !instance.replied) throw notYetReplied('followUp');
224
224
  instance.replied = true;
225
225
  return createMockMessage();
226
226
  }));
227
- stubs.set('editReply', jest.fn(async ()=>{
227
+ stubs.set('editReply', createMockFn(async ()=>{
228
228
  if (!instance.deferred && !instance.replied) throw notYetReplied('editReply');
229
229
  instance.replied = true;
230
230
  return createMockMessage();
231
231
  }));
232
- stubs.set('deleteReply', jest.fn(async ()=>{
232
+ stubs.set('deleteReply', createMockFn(async ()=>{
233
233
  if (!instance.deferred && !instance.replied) throw notYetReplied('deleteReply');
234
234
  }));
235
235
  // deferUpdate / update — MessageComponentInteraction only (type === MessageComponent)
236
236
  if (instance.type === InteractionType.MessageComponent) {
237
- stubs.set('update', jest.fn(async ()=>{
237
+ stubs.set('update', createMockFn(async ()=>{
238
238
  if (instance.deferred || instance.replied) throw alreadyReplied();
239
239
  instance.replied = true;
240
240
  }));
241
- stubs.set('deferUpdate', jest.fn(async ()=>{
241
+ stubs.set('deferUpdate', createMockFn(async ()=>{
242
242
  if (instance.deferred || instance.replied) throw alreadyReplied();
243
243
  instance.deferred = true;
244
244
  }));
@@ -249,18 +249,18 @@ function findPrototypeMethod(instance, name) {
249
249
  // ---------------------------------------------------------------------------
250
250
  // Convenience wrappers for common discord.js classes
251
251
  // ---------------------------------------------------------------------------
252
- /** Creates a mock {@link User}. All methods are auto-stubbed as `jest.fn()`. */ const createMockUser = ()=>createMockInteraction(User);
252
+ /** Creates a mock {@link User}. All methods are auto-stubbed as a mock fn. */ const createMockUser = ()=>createMockInteraction(User);
253
253
  /**
254
254
  * Creates a mock {@link Client}.
255
255
  *
256
256
  * Manager methods that are constructor-assigned (not on the prototype) are
257
- * pre-initialized as `jest.fn()` so they work out of the box without manual
257
+ * pre-initialized as a mock fn so they work out of the box without manual
258
258
  * setup: `users.fetch`, `channels.fetch`, `guilds.fetch`, and
259
259
  * `application.commands.fetch`.
260
260
  */ function createMockClient() {
261
261
  const instance = Object.create(Client.prototype);
262
262
  // Manager properties are constructor-assigned — pre-initialize as prototype-based
263
- // stubs so ALL manager methods (not just fetch) are auto-stubbed as jest.fn().
263
+ // stubs so ALL manager methods (not just fetch) are auto-stubbed as a mock fn.
264
264
  const appInstance = Object.create(null);
265
265
  appInstance.commands = stubDeep(Object.create(ApplicationCommandManager.prototype));
266
266
  instance.users = stubDeep(Object.create(UserManager.prototype));
@@ -275,7 +275,7 @@ function findPrototypeMethod(instance, name) {
275
275
  *
276
276
  * Manager properties are constructor-assigned in discord.js. This factory
277
277
  * pre-initializes each as a prototype-based stub so all methods are
278
- * auto-stubbed as `jest.fn()`.
278
+ * auto-stubbed as a mock fn.
279
279
  *
280
280
  * Pre-initialized: `members`, `channels`, `roles`, `bans`.
281
281
  */ function createMockGuild() {
@@ -291,7 +291,7 @@ function findPrototypeMethod(instance, name) {
291
291
  *
292
292
  * Manager properties that are constructor-assigned in discord.js are
293
293
  * pre-initialized as prototype-based stubs so all methods are auto-stubbed
294
- * as `jest.fn()`:
294
+ * as a mock fn:
295
295
  * - Guild text channels (`TextChannel`, `NewsChannel`): `messages`, `threads`
296
296
  * - `DMChannel`: `messages`
297
297
  * - `ThreadChannel`: `messages`, `members`
@@ -326,7 +326,7 @@ function findPrototypeMethod(instance, name) {
326
326
  * `msg.channel.send`, `msg.guild.members.fetch`, `msg.thread.fetch`,
327
327
  * and `msg.mentions.has` all work out of the box.
328
328
  *
329
- * All methods remain `jest.fn()` — overridable per test.
329
+ * All methods remain mock functions — overridable per test.
330
330
  *
331
331
  * Note: `createMockInteraction`'s `followUp()` and `editReply()` stubs return
332
332
  * a `createMockMessage()` by default, matching the official return types.
@@ -349,7 +349,7 @@ function createMockMessage() {
349
349
  // Constructor-assigned — set as prototype-based stubs
350
350
  instance.author = stubDeep(Object.create(User.prototype));
351
351
  // Getters on the prototype — the proxy sees them as functions and returns
352
- // jest.fn(), which is wrong. Pre-initialize as own properties to shadow
352
+ // a mock fn, which is wrong. Pre-initialize as own properties to shadow
353
353
  // the prototype getters.
354
354
  Object.defineProperty(instance, 'member', {
355
355
  value: stubDeep(Object.create(GuildMember.prototype)),
@@ -370,25 +370,25 @@ function createMockMessage() {
370
370
  // MessageMentions — constructor-assigned, has methods like .has(), .members
371
371
  instance.mentions = stubDeep(Object.create(MessageMentions.prototype));
372
372
  const alreadyDeleted = ()=>new Error('This message has already been deleted.');
373
- stubs.set('delete', jest.fn(async ()=>{
373
+ stubs.set('delete', createMockFn(async ()=>{
374
374
  if (instance.deleted) throw alreadyDeleted();
375
375
  instance.deleted = true;
376
376
  }));
377
- stubs.set('edit', jest.fn(async ()=>{
377
+ stubs.set('edit', createMockFn(async ()=>{
378
378
  if (instance.deleted) throw alreadyDeleted();
379
379
  return createMockMessage();
380
380
  }));
381
- stubs.set('reply', jest.fn(async ()=>{
381
+ stubs.set('reply', createMockFn(async ()=>{
382
382
  if (instance.deleted) throw alreadyDeleted();
383
383
  return createMockMessage();
384
384
  }));
385
- stubs.set('react', jest.fn(async ()=>{
385
+ stubs.set('react', createMockFn(async ()=>{
386
386
  if (instance.deleted) throw alreadyDeleted();
387
387
  }));
388
- stubs.set('pin', jest.fn(async ()=>{
388
+ stubs.set('pin', createMockFn(async ()=>{
389
389
  if (instance.deleted) throw alreadyDeleted();
390
390
  }));
391
- stubs.set('unpin', jest.fn(async ()=>{
391
+ stubs.set('unpin', createMockFn(async ()=>{
392
392
  if (instance.deleted) throw alreadyDeleted();
393
393
  }));
394
394
  return stubDeep(instance, stubs);
@@ -398,7 +398,7 @@ function createMockMessage() {
398
398
  * `CommandInteractionOptionResolver` works: declare what options the command
399
399
  * was invoked with, and the resolver finds them by name.
400
400
  *
401
- * All explicit methods are `jest.fn()` — override per test with `.mockReturnValue()`.
401
+ * All explicit methods are mock functions — override per test with `.mockReturnValue()`.
402
402
  * Methods not listed (e.g. `getAttachment`) are auto-stubbed by the Proxy.
403
403
  *
404
404
  * @example
@@ -430,20 +430,20 @@ function createMockMessage() {
430
430
  }
431
431
  const isObjectOption = (v)=>typeof v === 'object' && v !== null && 'id' in v;
432
432
  // Use a real prototype instance so unlisted methods (e.g. getAttachment)
433
- // are found on the prototype chain and auto-stubbed as jest.fn()
433
+ // are found on the prototype chain and auto-stubbed as a mock fn
434
434
  const base = Object.create(CommandInteractionOptionResolver.prototype);
435
- base.getSubcommandGroup = jest.fn((required)=>resolveSubEntry(subcommandGroup, 'subcommand group', required));
436
- base.getSubcommand = jest.fn((required)=>resolveSubEntry(subcommand, 'subcommand', required));
437
- base.getString = jest.fn((name, required)=>resolveOrThrow(name, typeof values[name] === 'string' ? values[name] : null, required));
438
- base.getNumber = jest.fn((name, required)=>resolveOrThrow(name, typeof values[name] === 'number' ? values[name] : null, required));
439
- base.getInteger = jest.fn((name, required)=>resolveOrThrow(name, typeof values[name] === 'number' ? values[name] : null, required));
440
- base.getBoolean = jest.fn((name, required)=>resolveOrThrow(name, typeof values[name] === 'boolean' ? values[name] : null, required));
435
+ base.getSubcommandGroup = createMockFn((required)=>resolveSubEntry(subcommandGroup, 'subcommand group', required));
436
+ base.getSubcommand = createMockFn((required)=>resolveSubEntry(subcommand, 'subcommand', required));
437
+ base.getString = createMockFn((name, required)=>resolveOrThrow(name, typeof values[name] === 'string' ? values[name] : null, required));
438
+ base.getNumber = createMockFn((name, required)=>resolveOrThrow(name, typeof values[name] === 'number' ? values[name] : null, required));
439
+ base.getInteger = createMockFn((name, required)=>resolveOrThrow(name, typeof values[name] === 'number' ? values[name] : null, required));
440
+ base.getBoolean = createMockFn((name, required)=>resolveOrThrow(name, typeof values[name] === 'boolean' ? values[name] : null, required));
441
441
  const getObjectOption = (name, required)=>resolveOrThrow(name, isObjectOption(values[name]) ? values[name] : null, required);
442
- base.getUser = jest.fn(getObjectOption);
443
- base.getRole = jest.fn(getObjectOption);
444
- base.getChannel = jest.fn(getObjectOption);
445
- base.getMember = jest.fn(getObjectOption);
446
- base.getMentionable = jest.fn(getObjectOption);
442
+ base.getUser = createMockFn(getObjectOption);
443
+ base.getRole = createMockFn(getObjectOption);
444
+ base.getChannel = createMockFn(getObjectOption);
445
+ base.getMember = createMockFn(getObjectOption);
446
+ base.getMentionable = createMockFn(getObjectOption);
447
447
  return stubDeep(base);
448
448
  }
449
449
 
@@ -1,6 +1,5 @@
1
1
  import { ServiceIdentifier, Container } from 'inversify';
2
2
  import { GuardInterface } from '../interface/index.js';
3
- import { jest } from '@jest/globals';
4
3
  import { CommandInteractionOptionResolver, Channel, Client, Guild, Message, User } from 'discord.js';
5
4
  import 'webpack';
6
5
  import '../controller.enum-QA-IuReF.js';
@@ -57,7 +56,7 @@ declare class TestingModuleBuilder {
57
56
  * const module = MeoCordTestingModule.create({
58
57
  * controllers: [PingController],
59
58
  * providers: [
60
- * { provide: PingService, useValue: { handlePing: jest.fn().mockResolvedValue('pong') } },
59
+ * { provide: PingService, useValue: { handlePing: vi.fn().mockResolvedValue('pong') } },
61
60
  * ],
62
61
  * }).compile()
63
62
  *
@@ -68,6 +67,50 @@ declare class MeoCordTestingModule {
68
67
  static create(options: TestingModuleOptions): TestingModuleBuilder;
69
68
  }
70
69
 
70
+ /**
71
+ * MeoCord Framework
72
+ * Copyright (c) 2025 Ukasyah Rahmatullah Zada
73
+ * SPDX-License-Identifier: MIT
74
+ */
75
+ interface MockResult<T = unknown> {
76
+ type: 'return' | 'throw';
77
+ value: T;
78
+ }
79
+ interface MockState<T extends (...args: any[]) => any = (...args: any[]) => any> {
80
+ /** Arguments from each call, in order. */
81
+ readonly calls: Parameters<T>[];
82
+ /** Return / throw result from each call, in order. */
83
+ readonly results: MockResult<ReturnType<T>>[];
84
+ /** The `this` value recorded for each call. */
85
+ readonly instances: any[];
86
+ /** Arguments from the most recent call, or undefined if never called. */
87
+ readonly lastCall?: Parameters<T>;
88
+ }
89
+ interface MockInstance<T extends (...args: any[]) => any = (...args: any[]) => any> {
90
+ /** Marker both jest and vitest check via `isMockFunction`. Do not remove. */
91
+ readonly _isMockFunction: true;
92
+ readonly mock: MockState<T>;
93
+ mockReturnValue(value: ReturnType<T>): MockedFunction<T>;
94
+ mockReturnValueOnce(value: ReturnType<T>): MockedFunction<T>;
95
+ mockResolvedValue(value: Awaited<ReturnType<T>>): MockedFunction<T>;
96
+ mockResolvedValueOnce(value: Awaited<ReturnType<T>>): MockedFunction<T>;
97
+ mockRejectedValue(value: unknown): MockedFunction<T>;
98
+ mockRejectedValueOnce(value: unknown): MockedFunction<T>;
99
+ mockImplementation(fn: T): MockedFunction<T>;
100
+ mockImplementationOnce(fn: T): MockedFunction<T>;
101
+ mockClear(): MockedFunction<T>;
102
+ mockReset(): MockedFunction<T>;
103
+ mockRestore(): MockedFunction<T>;
104
+ getMockName(): string;
105
+ mockName(name: string): MockedFunction<T>;
106
+ }
107
+ /**
108
+ * A callable that mirrors the signature of `T` and exposes the mock API
109
+ * (`mockReturnValue`, `mockResolvedValue`, `mockImplementation`, `.mock`, …).
110
+ * Drop-in replacement for `jest.MockedFunction<T>` / vitest `MockedFunction<T>`.
111
+ */
112
+ type MockedFunction<T extends (...args: any[]) => any> = ((...args: Parameters<T>) => ReturnType<T>) & MockInstance<T>;
113
+
71
114
  /**
72
115
  * MeoCord Framework
73
116
  * Copyright (c) 2025 Ukasyah Rahmatullah Zada
@@ -75,13 +118,13 @@ declare class MeoCordTestingModule {
75
118
  */
76
119
 
77
120
  /**
78
- * Recursively transforms all methods of T into jest.MockedFunction and all
121
+ * Recursively transforms all methods of T into MockedFunction and all
79
122
  * nested objects into DeepMocked. Depth cap at 5 prevents infinite recursion
80
123
  * on circular discord.js types (e.g. Guild ↔ GuildMember).
81
124
  * -readonly removes readonly modifiers so test setup can write any property.
82
125
  */
83
126
  type DeepMocked<T, Depth extends number[] = []> = Depth['length'] extends 5 ? T : {
84
- -readonly [K in keyof T]: T[K] extends (...args: infer A) => infer R ? jest.MockedFunction<(...args: A) => R> : T[K] extends object ? DeepMocked<T[K], [...Depth, 0]> : T[K];
127
+ -readonly [K in keyof T]: T[K] extends (...args: infer A) => infer R ? MockedFunction<(...args: A) => R> : T[K] extends object ? DeepMocked<T[K], [...Depth, 0]> : T[K];
85
128
  };
86
129
  interface InteractionClass<T> {
87
130
  prototype: T;
@@ -93,14 +136,14 @@ interface InteractionClass<T> {
93
136
  *
94
137
  * **Type guards** (`isButton()`, `isRepliable()`, etc.) run the real discord.js
95
138
  * prototype logic — no manual `.mockReturnValue(true)` setup needed. They are
96
- * still `jest.fn()` so you can override them per test.
139
+ * still mock functions so you can override them per test.
97
140
  *
98
141
  * **Reply state machine** — `replied` and `deferred` start as `false`. Calling
99
142
  * `reply()` or `deferReply()` twice throws, just like a real interaction would.
100
143
  * `followUp()`, `editReply()`, and `deleteReply()` throw if called before any
101
- * reply. These are still `jest.fn()` so call assertions work normally.
144
+ * reply. These are still mock functions so call assertions work normally.
102
145
  *
103
- * All other methods are auto-stubbed as `jest.fn()` via Proxy.
146
+ * All other methods are auto-stubbed as a mock fn via Proxy.
104
147
  *
105
148
  * @example
106
149
  * ```ts
@@ -115,13 +158,13 @@ interface InteractionClass<T> {
115
158
  * ```
116
159
  */
117
160
  declare function createMockInteraction<T extends object>(Class: InteractionClass<T>): DeepMocked<T>;
118
- /** Creates a mock {@link User}. All methods are auto-stubbed as `jest.fn()`. */
161
+ /** Creates a mock {@link User}. All methods are auto-stubbed as a mock fn. */
119
162
  declare const createMockUser: () => DeepMocked<User>;
120
163
  /**
121
164
  * Creates a mock {@link Client}.
122
165
  *
123
166
  * Manager methods that are constructor-assigned (not on the prototype) are
124
- * pre-initialized as `jest.fn()` so they work out of the box without manual
167
+ * pre-initialized as a mock fn so they work out of the box without manual
125
168
  * setup: `users.fetch`, `channels.fetch`, `guilds.fetch`, and
126
169
  * `application.commands.fetch`.
127
170
  */
@@ -131,7 +174,7 @@ declare function createMockClient(): DeepMocked<Client>;
131
174
  *
132
175
  * Manager properties are constructor-assigned in discord.js. This factory
133
176
  * pre-initializes each as a prototype-based stub so all methods are
134
- * auto-stubbed as `jest.fn()`.
177
+ * auto-stubbed as a mock fn.
135
178
  *
136
179
  * Pre-initialized: `members`, `channels`, `roles`, `bans`.
137
180
  */
@@ -141,7 +184,7 @@ declare function createMockGuild(): DeepMocked<Guild>;
141
184
  *
142
185
  * Manager properties that are constructor-assigned in discord.js are
143
186
  * pre-initialized as prototype-based stubs so all methods are auto-stubbed
144
- * as `jest.fn()`:
187
+ * as a mock fn:
145
188
  * - Guild text channels (`TextChannel`, `NewsChannel`): `messages`, `threads`
146
189
  * - `DMChannel`: `messages`
147
190
  * - `ThreadChannel`: `messages`, `members`
@@ -160,7 +203,7 @@ interface ChatInputOptions {
160
203
  * `CommandInteractionOptionResolver` works: declare what options the command
161
204
  * was invoked with, and the resolver finds them by name.
162
205
  *
163
- * All explicit methods are `jest.fn()` — override per test with `.mockReturnValue()`.
206
+ * All explicit methods are mock functions — override per test with `.mockReturnValue()`.
164
207
  * Methods not listed (e.g. `getAttachment`) are auto-stubbed by the Proxy.
165
208
  *
166
209
  * @example
@@ -83,7 +83,7 @@ const specConfig = {
83
83
  }
84
84
 
85
85
  module.exports = [
86
- { ignores: ['docs/*', 'build/*', 'lib/*', 'dist/*', 'meocord.config.ts', 'jest.config.ts'] },
86
+ { ignores: ['docs/*', 'build/*', 'lib/*', 'dist/*', 'meocord.config.ts', 'vitest.config.ts'] },
87
87
  ...recommendedTypeScriptConfigs,
88
88
  specConfig,
89
89
  eslintConfigPrettier,
@@ -83,7 +83,7 @@ const specConfig = {
83
83
  }
84
84
 
85
85
  export default [
86
- { ignores: ['docs/*', 'build/*', 'lib/*', 'dist/*', 'meocord.config.ts', 'jest.config.ts'] },
86
+ { ignores: ['docs/*', 'build/*', 'lib/*', 'dist/*', 'meocord.config.ts', 'vitest.config.ts'] },
87
87
  ...recommendedTypeScriptConfigs,
88
88
  specConfig,
89
89
  eslintConfigPrettier,
package/package.json CHANGED
@@ -1,16 +1,16 @@
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": "1.8.3",
4
+ "version": "1.8.4-beta.1",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "lint": "eslint --fix . && tsc --noEmit && tsc --noEmit --project tsconfig.test.json",
8
8
  "build": "rm -rf ./dist && rollup -c",
9
9
  "prepare": "husky",
10
- "test": "node --experimental-vm-modules node_modules/.bin/jest",
11
- "test:watch": "node --experimental-vm-modules node_modules/.bin/jest --watch",
12
- "test:coverage": "node --experimental-vm-modules node_modules/.bin/jest --coverage",
13
- "test:typecheck": "bunx tsc --noEmit --project tsconfig.test.json"
10
+ "test": "vitest run",
11
+ "test:watch": "vitest",
12
+ "test:coverage": "vitest run --coverage",
13
+ "test:typecheck": "tsc --noEmit --project tsconfig.test.json"
14
14
  },
15
15
  "bin": "./dist/esm/bin/meocord.js",
16
16
  "repository": {
@@ -71,7 +71,7 @@
71
71
  "README.md"
72
72
  ],
73
73
  "dependencies": {
74
- "@clack/prompts": "^1.6.0",
74
+ "@clack/prompts": "^1.7.0",
75
75
  "@swc/core": "1.15.43",
76
76
  "chalk": "^5.6.2",
77
77
  "cli-table3": "^0.6.5",
@@ -87,7 +87,7 @@
87
87
  "swc-loader": "^0.2.7",
88
88
  "terser-webpack-plugin": "^5.6.1",
89
89
  "tsconfig-paths-webpack-plugin": "^4.2.0",
90
- "webpack": "^5.108.0",
90
+ "webpack": "^5.108.3",
91
91
  "webpack-node-externals": "^3.0.0"
92
92
  },
93
93
  "devDependencies": {
@@ -98,31 +98,31 @@
98
98
  "@rollup/plugin-swc": "^0.4.1",
99
99
  "@semantic-release/commit-analyzer": "^13.0.1",
100
100
  "@semantic-release/exec": "^7.1.0",
101
- "@semantic-release/github": "^12.0.8",
101
+ "@semantic-release/github": "^12.0.9",
102
102
  "@semantic-release/release-notes-generator": "^14.1.1",
103
- "@swc/jest": "^0.2.39",
104
- "@types/jest": "30.0.0",
105
103
  "@types/lodash-es": "^4.17.12",
106
104
  "@types/webpack-node-externals": "^3.0.4",
107
- "@typescript-eslint/parser": "^8.62.0",
105
+ "@typescript-eslint/parser": "^8.62.1",
108
106
  "discord.js": "^14.26.4",
109
- "eslint": "^10.5.0",
107
+ "eslint": "^10.6.0",
110
108
  "eslint-config-prettier": "^10.1.8",
111
109
  "eslint-plugin-headers": "^1.3.4",
112
- "eslint-plugin-import-x": "^4.17.0",
110
+ "eslint-plugin-import-x": "^4.17.1",
113
111
  "eslint-plugin-prettier": "^5.5.6",
114
112
  "eslint-plugin-unused-imports": "^4.4.1",
115
113
  "globals": "^17.7.0",
116
114
  "husky": "^9.1.7",
117
- "jest": "30.4.2",
118
- "prettier": "^3.8.4",
115
+ "prettier": "^3.9.4",
119
116
  "rollup": "^4.62.2",
120
117
  "rollup-plugin-copy": "^3.5.0",
121
118
  "rollup-plugin-dts": "^6.4.1",
122
119
  "semantic-release": "^25.0.5",
123
120
  "ts-node": "^10.9.2",
124
121
  "typescript": "^6.0.3",
125
- "typescript-eslint": "^8.62.0"
122
+ "typescript-eslint": "^8.62.1",
123
+ "@vitest/coverage-istanbul": "^3.2.4",
124
+ "unplugin-swc": "^1.5.5",
125
+ "vitest": "^3.2.4"
126
126
  },
127
127
  "peerDependencies": {
128
128
  "discord.js": "^14.26.4",