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 +17 -14
- package/dist/cjs/testing/index.cjs +232 -44
- package/dist/esm/testing/meocord-testing-module.js +1 -1
- package/dist/esm/testing/mock-fn.js +190 -0
- package/dist/esm/testing/mock-interaction.js +43 -43
- package/dist/types/testing/index.d.ts +55 -12
- package/meocord.eslint.cjs +1 -1
- package/meocord.eslint.mjs +1 -1
- package/package.json +16 -16
package/README.md
CHANGED
|
@@ -124,7 +124,7 @@ export class App {}
|
|
|
124
124
|
.
|
|
125
125
|
├── meocord.config.ts
|
|
126
126
|
├── eslint.config.ts
|
|
127
|
-
├──
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 // →
|
|
464
|
+
edited.delete // → a mock fn
|
|
462
465
|
|
|
463
|
-
// still
|
|
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 {
|
|
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:
|
|
498
|
+
let greetingService: { buildGreeting: MockedFunction<GreetingService['buildGreeting']> }
|
|
496
499
|
|
|
497
500
|
beforeEach(() => {
|
|
498
|
-
greetingService = { buildGreeting:
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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' ?
|
|
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
|
|
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
|
|
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
|
|
461
|
+
* reply. These are still mock functions so call assertions work normally.
|
|
274
462
|
*
|
|
275
|
-
* All other methods are auto-stubbed as
|
|
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
|
|
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,
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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',
|
|
672
|
+
stubs.set('delete', createMockFn(async ()=>{
|
|
485
673
|
if (instance.deleted) throw alreadyDeleted();
|
|
486
674
|
instance.deleted = true;
|
|
487
675
|
}));
|
|
488
|
-
stubs.set('edit',
|
|
676
|
+
stubs.set('edit', createMockFn(async ()=>{
|
|
489
677
|
if (instance.deleted) throw alreadyDeleted();
|
|
490
678
|
return createMockMessage();
|
|
491
679
|
}));
|
|
492
|
-
stubs.set('reply',
|
|
680
|
+
stubs.set('reply', createMockFn(async ()=>{
|
|
493
681
|
if (instance.deleted) throw alreadyDeleted();
|
|
494
682
|
return createMockMessage();
|
|
495
683
|
}));
|
|
496
|
-
stubs.set('react',
|
|
684
|
+
stubs.set('react', createMockFn(async ()=>{
|
|
497
685
|
if (instance.deleted) throw alreadyDeleted();
|
|
498
686
|
}));
|
|
499
|
-
stubs.set('pin',
|
|
687
|
+
stubs.set('pin', createMockFn(async ()=>{
|
|
500
688
|
if (instance.deleted) throw alreadyDeleted();
|
|
501
689
|
}));
|
|
502
|
-
stubs.set('unpin',
|
|
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
|
|
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
|
|
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 =
|
|
547
|
-
base.getSubcommand =
|
|
548
|
-
base.getString =
|
|
549
|
-
base.getNumber =
|
|
550
|
-
base.getInteger =
|
|
551
|
-
base.getBoolean =
|
|
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 =
|
|
554
|
-
base.getRole =
|
|
555
|
-
base.getChannel =
|
|
556
|
-
base.getMember =
|
|
557
|
-
base.getMentionable =
|
|
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:
|
|
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 {
|
|
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
|
|
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
|
|
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' ?
|
|
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
|
|
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
|
|
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
|
|
162
|
+
* reply. These are still mock functions so call assertions work normally.
|
|
163
163
|
*
|
|
164
|
-
* All other methods are auto-stubbed as
|
|
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
|
|
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,
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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',
|
|
373
|
+
stubs.set('delete', createMockFn(async ()=>{
|
|
374
374
|
if (instance.deleted) throw alreadyDeleted();
|
|
375
375
|
instance.deleted = true;
|
|
376
376
|
}));
|
|
377
|
-
stubs.set('edit',
|
|
377
|
+
stubs.set('edit', createMockFn(async ()=>{
|
|
378
378
|
if (instance.deleted) throw alreadyDeleted();
|
|
379
379
|
return createMockMessage();
|
|
380
380
|
}));
|
|
381
|
-
stubs.set('reply',
|
|
381
|
+
stubs.set('reply', createMockFn(async ()=>{
|
|
382
382
|
if (instance.deleted) throw alreadyDeleted();
|
|
383
383
|
return createMockMessage();
|
|
384
384
|
}));
|
|
385
|
-
stubs.set('react',
|
|
385
|
+
stubs.set('react', createMockFn(async ()=>{
|
|
386
386
|
if (instance.deleted) throw alreadyDeleted();
|
|
387
387
|
}));
|
|
388
|
-
stubs.set('pin',
|
|
388
|
+
stubs.set('pin', createMockFn(async ()=>{
|
|
389
389
|
if (instance.deleted) throw alreadyDeleted();
|
|
390
390
|
}));
|
|
391
|
-
stubs.set('unpin',
|
|
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
|
|
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
|
|
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 =
|
|
436
|
-
base.getSubcommand =
|
|
437
|
-
base.getString =
|
|
438
|
-
base.getNumber =
|
|
439
|
-
base.getInteger =
|
|
440
|
-
base.getBoolean =
|
|
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 =
|
|
443
|
-
base.getRole =
|
|
444
|
-
base.getChannel =
|
|
445
|
-
base.getMember =
|
|
446
|
-
base.getMentionable =
|
|
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:
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
|
144
|
+
* reply. These are still mock functions so call assertions work normally.
|
|
102
145
|
*
|
|
103
|
-
* All other methods are auto-stubbed as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/meocord.eslint.cjs
CHANGED
|
@@ -83,7 +83,7 @@ const specConfig = {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
module.exports = [
|
|
86
|
-
{ ignores: ['docs/*', 'build/*', 'lib/*', 'dist/*', 'meocord.config.ts', '
|
|
86
|
+
{ ignores: ['docs/*', 'build/*', 'lib/*', 'dist/*', 'meocord.config.ts', 'vitest.config.ts'] },
|
|
87
87
|
...recommendedTypeScriptConfigs,
|
|
88
88
|
specConfig,
|
|
89
89
|
eslintConfigPrettier,
|
package/meocord.eslint.mjs
CHANGED
|
@@ -83,7 +83,7 @@ const specConfig = {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export default [
|
|
86
|
-
{ ignores: ['docs/*', 'build/*', 'lib/*', 'dist/*', 'meocord.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.
|
|
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": "
|
|
11
|
-
"test:watch": "
|
|
12
|
-
"test:coverage": "
|
|
13
|
-
"test:typecheck": "
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
105
|
+
"@typescript-eslint/parser": "^8.62.1",
|
|
108
106
|
"discord.js": "^14.26.4",
|
|
109
|
-
"eslint": "^10.
|
|
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.
|
|
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
|
-
"
|
|
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.
|
|
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",
|