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.
- package/CHANGELOG.md +35 -0
- package/README.md +23 -23
- 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 +439 -107
- 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 +392 -93
- 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
|
|
@@ -1169,13 +1325,10 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
|
|
|
1169
1325
|
instance[key] = value;
|
|
1170
1326
|
}
|
|
1171
1327
|
}
|
|
1172
|
-
//
|
|
1173
|
-
// Correct by default; overridable per test via .mockReturnValue().
|
|
1328
|
+
// Each type guard runs the real prototype implementation, as the mock's own, so a reset keeps it; a test may override it
|
|
1174
1329
|
for (const name of TYPE_GUARD_METHODS){
|
|
1175
1330
|
const method = findPrototypeMethod(instance, name);
|
|
1176
|
-
if (method !== null)
|
|
1177
|
-
stubs.set(name, createMockFn().mockImplementation(()=>method.call(instance)));
|
|
1178
|
-
}
|
|
1331
|
+
if (method !== null) stubs.set(name, createMockFn(()=>method.call(instance)));
|
|
1179
1332
|
}
|
|
1180
1333
|
// Guild checks read the mock's own data, since discord.js resolves `guild` through a client the
|
|
1181
1334
|
// mock lacks: a guildId is a guild, a guild object with it a cached one, neither a DM. A member
|
|
@@ -1188,7 +1341,7 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
|
|
|
1188
1341
|
inRawGuild: ()=>Boolean(own('guildId') && !own('guild') && hasMember())
|
|
1189
1342
|
};
|
|
1190
1343
|
for (const [name, check] of Object.entries(guildChecks)){
|
|
1191
|
-
if (findPrototypeMethod(instance, name) !== null) stubs.set(name, createMockFn(
|
|
1344
|
+
if (findPrototypeMethod(instance, name) !== null) stubs.set(name, createMockFn(check));
|
|
1192
1345
|
}
|
|
1193
1346
|
// Set up reply state machine for repliable interactions
|
|
1194
1347
|
const isRepliableMethod = findPrototypeMethod(instance, 'isRepliable');
|
|
@@ -1323,6 +1476,9 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
|
|
|
1323
1476
|
writable: true,
|
|
1324
1477
|
configurable: true
|
|
1325
1478
|
});
|
|
1479
|
+
// The client caches the guild the interaction came from
|
|
1480
|
+
const guildGiven = own('guild');
|
|
1481
|
+
if (guildGiven instanceof discord_js.Guild) cached(cacheOf(instance.client.guilds), guildGiven.id, ()=>guildGiven);
|
|
1326
1482
|
cacheOf(instance.client?.users)?.set(instance.user.id, instance.user);
|
|
1327
1483
|
// A channel given says where the interaction was made, for what the test leaves unset, as discord.js reads it
|
|
1328
1484
|
const given = own('channel');
|
|
@@ -1393,6 +1549,30 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
|
|
|
1393
1549
|
configurable: true
|
|
1394
1550
|
});
|
|
1395
1551
|
}
|
|
1552
|
+
// A user context menu's target: the user with its targetId, the client's cached one or one made and cached there,
|
|
1553
|
+
// and in a server that user's member; each read live, so a targetId set later picks its user, and assignable
|
|
1554
|
+
if (discord_js.UserContextMenuCommandInteraction.prototype.isPrototypeOf(instance)) {
|
|
1555
|
+
if (unset('targetId')) instance.targetId = own('targetUser')?.id ?? nextSnowflake();
|
|
1556
|
+
const targetUser = ()=>{
|
|
1557
|
+
const id = own('targetId');
|
|
1558
|
+
return cached(cacheOf(instance.client.users), id, ()=>mockUser(id));
|
|
1559
|
+
};
|
|
1560
|
+
const live = (key, read)=>Object.defineProperty(instance, key, {
|
|
1561
|
+
get: read,
|
|
1562
|
+
set: (value)=>Object.defineProperty(instance, key, {
|
|
1563
|
+
value,
|
|
1564
|
+
writable: true,
|
|
1565
|
+
enumerable: true,
|
|
1566
|
+
configurable: true
|
|
1567
|
+
}),
|
|
1568
|
+
enumerable: true,
|
|
1569
|
+
configurable: true
|
|
1570
|
+
});
|
|
1571
|
+
if (unset('targetUser')) live('targetUser', targetUser);
|
|
1572
|
+
if (unset('targetMember')) {
|
|
1573
|
+
live('targetMember', ()=>own('guildId') ? memberFor(own('guild'), targetUser(), true, own('guildId')) : null);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1396
1576
|
}
|
|
1397
1577
|
// Discord sends the user's locale with every interaction, and the server's preferred one with one made in a server
|
|
1398
1578
|
if (discord_js.BaseInteraction.prototype.isPrototypeOf(instance)) {
|
|
@@ -1569,7 +1749,7 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
|
|
|
1569
1749
|
* import { expect } from 'vitest'
|
|
1570
1750
|
*
|
|
1571
1751
|
* const client = createMockClient()
|
|
1572
|
-
* const botId = client.user
|
|
1752
|
+
* const botId = client.user.id
|
|
1573
1753
|
* const message = createMockMessage({ content: `<@${botId}> help`, client })
|
|
1574
1754
|
* expect(message.mentions.users.has(botId)).toBe(true)
|
|
1575
1755
|
* ```
|
|
@@ -1586,15 +1766,90 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
|
|
|
1586
1766
|
// Real caches, empty until something is put in them, as a client that has just logged in
|
|
1587
1767
|
instance.users = managerWith(discord_js.UserManager.prototype, undefined);
|
|
1588
1768
|
instance.channels = managerWith(discord_js.ChannelManager.prototype, undefined);
|
|
1589
|
-
instance.guilds =
|
|
1769
|
+
instance.guilds = managerWith(discord_js.GuildManager.prototype, undefined);
|
|
1590
1770
|
instance.user = stubDeep(Object.assign(Object.create(discord_js.ClientUser.prototype), {
|
|
1591
1771
|
id: MOCK_BOT_ID,
|
|
1592
1772
|
bot: true
|
|
1593
1773
|
}));
|
|
1594
1774
|
instance.application = stubDeep(appInstance);
|
|
1595
|
-
|
|
1775
|
+
// Discord's CDN routes, which avatar and icon URLs are built from; nothing is requested
|
|
1776
|
+
instance.rest = stubDeep(Object.assign(Object.create(null), {
|
|
1777
|
+
cdn: new discord_js.CDN()
|
|
1778
|
+
}));
|
|
1779
|
+
// A client a test is given is a logged-in one
|
|
1780
|
+
instance.isReady = createMockFn(()=>true);
|
|
1781
|
+
const client = stubDeep(instance);
|
|
1782
|
+
for (const key of [
|
|
1783
|
+
'users',
|
|
1784
|
+
'channels',
|
|
1785
|
+
'guilds'
|
|
1786
|
+
])instance[key].client = client;
|
|
1787
|
+
Object.defineProperty(instance.user, 'client', {
|
|
1788
|
+
value: client,
|
|
1789
|
+
writable: true,
|
|
1790
|
+
configurable: true
|
|
1791
|
+
});
|
|
1792
|
+
return client;
|
|
1596
1793
|
}
|
|
1597
|
-
|
|
1794
|
+
// What each manager holds, which discord.js's resolve() checks an item against
|
|
1795
|
+
const HOLDS = [
|
|
1796
|
+
[
|
|
1797
|
+
discord_js.GuildMemberManager.prototype,
|
|
1798
|
+
discord_js.GuildMember
|
|
1799
|
+
],
|
|
1800
|
+
[
|
|
1801
|
+
discord_js.RoleManager.prototype,
|
|
1802
|
+
discord_js.Role
|
|
1803
|
+
],
|
|
1804
|
+
[
|
|
1805
|
+
discord_js.GuildChannelManager.prototype,
|
|
1806
|
+
discord_js.GuildChannel
|
|
1807
|
+
],
|
|
1808
|
+
[
|
|
1809
|
+
discord_js.UserManager.prototype,
|
|
1810
|
+
discord_js.User
|
|
1811
|
+
],
|
|
1812
|
+
[
|
|
1813
|
+
discord_js.ChannelManager.prototype,
|
|
1814
|
+
discord_js.BaseChannel
|
|
1815
|
+
],
|
|
1816
|
+
[
|
|
1817
|
+
discord_js.GuildManager.prototype,
|
|
1818
|
+
discord_js.Guild
|
|
1819
|
+
],
|
|
1820
|
+
[
|
|
1821
|
+
discord_js.GuildBanManager.prototype,
|
|
1822
|
+
discord_js.GuildBan
|
|
1823
|
+
],
|
|
1824
|
+
[
|
|
1825
|
+
discord_js.ThreadMemberManager.prototype,
|
|
1826
|
+
discord_js.ThreadMember
|
|
1827
|
+
],
|
|
1828
|
+
[
|
|
1829
|
+
discord_js.GuildMessageManager.prototype,
|
|
1830
|
+
discord_js.Message
|
|
1831
|
+
],
|
|
1832
|
+
[
|
|
1833
|
+
discord_js.DMMessageManager.prototype,
|
|
1834
|
+
discord_js.Message
|
|
1835
|
+
],
|
|
1836
|
+
[
|
|
1837
|
+
discord_js.GuildTextThreadManager.prototype,
|
|
1838
|
+
discord_js.ThreadChannel
|
|
1839
|
+
],
|
|
1840
|
+
[
|
|
1841
|
+
discord_js.GuildForumThreadManager.prototype,
|
|
1842
|
+
discord_js.ThreadChannel
|
|
1843
|
+
],
|
|
1844
|
+
[
|
|
1845
|
+
discord_js.PermissionOverwriteManager.prototype,
|
|
1846
|
+
discord_js.PermissionOverwrites
|
|
1847
|
+
]
|
|
1848
|
+
];
|
|
1849
|
+
/**
|
|
1850
|
+
* A manager whose `cache` is a real collection of `items`, by id, empty without them, and whose methods are stubs but
|
|
1851
|
+
* for `resolve` and `resolveId`, which read the cache. Its `client` is the one its guild or channel has.
|
|
1852
|
+
*/ function managerWith(prototype, items) {
|
|
1598
1853
|
const manager = Object.create(prototype);
|
|
1599
1854
|
const cache = new discord_js.Collection((items ?? []).map((item)=>[
|
|
1600
1855
|
String(item.id ?? item.user?.id),
|
|
@@ -1604,6 +1859,19 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
|
|
|
1604
1859
|
value: cache,
|
|
1605
1860
|
writable: true
|
|
1606
1861
|
});
|
|
1862
|
+
const holds = HOLDS.find(([Manager])=>Manager === prototype || Object.prototype.isPrototypeOf.call(Manager, prototype))?.[1];
|
|
1863
|
+
if (holds) Object.defineProperty(manager, 'holds', {
|
|
1864
|
+
value: holds
|
|
1865
|
+
});
|
|
1866
|
+
let own;
|
|
1867
|
+
Object.defineProperty(manager, 'client', {
|
|
1868
|
+
get: ()=>{
|
|
1869
|
+
const owner = manager.guild ?? manager.channel ?? manager.thread;
|
|
1870
|
+
return owner instanceof discord_js.Base ? owner.client : own ?? (own = createMockClient());
|
|
1871
|
+
},
|
|
1872
|
+
set: (value)=>own = value,
|
|
1873
|
+
configurable: true
|
|
1874
|
+
});
|
|
1607
1875
|
return stubDeep(manager);
|
|
1608
1876
|
}
|
|
1609
1877
|
/**
|
|
@@ -1617,14 +1885,18 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
|
|
|
1617
1885
|
*
|
|
1618
1886
|
* @remarks
|
|
1619
1887
|
* 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
|
-
*
|
|
1888
|
+
* caches under that id: a member or channel in this guild, a role with that id, or a ban. `members.fetch({ user: ids })`
|
|
1889
|
+
* resolves to a collection of each of those members, found or made the same way; `create()` and `edit()` resolve to a
|
|
1890
|
+
* mock of its item, and a list fetch to an empty collection. Members, roles and channels given are put in
|
|
1622
1891
|
* their managers' caches, where dispatch looks first when it resolves a message's typed params; a member
|
|
1623
1892
|
* {@link createMockMember} made without a server is in this one. Its `roles.everyone` is the role given with the
|
|
1624
1893
|
* guild's id, or else an @everyone role of its own at position 0 with no permissions, in `roles.cache` as Discord has
|
|
1625
1894
|
* it. The guild is named `'Guild'` and its `preferredLocale` is `'en-US'` unless given, so `t.forGuild(guild)`
|
|
1626
1895
|
* translates as for a new English server.
|
|
1627
1896
|
*
|
|
1897
|
+
* 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
|
|
1898
|
+
* `guild.members.fetch.mockRejectedValue(createDiscordError(10007))`, or resolves a collection without that member.
|
|
1899
|
+
*
|
|
1628
1900
|
* @example
|
|
1629
1901
|
* ```ts
|
|
1630
1902
|
* const target = createMock<GuildMember>({ id: '111111111111111111' })
|
|
@@ -1647,7 +1919,7 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
|
|
|
1647
1919
|
instance.roles = guildRoleManager(instance.id, overrides.roles);
|
|
1648
1920
|
instance.bans = managerWith(discord_js.GuildBanManager.prototype, undefined);
|
|
1649
1921
|
const guild = stubDeep(instance);
|
|
1650
|
-
|
|
1922
|
+
settleGuild(instance, guild);
|
|
1651
1923
|
// A member made without a server of its own is in this one
|
|
1652
1924
|
for (const member of overrides.members ?? []){
|
|
1653
1925
|
if (!unhomedMembers.delete(member)) continue;
|
|
@@ -1664,7 +1936,8 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
|
|
|
1664
1936
|
* The managers the class has are ready to stub: `messages`, `threads` on text, announcement, forum and media channels,
|
|
1665
1937
|
* 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
1938
|
* Type guards such as `isTextBased()`, `isDMBased()` and `isThread()` run discord.js's own logic, so each answers what
|
|
1667
|
-
* the channel is.
|
|
1939
|
+
* the channel is. A thread is typed as discord.js types every thread it gives, a public or a private one, so it fits
|
|
1940
|
+
* an interaction's `channel`, a guild's channels and the `threadCreate` event.
|
|
1668
1941
|
*
|
|
1669
1942
|
* @param Class - The discord.js channel class to mock.
|
|
1670
1943
|
* @param props - Values for the channel's properties, such as its `id`, `name` or `topic`; see {@link MockProps}.
|
|
@@ -1708,13 +1981,16 @@ const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
|
|
|
1708
1981
|
instance.messages = managerWith(discord_js.GuildMessageManager.prototype, undefined);
|
|
1709
1982
|
instance.members = managerWith(discord_js.ThreadMemberManager.prototype, undefined);
|
|
1710
1983
|
}
|
|
1984
|
+
// A server's channel has its overwrites, none until a test puts some in the cache, which its permissions read
|
|
1985
|
+
if (is(discord_js.GuildChannel) && !is(discord_js.ThreadChannel)) instance.permissionOverwrites = managerWith(discord_js.PermissionOverwriteManager.prototype, undefined);
|
|
1711
1986
|
Object.assign(instance, props);
|
|
1712
1987
|
const stubs = new Map();
|
|
1713
1988
|
const channel = stubDeep(instance, stubs);
|
|
1714
1989
|
// Each manager knows its channel, as discord.js's do, and a thread's members their thread
|
|
1715
1990
|
for (const key of [
|
|
1716
1991
|
'messages',
|
|
1717
|
-
'threads'
|
|
1992
|
+
'threads',
|
|
1993
|
+
'permissionOverwrites'
|
|
1718
1994
|
])if (instance[key]) instance[key].channel = channel;
|
|
1719
1995
|
if (is(discord_js.ThreadChannel) && instance.members) instance.members.thread = channel;
|
|
1720
1996
|
// Type guards run discord.js's own logic, which reads the channel's type and the managers it has
|
|
@@ -1974,7 +2250,7 @@ const CHANNEL_TYPE_GUARDS = [
|
|
|
1974
2250
|
guild.roles = guildRoleManager(guild.id, undefined);
|
|
1975
2251
|
guild.bans = managerWith(discord_js.GuildBanManager.prototype, undefined);
|
|
1976
2252
|
const proxy = stubDeep(guild);
|
|
1977
|
-
|
|
2253
|
+
settleGuild(guild, proxy);
|
|
1978
2254
|
return proxy;
|
|
1979
2255
|
}
|
|
1980
2256
|
/** 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 +2261,35 @@ const CHANNEL_TYPE_GUARDS = [
|
|
|
1985
2261
|
'bans'
|
|
1986
2262
|
])instance[key].guild = guild;
|
|
1987
2263
|
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Makes `guild` whole, as discord.js's guilds are: its managers and the roles and channels in its caches belong to it,
|
|
2266
|
+
* 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
|
|
2267
|
+
* @everyone, made and cached once on first read.
|
|
2268
|
+
*/ function settleGuild(instance, guild) {
|
|
2269
|
+
homeManagers(instance, guild);
|
|
2270
|
+
let client;
|
|
2271
|
+
Object.defineProperty(instance, 'client', {
|
|
2272
|
+
get: ()=>client ?? (client = createMockClient()),
|
|
2273
|
+
set: (value)=>client = value,
|
|
2274
|
+
configurable: true
|
|
2275
|
+
});
|
|
2276
|
+
const members = instance.members;
|
|
2277
|
+
Object.defineProperty(members, 'me', {
|
|
2278
|
+
get: ()=>{
|
|
2279
|
+
const { user } = guild.client;
|
|
2280
|
+
return cached(cacheOf(members), user.id, ()=>memberIn(guild, user.id, user));
|
|
2281
|
+
},
|
|
2282
|
+
configurable: true
|
|
2283
|
+
});
|
|
2284
|
+
for (const key of [
|
|
2285
|
+
'roles',
|
|
2286
|
+
'channels'
|
|
2287
|
+
]){
|
|
2288
|
+
for (const item of cacheOf(instance[key])?.values() ?? []){
|
|
2289
|
+
if (!Object.prototype.hasOwnProperty.call(item, 'guild')) item.guild = guild;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
1988
2293
|
/** The ids each kind of mention in `content` names, in order and once each. */ function mentionedIds(content) {
|
|
1989
2294
|
const ids = (pattern)=>[
|
|
1990
2295
|
...new Set([
|
|
@@ -2047,11 +2352,15 @@ const CHANNEL_TYPE_GUARDS = [
|
|
|
2047
2352
|
const instance = Object.create(discord_js.MessageMentions.prototype);
|
|
2048
2353
|
for (const [key, value] of Object.entries({
|
|
2049
2354
|
...mentioned,
|
|
2050
|
-
everyone: false
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2355
|
+
everyone: false,
|
|
2356
|
+
client,
|
|
2357
|
+
guild: guild ?? null
|
|
2358
|
+
})){
|
|
2359
|
+
Object.defineProperty(instance, key, {
|
|
2360
|
+
value,
|
|
2361
|
+
writable: true
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2055
2364
|
return stubDeep(instance);
|
|
2056
2365
|
}
|
|
2057
2366
|
/** 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 +2439,8 @@ const CHANNEL_TYPE_GUARDS = [
|
|
|
2130
2439
|
* and `reply()` resolve to a new mock message. Without overrides the message is empty, in a server, from a new
|
|
2131
2440
|
* person who is its `member`; with `guild: null` it is a direct message. Give `author` to send several messages as
|
|
2132
2441
|
* 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.
|
|
2442
|
+
* mentions, are cached as the gateway delivers them. It is typed as discord.js emits a message, never in a group DM,
|
|
2443
|
+
* so it fits `messageCreate` and the other message events.
|
|
2134
2444
|
*
|
|
2135
2445
|
* @param overrides - The message's content, components and the rest; see {@link MockMessageOverrides}.
|
|
2136
2446
|
*
|
|
@@ -2179,33 +2489,55 @@ const CHANNEL_TYPE_GUARDS = [
|
|
|
2179
2489
|
});
|
|
2180
2490
|
// The author as a member of the message's server; a direct message has none. A given author's member is the one
|
|
2181
2491
|
// the server caches, as the gateway resolves it, so every message from that author has the same member
|
|
2182
|
-
|
|
2183
|
-
|
|
2492
|
+
// The author's member, cached as the gateway caches it with the message; `member` reads the cache, as discord.js does
|
|
2493
|
+
if (guild) memberFor(guild, instance.author, true);
|
|
2184
2494
|
Object.defineProperty(instance, 'member', {
|
|
2185
|
-
|
|
2186
|
-
|
|
2495
|
+
get: ()=>guild ? guild.members.resolve(instance.author) : null,
|
|
2496
|
+
set: (value)=>Object.defineProperty(instance, 'member', {
|
|
2497
|
+
value,
|
|
2498
|
+
writable: true,
|
|
2499
|
+
enumerable: true,
|
|
2500
|
+
configurable: true
|
|
2501
|
+
}),
|
|
2502
|
+
enumerable: true,
|
|
2503
|
+
configurable: true
|
|
2187
2504
|
});
|
|
2188
2505
|
// In a server's text channel, the ids matching the objects; with no guild, a DM
|
|
2189
2506
|
instance.channelId = channel.id;
|
|
2190
2507
|
instance.guildId = guild?.id ?? null;
|
|
2508
|
+
// The thread its channel caches under its id, as discord.js reads it; until 5.0, a placeholder thread otherwise
|
|
2509
|
+
let placeholderThread;
|
|
2191
2510
|
Object.defineProperty(instance, 'thread', {
|
|
2192
|
-
|
|
2193
|
-
|
|
2511
|
+
get: ()=>{
|
|
2512
|
+
const found = cacheOf(instance.channel.threads)?.get(instance.id);
|
|
2513
|
+
if (found) return found;
|
|
2514
|
+
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).');
|
|
2515
|
+
return placeholderThread ?? (placeholderThread = stubDeep(Object.create(discord_js.ThreadChannel.prototype)));
|
|
2516
|
+
},
|
|
2517
|
+
set: (value)=>Object.defineProperty(instance, 'thread', {
|
|
2518
|
+
value,
|
|
2519
|
+
writable: true,
|
|
2520
|
+
enumerable: true,
|
|
2521
|
+
configurable: true
|
|
2522
|
+
}),
|
|
2523
|
+
enumerable: true,
|
|
2524
|
+
configurable: true
|
|
2194
2525
|
});
|
|
2195
|
-
// Assigned once by discord.js's Base constructor, so an own value, as it is on a real message
|
|
2526
|
+
// Assigned once by discord.js's Base constructor, so an own value, as it is on a real message; a guild the message
|
|
2527
|
+
// made for itself has the same client, which caches the guild
|
|
2196
2528
|
const client = overrides.client ?? createMockClient();
|
|
2529
|
+
if (guild && overrides.guild === undefined) guild.client = client;
|
|
2197
2530
|
Object.defineProperty(instance, 'client', {
|
|
2198
2531
|
value: client,
|
|
2199
2532
|
writable: true,
|
|
2200
2533
|
configurable: true
|
|
2201
2534
|
});
|
|
2535
|
+
if (guild instanceof discord_js.Guild) cached(cacheOf(client.guilds), guild.id, ()=>guild);
|
|
2202
2536
|
cacheChannel(channel, guild, client);
|
|
2203
2537
|
const userCache = cacheOf(client.users);
|
|
2204
2538
|
for (const user of [
|
|
2205
2539
|
...overrides.users ?? [],
|
|
2206
|
-
|
|
2207
|
-
author
|
|
2208
|
-
] : []
|
|
2540
|
+
instance.author
|
|
2209
2541
|
])userCache?.set(user.id, user);
|
|
2210
2542
|
// MessageMentions — constructor-assigned; what the content mentions, cached as the gateway delivers it
|
|
2211
2543
|
instance.mentions = mentionsOf(overrides.content, client, guild);
|