meocord 4.1.0 → 4.1.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.
@@ -1,9 +1,11 @@
1
1
  import 'reflect-metadata';
2
2
  import { createMockFn } from './mock-fn.js';
3
- import { InteractionType, ComponentType, ApplicationCommandType, CommandInteractionOptionResolver, ApplicationCommandOptionType, GuildMessageManager, GuildTextThreadManager, GuildForumThreadManager, DMMessageManager, ThreadMemberManager, Client, ApplicationCommandManager, UserManager, ChannelManager, GuildManager, ClientUser, Guild, GuildMemberManager, GuildChannelManager, GuildBanManager, AuthorizingIntegrationOwners, BaseInteraction, Locale, StringSelectMenuInteraction, UserSelectMenuInteraction, RoleSelectMenuInteraction, ChannelSelectMenuInteraction, MentionableSelectMenuInteraction, Collection, Message, ThreadChannel, MessageFlagsBitField, User, RoleManager, SnowflakeUtil, BaseChannel, DMChannel, GuildMember, GuildMemberRoleManager, MessageMentions, Component, Embed, Role, Attachment, BaseManager, Base, TextChannel, GuildBan, ThreadMember, ApplicationCommand, NewsChannel, ForumChannel, MediaChannel, BaseGuildVoiceChannel, DiscordjsErrorCodes, DiscordjsTypeError } from 'discord.js';
3
+ import { DataManager, RoleManager, Role, GuildChannel, GuildMember, User, BaseGuild, MessageMentions, Message, ThreadChannel, BaseGuildVoiceChannel, BaseChannel, MessageReaction, InteractionType, ComponentType, ApplicationCommandType, GuildMemberManager, GuildChannelManager, UserManager, ChannelManager, GuildManager, Guild, GuildBanManager, GuildBan, ThreadMemberManager, ThreadMember, GuildMessageManager, DMMessageManager, GuildTextThreadManager, GuildForumThreadManager, PermissionOverwriteManager, PermissionOverwrites, CommandInteractionOptionResolver, ApplicationCommandOptionType, Client, ApplicationCommandManager, ClientUser, CDN, AuthorizingIntegrationOwners, BaseInteraction, UserContextMenuCommandInteraction, Locale, StringSelectMenuInteraction, UserSelectMenuInteraction, RoleSelectMenuInteraction, ChannelSelectMenuInteraction, MentionableSelectMenuInteraction, Collection, MessageFlagsBitField, Base, SnowflakeUtil, DMChannel, GuildMemberRoleManager, TextChannel, Component, Embed, Attachment, BaseManager, ApplicationCommand, NewsChannel, ForumChannel, MediaChannel, DiscordjsErrorCodes, DiscordjsTypeError } 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 } from './discord-defaults.js';
7
+ import { Logger } from '../common/logger.js';
8
+ import { warnPlaceholder } from '../common/deprecation.js';
7
9
  import { embedsAsDiscordStores, asDiscordStores } from './discord-shape.js';
8
10
  import { nextSnowflake, MOCK_BOT_ID } from './snowflake.js';
9
11
 
@@ -58,11 +60,14 @@ function stubDeep(instance, externalStubs, onSet) {
58
60
  const desc = Object.getOwnPropertyDescriptor(proto, key);
59
61
  if (desc !== undefined) {
60
62
  protoValue = desc.value;
63
+ if (desc.get && PLACEHOLDER_BOOLEANS.get(proto)?.has(key)) warnBooleanPlaceholder(target, key);
61
64
  break;
62
65
  }
63
66
  proto = Object.getPrototypeOf(proto);
64
67
  }
65
- const stub = typeof protoValue === 'function' ? methodStub(target, key, protoValue, ()=>proxy) : stubDeep({});
68
+ // A client the mock's structures share: its guild's, or one of its own
69
+ if (key === 'client' && target instanceof Base) return clientOf(target);
70
+ const stub = typeof protoValue === 'function' ? isRealMethod(target, key) ? createMockFn((...args)=>protoValue.apply(proxy, args)) : methodStub(target, key, protoValue, ()=>proxy) : stubDeep({});
66
71
  stubs.set(key, stub);
67
72
  return stub;
68
73
  },
@@ -83,6 +88,164 @@ function stubDeep(instance, externalStubs, onSet) {
83
88
  });
84
89
  return proxy;
85
90
  }
