meocord 1.8.4-beta.1 → 2.0.0-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 +36 -11
- package/dist/cjs/testing/index.cjs +42 -4
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/index.js +1 -0
- package/dist/esm/testing/meocord-testing-module.js +10 -2
- package/dist/esm/testing/mock-fn.js +7 -1
- package/dist/esm/testing/mock-interaction.js +24 -2
- package/dist/types/decorator/index.d.ts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/testing/index.d.ts +60 -6
- package/package.json +20 -17
package/README.md
CHANGED
|
@@ -337,7 +337,21 @@ async ban(interaction: ChatInputCommandInteraction) { ... }
|
|
|
337
337
|
|
|
338
338
|
## Testing
|
|
339
339
|
|
|
340
|
-
MeoCord ships a `meocord/testing` entry point with utilities for testing controllers in isolation — no real Discord connection required.
|
|
340
|
+
MeoCord ships a `meocord/testing` entry point with utilities for testing controllers in isolation — no real Discord connection required. The framework repo runs tests with [Vitest](https://vitest.dev/); the mocks themselves are **framework-agnostic** and work with Vitest or Jest assertions (see below).
|
|
341
|
+
|
|
342
|
+
### Running tests
|
|
343
|
+
|
|
344
|
+
From the MeoCord repo root:
|
|
345
|
+
|
|
346
|
+
```shell
|
|
347
|
+
bun run test # run once
|
|
348
|
+
bun run test:watch # watch mode
|
|
349
|
+
bun run test:coverage # coverage report
|
|
350
|
+
bun run test:typecheck # tsc -p tsconfig.test.json
|
|
351
|
+
bun run lint # eslint --fix + tsc
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
In your own bot project, add Vitest (or keep Jest) plus a `vitest.config.ts` with SWC if you use decorator metadata — see this repo's config for reference. Generated apps include `*.spec.ts` stubs but not a test runner config yet.
|
|
341
355
|
|
|
342
356
|
### `MeoCordTestingModule`
|
|
343
357
|
|
|
@@ -364,7 +378,7 @@ Creates a smart mock instance of any discord.js class. The full prototype chain
|
|
|
364
378
|
|
|
365
379
|
**Reply state machine** — for repliable interactions, `replied` and `deferred` start as `false`. Calling `reply()` or `deferReply()` twice throws, just like a real interaction. `followUp()`, `editReply()`, and `deleteReply()` throw if called before any reply. The ephemeral flag is tracked on `interaction.ephemeral`. All reply methods are still mock functions so call assertions work normally.
|
|
366
380
|
|
|
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.
|
|
381
|
+
> **Framework-agnostic** — the mocks returned here are plain mock functions that stamp `_isMockFunction` and expose `.mock.calls`, the exact contract both `jest` and `vitest` check. Use them with either framework's `expect(...).toHaveBeenCalledWith(...)` / `toHaveBeenCalledTimes(...)` — no jest or vitest import is required to produce them. For typed stubs in your own code, import `MockedFunction`, `createMockFn`, and `DeepMocked` from `meocord/testing`.
|
|
368
382
|
|
|
369
383
|
```typescript
|
|
370
384
|
import { createMockInteraction } from 'meocord/testing'
|
|
@@ -429,8 +443,13 @@ All methods are mock functions — override any per test with `.mockReturnValue(
|
|
|
429
443
|
Convenience wrappers for common discord.js classes. All methods are auto-stubbed as mock functions. Nested managers (`client.users`, `guild.members`, etc.) are independent nested stubs.
|
|
430
444
|
|
|
431
445
|
```typescript
|
|
432
|
-
import {
|
|
433
|
-
|
|
446
|
+
import {
|
|
447
|
+
createMockFn,
|
|
448
|
+
createMockUser,
|
|
449
|
+
createMockClient,
|
|
450
|
+
createMockGuild,
|
|
451
|
+
createMockChannel,
|
|
452
|
+
} from 'meocord/testing'
|
|
434
453
|
import { TextChannel } from 'discord.js'
|
|
435
454
|
|
|
436
455
|
const user = createMockUser()
|
|
@@ -438,8 +457,8 @@ const client = createMockClient()
|
|
|
438
457
|
const guild = createMockGuild()
|
|
439
458
|
const channel = createMockChannel(TextChannel)
|
|
440
459
|
|
|
441
|
-
// override nested manager methods per test
|
|
442
|
-
;(client.users as any).fetch =
|
|
460
|
+
// override nested manager methods per test (createMockFn works with vitest and jest matchers)
|
|
461
|
+
;(client.users as any).fetch = createMockFn(() => Promise.resolve(user))
|
|
443
462
|
await (client.users as any).fetch('user-123')
|
|
444
463
|
expect((client.users as any).fetch).toHaveBeenCalledWith('user-123')
|
|
445
464
|
```
|
|
@@ -486,8 +505,13 @@ const module = MeoCordTestingModule.create({
|
|
|
486
505
|
### Full example
|
|
487
506
|
|
|
488
507
|
```typescript
|
|
489
|
-
import {
|
|
490
|
-
|
|
508
|
+
import {
|
|
509
|
+
MeoCordTestingModule,
|
|
510
|
+
createMockFn,
|
|
511
|
+
createMockInteraction,
|
|
512
|
+
createChatInputOptions,
|
|
513
|
+
type MockedFunction,
|
|
514
|
+
} from 'meocord/testing'
|
|
491
515
|
import { ChatInputCommandInteraction } from 'discord.js'
|
|
492
516
|
import { GreetingSlashController } from '@src/controllers/slash/greeting.slash.controller.js'
|
|
493
517
|
import { GreetingService } from '@src/services/greeting.service.js'
|
|
@@ -498,7 +522,7 @@ describe('GreetingSlashController', () => {
|
|
|
498
522
|
let greetingService: { buildGreeting: MockedFunction<GreetingService['buildGreeting']> }
|
|
499
523
|
|
|
500
524
|
beforeEach(() => {
|
|
501
|
-
greetingService = { buildGreeting:
|
|
525
|
+
greetingService = { buildGreeting: createMockFn() }
|
|
502
526
|
|
|
503
527
|
const module = MeoCordTestingModule.create({
|
|
504
528
|
controllers: [GreetingSlashController],
|
|
@@ -566,9 +590,10 @@ npx meocord start --prod
|
|
|
566
590
|
1. Fork the repository
|
|
567
591
|
2. Create a feature branch: `git checkout -b feat/your-feature`
|
|
568
592
|
3. Commit with conventional commits: `git commit -m "feat: add X"`
|
|
569
|
-
4.
|
|
593
|
+
4. Run `bun run lint` and `bun run test` before pushing
|
|
594
|
+
5. Push and open a pull request against `main`
|
|
570
595
|
|
|
571
|
-
Include a description of what changed and why, and add tests for any new behaviour.
|
|
596
|
+
Include a description of what changed and why, and add tests for any new behaviour. Use `fix:` / `feat:` / `docs:` prefixes so [semantic-release](https://semantic-release.gitbook.io/) can version correctly — `test:` commits do not trigger a release.
|
|
572
597
|
|
|
573
598
|
---
|
|
574
599
|
|
|
@@ -22,7 +22,13 @@ function isValueProvider(p) {
|
|
|
22
22
|
* Builder returned by `MeoCordTestingModule.create()`.
|
|
23
23
|
* Call `.compile()` to get the resolved `TestingModule`.
|
|
24
24
|
*/ class TestingModuleBuilder {
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* A double covers the methods under test, not the whole class — and a class
|
|
27
|
+
* with any private member (a service holding a logger, say) can never be
|
|
28
|
+
* satisfied by an object literal at all. `Partial<T>` still rejects a
|
|
29
|
+
* misspelled method name, which is the check that actually earns its keep.
|
|
30
|
+
* Matches `overrideGuard`, which has always taken `Partial<GuardInterface>`.
|
|
31
|
+
*/ overrideProvider(token) {
|
|
26
32
|
return {
|
|
27
33
|
useValue: (value)=>{
|
|
28
34
|
this.overrides.set(token, {
|
|
@@ -97,10 +103,12 @@ function isValueProvider(p) {
|
|
|
97
103
|
*
|
|
98
104
|
* @example
|
|
99
105
|
* ```typescript
|
|
106
|
+
* import { MeoCordTestingModule, createMockFn } from 'meocord/testing'
|
|
107
|
+
*
|
|
100
108
|
* const module = MeoCordTestingModule.create({
|
|
101
109
|
* controllers: [PingController],
|
|
102
110
|
* providers: [
|
|
103
|
-
* { provide: PingService, useValue: { handlePing:
|
|
111
|
+
* { provide: PingService, useValue: { handlePing: createMockFn().mockResolvedValue('pong') } },
|
|
104
112
|
* ],
|
|
105
113
|
* }).compile()
|
|
106
114
|
*
|
|
@@ -126,6 +134,12 @@ function isValueProvider(p) {
|
|
|
126
134
|
// `fn.mock.calls` — so a mock produced here is recognized by assertions in
|
|
127
135
|
// either framework.
|
|
128
136
|
// ---------------------------------------------------------------------------
|
|
137
|
+
/**
|
|
138
|
+
* Type guard that recognises mocks produced by `createMockFn` as well as
|
|
139
|
+
* native `jest.fn()` / `vi.fn()` mocks — all stamp `_isMockFunction = true`.
|
|
140
|
+
*/ function isMockFunction(fn) {
|
|
141
|
+
return typeof fn === 'function' && '_isMockFunction' in fn && fn._isMockFunction === true;
|
|
142
|
+
}
|
|
129
143
|
/**
|
|
130
144
|
* Creates a framework-agnostic mock function. Callable; records calls on
|
|
131
145
|
* `.mock.calls`; supports the mock-return/resolved/rejected/implementation
|
|
@@ -347,8 +361,17 @@ function stubDeep(instance, externalStubs) {
|
|
|
347
361
|
stubs.set(key, stub);
|
|
348
362
|
return stub;
|
|
349
363
|
},
|
|
364
|
+
// defineProperty rather than assignment: many discord.js properties are
|
|
365
|
+
// prototype getters with no setter (targetUser, targetMessage, createdAt),
|
|
366
|
+
// and a plain write against one of those is a silent no-op. Defining an own
|
|
367
|
+
// data property shadows the accessor, which is what test setup means.
|
|
350
368
|
set (target, prop, value) {
|
|
351
|
-
|
|
369
|
+
Object.defineProperty(target, prop, {
|
|
370
|
+
value,
|
|
371
|
+
writable: true,
|
|
372
|
+
enumerable: true,
|
|
373
|
+
configurable: true
|
|
374
|
+
});
|
|
352
375
|
return true;
|
|
353
376
|
}
|
|
354
377
|
});
|
|
@@ -473,7 +496,7 @@ function findPrototypeMethod(instance, name) {
|
|
|
473
496
|
* interaction.replied // → true
|
|
474
497
|
* await interaction.reply({}) // throws — already replied
|
|
475
498
|
* ```
|
|
476
|
-
*/ function createMockInteraction(Class) {
|
|
499
|
+
*/ function createMockInteraction(Class, props) {
|
|
477
500
|
const instance = Object.create(Class.prototype);
|
|
478
501
|
const stubs = new Map();
|
|
479
502
|
// Set type fields so all prototype type-guard methods compute the right value
|
|
@@ -543,6 +566,19 @@ function findPrototypeMethod(instance, name) {
|
|
|
543
566
|
}));
|
|
544
567
|
}
|
|
545
568
|
}
|
|
569
|
+
// Applied last so an explicit prop wins over the type fields and the reply
|
|
570
|
+
// state machine. defineProperty rather than assignment for the same reason the
|
|
571
|
+
// Proxy uses it: several of these shadow a getter-only prototype accessor.
|
|
572
|
+
if (props !== undefined) {
|
|
573
|
+
for (const [key, value] of Object.entries(props)){
|
|
574
|
+
Object.defineProperty(instance, key, {
|
|
575
|
+
value,
|
|
576
|
+
writable: true,
|
|
577
|
+
enumerable: true,
|
|
578
|
+
configurable: true
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
546
582
|
return stubDeep(instance, stubs);
|
|
547
583
|
}
|
|
548
584
|
// ---------------------------------------------------------------------------
|
|
@@ -752,7 +788,9 @@ exports.TestingModuleBuilder = TestingModuleBuilder;
|
|
|
752
788
|
exports.createChatInputOptions = createChatInputOptions;
|
|
753
789
|
exports.createMockChannel = createMockChannel;
|
|
754
790
|
exports.createMockClient = createMockClient;
|
|
791
|
+
exports.createMockFn = createMockFn;
|
|
755
792
|
exports.createMockGuild = createMockGuild;
|
|
756
793
|
exports.createMockInteraction = createMockInteraction;
|
|
757
794
|
exports.createMockMessage = createMockMessage;
|
|
758
795
|
exports.createMockUser = createMockUser;
|
|
796
|
+
exports.isMockFunction = isMockFunction;
|
package/dist/esm/package.json.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { MeoCordTestingModule, TestingModule, TestingModuleBuilder } from './meocord-testing-module.js';
|
|
2
2
|
export { createChatInputOptions, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMessage, createMockUser } from './mock-interaction.js';
|
|
3
|
+
export { createMockFn, isMockFunction } from './mock-fn.js';
|
|
@@ -19,7 +19,13 @@ function isValueProvider(p) {
|
|
|
19
19
|
* Builder returned by `MeoCordTestingModule.create()`.
|
|
20
20
|
* Call `.compile()` to get the resolved `TestingModule`.
|
|
21
21
|
*/ class TestingModuleBuilder {
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* A double covers the methods under test, not the whole class — and a class
|
|
24
|
+
* with any private member (a service holding a logger, say) can never be
|
|
25
|
+
* satisfied by an object literal at all. `Partial<T>` still rejects a
|
|
26
|
+
* misspelled method name, which is the check that actually earns its keep.
|
|
27
|
+
* Matches `overrideGuard`, which has always taken `Partial<GuardInterface>`.
|
|
28
|
+
*/ overrideProvider(token) {
|
|
23
29
|
return {
|
|
24
30
|
useValue: (value)=>{
|
|
25
31
|
this.overrides.set(token, {
|
|
@@ -94,10 +100,12 @@ function isValueProvider(p) {
|
|
|
94
100
|
*
|
|
95
101
|
* @example
|
|
96
102
|
* ```typescript
|
|
103
|
+
* import { MeoCordTestingModule, createMockFn } from 'meocord/testing'
|
|
104
|
+
*
|
|
97
105
|
* const module = MeoCordTestingModule.create({
|
|
98
106
|
* controllers: [PingController],
|
|
99
107
|
* providers: [
|
|
100
|
-
* { provide: PingService, useValue: { handlePing:
|
|
108
|
+
* { provide: PingService, useValue: { handlePing: createMockFn().mockResolvedValue('pong') } },
|
|
101
109
|
* ],
|
|
102
110
|
* }).compile()
|
|
103
111
|
*
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
// `fn.mock.calls` — so a mock produced here is recognized by assertions in
|
|
13
13
|
// either framework.
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
|
+
/**
|
|
16
|
+
* Type guard that recognises mocks produced by `createMockFn` as well as
|
|
17
|
+
* native `jest.fn()` / `vi.fn()` mocks — all stamp `_isMockFunction = true`.
|
|
18
|
+
*/ function isMockFunction(fn) {
|
|
19
|
+
return typeof fn === 'function' && '_isMockFunction' in fn && fn._isMockFunction === true;
|
|
20
|
+
}
|
|
15
21
|
/**
|
|
16
22
|
* Creates a framework-agnostic mock function. Callable; records calls on
|
|
17
23
|
* `.mock.calls`; supports the mock-return/resolved/rejected/implementation
|
|
@@ -187,4 +193,4 @@
|
|
|
187
193
|
return mockFn;
|
|
188
194
|
}
|
|
189
195
|
|
|
190
|
-
export { createMockFn };
|
|
196
|
+
export { createMockFn, isMockFunction };
|
|
@@ -48,8 +48,17 @@ function stubDeep(instance, externalStubs) {
|
|
|
48
48
|
stubs.set(key, stub);
|
|
49
49
|
return stub;
|
|
50
50
|
},
|
|
51
|
+
// defineProperty rather than assignment: many discord.js properties are
|
|
52
|
+
// prototype getters with no setter (targetUser, targetMessage, createdAt),
|
|
53
|
+
// and a plain write against one of those is a silent no-op. Defining an own
|
|
54
|
+
// data property shadows the accessor, which is what test setup means.
|
|
51
55
|
set (target, prop, value) {
|
|
52
|
-
|
|
56
|
+
Object.defineProperty(target, prop, {
|
|
57
|
+
value,
|
|
58
|
+
writable: true,
|
|
59
|
+
enumerable: true,
|
|
60
|
+
configurable: true
|
|
61
|
+
});
|
|
53
62
|
return true;
|
|
54
63
|
}
|
|
55
64
|
});
|
|
@@ -174,7 +183,7 @@ function findPrototypeMethod(instance, name) {
|
|
|
174
183
|
* interaction.replied // → true
|
|
175
184
|
* await interaction.reply({}) // throws — already replied
|
|
176
185
|
* ```
|
|
177
|
-
*/ function createMockInteraction(Class) {
|
|
186
|
+
*/ function createMockInteraction(Class, props) {
|
|
178
187
|
const instance = Object.create(Class.prototype);
|
|
179
188
|
const stubs = new Map();
|
|
180
189
|
// Set type fields so all prototype type-guard methods compute the right value
|
|
@@ -244,6 +253,19 @@ function findPrototypeMethod(instance, name) {
|
|
|
244
253
|
}));
|
|
245
254
|
}
|
|
246
255
|
}
|
|
256
|
+
// Applied last so an explicit prop wins over the type fields and the reply
|
|
257
|
+
// state machine. defineProperty rather than assignment for the same reason the
|
|
258
|
+
// Proxy uses it: several of these shadow a getter-only prototype accessor.
|
|
259
|
+
if (props !== undefined) {
|
|
260
|
+
for (const [key, value] of Object.entries(props)){
|
|
261
|
+
Object.defineProperty(instance, key, {
|
|
262
|
+
value,
|
|
263
|
+
writable: true,
|
|
264
|
+
enumerable: true,
|
|
265
|
+
configurable: true
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
247
269
|
return stubDeep(instance, stubs);
|
|
248
270
|
}
|
|
249
271
|
// ---------------------------------------------------------------------------
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CommandType } from '../enum/index.js';
|
|
2
2
|
import { SlashCommandBuilder, SlashCommandSubcommandsOnlyBuilder, ContextMenuCommandBuilder, ButtonInteraction, StringSelectMenuInteraction, ChatInputCommandInteraction, UserContextMenuCommandInteraction, MessageContextMenuCommandInteraction, ModalSubmitInteraction, OmitPartialGroupDMChannel, Message, MessageReaction, PartialMessageReaction, ClientOptions, ActivityOptions } from 'discord.js';
|
|
3
3
|
import { ReactionHandlerOptions, GuardInterface } from '../interface/index.js';
|
|
4
4
|
import { ServiceIdentifier } from 'inversify';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { User, PartialUser, BaseInteraction, Message, MessageReaction } from 'discord.js';
|
|
2
2
|
import { Configuration } from 'webpack';
|
|
3
|
-
import {
|
|
3
|
+
import { ReactionHandlerAction } from '../enum/index.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* MeoCord Framework
|
|
@@ -40,8 +40,15 @@ declare class TestingModuleBuilder {
|
|
|
40
40
|
private readonly overrides;
|
|
41
41
|
private readonly guardOverrides;
|
|
42
42
|
constructor(options: TestingModuleOptions);
|
|
43
|
+
/**
|
|
44
|
+
* A double covers the methods under test, not the whole class — and a class
|
|
45
|
+
* with any private member (a service holding a logger, say) can never be
|
|
46
|
+
* satisfied by an object literal at all. `Partial<T>` still rejects a
|
|
47
|
+
* misspelled method name, which is the check that actually earns its keep.
|
|
48
|
+
* Matches `overrideGuard`, which has always taken `Partial<GuardInterface>`.
|
|
49
|
+
*/
|
|
43
50
|
overrideProvider<T>(token: ServiceIdentifier<T>): {
|
|
44
|
-
useValue: (value: T) => TestingModuleBuilder;
|
|
51
|
+
useValue: (value: Partial<T>) => TestingModuleBuilder;
|
|
45
52
|
};
|
|
46
53
|
overrideGuard(guard: new (...args: any[]) => GuardInterface): {
|
|
47
54
|
useValue: (stub: Partial<GuardInterface>) => TestingModuleBuilder;
|
|
@@ -53,10 +60,12 @@ declare class TestingModuleBuilder {
|
|
|
53
60
|
*
|
|
54
61
|
* @example
|
|
55
62
|
* ```typescript
|
|
63
|
+
* import { MeoCordTestingModule, createMockFn } from 'meocord/testing'
|
|
64
|
+
*
|
|
56
65
|
* const module = MeoCordTestingModule.create({
|
|
57
66
|
* controllers: [PingController],
|
|
58
67
|
* providers: [
|
|
59
|
-
* { provide: PingService, useValue: { handlePing:
|
|
68
|
+
* { provide: PingService, useValue: { handlePing: createMockFn().mockResolvedValue('pong') } },
|
|
60
69
|
* ],
|
|
61
70
|
* }).compile()
|
|
62
71
|
*
|
|
@@ -110,6 +119,26 @@ interface MockInstance<T extends (...args: any[]) => any = (...args: any[]) => a
|
|
|
110
119
|
* Drop-in replacement for `jest.MockedFunction<T>` / vitest `MockedFunction<T>`.
|
|
111
120
|
*/
|
|
112
121
|
type MockedFunction<T extends (...args: any[]) => any> = ((...args: Parameters<T>) => ReturnType<T>) & MockInstance<T>;
|
|
122
|
+
/** Alias kept for parity with `jest.Mock` / vitest `Mock`. */
|
|
123
|
+
type Mock<T extends (...args: any[]) => any = (...args: any[]) => any> = MockedFunction<T>;
|
|
124
|
+
/**
|
|
125
|
+
* Type guard that recognises mocks produced by `createMockFn` as well as
|
|
126
|
+
* native `jest.fn()` / `vi.fn()` mocks — all stamp `_isMockFunction = true`.
|
|
127
|
+
*/
|
|
128
|
+
declare function isMockFunction(fn: unknown): fn is MockInstance;
|
|
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
|
+
*/
|
|
141
|
+
declare function createMockFn<T extends (...args: any[]) => any = (...args: any[]) => any>(impl?: T): MockedFunction<T>;
|
|
113
142
|
|
|
114
143
|
/**
|
|
115
144
|
* MeoCord Framework
|
|
@@ -121,10 +150,35 @@ type MockedFunction<T extends (...args: any[]) => any> = ((...args: Parameters<T
|
|
|
121
150
|
* Recursively transforms all methods of T into MockedFunction and all
|
|
122
151
|
* nested objects into DeepMocked. Depth cap at 5 prevents infinite recursion
|
|
123
152
|
* on circular discord.js types (e.g. Guild ↔ GuildMember).
|
|
124
|
-
*
|
|
153
|
+
*
|
|
154
|
+
* Intersected with `T` so the mock is accepted wherever the real class is
|
|
155
|
+
* expected. A mapped type alone can never be: it iterates `keyof T`, and the
|
|
156
|
+
* private members classes use as brands — discord.js stamps
|
|
157
|
+
* `private readonly _cacheType` on `BaseInteraction` — are not in `keyof T`.
|
|
158
|
+
* Without the intersection every call site has to launder the mock through
|
|
159
|
+
* `as unknown as T`, which defeats the point of typing it at all.
|
|
160
|
+
*
|
|
161
|
+
* The intersection is honest rather than a convenient lie: the object really is
|
|
162
|
+
* `Object.create(Class.prototype)`, so its prototype chain is the real one, and
|
|
163
|
+
* TypeScript `private` is erased at runtime.
|
|
164
|
+
*
|
|
165
|
+
* `readonly` survives from the `T` side, so properties the class declares
|
|
166
|
+
* readonly cannot be assigned after construction. Pass those to the factory as
|
|
167
|
+
* {@link MockProps} instead.
|
|
125
168
|
*/
|
|
126
169
|
type DeepMocked<T, Depth extends number[] = []> = Depth['length'] extends 5 ? T : {
|
|
127
170
|
-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];
|
|
171
|
+
} & T;
|
|
172
|
+
/**
|
|
173
|
+
* Property overrides accepted at construction by the mock factories.
|
|
174
|
+
*
|
|
175
|
+
* Setup belongs here rather than in a post-construction assignment: the returned
|
|
176
|
+
* mock is assignable to the real discord.js class, so anything that class declares
|
|
177
|
+
* `readonly` — `ModalSubmitInteraction#customId`, `#fields`, `MessageComponentInteraction#message`
|
|
178
|
+
* — cannot be written afterwards without a cast.
|
|
179
|
+
*/
|
|
180
|
+
type MockProps<T> = {
|
|
181
|
+
-readonly [K in keyof T]?: T[K] extends (...args: any[]) => any ? (...args: any[]) => any : T[K];
|
|
128
182
|
};
|
|
129
183
|
interface InteractionClass<T> {
|
|
130
184
|
prototype: T;
|
|
@@ -157,7 +211,7 @@ interface InteractionClass<T> {
|
|
|
157
211
|
* await interaction.reply({}) // throws — already replied
|
|
158
212
|
* ```
|
|
159
213
|
*/
|
|
160
|
-
declare function createMockInteraction<T extends object>(Class: InteractionClass<T>): DeepMocked<T>;
|
|
214
|
+
declare function createMockInteraction<T extends object>(Class: InteractionClass<T>, props?: MockProps<T>): DeepMocked<T>;
|
|
161
215
|
/** Creates a mock {@link User}. All methods are auto-stubbed as a mock fn. */
|
|
162
216
|
declare const createMockUser: () => DeepMocked<User>;
|
|
163
217
|
/**
|
|
@@ -220,5 +274,5 @@ interface ChatInputOptions {
|
|
|
220
274
|
*/
|
|
221
275
|
declare function createChatInputOptions(opts?: ChatInputOptions): DeepMocked<CommandInteractionOptionResolver>;
|
|
222
276
|
|
|
223
|
-
export { MeoCordTestingModule, TestingModule, TestingModuleBuilder, createChatInputOptions, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMessage, createMockUser };
|
|
224
|
-
export type { ChatInputOptions, ClassProvider, DeepMocked, Provider, TestingModuleOptions, ValueProvider };
|
|
277
|
+
export { MeoCordTestingModule, TestingModule, TestingModuleBuilder, createChatInputOptions, createMockChannel, createMockClient, createMockFn, createMockGuild, createMockInteraction, createMockMessage, createMockUser, isMockFunction };
|
|
278
|
+
export type { ChatInputOptions, ClassProvider, DeepMocked, Mock, MockInstance, MockProps, MockResult, MockState, MockedFunction, Provider, TestingModuleOptions, ValueProvider };
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
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": "
|
|
4
|
+
"version": "2.0.0-beta.1",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
6
9
|
"scripts": {
|
|
7
10
|
"lint": "eslint --fix . && tsc --noEmit && tsc --noEmit --project tsconfig.test.json",
|
|
8
11
|
"build": "rm -rf ./dist && rollup -c",
|
|
@@ -72,13 +75,13 @@
|
|
|
72
75
|
],
|
|
73
76
|
"dependencies": {
|
|
74
77
|
"@clack/prompts": "^1.7.0",
|
|
75
|
-
"@swc/core": "1.15.
|
|
76
|
-
"chalk": "^
|
|
78
|
+
"@swc/core": "1.15.47",
|
|
79
|
+
"chalk": "^6.0.0",
|
|
77
80
|
"cli-table3": "^0.6.5",
|
|
78
81
|
"commander": "^15.0.0",
|
|
79
82
|
"dayjs": "^1.11.21",
|
|
80
83
|
"dotenv": "^17.4.2",
|
|
81
|
-
"inversify": "^8.
|
|
84
|
+
"inversify": "^8.2.3",
|
|
82
85
|
"jiti": "^2.7.0",
|
|
83
86
|
"lodash-es": "^4.18.1",
|
|
84
87
|
"nodemon": "^3.1.14",
|
|
@@ -87,7 +90,7 @@
|
|
|
87
90
|
"swc-loader": "^0.2.7",
|
|
88
91
|
"terser-webpack-plugin": "^5.6.1",
|
|
89
92
|
"tsconfig-paths-webpack-plugin": "^4.2.0",
|
|
90
|
-
"webpack": "^5.
|
|
93
|
+
"webpack": "^5.109.2",
|
|
91
94
|
"webpack-node-externals": "^3.0.0"
|
|
92
95
|
},
|
|
93
96
|
"devDependencies": {
|
|
@@ -102,27 +105,27 @@
|
|
|
102
105
|
"@semantic-release/release-notes-generator": "^14.1.1",
|
|
103
106
|
"@types/lodash-es": "^4.17.12",
|
|
104
107
|
"@types/webpack-node-externals": "^3.0.4",
|
|
105
|
-
"@typescript-eslint/parser": "^8.
|
|
106
|
-
"discord.js": "^14.
|
|
107
|
-
"eslint": "^10.
|
|
108
|
+
"@typescript-eslint/parser": "^8.67.0",
|
|
109
|
+
"discord.js": "^14.27.0",
|
|
110
|
+
"eslint": "^10.8.1",
|
|
108
111
|
"eslint-config-prettier": "^10.1.8",
|
|
109
112
|
"eslint-plugin-headers": "^1.3.4",
|
|
110
113
|
"eslint-plugin-import-x": "^4.17.1",
|
|
111
114
|
"eslint-plugin-prettier": "^5.5.6",
|
|
112
115
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
113
|
-
"globals": "^17.
|
|
116
|
+
"globals": "^17.11.0",
|
|
114
117
|
"husky": "^9.1.7",
|
|
115
|
-
"prettier": "^3.9.
|
|
116
|
-
"rollup": "^4.62.
|
|
118
|
+
"prettier": "^3.9.6",
|
|
119
|
+
"rollup": "^4.62.4",
|
|
117
120
|
"rollup-plugin-copy": "^3.5.0",
|
|
118
|
-
"rollup-plugin-dts": "^6.
|
|
119
|
-
"semantic-release": "^25.0.
|
|
121
|
+
"rollup-plugin-dts": "^6.5.1",
|
|
122
|
+
"semantic-release": "^25.0.9",
|
|
120
123
|
"ts-node": "^10.9.2",
|
|
121
124
|
"typescript": "^6.0.3",
|
|
122
|
-
"typescript-eslint": "^8.
|
|
123
|
-
"@vitest/coverage-istanbul": "^
|
|
124
|
-
"unplugin-swc": "^1.5.
|
|
125
|
-
"vitest": "^
|
|
125
|
+
"typescript-eslint": "^8.67.0",
|
|
126
|
+
"@vitest/coverage-istanbul": "^4.1.10",
|
|
127
|
+
"unplugin-swc": "^1.5.11",
|
|
128
|
+
"vitest": "^4.1.10"
|
|
126
129
|
},
|
|
127
130
|
"peerDependencies": {
|
|
128
131
|
"discord.js": "^14.26.4",
|