meocord 4.1.2 → 4.2.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.
Files changed (140) hide show
  1. package/CHANGELOG.md +362 -0
  2. package/THIRD_PARTY_NOTICES.md +14 -13
  3. package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-XKMlcUZz.cjs} +669 -185
  4. package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-BZgJyfZS.cjs} +218 -75
  5. package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
  6. package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CgfPQrNl.cjs} +374 -132
  7. package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-BUAR1WYG.cjs} +21 -7
  8. package/dist/cjs/common/index.cjs +4 -4
  9. package/dist/cjs/core/index.cjs +365 -310
  10. package/dist/cjs/decorator/index.cjs +85 -61
  11. package/dist/cjs/testing/index.cjs +2236 -496
  12. package/dist/cli.json +1 -1
  13. package/dist/esm/bin/app-template/README.md.template +3 -1
  14. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
  15. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
  16. package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
  17. package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
  18. package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
  19. package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
  20. package/dist/esm/bin/generator.js +4 -5
  21. package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
  22. package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
  23. package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
  24. package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
  25. package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
  26. package/dist/esm/bin/helper/service-generator.helper.js +4 -5
  27. package/dist/esm/bin/meocord.js +84 -25
  28. package/dist/esm/build/load-config.pre-entry.js +17 -4
  29. package/dist/esm/build/rsbuild-config.js +16 -12
  30. package/dist/esm/build/typed-externals.js +36 -0
  31. package/dist/esm/common/cooldown-store.js +10 -2
  32. package/dist/esm/common/decorator.js +6 -6
  33. package/dist/esm/common/deprecation.js +2 -2
  34. package/dist/esm/common/errors.js +27 -2
  35. package/dist/esm/common/execution-context.js +1 -1
  36. package/dist/esm/common/meocord-text.js +11 -8
  37. package/dist/esm/common/metadata.js +1 -1
  38. package/dist/esm/common/response/call-order.js +9 -4
  39. package/dist/esm/common/response/presenter.js +33 -6
  40. package/dist/esm/common/response/response-state.js +84 -10
  41. package/dist/esm/common/route.js +21 -5
  42. package/dist/esm/common/sharded-cooldown-store.js +1 -1
  43. package/dist/esm/common/translate-error.js +1 -1
  44. package/dist/esm/common/translator.js +37 -6
  45. package/dist/esm/core/command-conflicts.js +34 -34
  46. package/dist/esm/core/command-registration.js +93 -24
  47. package/dist/esm/core/command-routes.js +2 -2
  48. package/dist/esm/core/component-routes.js +74 -31
  49. package/dist/esm/core/cooldown-runner.js +29 -8
  50. package/dist/esm/core/dispatcher.js +51 -7
  51. package/dist/esm/core/fallback.js +20 -11
  52. package/dist/esm/core/filter-runner.js +2 -2
  53. package/dist/esm/core/guard-runner.js +16 -3
  54. package/dist/esm/core/handler-input.js +4 -3
  55. package/dist/esm/core/handler-pipeline.js +27 -14
  56. package/dist/esm/core/handler-registry.js +1 -1
  57. package/dist/esm/core/interceptor-runner.js +2 -2
  58. package/dist/esm/core/lifecycle-hooks.js +134 -47
  59. package/dist/esm/core/meocord-factory.js +241 -179
  60. package/dist/esm/core/meocord.app.js +116 -75
  61. package/dist/esm/core/message-routes.js +4 -3
  62. package/dist/esm/core/observer-runner.js +4 -3
  63. package/dist/esm/core/providers.js +45 -23
  64. package/dist/esm/core/route-specificity.js +4 -4
  65. package/dist/esm/core/running-calls.js +43 -0
  66. package/dist/esm/core/scalar-types.js +15 -3
  67. package/dist/esm/core/shard-context.js +6 -7
  68. package/dist/esm/core/shard-manager.js +16 -10
  69. package/dist/esm/core/startup-roots.js +23 -4
  70. package/dist/esm/core/theme-resolvers.js +4 -1
  71. package/dist/esm/core/theme-runtime.js +2 -2
  72. package/dist/esm/core/theme-scope.js +3 -1
  73. package/dist/esm/core/theme-validation.js +17 -10
  74. package/dist/esm/decorator/app.decorator.js +12 -7
  75. package/dist/esm/decorator/command-builder.decorator.js +4 -4
  76. package/dist/esm/decorator/controller-class.decorator.js +9 -4
  77. package/dist/esm/decorator/controller.decorator.js +162 -83
  78. package/dist/esm/decorator/cooldown.decorator.js +20 -3
  79. package/dist/esm/decorator/defer.decorator.js +4 -4
  80. package/dist/esm/decorator/event.decorator.js +5 -5
  81. package/dist/esm/decorator/filter.decorator.js +7 -7
  82. package/dist/esm/decorator/guard.decorator.js +11 -10
  83. package/dist/esm/decorator/inject.decorator.js +4 -4
  84. package/dist/esm/decorator/interceptor.decorator.js +7 -7
  85. package/dist/esm/decorator/observer.decorator.js +4 -4
  86. package/dist/esm/decorator/service.decorator.js +4 -4
  87. package/dist/esm/decorator/theme.decorator.js +3 -3
  88. package/dist/esm/decorator/validation.decorator.js +10 -10
  89. package/dist/esm/package.json.js +1 -1
  90. package/dist/esm/testing/catalog.js +27 -11
  91. package/dist/esm/testing/cooldown-store-suite.js +85 -15
  92. package/dist/esm/testing/discord-defaults.js +249 -21
  93. package/dist/esm/testing/index.js +4 -2
  94. package/dist/esm/testing/inspect-handler.js +20 -3
  95. package/dist/esm/testing/meocord-testing-module.js +352 -205
  96. package/dist/esm/testing/mock-fn.js +173 -22
  97. package/dist/esm/testing/mock-interaction.js +1214 -209
  98. package/dist/esm/testing/response.js +1 -1
  99. package/dist/esm/testing/routing.js +39 -14
  100. package/dist/esm/testing/snowflake.js +9 -2
  101. package/dist/esm/testing/startup-errors.js +26 -0
  102. package/dist/esm/testing/strict-mocks.js +65 -0
  103. package/dist/esm/testing/theme.js +1 -1
  104. package/dist/esm/util/bundle-entry.util.js +2 -1
  105. package/dist/esm/util/common.util.js +47 -19
  106. package/dist/esm/util/dev-runner.util.js +8 -8
  107. package/dist/esm/util/generator-cli.util.js +4 -5
  108. package/dist/esm/util/interaction.util.js +3 -3
  109. package/dist/esm/util/meocord-config-loader.util.js +10 -2
  110. package/dist/esm/util/meocord-config-validation.util.js +4 -5
  111. package/dist/esm/util/meocord-source-config.util.js +65 -20
  112. package/dist/esm/util/metadata-keys.js +1 -0
  113. package/dist/esm/util/method-wrapper.util.js +16 -0
  114. package/dist/esm/util/platform.util.js +10 -10
  115. package/dist/esm/util/refusal.util.js +148 -8
  116. package/dist/esm/util/shard-process.util.js +4 -1
  117. package/dist/esm/util/sharding-mode.util.js +5 -7
  118. package/dist/esm/util/shared-state.util.js +21 -0
  119. package/dist/esm/util/tsconfig-paths.util.js +29 -0
  120. package/dist/esm/util/tsconfig.util.js +10 -10
  121. package/dist/esm/util/value.util.js +12 -1
  122. package/dist/types/common/index.d.cts +8 -8
  123. package/dist/types/common/index.d.ts +8 -8
  124. package/dist/types/core/index.d.cts +10 -7
  125. package/dist/types/core/index.d.ts +10 -7
  126. package/dist/types/decorator/index.d.cts +31 -29
  127. package/dist/types/decorator/index.d.ts +31 -29
  128. package/dist/types/{index-D7n0FBMm.d.ts → index-DKJclIi-.d.ts} +107 -36
  129. package/dist/types/{index-DEP0NYrU.d.cts → index-Dlf0ZZtS.d.cts} +107 -36
  130. package/dist/types/interface/index.d.cts +1 -1
  131. package/dist/types/interface/index.d.ts +1 -1
  132. package/dist/types/{route-C-aMI7Sl.d.cts → route-Bh1U13yy.d.cts} +8 -4
  133. package/dist/types/{route-C-aMI7Sl.d.ts → route-Bh1U13yy.d.ts} +8 -4
  134. package/dist/types/testing/index.d.cts +406 -70
  135. package/dist/types/testing/index.d.ts +406 -70
  136. package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
  137. package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
  138. package/dist/types/{translator-J6tIUfdp.d.ts → translator-Bg7hwxDF.d.cts} +21 -8
  139. package/dist/types/{translator-B4mc1lzJ.d.cts → translator-CeP4bCzp.d.ts} +21 -8
  140. package/package.json +4 -3
