vitest-auto-spy 3.10.0 → 3.11.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/AGENTS.md +2 -1
- package/README.md +16 -2
- package/dist/eslint-plugin.cjs +98 -2
- package/dist/eslint-plugin.js +98 -2
- package/package.json +1 -1
- package/skills/vitest-auto-spy/SKILL.md +1 -1
package/AGENTS.md
CHANGED
|
@@ -1886,8 +1886,9 @@ export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
|
|
|
1886
1886
|
| `no-overridden-provider` | `error` | suggest | two providers for one token in one array → the earlier one never runs; the exact duplicate can be deleted |
|
|
1887
1887
|
| `no-inject-before-override` | `warn` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
|
|
1888
1888
|
| `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
|
|
1889
|
+
| `no-unregistered-inject-spy` | `warn` | — | `injectSpy(X)` for a token this file never registered → the real instance, whose spy helpers exist only for the compiler |
|
|
1889
1890
|
|
|
1890
|
-
|
|
1891
|
+
Fourteen rules; two fix on their own, six offer suggestions. `no-mocked-for-spy` only ever touches a
|
|
1891
1892
|
**type position**, where a wrong rewrite is a compile error rather than a test that quietly changed
|
|
1892
1893
|
meaning — so `--fix` renames the type, adds `import type { Spy } from 'vitest-auto-spy'` and drops
|
|
1893
1894
|
the orphaned `Mocked` import. Every type position, not only a `let`: a factory's return type, a
|
package/README.md
CHANGED
|
@@ -2271,6 +2271,20 @@ seven sites in an 8 673-file workspace, which is small enough to flag at the cur
|
|
|
2271
2271
|
and an instance field are deliberately not reported — they run later than the module does — while a
|
|
2272
2272
|
`static` field is.
|
|
2273
2273
|
|
|
2274
|
+
The fourteenth, `no-unregistered-inject-spy`, catches `injectSpy(X)` for a token nothing in the file
|
|
2275
|
+
registered as an auto-spy. What comes back is whatever Angular DI already had — the real service, or
|
|
2276
|
+
an object an imported testing module put there — and nothing complains, because `injectSpy` is
|
|
2277
|
+
declared to return a `Spy<T>`: the helpers are present for `tsc` and absent at run time, so the first
|
|
2278
|
+
`.mockReturnValue(…)` or `.calledWith(…)` throws on a real method. The library warns about exactly
|
|
2279
|
+
this at run time, and that warning is why the rule exists rather than why it is redundant — it does
|
|
2280
|
+
not fail the run, it scrolls past in a suite of a thousand files, and it arrives only for the tests
|
|
2281
|
+
that executed the line; in one consumer monorepo dozens of spec files print it on every CI run and it
|
|
2282
|
+
has never been acted on. Reading it at the cursor needs no types, only the file's own registrations,
|
|
2283
|
+
and the rule stays quiet unless it can read all of them: it needs at least one `provideAutoSpy`, and
|
|
2284
|
+
a spread, an unknown provider factory, `createWithAutoSpies`, `renderShallow` or
|
|
2285
|
+
`TestBed.overrideProvider` silences the file. A token handed a plain `useValue` is left to
|
|
2286
|
+
`prefer-provide-auto-spy` — two rules on one line only teach people to disable both.
|
|
2287
|
+
|
|
2274
2288
|
## Editor diagnostics — WebStorm & VS Code
|
|
2275
2289
|
|
|
2276
2290
|
The rules above are worth more while the cursor is still on the line than they are in CI, because
|
|
@@ -2280,7 +2294,7 @@ package's own — it needs its ESLint integration switched on.
|
|
|
2280
2294
|
### WebStorm and the other JetBrains IDEs
|
|
2281
2295
|
|
|
2282
2296
|
No plugin to install: WebStorm, IntelliJ IDEA Ultimate, PhpStorm, PyCharm Professional and RubyMine
|
|
2283
|
-
all run ESLint natively, so the
|
|
2297
|
+
all run ESLint natively, so the fourteen rules appear inline, in the **Problems** tool window, and
|
|
2284
2298
|
under **Code → Inspect Code** for the whole project.
|
|
2285
2299
|
|
|
2286
2300
|
```js
|
|
@@ -2297,7 +2311,7 @@ has supported flat config since 2023.3); scope the block to spec files yourself;
|
|
|
2297
2311
|
the fixes and suggestions live.
|
|
2298
2312
|
|
|
2299
2313
|
A native JetBrains plugin is **not** planned — it would duplicate an integration the IDE already has
|
|
2300
|
-
and then keep a second copy of
|
|
2314
|
+
and then keep a second copy of fourteen rules, in Kotlin, in step with the TypeScript ones.
|
|
2301
2315
|
|
|
2302
2316
|
### VS Code, Cursor, Windsurf, VSCodium
|
|
2303
2317
|
|
package/dist/eslint-plugin.cjs
CHANGED
|
@@ -708,6 +708,77 @@ function breaksAnOverride(injection) {
|
|
|
708
708
|
return countInSubtree(suite, (node) => isMemberCall(node, TEST_BED, OVERRIDES) && !runsFirst(node, injection, hook), true) > 0;
|
|
709
709
|
}
|
|
710
710
|
|
|
711
|
+
// src/lib/eslint/unregistered-spy.ts
|
|
712
|
+
var PROVIDE_AUTO_SPY = "provideAutoSpy";
|
|
713
|
+
var INJECT_SPY = "injectSpy";
|
|
714
|
+
var AUTO_SPY_FACTORIES = /* @__PURE__ */ new Set(["createAutoMock", "createSpyFromClass", "createMock", "mockDeep"]);
|
|
715
|
+
var OPAQUE_HELPERS = /* @__PURE__ */ new Set(["createWithAutoSpies", "renderShallow"]);
|
|
716
|
+
function emptyRegistrations() {
|
|
717
|
+
return { autoSpies: /* @__PURE__ */ new Set(), opaqueTokens: /* @__PURE__ */ new Set(), injections: [], sawProvideAutoSpy: false, opaque: false };
|
|
718
|
+
}
|
|
719
|
+
function calleeName(node) {
|
|
720
|
+
return isIdentifier(node.callee) ? node.callee.name : void 0;
|
|
721
|
+
}
|
|
722
|
+
function isOverrideProvider(node) {
|
|
723
|
+
const { callee } = node;
|
|
724
|
+
return isMemberExpression(callee) && isIdentifier(callee.object) && callee.object.name === "TestBed" && isIdentifier(callee.property) && callee.property.name === "overrideProvider";
|
|
725
|
+
}
|
|
726
|
+
function readCall(context, node, tally) {
|
|
727
|
+
const name = calleeName(node);
|
|
728
|
+
const [first] = node.arguments;
|
|
729
|
+
if (isOverrideProvider(node) || name !== void 0 && OPAQUE_HELPERS.has(name)) {
|
|
730
|
+
tally.opaque = true;
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (first === void 0) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (name === PROVIDE_AUTO_SPY) {
|
|
737
|
+
tally.sawProvideAutoSpy = true;
|
|
738
|
+
tally.autoSpies.add(context.sourceCode.getText(first));
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (name === INJECT_SPY) {
|
|
742
|
+
tally.injections.push({ node, token: context.sourceCode.getText(first) });
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function isAutoSpyValue(value) {
|
|
746
|
+
return isCallExpression(value) && isIdentifier(value.callee) && AUTO_SPY_FACTORIES.has(value.callee.name);
|
|
747
|
+
}
|
|
748
|
+
function readProviderObject(context, element, tally) {
|
|
749
|
+
const provide = findProperty(element, "provide");
|
|
750
|
+
if (provide === void 0) {
|
|
751
|
+
tally.opaque = true;
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const token = context.sourceCode.getText(propertyValue(provide));
|
|
755
|
+
const useValue = findProperty(element, "useValue");
|
|
756
|
+
if (useValue !== void 0 && isAutoSpyValue(propertyValue(useValue))) {
|
|
757
|
+
tally.autoSpies.add(token);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
tally.opaqueTokens.add(token);
|
|
761
|
+
}
|
|
762
|
+
function readProviders(context, array, tally) {
|
|
763
|
+
for (const element of array.elements) {
|
|
764
|
+
if (element === null) {
|
|
765
|
+
tally.opaque = true;
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (isObjectExpression(element)) {
|
|
769
|
+
readProviderObject(context, element, tally);
|
|
770
|
+
} else if (!isCallExpression(element) || calleeName(element) !== PROVIDE_AUTO_SPY) {
|
|
771
|
+
tally.opaque = true;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function unregisteredInjections(tally) {
|
|
776
|
+
if (tally.opaque || !tally.sawProvideAutoSpy) {
|
|
777
|
+
return [];
|
|
778
|
+
}
|
|
779
|
+
return tally.injections.filter(({ token }) => !tally.autoSpies.has(token) && !tally.opaqueTokens.has(token));
|
|
780
|
+
}
|
|
781
|
+
|
|
711
782
|
// src/lib/eslint/rules.ts
|
|
712
783
|
var README = "https://github.com/ASDAlexey/vitest-auto-spy#how-to-mock";
|
|
713
784
|
function defineRule(options) {
|
|
@@ -1116,6 +1187,29 @@ var noImportTimeSpread = defineRule({
|
|
|
1116
1187
|
}
|
|
1117
1188
|
})
|
|
1118
1189
|
});
|
|
1190
|
+
var noUnregisteredInjectSpy = defineRule({
|
|
1191
|
+
anchor: "-a-service-behind-angular-di",
|
|
1192
|
+
description: "Do not read a token with injectSpy unless this file registered it as an auto-spy",
|
|
1193
|
+
messages: {
|
|
1194
|
+
noUnregisteredInjectSpy: "Nothing in this file registers `{{token}}` as an auto-spy, so this hands back whatever Angular DI already had \u2014 the real service, or a hand-rolled object some imported module provides. The line still compiles and the spec still runs: `injectSpy` types the result as a spy, so the helpers are there for the compiler and absent at run time, and the first `.mockReturnValue(\u2026)` or `.calledWith(\u2026)` throws on a real method. Add `provideAutoSpy({{token}})` to the providers, or read the real implementation with `TestBed.inject({{token}})` and say so. Nothing is reported from a file whose providers this cannot read in full \u2014 a spread, an unknown provider factory, `createWithAutoSpies` or `TestBed.overrideProvider` all silence it."
|
|
1195
|
+
},
|
|
1196
|
+
create: (context) => {
|
|
1197
|
+
const tally = emptyRegistrations();
|
|
1198
|
+
return {
|
|
1199
|
+
CallExpression: (node) => {
|
|
1200
|
+
readCall(context, node, tally);
|
|
1201
|
+
},
|
|
1202
|
+
'Property[key.name="providers"] > ArrayExpression': (node) => {
|
|
1203
|
+
readProviders(context, node, tally);
|
|
1204
|
+
},
|
|
1205
|
+
"Program:exit": () => {
|
|
1206
|
+
unregisteredInjections(tally).forEach(({ node, token }) => {
|
|
1207
|
+
context.report({ node, messageId: "noUnregisteredInjectSpy", data: { token } });
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1119
1213
|
var rules = {
|
|
1120
1214
|
"prefer-provide-auto-spy": preferProvideAutoSpy,
|
|
1121
1215
|
"prefer-create-spy-from-class": preferCreateSpyFromClass,
|
|
@@ -1129,7 +1223,8 @@ var rules = {
|
|
|
1129
1223
|
"no-floating-assertion": noFloatingAssertion,
|
|
1130
1224
|
"no-overridden-provider": noOverriddenProvider,
|
|
1131
1225
|
"no-inject-before-override": noInjectBeforeOverride,
|
|
1132
|
-
"no-import-time-spread": noImportTimeSpread
|
|
1226
|
+
"no-import-time-spread": noImportTimeSpread,
|
|
1227
|
+
"no-unregistered-inject-spy": noUnregisteredInjectSpy
|
|
1133
1228
|
};
|
|
1134
1229
|
|
|
1135
1230
|
// src/eslint-plugin.ts
|
|
@@ -1147,7 +1242,8 @@ var recommendedRules = {
|
|
|
1147
1242
|
[`${PLUGIN_NAME}/no-floating-assertion`]: "error",
|
|
1148
1243
|
[`${PLUGIN_NAME}/no-overridden-provider`]: "error",
|
|
1149
1244
|
[`${PLUGIN_NAME}/no-inject-before-override`]: "warn",
|
|
1150
|
-
[`${PLUGIN_NAME}/no-import-time-spread`]: "error"
|
|
1245
|
+
[`${PLUGIN_NAME}/no-import-time-spread`]: "error",
|
|
1246
|
+
[`${PLUGIN_NAME}/no-unregistered-inject-spy`]: "warn"
|
|
1151
1247
|
};
|
|
1152
1248
|
var plugin = {
|
|
1153
1249
|
rules,
|
package/dist/eslint-plugin.js
CHANGED
|
@@ -706,6 +706,77 @@ function breaksAnOverride(injection) {
|
|
|
706
706
|
return countInSubtree(suite, (node) => isMemberCall(node, TEST_BED, OVERRIDES) && !runsFirst(node, injection, hook), true) > 0;
|
|
707
707
|
}
|
|
708
708
|
|
|
709
|
+
// src/lib/eslint/unregistered-spy.ts
|
|
710
|
+
var PROVIDE_AUTO_SPY = "provideAutoSpy";
|
|
711
|
+
var INJECT_SPY = "injectSpy";
|
|
712
|
+
var AUTO_SPY_FACTORIES = /* @__PURE__ */ new Set(["createAutoMock", "createSpyFromClass", "createMock", "mockDeep"]);
|
|
713
|
+
var OPAQUE_HELPERS = /* @__PURE__ */ new Set(["createWithAutoSpies", "renderShallow"]);
|
|
714
|
+
function emptyRegistrations() {
|
|
715
|
+
return { autoSpies: /* @__PURE__ */ new Set(), opaqueTokens: /* @__PURE__ */ new Set(), injections: [], sawProvideAutoSpy: false, opaque: false };
|
|
716
|
+
}
|
|
717
|
+
function calleeName(node) {
|
|
718
|
+
return isIdentifier(node.callee) ? node.callee.name : void 0;
|
|
719
|
+
}
|
|
720
|
+
function isOverrideProvider(node) {
|
|
721
|
+
const { callee } = node;
|
|
722
|
+
return isMemberExpression(callee) && isIdentifier(callee.object) && callee.object.name === "TestBed" && isIdentifier(callee.property) && callee.property.name === "overrideProvider";
|
|
723
|
+
}
|
|
724
|
+
function readCall(context, node, tally) {
|
|
725
|
+
const name = calleeName(node);
|
|
726
|
+
const [first] = node.arguments;
|
|
727
|
+
if (isOverrideProvider(node) || name !== void 0 && OPAQUE_HELPERS.has(name)) {
|
|
728
|
+
tally.opaque = true;
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (first === void 0) {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (name === PROVIDE_AUTO_SPY) {
|
|
735
|
+
tally.sawProvideAutoSpy = true;
|
|
736
|
+
tally.autoSpies.add(context.sourceCode.getText(first));
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (name === INJECT_SPY) {
|
|
740
|
+
tally.injections.push({ node, token: context.sourceCode.getText(first) });
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function isAutoSpyValue(value) {
|
|
744
|
+
return isCallExpression(value) && isIdentifier(value.callee) && AUTO_SPY_FACTORIES.has(value.callee.name);
|
|
745
|
+
}
|
|
746
|
+
function readProviderObject(context, element, tally) {
|
|
747
|
+
const provide = findProperty(element, "provide");
|
|
748
|
+
if (provide === void 0) {
|
|
749
|
+
tally.opaque = true;
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const token = context.sourceCode.getText(propertyValue(provide));
|
|
753
|
+
const useValue = findProperty(element, "useValue");
|
|
754
|
+
if (useValue !== void 0 && isAutoSpyValue(propertyValue(useValue))) {
|
|
755
|
+
tally.autoSpies.add(token);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
tally.opaqueTokens.add(token);
|
|
759
|
+
}
|
|
760
|
+
function readProviders(context, array, tally) {
|
|
761
|
+
for (const element of array.elements) {
|
|
762
|
+
if (element === null) {
|
|
763
|
+
tally.opaque = true;
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
if (isObjectExpression(element)) {
|
|
767
|
+
readProviderObject(context, element, tally);
|
|
768
|
+
} else if (!isCallExpression(element) || calleeName(element) !== PROVIDE_AUTO_SPY) {
|
|
769
|
+
tally.opaque = true;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function unregisteredInjections(tally) {
|
|
774
|
+
if (tally.opaque || !tally.sawProvideAutoSpy) {
|
|
775
|
+
return [];
|
|
776
|
+
}
|
|
777
|
+
return tally.injections.filter(({ token }) => !tally.autoSpies.has(token) && !tally.opaqueTokens.has(token));
|
|
778
|
+
}
|
|
779
|
+
|
|
709
780
|
// src/lib/eslint/rules.ts
|
|
710
781
|
var README = "https://github.com/ASDAlexey/vitest-auto-spy#how-to-mock";
|
|
711
782
|
function defineRule(options) {
|
|
@@ -1114,6 +1185,29 @@ var noImportTimeSpread = defineRule({
|
|
|
1114
1185
|
}
|
|
1115
1186
|
})
|
|
1116
1187
|
});
|
|
1188
|
+
var noUnregisteredInjectSpy = defineRule({
|
|
1189
|
+
anchor: "-a-service-behind-angular-di",
|
|
1190
|
+
description: "Do not read a token with injectSpy unless this file registered it as an auto-spy",
|
|
1191
|
+
messages: {
|
|
1192
|
+
noUnregisteredInjectSpy: "Nothing in this file registers `{{token}}` as an auto-spy, so this hands back whatever Angular DI already had \u2014 the real service, or a hand-rolled object some imported module provides. The line still compiles and the spec still runs: `injectSpy` types the result as a spy, so the helpers are there for the compiler and absent at run time, and the first `.mockReturnValue(\u2026)` or `.calledWith(\u2026)` throws on a real method. Add `provideAutoSpy({{token}})` to the providers, or read the real implementation with `TestBed.inject({{token}})` and say so. Nothing is reported from a file whose providers this cannot read in full \u2014 a spread, an unknown provider factory, `createWithAutoSpies` or `TestBed.overrideProvider` all silence it."
|
|
1193
|
+
},
|
|
1194
|
+
create: (context) => {
|
|
1195
|
+
const tally = emptyRegistrations();
|
|
1196
|
+
return {
|
|
1197
|
+
CallExpression: (node) => {
|
|
1198
|
+
readCall(context, node, tally);
|
|
1199
|
+
},
|
|
1200
|
+
'Property[key.name="providers"] > ArrayExpression': (node) => {
|
|
1201
|
+
readProviders(context, node, tally);
|
|
1202
|
+
},
|
|
1203
|
+
"Program:exit": () => {
|
|
1204
|
+
unregisteredInjections(tally).forEach(({ node, token }) => {
|
|
1205
|
+
context.report({ node, messageId: "noUnregisteredInjectSpy", data: { token } });
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1117
1211
|
var rules = {
|
|
1118
1212
|
"prefer-provide-auto-spy": preferProvideAutoSpy,
|
|
1119
1213
|
"prefer-create-spy-from-class": preferCreateSpyFromClass,
|
|
@@ -1127,7 +1221,8 @@ var rules = {
|
|
|
1127
1221
|
"no-floating-assertion": noFloatingAssertion,
|
|
1128
1222
|
"no-overridden-provider": noOverriddenProvider,
|
|
1129
1223
|
"no-inject-before-override": noInjectBeforeOverride,
|
|
1130
|
-
"no-import-time-spread": noImportTimeSpread
|
|
1224
|
+
"no-import-time-spread": noImportTimeSpread,
|
|
1225
|
+
"no-unregistered-inject-spy": noUnregisteredInjectSpy
|
|
1131
1226
|
};
|
|
1132
1227
|
|
|
1133
1228
|
// src/eslint-plugin.ts
|
|
@@ -1145,7 +1240,8 @@ var recommendedRules = {
|
|
|
1145
1240
|
[`${PLUGIN_NAME}/no-floating-assertion`]: "error",
|
|
1146
1241
|
[`${PLUGIN_NAME}/no-overridden-provider`]: "error",
|
|
1147
1242
|
[`${PLUGIN_NAME}/no-inject-before-override`]: "warn",
|
|
1148
|
-
[`${PLUGIN_NAME}/no-import-time-spread`]: "error"
|
|
1243
|
+
[`${PLUGIN_NAME}/no-import-time-spread`]: "error",
|
|
1244
|
+
[`${PLUGIN_NAME}/no-unregistered-inject-spy`]: "warn"
|
|
1149
1245
|
};
|
|
1150
1246
|
var plugin = {
|
|
1151
1247
|
rules,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitest-auto-spy",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.11.0",
|
|
4
4
|
"description": "Auto-generate fully-typed test spies from a class — across Vitest, Bun and node:test, with Angular/NestJS/React/Vue/Svelte recipes. Drop-in replacement for jest-auto-spies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auto-mock",
|
|
@@ -238,7 +238,7 @@ npx vitest-auto-spy codemod --verify # after a migration: anything the transfor
|
|
|
238
238
|
Most of this library's guarantees are type-level, so a green run that does not type-check is not
|
|
239
239
|
done. Report failures with their output rather than describing them as passing.
|
|
240
240
|
|
|
241
|
-
**After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The
|
|
241
|
+
**After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The fourteen rules in
|
|
242
242
|
`vitest-auto-spy/eslint-plugin` are lint, not typecheck: `no-mocked-for-spy` rewrites a declaration
|
|
243
243
|
to `Spy<T>` and cannot see what the name is assigned two lines below, so a clean lint pass is not
|
|
244
244
|
evidence that the types still hold. Where it cannot prove the rename it downgrades to a suggestion —
|