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