@@ -1,9 +1,66 @@
1
- import { GuildPremiumTier, GuildVerificationLevel, GuildNSFWLevel, Locale, Guild, ClientUser, User, GuildMember, Role, Collection, MessageType, Message, CommandInteraction, AutocompleteInteraction, GuildChannel, BaseInteraction, ChannelType, TextChannel, NewsChannel, VoiceChannel, ForumChannel, MediaChannel, ThreadChannel, DMChannel, BaseChannel, Client, BaseGuild, SnowflakeUtil, PermissionsBitField } from 'discord.js';
2
- import { memberRoles, createMockUser, createMockGuild } from './mock-interaction.js';
1
+ import { GuildChannel, MessageReaction, MessageMentions, Message, GuildMember, BaseGuild, Guild, ModalSubmitInteraction, TextChannel, ThreadChannel, BaseGuildVoiceChannel, GuildPremiumTier, GuildVerificationLevel, GuildNSFWLevel, Locale, PresenceManager, ClientUser, UserFlagsBitField, User, GuildMemberFlagsBitField, Role, Collection, MessageType, ReactionManager, ChatInputCommandInteraction, CommandInteraction, AutocompleteInteraction, ReactionUserManager, BaseInteraction, ChannelType, NewsChannel, VoiceChannel, ForumChannel, MediaChannel, DMChannel, BaseChannel, Client, PermissionsBitField, PermissionFlagsBits } from 'discord.js';
2
+ import { createMockChannel, createMockGuild, createMockMessage, ownedManager, memberRoles, createMockUser, createChatInputOptions, readPlaceAs, botMemberOf, heldChannelOf, isRawMember, isUnsetEveryone, NEW_SERVER_EVERYONE_PERMISSIONS } from './mock-interaction.js';
3
+ import { nextSnowflake } from './snowflake.js';
4
+ import { strictMocks } from './strict-mocks.js';
5
+ import { Logger } from '../common/logger.js';
6
+ import { warnOnce } from '../common/deprecation.js';
3
7
 
