meocord 4.2.1 → 4.2.2

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 (36) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-U2NBWcoU.cjs} +45 -13
  3. package/dist/cjs/_shared/{providers-BZgJyfZS.cjs → providers-B4MOFBrW.cjs} +58 -15
  4. package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-HTUb3Ii_.cjs} +1 -1
  5. package/dist/cjs/_shared/{startup-roots-CgfPQrNl.cjs → startup-roots-lA5xkBFq.cjs} +12 -11
  6. package/dist/cjs/_shared/{translate-error-BUAR1WYG.cjs → translate-error-CbBHUu5P.cjs} +1 -1
  7. package/dist/cjs/common/index.cjs +3 -3
  8. package/dist/cjs/core/index.cjs +8 -6
  9. package/dist/cjs/decorator/index.cjs +33 -3
  10. package/dist/cjs/testing/index.cjs +295 -60
  11. package/dist/cli.json +1 -1
  12. package/dist/esm/common/response/response-state.js +5 -5
  13. package/dist/esm/common/translator.js +15 -5
  14. package/dist/esm/core/dispatcher.js +9 -8
  15. package/dist/esm/core/filter-runner.js +2 -2
  16. package/dist/esm/core/handler-pipeline.js +10 -4
  17. package/dist/esm/core/meocord-factory.js +4 -2
  18. package/dist/esm/core/message-help.js +32 -6
  19. package/dist/esm/core/observer-runner.js +2 -1
  20. package/dist/esm/core/providers.js +12 -2
  21. package/dist/esm/core/stage-scope.js +2 -1
  22. package/dist/esm/core/theme-scope.js +4 -1
  23. package/dist/esm/decorator/app.decorator.js +31 -0
  24. package/dist/esm/decorator/filter.decorator.js +2 -2
  25. package/dist/esm/package.json.js +1 -1
  26. package/dist/esm/testing/catalog.js +4 -13
  27. package/dist/esm/testing/discord-defaults.js +21 -1
  28. package/dist/esm/testing/meocord-testing-module.js +24 -16
  29. package/dist/esm/testing/mock-interaction.js +242 -28
  30. package/dist/esm/testing/strict-mocks.js +7 -4
  31. package/dist/esm/util/value.util.js +19 -2
  32. package/dist/types/common/index.d.cts +4 -1
  33. package/dist/types/common/index.d.ts +4 -1
  34. package/dist/types/testing/index.d.cts +28 -14
  35. package/dist/types/testing/index.d.ts +28 -14
  36. package/package.json +1 -1
@@ -1,11 +1,11 @@
1
1
  import 'reflect-metadata';
2
2
  import { createMockFn, usesRunnerMockFn } from './mock-fn.js';
3
- import { DataManager, RoleManager, Role, GuildChannel, GuildMember, User, BaseGuild, MessageMentions, ThreadChannel, Message, BaseGuildVoiceChannel, VoiceChannel, BaseChannel, MessageReaction, InteractionType, ComponentType, ApplicationCommandType, GuildMemberManager, GuildChannelManager, UserManager, ChannelManager, GuildManager, Guild, GuildBanManager, GuildBan, ThreadMemberManager, ThreadMember, GuildMessageManager, DMMessageManager, GuildTextThreadManager, GuildForumThreadManager, PermissionOverwriteManager, PermissionOverwrites, ReactionManager, ReactionUserManager, PresenceManager, Presence, CommandInteractionOptionResolver, ApplicationCommandOptionType, Client, ApplicationCommandManager, ClientUser, CDN, DiscordjsErrorCodes, AuthorizingIntegrationOwners, BaseInteraction, UserContextMenuCommandInteraction, MessageContextMenuCommandInteraction, Attachment, Locale, StringSelectMenuInteraction, UserSelectMenuInteraction, RoleSelectMenuInteraction, ChannelSelectMenuInteraction, MentionableSelectMenuInteraction, Collection, PermissionsBitField, Base, SnowflakeUtil, DiscordjsError, DMChannel, GuildMemberRoleManager, MessageFlagsBitField, BaseManager, TextChannel, Component, Embed, ActionRow, ApplicationCommand, NewsChannel, ForumChannel, MediaChannel, DiscordjsTypeError, MessageFlags } from 'discord.js';
3
+ import { DataManager, RoleManager, Role, GuildChannel, GuildMember, User, BaseGuild, MessageMentions, ThreadChannel, Message, BaseGuildVoiceChannel, VoiceChannel, BaseChannel, MessageReaction, InteractionType, ComponentType, ApplicationCommandType, GuildMemberManager, GuildChannelManager, UserManager, ChannelManager, GuildManager, Guild, GuildBanManager, GuildBan, ThreadMemberManager, ThreadMember, GuildMessageManager, DMMessageManager, GuildTextThreadManager, GuildForumThreadManager, PermissionOverwriteManager, PermissionOverwrites, ReactionManager, ReactionUserManager, PresenceManager, Presence, GuildEmojiManager, GuildEmoji, GuildStickerManager, Sticker, GuildScheduledEventManager, GuildScheduledEvent, AutoModerationRuleManager, AutoModerationRule, GuildInviteManager, Invite, CommandInteractionOptionResolver, ApplicationCommandOptionType, Client, ApplicationCommandManager, ClientUser, CDN, DiscordjsErrorCodes, AuthorizingIntegrationOwners, BaseInteraction, UserContextMenuCommandInteraction, MessageContextMenuCommandInteraction, Attachment, Locale, StringSelectMenuInteraction, UserSelectMenuInteraction, RoleSelectMenuInteraction, ChannelSelectMenuInteraction, MentionableSelectMenuInteraction, Collection, PermissionsBitField, Base, SnowflakeUtil, DiscordjsError, DMChannel, GuildMemberRoleManager, MessageFlagsBitField, BaseManager, TextChannel, Component, Embed, ActionRow, ApplicationCommand, NewsChannel, ForumChannel, MediaChannel, DiscordjsTypeError, MessageFlags } from 'discord.js';
4
4
  import { createDiscordError } from './response.js';
