meocord 4.1.0 → 4.1.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.
@@ -2,11 +2,11 @@
2
2
 
3
3
  require('reflect-metadata');
4
4
  var inversify = require('inversify');
5
- var providers = require('../_shared/providers-BSL8vml4.cjs');
5
+ var providers = require('../_shared/providers-BMMLp2SN.cjs');
6
6
  var discord_js = require('discord.js');
7
7
  var enum_index = require('../enum/index.cjs');
8
- var controller_decorator = require('../_shared/controller.decorator-Ds7dj8uZ.cjs');
9
- var startupRoots = require('../_shared/startup-roots-H1ssJOrz.cjs');
8
+ var controller_decorator = require('../_shared/controller.decorator-BywwkU9O.cjs');
9
+ var startupRoots = require('../_shared/startup-roots-lFc69AxC.cjs');
10
10
  var node_crypto = require('node:crypto');
11
11
  require('node:async_hooks');
12
12
  require('crypto');
@@ -21,17 +21,13 @@ require('dayjs/plugin/utc.js');
21
21
  require('dayjs/plugin/timezone.js');
22
22
  require('module');
23
23
  require('chalk');
24
- require('../_shared/translate-error-Zwll3hC2.cjs');
24
+ require('../_shared/translate-error-BnM8FbSH.cjs');
25
25
 
26
26
  // Framework-agnostic mock function, so `meocord/testing` depends on neither jest nor vitest.
27
27
  // Both detect it through `_isMockFunction` and read `fn.mock.calls` in their assertions.
28
+ // Each call signature of `T`, up to four, as a union: `Parameters` and `ReturnType` read only an overloaded function's
29
+ // last signature, such as the list form of a manager's `fetch`
28
30
  /**
29
- * One call's outcome, as a mock function records it: the value it returned, or the error it threw.
30
- *
31
- * @group Testing
32
- * @category Mocks
33
- * @see {@link MockState}
34
- */ /**
35
31
  * Tells whether a value is a mock function: one from `meocord/testing`, `jest.fn()` or `vi.fn()`.
36
32
  *
37
33
  * Use it in a helper that takes mocks and real functions alike, such as one that sets a default on every mock method.
@@ -296,17 +292,18 @@ const VISIBLE = new Set([
296
292
  ...state?.history ?? []
297
293
  ];
298
294
  const sent = calls.some((call)=>VISIBLE.has(call.method) && !('error' in call));
295
+ // Each call recorded what its method received, as that method takes it
299
296
  if (state) return {
300
297
  state: state.state,
301
298
  sent,
302
- calls
299
+ calls: calls
303
300
  };
304
301
  const answered = interaction.isRepliable() ? interaction.replied : false;
305
302
  const deferred = interaction.isRepliable() ? interaction.deferred : false;
306
303
  return {
307
304
  state: answered ? 'replied' : deferred ? 'deferred' : 'unanswered',
308
305
  sent: log ? sent : answered,
309
- calls
306
+ calls: calls
310
307
  };
311
308
  }
312
309
  /**
@@ -340,6 +337,7 @@ const VISIBLE = new Set([
340
337
  }, code, 400, 'POST', 'https://discord.com/api/v10/interactions', {});
341
338
  }
342
339
 
340
+ /** A permission set with none, frozen as discord.js freezes the ones it gives. */ const noPermissions = ()=>new discord_js.PermissionsBitField().freeze();
343
341
  /** A property discord.js computes from the others: its own getter runs, reading the mock's values. */ const REAL_GETTER = Symbol('real getter');
344
342
  const snowflake = ()=>discord_js.SnowflakeUtil.generate().toString();
345
343
  /** A class and its defaults, typed together so a default that stops fitting its property fails to compile. */ const defaultsOf = (Class, defaults)=>[
@@ -383,6 +381,7 @@ const snowflake = ()=>discord_js.SnowflakeUtil.generate().toString();
383
381
  system: ()=>false,
384
382
  tag: REAL_GETTER,
385
383
  displayName: REAL_GETTER,
384
+ defaultAvatarURL: REAL_GETTER,
386
385
  createdTimestamp: REAL_GETTER,
387
386
  createdAt: REAL_GETTER
388
387
  }),
@@ -391,7 +390,12 @@ const snowflake = ()=>discord_js.SnowflakeUtil.generate().toString();
391
390
  id: REAL_GETTER,
392
391
  nickname: ()=>null,
393
392
  avatar: ()=>null,
394
- joinedTimestamp: ()=>null,
393
+ // In the server since the mock was made, as a member Discord sends always is
394
+ joinedTimestamp: ()=>Date.now(),
395
+ joinedAt: REAL_GETTER,
396
+ partial: REAL_GETTER,
397
+ premiumSince: REAL_GETTER,
398
+ communicationDisabledUntil: REAL_GETTER,
395
399
  premiumSinceTimestamp: ()=>null,
396
400
  communicationDisabledUntilTimestamp: ()=>null,
397
401
  pending: ()=>false,
@@ -404,7 +408,24 @@ const snowflake = ()=>discord_js.SnowflakeUtil.generate().toString();
404
408
  name: ()=>'role',
405
409
  // Above @everyone, which a server's role manager makes at position 0