8
+ const mockLogger = new Logger('Mocks');
9
+ /**
10
+ * A member's permissions where an interaction was made, as Discord computes them: under strict mocks, every permission
11
+ * for an Administrator or the owner, else the channel's overwrites on top of `member.permissions`, so a test that sets
12
+ * those keeps the base; otherwise `member.permissions`, with a warning once where strict mocks would read otherwise.
13
+ */ function inChannel(interaction, member) {
14
+ const base = member.permissions;
15
+ // Only a server the mock caches has the overwrites to apply
16
+ const { guild } = interaction;
17
+ if (!(guild instanceof Guild)) return base;
18
+ const computed = base.has(PermissionFlagsBits.Administrator) || member.id === guild.ownerId ? new PermissionsBitField(PermissionsBitField.All).freeze() : withOverwrites(base, overwritingChannel(interaction, guild), member);
19
+ if (strictMocks()) return computed;
20
+ if (!computed.equals(base)) {
21
+ const lost = new PermissionsBitField(base).remove(computed).toArray();
22
+ const gained = new PermissionsBitField(computed).remove(base).toArray();
23
+ const change = lost.length > 0 ? [
24
+ `without ${lost.join(', ')}`
25
+ ] : [];
26
+ if (gained.length > 0) change.push(`with ${gained.join(', ')}`);
27
+ warnOnce(mockLogger, `${interaction.constructor.name}.memberPermissions reads the member's own permissions here, without the ` + "channel's overwrites or every permission an Administrator or the owner has; in the next major version (5.0) " + `it reads them as Discord computes them in the channel, ${change.join(' and ')}. Set memberPermissions on the ` + 'mock, or call useStrictMocks() to compute them now.');
28
+ }
29
+ // Unless the test set them, a member's permissions come from its roles, @everyone's included
30
+ if (!Object.prototype.hasOwnProperty.call(member, 'permissions')) warnOnUnsetEveryone(interaction, guild, 'memberPermissions', base);
31
+ return base;
32
+ }
33
+ /**
34
+ * Warns once, as `key`, where `permissions` read a server's @everyone role default mode made with none, and lack what
35
+ * strict mocks give @everyone in a new server.
36
+ */ function warnOnUnsetEveryone(interaction, guild, key, permissions) {
37
+ const everyone = guild.roles.everyone;
38
+ if (!everyone || !isUnsetEveryone(everyone) || permissions.has(NEW_SERVER_EVERYONE_PERMISSIONS)) return;
39
+ warnOnce(mockLogger, `${interaction.constructor.name}.${key} reads the server's @everyone role here with no permissions; in the next major ` + 'version (5.0) it has the permissions Discord gives @everyone in a new server, as strict mocks read it. Set ' + 'guild.roles.everyone.permissions, or call useStrictMocks() to read it so now.');
40
+ }
41
+ /**
42
+ * The channel whose overwrites apply where an interaction was made, read without making or caching one: a thread's
43
+ * parent, or undefined where the channel, or a thread's parent, is one the mock would make, which has none.
44
+ */ function overwritingChannel(interaction, guild) {
45
+ const channel = heldChannelOf(interaction, guild);
46
+ // A thread's parent by the id the test gave it or one already read, so strict mocks' parent isn't made for this read
47
+ const parentId = (thread)=>Object.prototype.hasOwnProperty.call(thread, 'parentId') ? thread.parentId : madeParentIds.get(thread);
48
+ const at = channel instanceof ThreadChannel ? guild.channels.cache.get(parentId(channel) ?? '') : channel;
49
+ return at instanceof GuildChannel ? at : undefined;
50
+ }
51
+ // The parent id strict mocks made for each thread made without one, once read
52
+ const madeParentIds = new WeakMap();
53
+ // Private in discord.js's typings, though they are what its own permissionsFor computes with; called on the prototype
54
+ // so a test's stub on the channel stays for its caller
55
+ const channelPermissions = GuildChannel.prototype;
56
+ /** `base` with `channel`'s overwrites for `member` applied, in discord.js's order: @everyone's, the roles', the member's. */ function withOverwrites(base, channel, member) {
57
+ if (!channel) return base;
58
+ // Given the member and its roles as verified, as discord.js's memberPermissions does, so it resolves nothing again
59
+ const overwrites = channelPermissions.overwritesFor.call(channel, member, true, member.roles.cache);
60
+ return new PermissionsBitField(base).remove(overwrites.everyone?.deny ?? 0n).add(overwrites.everyone?.allow ?? 0n).remove(overwrites.roles.length > 0 ? overwrites.roles.map((role)=>role.deny) : 0n).add(overwrites.roles.length > 0 ? overwrites.roles.map((role)=>role.allow) : 0n).remove(overwrites.member?.deny ?? 0n).add(overwrites.member?.allow ?? 0n).freeze();
61
+ }
4
62
  /** A permission set with none, frozen as discord.js freezes the ones it gives. */ const noPermissions = ()=>new PermissionsBitField().freeze();