5
5
  import { stampCall } from '../common/response/call-order.js';
6
6
  import { discordDefault, REAL_GETTER, placeholderValue } from './discord-defaults.js';
7
7
  import { Logger } from '../common/logger.js';
8
- import { warnOnce, warnPlaceholder } from '../common/deprecation.js';
8
+ import { warnPlaceholder, warnOnce } from '../common/deprecation.js';
9
9
  import { embedsAsDiscordStores, asDiscordStores } from './discord-shape.js';
10
10
  import { nextSnowflake, MOCK_BOT_ID } from './snowflake.js';
11
11
  import { strictMocks, noteMockMade } from './strict-mocks.js';
@@ -501,8 +501,49 @@ const MANAGER_ITEMS = [
501
501
  guild: guildOf(manager)
502
502
  } : {}
503
503
  })
504
+ ],
505
+ [
506
+ GuildEmojiManager,
507
+ (id, manager)=>itemInGuild(GuildEmoji, {
508
+ id: id ?? nextSnowflake()
509
+ }, manager)
510
+ ],
511
+ [
512
+ GuildStickerManager,
513
+ (id, manager)=>itemInGuild(Sticker, {
514
+ id: id ?? nextSnowflake()
515
+ }, manager)
516
+ ],
517
+ [
518
+ GuildScheduledEventManager,
519
+ (id, manager)=>itemInGuild(GuildScheduledEvent, {
520
+ id: id ?? nextSnowflake()
521
+ }, manager)
522
+ ],
523
+ [
524
+ AutoModerationRuleManager,
525
+ (id, manager)=>itemInGuild(AutoModerationRule, {
526
+ id: id ?? nextSnowflake()
527
+ }, manager)
528
+ ],
529
+ // An invite is keyed by its code, as discord.js caches and fetches one
530
+ [
531
+ GuildInviteManager,
532
+ (code, manager)=>itemInGuild(Invite, {
533
+ code: code ?? BigInt(nextSnowflake()).toString(36)
534
+ }, manager)
504
535
  ]
505
536
  ];
537
+ /** A mock of `Class` with `props`, in the manager's guild, by the guild and its id, when the manager has one. */ function itemInGuild(Class, props, manager) {
538
+ const guild = guildOf(manager);
539
+ return createMockInteraction(Class, {
540
+ ...props,
541
+ ...guild ? {
542
+ guild,
543
+ guildId: guild.id
544
+ } : {}
545
+ });
546
+ }
506
547
  const returnsPromise = (key, method)=>method.constructor.name === 'AsyncFunction' || PROMISE_METHODS.has(key) || /^set[A-Z]/.test(key) && !SYNC_SETTERS.has(key);
507
548
  // The ids a fetch asks for: an id, a discord.js object, or options naming one or several, such as `{ user: id }` or
508
549
  // `{ user: [ids] }`; undefined for a fetch of a list
@@ -576,6 +617,18 @@ function fetchedIds(args) {
576
617
  return typeof user === 'string' ? user : user?.id;
577
618
  });
578
619
  }
620
+ // An invite is made for the channel given, in its guild, and cached nowhere, as discord.js's create does
621
+ if (item && key === 'create' && target instanceof GuildInviteManager) {
622
+ return createMockFn(async (channel)=>{
623
+ const invite = item(undefined, receiver());
624
+ const channelId = typeof channel === 'string' ? channel : channel?.id;
625
+ const found = channel instanceof BaseChannel ? channel : channelId ? cacheOf(guildOf(receiver())?.channels)?.get(channelId) ?? null : null;
626
+ return Object.assign(invite, {
627
+ channel: found,
628
+ channelId: channelId ?? null
629
+ });
630
+ });
631
+ }
579
632
  // Every other manager here caches what its create() makes, as discord.js's does; check a manager's create when
580
633
  // adding it to MANAGER_ITEMS
581
634
  if (item && key === 'create') {
@@ -792,24 +845,36 @@ const isSnowflake = (id)=>typeof id === 'string' && /^\d{1,20}$/.test(id);
792
845
  };
793
846
  stampCall(call);
794
847
  log.push(call);
795
- let undo;
848
+ let gated;
796
849
  try {
797
- // Before the behaviour, as discord.js changes the state at once: a second answer made without awaiting is refused
798
- if (gate) undo = gate(args, byTest);
850
+ // Before the behaviour, as discord.js checks the state before it sends an answer
851
+ if (gate) gated = gate(args, byTest);
799
852
  } catch (error) {
800
853
  call.error = error;
801
854
  return Promise.reject(error);
802
855
  }