91
+ // discord.js methods that read only what the mock holds, so they run for real: looking items up in a manager's cache,
92
+ // ranking roles, a member's or role's permissions in a channel, and whom a message mentions
93
+ const REAL_METHODS = new Map([
94
+ [
95
+ DataManager.prototype,
96
+ new Set([
97
+ 'resolve',
98
+ 'resolveId'
99
+ ])
100
+ ],
101
+ [
102
+ RoleManager.prototype,
103
+ new Set([
104
+ 'comparePositions'
105
+ ])
106
+ ],
107
+ [
108
+ Role.prototype,
109
+ new Set([
110
+ 'comparePositionTo',
111
+ 'permissionsIn'
112
+ ])
113
+ ],
114
+ [
115
+ GuildChannel.prototype,
116
+ new Set([
117
+ 'permissionsFor',
118
+ 'overwritesFor',
119
+ 'memberPermissions',
120
+ 'rolePermissions'
121
+ ])
122
+ ],
123
+ [
124
+ GuildMember.prototype,
125
+ new Set([
126
+ 'permissionsIn',
127
+ 'isCommunicationDisabled'
128
+ ])
129
+ ],
130
+ [
131
+ User.prototype,
132
+ new Set([
133
+ 'avatarURL',
134
+ 'displayAvatarURL',
135
+ 'bannerURL'
136
+ ])
137
+ ],
138
+ [
139
+ BaseGuild.prototype,
140
+ new Set([
141
+ 'iconURL',
142
+ 'bannerURL',
143
+ 'splashURL'
144
+ ])
145
+ ],
146
+ [
147
+ MessageMentions.prototype,
148
+ new Set([
149
+ 'has'
150
+ ])
151
+ ]
152
+ ]);
153
+ function isRealMethod(target, key) {
154
+ for(let proto = Object.getPrototypeOf(target); proto !== null; proto = Object.getPrototypeOf(proto)){
155
+ if (REAL_METHODS.get(proto)?.has(key)) return true;
156
+ }
157
+ return false;
158
+ }
159
+ // discord.js's computed booleans a mock reads as a placeholder, which is truthy: 5.0 computes them, and until then a
160
+ // test that reads one is told so once
161
+ const PLACEHOLDER_BOOLEANS = new Map([
162
+ [
163
+ Message.prototype,
164
+ new Set([
165
+ 'editable',
166
+ 'deletable',
167
+ 'pinnable',
168
+ 'crosspostable',
169
+ 'bulkDeletable',
170
+ 'hasThread',
171
+ 'partial'
172
+ ])
173
+ ],
174
+ [
175
+ GuildMember.prototype,
176
+ new Set([
177
+ 'manageable',
178
+ 'kickable',
179
+ 'bannable',
180
+ 'moderatable'
181
+ ])
182
+ ],
183
+ [
184
+ Role.prototype,
185
+ new Set([
186
+ 'editable'
187
+ ])
188
+ ],
189
+ [
190
+ GuildChannel.prototype,
191
+ new Set([
192
+ 'viewable',
193
+ 'manageable',
194
+ 'deletable'
195
+ ])
196
+ ],
197
+ [
198
+ ThreadChannel.prototype,
199
+ new Set([
200
+ 'joinable',
201
+ 'joined',
202
+ 'sendable',
203
+ 'unarchivable',
204
+ 'editable',
205
+ 'manageable',
206
+ 'viewable'
207
+ ])
208
+ ],
209
+ [
210
+ BaseGuildVoiceChannel.prototype,
211
+ new Set([
212
+ 'joinable'
213
+ ])
214
+ ],
215
+ [
216
+ User.prototype,
217
+ new Set([
218
+ 'partial'
219
+ ])
220
+ ],
221
+ [
222
+ BaseChannel.prototype,
223
+ new Set([
224
+ 'partial'
225
+ ])
226
+ ],
227
+ [
228
+ MessageReaction.prototype,
229
+ new Set([
230
+ 'partial'
231
+ ])
232
+ ]
233
+ ]);
234
+ const mockLogger = new Logger('Mocks');
235
+ function warnBooleanPlaceholder(target, key) {
236
+ const name = Object.getPrototypeOf(target).constructor.name;
237
+ const variable = name.charAt(0).toLowerCase() + name.slice(1);
238
+ warnPlaceholder(mockLogger, `${name}.${key}`, 'the mock computes it as discord.js does', `Set it on the mock, such as ${variable}.${key} = false, to test either way.`);
239
+ }
240
+ // The client of a structure made without one: the one its guild has, else one of its own, made once
241
+ const ownClients = new WeakMap();
242
+ function clientOf(target) {
243
+ const guild = Object.getOwnPropertyDescriptor(target, 'guild')?.value;
244
+ if (guild instanceof Guild) return guild.client;
245
+ let client = ownClients.get(target);
246
+ if (client === undefined) ownClients.set(target, client = createMockClient());
247
+ return client;
248
+ }
86
249
  // ---------------------------------------------------------------------------
