meocord 4.1.0-beta.0 → 4.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # meocord
2
2
 
3
+ ## 4.1.0-beta.1
4
+
5
+ ### Minor Changes
6
+
7
+ - [#120](https://github.com/meocord/meocord/pull/120) [`9d435d9`](https://github.com/meocord/meocord/commit/9d435d9da2bc62eab012ea2b80df2c1e001a926e) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `createMockMessage()` takes an `id`, `content`, `components`, `embeds` and `flags`, so a test can put controls on the message a button sits on, such as to check what `@Defer` locks; components and embeds may be API JSON, builders or discord.js instances.
8
+
9
+ ### Patch Changes
10
+
11
+ - [#118](https://github.com/meocord/meocord/pull/118) [`5c636aa`](https://github.com/meocord/meocord/commit/5c636aa930224aef9b4670a5e16b234a3f57397d) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Tools that read the installed version with `require('meocord/package.json')` no longer fail with `ERR_PACKAGE_PATH_NOT_EXPORTED`.
12
+
13
+ - [#121](https://github.com/meocord/meocord/pull/121) [`c6990c2`](https://github.com/meocord/meocord/commit/c6990c250f7694ca5adb9963e89f151b7ed5dc4b) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `inGuild()`, `inCachedGuild()` and `inRawGuild()` on a mock answer from its `guildId` and `guild`, so a mock created without a `guildId` is a DM and a guard that requires a guild returns `false`, not `undefined`.
14
+
3
15
  ## 4.1.0-beta.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -1524,6 +1524,8 @@ Creates a smart mock instance of any discord.js class. The full prototype chain
1524
1524
 
1525
1525
  **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.
1526
1526
 
1527
+ **Guild checks read the mock's data** — `inGuild()`, `inCachedGuild()` and `inRawGuild()` answer from the `guildId` and `guild` the mock is given, as discord.js does: a `guildId` means a guild, a `guildId` with a `guild` (such as `createMockGuild()`) a cached one, and a mock created without a `guildId` is a DM, where all three return `false`. A `member` set to `null` or `undefined` makes all three `false` too.
1528
+
1527
1529
  **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`, read from `flags` only — the deprecated `ephemeral: true` reply option is not honoured. All reply methods are still mock functions so call assertions work normally.
1528
1530
 
1529
1531
  Autocomplete interactions are not repliable but get the equivalent for their own single-shot response: `responded` starts as `false`, `respond()` sets it, and a second call throws.
@@ -1547,6 +1549,10 @@ interaction.isChatInputCommand() // → true
1547
1549
  interaction.isRepliable() // → true
1548
1550
  interaction.isButton() // → false
1549
1551
 
1552
+ // created without a guildId, the mock is a DM
1553
+ interaction.inGuild() // → false
1554
+ interaction.inCachedGuild() // → false
1555
+
1550
1556
  // reply state machine
1551
1557
  interaction.replied // → false
1552
1558
  await interaction.reply({ content: 'hi' })
@@ -1691,6 +1697,23 @@ edited.delete // → a mock fn
1691
1697
  expect(msg.delete).toHaveBeenCalledTimes(1)
1692
1698
  ```
1693
1699
 
1700
+ Without arguments the message is empty. Give it an `id`, `content`, `components`, `embeds` and `flags` to test code that reads them, such as a button on a message whose controls `@Defer` locks. Components and embeds may be API JSON, builders or discord.js instances, and `flags` a number, flag names or a `MessageFlagsBitField`:
1701
+
1702
+ ```typescript
1703
+ const message = createMockMessage({
1704
+ components: [
1705
+ new ActionRowBuilder<ButtonBuilder>().addComponents(
1706
+ new ButtonBuilder().setCustomId('card/refresh').setLabel('Refresh').setStyle(ButtonStyle.Primary),
1707
+ ),
1708
+ ],
1709
+ embeds: [{ title: 'Card' }],
1710
+ flags: MessageFlags.Ephemeral,
1711
+ })
1712
+ const interaction = createMockInteraction(ButtonInteraction, { customId: 'card/refresh', message })
1713
+ ```
1714
+
1715
+ API JSON and builders are kept as their JSON behind `toJSON()`, which is what `respond()` and `@Defer` read; discord.js instances are kept as they are.
1716
+
1694
1717
  </details>
1695
1718
 
1696
1719
  <details>
@@ -634,7 +634,9 @@ function findPrototypeMethod(instance, name) {
634
634
  /**
635
635
  * Creates a mock instance of a discord.js class, keeping its prototype so `instanceof` holds.
636
636
  *
637
- * Type guards such as `isButton()` run the real discord.js logic. Replies behave like a real
637
+ * Type guards such as `isButton()` run the real discord.js logic. `inGuild()`, `inCachedGuild()`
638
+ * and `inRawGuild()` answer from the mock's own `guildId` and `guild`, so a mock created without
639
+ * a `guildId` is a DM. Replies behave like a real
638
640
  * interaction: `reply()` or `deferReply()` twice throws, and `followUp()`, `editReply()` and
639
641
  * `deleteReply()` throw before a reply. Every method is a mock function you can override.
640
642
  *
@@ -667,6 +669,19 @@ function findPrototypeMethod(instance, name) {
667
669
  stubs.set(name, createMockFn().mockImplementation(()=>method.call(instance)));
668
670
  }
669
671
  }
672
+ // Guild checks read the mock's own data, since discord.js resolves `guild` through a client the
673
+ // mock lacks: a guildId is a guild, a guild object with it a cached one, neither a DM. A member
674
+ // not given is the auto-stub, so only one set to null or undefined fails the check.
675
+ const own = (key)=>Object.prototype.hasOwnProperty.call(instance, key) ? instance[key] : undefined;
676
+ const hasMember = ()=>!Object.prototype.hasOwnProperty.call(instance, 'member') || Boolean(instance.member);
677
+ const guildChecks = {
678
+ inGuild: ()=>Boolean(own('guildId') && hasMember()),
679
+ inCachedGuild: ()=>Boolean(own('guildId') && own('guild') && hasMember()),
680
+ inRawGuild: ()=>Boolean(own('guildId') && !own('guild') && hasMember())
681
+ };
682
+ for (const [name, check] of Object.entries(guildChecks)){
683
+ if (findPrototypeMethod(instance, name) !== null) stubs.set(name, createMockFn().mockImplementation(check));
684
+ }
670
685
  // Set up reply state machine for repliable interactions
671
686
  const isRepliableMethod = findPrototypeMethod(instance, 'isRepliable');
672
687
  const repliable = isRepliableMethod !== null && isRepliableMethod.call(instance);
@@ -900,6 +915,16 @@ function findPrototypeMethod(instance, name) {
900
915
  guild.bans = stubDeep(Object.create(discord_js.GuildBanManager.prototype));
901
916
  return stubDeep(guild);
902
917
  }
918
+ /**
919
+ * A component or embed as a message holds it: a discord.js instance as it is; anything else as
920
+ * its API JSON at the time of the call, behind `toJSON()`.
921
+ */ function asHeld(value) {
922
+ if (value instanceof discord_js.Component || value instanceof discord_js.Embed) return value;
923
+ const json = structuredClone(typeof value.toJSON === 'function' ? value.toJSON() : value);
924
+ return {
925
+ toJSON: ()=>json
926
+ };
927
+ }
903
928
  /**
904
929
  * Creates a mock {@link Message} that tracks whether it has been deleted.
905
930
  *
@@ -908,13 +933,30 @@ function findPrototypeMethod(instance, name) {
908
933
  * `msg.author.send` and `msg.guild.members.fetch` are ready to use, and every method is a mock
909
934
  * function you can override per test.
910
935
  *
936
+ * Without overrides the message is empty: no flags, components, embeds or attachments. Components
937
+ * and embeds given as API JSON or builders keep that JSON behind `toJSON()`, which is what
938
+ * `respond()` and `@Defer` read, such as when `@Defer` locks the controls of the message a button
939
+ * sits on; discord.js instances are kept as they are.
940
+ *
941
+ * @param overrides - The message's id, content, components, embeds and flags.
942
+ * @returns The mock message.
943
+ *
911
944
  * @example
912
945
  * ```ts
913
- * const message = createMockMessage()
946
+ * const message = createMockMessage({
947
+ * components: [
948
+ * new ActionRowBuilder<ButtonBuilder>().addComponents(
949
+ * new ButtonBuilder().setCustomId('card/refresh').setLabel('Refresh').setStyle(ButtonStyle.Primary),
950
+ * ),
951
+ * ],
952
+ * embeds: [{ title: 'Card' }],
953
+ * })
954
+ * const interaction = createMockInteraction(ButtonInteraction, { customId: 'card/refresh', message })
955
+ *
914
956
  * await message.delete()
915
957
  * message.deleted // true
916
958
  * ```
917
- */ function createMockMessage() {
959
+ */ function createMockMessage(overrides = {}) {
918
960
  const instance = Object.create(discord_js.Message.prototype);
919
961
  const stubs = new Map();
920
962
  instance.deleted = false;
@@ -942,10 +984,12 @@ function findPrototypeMethod(instance, name) {
942
984
  // MessageMentions — constructor-assigned, has methods like .has(), .members
943
985
  instance.mentions = stubDeep(Object.create(discord_js.MessageMentions.prototype));
944
986
  // Data a message always has, real rather than stubbed, so code reading it sees an empty message
945
- instance.flags = new discord_js.MessageFlagsBitField();
946
- instance.components = [];
947
- instance.embeds = [];
987
+ instance.flags = new discord_js.MessageFlagsBitField(overrides.flags);
988
+ instance.components = (overrides.components ?? []).map(asHeld);
989
+ instance.embeds = (overrides.embeds ?? []).map(asHeld);
948
990
  instance.attachments = new discord_js.Collection();
991
+ if (overrides.id !== undefined) instance.id = overrides.id;
992
+ if (overrides.content !== undefined) instance.content = overrides.content;
949
993
  const alreadyDeleted = ()=>new Error('This message has already been deleted.');
950
994
  stubs.set('delete', createMockFn(async ()=>{
951
995
  if (instance.deleted) throw alreadyDeleted();
@@ -1,4 +1,4 @@
1
- var version = "4.1.0-beta.0";
1
+ var version = "4.1.0-beta.1";
2
2
  var packageJson = {
3
3
  version: version};
4
4
 
@@ -1,6 +1,6 @@
1
1
  import 'reflect-metadata';
2
2
  import { createMockFn } from './mock-fn.js';
3
- import { InteractionType, ComponentType, ApplicationCommandType, CommandInteractionOptionResolver, GuildMessageManager, ThreadManager, DMMessageManager, MessageManager, ThreadMemberManager, Client, ApplicationCommandManager, UserManager, ChannelManager, GuildManager, ClientUser, Guild, GuildMemberManager, GuildChannelManager, RoleManager, GuildBanManager, AuthorizingIntegrationOwners, Message, User, GuildMember, TextChannel, ThreadChannel, MessageMentions, MessageFlagsBitField, Collection, ApplicationCommandOptionType, Role, BaseChannel, Attachment } from 'discord.js';
3
+ import { InteractionType, ComponentType, ApplicationCommandType, CommandInteractionOptionResolver, GuildMessageManager, ThreadManager, DMMessageManager, MessageManager, ThreadMemberManager, Client, ApplicationCommandManager, UserManager, ChannelManager, GuildManager, ClientUser, Guild, GuildMemberManager, GuildChannelManager, RoleManager, GuildBanManager, AuthorizingIntegrationOwners, Message, User, GuildMember, TextChannel, ThreadChannel, MessageMentions, MessageFlagsBitField, Collection, ApplicationCommandOptionType, Component, Embed, Role, BaseChannel, Attachment } from 'discord.js';
4
4
 
5
5
  // ---------------------------------------------------------------------------
6
6
  // stubDeep — Proxy that auto-creates a mock fn on any property access
@@ -163,7 +163,9 @@ function findPrototypeMethod(instance, name) {
163
163
  /**
164
164
  * Creates a mock instance of a discord.js class, keeping its prototype so `instanceof` holds.
165
165
  *
166
- * Type guards such as `isButton()` run the real discord.js logic. Replies behave like a real
166
+ * Type guards such as `isButton()` run the real discord.js logic. `inGuild()`, `inCachedGuild()`
167
+ * and `inRawGuild()` answer from the mock's own `guildId` and `guild`, so a mock created without
168
+ * a `guildId` is a DM. Replies behave like a real
167
169
  * interaction: `reply()` or `deferReply()` twice throws, and `followUp()`, `editReply()` and
168
170
  * `deleteReply()` throw before a reply. Every method is a mock function you can override.
169
171
  *
@@ -196,6 +198,19 @@ function findPrototypeMethod(instance, name) {
196
198
  stubs.set(name, createMockFn().mockImplementation(()=>method.call(instance)));
197
199
  }
198
200
  }
201
+ // Guild checks read the mock's own data, since discord.js resolves `guild` through a client the
202
+ // mock lacks: a guildId is a guild, a guild object with it a cached one, neither a DM. A member
203
+ // not given is the auto-stub, so only one set to null or undefined fails the check.
204
+ const own = (key)=>Object.prototype.hasOwnProperty.call(instance, key) ? instance[key] : undefined;
205
+ const hasMember = ()=>!Object.prototype.hasOwnProperty.call(instance, 'member') || Boolean(instance.member);
206
+ const guildChecks = {
207
+ inGuild: ()=>Boolean(own('guildId') && hasMember()),
208
+ inCachedGuild: ()=>Boolean(own('guildId') && own('guild') && hasMember()),
209
+ inRawGuild: ()=>Boolean(own('guildId') && !own('guild') && hasMember())
210
+ };
211
+ for (const [name, check] of Object.entries(guildChecks)){
212
+ if (findPrototypeMethod(instance, name) !== null) stubs.set(name, createMockFn().mockImplementation(check));
213
+ }
199
214
  // Set up reply state machine for repliable interactions
200
215
  const isRepliableMethod = findPrototypeMethod(instance, 'isRepliable');
201
216
  const repliable = isRepliableMethod !== null && isRepliableMethod.call(instance);
@@ -429,6 +444,16 @@ function findPrototypeMethod(instance, name) {
429
444
  guild.bans = stubDeep(Object.create(GuildBanManager.prototype));
430
445
  return stubDeep(guild);
431
446
  }
447
+ /**
448
+ * A component or embed as a message holds it: a discord.js instance as it is; anything else as
449
+ * its API JSON at the time of the call, behind `toJSON()`.
450
+ */ function asHeld(value) {
451
+ if (value instanceof Component || value instanceof Embed) return value;
452
+ const json = structuredClone(typeof value.toJSON === 'function' ? value.toJSON() : value);
453
+ return {
454
+ toJSON: ()=>json
455
+ };
456
+ }
432
457
  /**
433
458
  * Creates a mock {@link Message} that tracks whether it has been deleted.
434
459
  *
@@ -437,13 +462,30 @@ function findPrototypeMethod(instance, name) {
437
462
  * `msg.author.send` and `msg.guild.members.fetch` are ready to use, and every method is a mock
438
463
  * function you can override per test.
439
464
  *
465
+ * Without overrides the message is empty: no flags, components, embeds or attachments. Components
466
+ * and embeds given as API JSON or builders keep that JSON behind `toJSON()`, which is what
467
+ * `respond()` and `@Defer` read, such as when `@Defer` locks the controls of the message a button
468
+ * sits on; discord.js instances are kept as they are.
469
+ *
470
+ * @param overrides - The message's id, content, components, embeds and flags.
471
+ * @returns The mock message.
472
+ *
440
473
  * @example
441
474
  * ```ts
442
- * const message = createMockMessage()
475
+ * const message = createMockMessage({
476
+ * components: [
477
+ * new ActionRowBuilder<ButtonBuilder>().addComponents(
478
+ * new ButtonBuilder().setCustomId('card/refresh').setLabel('Refresh').setStyle(ButtonStyle.Primary),
479
+ * ),
480
+ * ],
481
+ * embeds: [{ title: 'Card' }],
482
+ * })
483
+ * const interaction = createMockInteraction(ButtonInteraction, { customId: 'card/refresh', message })
484
+ *
443
485
  * await message.delete()
444
486
  * message.deleted // true
445
487
  * ```
446
- */ function createMockMessage() {
488
+ */ function createMockMessage(overrides = {}) {
447
489
  const instance = Object.create(Message.prototype);
448
490
  const stubs = new Map();
449
491
  instance.deleted = false;
@@ -471,10 +513,12 @@ function findPrototypeMethod(instance, name) {
471
513
  // MessageMentions — constructor-assigned, has methods like .has(), .members
472
514
  instance.mentions = stubDeep(Object.create(MessageMentions.prototype));
473
515
  // Data a message always has, real rather than stubbed, so code reading it sees an empty message
474
- instance.flags = new MessageFlagsBitField();
475
- instance.components = [];
476
- instance.embeds = [];
516
+ instance.flags = new MessageFlagsBitField(overrides.flags);
517
+ instance.components = (overrides.components ?? []).map(asHeld);
518
+ instance.embeds = (overrides.embeds ?? []).map(asHeld);
477
519
  instance.attachments = new Collection();
520
+ if (overrides.id !== undefined) instance.id = overrides.id;
521
+ if (overrides.content !== undefined) instance.content = overrides.content;
478
522
  const alreadyDeleted = ()=>new Error('This message has already been deleted.');
479
523
  stubs.set('delete', createMockFn(async ()=>{
480
524
  if (instance.deleted) throw alreadyDeleted();
@@ -1,5 +1,5 @@
1
1
  import { ServiceIdentifier, Container } from 'inversify';
2
- import { ClientEvents, APIAuthorizingIntegrationOwnersMap, CacheType, CommandInteractionOptionResolver, Channel, Client, Guild, Message, User, ModalSubmitFields, DiscordAPIError, Interaction } from 'discord.js';
2
+ import { ClientEvents, APIMessageTopLevelComponent, JSONEncodable, APIEmbed, MessageFlagsResolvable, APIAuthorizingIntegrationOwnersMap, CacheType, CommandInteractionOptionResolver, Channel, Client, Guild, Message, User, ModalSubmitFields, DiscordAPIError, Interaction } from 'discord.js';
3
3
  import { GuardInterface, InterceptorInterface, ExceptionFilter } from '../interface/index.cjs';
4
4
  import { CommandType } from '../enum/index.cjs';
5
5
  import { Translator } from '../common/index.cjs';
@@ -306,7 +306,9 @@ interface InteractionClass<T> {
306
306
  /**
307
307
  * Creates a mock instance of a discord.js class, keeping its prototype so `instanceof` holds.
308
308
  *
309
- * Type guards such as `isButton()` run the real discord.js logic. Replies behave like a real
309
+ * Type guards such as `isButton()` run the real discord.js logic. `inGuild()`, `inCachedGuild()`
310
+ * and `inRawGuild()` answer from the mock's own `guildId` and `guild`, so a mock created without
311
+ * a `guildId` is a DM. Replies behave like a real
310
312
  * interaction: `reply()` or `deferReply()` twice throws, and `followUp()`, `editReply()` and
311
313
  * `deleteReply()` throw before a reply. Every method is a mock function you can override.
312
314
  *
@@ -359,6 +361,22 @@ declare function createMockGuild(): DeepMocked<Guild>;
359
361
  * @param Class - The discord.js channel class to mock.
360
362
  */
361
363
  declare function createMockChannel<T extends Channel>(Class: InteractionClass<T>): DeepMocked<T>;
364
+ /**
365
+ * What {@link createMockMessage} builds a message with. Components and embeds may be API JSON,
366
+ * builders or discord.js instances.
367
+ */
368
+ interface MockMessageOverrides {
369
+ /** The message's id. */
370
+ id?: string;
371
+ /** The message's text. */
372
+ content?: string;
373
+ /** Its top-level components: action rows, or Components V2 such as a container. */
374
+ components?: readonly (APIMessageTopLevelComponent | JSONEncodable<APIMessageTopLevelComponent>)[];
375
+ /** Its embeds. */
376
+ embeds?: readonly (APIEmbed | JSONEncodable<APIEmbed>)[];
377
+ /** Its flags: a number, flag names or a `MessageFlagsBitField`. */
378
+ flags?: MessageFlagsResolvable;
379
+ }
362
380
  /**
363
381
  * Creates a mock {@link Message} that tracks whether it has been deleted.
364
382
  *
@@ -367,14 +385,31 @@ declare function createMockChannel<T extends Channel>(Class: InteractionClass<T>
367
385
  * `msg.author.send` and `msg.guild.members.fetch` are ready to use, and every method is a mock
368
386
  * function you can override per test.
369
387
  *
388
+ * Without overrides the message is empty: no flags, components, embeds or attachments. Components
389
+ * and embeds given as API JSON or builders keep that JSON behind `toJSON()`, which is what
390
+ * `respond()` and `@Defer` read, such as when `@Defer` locks the controls of the message a button
391
+ * sits on; discord.js instances are kept as they are.
392
+ *
393
+ * @param overrides - The message's id, content, components, embeds and flags.
394
+ * @returns The mock message.
395
+ *
370
396
  * @example
371
397
  * ```ts
372
- * const message = createMockMessage()
398
+ * const message = createMockMessage({
399
+ * components: [
400
+ * new ActionRowBuilder<ButtonBuilder>().addComponents(
401
+ * new ButtonBuilder().setCustomId('card/refresh').setLabel('Refresh').setStyle(ButtonStyle.Primary),
402
+ * ),
403
+ * ],
404
+ * embeds: [{ title: 'Card' }],
405
+ * })
406
+ * const interaction = createMockInteraction(ButtonInteraction, { customId: 'card/refresh', message })
407
+ *
373
408
  * await message.delete()
374
409
  * message.deleted // true
375
410
  * ```
376
411
  */
377
- declare function createMockMessage(): DeepMocked<Message> & {
412
+ declare function createMockMessage(overrides?: MockMessageOverrides): DeepMocked<Message> & {
378
413
  deleted: boolean;
379
414
  };
380
415
  interface ChatInputOptions {
@@ -664,4 +699,4 @@ declare function getResponse(interaction: Interaction): ResponseReport;
664
699
  declare function createDiscordError(code: number, message?: string): DiscordAPIError;
665
700
 
666
701
  export { MeoCordTestingModule, TestingModule, TestingModuleBuilder, createChatInputOptions, createDiscordError, createExecutionContext, createMock, createMockChannel, createMockClient, createMockFn, createMockGuild, createMockInteraction, createMockMessage, createMockUser, createModalFields, expectCompleteCatalog, findRouteConflicts, getResponse, inspectHandler, isMockFunction, resolveRoute };
667
- export type { ChatInputOptions, ClassProvider, ComponentCommandType, DeepMocked, EmitResult, ExecutionContextOptions, HandlerInspection, HandlerName, InspectHandlerOptions, InspectedCooldown, InspectedFilter, InspectedGuard, InspectedInterceptor, InvocationResult, Mock, MockInstance, MockProps, MockResult, MockState, MockedFunction, Provider, ResolvedRoute, ResponseReport, RouteConflict, TestingModuleOptions, ValueProvider };
702
+ export type { ChatInputOptions, ClassProvider, ComponentCommandType, DeepMocked, EmitResult, ExecutionContextOptions, HandlerInspection, HandlerName, InspectHandlerOptions, InspectedCooldown, InspectedFilter, InspectedGuard, InspectedInterceptor, InvocationResult, Mock, MockInstance, MockMessageOverrides, MockProps, MockResult, MockState, MockedFunction, Provider, ResolvedRoute, ResponseReport, RouteConflict, TestingModuleOptions, ValueProvider };
@@ -1,5 +1,5 @@
1
1
  import { ServiceIdentifier, Container } from 'inversify';
2
- import { ClientEvents, APIAuthorizingIntegrationOwnersMap, CacheType, CommandInteractionOptionResolver, Channel, Client, Guild, Message, User, ModalSubmitFields, DiscordAPIError, Interaction } from 'discord.js';
2
+ import { ClientEvents, APIMessageTopLevelComponent, JSONEncodable, APIEmbed, MessageFlagsResolvable, APIAuthorizingIntegrationOwnersMap, CacheType, CommandInteractionOptionResolver, Channel, Client, Guild, Message, User, ModalSubmitFields, DiscordAPIError, Interaction } from 'discord.js';
3
3
  import { GuardInterface, InterceptorInterface, ExceptionFilter } from '../interface/index.js';
4
4
  import { CommandType } from '../enum/index.js';
5
5
  import { Translator } from '../common/index.js';
@@ -306,7 +306,9 @@ interface InteractionClass<T> {
306
306
  /**
307
307
  * Creates a mock instance of a discord.js class, keeping its prototype so `instanceof` holds.
308
308
  *
309
- * Type guards such as `isButton()` run the real discord.js logic. Replies behave like a real
309
+ * Type guards such as `isButton()` run the real discord.js logic. `inGuild()`, `inCachedGuild()`
310
+ * and `inRawGuild()` answer from the mock's own `guildId` and `guild`, so a mock created without
311
+ * a `guildId` is a DM. Replies behave like a real
310
312
  * interaction: `reply()` or `deferReply()` twice throws, and `followUp()`, `editReply()` and
311
313
  * `deleteReply()` throw before a reply. Every method is a mock function you can override.
312
314
  *
@@ -359,6 +361,22 @@ declare function createMockGuild(): DeepMocked<Guild>;
359
361
  * @param Class - The discord.js channel class to mock.
360
362
  */
361
363
  declare function createMockChannel<T extends Channel>(Class: InteractionClass<T>): DeepMocked<T>;
364
+ /**
365
+ * What {@link createMockMessage} builds a message with. Components and embeds may be API JSON,
366
+ * builders or discord.js instances.
367
+ */
368
+ interface MockMessageOverrides {
369
+ /** The message's id. */
370
+ id?: string;
371
+ /** The message's text. */
372
+ content?: string;
373
+ /** Its top-level components: action rows, or Components V2 such as a container. */
374
+ components?: readonly (APIMessageTopLevelComponent | JSONEncodable<APIMessageTopLevelComponent>)[];
375
+ /** Its embeds. */
376
+ embeds?: readonly (APIEmbed | JSONEncodable<APIEmbed>)[];
377
+ /** Its flags: a number, flag names or a `MessageFlagsBitField`. */
378
+ flags?: MessageFlagsResolvable;
379
+ }
362
380
  /**
363
381
  * Creates a mock {@link Message} that tracks whether it has been deleted.
364
382
  *
@@ -367,14 +385,31 @@ declare function createMockChannel<T extends Channel>(Class: InteractionClass<T>
367
385
  * `msg.author.send` and `msg.guild.members.fetch` are ready to use, and every method is a mock
368
386
  * function you can override per test.
369
387
  *
388
+ * Without overrides the message is empty: no flags, components, embeds or attachments. Components
389
+ * and embeds given as API JSON or builders keep that JSON behind `toJSON()`, which is what
390
+ * `respond()` and `@Defer` read, such as when `@Defer` locks the controls of the message a button
391
+ * sits on; discord.js instances are kept as they are.
392
+ *
393
+ * @param overrides - The message's id, content, components, embeds and flags.
394
+ * @returns The mock message.
395
+ *
370
396
  * @example
371
397
  * ```ts
372
- * const message = createMockMessage()
398
+ * const message = createMockMessage({
399
+ * components: [
400
+ * new ActionRowBuilder<ButtonBuilder>().addComponents(
401
+ * new ButtonBuilder().setCustomId('card/refresh').setLabel('Refresh').setStyle(ButtonStyle.Primary),
402
+ * ),
403
+ * ],
404
+ * embeds: [{ title: 'Card' }],
405
+ * })
406
+ * const interaction = createMockInteraction(ButtonInteraction, { customId: 'card/refresh', message })
407
+ *
373
408
  * await message.delete()
374
409
  * message.deleted // true
375
410
  * ```
376
411
  */
377
- declare function createMockMessage(): DeepMocked<Message> & {
412
+ declare function createMockMessage(overrides?: MockMessageOverrides): DeepMocked<Message> & {
378
413
  deleted: boolean;
379
414
  };
380
415
  interface ChatInputOptions {
@@ -664,4 +699,4 @@ declare function getResponse(interaction: Interaction): ResponseReport;
664
699
  declare function createDiscordError(code: number, message?: string): DiscordAPIError;
665
700
 
666
701
  export { MeoCordTestingModule, TestingModule, TestingModuleBuilder, createChatInputOptions, createDiscordError, createExecutionContext, createMock, createMockChannel, createMockClient, createMockFn, createMockGuild, createMockInteraction, createMockMessage, createMockUser, createModalFields, expectCompleteCatalog, findRouteConflicts, getResponse, inspectHandler, isMockFunction, resolveRoute };
667
- export type { ChatInputOptions, ClassProvider, ComponentCommandType, DeepMocked, EmitResult, ExecutionContextOptions, HandlerInspection, HandlerName, InspectHandlerOptions, InspectedCooldown, InspectedFilter, InspectedGuard, InspectedInterceptor, InvocationResult, Mock, MockInstance, MockProps, MockResult, MockState, MockedFunction, Provider, ResolvedRoute, ResponseReport, RouteConflict, TestingModuleOptions, ValueProvider };
702
+ export type { ChatInputOptions, ClassProvider, ComponentCommandType, DeepMocked, EmitResult, ExecutionContextOptions, HandlerInspection, HandlerName, InspectHandlerOptions, InspectedCooldown, InspectedFilter, InspectedGuard, InspectedInterceptor, InvocationResult, Mock, MockInstance, MockMessageOverrides, MockProps, MockResult, MockState, MockedFunction, Provider, ResolvedRoute, ResponseReport, RouteConflict, 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": "4.1.0-beta.0",
4
+ "version": "4.1.0-beta.1",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=22.13"
@@ -106,7 +106,8 @@
106
106
  "types": "./meocord.eslint.d.cts",
107
107
  "default": "./meocord.eslint.cjs"
108
108
  }
109
- }
109
+ },
110
+ "./package.json": "./package.json"
110
111
  },
111
112
  "files": [
112
113
  "dist/**/*",