vitest-auto-spy 5.47.0 → 5.48.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 +1 -1
- package/CHANGELOG.md +32 -1
- package/README.md +7 -4
- package/agent-docs/adapters.md +3 -1
- package/agent-docs/angular.md +2 -2
- package/agent-docs/eslint.md +20 -1
- package/dist/cli.js +1 -1
- package/dist/eslint-plugin.cjs +58 -5
- package/dist/eslint-plugin.d.ts +5 -23
- package/package.json +1 -1
- package/skills/vitest-auto-spy/SKILL.md +8 -1
package/AGENTS.md
CHANGED
|
@@ -534,7 +534,7 @@ In [`agent-docs/adapters.md`](./agent-docs/adapters.md). Read it for NestJS, Vue
|
|
|
534
534
|
|
|
535
535
|
## 16. ESLint plugin (flat config only)
|
|
536
536
|
|
|
537
|
-
In [`agent-docs/eslint.md`](./agent-docs/eslint.md). Read it when configuring `vitest-auto-spy/eslint-plugin` or fixing one of its reports.
|
|
537
|
+
In [`agent-docs/eslint.md`](./agent-docs/eslint.md). Read it when configuring `vitest-auto-spy/eslint-plugin` or fixing one of its reports. For `vitest/require-hook`, spread `autoSpy.hookRegisteringHelpers` into `allowedFunctionCalls` rather than keeping the names by hand.
|
|
538
538
|
|
|
539
539
|
<!-- agents-outline: agent-docs/eslint.md -->
|
|
540
540
|
<!-- /agents-outline -->
|
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,36 @@ The latest released version here must always match the one published on
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [5.48.0] - 2026-09-28
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`hookRegisteringHelpers` on `vitest-auto-spy/eslint-plugin`** — the frozen list of the
|
|
18
|
+
library's public helpers that register `beforeEach` / `afterEach` / `beforeAll` / `afterAll` when
|
|
19
|
+
called (`useConsoleSpies`, `setupAutoSpy`, `setupFakeTimers`, `useCountingClock`, `mockNow`, the
|
|
20
|
+
`guard*` family, …), for `vitest/require-hook`'s `allowedFunctionCalls`:
|
|
21
|
+
`allowedFunctionCalls: [...autoSpy.hookRegisteringHelpers]`. A bare `useConsoleSpies();` in a
|
|
22
|
+
`describe` body no longer needs a hand-kept list; a spec walks the call graph of every public entry
|
|
23
|
+
and fails when a hook-registering export is missing from it. It is a property of the plugin
|
|
24
|
+
object, so `require()` and `import` both reach it. `vitest-auto-spy/eslint-plugin` is +0.42 kB
|
|
25
|
+
min+gzip (+1.0 %) for the list and the two rule changes.
|
|
26
|
+
- **`prefer-to-have-signal-value`** also reports `expect(sig()).toBeNull()` and `.toBeUndefined()`
|
|
27
|
+
on a signal read, `.not` included, and fixes them to `toHaveSignalValue(null)` /
|
|
28
|
+
`toHaveSignalValue(undefined)`.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **`prefer-to-have-signal-value`** no longer rewrites an identity check. `expect(list.items()).toBe(items)`
|
|
33
|
+
asserts the signal holds that very array, and the fix turned it into `toHaveSignalValue(items)`,
|
|
34
|
+
which compares deeply and passes for any equal copy. A `toBe` is now reported only when the
|
|
35
|
+
expected value is a primitive literal or the signal's type is primitive (`any` does not count);
|
|
36
|
+
`toEqual` and `toStrictEqual` are unchanged.
|
|
37
|
+
- **Docs**: `vitest-auto-spy/console` imports `vitest` itself (the fallback adapter and the
|
|
38
|
+
`useConsoleSpies()` hooks), so on `node:test` and Rstest it loads only where the `vitest` package
|
|
39
|
+
resolves; the console page, the README and `agent-docs/adapters.md` now say so. The README API
|
|
40
|
+
table and `agent-docs/angular.md` list `registerSignalMatchers({ strict })`, and the types list of
|
|
41
|
+
`/angular/matchers` names `RegisterSignalMatchersOptions`.
|
|
42
|
+
|
|
13
43
|
## [5.47.0] - 2026-09-28
|
|
14
44
|
|
|
15
45
|
### Added
|
|
@@ -6046,7 +6076,8 @@ moved under 1 %: `/angular` +192 B (+1.0 %) for the override queue and the HTTP
|
|
|
6046
6076
|
This copy ships with the package and carries 5.x only. Releases before 5.0.0 are in the
|
|
6047
6077
|
[full changelog](https://github.com/ASDAlexey/vitest-auto-spy/blob/master/CHANGELOG.md).
|
|
6048
6078
|
|
|
6049
|
-
[Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.
|
|
6079
|
+
[Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.48.0...HEAD
|
|
6080
|
+
[5.48.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.47.0...v5.48.0
|
|
6050
6081
|
[5.47.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.46.0...v5.47.0
|
|
6051
6082
|
[5.46.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.45.0...v5.46.0
|
|
6052
6083
|
[5.45.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.44.0...v5.45.0
|
package/README.md
CHANGED
|
@@ -890,7 +890,9 @@ it('reports a failed load', () => {
|
|
|
890
890
|
`useConsoleSpies()` registers `beforeEach(installConsoleSpies)` and `afterEach(restoreConsole)` in
|
|
891
891
|
the enclosing `describe` (or the file) and returns the spies, which are the same objects in every
|
|
892
892
|
test. It registers Vitest hooks, which Bun resolves to its own; on `node:test` and Rstest write that
|
|
893
|
-
pair in the runner's hooks yourself
|
|
893
|
+
pair in the runner's hooks yourself — the entry imports `vitest` itself, so there it loads only where
|
|
894
|
+
the `vitest` package resolves. Under `vitest/require-hook`, spread `autoSpy.hookRegisteringHelpers`
|
|
895
|
+
from `vitest-auto-spy/eslint-plugin` into its `allowedFunctionCalls`. Do not
|
|
894
896
|
rely on the **import** to install them: it does so once per worker, so under `isolate: false` the
|
|
895
897
|
spies go on in whichever file imported them first and silence every later file of the worker — on a
|
|
896
898
|
1759-file Angular consumer, 32 of the 39 files importing the entry relied on that, and three files
|
|
@@ -2953,7 +2955,8 @@ expect(component.draft).toHaveSignalValue({}, { strict: false });
|
|
|
2953
2955
|
```
|
|
2954
2956
|
|
|
2955
2957
|
The `prefer-to-have-signal-value` lint rule rewrites the inline spellings — `expect(component.total()).toBe(3)`
|
|
2956
|
-
— into the matcher with a `--fix`.
|
|
2958
|
+
— into the matcher with a `--fix`. A `toBe` against an object-typed signal checks identity, which the
|
|
2959
|
+
deep matcher cannot keep, so the rule leaves it alone.
|
|
2957
2960
|
|
|
2958
2961
|
#### Where a spec spends its time
|
|
2959
2962
|
|
|
@@ -4017,7 +4020,7 @@ export can never be.
|
|
|
4017
4020
|
| `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError`, or a silently empty object, while the bundle loads |
|
|
4018
4021
|
| `no-unregistered-inject-spy` | `error` | — | `injectSpy(X)` for a token this file never registered → the real instance, whose spy helpers exist only for the compiler |
|
|
4019
4022
|
| `prefer-render-shallow` | `warn` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
|
|
4020
|
-
| `prefer-to-have-signal-value` | `warn` | `--fix` | `expect(signal()).toBe(…)` → `expect(signal).toHaveSignalValue(…)`; the matcher names the signal in the failure and refuses a non-signal. `toStrictEqual` keeps `{ strict: true }`. **Type-aware** — the callee must be callable and carry Angular's signal brand — and silent without `parserOptions.project`; safe to autofix since 5.46.0 fixed the `Set`/`Map` comparison
|
|
4023
|
+
| `prefer-to-have-signal-value` | `warn` | `--fix` | `expect(signal()).toBe(…)` → `expect(signal).toHaveSignalValue(…)`; the matcher names the signal in the failure and refuses a non-signal. `toStrictEqual` keeps `{ strict: true }`, `toBeNull()` / `toBeUndefined()` become `toHaveSignalValue(null)` / `(undefined)`. **Type-aware** — the callee must be callable and carry Angular's signal brand — and silent without `parserOptions.project`; safe to autofix since 5.46.0 fixed the `Set`/`Map` comparison. A `toBe` against an object-typed signal is an identity check and is left alone |
|
|
4021
4024
|
| `prefer-set-inputs` | `warn` | suggest | a run of `fixture.componentRef.setInput('title', v)` on one fixture → `await setInputs(fixture, { title: v })`, which resolves every name against the compiled definition before it writes one — an undeclared name is an `NG0303` on the console and no change at all — and types the value. The suggestion collapses the run, drops a `detectChanges()` under it and makes the callback `async`; it is offered rather than applied, because `stable()` ticks and a zone.js suite can answer that with `NG0101` |
|
|
4022
4025
|
| `prefer-spy-on-own-method` | `warn` | `--fix` / suggest | `createSpyFromInstance(x, { onlyMethodsToSpyOn: ['m'], passthrough: true })` read for `m` alone — `.m` on the call, `const { m } = …`, a bare statement, or a name read only as `v.m` → `spyOnOwnMethod(x, 'm')`; the same whitelist with `returns: { m: undefined }` → `spyOnVoidMethod(x, 'm')`. Both fix, import and all; a `Spy<X>` annotation becomes `Spy<X>['m']` in a suggestion. A bare `returns: { m: undefined }` seed is a suggestion, offered only on a real event or element — `new MouseEvent(…)`, `document.createElement(…)`, `fixture.nativeElement` — never on a double |
|
|
4023
4026
|
| `prefer-observer-stub` | `error` | — | a hand-rolled `IntersectionObserver` / `ResizeObserver` / `MutationObserver` written into a global → `stubIntersectionObserver()` and friends, whose undo `restoreMockedProps()` already runs |
|
|
@@ -4360,7 +4363,7 @@ another, and the next one is in that same file.
|
|
|
4360
4363
|
| `collectRouterEvents(events)` _(`/angular-router`)_ | The double's router events as one comparable value — `expect([[NavigationStart, '/checkout'], …])`, a class-and-URL pair per event |
|
|
4361
4364
|
| `provideLocationDouble()` / `injectLocationDouble(injector?)` / `createLocationDouble()` _(`/angular-router`)_ | Angular's own `SpyLocation` and `MockLocationStrategy` in one call — a real history, the `urlChanges` journal, `simulateUrlPop()` for the popstate no method call can cause |
|
|
4362
4365
|
| `createForm(model, schema?, options?)` / `registerFormMatchers()` _(`/signal-forms`)_ | Angular's own signal `form()`, built in the `TestBed`'s injection context, and the `toHaveFieldErrors` matcher over its `errors()` |
|
|
4363
|
-
| `registerSignalMatchers()` _(Angular)_
|
|
4366
|
+
| `registerSignalMatchers(opts?)` _(Angular)_ | Adds `expect(sig).toHaveSignalValue(value)`; `{ strict: true }` compares every assertion like `toStrictEqual` |
|
|
4364
4367
|
| `trackRecomputations(signal)` / `trackEffectRuns(effectRef)` _(Angular)_ | Count what the reactive graph re-ran — `{ count, stop() }`, undone by `restoreMockedProps()` |
|
|
4365
4368
|
| `provideWindowDouble(token, overrides?)` / `provideDocumentDouble(overrides?)` _(Angular)_ | A `window` / `document` merged over the real jsdom one; `createWindowDouble` / `createDocumentDouble` outside DI |
|
|
4366
4369
|
| `provideMatDialogData(token, data)` / `provideMatDialogRef(Ref, init?)` / `injectMatDialogRef(Ref)` _(Angular)_ | The Material dialog trio — the token and the ref class are arguments; `createMatDialogRef` builds the ref outside DI |
|
package/agent-docs/adapters.md
CHANGED
|
@@ -39,7 +39,9 @@ expect(consoleInfoSpy).toHaveBeenCalledWith('done'); // the output is silenced,
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
It registers Vitest hooks (Bun resolves them to its own); on `node:test` and Rstest write
|
|
42
|
-
`installConsoleSpies()` in a `beforeEach` and `restoreConsole()` in an `afterEach` yourself.
|
|
42
|
+
`installConsoleSpies()` in a `beforeEach` and `restoreConsole()` in an `afterEach` yourself. The entry
|
|
43
|
+
imports `vitest` itself, so there it loads only where the `vitest` package resolves. Under
|
|
44
|
+
`vitest/require-hook`, spread `autoSpy.hookRegisteringHelpers` into `allowedFunctionCalls` (§16).
|
|
43
45
|
|
|
44
46
|
The exported `consoleInfoSpy` & co. are the same objects; `restoreConsole()` keeps them and clears
|
|
45
47
|
their calls. **Do not rely on the import to install them**: it does so once per worker, so under
|
package/agent-docs/angular.md
CHANGED
|
@@ -1063,8 +1063,8 @@ service.products.set([edited]); // the component's own optimistic write — stat
|
|
|
1063
1063
|
expect(products.reload).toHaveBeenCalled(); // reload is spied, answers true, and re-issues nothing
|
|
1064
1064
|
|
|
1065
1065
|
// signal assertions
|
|
1066
|
-
registerSignalMatchers(); // once, in the setup file — /angular/matchers
|
|
1067
|
-
expect(component.total).toHaveSignalValue(3); // toEqual semantics; { strict: true } for toStrictEqual's
|
|
1066
|
+
registerSignalMatchers(); // once, in the setup file — /angular/matchers; ({ strict: true }) makes the whole suite strict
|
|
1067
|
+
expect(component.total).toHaveSignalValue(3); // toEqual semantics; { strict: true } for toStrictEqual's, { strict: false } opts out
|
|
1068
1068
|
|
|
1069
1069
|
// resource assertions — value AND status, which is the whole point
|
|
1070
1070
|
registerResourceMatchers(); // once, in the setup file — /angular/matchers
|
package/agent-docs/eslint.md
CHANGED
|
@@ -32,6 +32,25 @@ blanket downgrade so those keep their severity; do not copy the two names into a
|
|
|
32
32
|
`autoSpy.configs.strict` is `recommended` with every rule at `error`, instead of mapping
|
|
33
33
|
`Object.keys(autoSpy.rules)` by hand.
|
|
34
34
|
|
|
35
|
+
`@vitest/eslint-plugin`'s `vitest/require-hook` reports a bare `useConsoleSpies();` or
|
|
36
|
+
`setupFakeTimers();` in a `describe` body, since it cannot know the call registers hooks.
|
|
37
|
+
`autoSpy.hookRegisteringHelpers` is the library's list of such helpers — every public function that
|
|
38
|
+
calls `beforeEach` / `afterEach` / `beforeAll` / `afterAll` when it runs, kept in step with the
|
|
39
|
+
source by a spec — so spread it instead of maintaining the names by hand. Calls that register no
|
|
40
|
+
hook (`registerSignalMatchers()`, `trackStrayTimers()`) are not in it; add them yourself where a
|
|
41
|
+
linted file calls them at the top level.
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
export default [
|
|
45
|
+
{
|
|
46
|
+
files: ['**/*.spec.ts'],
|
|
47
|
+
rules: {
|
|
48
|
+
'vitest/require-hook': ['error', { allowedFunctionCalls: [...autoSpy.hookRegisteringHelpers] }],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
```
|
|
53
|
+
|
|
35
54
|
| Rule | Level | Fix | Flags |
|
|
36
55
|
| --- | --- | --- | --- |
|
|
37
56
|
| `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
|
|
@@ -56,7 +75,7 @@ blanket downgrade so those keep their severity; do not copy the two names into a
|
|
|
56
75
|
| `no-unregistered-inject-spy` | `error` | — | `injectSpy(X)` for a token this file never registered → the real instance, whose spy helpers exist only for the compiler |
|
|
57
76
|
| `prefer-render-shallow` | `warn` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
|
|
58
77
|
| `prefer-set-inputs` | `warn` | suggest | a run of `fixture.componentRef.setInput('title', v)` on one fixture → `await setInputs(fixture, { title: v })` — the name is resolved against the compiled definition before the first write (an undeclared one is an `NG0303` and no change) and the value is typed. The run collapses into one call and a `detectChanges()` under it goes; offered, not applied, because `stable()` ticks and a zone.js suite answers that with `NG0101` |
|
|
59
|
-
| `prefer-to-have-signal-value` | `warn` | `--fix` | `expect(component.total()).toBe(3)` / `toEqual(…)` → `expect(component.total).toHaveSignalValue(3)`, `toStrictEqual` → `{ strict: true }`; the matcher names the signal in the failure and refuses a value that is not one. **Type-aware**: the signal is recognised by Angular's brand on its type, so methods, functions and getters are never reported, and without `parserOptions.project` it reports nothing |
|
|
78
|
+
| `prefer-to-have-signal-value` | `warn` | `--fix` | `expect(component.total()).toBe(3)` / `toEqual(…)` → `expect(component.total).toHaveSignalValue(3)`, `toStrictEqual` → `{ strict: true }`, `toBeNull()` / `toBeUndefined()` → `toHaveSignalValue(null)` / `(undefined)`; the matcher names the signal in the failure and refuses a value that is not one. **Type-aware**: the signal is recognised by Angular's brand on its type, so methods, functions and getters are never reported, and without `parserOptions.project` it reports nothing. A `toBe` against an object-typed signal (`expect(list.items()).toBe(items)`) is an identity check the deep matcher would lose, so it is reported only when the expected value is a primitive literal or the signal's type is primitive |
|
|
60
79
|
| `prefer-spy-on-own-method` | `warn` | `--fix` / suggest | `createSpyFromInstance(x, { onlyMethodsToSpyOn: ['m'], passthrough: true })` read for `m` alone — `.m` on the call, `const { m } = …`, a bare statement, or a name read only as `v.m` → `spyOnOwnMethod(x, 'm')`; the same whitelist with `returns: { m: undefined }` → `spyOnVoidMethod(x, 'm')`. Both fix, import and all; a `Spy<X>` annotation becomes `Spy<X>['m']` in a suggestion. A bare `returns: { m: undefined }` seed is a suggestion, offered only on a real event or element — `new MouseEvent(…)`, `document.createElement(…)`, `fixture.nativeElement` — never on a double |
|
|
61
80
|
| `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
|
|
62
81
|
| `no-hand-assigned-global` | `error` | `--fix` | a double assigned to a global (`global.fetch = vi.fn()`, `window.matchMedia = vi.fn()`, `window.localStorage = { getItem: vi.fn() }`) with no restore in `afterEach` / `afterAll` / `onTestFinished` → `mockValueProp(globalThis, name, value)` or `vi.stubGlobal` + `unstubGlobals`; `blockNetwork()` for network globals, `stubWebStorage()` for the storages, `stubWorker({ respond })` for `Worker`; the three observers stay with `prefer-observer-stub`; any value written into an imported object (`environment.production = true`) → `mockValueProp(environment, 'production', true)`, fixed in a test or `beforeEach` |
|
package/dist/cli.js
CHANGED
|
@@ -3865,7 +3865,7 @@ function checkForeignPragma(graph) {
|
|
|
3865
3865
|
}
|
|
3866
3866
|
|
|
3867
3867
|
// src/cli/checks/export-map.generated.ts
|
|
3868
|
-
var EXPORT_MAP_VERSION = "5.
|
|
3868
|
+
var EXPORT_MAP_VERSION = "5.47.0";
|
|
3869
3869
|
var ENTRY_SPECIFIERS = "vitest-auto-spy vitest-auto-spy/bun vitest-auto-spy/bun-angular vitest-auto-spy/node vitest-auto-spy/rstest vitest-auto-spy/rxjs vitest-auto-spy/console vitest-auto-spy/dom-stubs vitest-auto-spy/diagnostics vitest-auto-spy/jasmine vitest-auto-spy/jasmine-compat vitest-auto-spy/observer-spy vitest-auto-spy/angular vitest-auto-spy/angular/diagnostics vitest-auto-spy/angular/doubles vitest-auto-spy/angular/matchers vitest-auto-spy/angular-http vitest-auto-spy/angular-router vitest-auto-spy/signal-forms vitest-auto-spy/nestjs vitest-auto-spy/react vitest-auto-spy/vue vitest-auto-spy/svelte vitest-auto-spy/setup vitest-auto-spy/zone vitest-auto-spy/eslint-plugin vitest-auto-spy/perf-reporter";
|
|
3870
3870
|
var EXPORTED_BY = {
|
|
3871
3871
|
AccessorImplementations: "0 1 2 3 4 12 20 21 22",
|
package/dist/eslint-plugin.cjs
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
var node_fs = require('node:fs');
|
|
4
4
|
var node_path = require('node:path');
|
|
5
5
|
|
|
6
|
+
// src/lib/eslint/hook-registering-helpers.ts
|
|
7
|
+
var hookRegisteringHelpers = Object.freeze([
|
|
8
|
+
"enableAngularDiagnostics",
|
|
9
|
+
"enableTestBedDiagnostics",
|
|
10
|
+
"guardDocumentPollution",
|
|
11
|
+
"guardGlobalPatches",
|
|
12
|
+
"guardPrototypePollution",
|
|
13
|
+
"guardStrayConsole",
|
|
14
|
+
"installPerTest",
|
|
15
|
+
"mockNow",
|
|
16
|
+
"setupAngularTestEnv",
|
|
17
|
+
"setupAutoSpy",
|
|
18
|
+
"setupFakeTimers",
|
|
19
|
+
"trackMockRegistry",
|
|
20
|
+
"trackNodeMocks",
|
|
21
|
+
"useConsoleSpies",
|
|
22
|
+
"useCountingClock"
|
|
23
|
+
]);
|
|
24
|
+
|
|
6
25
|
// src/lib/eslint/rule-types.ts
|
|
7
26
|
function isCast(node) {
|
|
8
27
|
return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
|
|
@@ -4138,11 +4157,15 @@ var preferRenderShallow = defineRule({
|
|
|
4138
4157
|
|
|
4139
4158
|
// src/lib/eslint/prefer-to-have-signal-value.ts
|
|
4140
4159
|
var MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
|
|
4160
|
+
var VALUE_MATCHERS = /* @__PURE__ */ new Map([
|
|
4161
|
+
["toBeNull", "null"],
|
|
4162
|
+
["toBeUndefined", "undefined"]
|
|
4163
|
+
]);
|
|
4141
4164
|
var SIGNAL_BRAND = "\u0275SIGNAL";
|
|
4142
4165
|
var SIGNAL_BRAND_PROPERTY = /^__@(?:SIGNAL|ɵSIGNAL|ɵWRITABLE_SIGNAL)@\d+$/;
|
|
4143
4166
|
function signalAssertionOf(node) {
|
|
4144
4167
|
const callee = node.callee;
|
|
4145
|
-
if (!isMemberExpression(callee) || callee.computed || !isIdentifier(callee.property) || !MATCHERS.has(callee.property.name)) {
|
|
4168
|
+
if (!isMemberExpression(callee) || callee.computed || !isIdentifier(callee.property) || !(MATCHERS.has(callee.property.name) || VALUE_MATCHERS.has(callee.property.name))) {
|
|
4146
4169
|
return void 0;
|
|
4147
4170
|
}
|
|
4148
4171
|
let expected = callee.object;
|
|
@@ -4164,9 +4187,37 @@ function readsSignal(context, node) {
|
|
|
4164
4187
|
const signatures = type.getCallSignatures();
|
|
4165
4188
|
return type.getProperties().some((property) => property.getName() === SIGNAL_BRAND || SIGNAL_BRAND_PROPERTY.test(property.getName())) && signatures.length > 0;
|
|
4166
4189
|
}
|
|
4190
|
+
function isPrimitiveLiteral(node) {
|
|
4191
|
+
if (node === void 0) {
|
|
4192
|
+
return false;
|
|
4193
|
+
}
|
|
4194
|
+
switch (node.type) {
|
|
4195
|
+
case "Literal":
|
|
4196
|
+
return Reflect.get(node, "regex") === void 0;
|
|
4197
|
+
case "Identifier":
|
|
4198
|
+
return isIdentifier(node) && node.name === "undefined";
|
|
4199
|
+
case "UnaryExpression":
|
|
4200
|
+
return isPrimitiveLiteral(Reflect.get(node, "argument"));
|
|
4201
|
+
case "TemplateLiteral":
|
|
4202
|
+
return Reflect.get(node, "expressions")?.length === 0;
|
|
4203
|
+
default:
|
|
4204
|
+
return false;
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
function primitiveValue({ checker, toTs }, received) {
|
|
4208
|
+
const value = checker.getTypeAtLocation(toTs.get(received));
|
|
4209
|
+
return checker.typeToString(value) !== "any" && isPrimitiveLike(checker, value);
|
|
4210
|
+
}
|
|
4211
|
+
function identityIsMoot(context, node, shape) {
|
|
4212
|
+
if (shape.matcher.name !== "toBe" || isPrimitiveLiteral(node.arguments.at(0))) {
|
|
4213
|
+
return true;
|
|
4214
|
+
}
|
|
4215
|
+
const services = checkerServices(context, PRIMITIVE_CHECKER_METHODS);
|
|
4216
|
+
return services !== void 0 && askChecker(() => primitiveValue(services, shape.received)) === true;
|
|
4217
|
+
}
|
|
4167
4218
|
var preferToHaveSignalValue = defineRule({
|
|
4168
4219
|
name: "prefer-to-have-signal-value",
|
|
4169
|
-
description: "Assert a signal with toHaveSignalValue instead of reading it inline into toBe/toEqual",
|
|
4220
|
+
description: "Assert a signal with toHaveSignalValue instead of reading it inline into toBe/toEqual/toBeNull",
|
|
4170
4221
|
messages: {
|
|
4171
4222
|
preferToHaveSignalValue: "Assert the signal, not the value it happens to hold: `expect({{signal}}).toHaveSignalValue(\u2026)` keeps the signal's name in the failure output and refuses a value that is not a signal."
|
|
4172
4223
|
},
|
|
@@ -4174,7 +4225,7 @@ var preferToHaveSignalValue = defineRule({
|
|
|
4174
4225
|
create: (context) => ({
|
|
4175
4226
|
CallExpression: (node) => {
|
|
4176
4227
|
const shape = signalAssertionOf(node);
|
|
4177
|
-
if (!shape || !readsSignal(context, shape.received.callee)) {
|
|
4228
|
+
if (!shape || !readsSignal(context, shape.received.callee) || !identityIsMoot(context, node, shape)) {
|
|
4178
4229
|
return;
|
|
4179
4230
|
}
|
|
4180
4231
|
context.report({
|
|
@@ -4183,9 +4234,10 @@ var preferToHaveSignalValue = defineRule({
|
|
|
4183
4234
|
data: { signal: excerpt(context, shape.received.callee, 40) },
|
|
4184
4235
|
...shape.received.typeArguments === void 0 && {
|
|
4185
4236
|
fix: (fixer) => {
|
|
4237
|
+
const value = VALUE_MATCHERS.get(shape.matcher.name);
|
|
4186
4238
|
const fixes = [
|
|
4187
4239
|
fixer.replaceText(shape.received, context.sourceCode.getText(shape.received.callee)),
|
|
4188
|
-
fixer.replaceText(shape.matcher, "toHaveSignalValue")
|
|
4240
|
+
value === void 0 ? fixer.replaceText(shape.matcher, "toHaveSignalValue") : fixer.replaceTextRange([shape.matcher.range[0], node.range[1]], `toHaveSignalValue(${value})`)
|
|
4189
4241
|
];
|
|
4190
4242
|
const last = node.arguments.at(-1);
|
|
4191
4243
|
if (shape.strict && last !== void 0) {
|
|
@@ -5853,7 +5905,8 @@ var plugin = {
|
|
|
5853
5905
|
recommended: { plugins: {}, rules: recommendedRules },
|
|
5854
5906
|
strict: { plugins: {}, rules: strictRules },
|
|
5855
5907
|
typeErrors: { plugins: {}, rules: typeErrorRules }
|
|
5856
|
-
}
|
|
5908
|
+
},
|
|
5909
|
+
hookRegisteringHelpers
|
|
5857
5910
|
};
|
|
5858
5911
|
plugin.configs.recommended.plugins[PLUGIN_NAME] = plugin;
|
|
5859
5912
|
plugin.configs.strict.plugins[PLUGIN_NAME] = plugin;
|
package/dist/eslint-plugin.d.ts
CHANGED
|
@@ -227,29 +227,6 @@ interface RuleModule {
|
|
|
227
227
|
create(context: RuleContext): RuleListener;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
/**
|
|
231
|
-
* `vitest-auto-spy/eslint-plugin` — the lint rules that keep a suite on the library's API.
|
|
232
|
-
*
|
|
233
|
-
* ```js
|
|
234
|
-
* // eslint.config.js (flat config)
|
|
235
|
-
* import autoSpy from 'vitest-auto-spy/eslint-plugin';
|
|
236
|
-
*
|
|
237
|
-
* export default [
|
|
238
|
-
* { files: ['**\/*.spec.ts'], ...autoSpy.configs.recommended },
|
|
239
|
-
* ];
|
|
240
|
-
* ```
|
|
241
|
-
*
|
|
242
|
-
* Scope the config to spec files yourself: every rule here is about test code, and `Object.
|
|
243
|
-
* defineProperty` or an object of `vi.fn()`s is perfectly reasonable in application code.
|
|
244
|
-
*
|
|
245
|
-
* `configs.strict` is `recommended` with every rule at `error`. `configs.typeErrors` is the subset whose
|
|
246
|
-
* findings are compile errors — already `error` in `recommended` — to spread back over a blanket
|
|
247
|
-
* downgrade while a large suite is being adopted.
|
|
248
|
-
*
|
|
249
|
-
* Flat config only. The legacy `.eslintrc` `plugins: ['…']` form resolves plugin names to
|
|
250
|
-
* `eslint-plugin-*` packages, which a subpath export of this package can never be.
|
|
251
|
-
*/
|
|
252
|
-
|
|
253
230
|
/** Severity map shared by the shipped configs. */
|
|
254
231
|
type RuleSeverity = 'error' | 'off' | 'warn';
|
|
255
232
|
/** A flat-config object: the plugin under its name, plus the rules it turns on. */
|
|
@@ -277,6 +254,11 @@ interface AutoSpyEslintPlugin {
|
|
|
277
254
|
strict: FlatConfig;
|
|
278
255
|
typeErrors: FlatConfig;
|
|
279
256
|
};
|
|
257
|
+
/**
|
|
258
|
+
* The library's helpers that register `beforeEach` / `afterEach` / `beforeAll` / `afterAll` when
|
|
259
|
+
* called, for `vitest/require-hook`'s `allowedFunctionCalls`.
|
|
260
|
+
*/
|
|
261
|
+
hookRegisteringHelpers: readonly string[];
|
|
280
262
|
}
|
|
281
263
|
declare const plugin: AutoSpyEslintPlugin;
|
|
282
264
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitest-auto-spy",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.48.0",
|
|
4
4
|
"description": "Auto-generate fully-typed test spies from a class — across Vitest, Bun, node:test and Rstest, with Angular/NestJS/React/Vue/Svelte recipes. Drop-in replacement for jest-auto-spies and jasmine-auto-spies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auto-mock",
|
|
@@ -384,7 +384,9 @@ it('loads', async () => {
|
|
|
384
384
|
`toHaveSignalValue(v)` after `registerSignalMatchers()` from `vitest-auto-spy/angular/matchers`
|
|
385
385
|
(`toEqual` semantics; `toHaveSignalValue(v, { strict: true })` for `toStrictEqual`'s, or
|
|
386
386
|
`registerSignalMatchers({ strict: true })` for the whole suite). The type-aware
|
|
387
|
-
`prefer-to-have-signal-value` lint rule rewrites `expect(sig()).toBe(v)`
|
|
387
|
+
`prefer-to-have-signal-value` lint rule rewrites `expect(sig()).toBe(v)`, `.toBeNull()` and
|
|
388
|
+
`.toBeUndefined()` into the matcher, except a `toBe` that checks an object's identity, which the
|
|
389
|
+
deep matcher cannot keep.
|
|
388
390
|
- **Never assert a resource with `expect(r.value()).toEqual(...)` alone** — an unresolved resource
|
|
389
391
|
still holds its _default_, so that passes while proving nothing. `toHaveResourceValue(v)` after
|
|
390
392
|
`registerResourceMatchers()`, from the same `/angular/matchers` entry, fails unless the resource
|
|
@@ -492,6 +494,11 @@ evidence that the types still hold. Where it cannot prove the rename it downgrad
|
|
|
492
494
|
accept those together with the repair at the creation site, usually `createAutoMock<T>()` in place of
|
|
493
495
|
an object literal.
|
|
494
496
|
|
|
497
|
+
**`vitest/require-hook` reports a bare `useConsoleSpies();` or `setupFakeTimers();`** in a
|
|
498
|
+
`describe` body. Do not wrap it in a hook or disable the line: spread
|
|
499
|
+
`autoSpy.hookRegisteringHelpers` from `vitest-auto-spy/eslint-plugin` into the rule's
|
|
500
|
+
`allowedFunctionCalls`.
|
|
501
|
+
|
|
495
502
|
**`no-private-member-access`, `no-mistyped-use-value` and `no-unknown-use-value-key` are the three
|
|
496
503
|
rules here that need type information.** The first reports
|
|
497
504
|
`instance['privateMember']` (bracket access, which TypeScript does not visibility-check),
|