meocord 4.1.2 → 4.2.0
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 +98 -0
- package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-CRn9kWob.cjs} +295 -87
- package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-ChvMqnLd.cjs} +74 -40
- package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-BdAUTdYs.cjs} +1 -1
- package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CQo8mSP8.cjs} +158 -72
- package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-DhDVbT5M.cjs} +12 -3
- package/dist/cjs/common/index.cjs +4 -4
- package/dist/cjs/core/index.cjs +195 -157
- package/dist/cjs/decorator/index.cjs +66 -57
- package/dist/cjs/testing/index.cjs +733 -301
- package/dist/cli.json +1 -1
- package/dist/esm/bin/app-template/README.md.template +3 -1
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
- package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
- package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
- package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
- package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
- package/dist/esm/build/load-config.pre-entry.js +4 -1
- package/dist/esm/common/decorator.js +6 -6
- package/dist/esm/common/deprecation.js +2 -2
- package/dist/esm/common/errors.js +2 -2
- package/dist/esm/common/execution-context.js +1 -1
- package/dist/esm/common/metadata.js +1 -1
- package/dist/esm/common/route.js +10 -1
- package/dist/esm/common/translate-error.js +1 -1
- package/dist/esm/core/command-conflicts.js +34 -34
- package/dist/esm/core/component-routes.js +74 -31
- package/dist/esm/core/dispatcher.js +51 -7
- package/dist/esm/core/filter-runner.js +2 -2
- package/dist/esm/core/handler-pipeline.js +27 -14
- package/dist/esm/core/handler-registry.js +1 -1
- package/dist/esm/core/interceptor-runner.js +2 -2
- package/dist/esm/core/meocord-factory.js +196 -157
- package/dist/esm/core/message-routes.js +4 -3
- package/dist/esm/core/observer-runner.js +4 -3
- package/dist/esm/core/providers.js +38 -21
- package/dist/esm/core/route-specificity.js +4 -4
- package/dist/esm/core/scalar-types.js +15 -3
- package/dist/esm/core/shard-context.js +1 -1
- package/dist/esm/core/startup-roots.js +2 -3
- package/dist/esm/core/theme-runtime.js +2 -2
- package/dist/esm/decorator/app.decorator.js +12 -7
- package/dist/esm/decorator/command-builder.decorator.js +4 -4
- package/dist/esm/decorator/controller-class.decorator.js +9 -4
- package/dist/esm/decorator/controller.decorator.js +110 -67
- package/dist/esm/decorator/cooldown.decorator.js +3 -3
- package/dist/esm/decorator/defer.decorator.js +4 -4
- package/dist/esm/decorator/event.decorator.js +5 -5
- package/dist/esm/decorator/filter.decorator.js +7 -7
- package/dist/esm/decorator/guard.decorator.js +8 -8
- package/dist/esm/decorator/inject.decorator.js +4 -4
- package/dist/esm/decorator/interceptor.decorator.js +7 -7
- package/dist/esm/decorator/observer.decorator.js +4 -4
- package/dist/esm/decorator/service.decorator.js +4 -4
- package/dist/esm/decorator/theme.decorator.js +3 -3
- package/dist/esm/decorator/validation.decorator.js +10 -10
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +1 -1
- package/dist/esm/testing/cooldown-store-suite.js +18 -10
- package/dist/esm/testing/discord-defaults.js +60 -7
- package/dist/esm/testing/index.js +4 -2
- package/dist/esm/testing/inspect-handler.js +20 -3
- package/dist/esm/testing/meocord-testing-module.js +229 -188
- package/dist/esm/testing/mock-fn.js +80 -10
- package/dist/esm/testing/mock-interaction.js +223 -72
- package/dist/esm/testing/response.js +1 -1
- package/dist/esm/testing/routing.js +39 -14
- package/dist/esm/testing/startup-errors.js +26 -0
- package/dist/esm/testing/strict-mocks.js +45 -0
- package/dist/esm/testing/theme.js +1 -1
- package/dist/esm/util/bundle-entry.util.js +2 -1
- package/dist/esm/util/interaction.util.js +1 -1
- package/dist/esm/util/meocord-config-validation.util.js +1 -0
- package/dist/esm/util/metadata-keys.js +1 -0
- package/dist/esm/util/refusal.util.js +148 -8
- package/dist/types/common/index.d.cts +8 -8
- package/dist/types/common/index.d.ts +8 -8
- package/dist/types/core/index.d.cts +8 -6
- package/dist/types/core/index.d.ts +8 -6
- package/dist/types/decorator/index.d.cts +30 -29
- package/dist/types/decorator/index.d.ts +30 -29
- package/dist/types/{index-D7n0FBMm.d.ts → index-CdXsFgjE.d.ts} +62 -15
- package/dist/types/{index-DEP0NYrU.d.cts → index-nn5p7OoX.d.cts} +62 -15
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-C-aMI7Sl.d.cts → route-BZgFoNeB.d.cts} +5 -2
- package/dist/types/{route-C-aMI7Sl.d.ts → route-BZgFoNeB.d.ts} +5 -2
- package/dist/types/testing/index.d.cts +245 -31
- package/dist/types/testing/index.d.ts +245 -31
- package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-BcOJVzR9.d.ts} +1 -1
- package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-UKr3-6Ys.d.cts} +1 -1
- package/dist/types/{translator-J6tIUfdp.d.ts → translator-BViWKtLJ.d.ts} +1 -1
- package/dist/types/{translator-B4mc1lzJ.d.cts → translator-DHMkkCpg.d.cts} +1 -1
- package/package.json +3 -2
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
import { createMockFn } from './mock-fn.js';
|
|
3
|
-
import { DataManager, RoleManager, Role, GuildChannel, GuildMember, User, BaseGuild, MessageMentions,
|
|
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, CommandInteractionOptionResolver, ApplicationCommandOptionType, Client, ApplicationCommandManager, ClientUser, CDN, AuthorizingIntegrationOwners, BaseInteraction, UserContextMenuCommandInteraction, Locale, StringSelectMenuInteraction, UserSelectMenuInteraction, RoleSelectMenuInteraction, ChannelSelectMenuInteraction, MentionableSelectMenuInteraction, Collection, MessageFlagsBitField, PermissionsBitField, 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
7
|
import { Logger } from '../common/logger.js';
|
|
8
|
-
import { warnPlaceholder } from '../common/deprecation.js';
|
|
8
|
+
import { warnOnce, warnPlaceholder } from '../common/deprecation.js';
|
|
9
9
|
import { embedsAsDiscordStores, asDiscordStores } from './discord-shape.js';
|
|
10
10
|
import { nextSnowflake, MOCK_BOT_ID } from './snowflake.js';
|
|
11
|
+
import { noteMockMade, strictMocks } from './strict-mocks.js';
|
|
11
12
|
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
// stubDeep — Proxy that answers each property it lacks: a mock fn for a method, a default, or a nested stub
|
|
@@ -20,6 +21,7 @@ const SKIP = new Set([
|
|
|
20
21
|
'then'
|
|
21
22
|
]);
|
|
22
23
|
function stubDeep(instance, externalStubs, onSet) {
|
|
24
|
+
noteMockMade();
|
|
23
25
|
const stubs = externalStubs ?? new Map();
|
|
24
26
|
const proxy = new Proxy(instance, {
|
|
25
27
|
get (target, prop) {
|
|
@@ -60,7 +62,11 @@ function stubDeep(instance, externalStubs, onSet) {
|
|
|
60
62
|
const desc = Object.getOwnPropertyDescriptor(proto, key);
|
|
61
63
|
if (desc !== undefined) {
|
|
62
64
|
protoValue = desc.value;
|
|
63
|
-
if (desc.get &&
|
|
65
|
+
if (desc.get && isPlaceholder(proto, key)) {
|
|
66
|
+
// Strict mocks run discord.js's getter, read live; otherwise the placeholder, with a warning once
|
|
67
|
+
if (strictMocks()) return Reflect.get(target, prop, proxy);
|
|
68
|
+
warnBooleanPlaceholder(target, key);
|
|
69
|
+
}
|
|
64
70
|
break;
|
|
65
71
|
}
|
|
66
72
|
proto = Object.getPrototypeOf(proto);
|
|
@@ -150,9 +156,18 @@ const REAL_METHODS = new Map([
|
|
|
150
156
|
])
|
|
151
157
|
]
|
|
152
158
|
]);
|
|
159
|
+
// What strict mocks also run for real: a thread's permissions are its parent channel's, as discord.js reads them
|
|
160
|
+
const STRICT_REAL_METHODS = new Map([
|
|
161
|
+
[
|
|
162
|
+
ThreadChannel.prototype,
|
|
163
|
+
new Set([
|
|
164
|
+
'permissionsFor'
|
|
165
|
+
])
|
|
166
|
+
]
|
|
167
|
+
]);
|
|
153
168
|
function isRealMethod(target, key) {
|
|
154
169
|
for(let proto = Object.getPrototypeOf(target); proto !== null; proto = Object.getPrototypeOf(proto)){
|
|
155
|
-
if (REAL_METHODS.get(proto)?.has(key)) return true;
|
|
170
|
+
if (REAL_METHODS.get(proto)?.has(key) || strictMocks() && STRICT_REAL_METHODS.get(proto)?.has(key)) return true;
|
|
156
171
|
}
|
|
157
172
|
return false;
|
|
158
173
|
}
|
|
@@ -212,6 +227,12 @@ const PLACEHOLDER_BOOLEANS = new Map([
|
|
|
212
227
|
'joinable'
|
|
213
228
|
])
|
|
214
229
|
],
|
|
230
|
+
[
|
|
231
|
+
VoiceChannel.prototype,
|
|
232
|
+
new Set([
|
|
233
|
+
'speakable'
|
|
234
|
+
])
|
|
235
|
+
],
|
|
215
236
|
[
|
|
216
237
|
User.prototype,
|
|
217
238
|
new Set([
|
|
@@ -231,11 +252,18 @@ const PLACEHOLDER_BOOLEANS = new Map([
|
|
|
231
252
|
])
|
|
232
253
|
]
|
|
233
254
|
]);
|
|
255
|
+
/** Whether `proto`'s getter for `key` is one of those, or a subclass's override of one, such as a voice channel's `joinable`. */ function isPlaceholder(proto, key) {
|
|
256
|
+
for(; proto !== null; proto = Object.getPrototypeOf(proto)){
|
|
257
|
+
if (PLACEHOLDER_BOOLEANS.get(proto)?.has(key)) return true;
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
234
261
|
const mockLogger = new Logger('Mocks');
|
|
235
262
|
function warnBooleanPlaceholder(target, key) {
|
|
236
263
|
const name = Object.getPrototypeOf(target).constructor.name;
|
|
237
|
-
|
|
238
|
-
|
|
264
|
+
// The class name in camel case, a leading acronym included: dmChannel, guildMember
|
|
265
|
+
const variable = name.replace(/^[A-Z]+(?=[A-Z][a-z])|^[A-Z]/, (start)=>start.toLowerCase());
|
|
266
|
+
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, or call useStrictMocks() to have the mock compute it now.`);
|
|
239
267
|
}
|
|
240
268
|
// The client of a structure made without one: the one its guild has, else one of its own, made once
|
|
241
269
|
const ownClients = new WeakMap();
|
|
@@ -618,22 +646,13 @@ const isSnowflake = (id)=>typeof id === 'string' && /^\d{1,20}$/.test(id);
|
|
|
618
646
|
}
|
|
619
647
|
}
|
|
620
648
|
/** Where a mock interaction keeps every answer it got, through respond() or discord.js directly, in order. */ const RESPONSE_LOG = Symbol('response log');
|
|
621
|
-
const ANSWER_METHODS = [
|
|
622
|
-
'reply',
|
|
623
|
-
'deferReply',
|
|
624
|
-
'editReply',
|
|
625
|
-
'followUp',
|
|
626
|
-
'deleteReply',
|
|
627
|
-
'update',
|
|
628
|
-
'deferUpdate',
|
|
629
|
-
'showModal'
|
|
630
|
-
];
|
|
631
649
|
/**
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
650
|
+
* A mock of an answer method, `impl` its behaviour, that records each call in `log` as it is made, with what it sent
|
|
651
|
+
* and, once it settles, what it rejected with. The record wraps every behaviour the mock is given, its own and any a
|
|
652
|
+
* test sets, so the mock stays the plain mock function a runner's matchers read, bun's included. `withResponse` is how
|
|
653
|
+
* a call asks discord.js for the message back, not part of what it sends, so it is left out.
|
|
654
|
+
*/ function recordedMock(method, impl, log) {
|
|
655
|
+
const record = (behaviour)=>function(...args) {
|
|
637
656
|
const [payload] = args;
|
|
638
657
|
const sent = payload && typeof payload === 'object' && 'withResponse' in payload ? Object.fromEntries(Object.entries(payload).filter(([key])=>key !== 'withResponse')) : payload;
|
|
639
658
|
const call = {
|
|
@@ -643,7 +662,7 @@ const ANSWER_METHODS = [
|
|
|
643
662
|
stampCall(call);
|
|
644
663
|
log.push(call);
|
|
645
664
|
try {
|
|
646
|
-
const result = Reflect.apply(
|
|
665
|
+
const result = Reflect.apply(behaviour, this, args);
|
|
647
666
|
// Recorded and passed on, so a rejection nobody awaits is unhandled in the test, as it is on a bot
|
|
648
667
|
if (result instanceof Promise) {
|
|
649
668
|
return result.then(undefined, (error)=>{
|
|
@@ -656,8 +675,38 @@ const ANSWER_METHODS = [
|
|
|
656
675
|
call.error = error;
|
|
657
676
|
throw error;
|
|
658
677
|
}
|
|
659
|
-
}
|
|
660
|
-
|
|
678
|
+
};
|
|
679
|
+
const mock = createMockFn(record(impl));
|
|
680
|
+
const always = mock.mockImplementation.bind(mock);
|
|
681
|
+
const once = mock.mockImplementationOnce.bind(mock);
|
|
682
|
+
// Each way a test sets a behaviour goes through the record, a runner's own extras included where it has them
|
|
683
|
+
const setters = {
|
|
684
|
+
mockImplementation: (fn)=>fn,
|
|
685
|
+
mockReturnValue: (value)=>()=>value,
|
|
686
|
+
mockResolvedValue: (value)=>()=>Promise.resolve(value),
|
|
687
|
+
mockRejectedValue: (value)=>()=>Promise.reject(value)
|
|
688
|
+
};
|
|
689
|
+
// In place of the mock's own, keeping whether each is among its keys
|
|
690
|
+
const define = (name, value)=>Object.defineProperty(mock, name, {
|
|
691
|
+
value,
|
|
692
|
+
writable: true,
|
|
693
|
+
enumerable: Object.getOwnPropertyDescriptor(mock, name)?.enumerable ?? false,
|
|
694
|
+
configurable: true
|
|
695
|
+
});
|
|
696
|
+
for (const [name, behaviourOf] of Object.entries(setters)){
|
|
697
|
+
define(name, (value)=>(always(record(behaviourOf(value))), mock));
|
|
698
|
+
define(`${name}Once`, (value)=>(once(record(behaviourOf(value))), mock));
|
|
699
|
+
}
|
|
700
|
+
if (typeof mock.mockReturnThis === 'function') {
|
|
701
|
+
define('mockReturnThis', ()=>(always(record(function() {
|
|
702
|
+
return this;
|
|
703
|
+
})), mock));
|
|
704
|
+
}
|
|
705
|
+
const withImplementation = mock.withImplementation;
|
|
706
|
+
if (typeof withImplementation === 'function') {
|
|
707
|
+
define('withImplementation', (fn, callback)=>withImplementation.call(mock, record(fn), callback));
|
|
708
|
+
}
|
|
709
|
+
return mock;
|
|
661
710
|
}
|
|
662
711
|
/** The interaction a mock options resolver was given to, for the server a user option's member is in. */ const optionOwners = new WeakMap();
|
|
663
712
|
/** An error as discord.js's options resolver throws it: a `TypeError` with discord.js's code and message. */ const optionError = (code, ...args)=>new DiscordjsTypeError(code, ...args);
|
|
@@ -716,21 +765,27 @@ function createMockInteraction(Class, props) {
|
|
|
716
765
|
if (typeof flags === 'bigint') return (flags & 64n) !== 0n;
|
|
717
766
|
return false;
|
|
718
767
|
};
|
|
719
|
-
|
|
768
|
+
// Every answer the interaction gets, through respond() or discord.js directly, in order, for getResponse
|
|
769
|
+
const log = [];
|
|
770
|
+
Object.defineProperty(instance, RESPONSE_LOG, {
|
|
771
|
+
value: log
|
|
772
|
+
});
|
|
773
|
+
const answer = (method, impl)=>stubs.set(method, recordedMock(method, impl, log));
|
|
774
|
+
answer('reply', async (...args)=>{
|
|
720
775
|
if (instance.deferred || instance.replied) throw alreadyReplied();
|
|
721
776
|
instance.replied = true;
|
|
722
777
|
if (hasEphemeralFlag(args[0])) instance.ephemeral = true;
|
|
723
|
-
})
|
|
724
|
-
|
|
778
|
+
});
|
|
779
|
+
answer('deferReply', async (...args)=>{
|
|
725
780
|
if (instance.deferred || instance.replied) throw alreadyReplied();
|
|
726
781
|
instance.deferred = true;
|
|
727
782
|
if (hasEphemeralFlag(args[0])) instance.ephemeral = true;
|
|
728
|
-
})
|
|
729
|
-
|
|
783
|
+
});
|
|
784
|
+
answer('followUp', async ()=>{
|
|
730
785
|
if (!instance.deferred && !instance.replied) throw notYetReplied('followUp');
|
|
731
786
|
instance.replied = true;
|
|
732
787
|
return createMockMessage();
|
|
733
|
-
})
|
|
788
|
+
});
|
|
734
789
|
// The interaction's own message as Discord holds it: the message a component is on, or the original response.
|
|
735
790
|
// An edit replaces it in the shape Discord stores, with ids and resolved media, stamped with when it was edited,
|
|
736
791
|
// and fetchReply() reads that back, so code comparing the two sees what it would against Discord.
|
|
@@ -739,7 +794,7 @@ function createMockInteraction(Class, props) {
|
|
|
739
794
|
// A command answered with a modal has no original response, and Discord refuses one asked for
|
|
740
795
|
let modalAnswered = false;
|
|
741
796
|
const unknownMessage = ()=>createDiscordError(10008, 'Unknown Message');
|
|
742
|
-
|
|
797
|
+
answer('editReply', async (options)=>{
|
|
743
798
|
if (!instance.deferred && !instance.replied) throw notYetReplied('editReply');
|
|
744
799
|
if (modalAnswered) throw unknownMessage();
|
|
745
800
|
instance.replied = true;
|
|
@@ -749,42 +804,33 @@ function createMockInteraction(Class, props) {
|
|
|
749
804
|
...held,
|
|
750
805
|
components: held.returned ?? held.components
|
|
751
806
|
});
|
|
752
|
-
})
|
|
807
|
+
});
|
|
753
808
|
stubs.set('fetchReply', createMockFn(async ()=>{
|
|
754
809
|
if (modalAnswered) throw unknownMessage();
|
|
755
810
|
return messageFrom(current());
|
|
756
811
|
}));
|
|
757
|
-
|
|
812
|
+
answer('deleteReply', async ()=>{
|
|
758
813
|
if (!instance.deferred && !instance.replied) throw notYetReplied('deleteReply');
|
|
759
814
|
if (modalAnswered) throw unknownMessage();
|
|
760
|
-
})
|
|
815
|
+
});
|
|
761
816
|
// showModal — the first response of a command or a component, like reply()
|
|
762
817
|
if (instance.type === InteractionType.ApplicationCommand || instance.type === InteractionType.MessageComponent) {
|
|
763
|
-
|
|
818
|
+
answer('showModal', async ()=>{
|
|
764
819
|
if (instance.deferred || instance.replied) throw alreadyReplied();
|
|
765
820
|
instance.replied = true;
|
|
766
821
|
modalAnswered = instance.type === InteractionType.ApplicationCommand;
|
|
767
|
-
})
|
|
822
|
+
});
|
|
768
823
|
}
|
|
769
824
|
// deferUpdate / update — components, and modals submitted from a message's component
|
|
770
825
|
if (instance.type === InteractionType.MessageComponent || instance.type === InteractionType.ModalSubmit) {
|
|
771
|
-
|
|
826
|
+
answer('update', async ()=>{
|
|
772
827
|
if (instance.deferred || instance.replied) throw alreadyReplied();
|
|
773
828
|
instance.replied = true;
|
|
774
|
-
})
|
|
775
|
-
|
|
829
|
+
});
|
|
830
|
+
answer('deferUpdate', async ()=>{
|
|
776
831
|
if (instance.deferred || instance.replied) throw alreadyReplied();
|
|
777
832
|
instance.deferred = true;
|
|
778
|
-
})
|
|
779
|
-
}
|
|
780
|
-
// Every answer the interaction gets, through respond() or discord.js directly, in order, for getResponse
|
|
781
|
-
const log = [];
|
|
782
|
-
Object.defineProperty(instance, RESPONSE_LOG, {
|
|
783
|
-
value: log
|
|
784
|
-
});
|
|
785
|
-
for (const method of ANSWER_METHODS){
|
|
786
|
-
const stub = stubs.get(method);
|
|
787
|
-
if (stub) stubs.set(method, recorded(method, stub, log));
|
|
833
|
+
});
|
|
788
834
|
}
|
|
789
835
|
}
|
|
790
836
|
// Autocomplete is not repliable, but it has a response of its own: Discord accepts
|
|
@@ -823,6 +869,43 @@ function createMockInteraction(Class, props) {
|
|
|
823
869
|
const unset = (key)=>!Object.prototype.hasOwnProperty.call(instance, key);
|
|
824
870
|
const generatedId = unset('id') ? instance.id = nextSnowflake() : undefined;
|
|
825
871
|
defineCreatedTime(instance, generatedId);
|
|
872
|
+
// A raw member is from a server the bot isn't in: no cached guild, and its user the interaction's, as discord.js reads it
|
|
873
|
+
const raw = own('member');
|
|
874
|
+
// Given with a guild, a raw-shaped member is kept as the cached server's member it was before raw members
|
|
875
|
+
if (isRawMember(raw) && own('guild')) {
|
|
876
|
+
warnOnce(mockLogger, "The member given is a raw member, from a server the bot isn't in, but the mock was given a guild, so it reads as a " + 'cached server: leave the guild out for a raw member, or give a GuildMember.');
|
|
877
|
+
} else if (isRawMember(raw)) {
|
|
878
|
+
const rawOnly = "The member given is a raw member, from a server the bot isn't in";
|
|
879
|
+
if (!own('guildId')) throw new Error(`${rawOnly}, so the mock needs that server's guildId.`);
|
|
880
|
+
if (own('channel') !== undefined) throw new Error(`${rawOnly}, where discord.js caches no channel, but the mock was given a channel: leave the channel out.`);
|
|
881
|
+
const givenUser = own('user');
|
|
882
|
+
if (givenUser && givenUser.id !== raw.user.id) {
|
|
883
|
+
throw new Error(`The member given is user ${raw.user.id}'s, but the mock's user is ${givenUser.id}: give the same user, or leave one of them out.`);
|
|
884
|
+
}
|
|
885
|
+
if (!givenUser) {
|
|
886
|
+
const { id, username, global_name: globalName, discriminator, avatar } = raw.user;
|
|
887
|
+
instance.user = createMockUser({
|
|
888
|
+
id,
|
|
889
|
+
username,
|
|
890
|
+
globalName: globalName ?? null,
|
|
891
|
+
discriminator,
|
|
892
|
+
avatar: avatar ?? null
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
Object.defineProperty(instance, 'guild', {
|
|
896
|
+
value: null,
|
|
897
|
+
writable: true,
|
|
898
|
+
enumerable: true,
|
|
899
|
+
configurable: true
|
|
900
|
+
});
|
|
901
|
+
// Only its channelId: discord.js caches no channel from a server the bot isn't in
|
|
902
|
+
Object.defineProperty(instance, 'channel', {
|
|
903
|
+
value: null,
|
|
904
|
+
writable: true,
|
|
905
|
+
enumerable: true,
|
|
906
|
+
configurable: true
|
|
907
|
+
});
|
|
908
|
+
}
|
|
826
909
|
const userGiven = !unset('user');
|
|
827
910
|
if (!userGiven) instance.user = mockUser();
|
|
828
911
|
// Assigned once by discord.js's Base constructor, as on a message, and the gateway caches the user on it
|
|
@@ -965,29 +1048,25 @@ function createMockInteraction(Class, props) {
|
|
|
965
1048
|
* on a double whose shape is an interface with nothing at runtime to read it from.
|
|
966
1049
|
*/ function stubCallable() {
|
|
967
1050
|
const fn = createMockFn();
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1051
|
+
// Any property beyond the mock's own API is a nested mock, made on first read and kept on the function, out of its
|
|
1052
|
+
// keys. Its prototype makes them rather than a Proxy around it, so it stays the plain mock function matchers read
|
|
1053
|
+
const inherited = Object.getPrototypeOf(fn);
|
|
1054
|
+
Object.setPrototypeOf(fn, new Proxy(inherited, {
|
|
1055
|
+
get (proto, prop, receiver) {
|
|
1056
|
+
if (typeof prop === 'symbol' || prop in proto) return Reflect.get(proto, prop, receiver);
|
|
973
1057
|
// Never thenable — otherwise awaiting a mock hangs on itself
|
|
974
|
-
if (
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
if (!nested.has(key)) nested.set(key, stubCallable());
|
|
979
|
-
return nested.get(key);
|
|
980
|
-
},
|
|
981
|
-
set (target, prop, value) {
|
|
982
|
-
Object.defineProperty(target, prop, {
|
|
983
|
-
value,
|
|
1058
|
+
if (prop === 'then') return undefined;
|
|
1059
|
+
const nested = stubCallable();
|
|
1060
|
+
Object.defineProperty(receiver, prop, {
|
|
1061
|
+
value: nested,
|
|
984
1062
|
writable: true,
|
|
985
|
-
enumerable:
|
|
1063
|
+
enumerable: false,
|
|
986
1064
|
configurable: true
|
|
987
1065
|
});
|
|
988
|
-
return
|
|
1066
|
+
return nested;
|
|
989
1067
|
}
|
|
990
|
-
});
|
|
1068
|
+
}));
|
|
1069
|
+
return fn;
|
|
991
1070
|
}
|
|
992
1071
|
/**
|
|
993
1072
|
* Creates a mock of any type, with no class needed, for service doubles and interfaces.
|
|
@@ -1463,10 +1542,16 @@ const CHANNEL_TYPE_GUARDS = [
|
|
|
1463
1542
|
});
|
|
1464
1543
|
return stubDeep(manager);
|
|
1465
1544
|
}
|
|
1466
|
-
/**
|
|
1545
|
+
/** The permissions Discord gives a server's @everyone role in a server made today. */ const NEW_SERVER_EVERYONE_PERMISSIONS = 2248473465835073n;
|
|
1546
|
+
// A server's @everyone role, which has the server's id and ranks lowest, at position 0; with strict mocks, the
|
|
1547
|
+
// permissions Discord gives it in a new server
|
|
1548
|
+
const everyoneRole = (guildId)=>createMockInteraction(Role, {
|
|
1467
1549
|
id: guildId,
|
|
1468
1550
|
name: '@everyone',
|
|
1469
|
-
position: 0
|
|
1551
|
+
position: 0,
|
|
1552
|
+
...strictMocks() ? {
|
|
1553
|
+
permissions: new PermissionsBitField(NEW_SERVER_EVERYONE_PERMISSIONS).freeze()
|
|
1554
|
+
} : {}
|
|
1470
1555
|
});
|
|
1471
1556
|
/**
|
|
1472
1557
|
* A guild's role manager with `roles` in its cache, and the guild's @everyone role: the one given with the guild's id,
|
|
@@ -1494,6 +1579,68 @@ const CHANNEL_TYPE_GUARDS = [
|
|
|
1494
1579
|
});
|
|
1495
1580
|
return manager;
|
|
1496
1581
|
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Creates the member Discord sends with an interaction from a server the bot isn't in, as discord.js keeps it.
|
|
1584
|
+
*
|
|
1585
|
+
* Give it as an interaction's `member`, with the server's `guildId` and no `guild`, to test a user-installed command
|
|
1586
|
+
* run there: the interaction reads `inRawGuild()` true, `guild` and `channel` null, `user` the member's user and
|
|
1587
|
+
* `memberPermissions` the member's, and a user option's member is the plain member Discord resolves too.
|
|
1588
|
+
*
|
|
1589
|
+
* @remarks
|
|
1590
|
+
* It is plain data, not a `GuildMember`: `roles` are role ids, `permissions` is the permission bitfield as a string,
|
|
1591
|
+
* and the rest are as Discord sends them, `null` for what may be absent and `false` for a flag. Code that reads
|
|
1592
|
+
* `member.roles.cache` throws on it, as it does in Discord.
|
|
1593
|
+
*
|
|
1594
|
+
* @param overrides - Fields for the member; see {@link MockRawMemberOverrides}.
|
|
1595
|
+
* @returns The member, as an `APIInteractionGuildMember`.
|
|
1596
|
+
*
|
|
1597
|
+
* @example
|
|
1598
|
+
* ```ts
|
|
1599
|
+
* import { expect } from 'vitest'
|
|
1600
|
+
*
|
|
1601
|
+
* const member = createMockRawMember({ roles: ['300000000000000001'], permissions: [PermissionFlagsBits.SendMessages] })
|
|
1602
|
+
* const command = createMockInteraction(ChatInputCommandInteraction, { guildId: '100000000000000001', member })
|
|
1603
|
+
* expect(command.inRawGuild()).toBe(true)
|
|
1604
|
+
* ```
|
|
1605
|
+
*
|
|
1606
|
+
* @group Testing
|
|
1607
|
+
* @category Mocks
|
|
1608
|
+
* @see {@link createMockMember}
|
|
1609
|
+
*/ function createMockRawMember(overrides = {}) {
|
|
1610
|
+
const { permissions, user, ...given } = overrides;
|
|
1611
|
+
return {
|
|
1612
|
+
user: {
|
|
1613
|
+
id: nextSnowflake(),
|
|
1614
|
+
username: 'user',
|
|
1615
|
+
discriminator: '0',
|
|
1616
|
+
global_name: null,
|
|
1617
|
+
avatar: null,
|
|
1618
|
+
...user
|
|
1619
|
+
},
|
|
1620
|
+
roles: [],
|
|
1621
|
+
nick: null,
|
|
1622
|
+
avatar: null,
|
|
1623
|
+
banner: null,
|
|
1624
|
+
premium_since: null,
|
|
1625
|
+
communication_disabled_until: null,
|
|
1626
|
+
joined_at: new Date().toISOString(),
|
|
1627
|
+
deaf: false,
|
|
1628
|
+
mute: false,
|
|
1629
|
+
pending: false,
|
|
1630
|
+
flags: 0,
|
|
1631
|
+
...given,
|
|
1632
|
+
permissions: String(new PermissionsBitField(permissions ?? NEW_SERVER_EVERYONE_PERMISSIONS).bitfield)
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
/** Whether `value` is a member Discord sends from a server the bot isn't in: plain data, not a `GuildMember`. */ function isRawMember(value) {
|
|
1636
|
+
if (typeof value !== 'object' || value === null || value instanceof GuildMember) return false;
|
|
1637
|
+
const { roles, permissions, user } = value;
|
|
1638
|
+
return Array.isArray(roles) && typeof permissions === 'string' && typeof user === 'object' && user !== null;
|
|
1639
|
+
}
|
|
1640
|
+
/** A user option's member as Discord resolves it with an interaction from a server the bot isn't in. */ function rawResolvedMember() {
|
|
1641
|
+
const { user: _user, deaf: _deaf, mute: _mute, ...resolved } = createMockRawMember();
|
|
1642
|
+
return resolved;
|
|
1643
|
+
}
|
|
1497
1644
|
/**
|
|
1498
1645
|
* Creates a mock {@link GuildMember}: a user in a server, with the roles given.
|
|
1499
1646
|
*
|
|
@@ -1644,6 +1791,8 @@ const CHANNEL_TYPE_GUARDS = [
|
|
|
1644
1791
|
if (!Object.prototype.hasOwnProperty.call(item, 'guild')) item.guild = guild;
|
|
1645
1792
|
}
|
|
1646
1793
|
}
|
|
1794
|
+
// Strict mocks cache the bot's member from the start, as the gateway does when the bot joins, so lookups find it
|
|
1795
|
+
if (strictMocks()) void members.me;
|
|
1647
1796
|
}
|
|
1648
1797
|
/** The ids each kind of mention in `content` names, in order and once each. */ function mentionedIds(content) {
|
|
1649
1798
|
const ids = (pattern)=>[
|
|
@@ -1866,7 +2015,8 @@ const CHANNEL_TYPE_GUARDS = [
|
|
|
1866
2015
|
get: ()=>{
|
|
1867
2016
|
const found = cacheOf(instance.channel.threads)?.get(instance.id);
|
|
1868
2017
|
if (found) return found;
|
|
1869
|
-
|
|
2018
|
+
if (strictMocks()) return null;
|
|
2019
|
+
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), or call useStrictMocks() to have it read null now.');
|
|
1870
2020
|
return placeholderThread ?? (placeholderThread = stubDeep(Object.create(ThreadChannel.prototype)));
|
|
1871
2021
|
},
|
|
1872
2022
|
set: (value)=>Object.defineProperty(instance, 'thread', {
|
|
@@ -2135,7 +2285,8 @@ const CHANNEL_TYPE_GUARDS = [
|
|
|
2135
2285
|
if (!unowned.has(name)) unowned.set(name, memberIn(undefined, user.id, user));
|
|
2136
2286
|
return unowned.get(name);
|
|
2137
2287
|
}
|
|
2138
|
-
|
|
2288
|
+
const rawOwner = !owner.guild && isRawMember(Object.getOwnPropertyDescriptor(owner, 'member')?.value);
|
|
2289
|
+
if (!members.has(name)) members.set(name, owner.guildId ? rawOwner ? rawResolvedMember() : memberFor(owner.guild, user, true) : null);
|
|
2139
2290
|
return members.get(name) ?? null;
|
|
2140
2291
|
};
|
|
2141
2292
|
base.getUser = createMockFn((name, required)=>{
|
|
@@ -2194,4 +2345,4 @@ const CHANNEL_TYPE_GUARDS = [
|
|
|
2194
2345
|
return resolver;
|
|
2195
2346
|
}
|
|
2196
2347
|
|
|
2197
|
-
export { RESPONSE_LOG, createChatInputOptions, createMock, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMember, createMockMessage, createMockUser, memberRoles };
|
|
2348
|
+
export { RESPONSE_LOG, createChatInputOptions, createMock, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMember, createMockMessage, createMockRawMember, createMockUser, isRawMember, memberRoles };
|
|
@@ -42,7 +42,7 @@ const VISIBLE = new Set([
|
|
|
42
42
|
* @group Testing
|
|
43
43
|
* @category Inspection
|
|
44
44
|
* @see {@link TestingModule}
|
|
45
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
45
|
+
* @see {@link https://meocord.dev/docs/4.2/invoke-and-dispatch | Invoke and dispatch}
|
|
46
46
|
*/ function getResponse(interaction) {
|
|
47
47
|
const state = existingResponse(interaction);
|
|
48
48
|
// A mock records every answer it gets; respond()'s edits through the channel are its own, so they are merged in
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { findComponentRouteConflicts, buildComponentRoutes, matchComponentRoute } from '../core/component-routes.js';
|
|
1
|
+
import { findComponentRouteConflicts, buildComponentRoutes, matchComponentRoute, readCustomId } from '../core/component-routes.js';
|
|
2
2
|
import { isCustomIdRouted } from '../util/interaction.util.js';
|
|
3
3
|
import { staticMessageStarts, matchMessageRoute, buildMessageRoutes, fitsScope } from '../core/message-routes.js';
|
|
4
4
|
import { needsGuild } from '../core/message-params.js';
|
|
@@ -47,7 +47,7 @@ import { META } from '../util/metadata-keys.js';
|
|
|
47
47
|
* @group Testing
|
|
48
48
|
* @category Inspection
|
|
49
49
|
* @see {@link findRouteConflicts}
|
|
50
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
50
|
+
* @see {@link https://meocord.dev/docs/4.2/components | Buttons, selects and modals}
|
|
51
51
|
*/ function resolveRoute(app, input) {
|
|
52
52
|
if ('content' in input) {
|
|
53
53
|
const { messages = {} } = appOptionsOf(app);
|
|
@@ -73,7 +73,10 @@ import { META } from '../util/metadata-keys.js';
|
|
|
73
73
|
const { route, params, text } = matched;
|
|
74
74
|
const method = route.meta.methodName;
|
|
75
75
|
const typed = Object.keys(route.types).length > 0;
|
|
76
|
-
|
|
76
|
+
// The routes ranked behind it that the customId matches too, which dispatch never reaches for it
|
|
77
|
+
const behind = routes.slice(routes.indexOf(route) + 1);
|
|
78
|
+
const alsoMatches = behind.filter((other)=>other.meta.type === input.type && readCustomId(other.pattern, other.meta.regex, input.customId)).map((other)=>other.pattern);
|
|
79
|
+
const resolved = {
|
|
77
80
|
controller: route.controllerClass,
|
|
78
81
|
method,
|
|
79
82
|
handler: route.controllerClass.prototype[method],
|
|
@@ -82,28 +85,34 @@ import { META } from '../util/metadata-keys.js';
|
|
|
82
85
|
values: params
|
|
83
86
|
}
|
|
84
87
|
};
|
|
88
|
+
return alsoMatches.length > 0 ? readableOnly(resolved, {
|
|
89
|
+
alsoMatches
|
|
90
|
+
}) : resolved;
|
|
85
91
|
}
|
|
86
92
|
/**
|
|
87
|
-
* Finds component patterns that can match the same `customId`, which MeoCord otherwise only warns
|
|
93
|
+
* Finds component patterns that rank equally and can match the same `customId`, which MeoCord otherwise only warns
|
|
94
|
+
* about at startup.
|
|
88
95
|
*
|
|
89
|
-
* Use it in a test to keep the warning from reaching production
|
|
90
|
-
*
|
|
96
|
+
* Use it in a test to keep the warning from reaching production. Patterns rank segment by segment, a literal before a
|
|
97
|
+
* param and then the narrower type, so two collide only with the same literals and equally narrow params at each
|
|
98
|
+
* position, such as `{action:close|reopen}` and `{step:close|confirm}`, which both take `close`; the one listed
|
|
99
|
+
* first runs. Patterns are compared within a component type.
|
|
91
100
|
*
|
|
92
101
|
* @param app - The application class decorated with `@MeoCord`.
|
|
93
|
-
* @returns Each pair of patterns that can match the same `customId`, with its component type.
|
|
102
|
+
* @returns Each pair of patterns that rank equally and can match the same `customId`, with its component type.
|
|
94
103
|
*
|
|
95
104
|
* @example
|
|
96
105
|
* ```ts
|
|
97
106
|
* import { expect } from 'vitest'
|
|
98
107
|
*
|
|
99
108
|
* @Controller()
|
|
100
|
-
* class
|
|
101
|
-
* @Command('
|
|
102
|
-
* async
|
|
103
|
-
* @Command('
|
|
104
|
-
* async
|
|
109
|
+
* class TicketController {
|
|
110
|
+
* @Command('ticket/{action:close|reopen}', CommandType.BUTTON)
|
|
111
|
+
* async act(interaction: ButtonInteraction) { await respond(interaction).send('Done.') }
|
|
112
|
+
* @Command('ticket/{step:close|confirm}', CommandType.BUTTON)
|
|
113
|
+
* async step(interaction: ButtonInteraction) { await respond(interaction).send('Next.') }
|
|
105
114
|
* }
|
|
106
|
-
* @MeoCord({ controllers: [
|
|
115
|
+
* @MeoCord({ controllers: [TicketController], clientOptions: { intents: [] } })
|
|
107
116
|
* class App {}
|
|
108
117
|
* expect(findRouteConflicts(App)).toHaveLength(1)
|
|
109
118
|
* ```
|
|
@@ -112,7 +121,23 @@ import { META } from '../util/metadata-keys.js';
|
|
|
112
121
|
* @category Inspection
|
|
113
122
|
* @see {@link resolveRoute}
|
|
114
123
|
*/ function findRouteConflicts(app) {
|
|
115
|
-
return findComponentRouteConflicts(buildComponentRoutes(controllersOf(app)))
|
|
124
|
+
return findComponentRouteConflicts(buildComponentRoutes(controllersOf(app))).map(({ type, patterns, runs })=>readableOnly({
|
|
125
|
+
type: type,
|
|
126
|
+
patterns
|
|
127
|
+
}, {
|
|
128
|
+
runs
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* `target` with `fields` added as properties a test reads but `toEqual` and its kin, which compare enumerable keys,
|
|
133
|
+
* don't see, so an assertion written for the fields it had before still holds.
|
|
134
|
+
*/ function readableOnly(target, fields) {
|
|
135
|
+
for (const [key, value] of Object.entries(fields))Object.defineProperty(target, key, {
|
|
136
|
+
value,
|
|
137
|
+
enumerable: false,
|
|
138
|
+
configurable: true
|
|
139
|
+
});
|
|
140
|
+
return target;
|
|
116
141
|
}
|
|
117
142
|
|
|
118
143
|
export { findRouteConflicts, resolveRoute };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { setStartupErrorsMode } from '../util/refusal.util.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Has decorators keep the startup errors they find, as a bot built with `startupErrors: 'all'` does, so a testing
|
|
5
|
+
* module reports every one at once.
|
|
6
|
+
*
|
|
7
|
+
* Call it once in the test runner's setup file, before any test imports a controller: a decorator runs when its class
|
|
8
|
+
* is defined, as its file is imported. A decorator given what it cannot use then throws nothing as the file loads, and
|
|
9
|
+
* `MeoCordTestingModule.create(...).compile()` logs every startup error of the classes it runs, each with its handler
|
|
10
|
+
* and file, then throws the first. Without it, a decorator throws its error as its class is defined.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // vitest.setup.ts
|
|
15
|
+
* reportAllStartupErrors()
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @group Testing
|
|
19
|
+
* @category Module
|
|
20
|
+
* @see {@link MeoCordTestingModule}
|
|
21
|
+
* @see {@link https://meocord.dev/docs/4.2/configuration | Configuration}
|
|
22
|
+
*/ function reportAllStartupErrors() {
|
|
23
|
+
setStartupErrorsMode('all');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { reportAllStartupErrors };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Whether useStrictMocks() is on, and whether a mock was made before it could be
|
|
2
|
+
let strict = false;
|
|
3
|
+
let mockMade = false;
|
|
4
|
+
/** Whether mocks compute discord.js's state, as {@link useStrictMocks} turns on. */ const strictMocks = ()=>strict;
|
|
5
|
+
/** Notes that a mock was made, which useStrictMocks() must come before. */ function noteMockMade() {
|
|
6
|
+
mockMade = true;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Has every mock from `meocord/testing` compute the values discord.js computes, where it reads a placeholder otherwise.
|
|
10
|
+
*
|
|
11
|
+
* Call it once in the test runner's setup file. A message's `editable`, `deletable`, `pinnable`, `crosspostable`,
|
|
12
|
+
* `bulkDeletable`, `hasThread` and `partial`, a member's `manageable`, `kickable`, `bannable` and `moderatable`, a
|
|
13
|
+
* role's `editable`, a channel's and a thread's `viewable`, `manageable`, `deletable`, `joinable` and the rest, and
|
|
14
|
+
* `partial` on users, channels and reactions then run discord.js's own getters against the mock, and `message.thread`
|
|
15
|
+
* is `null` when the channel caches no thread under the message's id. No placeholder warning is logged.
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* Those getters read what a server holds, so strict mocks also have it as Discord sends it: the bot's member is in
|
|
19
|
+
* the server's member cache from the start, @everyone has the permissions Discord gives it in a new server, and a
|
|
20
|
+
* channel or thread made without a server has one of its own, a thread with a text channel as its parent. A message
|
|
21
|
+
* has empty `content`, a thread is neither archived nor locked, and a reaction's `count` is 1. Give the bot's member
|
|
22
|
+
* a role 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
|
|
23
|
+
* computed one. The next major version (5.0) computes these values without the call.
|
|
24
|
+
*
|
|
25
|
+
* @throws Error when a mock already exists, which was made without it.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* // vitest.setup.ts
|
|
30
|
+
* useStrictMocks()
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @group Testing
|
|
34
|
+
* @category Mocks
|
|
35
|
+
* @see {@link useMockFn}
|
|
36
|
+
* @see {@link https://meocord.dev/docs/4.2/mocks | Mocks}
|
|
37
|
+
*/ function useStrictMocks() {
|
|
38
|
+
if (strict) return;
|
|
39
|
+
if (mockMade) {
|
|
40
|
+
throw new Error('useStrictMocks() goes before any mock is made: a mock made before it reads placeholders, and the two kinds would ' + "mix. Call it once, in the test runner's setup file.");
|
|
41
|
+
}
|
|
42
|
+
strict = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { noteMockMade, strictMocks, useStrictMocks };
|