406
410
  position: ()=>1,
407
- permissions: ()=>new discord_js.PermissionsBitField().freeze()
411
+ // Every role has a server; one made without is in one of its own, unless a guild given it takes it in
412
+ guild: ()=>createMockGuild(),
413
+ rawPosition: (role)=>role.position,
414
+ permissions: noPermissions,
415
+ hoist: ()=>false,
416
+ managed: ()=>false,
417
+ mentionable: ()=>false,
418
+ icon: ()=>null,
419
+ unicodeEmoji: ()=>null,
420
+ tags: ()=>null,
421
+ colors: ()=>({
422
+ primaryColor: 0,
423
+ secondaryColor: null,
424
+ tertiaryColor: null
425
+ }),
426
+ hexColor: REAL_GETTER,
427
+ createdTimestamp: REAL_GETTER,
428
+ createdAt: REAL_GETTER
408
429
  }),
409
430
  // A message's text says which handler it is for, so it stays the test's to give, as a command's name does
410
431
  defaultsOf(discord_js.Message, {
@@ -417,6 +438,10 @@ const snowflake = ()=>discord_js.SnowflakeUtil.generate().toString();
417
438
  nonce: ()=>null,
418
439
  position: ()=>null,
419
440
  activity: ()=>null,
441
+ reference: ()=>null,
442
+ poll: ()=>null,
443
+ interactionMetadata: ()=>null,
444
+ stickers: ()=>new discord_js.Collection(),
420
445
  url: REAL_GETTER
421
446
  }),
422
447
  // A command's name and a component's customId say which handler a call is for, so they stay the test's to give
@@ -434,7 +459,13 @@ const snowflake = ()=>discord_js.SnowflakeUtil.generate().toString();
434
459
  version: ()=>1,
435
460
  context: ()=>null,
436
461
  // What Discord sends with an interaction in a server, the member's permissions there; typed for a cached one
437
- memberPermissions: (interaction)=>interaction.guildId && interaction.member instanceof discord_js.GuildMember ? interaction.member.permissions : null
462
+ memberPermissions: (interaction)=>interaction.guildId && interaction.member instanceof discord_js.GuildMember ? interaction.member.permissions : null,
463
+ // The bot's permissions where the interaction was made: its member's in the channel, or none outside a server
464
+ appPermissions: (interaction)=>{
465
+ const { guild, channel } = interaction;
466
+ const me = guild instanceof discord_js.Guild ? guild.members.me : null;
467
+ return (me && channel instanceof discord_js.GuildChannel ? channel.permissionsFor(me) : null) ?? noPermissions();
468
+ }
438
469
  }),
439
470
  defaultsOf(discord_js.TextChannel, {
440
471
  type: ()=>discord_js.ChannelType.GuildText,
@@ -481,8 +512,12 @@ const snowflake = ()=>discord_js.SnowflakeUtil.generate().toString();
481
512
  }),
482
513
  defaultsOf(discord_js.Client, {
483
514
  readyTimestamp: ()=>Date.now(),
515
+ readyAt: REAL_GETTER,
484
516
  uptime: REAL_GETTER,
485
517
  token: ()=>null
518
+ }),
519
+ defaultsOf(discord_js.BaseGuild, {
520
+ nameAcronym: REAL_GETTER
486
521
  })
487
522
  ]);
488
523
  /** The default a mock of `target`'s class has for `key`, read from the nearest class that has one. */ function discordDefault(target, key) {
@@ -670,11 +705,14 @@ function stubDeep(instance, externalStubs, onSet) {
670
705
  const desc = Object.getOwnPropertyDescriptor(proto, key);
671
706
  if (desc !== undefined) {
672
707
  protoValue = desc.value;
708
+ if (desc.get && PLACEHOLDER_BOOLEANS.get(proto)?.has(key)) warnBooleanPlaceholder(target, key);
673
709
  break;
674
710
  }
675
711
  proto = Object.getPrototypeOf(proto);
676
712
  }
677
- const stub = typeof protoValue === 'function' ? methodStub(target, key, protoValue, ()=>proxy) : stubDeep({});
713
+ // A client the mock's structures share: its guild's, or one of its own
714
+ if (key === 'client' && target instanceof discord_js.Base) return clientOf(target);
715
+ const stub = typeof protoValue === 'function' ? isRealMethod(target, key) ? createMockFn((...args)=>protoValue.apply(proxy, args)) : methodStub(target, key, protoValue, ()=>proxy) : stubDeep({});
678
716
  stubs.set(key, stub);
679
717
  return stub;
680
718
  },
@@ -695,6 +733,164 @@ function stubDeep(instance, externalStubs, onSet) {
695
733
  });
696
734
  return proxy;
697
735
  }