87
250
  // methodStub — what an auto-stubbed discord.js method returns
88
251
  // ---------------------------------------------------------------------------
@@ -241,9 +404,9 @@ const MANAGER_ITEMS = [
241
404
  ]
242
405
  ];
243
406
  const returnsPromise = (key, method)=>method.constructor.name === 'AsyncFunction' || PROMISE_METHODS.has(key) || /^set[A-Z]/.test(key) && !SYNC_SETTERS.has(key);
244
- // The id of the one item a fetch asks for: an id, a discord.js object, or options naming one, such as `{ user: id }`;
245
- // undefined for a fetch of a list
246
- function fetchedId(args) {
407
+ // The ids a fetch asks for: an id, a discord.js object, or options naming one or several, such as `{ user: id }` or
408
+ // `{ user: [ids] }`; undefined for a fetch of a list
409
+ function fetchedIds(args) {
247
410
  const idOf = (value)=>typeof value === 'string' ? value : value instanceof Base ? value.id : undefined;
248
411
  const [first] = args;
249
412
  if (typeof first !== 'object' || first === null || first instanceof Base) return idOf(first);
@@ -255,7 +418,9 @@ function fetchedId(args) {
255
418
  'thread',
256
419
  'id'
257
420
  ]){
258
- const id = idOf(first[key]);
421
+ const value = first[key];
422
+ if (Array.isArray(value)) return value.map(idOf).filter((id)=>id !== undefined);
423
+ const id = idOf(value);
259
424
  if (id !== undefined) return id;
260
425
  }
261
426
  return undefined;
@@ -263,9 +428,9 @@ function fetchedId(args) {
263
428
  /**
264
429
  * The mock function for a method found on a discord.js prototype. One that returns a promise in discord.js resolves:
265
430
  * `createDM` to the user's DM channel, `send` and its kin to a message, a manager's `fetch`, `create` and `edit` to its
266
- * item (an empty collection for a list fetch), a structure's own `edit`, `fetch`, `delete`, `ban`, `pin`, `unpin`,
267
- * `timeout`, `disableCommunicationUntil` and setters to the structure, and the rest to `undefined`. Any other method
268
- * returns `undefined`.
431
+ * item (a collection of each for a fetch of several, an empty one for a list fetch), a structure's own `edit`, `fetch`,
432
+ * `delete`, `ban`, `pin`, `unpin`, `timeout`, `disableCommunicationUntil` and setters to the structure, and the rest to
433
+ * `undefined`. Any other method returns `undefined`.
269
434
  */ function methodStub(target, key, method, receiver) {
270
435
  if (!returnsPromise(key, method)) return createMockFn();
271
436
  // A member's DMs go through its user, and a user's through its one DM channel, as discord.js sends them
@@ -279,12 +444,17 @@ function fetchedId(args) {
279
444
  if (MESSAGE_METHODS.has(key)) return createMockFn(async ()=>createMockMessage());
280
445
  if (target instanceof BaseManager) {
281
446
  const item = MANAGER_ITEMS.find(([Manager])=>Manager.prototype.isPrototypeOf(target))?.[1];
282
- // A fetch of one item finds it in the cache first, as discord.js does, and caches one it makes
447
+ // A fetch of one item finds it in the cache first, as discord.js does, and caches one it makes; a fetch of
448
+ // several does so for each
283
449
  if (item && key === 'fetch') {
284
450
  return createMockFn(async (...args)=>{
285
- const id = fetchedId(args);
286
- if (id === undefined) return new Collection();
287
- return cached(cacheOf(receiver()), id, ()=>item(id, receiver()));
451
+ const ids = fetchedIds(args);
452
+ const one = (id)=>cached(cacheOf(receiver()), id, ()=>item(id, receiver()));
453
+ if (Array.isArray(ids)) return new Collection(ids.map((id)=>[
454
+ id,
455
+ one(id)
456
+ ]));
457
+ return ids === undefined ? new Collection() : one(ids);
288
458
  });
289
459
  }
290
460
  if (item && (key === 'create' || key === 'edit')) return createMockFn(async ()=>item(undefined, receiver()));
@@ -500,54 +670,7 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
500
670
  id,
501
671
  bot: false
502
672
  }));
503
- // ---------------------------------------------------------------------------
504
- // createMockInteraction
505
- // ---------------------------------------------------------------------------
506
- /**
507
- * Creates a mock instance of a discord.js class, such as an interaction, keeping its prototype so `instanceof` holds.
508
- *
509
- * Use it for the interaction a test hands to `invoke` or `dispatch`. For a user, a message, a guild, a channel or a
510
- * client, the dedicated factories fill in what discord.js would; for a type with no class, use {@link createMock}.
511
- *
512
- * @remarks
513
- * Type guards such as `isButton()` run the real discord.js logic. An interaction gets an `id`, a `channelId` and a
514
- * `user` of its own unless given, and its `locale` is `'en-US'`; without a `guildId` it is a DM, and with one its
515
- * `member` is its user: for a `user` given, when the test gives the `guild`, the guild's cached member for that user,
516
- * so a message and an interaction from one user in that server share it, and `memberPermissions` are that member's
517
- * permissions (`null` in a DM). A user is a person, `bot: false`, and a member has the server's @everyone role and the
518
- * roles {@link createMockMember} gave it; with a `guildId` but no `guild`, a server the bot isn't in, that @everyone role
519
- * has the `guildId`. A DM sent to a member goes through its user's `send()` and the user's one DM channel. Its
520
- * `channel` is a text channel of its server, the one its guild caches under `channelId`, or the user's DM channel. A
521
- * `channel` given sets what the test leaves out of `channelId`, `guildId` and `guild`, as discord.js reads them from
522
- * it: a DM channel is no server, and a server's channel its server; one in another server than the `guildId` given is
523
- * refused, naming both. A select menu has picked nothing unless given: its
524
- * `values` are the ids of the `users` and `members`, `roles` or `channels` given, the collections of what it picks,
525
- * each empty unless given. Other data Discord always sends reads as
526
- * Discord sends it, such as `false` for a flag and `null` for what may be absent; what picks the handler, `commandName`
527
- * or `customId`, is the test's to give. Replies follow Discord's order, so a second `reply()` rejects, and
528
- * {@link getResponse} reports every answer the interaction got. Every method is a mock function, and one that returns a
529
- * promise in discord.js resolves.
530
- *
531
- * @param Class - The discord.js class to mock.
532
- * @param props - Values for properties the class declares `readonly`; see {@link MockProps}.
533
- *
534
- * @example
535
- * ```ts
536
- * import { expect } from 'vitest'
537
- *
538
- * const interaction = createMockInteraction(ButtonInteraction, { customId: 'ticket/42/close' })
539
- * expect(interaction.isButton()).toBe(true)
540
- * await interaction.reply({ content: 'Closed.' })
541
- * expect(interaction.replied).toBe(true)
542
- * await expect(interaction.reply({ content: 'Again.' })).rejects.toThrow()
543
- * ```
544
- *
545
- * @group Testing
546
- * @category Mocks
547
- * @see {@link MockProps}
548
- * @see {@link createChatInputOptions}
549
- * @see {@link https://meocord.dev/docs/4.1/mocks | Mocks}
550
- */ function createMockInteraction(Class, props) {
673
+ function createMockInteraction(Class, props) {
551
674
  const instance = Object.create(Class.prototype);
552
675
  const stubs = new Map();
553
676
  // Set type fields so all prototype type-guard methods compute the right value
@@ -557,13 +680,10 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
557
680
  instance[key] = value;
558
681
  }
559
682
  }
560
- // Wire each type guard as a mock fn calling the real prototype implementation.
561
- // Correct by default; overridable per test via .mockReturnValue().
683
+ // Each type guard runs the real prototype implementation, as the mock's own, so a reset keeps it; a test may override it
562
684
  for (const name of TYPE_GUARD_METHODS){
563
685
  const method = findPrototypeMethod(instance, name);
564
- if (method !== null) {
565
- stubs.set(name, createMockFn().mockImplementation(()=>method.call(instance)));
566
- }
686
+ if (method !== null) stubs.set(name, createMockFn(()=>method.call(instance)));
567
687
  }
568
688
  // Guild checks read the mock's own data, since discord.js resolves `guild` through a client the
569
689
  // mock lacks: a guildId is a guild, a guild object with it a cached one, neither a DM. A member
@@ -576,7 +696,7 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
576
696
  inRawGuild: ()=>Boolean(own('guildId') && !own('guild') && hasMember())
577
697
  };
578
698
  for (const [name, check] of Object.entries(guildChecks)){
579
- if (findPrototypeMethod(instance, name) !== null) stubs.set(name, createMockFn().mockImplementation(check));
699
+ if (findPrototypeMethod(instance, name) !== null) stubs.set(name, createMockFn(check));
580
700
  }
581
701
  // Set up reply state machine for repliable interactions
582
702
  const isRepliableMethod = findPrototypeMethod(instance, 'isRepliable');
@@ -711,6 +831,9 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
711
831
  writable: true,
712
832
  configurable: true
713
833
  });
