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.
- package/CHANGELOG.md +29 -0
- package/dist/cjs/_shared/{controller.decorator-Ds7dj8uZ.cjs → controller.decorator-BywwkU9O.cjs} +8 -1
- package/dist/cjs/_shared/{providers-BSL8vml4.cjs → providers-BMMLp2SN.cjs} +1 -1
- package/dist/cjs/_shared/{sharded-cooldown-store-BGfooXEO.cjs → sharded-cooldown-store-B65iaiAy.cjs} +1 -1
- package/dist/cjs/_shared/{startup-roots-H1ssJOrz.cjs → startup-roots-lFc69AxC.cjs} +3 -3
- package/dist/cjs/_shared/{translate-error-Zwll3hC2.cjs → translate-error-BnM8FbSH.cjs} +1 -1
- package/dist/cjs/common/index.cjs +3 -3
- package/dist/cjs/core/index.cjs +5 -5
- package/dist/cjs/decorator/index.cjs +2 -2
- package/dist/cjs/testing/index.cjs +436 -101
- package/dist/cli.json +1 -1
- package/dist/esm/common/deprecation.js +7 -1
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/discord-defaults.js +43 -5
- package/dist/esm/testing/mock-fn.js +2 -6
- package/dist/esm/testing/mock-interaction.js +389 -87
- package/dist/esm/testing/response.js +3 -2
- package/dist/types/testing/index.d.cts +101 -23
- package/dist/types/testing/index.d.ts +101 -23
- package/package.json +2 -1
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require('reflect-metadata');
|
|
4
4
|
var inversify = require('inversify');
|
|
5
|
-
var providers = require('../_shared/providers-
|
|
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-
|
|
9
|
-
var startupRoots = require('../_shared/startup-roots-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
857
|
-
// undefined for a fetch of a list
|
|
858
|
-
function
|
|
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
|
|
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
|
|
879
|
-
* `timeout`, `disableCommunicationUntil` and setters to the structure, and the rest to
|
|
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
|
|
898
|
-
|
|
899
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1621
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
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
|
-
|
|
2183
|
-
|
|
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
|
-
|
|
2186
|
-
|
|
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
|
-
|
|
2193
|
-
|
|
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
|
-
|
|
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);
|