736
+ // discord.js methods that read only what the mock holds, so they run for real: looking items up in a manager's cache,
737
+ // ranking roles, a member's or role's permissions in a channel, and whom a message mentions
738
+ const REAL_METHODS = new Map([
739
+ [
740
+ discord_js.DataManager.prototype,
741
+ new Set([
742
+ 'resolve',
743
+ 'resolveId'
744
+ ])
745
+ ],
746
+ [
747
+ discord_js.RoleManager.prototype,
748
+ new Set([
749
+ 'comparePositions'
750
+ ])
751
+ ],
752
+ [
753
+ discord_js.Role.prototype,
754
+ new Set([
755
+ 'comparePositionTo',
756
+ 'permissionsIn'
757
+ ])
758
+ ],
759
+ [
760
+ discord_js.GuildChannel.prototype,
761
+ new Set([
762
+ 'permissionsFor',
763
+ 'overwritesFor',
764
+ 'memberPermissions',
765
+ 'rolePermissions'
766
+ ])
767
+ ],
768
+ [
769
+ discord_js.GuildMember.prototype,
770
+ new Set([
771
+ 'permissionsIn',
772
+ 'isCommunicationDisabled'
773
+ ])
774
+ ],
775
+ [
776
+ discord_js.User.prototype,
777
+ new Set([
778
+ 'avatarURL',
779
+ 'displayAvatarURL',
780
+ 'bannerURL'
781
+ ])
782
+ ],
783
+ [
784
+ discord_js.BaseGuild.prototype,
785
+ new Set([
786
+ 'iconURL',
787
+ 'bannerURL',
788
+ 'splashURL'
789
+ ])
790
+ ],
791
+ [
792
+ discord_js.MessageMentions.prototype,
793
+ new Set([
794
+ 'has'
795
+ ])
796
+ ]
797
+ ]);
798
+ function isRealMethod(target, key) {
799
+ for(let proto = Object.getPrototypeOf(target); proto !== null; proto = Object.getPrototypeOf(proto)){
800
+ if (REAL_METHODS.get(proto)?.has(key)) return true;
801
+ }
802
+ return false;
803
+ }
804
+ // discord.js's computed booleans a mock reads as a placeholder, which is truthy: 5.0 computes them, and until then a
805
+ // test that reads one is told so once
806
+ const PLACEHOLDER_BOOLEANS = new Map([
807
+ [
808
+ discord_js.Message.prototype,
809
+ new Set([
810
+ 'editable',
811
+ 'deletable',
812
+ 'pinnable',
813
+ 'crosspostable',
814
+ 'bulkDeletable',
815
+ 'hasThread',
816
+ 'partial'
817
+ ])
818
+ ],
819
+ [
820
+ discord_js.GuildMember.prototype,
821
+ new Set([
822
+ 'manageable',
823
+ 'kickable',
824
+ 'bannable',
825
+ 'moderatable'
826
+ ])
827
+ ],
828
+ [
829
+ discord_js.Role.prototype,
830
+ new Set([
831
+ 'editable'
832
+ ])
833
+ ],
834
+ [
835
+ discord_js.GuildChannel.prototype,
836
+ new Set([
837
+ 'viewable',
838
+ 'manageable',
839
+ 'deletable'
840
+ ])
841
+ ],
842
+ [
843
+ discord_js.ThreadChannel.prototype,
844
+ new Set([
845
+ 'joinable',
846
+ 'joined',
847
+ 'sendable',
848
+ 'unarchivable',
849
+ 'editable',
850
+ 'manageable',
851
+ 'viewable'
852
+ ])
853
+ ],
854
+ [
855
+ discord_js.BaseGuildVoiceChannel.prototype,
856
+ new Set([
857
+ 'joinable'
858
+ ])
859
+ ],
860
+ [
861
+ discord_js.User.prototype,
862
+ new Set([
863
+ 'partial'
864
+ ])
865
+ ],
866
+ [
867
+ discord_js.BaseChannel.prototype,
868
+ new Set([
869
+ 'partial'
870
+ ])
871
+ ],
872
+ [
873
+ discord_js.MessageReaction.prototype,
874
+ new Set([
875
+ 'partial'
876
+ ])
877
+ ]
878
+ ]);
879
+ const mockLogger = new controller_decorator.Logger('Mocks');
880
+ function warnBooleanPlaceholder(target, key) {
881
+ const name = Object.getPrototypeOf(target).constructor.name;
882
+ const variable = name.charAt(0).toLowerCase() + name.slice(1);
883
+ controller_decorator.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.`);
884
+ }
885
+ // The client of a structure made without one: the one its guild has, else one of its own, made once
886
+ const ownClients = new WeakMap();
887
+ function clientOf(target) {
888
+ const guild = Object.getOwnPropertyDescriptor(target, 'guild')?.value;
889
+ if (guild instanceof discord_js.Guild) return guild.client;
890
+ let client = ownClients.get(target);
891
+ if (client === undefined) ownClients.set(target, client = createMockClient());
892
+ return client;
893
+ }
698
894
  // ---------------------------------------------------------------------------
699
895
  // methodStub — what an auto-stubbed discord.js method returns
700
896
  // ---------------------------------------------------------------------------
@@ -853,9 +1049,9 @@ const MANAGER_ITEMS = [
853
1049
  ]
854
1050
  ];
855
1051
  const returnsPromise = (key, method)=>method.constructor.name === 'AsyncFunction' || PROMISE_METHODS.has(key) || /^set[A-Z]/.test(key) && !SYNC_SETTERS.has(key);
856
- // The id of the one item a fetch asks for: an id, a discord.js object, or options naming one, such as `{ user: id }`;
857
- // undefined for a fetch of a list
858
- function fetchedId(args) {
1052
+ // The ids a fetch asks for: an id, a discord.js object, or options naming one or several, such as `{ user: id }` or
1053
+ // `{ user: [ids] }`; undefined for a fetch of a list
1054
+ function fetchedIds(args) {
859
1055
  const idOf = (value)=>typeof value === 'string' ? value : value instanceof discord_js.Base ? value.id : undefined;
860
1056
  const [first] = args;
861
1057
  if (typeof first !== 'object' || first === null || first instanceof discord_js.Base) return idOf(first);
@@ -867,7 +1063,9 @@ function fetchedId(args) {
867
1063
  'thread',
868
1064
  'id'
869
1065
  ]){
870
- const id = idOf(first[key]);
1066
+ const value = first[key];
1067
+ if (Array.isArray(value)) return value.map(idOf).filter((id)=>id !== undefined);
1068
+ const id = idOf(value);
871
1069
  if (id !== undefined) return id;
872
1070
  }
873
1071
  return undefined;
@@ -875,9 +1073,9 @@ function fetchedId(args) {
875
1073
  /**
876
1074
  * The mock function for a method found on a discord.js prototype. One that returns a promise in discord.js resolves:
877
1075
  * `createDM` to the user's DM channel, `send` and its kin to a message, a manager's `fetch`, `create` and `edit` to its
878
- * item (an empty collection for a list fetch), a structure's own `edit`, `fetch`, `delete`, `ban`, `pin`, `unpin`,
879
- * `timeout`, `disableCommunicationUntil` and setters to the structure, and the rest to `undefined`. Any other method
880
- * returns `undefined`.
1076
+ * item (a collection of each for a fetch of several, an empty one for a list fetch), a structure's own `edit`, `fetch`,
1077
+ * `delete`, `ban`, `pin`, `unpin`, `timeout`, `disableCommunicationUntil` and setters to the structure, and the rest to
1078
+ * `undefined`. Any other method returns `undefined`.
881
1079
  */ function methodStub(target, key, method, receiver) {
882
1080
  if (!returnsPromise(key, method)) return createMockFn();
883
1081
  // A member's DMs go through its user, and a user's through its one DM channel, as discord.js sends them
@@ -891,12 +1089,17 @@ function fetchedId(args) {
891
1089
  if (MESSAGE_METHODS.has(key)) return createMockFn(async ()=>createMockMessage());
892
1090
  if (target instanceof discord_js.BaseManager) {
893
1091
  const item = MANAGER_ITEMS.find(([Manager])=>Manager.prototype.isPrototypeOf(target))?.[1];
894
- // A fetch of one item finds it in the cache first, as discord.js does, and caches one it makes
1092
+ // A fetch of one item finds it in the cache first, as discord.js does, and caches one it makes; a fetch of
1093
+ // several does so for each
895
1094
  if (item && key === 'fetch') {
896
1095
  return createMockFn(async (...args)=>{
897
- const id = fetchedId(args);
898
- if (id === undefined) return new discord_js.Collection();
899
- return cached(cacheOf(receiver()), id, ()=>item(id, receiver()));
1096
+ const ids = fetchedIds(args);
1097
+ const one = (id)=>cached(cacheOf(receiver()), id, ()=>item(id, receiver()));
1098
+ if (Array.isArray(ids)) return new discord_js.Collection(ids.map((id)=>[
1099
+ id,
1100
+ one(id)
1101
+ ]));
1102
+ return ids === undefined ? new discord_js.Collection() : one(ids);
900
1103
  });
901
1104
  }
902
1105
  if (item && (key === 'create' || key === 'edit')) return createMockFn(async ()=>item(undefined, receiver()));
@@ -1112,54 +1315,7 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
1112
1315
  id,
1113
1316
  bot: false
1114
1317
  }));
1115
- // ---------------------------------------------------------------------------
1116
- // createMockInteraction
1117
- // ---------------------------------------------------------------------------
1118
- /**
1119
- * Creates a mock instance of a discord.js class, such as an interaction, keeping its prototype so `instanceof` holds.
1120
- *
1121
- * Use it for the interaction a test hands to `invoke` or `dispatch`. For a user, a message, a guild, a channel or a
1122
- * client, the dedicated factories fill in what discord.js would; for a type with no class, use {@link createMock}.
1123
- *
1124
- * @remarks
1125
- * Type guards such as `isButton()` run the real discord.js logic. An interaction gets an `id`, a `channelId` and a
1126
- * `user` of its own unless given, and its `locale` is `'en-US'`; without a `guildId` it is a DM, and with one its
1127
- * `member` is its user: for a `user` given, when the test gives the `guild`, the guild's cached member for that user,
1128
- * so a message and an interaction from one user in that server share it, and `memberPermissions` are that member's
1129
- * permissions (`null` in a DM). A user is a person, `bot: false`, and a member has the server's @everyone role and the
1130
- * roles {@link createMockMember} gave it; with a `guildId` but no `guild`, a server the bot isn't in, that @everyone role
1131
- * has the `guildId`. A DM sent to a member goes through its user's `send()` and the user's one DM channel. Its
1132
- * `channel` is a text channel of its server, the one its guild caches under `channelId`, or the user's DM channel. A
1133
- * `channel` given sets what the test leaves out of `channelId`, `guildId` and `guild`, as discord.js reads them from
1134
- * it: a DM channel is no server, and a server's channel its server; one in another server than the `guildId` given is
1135
- * refused, naming both. A select menu has picked nothing unless given: its
1136
- * `values` are the ids of the `users` and `members`, `roles` or `channels` given, the collections of what it picks,
1137
- * each empty unless given. Other data Discord always sends reads as
1138
- * Discord sends it, such as `false` for a flag and `null` for what may be absent; what picks the handler, `commandName`
1139
- * or `customId`, is the test's to give. Replies follow Discord's order, so a second `reply()` rejects, and
1140
- * {@link getResponse} reports every answer the interaction got. Every method is a mock function, and one that returns a
1141
- * promise in discord.js resolves.
1142
- *
1143
- * @param Class - The discord.js class to mock.
1144
- * @param props - Values for properties the class declares `readonly`; see {@link MockProps}.
1145
- *
1146
- * @example
1147
- * ```ts
1148
- * import { expect } from 'vitest'
1149
- *
1150
- * const interaction = createMockInteraction(ButtonInteraction, { customId: 'ticket/42/close' })
1151
- * expect(interaction.isButton()).toBe(true)
1152
- * await interaction.reply({ content: 'Closed.' })
1153
- * expect(interaction.replied).toBe(true)
1154
- * await expect(interaction.reply({ content: 'Again.' })).rejects.toThrow()
1155
- * ```
1156
- *
1157
- * @group Testing
1158
- * @category Mocks
1159
- * @see {@link MockProps}
1160
- * @see {@link createChatInputOptions}
1161
- * @see {@link https://meocord.dev/docs/4.1/mocks | Mocks}
1162
- */ function createMockInteraction(Class, props) {
1318
+ function createMockInteraction(Class, props) {
1163
1319
  const instance = Object.create(Class.prototype);
1164
1320
  const stubs = new Map();
1165
1321
  // Set type fields so all prototype type-guard methods compute the right value
@@ -1323,6 +1479,9 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
1323
1479
  writable: true,
1324
1480
  configurable: true
1325
1481
  });
1482
+ // The client caches the guild the interaction came from
1483
+ const guildGiven = own('guild');
1484
+ if (guildGiven instanceof discord_js.Guild) cached(cacheOf(instance.client.guilds), guildGiven.id, ()=>guildGiven);
1326
1485
  cacheOf(instance.client?.users)?.set(instance.user.id, instance.user);
1327
1486
  // A channel given says where the interaction was made, for what the test leaves unset, as discord.js reads it
1328
1487
  const given = own('channel');
@@ -1393,6 +1552,30 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
1393
1552
  configurable: true
1394
1553
  });
1395
1554
  }
1555
+ // A user context menu's target: the user with its targetId, the client's cached one or one made and cached there,
1556
+ // and in a server that user's member; each read live, so a targetId set later picks its user, and assignable
1557
+ if (discord_js.UserContextMenuCommandInteraction.prototype.isPrototypeOf(instance)) {
1558
+ if (unset('targetId')) instance.targetId = own('targetUser')?.id ?? nextSnowflake();
1559
+ const targetUser = ()=>{
1560
+ const id = own('targetId');
1561
+ return cached(cacheOf(instance.client.users), id, ()=>mockUser(id));
1562
+ };
1563
+ const live = (key, read)=>Object.defineProperty(instance, key, {
1564
+ get: read,
1565
+ set: (value)=>Object.defineProperty(instance, key, {
1566
+ value,
1567
+ writable: true,
1568
+ enumerable: true,
1569
+ configurable: true
1570
+ }),
1571
+ enumerable: true,
1572
+ configurable: true
1573
+ });
1574
+ if (unset('targetUser')) live('targetUser', targetUser);
1575
+ if (unset('targetMember')) {
1576
+ live('targetMember', ()=>own('guildId') ? memberFor(own('guild'), targetUser(), true, own('guildId')) : null);
1577
+ }
1578
+ }
1396
1579
  }
1397
1580
  // Discord sends the user's locale with every interaction, and the server's preferred one with one made in a server
1398
1581
  if (discord_js.BaseInteraction.prototype.isPrototypeOf(instance)) {
@@ -1569,7 +1752,7 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
1569
1752
  * import { expect } from 'vitest'
1570
1753
  *
1571
1754
  * const client = createMockClient()
1572
- * const botId = client.user!.id
1755
+ * const botId = client.user.id
1573
1756
  * const message = createMockMessage({ content: `<@${botId}> help`, client })
1574
1757
  * expect(message.mentions.users.has(botId)).toBe(true)
1575
1758
  * ```
@@ -1586,15 +1769,90 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
1586
1769
  // Real caches, empty until something is put in them, as a client that has just logged in
1587
1770
  instance.users = managerWith(discord_js.UserManager.prototype, undefined);
1588
1771
  instance.channels = managerWith(discord_js.ChannelManager.prototype, undefined);
1589
- instance.guilds = stubDeep(Object.create(discord_js.GuildManager.prototype));
1772
+ instance.guilds = managerWith(discord_js.GuildManager.prototype, undefined);
1590
1773
  instance.user = stubDeep(Object.assign(Object.create(discord_js.ClientUser.prototype), {
1591
1774
  id: MOCK_BOT_ID,
1592
1775
  bot: true
1593
1776
  }));
1594
1777
  instance.application = stubDeep(appInstance);
1595
- return stubDeep(instance);
1778
+ // Discord's CDN routes, which avatar and icon URLs are built from; nothing is requested
1779
+ instance.rest = stubDeep(Object.assign(Object.create(null), {
1780
+ cdn: new discord_js.CDN()
1781
+ }));
1782
+ // A client a test is given is a logged-in one
1783
+ instance.isReady = createMockFn(()=>true);
1784
+ const client = stubDeep(instance);
1785
+ for (const key of [
1786
+ 'users',
1787
+ 'channels',
1788
+ 'guilds'
1789
+ ])instance[key].client = client;
1790
+ Object.defineProperty(instance.user, 'client', {
1791
+ value: client,
1792
+ writable: true,
1793
+ configurable: true
1794
+ });
1795
+ return client;
1596
1796
  }