834
+ // The client caches the guild the interaction came from
835
+ const guildGiven = own('guild');
836
+ if (guildGiven instanceof Guild) cached(cacheOf(instance.client.guilds), guildGiven.id, ()=>guildGiven);
714
837
  cacheOf(instance.client?.users)?.set(instance.user.id, instance.user);
715
838
  // A channel given says where the interaction was made, for what the test leaves unset, as discord.js reads it
716
839
  const given = own('channel');
@@ -781,6 +904,30 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
781
904
  configurable: true
782
905
  });
783
906
  }
907
+ // A user context menu's target: the user with its targetId, the client's cached one or one made and cached there,
908
+ // and in a server that user's member; each read live, so a targetId set later picks its user, and assignable
909
+ if (UserContextMenuCommandInteraction.prototype.isPrototypeOf(instance)) {
910
+ if (unset('targetId')) instance.targetId = own('targetUser')?.id ?? nextSnowflake();
911
+ const targetUser = ()=>{
912
+ const id = own('targetId');
913
+ return cached(cacheOf(instance.client.users), id, ()=>mockUser(id));
914
+ };
915
+ const live = (key, read)=>Object.defineProperty(instance, key, {
916
+ get: read,
917
+ set: (value)=>Object.defineProperty(instance, key, {
918
+ value,
919
+ writable: true,
920
+ enumerable: true,
921
+ configurable: true
922
+ }),
923
+ enumerable: true,
924
+ configurable: true
925
+ });
926
+ if (unset('targetUser')) live('targetUser', targetUser);
927
+ if (unset('targetMember')) {
928
+ live('targetMember', ()=>own('guildId') ? memberFor(own('guild'), targetUser(), true, own('guildId')) : null);
929
+ }
930
+ }
784
931
  }