803
856
  const failed = (error)=>{
804
- undo?.();
857
+ gated?.settle?.();
858
+ gated?.undo?.();
805
859
  call.error = error;
806
860
  throw error;
807
861
  };
862
+ // Once the behaviour resolves, as discord.js changes the state once Discord has taken the answer
863
+ const landed = (value)=>{
864
+ gated?.settle?.();
865
+ try {
866
+ gated?.land?.();
867
+ } catch (error) {
868
+ call.error = error;
869
+ throw error;
870
+ }
871
+ return value;
872
+ };
808
873
  try {
809
874
  const result = Reflect.apply(behaviour, this, args);
810
875
  // Recorded and passed on, so a rejection nobody awaits is unhandled in the test, as it is on a bot
811
- if (result instanceof Promise) return result.then(undefined, failed);
812
- return result;
876
+ if (result instanceof Promise) return result.then(landed, failed);
877
+ return landed(result);
813
878
  } catch (error) {
814
879
  return failed(error);
815
880
  }
@@ -923,7 +988,31 @@ function createMockInteraction(Class, props) {
923
988
  if (repliable) {
924
989
  instance.replied = false;
925
990
  instance.deferred = false;
926
- instance.ephemeral = false;
991
+ // Null until reply() or deferReply() decides it, as discord.js has it; default mode reads false until then, and a
992
+ // test reading it there is told once. A value the test sets is read as set
993
+ const strict = strictMocks();
994
+ let ephemeral = strict ? null : false;
995
+ let ephemeralDecided = false;
996
+ // Whether one of the mock's own first answers ran; flags a test sets without one leave ephemeral as discord.js would
997
+ let answeredByMock = false;
998
+ const answeredOutside = ()=>Boolean(instance.replied || instance.deferred) && !answeredByMock;
999
+ const decideEphemeral = (value, decided)=>{
1000
+ ephemeral = value;
1001
+ ephemeralDecided = decided;
1002
+ };
1003
+ Object.defineProperty(instance, 'ephemeral', {
1004
+ get: ()=>{
1005
+ // Quiet once a test answered the interaction itself: MeoCord then reads ephemeral only for truthiness, where false
1006
+ // and null act alike
1007
+ if (!ephemeralDecided && !strict && !answeredOutside()) {
1008
+ warnPlaceholder(mockLogger, `${Object.getPrototypeOf(instance).constructor.name}.ephemeral`, 'it is null until reply() or deferReply() decides it, as discord.js has it', 'Reply or defer first, or call useStrictMocks() to read null now.');
1009
+ }
1010
+ return ephemeral;
1011
+ },
1012
+ set: (value)=>decideEphemeral(value, true),
1013
+ enumerable: true,
1014
+ configurable: true
1015
+ });
927
1016
  // discord.js's errors, with their codes; a call before any answer keeps the message the mock has always given
928
1017
  const alreadyReplied = ()=>discordjsError(DiscordjsErrorCodes.InteractionAlreadyReplied);
929
1018
  const notYetReplied = (method)=>discordjsError(DiscordjsErrorCodes.InteractionNotReplied, `Cannot call ${method}() before replying or deferring.`);
@@ -949,7 +1038,9 @@ function createMockInteraction(Class, props) {
949
1038
  const saved = {
950
1039
  replied: instance.replied,
951
1040
  deferred: instance.deferred,
952
- ephemeral: instance.ephemeral,
1041
+ ephemeral,
1042
+ ephemeralDecided,
1043
+ answeredByMock,
953
1044
  original,
954
1045
  originalGone,
955
1046
  followUps: new Map(followUps),
@@ -959,10 +1050,10 @@ function createMockInteraction(Class, props) {
959
1050
  return ()=>{
960
1051
  Object.assign(instance, {
961
1052
  replied: saved.replied,
962
- deferred: saved.deferred,
963
- ephemeral: saved.ephemeral
1053
+ deferred: saved.deferred
964
1054
  });
965
- ({ original, originalGone, answeredByTest, modalAnswered } = saved);
1055
+ decideEphemeral(saved.ephemeral, saved.ephemeralDecided);
1056
+ ({ original, originalGone, answeredByTest, answeredByMock, modalAnswered } = saved);
966
1057
  followUps.clear();
967
1058
  for (const [id, held] of saved.followUps)followUps.set(id, held);
968
1059
  };
@@ -987,24 +1078,76 @@ function createMockInteraction(Class, props) {
987
1078
  return build;
988
1079
  }
989
1080
  };
990
- /** A first answer: refused once the interaction has one, else it replies or defers. */ const first = (method, kind, then)=>(args, byTest)=>{
991
- if (answered()) refuse(method, alreadyReplied(), byTest);
992
- // Read first: flags discord.js refuses throw before anything changes
993
- const ephemeral = method === 'reply' || method === 'deferReply' ? hasEphemeralFlag(args[0]) : undefined;
1081
+ // The changes answers make to the reply state, in order. One whose answer fails is undone once every change after
1082
+ // it is undone too, so a later answer's change stands while it does, as on a bot
1083
+ const changes = [];
1084
+ const undoneInTurn = (undo)=>{
1085
+ const change = {
1086
+ undo,
1087
+ failed: false
1088
+ };
1089
+ changes.push(change);
1090
+ return ()=>{
1091
+ change.failed = true;
1092
+ while(changes.at(-1)?.failed)changes.pop().undo();
1093
+ };
1094
+ };
1095
+ // The first answers called and not yet settled, oldest first, for default mode to warn where a bot would differ
1096
+ const unsettled = [];
1097
+ // Default mode warns once per interaction where code that doesn't await an answer would fail on a bot
1098
+ let inFlightWarned = false;
1099
+ const warnInFlight = (text)=>{
1100
+ if (inFlightWarned) return;
1101
+ inFlightWarned = true;
1102
+ mockLogger.warn(text);
1103
+ };
1104
+ /**
1105
+ * A first answer: refused once the interaction has one, else it replies or defers. Under strict mocks the change
1106
+ * waits for the answer to land, as discord.js makes it once the request resolves, so one sent while another is in
1107
+ * flight passes discord.js's check and Discord refuses it with 40060 if the other landed first. Default mode makes
1108
+ * the change at once.
1109
+ */ const first = (method, kind, then)=>(args, byTest)=>{
1110
+ if (answered()) {
1111
+ if (unsettled.length > 0) {
1112
+ warnInFlight(`${method}() was called while ${unsettled[0]}() had not resolved; on a bot discord.js sends it, and Discord ` + `refuses it with 40060 ("Interaction has already been acknowledged") once the first lands. Await ` + `${unsettled[0]}() before answering again, or call useStrictMocks() to have the mock answer as Discord does.`);
1113
+ }
1114
+ refuse(method, alreadyReplied(), byTest);
1115
+ }
1116
+ // Read first: flags discord.js refuses throw before anything changes; reply() and deferReply() decide
1117
+ // ephemeral, as discord.js's do, and update() and deferUpdate() leave it
1118
+ const flagged = method === 'reply' || method === 'deferReply' ? hasEphemeralFlag(args[0]) : undefined;
994
1119
  const undo = snapshot();
995
1120
  instance[kind] = true;
996
- if (ephemeral) instance.ephemeral = true;
1121
+ if (flagged !== undefined) decideEphemeral(flagged, true);
997
1122
  answeredByTest = byTest;
1123
+ answeredByMock = true;
998
1124
  try {
999
1125
  then(args);
1000
1126
  } catch (error) {
1001
1127
  undo();
1002
1128
  throw error;
1003
1129
  }
1004
- return undo;
1130
+ if (strict) {
1131
+ const land = snapshot();
1132
+ undo();
1133
+ return {
1134
+ land: ()=>{
1135
+ if (answered()) throw createDiscordError(40060, 'Interaction has already been acknowledged.');
1136
+ land();
1137
+ }
1138
+ };
1139
+ }
1140
+ unsettled.push(method);
1141
+ return {
1142
+ undo: undoneInTurn(undo),
1143
+ settle: ()=>void unsettled.splice(unsettled.indexOf(method), 1)
1144
+ };
1005
1145
  };
1006
1146
  /** A later answer: refused before the interaction has one. */ const later = (method, then)=>(args, byTest)=>{
1007
1147
  if (!answered()) refuse(method, notYetReplied(method), byTest);
1148
+ if (unsettled.length > 0) {
1149
+ warnInFlight(`${unsettled[0]}() had not resolved when ${method}() was called; on a bot this throws InteractionNotReplied. ` + `Await ${unsettled[0]}() first, or call useStrictMocks() to have the mock refuse it now.`);
1150
+ }
1008
1151
  const undo = snapshot();
1009
1152
  try {
1010
1153
  then(args);
@@ -1012,7 +1155,9 @@ function createMockInteraction(Class, props) {
1012
1155
  undo();
1013
1156
  throw error;
1014
1157
  }
1015
- return undo;
1158
+ return {
1159
+ undo: undoneInTurn(undo)
1160
+ };
1016
1161
  };
1017
1162
  /** The id an edit, fetch or delete names, `undefined` for the original response. */ const targetId = (message)=>{
1018
1163
  if (message === undefined || message === '@original') return undefined;
@@ -1130,12 +1275,31 @@ function createMockInteraction(Class, props) {
1130
1275
  // it before answering would decide the window was already closed.
1131
1276
  if (instance.type === InteractionType.ApplicationCommandAutocomplete) {
1132
1277
  instance.responded = false;
1278
+ // Under strict mocks responded is set once respond() lands, as discord.js sets it once the request resolves, and a
1279
+ // second one sent while the first is in flight is refused by Discord with 40060; default mode sets it at once
1280
+ let respondsInFlight = 0;
1281
+ let respondWarned = false;
1133
1282
  stubs.set('respond', createMockFn(async (choices)=>{
1134
- if (instance.responded) throw discordjsError(DiscordjsErrorCodes.InteractionAlreadyReplied);
1283
+ if (instance.responded) {
1284
+ if (respondsInFlight > 0 && !respondWarned) {
1285
+ respondWarned = true;
1286
+ mockLogger.warn('respond() was called while respond() had not resolved; on a bot discord.js sends it, and Discord refuses ' + 'it with 40060 ("Interaction has already been acknowledged") once the first lands. Await respond() ' + 'before responding again, or call useStrictMocks() to have the mock respond as Discord does.');
1287
+ }
1288
+ throw discordjsError(DiscordjsErrorCodes.InteractionAlreadyReplied);
1289
+ }
1135
1290
  // Discord refuses a list longer than its limit, and the menu shows nothing
1136
1291
  if (Array.isArray(choices) && choices.length > MAX_AUTOCOMPLETE_CHOICES) {
1137
1292
  throw createDiscordError(50035, `Invalid Form Body\ndata.choices[BASE_TYPE_MAX_LENGTH]: Must be ${MAX_AUTOCOMPLETE_CHOICES} or fewer in length.`);
1138
1293
  }
1294
+ respondsInFlight++;
1295
+ if (!strictMocks()) instance.responded = true;
1296
+ try {
1297
+ await undefined;
1298
+ } finally{
1299
+ respondsInFlight--;
1300
+ }
1301
+ if (!strictMocks()) return;
1302
+ if (instance.responded) throw createDiscordError(40060, 'Interaction has already been acknowledged.');
1139
1303
  instance.responded = true;
1140
1304
  }));
1141
1305
  }
@@ -1713,6 +1877,26 @@ const HOLDS = [
1713
1877
  [
1714
1878
  PresenceManager.prototype,
1715
1879
  Presence
1880
+ ],
1881
+ [
1882
+ GuildEmojiManager.prototype,
1883
+ GuildEmoji
1884
+ ],
1885
+ [
1886
+ GuildStickerManager.prototype,
1887
+ Sticker
1888
+ ],
1889
+ [
1890
+ GuildScheduledEventManager.prototype,
1891
+ GuildScheduledEvent
1892
+ ],
1893
+ [
1894
+ AutoModerationRuleManager.prototype,
1895
+ AutoModerationRule
1896
+ ],
1897
+ [
1898
+ GuildInviteManager.prototype,
1899
+ Invite
1716
1900
  ]
1717
1901
  ];
1718
1902
  /**
@@ -1760,7 +1944,8 @@ const HOLDS = [
1760
1944
  return manager;
1761
1945
  }
1762
1946
  /**
1763
- * Creates a mock {@link Guild}, with the `members`, `channels`, `roles` and `bans` managers ready to stub.
1947
+ * Creates a mock {@link Guild}, with its `members`, `channels`, `roles`, `bans`, `emojis`, `stickers`, `scheduledEvents`,
1948
+ * `autoModerationRules` and `invites` managers ready to stub.
1764
1949
  *
1765
1950
  * Use it for the server a message or an interaction came from, with the members, roles and channels a handler looks
1766
1951
  * up in it.
@@ -1770,9 +1955,11 @@ const HOLDS = [
1770
1955
  *
1771
1956
  * @remarks
1772
1957
  * A manager's `fetch(id)` resolves to its cached item with that id, as discord.js looks there first, or to a new one it
1773
- * caches under that id: a member or channel in this guild, a role with that id, or a ban. `members.fetch({ user: ids })`
1774
- * resolves to a collection of each of those members, found or made the same way; `create()` and `edit()` resolve to a
1775
- * mock of its item, and a list fetch to an empty collection. Members, roles and channels given are put in
1958
+ * caches under that id: a member, channel, emoji, sticker, scheduled event or AutoMod rule in this guild, a role with
1959
+ * that id, a ban, or an invite with that code. `members.fetch({ user: ids })` resolves to a collection of each of those
1960
+ * members, found or made the same way; `create()` and `edit()` resolve to a mock of its item, which `create()` caches
1961
+ * as discord.js does, but for an invite, made for the channel given and cached nowhere; a list fetch resolves to an
1962
+ * empty collection. Members, roles and channels given are put in
1776
1963
  * their managers' caches, where dispatch looks first when it resolves a message's typed params; a member
1777
1964
  * {@link createMockMember} made without a server is in this one. Its `roles.everyone` is the role given with the
1778
1965
  * guild's id, or else an @everyone role of its own at position 0 with no permissions, in `roles.cache` as Discord has
@@ -1803,7 +1990,7 @@ const HOLDS = [
1803
1990
  instance.members = managerWith(GuildMemberManager.prototype, overrides.members);
1804
1991
  instance.channels = managerWith(GuildChannelManager.prototype, overrides.channels);
1805
1992
  instance.roles = guildRoleManager(instance.id, overrides.roles);
1806
- instance.bans = managerWith(GuildBanManager.prototype, undefined);
1993
+ for (const [key, Manager] of GUILD_MANAGERS)instance[key] = managerWith(Manager.prototype, undefined);
1807
1994
  const guild = stubDeep(instance);
1808
1995
  settleGuild(instance, guild);
1809
1996
  // A member made without a server of its own is in this one
@@ -2427,7 +2614,7 @@ let placeReadsSeen;
2427
2614
  guild.members = managerWith(GuildMemberManager.prototype, undefined);
2428
2615
  guild.channels = managerWith(GuildChannelManager.prototype, undefined);
2429
2616
  guild.roles = guildRoleManager(guild.id, undefined);
2430
- guild.bans = managerWith(GuildBanManager.prototype, undefined);
2617
+ for (const [key, Manager] of GUILD_MANAGERS)guild[key] = managerWith(Manager.prototype, undefined);
2431
2618
  const proxy = stubDeep(guild);
2432
2619
  settleGuild(guild, proxy);
2433
2620
  return proxy;
@@ -2449,12 +2636,39 @@ const botMembers = new WeakMap();
2449
2636
  return guildId ? cacheOf(guild.channels)?.get(channelId) : null;
2450
2637
  });
2451
2638
  }
2639
+ // A guild's managers beside members, channels and roles, each empty, of its discord.js class
2640
+ const GUILD_MANAGERS = [
2641
+ [
2642
+ 'bans',
2643
+ GuildBanManager
2644
+ ],
2645
+ [
2646
+ 'emojis',
2647
+ GuildEmojiManager
2648
+ ],
2649
+ [
2650
+ 'stickers',
2651
+ GuildStickerManager
2652
+ ],
2653
+ [
2654
+ 'scheduledEvents',
2655
+ GuildScheduledEventManager
2656
+ ],
2657
+ [
2658
+ 'autoModerationRules',
2659
+ AutoModerationRuleManager
2660
+ ],
2661
+ [
2662
+ 'invites',
2663
+ GuildInviteManager
2664
+ ]
2665
+ ];
2452
2666
  /** Gives a guild's managers the guild, as discord.js's do, so what they fetch or make is in it. */ function homeManagers(instance, guild) {
2453
2667
  for (const key of [
2454
2668
  'members',
2455
2669
  'channels',
2456
2670
  'roles',
2457
- 'bans'
2671
+ ...GUILD_MANAGERS.map(([key])=>key)
2458
2672
  ])instance[key].guild = guild;
2459
2673
  }
2460
2674
  /**
@@ -19,8 +19,9 @@ const settings = shared('strictMocks', ()=>({
19
19
  * and `message.thread` is `null` when the channel caches no thread under the message's id. So do a message's
20
20
  * `editedAt`, a member's `presence`, a server's `verified` and `systemChannel` and a channel's `parent`, each `null` or
21
21
  * `false` until the test gives what it is computed from. A reaction's `me` is `false`, a message's
22
- * `mentions.repliedUser` and a modal's `message` are `null`, and a reaction has a whole message of its own, so
23
- * MeoCord's dispatcher fetches neither. No placeholder warning is logged.
22
+ * `mentions.repliedUser` and a modal's `message` are `null`, an interaction's `ephemeral` is `null` until `reply()` or
23
+ * `deferReply()` decides it, and a reaction has a whole message of its own, so MeoCord's dispatcher fetches neither. No
24
+ * placeholder warning is logged.
24
25
  *
25
26
  * @remarks
26
27
  * Those getters read what a server holds, so strict mocks also have it as Discord sends it: the bot's member is in the
@@ -33,8 +34,10 @@ const settings = shared('strictMocks', ()=>({
33
34
  * A message the bot sends is its `client.user`'s, and one it sends in a DM is in that DM. Give the bot's member a role
34
35
  * to let it do more, such as kick a member its role ranks above. A value the test sets on a mock wins over the computed
35
36
  * one. An interaction's answer that discord.js would refuse, such as a second `reply()` after one a test set with
36
- * `mockResolvedValue`, is refused too, where without it the mock runs it and warns. The next major version (5.0)
37
- * computes these values, and refuses those answers, without the call.
37
+ * `mockResolvedValue`, is refused too, where without it the mock runs it and warns. An answer marks the interaction
38
+ * `replied`, `deferred` or `responded` once it resolves, as discord.js does, so code that doesn't await one fails as it
39
+ * would on a bot, within one run of the test's code. The next major version (5.0) computes these values, and refuses
40
+ * those answers, without the call.
38
41
  *
39
42
  * `invoke` also refuses a call that gives a handler fewer arguments than it declares where it builds none, such as a
40
43
  * reaction handler given its reaction without its `ReactionEvent`; without strict mocks, it warns once and calls it.
@@ -1,6 +1,23 @@
1
1
  import { quoteForLog } from './user-text.util.js';
2
2
 
3
- /** Whether a value is a class, which a stage entry or a `@Catch` type must be: what `new` and `instanceof` take. */ const isConstructor = (value)=>typeof value === 'function';
3
+ /** Whether `new` takes a value, as `instanceof` needs of its right side, such as a `@Catch` type. */ function isConstructible(value) {
4
+ if (typeof value !== 'function') return false;
5
+ try {
6
+ // Fails unless value can be new.target, which only a constructor can; none of value's constructor code runs
7
+ Reflect.construct(String, [], value);
8
+ return true;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+ /**
14
+ * Whether a value is a class, which a stage entry or an app option must be: constructible, with an object prototype for
15
+ * the metadata MeoCord reads. An arrow function, a method, an async or a generator function is none.
16
+ */ function isConstructor(value) {
17
+ if (!isConstructible(value)) return false;
18
+ const prototype = value.prototype;
19
+ return typeof prototype === 'object' && prototype !== null;
20
+ }
4
21
  /** A value as a refusal or a warning names it: a string quoted, `null` and `undefined` as written, else its kind. */ function describeValue(value) {
5
22
  if (value === null || value === undefined) return String(value);
6
23
  if (typeof value === 'string') return quoteForLog(value);
@@ -20,4 +37,4 @@ import { quoteForLog } from './user-text.util.js';
20
37
  });
21
38
  }
22
39
 
23
- export { describeValue, isConstructor, setOwn, withArticle };
40
+ export { describeValue, isConstructible, isConstructor, setOwn, withArticle };
@@ -596,6 +596,9 @@ declare function useTheme(): ResolvedTheme;
596
596
  * Such a listener runs in its emitter's context, so without it a `useTheme()` inside reads the theme outside any call.
597
597
  * Timers and promises the handler starts keep its theme without it.
598
598
  *
599
+ * The theme is the binding call's whole theme, its server's and user's themes included, so every later call, whoever
600
+ * it is for, is answered in the starting member's theme.
601
+ *
599
602
  * @param fn - The function to run in the call's theme; `this` and its arguments are passed through.
600
603
  * @returns A function that runs `fn` in the theme `useTheme()` returns where `bindTheme` is called.
601
604
  *
@@ -605,7 +608,7 @@ declare function useTheme(): ResolvedTheme;
605
608
  * async poll(message: Message) {
606
609
  * const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, time: 60_000 })
607
610
  * collector.on('collect', bindTheme(async (click: ButtonInteraction) => {
608
- * const { emojis } = useTheme() // the handler's theme, @UseTheme included
611
+ * const { emojis } = useTheme() // the starting member's theme, @UseTheme included
609
612
  * await click.reply(`${emojis.success} Picked`)
610
613
  * }))
611
614
  * }
@@ -596,6 +596,9 @@ declare function useTheme(): ResolvedTheme;
596
596
  * Such a listener runs in its emitter's context, so without it a `useTheme()` inside reads the theme outside any call.
597
597
  * Timers and promises the handler starts keep its theme without it.
598
598
  *
599
+ * The theme is the binding call's whole theme, its server's and user's themes included, so every later call, whoever
600
+ * it is for, is answered in the starting member's theme.
601
+ *
599
602
  * @param fn - The function to run in the call's theme; `this` and its arguments are passed through.
600
603
  * @returns A function that runs `fn` in the theme `useTheme()` returns where `bindTheme` is called.
601
604
  *
@@ -605,7 +608,7 @@ declare function useTheme(): ResolvedTheme;
605
608
  * async poll(message: Message) {
606
609
  * const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, time: 60_000 })
607
610
  * collector.on('collect', bindTheme(async (click: ButtonInteraction) => {
608
- * const { emojis } = useTheme() // the handler's theme, @UseTheme included
611
+ * const { emojis } = useTheme() // the starting member's theme, @UseTheme included
609
612
  * await click.reply(`${emojis.success} Picked`)
610
613
  * }))
611
614
  * }
@@ -684,7 +684,8 @@ declare class MeoCordTestingModule {
684
684
  * @param app - The class `@MeoCord` decorates.
685
685
  * @param options - Providers that replace the app's by token, extra controllers, and observers.
686
686
  * @returns The builder, whose `override*` methods still apply, for `compile()`.
687
- * @throws TypeError when `app` is not a `@MeoCord` class.
687
+ * @throws TypeError when `app` is not a `@MeoCord` class. After {@link reportAllStartupErrors}, an app whose
688
+ * `@MeoCord` was refused throws the error it was refused for instead.
688
689
  *
689
690
  * @example
690
691
  * ```ts
@@ -796,7 +797,8 @@ interface MockState<T extends (...args: any[]) => any = (...args: any[]) => any>
796
797
  * The mock API of a mock function: what it has recorded, and the methods that change what it does.
797
798
  *
798
799
  * Every mock from `meocord/testing` has it, with the names jest and Vitest use, so a test written for either runner
799
- * reads the same.
800
+ * reads the same. The values it takes and records aren't checked against the function it mocks; see
801
+ * {@link MockedFunction}.
800
802
  *
801
803
  * @group Testing
802
804
  * @category Mocks
@@ -838,9 +840,12 @@ interface MockInstance<T extends (...args: any[]) => any = (...args: any[]) => a
838
840
  /**
839
841
  * A mock function with the signature of `T`, and the mock API of {@link MockInstance}.
840
842
  *
841
- * It takes the place of jest's `MockedFunction<T>` and Vitest's `MockedFunction<T>`. The mock factories type each
842
- * method of a mock with it, so `interaction.reply.mockResolvedValue(...)` type-checks. For an overloaded method, such as
843
- * a manager's `fetch`, the mock API takes what any of its overloads takes or returns.
843
+ * It takes the place of jest's `MockedFunction<T>` and Vitest's `MockedFunction<T>`, and the mock factories type each
844
+ * method of a mock with it. Calling the mock is checked against `T`. The values its mock API takes and records are
845
+ * not, for a plain function as for an overloaded one: `mockReturnValue` and `mockResolvedValue` take any value,
846
+ * `mockImplementation` takes any function, and each of `mock.calls`' arguments and each of `mock.results`' values reads
847
+ * as `any`. So a partial fake, such as `guild.members.fetch.mockResolvedValue({ id })`, type-checks, and so does a
848
+ * value of the wrong type.
844
849
  *
845
850
  * @group Testing
846
851
  * @category Mocks
@@ -1103,7 +1108,10 @@ type NotRaw<T> = T extends {
1103
1108
  * collections of what it picks, each empty unless given. Other data Discord always sends reads as Discord sends it,
1104
1109
  * such as `false` for a flag and `null` for what may be absent; what picks the handler, `commandName` or `customId`, is
1105
1110
  * the test's to give. Replies follow Discord's order, so a second `reply()` rejects, and {@link getResponse} reports
1106
- * every answer the interaction got. Every method is a mock function, and one that returns a promise in discord.js
1111
+ * every answer the interaction got. Under `useStrictMocks()` an answer marks the interaction `replied` or `deferred`
1112
+ * once it resolves, as discord.js does after Discord takes it: an edit or follow-up made before then rejects with
1113
+ * `InteractionNotReplied`, and a second answer sent while the first is in flight is refused with Discord's 40060. In
1114
+ * default mode the mark is made at the call, with a warning where a bot would fail. Every method is a mock function, and one that returns a promise in discord.js
1107
1115
  * resolves. What the mock computes for itself, such as `memberPermissions` and `appPermissions`, calls none of them and
1108
1116
  * caches nothing, so a stub a test sets stays for its own code.
1109
1117
  *
@@ -1295,7 +1303,8 @@ interface MockGuildOverrides {
1295
1303
  channels?: readonly GuildBasedChannel[];
1296
1304
  }
1297
1305
  /**
1298
- * Creates a mock {@link Guild}, with the `members`, `channels`, `roles` and `bans` managers ready to stub.
1306
+ * Creates a mock {@link Guild}, with its `members`, `channels`, `roles`, `bans`, `emojis`, `stickers`, `scheduledEvents`,
1307
+ * `autoModerationRules` and `invites` managers ready to stub.
1299
1308
  *
1300
1309
  * Use it for the server a message or an interaction came from, with the members, roles and channels a handler looks
1301
1310
  * up in it.
@@ -1305,9 +1314,11 @@ interface MockGuildOverrides {
1305
1314
  *
1306
1315
  * @remarks
1307
1316
  * A manager's `fetch(id)` resolves to its cached item with that id, as discord.js looks there first, or to a new one it
1308
- * caches under that id: a member or channel in this guild, a role with that id, or a ban. `members.fetch({ user: ids })`
1309
- * resolves to a collection of each of those members, found or made the same way; `create()` and `edit()` resolve to a
1310
- * mock of its item, and a list fetch to an empty collection. Members, roles and channels given are put in
1317
+ * caches under that id: a member, channel, emoji, sticker, scheduled event or AutoMod rule in this guild, a role with
1318
+ * that id, a ban, or an invite with that code. `members.fetch({ user: ids })` resolves to a collection of each of those
1319
+ * members, found or made the same way; `create()` and `edit()` resolve to a mock of its item, which `create()` caches
1320
+ * as discord.js does, but for an invite, made for the channel given and cached nowhere; a list fetch resolves to an
1321
+ * empty collection. Members, roles and channels given are put in
1311
1322
  * their managers' caches, where dispatch looks first when it resolves a message's typed params; a member
1312
1323
  * {@link createMockMember} made without a server is in this one. Its `roles.everyone` is the role given with the
1313
1324
  * guild's id, or else an @everyone role of its own at position 0 with no permissions, in `roles.cache` as Discord has
@@ -1616,8 +1627,9 @@ declare function createChatInputOptions<Cached extends CacheType = any>(opts?: C
1616
1627
  * and `message.thread` is `null` when the channel caches no thread under the message's id. So do a message's
1617
1628
  * `editedAt`, a member's `presence`, a server's `verified` and `systemChannel` and a channel's `parent`, each `null` or
1618
1629
  * `false` until the test gives what it is computed from. A reaction's `me` is `false`, a message's
1619
- * `mentions.repliedUser` and a modal's `message` are `null`, and a reaction has a whole message of its own, so
1620
- * MeoCord's dispatcher fetches neither. No placeholder warning is logged.
1630
+ * `mentions.repliedUser` and a modal's `message` are `null`, an interaction's `ephemeral` is `null` until `reply()` or
1631
+ * `deferReply()` decides it, and a reaction has a whole message of its own, so MeoCord's dispatcher fetches neither. No
1632
+ * placeholder warning is logged.
1621
1633
  *
1622
1634
  * @remarks
1623
1635
  * Those getters read what a server holds, so strict mocks also have it as Discord sends it: the bot's member is in the
@@ -1630,8 +1642,10 @@ declare function createChatInputOptions<Cached extends CacheType = any>(opts?: C
1630
1642
  * A message the bot sends is its `client.user`'s, and one it sends in a DM is in that DM. Give the bot's member a role
1631
1643
  * to let it do more, such as kick a member its role ranks above. A value the test sets on a mock wins over the computed
1632
1644
  * one. An interaction's answer that discord.js would refuse, such as a second `reply()` after one a test set with
1633
- * `mockResolvedValue`, is refused too, where without it the mock runs it and warns. The next major version (5.0)
1634
- * computes these values, and refuses those answers, without the call.
1645
+ * `mockResolvedValue`, is refused too, where without it the mock runs it and warns. An answer marks the interaction
1646
+ * `replied`, `deferred` or `responded` once it resolves, as discord.js does, so code that doesn't await one fails as it
1647
+ * would on a bot, within one run of the test's code. The next major version (5.0) computes these values, and refuses
1648
+ * those answers, without the call.
1635
1649
  *
1636
1650
  * `invoke` also refuses a call that gives a handler fewer arguments than it declares where it builds none, such as a
1637
1651
  * reaction handler given its reaction without its `ReactionEvent`; without strict mocks, it warns once and calls it.