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
|
@@ -7,7 +7,7 @@ import { CooldownStore, MemoryCooldownStore } from '../common/cooldown-store.js'
|
|
|
7
7
|
import { ExecutionContext } from '../common/execution-context.js';
|
|
8
8
|
import { Translator, missingTranslatorError } from '../common/translator.js';
|
|
9
9
|
import { injectedTokens, singletonContextError } from '../core/guard-runner.js';
|
|
10
|
-
import { appPresenterOf, runHandler, appStages, bindGlobalStages, prepareHandlerStages, bindAppPresenter } from '../core/handler-pipeline.js';
|
|
10
|
+
import { appPresenterOf, runHandler, appStages, bindGlobalStages, prepareHandlerStages, prepareAppPresenter, prepareThemes, bindAppPresenter } from '../core/handler-pipeline.js';
|
|
11
11
|
import { setPresenter } from '../common/response/presenter.js';
|
|
12
12
|
import { componentRouteFor, commandMismatch, handlerInput } from '../core/handler-input.js';
|
|
13
13
|
import { buildComponentRoutes } from '../core/component-routes.js';
|
|
@@ -21,11 +21,11 @@ import { messageParamsFor, buildMessageRoutes } from '../core/message-routes.js'
|
|
|
21
21
|
import { assertDistinctCommands, warnUnregisteredCommands, warnInheritedRoutes, warnOverlappingPatterns, warnHandlersOffControllers } from '../core/command-conflicts.js';
|
|
22
22
|
import { messageCommandHooks } from '../core/message-params.js';
|
|
23
23
|
import { appObservers, assertObservers, bindObservers } from '../core/observer-runner.js';
|
|
24
|
-
import { makeInjectable } from '../util/injectable.util.js';
|
|
25
24
|
import { HandlerRegistry } from '../core/handler-registry.js';
|
|
26
25
|
import { adviseInPlaceOfInjecting, meocordClasses } from '../core/meocord-classes.js';
|
|
27
26
|
import { startupClasses, assertStartupClasses, lateGuardCheck } from '../core/startup-roots.js';
|
|
28
27
|
import { markStartupChecked, assertStartupChecked } from '../core/startup-checked.js';
|
|
28
|
+
import { makeInjectable } from '../util/injectable.util.js';
|
|
29
29
|
import { ShardContext, shardCallHandler } from '../core/shard-context.js';
|
|
30
30
|
import { isAppClassToken } from '../core/lifecycle-order.js';
|
|
31
31
|
import { lifecycleEntry, runShutdownSequence, runReadyHooks, callsSettled } from '../core/lifecycle-hooks.js';
|
|
@@ -33,6 +33,8 @@ import { createMockClient } from './mock-interaction.js';
|
|
|
33
33
|
import { Dispatcher } from '../core/dispatcher.js';
|
|
34
34
|
import { createFallback } from '../core/fallback.js';
|
|
35
35
|
import { Logger } from '../common/logger.js';
|
|
36
|
+
import { isExplainedError, markExplained } from '../common/explained-error.js';
|
|
37
|
+
import { declaredErrorsOf, collectStartupErrors, startupError, stopOnStartupErrors, isRefusal, logStartupErrors } from '../util/refusal.util.js';
|
|
36
38
|
import { DEFAULT_SHUTDOWN_TIMEOUT_MS, shutdownTimeoutProblem } from '../util/shutdown-timeout.util.js';
|
|
37
39
|
import { resolveProviders, tokenName, bindsOwnToken, providerMap, bindProvider, resolutionOrder, isClassProvider, assertProvided, tokenDependencies } from '../core/providers.js';
|
|
38
40
|
import { getEventHandlers } from '../decorator/event.decorator.js';
|
|
@@ -69,7 +71,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
69
71
|
* @category Module
|
|
70
72
|
* @see {@link MeoCordTestingModule}
|
|
71
73
|
* @see {@link getResponse}
|
|
72
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
74
|
+
* @see {@link https://meocord.dev/docs/4.2/invoke-and-dispatch | Invoke and dispatch}
|
|
73
75
|
*/ class TestingModule {
|
|
74
76
|
/**
|
|
75
77
|
* Resolves the module's `useFactory` providers, awaiting those that return a promise, in dependency
|
|
@@ -343,6 +345,8 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
343
345
|
controllerClasses: this.controllers,
|
|
344
346
|
messageOptions: this.messageOptions,
|
|
345
347
|
logger,
|
|
348
|
+
// A test's fake clock can pass 5 s inside a handler, so the slow-handler warning waits to be asked for
|
|
349
|
+
warnSlowMessageHandlers: false,
|
|
346
350
|
// Strict: an answer MeoCord fails to build rejects the dispatch, where a bot logs it and carries on
|
|
347
351
|
fallback: createFallback(logger, ()=>this.messageOptions, {
|
|
348
352
|
strict: true,
|
|
@@ -509,7 +513,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
509
513
|
* @group Testing
|
|
510
514
|
* @category Module
|
|
511
515
|
* @see {@link MeoCordTestingModule}
|
|
512
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
516
|
+
* @see {@link https://meocord.dev/docs/4.2/testing | The testing module}
|
|
513
517
|
*/ class TestingModuleBuilder {
|
|
514
518
|
/**
|
|
515
519
|
* Replaces a provider with a test double.
|
|
@@ -657,199 +661,236 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
657
661
|
* its command's builder does not register, a customId pattern given as a command name, a builder that registers
|
|
658
662
|
* another name, and component patterns that can match the same customId are named, as the bot names them.
|
|
659
663
|
*
|
|
664
|
+
* Each check reports every startup error it finds, and the first is thrown, unchanged; when there are several, each
|
|
665
|
+
* is logged first, with the file it comes from. After {@link reportAllStartupErrors}, the errors decorators kept on
|
|
666
|
+
* the classes the module runs are among them.
|
|
667
|
+
*
|
|
660
668
|
* @returns The compiled module.
|
|
669
|
+
* @throws Error for the first startup error the checks find.
|
|
661
670
|
*/ compile() {
|
|
662
671
|
// The bound the bot's config gives it, with the same words
|
|
663
672
|
const shutdownTimeout = this.options.shutdownTimeout === undefined ? undefined : shutdownTimeoutProblem(this.options.shutdownTimeout);
|
|
664
673
|
if (shutdownTimeout) throw new TypeError(`shutdownTimeout ${shutdownTimeout}.`);
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const appClasses = [];
|
|
670
|
-
const translator = ()=>container.isBound(Translator) ? container.get(Translator) : undefined;
|
|
671
|
-
container.bind(HandlerRegistry).toConstantValue(new HandlerRegistry(appClasses, messagesOf(this.options.app), translator));
|
|
672
|
-
// A testing module runs as one process, so a cross-shard call runs once, here
|
|
673
|
-
container.bind(ShardContext).toConstantValue(new ShardContext(undefined, shardCallHandler(container, ()=>[
|
|
674
|
-
...appClasses,
|
|
675
|
-
...[
|
|
676
|
-
...providers.keys()
|
|
677
|
-
].filter(isAppClassToken)
|
|
678
|
-
], 'this testing module')));
|
|
679
|
-
bindsOwnToken(container, HandlerRegistry);
|
|
680
|
-
bindsOwnToken(container, ShardContext);
|
|
681
|
-
// Checked as the app checks its own: the app's providers in their order, then the test's, which replace them
|
|
682
|
-
// by token, then the overrides, which win
|
|
683
|
-
const providers = providerMap(this.wiring?.providers ?? [], '@MeoCord({ providers })');
|
|
684
|
-
for (const [token, provider] of providerMap(this.options.providers ?? [], "the testing module's providers"))providers.set(token, provider);
|
|
685
|
-
for (const [token, override] of this.overrides)providers.set(token, override);
|
|
686
|
-
const services = this.wiring?.services ?? [];
|
|
687
|
-
const appOptions = this.options.app ? Reflect.getMetadata(META.appOptions, this.options.app) : undefined;
|
|
688
|
-
// The app's own store, as the bot binds it, whether the module is given the app or built from it; none when the
|
|
689
|
-
// test provides the CooldownStore, so nothing builds the app's or asks for what it injects
|
|
690
|
-
const store = providers.has(CooldownStore) ? undefined : appOptions?.cooldownStore;
|
|
691
|
-
// The themeFor class this module runs, the app's or overrideThemeFor's, bound on its own as the app binds it
|
|
692
|
-
const themeResolver = themeResolverClass(stages?.themeFor?.resolvers);
|
|
693
|
-
// Every class the module runs, as the app finds them, except stage classes an override stands in for
|
|
694
|
-
const i18n = this.options.app && Reflect.getMetadata(META.appOptions, this.options.app)?.i18n;
|
|
695
|
-
const startup = startupClasses({
|
|
696
|
-
controllers: this.options.controllers ?? [],
|
|
697
|
-
services,
|
|
698
|
-
providers,
|
|
699
|
-
stages,
|
|
700
|
-
presenter: this.options.app && Reflect.getMetadata(META.appOptions, this.options.app)?.presenter,
|
|
701
|
-
cooldownStore: store,
|
|
702
|
-
themeFor: stages?.themeFor?.resolvers,
|
|
703
|
-
observers: [
|
|
704
|
-
...this.options.app ? appObservers(this.options.app) : [],
|
|
705
|
-
...this.options.observers ?? []
|
|
706
|
-
],
|
|
707
|
-
stubs: [
|
|
708
|
-
...this.guardOverrides.keys(),
|
|
709
|
-
...this.interceptorOverrides.keys(),
|
|
710
|
-
...this.filterOverrides.keys()
|
|
711
|
-
]
|
|
712
|
-
});
|
|
713
|
-
const hasTranslator = i18n !== undefined || providers.has(Translator);
|
|
714
|
-
assertStartupClasses(startup, {
|
|
715
|
-
translator: hasTranslator
|
|
716
|
-
});
|
|
717
|
-
markStartupChecked(container, [
|
|
718
|
-
...startup.classes,
|
|
719
|
-
...meocordClasses()
|
|
720
|
-
], lateGuardCheck(providers, hasTranslator));
|
|
721
|
-
// The bot binds the Client it logs in with; a test gives its own, and is told so where one is needed
|
|
722
|
-
const needClient = startup.classes.filter((cls)=>injectedTokens(cls).includes(Client));
|
|
723
|
-
if (needClient.length > 0 && !providers.has(Client)) {
|
|
724
|
-
container.bind(Client).toDynamicValue(()=>{
|
|
725
|
-
throw new Error(`${needClient.map((cls)=>cls.name).join(', ')} ${needClient.length === 1 ? 'injects' : 'inject'} the Discord Client, ` + 'which a testing module does not make: give one in its providers, such as ' + '{ provide: Client, useValue: createMockClient() }.');
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
// Bind guard overrides — prevents inversify from auto-wiring guard dependencies
|
|
729
|
-
for (const [guardClass, stub] of this.guardOverrides){
|
|
730
|
-
container.bind(guardClass).toConstantValue(stub);
|
|
731
|
-
}
|
|
732
|
-
for (const [filterClass, stub] of this.filterOverrides){
|
|
733
|
-
container.bind(filterClass).toConstantValue(stub);
|
|
734
|
-
}
|
|
735
|
-
for (const [interceptorClass, stub] of this.interceptorOverrides){
|
|
736
|
-
container.bind(interceptorClass).toConstantValue(stub);
|
|
737
|
-
}
|
|
738
|
-
// The app's cooldown policy, so a test of a failing store sees what the bot would do
|
|
739
|
-
if (appOptions && !container.isBound(COOLDOWN_POLICY)) container.bind(COOLDOWN_POLICY).toConstantValue(cooldownPolicyFrom(appOptions));
|
|
740
|
-
// The app's translator, unless a provider stands in for it
|
|
741
|
-
if (i18n && !providers.has(Translator)) {
|
|
742
|
-
container.bind(Translator).toConstantValue(i18n);
|
|
743
|
-
bindsOwnToken(container, Translator);
|
|
744
|
-
}
|
|
745
|
-
// Recursively bind controllers and their dependencies, skipping already-bound tokens
|
|
746
|
-
const bindClass = (cls)=>{
|
|
747
|
-
// A provided class is bound by its own provider, wherever in the list that provider comes
|
|
748
|
-
if (container.isBound(cls) || providers.has(cls)) return;
|
|
749
|
-
if (injectedTokens(cls).includes(ExecutionContext)) throw singletonContextError(cls);
|
|
750
|
-
assertStartupChecked(container, cls);
|
|
751
|
-
makeInjectable(cls);
|
|
752
|
-
container.bind(cls).toSelf().inSingletonScope();
|
|
753
|
-
// By constructor type or @inject token, as the app binds them
|
|
754
|
-
for (const dep of injectedTokens(cls)){
|
|
755
|
-
if (dep === Translator && !container.isBound(Translator)) throw missingTranslatorError(cls);
|
|
756
|
-
if (isAppClassToken(dep)) bindClass(dep);
|
|
757
|
-
}
|
|
758
|
-
};
|
|
759
|
-
// The app's own store, resolved like a service, unless the test provides the CooldownStore itself; bound first, as the
|
|
760
|
-
// app binds it, so a class that injects the token gets the store
|
|
761
|
-
if (!providers.has(CooldownStore)) {
|
|
762
|
-
if (store) {
|
|
763
|
-
bindClass(store);
|
|
764
|
-
container.bind(CooldownStore).toService(store);
|
|
765
|
-
} else {
|
|
766
|
-
container.bind(CooldownStore).toConstantValue(new MemoryCooldownStore());
|
|
767
|
-
}
|
|
768
|
-
bindsOwnToken(container, CooldownStore, store);
|
|
769
|
-
}
|
|
770
|
-
// Before the controllers, so a class token that is provided is not also bound as itself
|
|
771
|
-
for (const provider of providers.values())bindProvider(container, provider, bindClass);
|
|
772
|
-
for (const ctrl of this.options.controllers ?? []){
|
|
773
|
-
bindClass(ctrl);
|
|
774
|
-
// Stamp container on controller class so @UseGuard works in tests too
|
|
775
|
-
Reflect.defineMetadata(META.container, container, ctrl);
|
|
776
|
-
}
|
|
777
|
-
for (const service of services)bindClass(service);
|
|
778
|
-
if (themeResolver) bindClass(themeResolver);
|
|
779
|
-
// The classes whose @On and @Once handlers emit reaches: class providers bound as themselves, the
|
|
780
|
-
// controllers, and what they inject; factories resolve in the same order
|
|
781
|
-
const order = resolutionOrder(container, providers, [
|
|
782
|
-
...providers.keys(),
|
|
783
|
-
...services,
|
|
784
|
-
...themeResolver ? [
|
|
785
|
-
themeResolver
|
|
786
|
-
] : [],
|
|
787
|
-
...this.options.controllers ?? []
|
|
788
|
-
], {
|
|
789
|
-
followOwnTokens: false
|
|
790
|
-
});
|
|
791
|
-
appClasses.push(...order.filter((token)=>{
|
|
792
|
-
const provider = providers.get(token);
|
|
793
|
-
return isAppClassToken(token) && (!provider || isClassProvider(provider) && provider.useClass === token);
|
|
794
|
-
}));
|
|
795
|
-
assertProvided(container, providers, startup.classes, "the testing module's providers");
|
|
796
|
-
// Every class the module runs, so @UseGuard resolves guards on a direct call to any of them
|
|
797
|
-
for (const cls of startup.classes)Reflect.defineMetadata(META.container, container, cls);
|
|
798
|
-
prepareHandlerStages(container, appClasses);
|
|
799
|
-
const messages = messagesOf(this.options.app);
|
|
800
|
-
// As the app would at startup, refuses a message pattern that cannot be read or two that match the same messages
|
|
801
|
-
buildMessageRoutes(this.options.controllers ?? [], messages);
|
|
802
|
-
// As the app does when it is created, so a test sees the refusal the bot would give
|
|
803
|
-
assertDistinctCommands(this.options.controllers ?? []);
|
|
804
|
-
// A handler with no builder is how a fixture is written, so only what is always a mistake is named
|
|
805
|
-
warnUnregisteredCommands(this.options.controllers ?? [], {
|
|
806
|
-
missingBuilders: false
|
|
807
|
-
});
|
|
808
|
-
warnInheritedRoutes(this.options.controllers ?? []);
|
|
809
|
-
warnOverlappingPatterns(this.options.controllers ?? []);
|
|
810
|
-
warnHandlersOffControllers(this.options.controllers ?? [], appClasses);
|
|
811
|
-
if (this.options.app) bindAppPresenter(container, this.options.app);
|
|
812
|
-
const observers = [
|
|
813
|
-
...this.options.app ? appObservers(this.options.app) : [],
|
|
814
|
-
...this.options.observers ?? []
|
|
815
|
-
];
|
|
816
|
-
assertObservers("the testing module's observers", observers);
|
|
817
|
-
bindObservers(container, observers);
|
|
818
|
-
// The store calls ask: the test's own when it provides CooldownStore, else the app's
|
|
819
|
-
const boundStore = store ?? (providers.has(CooldownStore) ? CooldownStore : undefined);
|
|
820
|
-
// The order the app runs lifecycle hooks in: its cooldown store, then providers, controllers and observers, each
|
|
821
|
-
// after what it injects
|
|
822
|
-
const lifecycle = resolutionOrder(container, providers, [
|
|
823
|
-
...boundStore ? [
|
|
824
|
-
boundStore
|
|
825
|
-
] : [],
|
|
826
|
-
...providers.keys(),
|
|
827
|
-
...services,
|
|
828
|
-
...themeResolver ? [
|
|
829
|
-
themeResolver
|
|
674
|
+
// With startupErrors: 'all', what the decorators kept on the classes this module runs
|
|
675
|
+
const declared = declaredErrorsOf([
|
|
676
|
+
...this.options.app ? [
|
|
677
|
+
this.options.app
|
|
830
678
|
] : [],
|
|
831
679
|
...this.options.controllers ?? [],
|
|
832
|
-
...
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
680
|
+
...this.wiring?.services ?? [],
|
|
681
|
+
...this.options.observers ?? []
|
|
682
|
+
]);
|
|
683
|
+
// Every error each check finds is reported, as the bot reports them, and the first is thrown
|
|
684
|
+
try {
|
|
685
|
+
return collectStartupErrors(()=>{
|
|
686
|
+
declared.forEach(startupError);
|
|
687
|
+
const container = new Container();
|
|
688
|
+
const stages = this.globalStages();
|
|
689
|
+
if (stages) bindGlobalStages(container, stages);
|
|
690
|
+
// Bound first, as in the app, so a class that injects it gets this instance
|
|
691
|
+
const appClasses = [];
|
|
692
|
+
const translator = ()=>container.isBound(Translator) ? container.get(Translator) : undefined;
|
|
693
|
+
container.bind(HandlerRegistry).toConstantValue(new HandlerRegistry(appClasses, messagesOf(this.options.app), translator));
|
|
694
|
+
// A testing module runs as one process, so a cross-shard call runs once, here
|
|
695
|
+
container.bind(ShardContext).toConstantValue(new ShardContext(undefined, shardCallHandler(container, ()=>[
|
|
696
|
+
...appClasses,
|
|
697
|
+
...[
|
|
698
|
+
...providers.keys()
|
|
699
|
+
].filter(isAppClassToken)
|
|
700
|
+
], 'this testing module')));
|
|
701
|
+
bindsOwnToken(container, HandlerRegistry);
|
|
702
|
+
bindsOwnToken(container, ShardContext);
|
|
703
|
+
// Checked as the app checks its own: the app's providers in their order, then the test's, which replace them
|
|
704
|
+
// by token, then the overrides, which win
|
|
705
|
+
const providers = providerMap(this.wiring?.providers ?? [], '@MeoCord({ providers })');
|
|
706
|
+
for (const [token, provider] of providerMap(this.options.providers ?? [], "the testing module's providers"))providers.set(token, provider);
|
|
707
|
+
for (const [token, override] of this.overrides)providers.set(token, override);
|
|
708
|
+
// Each pass of checks ends before the next step that is no check, so a module with one error stops where it always did
|
|
709
|
+
stopOnStartupErrors();
|
|
710
|
+
const services = this.wiring?.services ?? [];
|
|
711
|
+
const appOptions = this.options.app ? Reflect.getMetadata(META.appOptions, this.options.app) : undefined;
|
|
712
|
+
// The app's own store, as the bot binds it, whether the module is given the app or built from it; none when the
|
|
713
|
+
// test provides the CooldownStore, so nothing builds the app's or asks for what it injects
|
|
714
|
+
const store = providers.has(CooldownStore) ? undefined : appOptions?.cooldownStore;
|
|
715
|
+
// The themeFor class this module runs, the app's or overrideThemeFor's, bound on its own as the app binds it
|
|
716
|
+
const themeResolver = themeResolverClass(stages?.themeFor?.resolvers);
|
|
717
|
+
// Every class the module runs, as the app finds them, except stage classes an override stands in for
|
|
718
|
+
const i18n = this.options.app && Reflect.getMetadata(META.appOptions, this.options.app)?.i18n;
|
|
719
|
+
const startup = startupClasses({
|
|
720
|
+
controllers: this.options.controllers ?? [],
|
|
721
|
+
services,
|
|
722
|
+
providers,
|
|
723
|
+
stages,
|
|
724
|
+
presenter: this.options.app && Reflect.getMetadata(META.appOptions, this.options.app)?.presenter,
|
|
725
|
+
cooldownStore: store,
|
|
726
|
+
themeFor: stages?.themeFor?.resolvers,
|
|
727
|
+
observers: [
|
|
728
|
+
...this.options.app ? appObservers(this.options.app) : [],
|
|
729
|
+
...this.options.observers ?? []
|
|
730
|
+
],
|
|
731
|
+
stubs: [
|
|
732
|
+
...this.guardOverrides.keys(),
|
|
733
|
+
...this.interceptorOverrides.keys(),
|
|
734
|
+
...this.filterOverrides.keys()
|
|
735
|
+
]
|
|
736
|
+
});
|
|
737
|
+
const hasTranslator = i18n !== undefined || providers.has(Translator);
|
|
738
|
+
assertStartupClasses(startup, {
|
|
739
|
+
translator: hasTranslator
|
|
740
|
+
});
|
|
741
|
+
// Those of the stages, guards and other classes they reach
|
|
742
|
+
declaredErrorsOf(startup.classes).forEach(startupError);
|
|
743
|
+
stopOnStartupErrors();
|
|
744
|
+
markStartupChecked(container, [
|
|
745
|
+
...startup.classes,
|
|
746
|
+
...meocordClasses()
|
|
747
|
+
], lateGuardCheck(providers, hasTranslator));
|
|
748
|
+
// The bot binds the Client it logs in with; a test gives its own, and is told so where one is needed
|
|
749
|
+
const needClient = startup.classes.filter((cls)=>injectedTokens(cls).includes(Client));
|
|
750
|
+
if (needClient.length > 0 && !providers.has(Client)) {
|
|
751
|
+
container.bind(Client).toDynamicValue(()=>{
|
|
752
|
+
throw new Error(`${needClient.map((cls)=>cls.name).join(', ')} ${needClient.length === 1 ? 'injects' : 'inject'} the Discord Client, ` + 'which a testing module does not make: give one in its providers, such as ' + '{ provide: Client, useValue: createMockClient() }.');
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
// Bind guard overrides — prevents inversify from auto-wiring guard dependencies
|
|
756
|
+
for (const [guardClass, stub] of this.guardOverrides){
|
|
757
|
+
container.bind(guardClass).toConstantValue(stub);
|
|
758
|
+
}
|
|
759
|
+
for (const [filterClass, stub] of this.filterOverrides){
|
|
760
|
+
container.bind(filterClass).toConstantValue(stub);
|
|
761
|
+
}
|
|
762
|
+
for (const [interceptorClass, stub] of this.interceptorOverrides){
|
|
763
|
+
container.bind(interceptorClass).toConstantValue(stub);
|
|
764
|
+
}
|
|
765
|
+
// The app's cooldown policy, so a test of a failing store sees what the bot would do
|
|
766
|
+
if (appOptions && !container.isBound(COOLDOWN_POLICY)) container.bind(COOLDOWN_POLICY).toConstantValue(cooldownPolicyFrom(appOptions));
|
|
767
|
+
// The app's translator, unless a provider stands in for it
|
|
768
|
+
if (i18n && !providers.has(Translator)) {
|
|
769
|
+
container.bind(Translator).toConstantValue(i18n);
|
|
770
|
+
bindsOwnToken(container, Translator);
|
|
839
771
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
772
|
+
// Recursively bind controllers and their dependencies, skipping already-bound tokens
|
|
773
|
+
const bindClass = (cls)=>{
|
|
774
|
+
// A provided class is bound by its own provider, wherever in the list that provider comes
|
|
775
|
+
if (container.isBound(cls) || providers.has(cls)) return;
|
|
776
|
+
if (injectedTokens(cls).includes(ExecutionContext)) return startupError(singletonContextError(cls));
|
|
777
|
+
assertStartupChecked(container, cls);
|
|
778
|
+
makeInjectable(cls);
|
|
779
|
+
container.bind(cls).toSelf().inSingletonScope();
|
|
780
|
+
// By constructor type or @inject token, as the app binds them
|
|
781
|
+
for (const dep of injectedTokens(cls)){
|
|
782
|
+
if (dep === Translator && !container.isBound(Translator)) startupError(missingTranslatorError(cls));
|
|
783
|
+
else if (isAppClassToken(dep)) bindClass(dep);
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
// The app's own store, resolved like a service, unless the test provides the CooldownStore itself; bound first, as the
|
|
787
|
+
// app binds it, so a class that injects the token gets the store
|
|
788
|
+
if (!providers.has(CooldownStore)) {
|
|
789
|
+
if (store) {
|
|
790
|
+
bindClass(store);
|
|
791
|
+
container.bind(CooldownStore).toService(store);
|
|
792
|
+
} else {
|
|
793
|
+
container.bind(CooldownStore).toConstantValue(new MemoryCooldownStore());
|
|
794
|
+
}
|
|
795
|
+
bindsOwnToken(container, CooldownStore, store);
|
|
796
|
+
}
|
|
797
|
+
// Before the controllers, so a class token that is provided is not also bound as itself
|
|
798
|
+
for (const provider of providers.values())bindProvider(container, provider, bindClass);
|
|
799
|
+
for (const ctrl of this.options.controllers ?? [])bindClass(ctrl);
|
|
800
|
+
for (const service of services)bindClass(service);
|
|
801
|
+
if (themeResolver) bindClass(themeResolver);
|
|
802
|
+
// What follows walks what these bound, so a class refused here is not reported again as missing
|
|
803
|
+
stopOnStartupErrors();
|
|
804
|
+
// The classes whose @On and @Once handlers emit reaches: class providers bound as themselves, the
|
|
805
|
+
// controllers, and what they inject; factories resolve in the same order
|
|
806
|
+
const order = resolutionOrder(container, providers, [
|
|
807
|
+
...providers.keys(),
|
|
808
|
+
...services,
|
|
809
|
+
...themeResolver ? [
|
|
810
|
+
themeResolver
|
|
811
|
+
] : [],
|
|
812
|
+
...this.options.controllers ?? []
|
|
813
|
+
], {
|
|
814
|
+
followOwnTokens: false
|
|
815
|
+
});
|
|
816
|
+
appClasses.push(...order.filter((token)=>{
|
|
817
|
+
const provider = providers.get(token);
|
|
818
|
+
return isAppClassToken(token) && (!provider || isClassProvider(provider) && provider.useClass === token);
|
|
819
|
+
}));
|
|
820
|
+
assertProvided(container, providers, startup.classes, "the testing module's providers");
|
|
821
|
+
prepareHandlerStages(container, appClasses);
|
|
822
|
+
const messages = messagesOf(this.options.app);
|
|
823
|
+
// As the app would at startup, refuses a message pattern that cannot be read or two that match the same messages
|
|
824
|
+
buildMessageRoutes(this.options.controllers ?? [], messages);
|
|
825
|
+
// As the app does when it is created, so a test sees the refusal the bot would give
|
|
826
|
+
assertDistinctCommands(this.options.controllers ?? []);
|
|
827
|
+
if (this.options.app) prepareAppPresenter(container, this.options.app);
|
|
828
|
+
const observers = [
|
|
829
|
+
...this.options.app ? appObservers(this.options.app) : [],
|
|
830
|
+
...this.options.observers ?? []
|
|
831
|
+
];
|
|
832
|
+
assertObservers("the testing module's observers", observers);
|
|
833
|
+
// Only @Observer classes are bound as observers
|
|
834
|
+
stopOnStartupErrors();
|
|
835
|
+
bindObservers(container, observers);
|
|
836
|
+
// The store calls ask: the test's own when it provides CooldownStore, else the app's
|
|
837
|
+
const boundStore = store ?? (providers.has(CooldownStore) ? CooldownStore : undefined);
|
|
838
|
+
// The order the app runs lifecycle hooks in: its cooldown store, then providers, controllers and observers, each
|
|
839
|
+
// after what it injects
|
|
840
|
+
const lifecycle = resolutionOrder(container, providers, [
|
|
841
|
+
...boundStore ? [
|
|
842
|
+
boundStore
|
|
843
|
+
] : [],
|
|
844
|
+
...providers.keys(),
|
|
845
|
+
...services,
|
|
846
|
+
...themeResolver ? [
|
|
847
|
+
themeResolver
|
|
848
|
+
] : [],
|
|
849
|
+
...this.options.controllers ?? [],
|
|
850
|
+
...observers
|
|
851
|
+
]).map((token)=>({
|
|
852
|
+
token,
|
|
853
|
+
name: tokenName(token),
|
|
854
|
+
dependencies: tokenDependencies(container, providers, token),
|
|
855
|
+
...token === boundStore && {
|
|
856
|
+
cooldownStore: true
|
|
857
|
+
}
|
|
858
|
+
}));
|
|
859
|
+
// Every check has run, and touched only the container: from here the module is made, once none found an error
|
|
860
|
+
stopOnStartupErrors();
|
|
861
|
+
// Every class the module runs, so @UseGuard resolves guards on a direct call to any of them
|
|
862
|
+
for (const cls of [
|
|
863
|
+
...this.options.controllers ?? [],
|
|
864
|
+
...startup.classes
|
|
865
|
+
])Reflect.defineMetadata(META.container, container, cls);
|
|
866
|
+
prepareThemes(container, appClasses);
|
|
867
|
+
// A handler with no builder is how a fixture is written, so only what is always a mistake is named
|
|
868
|
+
warnUnregisteredCommands(this.options.controllers ?? [], {
|
|
869
|
+
missingBuilders: false
|
|
870
|
+
});
|
|
871
|
+
warnInheritedRoutes(this.options.controllers ?? []);
|
|
872
|
+
warnOverlappingPatterns(this.options.controllers ?? []);
|
|
873
|
+
warnHandlersOffControllers(this.options.controllers ?? [], appClasses);
|
|
874
|
+
if (this.options.app) bindAppPresenter(container, this.options.app);
|
|
875
|
+
// Recorded as the container makes each one, so close() shuts down exactly what exists
|
|
876
|
+
const constructed = new Set();
|
|
877
|
+
for (const { token } of lifecycle){
|
|
878
|
+
container.onActivation(token, (_context, instance)=>{
|
|
879
|
+
constructed.add(token);
|
|
880
|
+
return instance;
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
const warnUnanswered = this.options.app && Reflect.getMetadata(META.appOptions, this.options.app)?.warnUnanswered;
|
|
884
|
+
return new TestingModule(container, [
|
|
885
|
+
...this.options.controllers ?? []
|
|
886
|
+
], appClasses, providers, order, messages, lifecycle, constructed, warnUnanswered, services, this.options.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT_MS);
|
|
847
887
|
});
|
|
888
|
+
} catch (error) {
|
|
889
|
+
if (isRefusal(error) && !isExplainedError(error)) {
|
|
890
|
+
if (logStartupErrors(error, (text)=>new Logger('TestingModule').error(text), 'the testing module did not compile')) markExplained(error);
|
|
891
|
+
}
|
|
892
|
+
throw error;
|
|
848
893
|
}
|
|
849
|
-
const warnUnanswered = this.options.app && Reflect.getMetadata(META.appOptions, this.options.app)?.warnUnanswered;
|
|
850
|
-
return new TestingModule(container, [
|
|
851
|
-
...this.options.controllers ?? []
|
|
852
|
-
], appClasses, providers, order, messages, lifecycle, constructed, warnUnanswered, services, this.options.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT_MS);
|
|
853
894
|
}
|
|
854
895
|
constructor(options, wiring){
|
|
855
896
|
this.options = options;
|
|
@@ -887,7 +928,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
|
|
|
887
928
|
* @category Module
|
|
888
929
|
* @see {@link TestingModuleBuilder}
|
|
889
930
|
* @see {@link TestingModule}
|
|
890
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
931
|
+
* @see {@link https://meocord.dev/docs/4.2/testing | The testing module}
|
|
891
932
|
*/ class MeoCordTestingModule {
|
|
892
933
|
/**
|
|
893
934
|
* Starts building a testing module from the classes a test needs.
|
|
@@ -27,12 +27,70 @@
|
|
|
27
27
|
// Every mock createMockFn made, held weakly so the mocks of a finished test can still be collected
|
|
28
28
|
const created = new Set();
|
|
29
29
|
const collected = new FinalizationRegistry((ref)=>created.delete(ref));
|
|
30
|
+
// The runner's factory useMockFn registered, and for each of its mocks how to give back the implementation it was
|
|
31
|
+
// created with, through the runner's own setter, which a mock's behaviour may wrap
|
|
32
|
+
let runnerFactory;
|
|
33
|
+
const restoreImpl = new WeakMap();
|
|
34
|
+
/**
|
|
35
|
+
* Makes every mock `meocord/testing` creates with the test runner's own mock function, such as Vitest's `vi.fn`.
|
|
36
|
+
*
|
|
37
|
+
* Call it once in the runner's setup file. The runner then treats meocord's mocks as its own: its `clearMocks` and
|
|
38
|
+
* `mockReset` config and `vi.clearAllMocks()` reach them, its matchers read them, and its whole mock API is there.
|
|
39
|
+
* Without it, mocks are {@link createMockFn}'s own, which jest's and Vitest's `expect` read too.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* Each mock starts with the runner's mock holding meocord's behaviour, such as an interaction's replies, as its own
|
|
43
|
+
* implementation. {@link resetAllMocks} puts that behaviour back where the runner's `mockReset` drops it, as jest's and
|
|
44
|
+
* bun's do; a runner's own reset-all, such as jest's `resetMocks`, can't, so under jest and bun reset with
|
|
45
|
+
* {@link resetAllMocks}. node:test's `mock.fn` records calls in a shape of its own, so under node:test leave meocord's
|
|
46
|
+
* own function in place. Calling it again with the same factory does nothing; another, once a mock exists, throws.
|
|
47
|
+
*
|
|
48
|
+
* @param factory - The runner's mock function factory: `vi.fn`, `jest.fn` or bun's `mock`.
|
|
49
|
+
* @throws Error when a mock already exists from another factory, or when `factory` doesn't make a mock with jest's and
|
|
50
|
+
* Vitest's API.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* // vitest.setup.ts
|
|
55
|
+
* import { vi } from 'vitest'
|
|
56
|
+
*
|
|
57
|
+
* useMockFn(vi.fn)
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @group Testing
|
|
61
|
+
* @category Mocks
|
|
62
|
+
* @see {@link createMockFn}
|
|
63
|
+
*/ function useMockFn(factory) {
|
|
64
|
+
if (factory === runnerFactory) return;
|
|
65
|
+
if ([
|
|
66
|
+
...created
|
|
67
|
+
].some((ref)=>ref.deref() !== undefined)) {
|
|
68
|
+
throw new Error("useMockFn() goes before any mock is made: a mock made before it keeps meocord's own function, and the two kinds " + "would mix. Call it once, in the test runner's setup file.");
|
|
69
|
+
}
|
|
70
|
+
const probe = factory();
|
|
71
|
+
const api = [
|
|
72
|
+
'mockImplementation',
|
|
73
|
+
'mockClear',
|
|
74
|
+
'mockReset'
|
|
75
|
+
];
|
|
76
|
+
if (!isMockFunction(probe) || !Array.isArray(probe.mock?.calls) || api.some((name)=>typeof probe[name] !== 'function')) {
|
|
77
|
+
throw new Error("useMockFn() takes a function that makes mocks with jest's and Vitest's API, such as vi.fn, jest.fn or bun's " + "mock. node:test's mock.fn records calls in its own shape, so under node:test leave meocord's own mock function " + 'in place.');
|
|
78
|
+
}
|
|
79
|
+
runnerFactory = factory;
|
|
80
|
+
}
|
|
81
|
+
/** Tracks a mock for clearAllMocks and resetAllMocks. */ function track(mockFn) {
|
|
82
|
+
const ref = new WeakRef(mockFn);
|
|
83
|
+
created.add(ref);
|
|
84
|
+
collected.register(mockFn, ref);
|
|
85
|
+
return mockFn;
|
|
86
|
+
}
|
|
30
87
|
/**
|
|
31
88
|
* Clears the calls every mock from `meocord/testing` has recorded, keeping what each was told to do.
|
|
32
89
|
*
|
|
33
|
-
* Vitest's `clearMocks` and jest's `clearAllMocks()` reach only their own `vi.fn()` and `jest.fn()` mocks
|
|
34
|
-
*
|
|
35
|
-
*
|
|
90
|
+
* Vitest's `clearMocks` and jest's `clearAllMocks()` reach only their own `vi.fn()` and `jest.fn()` mocks, and
|
|
91
|
+
* meocord's too once {@link useMockFn} makes them with the runner's function. This one covers `createMockFn` and
|
|
92
|
+
* everything built on it, such as `createMockInteraction` and `createMockClient`, either way. To also undo what a test
|
|
93
|
+
* set, use {@link resetAllMocks}.
|
|
36
94
|
*
|
|
37
95
|
* @example
|
|
38
96
|
* ```ts
|
|
@@ -68,13 +126,20 @@ const collected = new FinalizationRegistry((ref)=>created.delete(ref));
|
|
|
68
126
|
* @category Mocks
|
|
69
127
|
* @see {@link clearAllMocks}
|
|
70
128
|
*/ function resetAllMocks() {
|
|
71
|
-
for (const ref of created)
|
|
129
|
+
for (const ref of created){
|
|
130
|
+
const mockFn = ref.deref();
|
|
131
|
+
if (!mockFn) continue;
|
|
132
|
+
mockFn.mockReset();
|
|
133
|
+
// A runner whose reset drops the implementation the mock was created with, as jest's and bun's do, gets it back
|
|
134
|
+
restoreImpl.get(mockFn)?.();
|
|
135
|
+
}
|
|
72
136
|
}
|
|
73
137
|
/**
|
|
74
138
|
* Creates a mock function that jest's and Vitest's `expect` both read.
|
|
75
139
|
*
|
|
76
140
|
* Use it for a function double in code that must not depend on one test runner, such as a shared test helper; in a
|
|
77
|
-
* Vitest test, `vi.fn()` works as well. {@link clearAllMocks} and {@link resetAllMocks} reach it.
|
|
141
|
+
* Vitest test, `vi.fn()` works as well. {@link clearAllMocks} and {@link resetAllMocks} reach it. Once
|
|
142
|
+
* {@link useMockFn} registers a runner's function, it returns that runner's mock.
|
|
78
143
|
*
|
|
79
144
|
* @remarks
|
|
80
145
|
* `mockReturnValue`, `mockResolvedValue`, `mockRejectedValue` and `mockImplementation` share one implementation, so
|
|
@@ -97,6 +162,14 @@ const collected = new FinalizationRegistry((ref)=>created.delete(ref));
|
|
|
97
162
|
* @category Mocks
|
|
98
163
|
* @see {@link MockInstance}
|
|
99
164
|
*/ function createMockFn(impl) {
|
|
165
|
+
if (runnerFactory) {
|
|
166
|
+
const runnerMock = runnerFactory(impl);
|
|
167
|
+
if (impl) {
|
|
168
|
+
const setImpl = runnerMock.mockImplementation.bind(runnerMock);
|
|
169
|
+
restoreImpl.set(runnerMock, ()=>setImpl(impl));
|
|
170
|
+
}
|
|
171
|
+
return track(runnerMock);
|
|
172
|
+
}
|
|
100
173
|
// One persistent implementation and one queue of single-use ones, as in jest and vitest: the
|
|
101
174
|
// four setters share the slot, so the last call wins, and the `*Once` variants share the queue,
|
|
102
175
|
// so they run in declaration order.
|
|
@@ -209,10 +282,7 @@ const collected = new FinalizationRegistry((ref)=>created.delete(ref));
|
|
|
209
282
|
name = n;
|
|
210
283
|
return mockFn;
|
|
211
284
|
};
|
|
212
|
-
|
|
213
|
-
created.add(ref);
|
|
214
|
-
collected.register(mockFn, ref);
|
|
215
|
-
return mockFn;
|
|
285
|
+
return track(mockFn);
|
|
216
286
|
}
|
|
217
287
|
|
|
218
|
-
export { clearAllMocks, createMockFn, isMockFunction, resetAllMocks };
|
|
288
|
+
export { clearAllMocks, createMockFn, isMockFunction, resetAllMocks, useMockFn };
|