meocord 1.8.4-beta.1 → 1.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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 { vi } from 'vitest'
433
- import { createMockUser, createMockClient, createMockGuild, createMockChannel } from 'meocord/testing'
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 = vi.fn(() => Promise.resolve(user))
460
+ // override nested manager methods per test (createMockFn works with vitest and jest matchers)
461
+ ;(client.users as any).fetch = createMockFn(() => Promise.resolve(user))
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 { vi, type MockedFunction } from 'vitest'
490
- import { MeoCordTestingModule, createMockInteraction, createChatInputOptions } from 'meocord/testing'
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: vi.fn() }
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. Push and open a pull request against `main`
593
+ 4. Run `bun run lint` and `bun run test` before pushing
594
+ 5. Push and open a pull request against `main`
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
 
@@ -97,10 +97,12 @@ function isValueProvider(p) {
97
97
  *
98
98
  * @example
99
99
  * ```typescript
100
+ * import { MeoCordTestingModule, createMockFn } from 'meocord/testing'
101
+ *
100
102
  * const module = MeoCordTestingModule.create({
101
103
  * controllers: [PingController],
102
104
  * providers: [
103
- * { provide: PingService, useValue: { handlePing: vi.fn().mockResolvedValue('pong') } },
105
+ * { provide: PingService, useValue: { handlePing: createMockFn().mockResolvedValue('pong') } },
104
106
  * ],
105
107
  * }).compile()
106
108
  *
@@ -126,6 +128,12 @@ function isValueProvider(p) {
126
128
  // `fn.mock.calls` — so a mock produced here is recognized by assertions in
127
129
  // either framework.
128
130
  // ---------------------------------------------------------------------------
131
+ /**
132
+ * Type guard that recognises mocks produced by `createMockFn` as well as
133
+ * native `jest.fn()` / `vi.fn()` mocks — all stamp `_isMockFunction = true`.
134
+ */ function isMockFunction(fn) {
135
+ return typeof fn === 'function' && '_isMockFunction' in fn && fn._isMockFunction === true;
136
+ }
129
137
  /**
130
138
  * Creates a framework-agnostic mock function. Callable; records calls on
131
139
  * `.mock.calls`; supports the mock-return/resolved/rejected/implementation
@@ -752,7 +760,9 @@ exports.TestingModuleBuilder = TestingModuleBuilder;
752
760
  exports.createChatInputOptions = createChatInputOptions;
753
761
  exports.createMockChannel = createMockChannel;
754
762
  exports.createMockClient = createMockClient;
763
+ exports.createMockFn = createMockFn;
755
764
  exports.createMockGuild = createMockGuild;
756
765
  exports.createMockInteraction = createMockInteraction;
757
766
  exports.createMockMessage = createMockMessage;
758
767
  exports.createMockUser = createMockUser;
768
+ exports.isMockFunction = isMockFunction;
@@ -1,2 +1,3 @@
1
1
  export { MeoCordTestingModule, TestingModule, TestingModuleBuilder } from './meocord-testing-module.js';
2
2
  export { createChatInputOptions, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMessage, createMockUser } from './mock-interaction.js';
3
+ export { createMockFn, isMockFunction } from './mock-fn.js';
@@ -94,10 +94,12 @@ function isValueProvider(p) {
94
94
  *
95
95
  * @example
96
96
  * ```typescript
97
+ * import { MeoCordTestingModule, createMockFn } from 'meocord/testing'
98
+ *
97
99
  * const module = MeoCordTestingModule.create({
98
100
  * controllers: [PingController],
99
101
  * providers: [
100
- * { provide: PingService, useValue: { handlePing: vi.fn().mockResolvedValue('pong') } },
102
+ * { provide: PingService, useValue: { handlePing: createMockFn().mockResolvedValue('pong') } },
101
103
  * ],
102
104
  * }).compile()
103
105
  *
@@ -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 };
@@ -53,10 +53,12 @@ declare class TestingModuleBuilder {
53
53
  *
54
54
  * @example
55
55
  * ```typescript
56
+ * import { MeoCordTestingModule, createMockFn } from 'meocord/testing'
57
+ *
56
58
  * const module = MeoCordTestingModule.create({
57
59
  * controllers: [PingController],
58
60
  * providers: [
59
- * { provide: PingService, useValue: { handlePing: vi.fn().mockResolvedValue('pong') } },
61
+ * { provide: PingService, useValue: { handlePing: createMockFn().mockResolvedValue('pong') } },
60
62
  * ],
61
63
  * }).compile()
62
64
  *
@@ -110,6 +112,26 @@ interface MockInstance<T extends (...args: any[]) => any = (...args: any[]) => a
110
112
  * Drop-in replacement for `jest.MockedFunction<T>` / vitest `MockedFunction<T>`.
111
113
  */
112
114
  type MockedFunction<T extends (...args: any[]) => any> = ((...args: Parameters<T>) => ReturnType<T>) & MockInstance<T>;
115
+ /** Alias kept for parity with `jest.Mock` / vitest `Mock`. */
116
+ type Mock<T extends (...args: any[]) => any = (...args: any[]) => any> = MockedFunction<T>;
117
+ /**
118
+ * Type guard that recognises mocks produced by `createMockFn` as well as
119
+ * native `jest.fn()` / `vi.fn()` mocks — all stamp `_isMockFunction = true`.
120
+ */
121
+ declare function isMockFunction(fn: unknown): fn is MockInstance;
122
+ /**
123
+ * Creates a framework-agnostic mock function. Callable; records calls on
124
+ * `.mock.calls`; supports the mock-return/resolved/rejected/implementation
125
+ * API used by the testing utilities and by user assertions.
126
+ *
127
+ * @example
128
+ * const fn = createMockFn<(x: number) => number>((x) => x + 1)
129
+ * fn(2) // → 3
130
+ * fn.mockReturnValue(99)
131
+ * fn(2) // → 99
132
+ * fn.mock.calls // → [[2], [2]]
133
+ */
134
+ declare function createMockFn<T extends (...args: any[]) => any = (...args: any[]) => any>(impl?: T): MockedFunction<T>;
113
135
 
114
136
  /**
115
137
  * MeoCord Framework
@@ -220,5 +242,5 @@ interface ChatInputOptions {
220
242
  */
221
243
  declare function createChatInputOptions(opts?: ChatInputOptions): DeepMocked<CommandInteractionOptionResolver>;
222
244
 
223
- export { MeoCordTestingModule, TestingModule, TestingModuleBuilder, createChatInputOptions, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMessage, createMockUser };
224
- export type { ChatInputOptions, ClassProvider, DeepMocked, Provider, TestingModuleOptions, ValueProvider };
245
+ export { MeoCordTestingModule, TestingModule, TestingModuleBuilder, createChatInputOptions, createMockChannel, createMockClient, createMockFn, createMockGuild, createMockInteraction, createMockMessage, createMockUser, isMockFunction };
246
+ export type { ChatInputOptions, ClassProvider, DeepMocked, Mock, MockInstance, MockResult, MockState, MockedFunction, Provider, TestingModuleOptions, ValueProvider };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "meocord",
3
3
  "description": "Decorator-based Discord bot framework built on discord.js. Brings NestJS-style controllers, dependency injection, guards, and testing utilities to bot development — with a full CLI and TypeScript-first design.",
4
- "version": "1.8.4-beta.1",
4
+ "version": "1.8.4",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "lint": "eslint --fix . && tsc --noEmit && tsc --noEmit --project tsconfig.test.json",