1597
- /** A manager whose `cache` is a real collection of `items`, by id, empty without them, and whose methods are stubs. */ function managerWith(prototype, items) {
1797
+ // What each manager holds, which discord.js's resolve() checks an item against
1798
+ const HOLDS = [
1799
+ [
1800
+ discord_js.GuildMemberManager.prototype,
1801
+ discord_js.GuildMember
1802
+ ],
1803
+ [
1804
+ discord_js.RoleManager.prototype,
1805
+ discord_js.Role
1806
+ ],
1807
+ [
1808
+ discord_js.GuildChannelManager.prototype,
1809
+ discord_js.GuildChannel
1810
+ ],
1811
+ [
1812
+ discord_js.UserManager.prototype,
1813
+ discord_js.User
1814
+ ],
1815
+ [
1816
+ discord_js.ChannelManager.prototype,
1817
+ discord_js.BaseChannel
1818
+ ],
1819
+ [
1820
+ discord_js.GuildManager.prototype,
1821
+ discord_js.Guild
1822
+ ],
1823
+ [
1824
+ discord_js.GuildBanManager.prototype,
1825
+ discord_js.GuildBan
1826
+ ],
1827
+ [
1828
+ discord_js.ThreadMemberManager.prototype,
1829
+ discord_js.ThreadMember
1830
+ ],
1831
+ [
1832
+ discord_js.GuildMessageManager.prototype,
1833
+ discord_js.Message
1834
+ ],
1835
+ [
1836
+ discord_js.DMMessageManager.prototype,
1837
+ discord_js.Message
1838
+ ],
1839
+ [
1840
+ discord_js.GuildTextThreadManager.prototype,
1841
+ discord_js.ThreadChannel
1842
+ ],
1843
+ [
1844
+ discord_js.GuildForumThreadManager.prototype,
1845
+ discord_js.ThreadChannel
1846
+ ],
1847
+ [
1848
+ discord_js.PermissionOverwriteManager.prototype,
1849
+ discord_js.PermissionOverwrites
1850
+ ]
1851
+ ];
1852
+ /**
1853
+ * A manager whose `cache` is a real collection of `items`, by id, empty without them, and whose methods are stubs but
1854
+ * for `resolve` and `resolveId`, which read the cache. Its `client` is the one its guild or channel has.
1855
+ */ function managerWith(prototype, items) {
1598
1856
  const manager = Object.create(prototype);
1599
1857
  const cache = new discord_js.Collection((items ?? []).map((item)=>[
1600
1858
  String(item.id ?? item.user?.id),
@@ -1604,6 +1862,19 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
1604
1862
  value: cache,
1605
1863
  writable: true
1606
1864
  });
1865
+ const holds = HOLDS.find(([Manager])=>Manager === prototype || Object.prototype.isPrototypeOf.call(Manager, prototype))?.[1];
1866
+ if (holds) Object.defineProperty(manager, 'holds', {
1867
+ value: holds
1868
+ });
1869
+ let own;
1870
+ Object.defineProperty(manager, 'client', {
1871
+ get: ()=>{
1872
+ const owner = manager.guild ?? manager.channel ?? manager.thread;
1873
+ return owner instanceof discord_js.Base ? owner.client : own ?? (own = createMockClient());
1874
+ },
1875
+ set: (value)=>own = value,
1876
+ configurable: true
1877
+ });
1607
1878
  return stubDeep(manager);
1608
1879
  }
1609
1880
  /**
@@ -1617,14 +1888,18 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
1617
1888
  *
1618
1889
  * @remarks
1619
1890
  * 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
1620
- * caches under that id: a member or channel in this guild, a role with that id, or a ban; `create()` and `edit()`
1621
- * resolve to a mock of its item, and a list fetch to an empty collection. Members, roles and channels given are put in
1891
+ * caches under that id: a member or channel in this guild, a role with that id, or a ban. `members.fetch({ user: ids })`
1892
+ * resolves to a collection of each of those members, found or made the same way; `create()` and `edit()` resolve to a
1893
+ * mock of its item, and a list fetch to an empty collection. Members, roles and channels given are put in
1622
1894
  * their managers' caches, where dispatch looks first when it resolves a message's typed params; a member
1623
1895
  * {@link createMockMember} made without a server is in this one. Its `roles.everyone` is the role given with the
1624
1896
  * guild's id, or else an @everyone role of its own at position 0 with no permissions, in `roles.cache` as Discord has
1625
1897
  * it. The guild is named `'Guild'` and its `preferredLocale` is `'en-US'` unless given, so `t.forGuild(guild)`
1626
1898
  * translates as for a new English server.
1627
1899
  *
1900
+ * 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
1901
+ * `guild.members.fetch.mockRejectedValue(createDiscordError(10007))`, or resolves a collection without that member.
1902
+ *
1628
1903
  * @example
1629
1904
  * ```ts
1630
1905
  * const target = createMock<GuildMember>({ id: '111111111111111111' })
@@ -1647,7 +1922,7 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
1647
1922
  instance.roles = guildRoleManager(instance.id, overrides.roles);
1648
1923
  instance.bans = managerWith(discord_js.GuildBanManager.prototype, undefined);
1649
1924
  const guild = stubDeep(instance);
1650
- homeManagers(instance, guild);
1925
+ settleGuild(instance, guild);
1651
1926
  // A member made without a server of its own is in this one
1652
1927
  for (const member of overrides.members ?? []){
1653
1928
  if (!unhomedMembers.delete(member)) continue;
@@ -1664,7 +1939,8 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
1664
1939
  * The managers the class has are ready to stub: `messages`, `threads` on text, announcement, forum and media channels,
1665
1940
  * 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.
1666
1941
  * Type guards such as `isTextBased()`, `isDMBased()` and `isThread()` run discord.js's own logic, so each answers what
1667
- * the channel is.
1942
+ * the channel is. A thread is typed as discord.js types every thread it gives, a public or a private one, so it fits
1943
+ * an interaction's `channel`, a guild's channels and the `threadCreate` event.
1668
1944
  *
1669
1945
  * @param Class - The discord.js channel class to mock.
1670
1946
  * @param props - Values for the channel's properties, such as its `id`, `name` or `topic`; see {@link MockProps}.
@@ -1708,13 +1984,16 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
1708
1984
  instance.messages = managerWith(discord_js.GuildMessageManager.prototype, undefined);
1709
1985
  instance.members = managerWith(discord_js.ThreadMemberManager.prototype, undefined);
1710
1986
  }
1987
+ // A server's channel has its overwrites, none until a test puts some in the cache, which its permissions read
1988
+ if (is(discord_js.GuildChannel) && !is(discord_js.ThreadChannel)) instance.permissionOverwrites = managerWith(discord_js.PermissionOverwriteManager.prototype, undefined);
1711
1989
  Object.assign(instance, props);
1712
1990
  const stubs = new Map();
1713
1991
  const channel = stubDeep(instance, stubs);
1714
1992
  // Each manager knows its channel, as discord.js's do, and a thread's members their thread
1715
1993
  for (const key of [
1716
1994
  'messages',
1717
- 'threads'
1995
+ 'threads',
1996
+ 'permissionOverwrites'
1718
1997
  ])if (instance[key]) instance[key].channel = channel;
1719
1998
  if (is(discord_js.ThreadChannel) && instance.members) instance.members.thread = channel;
1720
1999
  // Type guards run discord.js's own logic, which reads the channel's type and the managers it has
@@ -1974,7 +2253,7 @@ const CHANNEL_TYPE_GUARDS = [
1974
2253
  guild.roles = guildRoleManager(guild.id, undefined);
1975
2254
  guild.bans = managerWith(discord_js.GuildBanManager.prototype, undefined);
1976
2255
  const proxy = stubDeep(guild);
1977
- homeManagers(guild, proxy);
2256
+ settleGuild(guild, proxy);
1978
2257
  return proxy;
1979
2258
  }
1980
2259
  /** 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) {
@@ -1985,6 +2264,35 @@ const CHANNEL_TYPE_GUARDS = [
1985
2264
  'bans'
1986
2265
  ])instance[key].guild = guild;
1987
2266
  }
2267
+ /**
2268
+ * Makes `guild` whole, as discord.js's guilds are: its managers and the roles and channels in its caches belong to it,
2269
+ * 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
2270
+ * @everyone, made and cached once on first read.
2271
+ */ function settleGuild(instance, guild) {
2272
+ homeManagers(instance, guild);
2273
+ let client;
2274
+ Object.defineProperty(instance, 'client', {
2275
+ get: ()=>client ?? (client = createMockClient()),
2276
+ set: (value)=>client = value,
2277
+ configurable: true
2278
+ });
2279
+ const members = instance.members;
2280
+ Object.defineProperty(members, 'me', {
2281
+ get: ()=>{
2282
+ const { user } = guild.client;
2283
+ return cached(cacheOf(members), user.id, ()=>memberIn(guild, user.id, user));
2284
+ },
2285
+ configurable: true
2286
+ });
2287
+ for (const key of [
2288
+ 'roles',
2289
+ 'channels'
2290
+ ]){
2291
+ for (const item of cacheOf(instance[key])?.values() ?? []){
2292
+ if (!Object.prototype.hasOwnProperty.call(item, 'guild')) item.guild = guild;
2293
+ }
2294
+ }
2295
+ }
1988
2296
  /** The ids each kind of mention in `content` names, in order and once each. */ function mentionedIds(content) {
1989
2297
  const ids = (pattern)=>[
1990
2298
  ...new Set([
@@ -2047,11 +2355,15 @@ const CHANNEL_TYPE_GUARDS = [
2047
2355
  const instance = Object.create(discord_js.MessageMentions.prototype);
2048
2356
  for (const [key, value] of Object.entries({
2049
2357
  ...mentioned,
2050
- everyone: false
2051
- }))Object.defineProperty(instance, key, {
2052
- value,
2053
- writable: true
2054
- });
2358
+ everyone: false,
2359
+ client,
2360
+ guild: guild ?? null
2361
+ })){
2362
+ Object.defineProperty(instance, key, {
2363
+ value,
2364
+ writable: true
2365
+ });
2366
+ }
2055
2367
  return stubDeep(instance);
2056
2368
  }
2057
2369
  /** 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;
@@ -2130,7 +2442,8 @@ const CHANNEL_TYPE_GUARDS = [
2130
2442
  * and `reply()` resolve to a new mock message. Without overrides the message is empty, in a server, from a new
2131
2443
  * person who is its `member`; with `guild: null` it is a direct message. Give `author` to send several messages as
2132
2444
  * one user, such as to reach a per-user cooldown. An author given, and the users, roles and channels the content
2133
- * mentions, are cached as the gateway delivers them.
2445
+ * mentions, are cached as the gateway delivers them. It is typed as discord.js emits a message, never in a group DM,
2446
+ * so it fits `messageCreate` and the other message events.
2134
2447
  *
2135
2448
  * @param overrides - The message's content, components and the rest; see {@link MockMessageOverrides}.
2136
2449
  *
@@ -2179,33 +2492,55 @@ const CHANNEL_TYPE_GUARDS = [
2179
2492
  });
2180
2493
  // The author as a member of the message's server; a direct message has none. A given author's member is the one
2181
2494
  // the server caches, as the gateway resolves it, so every message from that author has the same member
2182
- const author = overrides.author;
2183
- const member = guild ? memberFor(guild, instance.author, author !== undefined) : null;
2495
+ // The author's member, cached as the gateway caches it with the message; `member` reads the cache, as discord.js does
2496
+ if (guild) memberFor(guild, instance.author, true);
2184
2497
  Object.defineProperty(instance, 'member', {
2185
- value: member,
2186
- writable: true
2498
+ get: ()=>guild ? guild.members.resolve(instance.author) : null,
2499
+ set: (value)=>Object.defineProperty(instance, 'member', {
2500
+ value,
2501
+ writable: true,
2502
+ enumerable: true,
2503
+ configurable: true
2504
+ }),
2505
+ enumerable: true,
2506
+ configurable: true
2187
2507
  });
2188
2508
  // In a server's text channel, the ids matching the objects; with no guild, a DM
2189
2509
  instance.channelId = channel.id;
2190
2510
  instance.guildId = guild?.id ?? null;
2511
+ // The thread its channel caches under its id, as discord.js reads it; until 5.0, a placeholder thread otherwise
2512
+ let placeholderThread;
2191
2513
  Object.defineProperty(instance, 'thread', {
2192
- value: stubDeep(Object.create(discord_js.ThreadChannel.prototype)),
2193
- writable: true
2514
+ get: ()=>{
2515
+ const found = cacheOf(instance.channel.threads)?.get(instance.id);
2516
+ if (found) return found;
2517
+ controller_decorator.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).');
2518
+ return placeholderThread ?? (placeholderThread = stubDeep(Object.create(discord_js.ThreadChannel.prototype)));
2519
+ },
2520
+ set: (value)=>Object.defineProperty(instance, 'thread', {
2521
+ value,
2522
+ writable: true,
2523
+ enumerable: true,
2524
+ configurable: true
2525
+ }),
2526
+ enumerable: true,
2527
+ configurable: true
2194
2528
  });
2195
- // Assigned once by discord.js's Base constructor, so an own value, as it is on a real message
2529
+ // Assigned once by discord.js's Base constructor, so an own value, as it is on a real message; a guild the message
2530
+ // made for itself has the same client, which caches the guild
2196
2531
  const client = overrides.client ?? createMockClient();
2532
+ if (guild && overrides.guild === undefined) guild.client = client;
2197
2533
  Object.defineProperty(instance, 'client', {
2198
2534
  value: client,
2199
2535
  writable: true,
2200
2536
  configurable: true
2201
2537
  });
2538
+ if (guild instanceof discord_js.Guild) cached(cacheOf(client.guilds), guild.id, ()=>guild);
2202
2539
  cacheChannel(channel, guild, client);
2203
2540
  const userCache = cacheOf(client.users);
2204
2541
  for (const user of [
2205
2542
  ...overrides.users ?? [],
2206
- ...author ? [
2207
- author
2208
- ] : []
2543
+ instance.author
2209
2544
  ])userCache?.set(user.id, user);
2210
2545
  // MessageMentions — constructor-assigned; what the content mentions, cached as the gateway delivers it
2211
2546
  instance.mentions = mentionsOf(overrides.content, client, guild);