785
932
  // Discord sends the user's locale with every interaction, and the server's preferred one with one made in a server
786
933
  if (BaseInteraction.prototype.isPrototypeOf(instance)) {
@@ -957,7 +1104,7 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
957
1104
  * import { expect } from 'vitest'
958
1105
  *
959
1106
  * const client = createMockClient()
960
- * const botId = client.user!.id
1107
+ * const botId = client.user.id
961
1108
  * const message = createMockMessage({ content: `<@${botId}> help`, client })
962
1109
  * expect(message.mentions.users.has(botId)).toBe(true)
963
1110
  * ```
@@ -974,15 +1121,90 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
974
1121
  // Real caches, empty until something is put in them, as a client that has just logged in
975
1122
  instance.users = managerWith(UserManager.prototype, undefined);
976
1123
  instance.channels = managerWith(ChannelManager.prototype, undefined);
977
- instance.guilds = stubDeep(Object.create(GuildManager.prototype));
1124
+ instance.guilds = managerWith(GuildManager.prototype, undefined);
978
1125
  instance.user = stubDeep(Object.assign(Object.create(ClientUser.prototype), {
979
1126
  id: MOCK_BOT_ID,
980
1127
  bot: true
981
1128
  }));
982
1129
  instance.application = stubDeep(appInstance);
983
- return stubDeep(instance);
1130
+ // Discord's CDN routes, which avatar and icon URLs are built from; nothing is requested
1131
+ instance.rest = stubDeep(Object.assign(Object.create(null), {
1132
+ cdn: new CDN()
1133
+ }));
1134
+ // A client a test is given is a logged-in one
1135
+ instance.isReady = createMockFn(()=>true);
1136
+ const client = stubDeep(instance);
1137
+ for (const key of [
1138
+ 'users',
1139
+ 'channels',
1140
+ 'guilds'
1141
+ ])instance[key].client = client;
1142
+ Object.defineProperty(instance.user, 'client', {
1143
+ value: client,
1144
+ writable: true,
1145
+ configurable: true
1146
+ });
1147
+ return client;
984
1148
  }
985
- /** A manager whose `cache` is a real collection of `items`, by id, empty without them, and whose methods are stubs. */ function managerWith(prototype, items) {
1149
+ // What each manager holds, which discord.js's resolve() checks an item against
1150
+ const HOLDS = [
1151
+ [
1152
+ GuildMemberManager.prototype,
1153
+ GuildMember
1154
+ ],
1155
+ [
1156
+ RoleManager.prototype,
1157
+ Role
1158
+ ],
1159
+ [
1160
+ GuildChannelManager.prototype,
1161
+ GuildChannel
1162
+ ],
1163
+ [
1164
+ UserManager.prototype,
1165
+ User
1166
+ ],
1167
+ [
1168
+ ChannelManager.prototype,
1169
+ BaseChannel
1170
+ ],
1171
+ [
1172
+ GuildManager.prototype,
1173
+ Guild
1174
+ ],
1175
+ [
1176
+ GuildBanManager.prototype,
1177
+ GuildBan
1178
+ ],
1179
+ [
1180
+ ThreadMemberManager.prototype,
1181
+ ThreadMember
1182
+ ],
1183
+ [
1184
+ GuildMessageManager.prototype,
1185
+ Message
1186
+ ],
1187
+ [
1188
+ DMMessageManager.prototype,
1189
+ Message
1190
+ ],
1191
+ [
1192
+ GuildTextThreadManager.prototype,
1193
+ ThreadChannel
1194
+ ],
1195
+ [
1196
+ GuildForumThreadManager.prototype,
1197
+ ThreadChannel
1198
+ ],
1199
+ [
1200
+ PermissionOverwriteManager.prototype,
1201
+ PermissionOverwrites
1202
+ ]
1203
+ ];
1204
+ /**
1205
+ * A manager whose `cache` is a real collection of `items`, by id, empty without them, and whose methods are stubs but
1206
+ * for `resolve` and `resolveId`, which read the cache. Its `client` is the one its guild or channel has.
1207
+ */ function managerWith(prototype, items) {
986
1208
  const manager = Object.create(prototype);
987
1209
  const cache = new Collection((items ?? []).map((item)=>[
988
1210
  String(item.id ?? item.user?.id),
@@ -992,6 +1214,19 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
992
1214
  value: cache,
993
1215
  writable: true
994
1216
  });
1217
+ const holds = HOLDS.find(([Manager])=>Manager === prototype || Object.prototype.isPrototypeOf.call(Manager, prototype))?.[1];
1218
+ if (holds) Object.defineProperty(manager, 'holds', {
1219
+ value: holds
1220
+ });
1221
+ let own;
1222
+ Object.defineProperty(manager, 'client', {
1223
+ get: ()=>{
1224
+ const owner = manager.guild ?? manager.channel ?? manager.thread;
1225
+ return owner instanceof Base ? owner.client : own ?? (own = createMockClient());
1226
+ },
1227
+ set: (value)=>own = value,
1228
+ configurable: true
1229
+ });
995
1230
  return stubDeep(manager);
996
1231
  }
997
1232
  /**
@@ -1005,14 +1240,18 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
1005
1240
  *
1006
1241
  * @remarks
1007
1242
  * 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
1008
- * caches under that id: a member or channel in this guild, a role with that id, or a ban; `create()` and `edit()`
1009
- * resolve to a mock of its item, and a list fetch to an empty collection. Members, roles and channels given are put in
1243
+ * caches under that id: a member or channel in this guild, a role with that id, or a ban. `members.fetch({ user: ids })`
1244
+ * resolves to a collection of each of those members, found or made the same way; `create()` and `edit()` resolve to a
1245
+ * mock of its item, and a list fetch to an empty collection. Members, roles and channels given are put in
1010
1246
  * their managers' caches, where dispatch looks first when it resolves a message's typed params; a member
1011
1247
  * {@link createMockMember} made without a server is in this one. Its `roles.everyone` is the role given with the
1012
1248
  * guild's id, or else an @everyone role of its own at position 0 with no permissions, in `roles.cache` as Discord has
1013
1249
  * it. The guild is named `'Guild'` and its `preferredLocale` is `'en-US'` unless given, so `t.forGuild(guild)`
1014
1250
  * translates as for a new English server.
1015
1251
  *
1252
+ * The mock makes a member for any id it's asked for, so a test of an id that isn't a member rejects the fetch, as
1253
+ * `guild.members.fetch.mockRejectedValue(createDiscordError(10007))`, or resolves a collection without that member.
1254
+ *
1016
1255
  * @example
1017
1256
  * ```ts
1018
1257
  * const target = createMock<GuildMember>({ id: '111111111111111111' })
@@ -1035,7 +1274,7 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
1035
1274
  instance.roles = guildRoleManager(instance.id, overrides.roles);
1036
1275
  instance.bans = managerWith(GuildBanManager.prototype, undefined);
1037
1276
  const guild = stubDeep(instance);
1038
- homeManagers(instance, guild);
1277
+ settleGuild(instance, guild);
1039
1278
  // A member made without a server of its own is in this one
1040
1279
  for (const member of overrides.members ?? []){
1041
1280
  if (!unhomedMembers.delete(member)) continue;
@@ -1052,7 +1291,8 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
1052
1291
  * The managers the class has are ready to stub: `messages`, `threads` on text, announcement, forum and media channels,
1053
1292
  * and `members` on threads, each with a real, empty `cache` and the channel as its `channel`, or `thread`. A subclass gets the managers of the class it extends.
1054
1293
  * Type guards such as `isTextBased()`, `isDMBased()` and `isThread()` run discord.js's own logic, so each answers what
1055
- * the channel is.
1294
+ * the channel is. A thread is typed as discord.js types every thread it gives, a public or a private one, so it fits
1295
+ * an interaction's `channel`, a guild's channels and the `threadCreate` event.
1056
1296
  *
1057
1297
  * @param Class - The discord.js channel class to mock.
1058
1298
  * @param props - Values for the channel's properties, such as its `id`, `name` or `topic`; see {@link MockProps}.
@@ -1096,13 +1336,16 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
1096
1336
  instance.messages = managerWith(GuildMessageManager.prototype, undefined);
1097
1337
  instance.members = managerWith(ThreadMemberManager.prototype, undefined);
1098
1338
  }
1339
+ // A server's channel has its overwrites, none until a test puts some in the cache, which its permissions read
1340
+ if (is(GuildChannel) && !is(ThreadChannel)) instance.permissionOverwrites = managerWith(PermissionOverwriteManager.prototype, undefined);
1099
1341
  Object.assign(instance, props);
1100
1342
  const stubs = new Map();
1101
1343
  const channel = stubDeep(instance, stubs);
1102
1344
  // Each manager knows its channel, as discord.js's do, and a thread's members their thread
1103
1345
  for (const key of [
1104
1346
  'messages',
1105
- 'threads'
1347
+ 'threads',
1348
+ 'permissionOverwrites'
1106
1349
  ])if (instance[key]) instance[key].channel = channel;
1107
1350
  if (is(ThreadChannel) && instance.members) instance.members.thread = channel;
1108
1351
  // Type guards run discord.js's own logic, which reads the channel's type and the managers it has
@@ -1362,7 +1605,7 @@ const CHANNEL_TYPE_GUARDS = [
1362
1605
  guild.roles = guildRoleManager(guild.id, undefined);
1363
1606
  guild.bans = managerWith(GuildBanManager.prototype, undefined);
1364
1607
  const proxy = stubDeep(guild);
1365
- homeManagers(guild, proxy);
1608
+ settleGuild(guild, proxy);
1366
1609
  return proxy;
1367
1610
  }
1368
1611
  /** 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) {
@@ -1373,6 +1616,35 @@ const CHANNEL_TYPE_GUARDS = [
1373
1616
  'bans'
1374
1617
  ])instance[key].guild = guild;
1375
1618
  }
1619
+ /**
1620
+ * Makes `guild` whole, as discord.js's guilds are: its managers and the roles and channels in its caches belong to it,
1621
+ * it has a client of its own until one is given, and `members.me` is the bot's member, the cached one or one with
1622
+ * @everyone, made and cached once on first read.
1623
+ */ function settleGuild(instance, guild) {
1624
+ homeManagers(instance, guild);
1625
+ let client;
1626
+ Object.defineProperty(instance, 'client', {
1627
+ get: ()=>client ?? (client = createMockClient()),
1628
+ set: (value)=>client = value,
1629
+ configurable: true
1630
+ });
1631
+ const members = instance.members;
1632
+ Object.defineProperty(members, 'me', {
1633
+ get: ()=>{
1634
+ const { user } = guild.client;
1635
+ return cached(cacheOf(members), user.id, ()=>memberIn(guild, user.id, user));
1636
+ },
1637
+ configurable: true
1638
+ });
1639
+ for (const key of [
1640
+ 'roles',
1641
+ 'channels'
1642
+ ]){
1643
+ for (const item of cacheOf(instance[key])?.values() ?? []){
1644
+ if (!Object.prototype.hasOwnProperty.call(item, 'guild')) item.guild = guild;
1645
+ }
1646
+ }
1647
+ }
1376
1648
  /** The ids each kind of mention in `content` names, in order and once each. */ function mentionedIds(content) {
1377
1649
  const ids = (pattern)=>[
1378
1650
  ...new Set([
@@ -1435,11 +1707,15 @@ const CHANNEL_TYPE_GUARDS = [
1435
1707
  const instance = Object.create(MessageMentions.prototype);
1436
1708
  for (const [key, value] of Object.entries({
1437
1709
  ...mentioned,
1438
- everyone: false
1439
- }))Object.defineProperty(instance, key, {
1440
- value,
1441
- writable: true
1442
- });
1710
+ everyone: false,
1711
+ client,
1712
+ guild: guild ?? null
1713
+ })){
1714
+ Object.defineProperty(instance, key, {
1715
+ value,
1716
+ writable: true
1717
+ });
1718
+ }
1443
1719
  return stubDeep(instance);
1444
1720
  }
1445
1721
  /** A value as its API JSON: a builder or discord.js structure through `toJSON()`, anything else as it is. */ const jsonOf = (value)=>value && typeof value.toJSON === 'function' ? value.toJSON() : value;
@@ -1518,7 +1794,8 @@ const CHANNEL_TYPE_GUARDS = [
1518
1794
  * and `reply()` resolve to a new mock message. Without overrides the message is empty, in a server, from a new
1519
1795
  * person who is its `member`; with `guild: null` it is a direct message. Give `author` to send several messages as
1520
1796
  * one user, such as to reach a per-user cooldown. An author given, and the users, roles and channels the content
1521
- * mentions, are cached as the gateway delivers them.
1797
+ * mentions, are cached as the gateway delivers them. It is typed as discord.js emits a message, never in a group DM,
1798
+ * so it fits `messageCreate` and the other message events.
1522
1799
  *
1523
1800
  * @param overrides - The message's content, components and the rest; see {@link MockMessageOverrides}.
1524
1801
  *
@@ -1567,33 +1844,55 @@ const CHANNEL_TYPE_GUARDS = [
1567
1844
  });
1568
1845
  // The author as a member of the message's server; a direct message has none. A given author's member is the one
1569
1846
  // the server caches, as the gateway resolves it, so every message from that author has the same member
1570
- const author = overrides.author;
1571
- const member = guild ? memberFor(guild, instance.author, author !== undefined) : null;
1847
+ // The author's member, cached as the gateway caches it with the message; `member` reads the cache, as discord.js does
1848
+ if (guild) memberFor(guild, instance.author, true);
1572
1849
  Object.defineProperty(instance, 'member', {
1573
- value: member,
1574
- writable: true
1850
+ get: ()=>guild ? guild.members.resolve(instance.author) : null,
1851
+ set: (value)=>Object.defineProperty(instance, 'member', {
1852
+ value,
1853
+ writable: true,
1854
+ enumerable: true,
1855
+ configurable: true
1856
+ }),
1857
+ enumerable: true,
1858
+ configurable: true
1575
1859
  });
1576
1860
  // In a server's text channel, the ids matching the objects; with no guild, a DM
1577
1861
  instance.channelId = channel.id;
1578
1862
  instance.guildId = guild?.id ?? null;
1863
+ // The thread its channel caches under its id, as discord.js reads it; until 5.0, a placeholder thread otherwise
1864
+ let placeholderThread;
1579
1865
  Object.defineProperty(instance, 'thread', {
1580
- value: stubDeep(Object.create(ThreadChannel.prototype)),
1581
- writable: true
1866
+ get: ()=>{
1867
+ const found = cacheOf(instance.channel.threads)?.get(instance.id);
1868
+ if (found) return found;
1869
+ warnPlaceholder(mockLogger, 'Message.thread', 'it is null unless the channel caches a thread under the message id, as discord.js reads it', 'Cache one with message.channel.threads.cache.set(message.id, thread).');
1870
+ return placeholderThread ?? (placeholderThread = stubDeep(Object.create(ThreadChannel.prototype)));
1871
+ },
1872
+ set: (value)=>Object.defineProperty(instance, 'thread', {
1873
+ value,
1874
+ writable: true,
1875
+ enumerable: true,
1876
+ configurable: true
1877
+ }),
1878
+ enumerable: true,
1879
+ configurable: true
1582
1880
  });
1583
- // Assigned once by discord.js's Base constructor, so an own value, as it is on a real message
1881
+ // Assigned once by discord.js's Base constructor, so an own value, as it is on a real message; a guild the message
1882
+ // made for itself has the same client, which caches the guild
1584
1883
  const client = overrides.client ?? createMockClient();
1884
+ if (guild && overrides.guild === undefined) guild.client = client;
1585
1885
  Object.defineProperty(instance, 'client', {
1586
1886
  value: client,
1587
1887
  writable: true,
1588
1888
  configurable: true
1589
1889
  });
1890
+ if (guild instanceof Guild) cached(cacheOf(client.guilds), guild.id, ()=>guild);
1590
1891
  cacheChannel(channel, guild, client);
1591
1892
  const userCache = cacheOf(client.users);
1592
1893
  for (const user of [
1593
1894
  ...overrides.users ?? [],
1594
- ...author ? [
1595
- author
1596
- ] : []
1895
+ instance.author
1597
1896
  ])userCache?.set(user.id, user);
1598
1897
  // MessageMentions — constructor-assigned; what the content mentions, cached as the gateway delivers it
1599
1898
  instance.mentions = mentionsOf(overrides.content, client, guild);