meocord 4.2.1 → 4.2.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 +58 -0
- package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-U2NBWcoU.cjs} +45 -13
- package/dist/cjs/_shared/{providers-BZgJyfZS.cjs → providers-B4MOFBrW.cjs} +58 -15
- package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-HTUb3Ii_.cjs} +1 -1
- package/dist/cjs/_shared/{startup-roots-CgfPQrNl.cjs → startup-roots-lA5xkBFq.cjs} +12 -11
- package/dist/cjs/_shared/{translate-error-BUAR1WYG.cjs → translate-error-CbBHUu5P.cjs} +1 -1
- package/dist/cjs/common/index.cjs +3 -3
- package/dist/cjs/core/index.cjs +8 -6
- package/dist/cjs/decorator/index.cjs +33 -3
- package/dist/cjs/testing/index.cjs +295 -60
- package/dist/cli.json +1 -1
- package/dist/esm/common/response/response-state.js +5 -5
- package/dist/esm/common/translator.js +15 -5
- package/dist/esm/core/dispatcher.js +9 -8
- package/dist/esm/core/filter-runner.js +2 -2
- package/dist/esm/core/handler-pipeline.js +10 -4
- package/dist/esm/core/meocord-factory.js +4 -2
- package/dist/esm/core/message-help.js +32 -6
- package/dist/esm/core/observer-runner.js +2 -1
- package/dist/esm/core/providers.js +12 -2
- package/dist/esm/core/stage-scope.js +2 -1
- package/dist/esm/core/theme-scope.js +4 -1
- package/dist/esm/decorator/app.decorator.js +31 -0
- package/dist/esm/decorator/filter.decorator.js +2 -2
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +4 -13
- package/dist/esm/testing/discord-defaults.js +21 -1
- package/dist/esm/testing/meocord-testing-module.js +24 -16
- package/dist/esm/testing/mock-interaction.js +242 -28
- package/dist/esm/testing/strict-mocks.js +7 -4
- package/dist/esm/util/value.util.js +19 -2
- package/dist/types/common/index.d.cts +4 -1
- package/dist/types/common/index.d.ts +4 -1
- package/dist/types/testing/index.d.cts +28 -14
- package/dist/types/testing/index.d.ts +28 -14
- package/package.json +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
import { createMockFn, usesRunnerMockFn } from './mock-fn.js';
|
|
3
|
-
import { DataManager, RoleManager, Role, GuildChannel, GuildMember, User, BaseGuild, MessageMentions, ThreadChannel, Message, BaseGuildVoiceChannel, VoiceChannel, BaseChannel, MessageReaction, InteractionType, ComponentType, ApplicationCommandType, GuildMemberManager, GuildChannelManager, UserManager, ChannelManager, GuildManager, Guild, GuildBanManager, GuildBan, ThreadMemberManager, ThreadMember, GuildMessageManager, DMMessageManager, GuildTextThreadManager, GuildForumThreadManager, PermissionOverwriteManager, PermissionOverwrites, ReactionManager, ReactionUserManager, PresenceManager, Presence, CommandInteractionOptionResolver, ApplicationCommandOptionType, Client, ApplicationCommandManager, ClientUser, CDN, DiscordjsErrorCodes, AuthorizingIntegrationOwners, BaseInteraction, UserContextMenuCommandInteraction, MessageContextMenuCommandInteraction, Attachment, Locale, StringSelectMenuInteraction, UserSelectMenuInteraction, RoleSelectMenuInteraction, ChannelSelectMenuInteraction, MentionableSelectMenuInteraction, Collection, PermissionsBitField, Base, SnowflakeUtil, DiscordjsError, DMChannel, GuildMemberRoleManager, MessageFlagsBitField, BaseManager, TextChannel, Component, Embed, ActionRow, ApplicationCommand, NewsChannel, ForumChannel, MediaChannel, DiscordjsTypeError, MessageFlags } from 'discord.js';
|
|
3
|
+
import { DataManager, RoleManager, Role, GuildChannel, GuildMember, User, BaseGuild, MessageMentions, ThreadChannel, Message, BaseGuildVoiceChannel, VoiceChannel, BaseChannel, MessageReaction, InteractionType, ComponentType, ApplicationCommandType, GuildMemberManager, GuildChannelManager, UserManager, ChannelManager, GuildManager, Guild, GuildBanManager, GuildBan, ThreadMemberManager, ThreadMember, GuildMessageManager, DMMessageManager, GuildTextThreadManager, GuildForumThreadManager, PermissionOverwriteManager, PermissionOverwrites, ReactionManager, ReactionUserManager, PresenceManager, Presence, GuildEmojiManager, GuildEmoji, GuildStickerManager, Sticker, GuildScheduledEventManager, GuildScheduledEvent, AutoModerationRuleManager, AutoModerationRule, GuildInviteManager, Invite, CommandInteractionOptionResolver, ApplicationCommandOptionType, Client, ApplicationCommandManager, ClientUser, CDN, DiscordjsErrorCodes, AuthorizingIntegrationOwners, BaseInteraction, UserContextMenuCommandInteraction, MessageContextMenuCommandInteraction, Attachment, Locale, StringSelectMenuInteraction, UserSelectMenuInteraction, RoleSelectMenuInteraction, ChannelSelectMenuInteraction, MentionableSelectMenuInteraction, Collection, PermissionsBitField, Base, SnowflakeUtil, DiscordjsError, DMChannel, GuildMemberRoleManager, MessageFlagsBitField, BaseManager, TextChannel, Component, Embed, ActionRow, ApplicationCommand, NewsChannel, ForumChannel, MediaChannel, DiscordjsTypeError, MessageFlags } 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, placeholderValue } from './discord-defaults.js';
|
|
7
7
|
import { Logger } from '../common/logger.js';
|
|
8
|
-
import {
|
|
8
|
+
import { warnPlaceholder, warnOnce } from '../common/deprecation.js';
|
|
9
9
|
import { embedsAsDiscordStores, asDiscordStores } from './discord-shape.js';
|
|
10
10
|
import { nextSnowflake, MOCK_BOT_ID } from './snowflake.js';
|
|
11
11
|
import { strictMocks, noteMockMade } from './strict-mocks.js';
|
|
@@ -501,8 +501,49 @@ const MANAGER_ITEMS = [
|
|
|
501
501
|
guild: guildOf(manager)
|
|
502
502
|
} : {}
|
|
503
503
|
})
|
|
504
|
+
],
|
|
505
|
+
[
|
|
506
|
+
GuildEmojiManager,
|
|
507
|
+
(id, manager)=>itemInGuild(GuildEmoji, {
|
|
508
|
+
id: id ?? nextSnowflake()
|
|
509
|
+
}, manager)
|
|
510
|
+
],
|
|
511
|
+
[
|
|
512
|
+
GuildStickerManager,
|
|
513
|
+
(id, manager)=>itemInGuild(Sticker, {
|
|
514
|
+
id: id ?? nextSnowflake()
|
|
515
|
+
}, manager)
|
|
516
|
+
],
|
|
517
|
+
[
|
|
518
|
+
GuildScheduledEventManager,
|
|
519
|
+
(id, manager)=>itemInGuild(GuildScheduledEvent, {
|
|
520
|
+
id: id ?? nextSnowflake()
|
|
521
|
+
}, manager)
|
|
522
|
+
],
|
|
523
|
+
[
|
|
524
|
+
AutoModerationRuleManager,
|
|
525
|
+
(id, manager)=>itemInGuild(AutoModerationRule, {
|
|
526
|
+
id: id ?? nextSnowflake()
|
|
527
|
+
}, manager)
|
|
528
|
+
],
|
|
529
|
+
// An invite is keyed by its code, as discord.js caches and fetches one
|
|
530
|
+
[
|
|
531
|
+
GuildInviteManager,
|
|
532
|
+
(code, manager)=>itemInGuild(Invite, {
|
|
533
|
+
code: code ?? BigInt(nextSnowflake()).toString(36)
|
|
534
|
+
}, manager)
|
|
504
535
|
]
|
|
505
536
|
];
|
|
537
|
+
/** A mock of `Class` with `props`, in the manager's guild, by the guild and its id, when the manager has one. */ function itemInGuild(Class, props, manager) {
|
|
538
|
+
const guild = guildOf(manager);
|
|
539
|
+
return createMockInteraction(Class, {
|
|
540
|
+
...props,
|
|
541
|
+
...guild ? {
|
|
542
|
+
guild,
|
|
543
|
+
guildId: guild.id
|
|
544
|
+
} : {}
|
|
545
|
+
});
|
|
546
|
+
}
|
|
506
547
|
const returnsPromise = (key, method)=>method.constructor.name === 'AsyncFunction' || PROMISE_METHODS.has(key) || /^set[A-Z]/.test(key) && !SYNC_SETTERS.has(key);
|
|
507
548
|
// The ids a fetch asks for: an id, a discord.js object, or options naming one or several, such as `{ user: id }` or
|
|
508
549
|
// `{ user: [ids] }`; undefined for a fetch of a list
|
|
@@ -576,6 +617,18 @@ function fetchedIds(args) {
|
|
|
576
617
|
return typeof user === 'string' ? user : user?.id;
|
|
577
618
|
});
|
|
578
619
|
}
|
|
620
|
+
// An invite is made for the channel given, in its guild, and cached nowhere, as discord.js's create does
|
|
621
|
+
if (item && key === 'create' && target instanceof GuildInviteManager) {
|
|
622
|
+
return createMockFn(async (channel)=>{
|
|
623
|
+
const invite = item(undefined, receiver());
|
|
624
|
+
const channelId = typeof channel === 'string' ? channel : channel?.id;
|
|
625
|
+
const found = channel instanceof BaseChannel ? channel : channelId ? cacheOf(guildOf(receiver())?.channels)?.get(channelId) ?? null : null;
|
|
626
|
+
return Object.assign(invite, {
|
|
627
|
+
channel: found,
|
|
628
|
+
channelId: channelId ?? null
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
}
|
|
579
632
|
// Every other manager here caches what its create() makes, as discord.js's does; check a manager's create when
|
|
580
633
|
// adding it to MANAGER_ITEMS
|
|
581
634
|
if (item && key === 'create') {
|
|
@@ -792,24 +845,36 @@ const isSnowflake = (id)=>typeof id === 'string' && /^\d{1,20}$/.test(id);
|
|
|
792
845
|
};
|
|
793
846
|
stampCall(call);
|
|
794
847
|
log.push(call);
|
|
795
|
-
let
|
|
848
|
+
let gated;
|
|
796
849
|
try {
|
|
797
|
-
// Before the behaviour, as discord.js
|
|
798
|
-
if (gate)
|
|
850
|
+
// Before the behaviour, as discord.js checks the state before it sends an answer
|
|
851
|
+
if (gate) gated = gate(args, byTest);
|
|
799
852
|
} catch (error) {
|
|
800
853
|
call.error = error;
|
|
801
854
|
return Promise.reject(error);
|
|
802
855
|
}
|
|
803
856
|
const failed = (error)=>{
|
|
804
|
-
|
|
857
|
+
gated?.settle?.();
|
|
858
|
+
gated?.undo?.();
|
|
805
859
|
call.error = error;
|
|
806
860
|
throw error;
|
|
807
861
|
};
|
|
862
|
+
// Once the behaviour resolves, as discord.js changes the state once Discord has taken the answer
|
|
863
|
+
const landed = (value)=>{
|
|
864
|
+
gated?.settle?.();
|
|
865
|
+
try {
|
|
866
|
+
gated?.land?.();
|
|
867
|
+
} catch (error) {
|
|
868
|
+
call.error = error;
|
|
869
|
+
throw error;
|
|
870
|
+
}
|
|
871
|
+
return value;
|
|
872
|
+
};
|
|
808
873
|
try {
|
|
809
874
|
const result = Reflect.apply(behaviour, this, args);
|
|
810
875
|
// Recorded and passed on, so a rejection nobody awaits is unhandled in the test, as it is on a bot
|
|
811
|
-
if (result instanceof Promise) return result.then(
|
|
812
|
-
return result;
|
|
876
|
+
if (result instanceof Promise) return result.then(landed, failed);
|
|
877
|
+
return landed(result);
|
|
813
878
|
} catch (error) {
|
|
814
879
|
return failed(error);
|
|
815
880
|
}
|
|
@@ -923,7 +988,31 @@ function createMockInteraction(Class, props) {
|
|
|
923
988
|
if (repliable) {
|
|
924
989
|
instance.replied = false;
|
|
925
990
|
instance.deferred = false;
|
|
926
|
-
|
|
991
|
+
// Null until reply() or deferReply() decides it, as discord.js has it; default mode reads false until then, and a
|
|
992
|
+
// test reading it there is told once. A value the test sets is read as set
|
|
993
|
+
const strict = strictMocks();
|
|
994
|
+
let ephemeral = strict ? null : false;
|
|
995
|
+
let ephemeralDecided = false;
|
|
996
|
+
// Whether one of the mock's own first answers ran; flags a test sets without one leave ephemeral as discord.js would
|
|
997
|
+
let answeredByMock = false;
|
|
998
|
+
const answeredOutside = ()=>Boolean(instance.replied || instance.deferred) && !answeredByMock;
|
|
999
|
+
const decideEphemeral = (value, decided)=>{
|
|
1000
|
+
ephemeral = value;
|
|
1001
|
+
ephemeralDecided = decided;
|
|
1002
|
+
};
|
|
1003
|
+
Object.defineProperty(instance, 'ephemeral', {
|
|
1004
|
+
get: ()=>{
|
|
1005
|
+
// Quiet once a test answered the interaction itself: MeoCord then reads ephemeral only for truthiness, where false
|
|
1006
|
+
// and null act alike
|
|
1007
|
+
if (!ephemeralDecided && !strict && !answeredOutside()) {
|
|
1008
|
+
warnPlaceholder(mockLogger, `${Object.getPrototypeOf(instance).constructor.name}.ephemeral`, 'it is null until reply() or deferReply() decides it, as discord.js has it', 'Reply or defer first, or call useStrictMocks() to read null now.');
|
|
1009
|
+
}
|
|
1010
|
+
return ephemeral;
|
|
1011
|
+
},
|
|
1012
|
+
set: (value)=>decideEphemeral(value, true),
|
|
1013
|
+
enumerable: true,
|
|
1014
|
+
configurable: true
|
|
1015
|
+
});
|
|
927
1016
|
// discord.js's errors, with their codes; a call before any answer keeps the message the mock has always given
|
|
928
1017
|
const alreadyReplied = ()=>discordjsError(DiscordjsErrorCodes.InteractionAlreadyReplied);
|
|
929
1018
|
const notYetReplied = (method)=>discordjsError(DiscordjsErrorCodes.InteractionNotReplied, `Cannot call ${method}() before replying or deferring.`);
|
|
@@ -949,7 +1038,9 @@ function createMockInteraction(Class, props) {
|
|
|
949
1038
|
const saved = {
|
|
950
1039
|
replied: instance.replied,
|
|
951
1040
|
deferred: instance.deferred,
|
|
952
|
-
ephemeral
|
|
1041
|
+
ephemeral,
|
|
1042
|
+
ephemeralDecided,
|
|
1043
|
+
answeredByMock,
|
|
953
1044
|
original,
|
|
954
1045
|
originalGone,
|
|
955
1046
|
followUps: new Map(followUps),
|
|
@@ -959,10 +1050,10 @@ function createMockInteraction(Class, props) {
|
|
|
959
1050
|
return ()=>{
|
|
960
1051
|
Object.assign(instance, {
|
|
961
1052
|
replied: saved.replied,
|
|
962
|
-
deferred: saved.deferred
|
|
963
|
-
ephemeral: saved.ephemeral
|
|
1053
|
+
deferred: saved.deferred
|
|
964
1054
|
});
|
|
965
|
-
(
|
|
1055
|
+
decideEphemeral(saved.ephemeral, saved.ephemeralDecided);
|
|
1056
|
+
({ original, originalGone, answeredByTest, answeredByMock, modalAnswered } = saved);
|
|
966
1057
|
followUps.clear();
|
|
967
1058
|
for (const [id, held] of saved.followUps)followUps.set(id, held);
|
|
968
1059
|
};
|
|
@@ -987,24 +1078,76 @@ function createMockInteraction(Class, props) {
|
|
|
987
1078
|
return build;
|
|
988
1079
|
}
|
|
989
1080
|
};
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1081
|
+
// The changes answers make to the reply state, in order. One whose answer fails is undone once every change after
|
|
1082
|
+
// it is undone too, so a later answer's change stands while it does, as on a bot
|
|
1083
|
+
const changes = [];
|
|
1084
|
+
const undoneInTurn = (undo)=>{
|
|
1085
|
+
const change = {
|
|
1086
|
+
undo,
|
|
1087
|
+
failed: false
|
|
1088
|
+
};
|
|
1089
|
+
changes.push(change);
|
|
1090
|
+
return ()=>{
|
|
1091
|
+
change.failed = true;
|
|
1092
|
+
while(changes.at(-1)?.failed)changes.pop().undo();
|
|
1093
|
+
};
|
|
1094
|
+
};
|
|
1095
|
+
// The first answers called and not yet settled, oldest first, for default mode to warn where a bot would differ
|
|
1096
|
+
const unsettled = [];
|
|
1097
|
+
// Default mode warns once per interaction where code that doesn't await an answer would fail on a bot
|
|
1098
|
+
let inFlightWarned = false;
|
|
1099
|
+
const warnInFlight = (text)=>{
|
|
1100
|
+
if (inFlightWarned) return;
|
|
1101
|
+
inFlightWarned = true;
|
|
1102
|
+
mockLogger.warn(text);
|
|
1103
|
+
};
|
|
1104
|
+
/**
|
|
1105
|
+
* A first answer: refused once the interaction has one, else it replies or defers. Under strict mocks the change
|
|
1106
|
+
* waits for the answer to land, as discord.js makes it once the request resolves, so one sent while another is in
|
|
1107
|
+
* flight passes discord.js's check and Discord refuses it with 40060 if the other landed first. Default mode makes
|
|
1108
|
+
* the change at once.
|
|
1109
|
+
*/ const first = (method, kind, then)=>(args, byTest)=>{
|
|
1110
|
+
if (answered()) {
|
|
1111
|
+
if (unsettled.length > 0) {
|
|
1112
|
+
warnInFlight(`${method}() was called while ${unsettled[0]}() had not resolved; on a bot discord.js sends it, and Discord ` + `refuses it with 40060 ("Interaction has already been acknowledged") once the first lands. Await ` + `${unsettled[0]}() before answering again, or call useStrictMocks() to have the mock answer as Discord does.`);
|
|
1113
|
+
}
|
|
1114
|
+
refuse(method, alreadyReplied(), byTest);
|
|
1115
|
+
}
|
|
1116
|
+
// Read first: flags discord.js refuses throw before anything changes; reply() and deferReply() decide
|
|
1117
|
+
// ephemeral, as discord.js's do, and update() and deferUpdate() leave it
|
|
1118
|
+
const flagged = method === 'reply' || method === 'deferReply' ? hasEphemeralFlag(args[0]) : undefined;
|
|
994
1119
|
const undo = snapshot();
|
|
995
1120
|
instance[kind] = true;
|
|
996
|
-
if (
|
|
1121
|
+
if (flagged !== undefined) decideEphemeral(flagged, true);
|
|
997
1122
|
answeredByTest = byTest;
|
|
1123
|
+
answeredByMock = true;
|
|
998
1124
|
try {
|
|
999
1125
|
then(args);
|
|
1000
1126
|
} catch (error) {
|
|
1001
1127
|
undo();
|
|
1002
1128
|
throw error;
|
|
1003
1129
|
}
|
|
1004
|
-
|
|
1130
|
+
if (strict) {
|
|
1131
|
+
const land = snapshot();
|
|
1132
|
+
undo();
|
|
1133
|
+
return {
|
|
1134
|
+
land: ()=>{
|
|
1135
|
+
if (answered()) throw createDiscordError(40060, 'Interaction has already been acknowledged.');
|
|
1136
|
+
land();
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
unsettled.push(method);
|
|
1141
|
+
return {
|
|
1142
|
+
undo: undoneInTurn(undo),
|
|
1143
|
+
settle: ()=>void unsettled.splice(unsettled.indexOf(method), 1)
|
|
1144
|
+
};
|
|
1005
1145
|
};
|
|
1006
1146
|
/** A later answer: refused before the interaction has one. */ const later = (method, then)=>(args, byTest)=>{
|
|
1007
1147
|
if (!answered()) refuse(method, notYetReplied(method), byTest);
|
|
1148
|
+
if (unsettled.length > 0) {
|
|
1149
|
+
warnInFlight(`${unsettled[0]}() had not resolved when ${method}() was called; on a bot this throws InteractionNotReplied. ` + `Await ${unsettled[0]}() first, or call useStrictMocks() to have the mock refuse it now.`);
|
|
1150
|
+
}
|
|
1008
1151
|
const undo = snapshot();
|
|
1009
1152
|
try {
|
|
1010
1153
|
then(args);
|
|
@@ -1012,7 +1155,9 @@ function createMockInteraction(Class, props) {
|
|
|
1012
1155
|
undo();
|
|
1013
1156
|
throw error;
|
|
1014
1157
|
}
|
|
1015
|
-
return
|
|
1158
|
+
return {
|
|
1159
|
+
undo: undoneInTurn(undo)
|
|
1160
|
+
};
|
|
1016
1161
|
};
|
|
1017
1162
|
/** The id an edit, fetch or delete names, `undefined` for the original response. */ const targetId = (message)=>{
|
|
1018
1163
|
if (message === undefined || message === '@original') return undefined;
|
|
@@ -1130,12 +1275,31 @@ function createMockInteraction(Class, props) {
|
|
|
1130
1275
|
// it before answering would decide the window was already closed.
|
|
1131
1276
|
if (instance.type === InteractionType.ApplicationCommandAutocomplete) {
|
|
1132
1277
|
instance.responded = false;
|
|
1278
|
+
// Under strict mocks responded is set once respond() lands, as discord.js sets it once the request resolves, and a
|
|
1279
|
+
// second one sent while the first is in flight is refused by Discord with 40060; default mode sets it at once
|
|
1280
|
+
let respondsInFlight = 0;
|
|
1281
|
+
let respondWarned = false;
|
|
1133
1282
|
stubs.set('respond', createMockFn(async (choices)=>{
|
|
1134
|
-
if (instance.responded)
|
|
1283
|
+
if (instance.responded) {
|
|
1284
|
+
if (respondsInFlight > 0 && !respondWarned) {
|
|
1285
|
+
respondWarned = true;
|
|
1286
|
+
mockLogger.warn('respond() was called while respond() had not resolved; on a bot discord.js sends it, and Discord refuses ' + 'it with 40060 ("Interaction has already been acknowledged") once the first lands. Await respond() ' + 'before responding again, or call useStrictMocks() to have the mock respond as Discord does.');
|
|
1287
|
+
}
|
|
1288
|
+
throw discordjsError(DiscordjsErrorCodes.InteractionAlreadyReplied);
|
|
1289
|
+
}
|
|
1135
1290
|
// Discord refuses a list longer than its limit, and the menu shows nothing
|
|
1136
1291
|
if (Array.isArray(choices) && choices.length > MAX_AUTOCOMPLETE_CHOICES) {
|
|
1137
1292
|
throw createDiscordError(50035, `Invalid Form Body\ndata.choices[BASE_TYPE_MAX_LENGTH]: Must be ${MAX_AUTOCOMPLETE_CHOICES} or fewer in length.`);
|
|
1138
1293
|
}
|
|
1294
|
+
respondsInFlight++;
|
|
1295
|
+
if (!strictMocks()) instance.responded = true;
|
|
1296
|
+
try {
|
|
1297
|
+
await undefined;
|
|
1298
|
+
} finally{
|
|
1299
|
+
respondsInFlight--;
|
|
1300
|
+
}
|
|
1301
|
+
if (!strictMocks()) return;
|
|
1302
|
+
if (instance.responded) throw createDiscordError(40060, 'Interaction has already been acknowledged.');
|
|
1139
1303
|
instance.responded = true;
|
|
1140
1304
|
}));
|
|
1141
1305
|
}
|
|
@@ -1713,6 +1877,26 @@ const HOLDS = [
|
|
|
1713
1877
|
[
|
|
1714
1878
|
PresenceManager.prototype,
|
|
1715
1879
|
Presence
|
|
1880
|
+
],
|
|
1881
|
+
[
|
|
1882
|
+
GuildEmojiManager.prototype,
|
|
1883
|
+
GuildEmoji
|
|
1884
|
+
],
|
|
1885
|
+
[
|
|
1886
|
+
GuildStickerManager.prototype,
|
|
1887
|
+
Sticker
|
|
1888
|
+
],
|
|
1889
|
+
[
|
|
1890
|
+
GuildScheduledEventManager.prototype,
|
|
1891
|
+
GuildScheduledEvent
|
|
1892
|
+
],
|
|
1893
|
+
[
|
|
1894
|
+
AutoModerationRuleManager.prototype,
|
|
1895
|
+
AutoModerationRule
|
|
1896
|
+
],
|
|
1897
|
+
[
|
|
1898
|
+
GuildInviteManager.prototype,
|
|
1899
|
+
Invite
|
|
1716
1900
|
]
|
|
1717
1901
|
];
|
|
1718
1902
|
/**
|
|
@@ -1760,7 +1944,8 @@ const HOLDS = [
|
|
|
1760
1944
|
return manager;
|
|
1761
1945
|
}
|
|
1762
1946
|
/**
|
|
1763
|
-
* Creates a mock {@link Guild}, with
|
|
1947
|
+
* Creates a mock {@link Guild}, with its `members`, `channels`, `roles`, `bans`, `emojis`, `stickers`, `scheduledEvents`,
|
|
1948
|
+
* `autoModerationRules` and `invites` managers ready to stub.
|
|
1764
1949
|
*
|
|
1765
1950
|
* Use it for the server a message or an interaction came from, with the members, roles and channels a handler looks
|
|
1766
1951
|
* up in it.
|
|
@@ -1770,9 +1955,11 @@ const HOLDS = [
|
|
|
1770
1955
|
*
|
|
1771
1956
|
* @remarks
|
|
1772
1957
|
* 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
|
|
1773
|
-
* caches under that id: a member or
|
|
1774
|
-
*
|
|
1775
|
-
*
|
|
1958
|
+
* caches under that id: a member, channel, emoji, sticker, scheduled event or AutoMod rule in this guild, a role with
|
|
1959
|
+
* that id, a ban, or an invite with that code. `members.fetch({ user: ids })` resolves to a collection of each of those
|
|
1960
|
+
* members, found or made the same way; `create()` and `edit()` resolve to a mock of its item, which `create()` caches
|
|
1961
|
+
* as discord.js does, but for an invite, made for the channel given and cached nowhere; a list fetch resolves to an
|
|
1962
|
+
* empty collection. Members, roles and channels given are put in
|
|
1776
1963
|
* their managers' caches, where dispatch looks first when it resolves a message's typed params; a member
|
|
1777
1964
|
* {@link createMockMember} made without a server is in this one. Its `roles.everyone` is the role given with the
|
|
1778
1965
|
* guild's id, or else an @everyone role of its own at position 0 with no permissions, in `roles.cache` as Discord has
|
|
@@ -1803,7 +1990,7 @@ const HOLDS = [
|
|
|
1803
1990
|
instance.members = managerWith(GuildMemberManager.prototype, overrides.members);
|
|
1804
1991
|
instance.channels = managerWith(GuildChannelManager.prototype, overrides.channels);
|
|
1805
1992
|
instance.roles = guildRoleManager(instance.id, overrides.roles);
|
|
1806
|
-
instance
|
|
1993
|
+
for (const [key, Manager] of GUILD_MANAGERS)instance[key] = managerWith(Manager.prototype, undefined);
|
|
1807
1994
|
const guild = stubDeep(instance);
|
|
1808
1995
|
settleGuild(instance, guild);
|
|
1809
1996
|
// A member made without a server of its own is in this one
|
|
@@ -2427,7 +2614,7 @@ let placeReadsSeen;
|
|
|
2427
2614
|
guild.members = managerWith(GuildMemberManager.prototype, undefined);
|
|
2428
2615
|
guild.channels = managerWith(GuildChannelManager.prototype, undefined);
|
|
2429
2616
|
guild.roles = guildRoleManager(guild.id, undefined);
|
|
2430
|
-
guild
|
|
2617
|
+
for (const [key, Manager] of GUILD_MANAGERS)guild[key] = managerWith(Manager.prototype, undefined);
|
|
2431
2618
|
const proxy = stubDeep(guild);
|
|
2432
2619
|
settleGuild(guild, proxy);
|
|
2433
2620
|
return proxy;
|
|
@@ -2449,12 +2636,39 @@ const botMembers = new WeakMap();
|
|
|
2449
2636
|
return guildId ? cacheOf(guild.channels)?.get(channelId) : null;
|
|
2450
2637
|
});
|
|
2451
2638
|
}
|
|
2639
|
+
// A guild's managers beside members, channels and roles, each empty, of its discord.js class
|
|
2640
|
+
const GUILD_MANAGERS = [
|
|
2641
|
+
[
|
|
2642
|
+
'bans',
|
|
2643
|
+
GuildBanManager
|
|
2644
|
+
],
|
|
2645
|
+
[
|
|
2646
|
+
'emojis',
|
|
2647
|
+
GuildEmojiManager
|
|
2648
|
+
],
|
|
2649
|
+
[
|
|
2650
|
+
'stickers',
|
|
2651
|
+
GuildStickerManager
|
|
2652
|
+
],
|
|
2653
|
+
[
|
|
2654
|
+
'scheduledEvents',
|
|
2655
|
+
GuildScheduledEventManager
|
|
2656
|
+
],
|
|
2657
|
+
[
|
|
2658
|
+
'autoModerationRules',
|
|
2659
|
+
AutoModerationRuleManager
|
|
2660
|
+
],
|
|
2661
|
+
[
|
|
2662
|
+
'invites',
|
|
2663
|
+
GuildInviteManager
|
|
2664
|
+
]
|
|
2665
|
+
];
|
|
2452
2666
|
/** 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) {
|
|
2453
2667
|
for (const key of [
|
|
2454
2668
|
'members',
|
|
2455
2669
|
'channels',
|
|
2456
2670
|
'roles',
|
|
2457
|
-
|
|
2671
|
+
...GUILD_MANAGERS.map(([key])=>key)
|
|
2458
2672
|
])instance[key].guild = guild;
|
|
2459
2673
|
}
|
|
2460
2674
|
/**
|
|
@@ -19,8 +19,9 @@ const settings = shared('strictMocks', ()=>({
|
|
|
19
19
|
* and `message.thread` is `null` when the channel caches no thread under the message's id. So do a message's
|
|
20
20
|
* `editedAt`, a member's `presence`, a server's `verified` and `systemChannel` and a channel's `parent`, each `null` or
|
|
21
21
|
* `false` until the test gives what it is computed from. A reaction's `me` is `false`, a message's
|
|
22
|
-
* `mentions.repliedUser` and a modal's `message` are `null`,
|
|
23
|
-
* MeoCord's dispatcher fetches neither. No
|
|
22
|
+
* `mentions.repliedUser` and a modal's `message` are `null`, an interaction's `ephemeral` is `null` until `reply()` or
|
|
23
|
+
* `deferReply()` decides it, and a reaction has a whole message of its own, so MeoCord's dispatcher fetches neither. No
|
|
24
|
+
* placeholder warning is logged.
|
|
24
25
|
*
|
|
25
26
|
* @remarks
|
|
26
27
|
* Those getters read what a server holds, so strict mocks also have it as Discord sends it: the bot's member is in the
|
|
@@ -33,8 +34,10 @@ const settings = shared('strictMocks', ()=>({
|
|
|
33
34
|
* A message the bot sends is its `client.user`'s, and one it sends in a DM is in that DM. Give the bot's member a role
|
|
34
35
|
* to let it do more, such as kick a member its role ranks above. A value the test sets on a mock wins over the computed
|
|
35
36
|
* one. An interaction's answer that discord.js would refuse, such as a second `reply()` after one a test set with
|
|
36
|
-
* `mockResolvedValue`, is refused too, where without it the mock runs it and warns.
|
|
37
|
-
*
|
|
37
|
+
* `mockResolvedValue`, is refused too, where without it the mock runs it and warns. An answer marks the interaction
|
|
38
|
+
* `replied`, `deferred` or `responded` once it resolves, as discord.js does, so code that doesn't await one fails as it
|
|
39
|
+
* would on a bot, within one run of the test's code. The next major version (5.0) computes these values, and refuses
|
|
40
|
+
* those answers, without the call.
|
|
38
41
|
*
|
|
39
42
|
* `invoke` also refuses a call that gives a handler fewer arguments than it declares where it builds none, such as a
|
|
40
43
|
* reaction handler given its reaction without its `ReactionEvent`; without strict mocks, it warns once and calls it.
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { quoteForLog } from './user-text.util.js';
|
|
2
2
|
|
|
3
|
-
/** Whether a value
|
|
3
|
+
/** Whether `new` takes a value, as `instanceof` needs of its right side, such as a `@Catch` type. */ function isConstructible(value) {
|
|
4
|
+
if (typeof value !== 'function') return false;
|
|
5
|
+
try {
|
|
6
|
+
// Fails unless value can be new.target, which only a constructor can; none of value's constructor code runs
|
|
7
|
+
Reflect.construct(String, [], value);
|
|
8
|
+
return true;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Whether a value is a class, which a stage entry or an app option must be: constructible, with an object prototype for
|
|
15
|
+
* the metadata MeoCord reads. An arrow function, a method, an async or a generator function is none.
|
|
16
|
+
*/ function isConstructor(value) {
|
|
17
|
+
if (!isConstructible(value)) return false;
|
|
18
|
+
const prototype = value.prototype;
|
|
19
|
+
return typeof prototype === 'object' && prototype !== null;
|
|
20
|
+
}
|
|
4
21
|
/** A value as a refusal or a warning names it: a string quoted, `null` and `undefined` as written, else its kind. */ function describeValue(value) {
|
|
5
22
|
if (value === null || value === undefined) return String(value);
|
|
6
23
|
if (typeof value === 'string') return quoteForLog(value);
|
|
@@ -20,4 +37,4 @@ import { quoteForLog } from './user-text.util.js';
|
|
|
20
37
|
});
|
|
21
38
|
}
|
|
22
39
|
|
|
23
|
-
export { describeValue, isConstructor, setOwn, withArticle };
|
|
40
|
+
export { describeValue, isConstructible, isConstructor, setOwn, withArticle };
|
|
@@ -596,6 +596,9 @@ declare function useTheme(): ResolvedTheme;
|
|
|
596
596
|
* Such a listener runs in its emitter's context, so without it a `useTheme()` inside reads the theme outside any call.
|
|
597
597
|
* Timers and promises the handler starts keep its theme without it.
|
|
598
598
|
*
|
|
599
|
+
* The theme is the binding call's whole theme, its server's and user's themes included, so every later call, whoever
|
|
600
|
+
* it is for, is answered in the starting member's theme.
|
|
601
|
+
*
|
|
599
602
|
* @param fn - The function to run in the call's theme; `this` and its arguments are passed through.
|
|
600
603
|
* @returns A function that runs `fn` in the theme `useTheme()` returns where `bindTheme` is called.
|
|
601
604
|
*
|
|
@@ -605,7 +608,7 @@ declare function useTheme(): ResolvedTheme;
|
|
|
605
608
|
* async poll(message: Message) {
|
|
606
609
|
* const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, time: 60_000 })
|
|
607
610
|
* collector.on('collect', bindTheme(async (click: ButtonInteraction) => {
|
|
608
|
-
* const { emojis } = useTheme() // the
|
|
611
|
+
* const { emojis } = useTheme() // the starting member's theme, @UseTheme included
|
|
609
612
|
* await click.reply(`${emojis.success} Picked`)
|
|
610
613
|
* }))
|
|
611
614
|
* }
|
|
@@ -596,6 +596,9 @@ declare function useTheme(): ResolvedTheme;
|
|
|
596
596
|
* Such a listener runs in its emitter's context, so without it a `useTheme()` inside reads the theme outside any call.
|
|
597
597
|
* Timers and promises the handler starts keep its theme without it.
|
|
598
598
|
*
|
|
599
|
+
* The theme is the binding call's whole theme, its server's and user's themes included, so every later call, whoever
|
|
600
|
+
* it is for, is answered in the starting member's theme.
|
|
601
|
+
*
|
|
599
602
|
* @param fn - The function to run in the call's theme; `this` and its arguments are passed through.
|
|
600
603
|
* @returns A function that runs `fn` in the theme `useTheme()` returns where `bindTheme` is called.
|
|
601
604
|
*
|
|
@@ -605,7 +608,7 @@ declare function useTheme(): ResolvedTheme;
|
|
|
605
608
|
* async poll(message: Message) {
|
|
606
609
|
* const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, time: 60_000 })
|
|
607
610
|
* collector.on('collect', bindTheme(async (click: ButtonInteraction) => {
|
|
608
|
-
* const { emojis } = useTheme() // the
|
|
611
|
+
* const { emojis } = useTheme() // the starting member's theme, @UseTheme included
|
|
609
612
|
* await click.reply(`${emojis.success} Picked`)
|
|
610
613
|
* }))
|
|
611
614
|
* }
|
|
@@ -684,7 +684,8 @@ declare class MeoCordTestingModule {
|
|
|
684
684
|
* @param app - The class `@MeoCord` decorates.
|
|
685
685
|
* @param options - Providers that replace the app's by token, extra controllers, and observers.
|
|
686
686
|
* @returns The builder, whose `override*` methods still apply, for `compile()`.
|
|
687
|
-
* @throws TypeError when `app` is not a `@MeoCord` class.
|
|
687
|
+
* @throws TypeError when `app` is not a `@MeoCord` class. After {@link reportAllStartupErrors}, an app whose
|
|
688
|
+
* `@MeoCord` was refused throws the error it was refused for instead.
|
|
688
689
|
*
|
|
689
690
|
* @example
|
|
690
691
|
* ```ts
|
|
@@ -796,7 +797,8 @@ interface MockState<T extends (...args: any[]) => any = (...args: any[]) => any>
|
|
|
796
797
|
* The mock API of a mock function: what it has recorded, and the methods that change what it does.
|
|
797
798
|
*
|
|
798
799
|
* Every mock from `meocord/testing` has it, with the names jest and Vitest use, so a test written for either runner
|
|
799
|
-
* reads the same.
|
|
800
|
+
* reads the same. The values it takes and records aren't checked against the function it mocks; see
|
|
801
|
+
* {@link MockedFunction}.
|
|
800
802
|
*
|
|
801
803
|
* @group Testing
|
|
802
804
|
* @category Mocks
|
|
@@ -838,9 +840,12 @@ interface MockInstance<T extends (...args: any[]) => any = (...args: any[]) => a
|
|
|
838
840
|
/**
|
|
839
841
|
* A mock function with the signature of `T`, and the mock API of {@link MockInstance}.
|
|
840
842
|
*
|
|
841
|
-
* It takes the place of jest's `MockedFunction<T>` and Vitest's `MockedFunction<T
|
|
842
|
-
* method of a mock with it
|
|
843
|
-
* a
|
|
843
|
+
* It takes the place of jest's `MockedFunction<T>` and Vitest's `MockedFunction<T>`, and the mock factories type each
|
|
844
|
+
* method of a mock with it. Calling the mock is checked against `T`. The values its mock API takes and records are
|
|
845
|
+
* not, for a plain function as for an overloaded one: `mockReturnValue` and `mockResolvedValue` take any value,
|
|
846
|
+
* `mockImplementation` takes any function, and each of `mock.calls`' arguments and each of `mock.results`' values reads
|
|
847
|
+
* as `any`. So a partial fake, such as `guild.members.fetch.mockResolvedValue({ id })`, type-checks, and so does a
|
|
848
|
+
* value of the wrong type.
|
|
844
849
|
*
|
|
845
850
|
* @group Testing
|
|
846
851
|
* @category Mocks
|
|
@@ -1103,7 +1108,10 @@ type NotRaw<T> = T extends {
|
|
|
1103
1108
|
* collections of what it picks, each empty unless given. Other data Discord always sends reads as Discord sends it,
|
|
1104
1109
|
* such as `false` for a flag and `null` for what may be absent; what picks the handler, `commandName` or `customId`, is
|
|
1105
1110
|
* the test's to give. Replies follow Discord's order, so a second `reply()` rejects, and {@link getResponse} reports
|
|
1106
|
-
* every answer the interaction got.
|
|
1111
|
+
* every answer the interaction got. Under `useStrictMocks()` an answer marks the interaction `replied` or `deferred`
|
|
1112
|
+
* once it resolves, as discord.js does after Discord takes it: an edit or follow-up made before then rejects with
|
|
1113
|
+
* `InteractionNotReplied`, and a second answer sent while the first is in flight is refused with Discord's 40060. In
|
|
1114
|
+
* default mode the mark is made at the call, with a warning where a bot would fail. Every method is a mock function, and one that returns a promise in discord.js
|
|
1107
1115
|
* resolves. What the mock computes for itself, such as `memberPermissions` and `appPermissions`, calls none of them and
|
|
1108
1116
|
* caches nothing, so a stub a test sets stays for its own code.
|
|
1109
1117
|
*
|
|
@@ -1295,7 +1303,8 @@ interface MockGuildOverrides {
|
|
|
1295
1303
|
channels?: readonly GuildBasedChannel[];
|
|
1296
1304
|
}
|
|
1297
1305
|
/**
|
|
1298
|
-
* Creates a mock {@link Guild}, with
|
|
1306
|
+
* Creates a mock {@link Guild}, with its `members`, `channels`, `roles`, `bans`, `emojis`, `stickers`, `scheduledEvents`,
|
|
1307
|
+
* `autoModerationRules` and `invites` managers ready to stub.
|
|
1299
1308
|
*
|
|
1300
1309
|
* Use it for the server a message or an interaction came from, with the members, roles and channels a handler looks
|
|
1301
1310
|
* up in it.
|
|
@@ -1305,9 +1314,11 @@ interface MockGuildOverrides {
|
|
|
1305
1314
|
*
|
|
1306
1315
|
* @remarks
|
|
1307
1316
|
* 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
|
|
1308
|
-
* caches under that id: a member or
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1317
|
+
* caches under that id: a member, channel, emoji, sticker, scheduled event or AutoMod rule in this guild, a role with
|
|
1318
|
+
* that id, a ban, or an invite with that code. `members.fetch({ user: ids })` resolves to a collection of each of those
|
|
1319
|
+
* members, found or made the same way; `create()` and `edit()` resolve to a mock of its item, which `create()` caches
|
|
1320
|
+
* as discord.js does, but for an invite, made for the channel given and cached nowhere; a list fetch resolves to an
|
|
1321
|
+
* empty collection. Members, roles and channels given are put in
|
|
1311
1322
|
* their managers' caches, where dispatch looks first when it resolves a message's typed params; a member
|
|
1312
1323
|
* {@link createMockMember} made without a server is in this one. Its `roles.everyone` is the role given with the
|
|
1313
1324
|
* guild's id, or else an @everyone role of its own at position 0 with no permissions, in `roles.cache` as Discord has
|
|
@@ -1616,8 +1627,9 @@ declare function createChatInputOptions<Cached extends CacheType = any>(opts?: C
|
|
|
1616
1627
|
* and `message.thread` is `null` when the channel caches no thread under the message's id. So do a message's
|
|
1617
1628
|
* `editedAt`, a member's `presence`, a server's `verified` and `systemChannel` and a channel's `parent`, each `null` or
|
|
1618
1629
|
* `false` until the test gives what it is computed from. A reaction's `me` is `false`, a message's
|
|
1619
|
-
* `mentions.repliedUser` and a modal's `message` are `null`,
|
|
1620
|
-
* MeoCord's dispatcher fetches neither. No
|
|
1630
|
+
* `mentions.repliedUser` and a modal's `message` are `null`, an interaction's `ephemeral` is `null` until `reply()` or
|
|
1631
|
+
* `deferReply()` decides it, and a reaction has a whole message of its own, so MeoCord's dispatcher fetches neither. No
|
|
1632
|
+
* placeholder warning is logged.
|
|
1621
1633
|
*
|
|
1622
1634
|
* @remarks
|
|
1623
1635
|
* Those getters read what a server holds, so strict mocks also have it as Discord sends it: the bot's member is in the
|
|
@@ -1630,8 +1642,10 @@ declare function createChatInputOptions<Cached extends CacheType = any>(opts?: C
|
|
|
1630
1642
|
* A message the bot sends is its `client.user`'s, and one it sends in a DM is in that DM. Give the bot's member a role
|
|
1631
1643
|
* to let it do more, such as kick a member its role ranks above. A value the test sets on a mock wins over the computed
|
|
1632
1644
|
* one. An interaction's answer that discord.js would refuse, such as a second `reply()` after one a test set with
|
|
1633
|
-
* `mockResolvedValue`, is refused too, where without it the mock runs it and warns.
|
|
1634
|
-
*
|
|
1645
|
+
* `mockResolvedValue`, is refused too, where without it the mock runs it and warns. An answer marks the interaction
|
|
1646
|
+
* `replied`, `deferred` or `responded` once it resolves, as discord.js does, so code that doesn't await one fails as it
|
|
1647
|
+
* would on a bot, within one run of the test's code. The next major version (5.0) computes these values, and refuses
|
|
1648
|
+
* those answers, without the call.
|
|
1635
1649
|
*
|
|
1636
1650
|
* `invoke` also refuses a call that gives a handler fewer arguments than it declares where it builds none, such as a
|
|
1637
1651
|
* reaction handler given its reaction without its `ReactionEvent`; without strict mocks, it warns once and calls it.
|