5
63
  /** A property discord.js computes from the others: its own getter runs, reading the mock's values. */ const REAL_GETTER = Symbol('real getter');
6
- const snowflake = ()=>SnowflakeUtil.generate().toString();
7
64
  /** A class and its defaults, typed together so a default that stops fitting its property fails to compile. */ const defaultsOf = (Class, defaults)=>[
8
65
  Class.prototype,
9
66
  defaults
@@ -16,7 +73,7 @@ const snowflake = ()=>SnowflakeUtil.generate().toString();
16
73
  defaultsOf(Guild, {
17
74
  name: ()=>'Guild',
18
75
  preferredLocale: ()=>Locale.EnglishUS,
19
- ownerId: snowflake,
76
+ ownerId: nextSnowflake,
20
77
  memberCount: ()=>1,
21
78
  available: ()=>true,
22
79
  large: ()=>false,
@@ -29,6 +86,9 @@ const snowflake = ()=>SnowflakeUtil.generate().toString();
29
86
  shardId: ()=>0,
30
87
  nsfwLevel: ()=>GuildNSFWLevel.Default,
31
88
  verificationLevel: ()=>GuildVerificationLevel.None,
89
+ presences: (guild)=>ownedManager(PresenceManager.prototype, {
90
+ guild
91
+ }),
32
92
  premiumTier: ()=>GuildPremiumTier.None,
33
93
  createdTimestamp: REAL_GETTER,
34
94
  createdAt: REAL_GETTER
@@ -43,6 +103,7 @@ const snowflake = ()=>SnowflakeUtil.generate().toString();
43
103
  discriminator: ()=>'0',
44
104
  avatar: ()=>null,
45
105
  system: ()=>false,
106
+ flags: ()=>new UserFlagsBitField(),
46
107
  tag: REAL_GETTER,
47
108
  displayName: REAL_GETTER,
48
109
  defaultAvatarURL: REAL_GETTER,
@@ -63,12 +124,12 @@ const snowflake = ()=>SnowflakeUtil.generate().toString();
63
124
  premiumSinceTimestamp: ()=>null,
64
125
  communicationDisabledUntilTimestamp: ()=>null,
65
126
  pending: ()=>false,
127
+ flags: ()=>new GuildMemberFlagsBitField().freeze(),
66
128
  displayName: REAL_GETTER,
67
129
  roles: (member)=>memberRoles(member, []),
68
130
  permissions: REAL_GETTER
69
131
  }),
70
132
  defaultsOf(Role, {
71
- id: snowflake,
72
133
  name: ()=>'role',
73
134
  // Above @everyone, which a server's role manager makes at position 0
74
135
  position: ()=>1,
@@ -106,30 +167,71 @@ const snowflake = ()=>SnowflakeUtil.generate().toString();
106
167
  poll: ()=>null,
107
168
  interactionMetadata: ()=>null,
108
169
  stickers: ()=>new Collection(),
170
+ messageSnapshots: ()=>new Collection(),
171
+ reactions: (message)=>ownedManager(ReactionManager.prototype, {
172
+ message
173
+ }),
109
174
  url: REAL_GETTER
110
175
  }),
111
176
  // A command's name and a component's customId say which handler a call is for, so they stay the test's to give
177
+ defaultsOf(ChatInputCommandInteraction, {
178
+ options: ()=>createChatInputOptions()
179
+ }),
112
180
  defaultsOf(CommandInteraction, {
113
- commandId: snowflake,
181
+ commandId: nextSnowflake,
114
182
  commandGuildId: ()=>null
115
183
  }),
116
184
  defaultsOf(AutocompleteInteraction, {
117
- commandId: snowflake,
118
- commandGuildId: ()=>null
185
+ commandId: nextSnowflake,
186
+ commandGuildId: ()=>null,
187
+ options: ()=>createChatInputOptions()
188
+ }),
189
+ defaultsOf(MessageReaction, {
190
+ users: (reaction)=>ownedManager(ReactionUserManager.prototype, {
191
+ reaction
192
+ })
119
193
  }),
120
194
  defaultsOf(BaseInteraction, {
121
- applicationId: snowflake,
195
+ applicationId: nextSnowflake,
122
196
  token: ()=>'mock-interaction-token',
123
197
  version: ()=>1,
124
198
  context: ()=>null,
199
+ entitlements: ()=>new Collection(),
125
200
  // What Discord sends with an interaction in a server, the member's permissions there; typed for a cached one
126
- memberPermissions: (interaction)=>interaction.guildId && interaction.member instanceof GuildMember ? interaction.member.permissions : null,
127
- // The bot's permissions where the interaction was made: its member's in the channel, or none outside a server
128
- appPermissions: (interaction)=>{
129
- const { guild, channel } = interaction;
130
- const me = guild instanceof Guild ? guild.members.me : null;
131
- return (me && channel instanceof GuildChannel ? channel.permissionsFor(me) : null) ?? noPermissions();
132
- }
201
+ // Each reads where the interaction is as its own, and warns by its own name where strict mocks place it otherwise
202
+ memberPermissions: (interaction)=>readPlaceAs(interaction, 'memberPermissions', ()=>{
203
+ const { guildId } = interaction;
204
+ const member = interaction.member;
205
+ if (!guildId) return null;
206
+ if (member instanceof GuildMember) return inChannel(interaction, member);
207
+ // A raw member's permissions are the string Discord sends, read as discord.js reads them
208
+ return isRawMember(member) && !interaction.guild ? new PermissionsBitField(BigInt(member.permissions)).freeze() : null;
209
+ }),
210
+ // The bot's permissions where the interaction was made: its member's in the channel, or in a thread its parent, as
211
+ // discord.js computes them; none outside a server
212
+ appPermissions: (interaction)=>readPlaceAs(interaction, 'appPermissions', ()=>{
213
+ const { guild } = interaction;
214
+ const me = guild instanceof Guild ? botMemberOf(guild) : null;
215
+ if (!(me instanceof GuildMember) || heldChannelOf(interaction, guild) === null) return noPermissions();
216
+ // discord.js's own computation, given its own overwritesFor over a test's stub on the channel; a channel the
217
+ // mock would make has no overwrites
218
+ const at = overwritingChannel(interaction, guild) ?? {
219
+ guild,
220
+ permissionOverwrites: {
221
+ cache: new Collection()
222
+ }
223
+ };
224
+ const channel = Object.create(at, {
225
+ overwritesFor: {
226
+ value: channelPermissions.overwritesFor
227
+ }
228
+ });
229
+ const permissions = channelPermissions.memberPermissions.call(channel, me, true);
230
+ // The bot's member has @everyone's too
231
+ warnOnUnsetEveryone(interaction, guild, 'appPermissions', permissions);
232
+ return permissions;
233
+ }, // A server the bot isn't in has no guild in either mode, so no permissions in both
234
+ (place)=>!place.raw)
133
235
  }),
134
236
  defaultsOf(TextChannel, {
135
237
  type: ()=>ChannelType.GuildText,
@@ -165,7 +267,7 @@ const snowflake = ()=>SnowflakeUtil.generate().toString();
165
267
  }),
166
268
  defaultsOf(GuildChannel, {
167
269
  name: ()=>'channel',
168
- guildId: (channel)=>typeof channel.guild?.id === 'string' ? channel.guild.id : snowflake(),
270
+ guildId: (channel)=>typeof channel.guild?.id === 'string' ? channel.guild.id : nextSnowflake(),
169
271
  position: ()=>0,
170
272
  rawPosition: ()=>0,
171
273
  parentId: ()=>null
@@ -184,12 +286,138 @@ const snowflake = ()=>SnowflakeUtil.generate().toString();
184
286
  nameAcronym: REAL_GETTER
185
287
  })
186
288
  ]);
187
- /** The default a mock of `target`'s class has for `key`, read from the nearest class that has one. */ function discordDefault(target, key) {
289
+ /**
290
+ * What strict mocks also read as Discord sends it, for discord.js's getters that read it: a server for a channel or
291
+ * thread made without one, a thread's parent text channel in it, and data that is otherwise a placeholder.
292
+ */ const STRICT_DEFAULTS = new Map([
293
+ defaultsOf(Message, {
294
+ content: ()=>'',
295
+ editedTimestamp: ()=>null,
296
+ editedAt: REAL_GETTER
297
+ }),
298
+ defaultsOf(MessageMentions, {
299
+ repliedUser: ()=>null
300
+ }),
301
+ defaultsOf(Guild, {
302
+ rulesChannelId: ()=>null,
303
+ publicUpdatesChannelId: ()=>null,
304
+ systemChannel: REAL_GETTER
305
+ }),
306
+ defaultsOf(BaseGuild, {
307
+ verified: REAL_GETTER
308
+ }),
309
+ defaultsOf(GuildMember, {
310
+ presence: REAL_GETTER
311
+ }),
312
+ defaultsOf(ModalSubmitInteraction, {
313
+ message: ()=>null
314
+ }),
315
+ defaultsOf(ThreadChannel, {
316
+ guild: ()=>createMockGuild(),
317
+ guildId: (thread)=>thread.guild.id,
318
+ parentId: (thread)=>{
319
+ const parent = createMockChannel(TextChannel, {
320
+ guild: thread.guild
321
+ });
322
+ thread.guild.channels.cache.set(parent.id, parent);
323
+ madeParentIds.set(thread, parent.id);
324
+ return parent.id;
325
+ },
326
+ parent: REAL_GETTER,
327
+ ownerId: nextSnowflake,
328
+ archived: ()=>false,
329
+ locked: ()=>false
330
+ }),
331
+ defaultsOf(GuildChannel, {
332
+ guild: ()=>createMockGuild(),
333
+ parent: REAL_GETTER
334
+ }),
335
+ // Full once its members reach a user limit, which a voice channel has none of unless given one
336
+ defaultsOf(BaseGuildVoiceChannel, {
337
+ userLimit: ()=>0,
338
+ full: REAL_GETTER
339
+ }),
340
+ // A reaction's message, as discord.js always has one, is a whole one
341
+ defaultsOf(MessageReaction, {
342
+ count: ()=>1,
343
+ me: ()=>false,
344
+ message: ()=>createMockMessage()
345
+ })
346
+ ]);
347
+ /**
348
+ * What a mock reads as a placeholder unless strict mocks read it as discord.js gives it, by the value a test sets for
349
+ * that, which the warning on reading one names.
350
+ */ const PLACEHOLDER_VALUES = new Map([
351
+ [
352
+ MessageReaction.prototype,
353
+ {
354
+ me: 'false',
355
+ message: 'createMockMessage()'
356
+ }
357
+ ],
358
+ [
359
+ MessageMentions.prototype,
360
+ {
361
+ repliedUser: 'null'
362
+ }
363
+ ],
364
+ [
365
+ Message.prototype,
366
+ {
367
+ editedAt: 'null'
368
+ }
369
+ ],
370
+ [
371
+ GuildMember.prototype,
372
+ {
373
+ presence: 'null'
374
+ }
375
+ ],
376
+ [
377
+ BaseGuild.prototype,
378
+ {
379
+ verified: 'false'
380
+ }
381
+ ],
382
+ [
383
+ Guild.prototype,
384
+ {
385
+ systemChannel: 'null'
386
+ }
387
+ ],
388
+ [
389
+ GuildChannel.prototype,
390
+ {
391
+ parent: 'null'
392
+ }
393
+ ],
394
+ [
395
+ ModalSubmitInteraction.prototype,
396
+ {
397
+ message: 'null'
398
+ }
399
+ ]
400
+ ]);
401
+ /** The value a test sets for `key` on a mock of `target`'s class, when a mock reads it as a placeholder. */ function placeholderValue(target, key) {
188
402
  for(let proto = Object.getPrototypeOf(target); proto !== null; proto = Object.getPrototypeOf(proto)){
189
- const found = DEFAULTS.get(proto)?.[key];
190
- if (found !== undefined) return found;
403
+ const value = PLACEHOLDER_VALUES.get(proto)?.[key];
404
+ if (value !== undefined) return value;
405
+ }
406
+ return undefined;
407
+ }
408
+ /** The default a mock of `target`'s class has for `key`, read from the nearest class that has one, strict ones first. */ function discordDefault(target, key) {
409
+ for (const defaults of strictMocks() ? [
410
+ STRICT_DEFAULTS,
411
+ DEFAULTS
412
+ ] : [
413
+ DEFAULTS
414
+ ]){
415
+ for(let proto = Object.getPrototypeOf(target); proto !== null; proto = Object.getPrototypeOf(proto)){
416
+ const found = defaults.get(proto)?.[key];
417
+ if (found !== undefined) return found;
418
+ }
191
419
  }
192
420
  return undefined;
193
421
  }
194
422
 
195
- export { REAL_GETTER, discordDefault };
423
+ export { REAL_GETTER, discordDefault, placeholderValue };
@@ -1,6 +1,8 @@
1
1
  export { MeoCordTestingModule, TestingModule, TestingModuleBuilder } from './meocord-testing-module.js';
2
- export { createChatInputOptions, createMock, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMember, createMockMessage, createMockUser } from './mock-interaction.js';
3
- export { clearAllMocks, createMockFn, isMockFunction, resetAllMocks } from './mock-fn.js';
2
+ export { createChatInputOptions, createMock, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMember, createMockMessage, createMockRawMember, createMockUser } from './mock-interaction.js';
3
+ export { clearAllMocks, createMockFn, isMockFunction, resetAllMocks, useMockFn } from './mock-fn.js';
4
+ export { useStrictMocks } from './strict-mocks.js';
5
+ export { reportAllStartupErrors } from './startup-errors.js';
4
6
  export { findRouteConflicts, resolveRoute } from './routing.js';
5
7
  export { createModalFields } from './modal-fields.js';
6
8
  export { expectCompleteCatalog } from './catalog.js';
@@ -2,7 +2,7 @@ import 'reflect-metadata';
2
2
  import { HandlerExecutionContext } from '../common/execution-context.js';
3
3
  import { appStages, handlerStages } from '../core/handler-pipeline.js';
4
4
  import { handlerCooldowns } from '../core/cooldown-runner.js';
5
- import { getMessageHandlers } from '../decorator/controller.decorator.js';
5
+ import { getMessageHandlers, getHandlerRoutes, getDeclaredRoutes } from '../decorator/controller.decorator.js';
6
6
  import { appObservers } from '../core/observer-runner.js';
7
7
 
8
8
  /**
@@ -37,7 +37,7 @@ import { appObservers } from '../core/observer-runner.js';
37
37
  * @group Testing
38
38
  * @category Inspection
39
39
  * @see {@link createExecutionContext}
40
- * @see {@link https://meocord.dev/docs/4.1/testing-recipes | Testing recipes}
40
+ * @see {@link https://meocord.dev/docs/4.2/testing-recipes | Testing recipes}
41
41
  */ function inspectHandler(controller, methodName, options = {}) {
42
42
  const context = new HandlerExecutionContext({
43
43
  controller,
@@ -46,7 +46,7 @@ import { appObservers } from '../core/observer-runner.js';
46
46
  });
47
47
  const globals = options.app ? appStages(options.app) : undefined;
48
48
  const { guards, interceptors, filters } = handlerStages(controller.prototype, methodName, globals);
49
- return {
49
+ const inspection = {
50
50
  controller,
51
51
  methodName,
52
52
  guards: Object.freeze([
@@ -68,6 +68,23 @@ import { appObservers } from '../core/observer-runner.js';
68
68
  get: (metadata)=>context.get(metadata),
69
69
  getAll: (metadata)=>context.getAll(metadata)
70
70
  };
71
+ // Read by a test but not compared by toEqual, so an assertion on the result's other fields holds
72
+ Object.defineProperty(inspection, 'inheritedRoutes', {
73
+ value: Object.freeze(inheritedRoutesOf(controller.prototype, methodName)),
74
+ enumerable: false
75
+ });
76
+ return inspection;
77
+ }
78
+ /** The routes `methodName` answers that the class declaring it last doesn't declare for it: those it inherits. */ function inheritedRoutesOf(prototype, methodName) {
79
+ const answered = [
80
+ ...new Set(getHandlerRoutes(prototype).filter((route)=>route.method === methodName).map((route)=>route.label))
81
+ ];
82
+ // The nearest class with its own decorators on the method; a class that only overrides it declares none
83
+ for(let at = prototype; at && at !== Object.prototype; at = Object.getPrototypeOf(at)){
84
+ const own = getDeclaredRoutes(at).filter((route)=>route.method === methodName).map((route)=>route.label);
85
+ if (own.length > 0) return at === prototype ? answered.filter((label)=>!own.includes(label)) : answered;
86
+ }
87
+ return [];
71
88
  }
72
89
 
73
90
  export { inspectHandler };