Upgrading to 4.0
Version 4.0 makes the package lighter for your project. No helper was removed and spies behave the same. You change at most a few import lines:
// before
import { createSpyFromClass, stubIntersectionObserver } from 'vitest-auto-spy';
// after
import { createSpyFromClass } from 'vitest-auto-spy';
import { stubIntersectionObserver } from 'vitest-auto-spy/dom-stubs';Checklist
- Bump the version and run your type-check, tests and linter.
- If the compiler cannot find a DOM stub or a run-diagnostics helper, change its import to
vitest-auto-spy/dom-stubsorvitest-auto-spy/diagnostics.npx vitest-auto-spy codemoddoes it for you. See section 2. - If you annotate
returnSubject()as rxjsSubject<T>and it stops compiling, addimport 'vitest-auto-spy/rxjs';to a file yourtsconfigincludes. See section 1. - If the linter turns red, every rule is now an error. Tune three rules or take everything as warnings first. See section 3.
Why upgrade
This release adds no feature and changes no runtime behaviour. Every row below is a cost your project stops paying.
| What you get | Measured |
|---|---|
rxjs leaves your TypeScript program. Before, the published types named Observable and Subject, so every project loaded rxjs types. | program 303 → 114 files; rxjs files 189 → 0 |
The published .d.ts compiles without rxjs installed | TS2307 under skipLibCheck: false with no rxjs: reproducible → gone |
| Every spec file that does not use a DOM global starts faster | −0.159 ms per file; root entry 15.5 → 12.9 kB min+gzip |
| A second copy of rxjs no longer breaks observable spies | nextWith is not a function with no hint at the duplicate → the helpers are there |
| Your project decides how strict the lint rules are | all nineteen rules are error; each rule page shows how to relax it |
The cost: two import specifiers, plus one import if you annotate returnSubject().
What changed
Three breaking changes. None of them changes what a spy does:
- no helper was removed or renamed;
- no configuration key changed meaning;
- runtime behaviour is the same.
What changed is where two groups of helpers are imported from, the type of returnSubject(), and how strict the lint rules are.
| What to do | |
|---|---|
| 1. rxjs left the declarations | usually nothing; one import if you annotate returnSubject() |
| 2. DOM stubs and run diagnostics moved | change the import of thirteen helpers, on every entry that re-exports the core |
| 3. Every lint rule is an error | nothing, or one config block |
1. rxjs left the published declarations
At runtime, rxjs was always optional: only vitest-auto-spy/rxjs imports it. The published types were different. They started with import { Observable, Subject } from 'rxjs'. TypeScript loads a type-only import like any other import. So:
- every project loaded rxjs types, even without using rxjs;
- a project without rxjs got
TS2307inside the package's.d.tsunderskipLibCheck: false.
Measured on the published package, for a project that only calls createSpyFromClass on a promise-based service:
| 3.18 | 4.0 | |
|---|---|---|
| files in the TypeScript program | 303 | 114 |
of those, rxjs .d.ts files | 189 | 0 |
TS2307 with skipLibCheck: false, no rxjs installed | yes | none |
Why import type does not help: see the rxjs page.
Does it affect you?
Run your type-check. It can report two things; only the first is common.
returnSubject() is typed as SubjectLike when the rxjs layer is not in your program
returnSubject() and nextWithPerCall() now return SubjectOf<T>:
- If
vitest-auto-spy/rxjsis part of the program TypeScript checks your specs in,SubjectOf<T>is rxjs's ownSubject<T>. - If it is not, it is the structural
SubjectLike<T>:next,error,completeandasObservable.
In the second case, an annotation like this stops compiling:
const subject: Subject<Product[]> = service.getProducts$.returnSubject();
// ~~~~~~~ Type 'SubjectLike<Product[]>' is not assignable to type 'Subject<Product[]>'The fix is one line in a file your tsconfig includes:
import 'vitest-auto-spy/rxjs';The observable helpers already need this import at runtime. If your tests call nextWith, you have it somewhere. The error means the compiler does not see that file. Usually it is a Vitest setupFiles entry that no tsconfig includes. Add the file to include, or move the import into a spec.
Under @angular/build:unit-test 22.2.0 and later, include alone is not enough. The builder compiles only the specs, the providersFile, the setupFiles and the .d.ts files of tsconfig.spec.json. Put the import in a setup file, a spec or a .d.ts.
If you never annotate the result (const subject = spy.load.returnSubject()), nothing changes for you.
Detection is structural, so a duplicated rxjs now matches
A member counts as an observable when its type has subscribe and a forEach(next) that returns a promise.
- These match: rxjs
Observable, everySubject, AngularEventEmitter. - These do not:
Promise, arrays,Signal, AngularOutputEmitterRef.
So nothing that used to be a plain spy becomes an observable spy by accident.
One case gets better: an Observable from a second copy of rxjs in node_modules. Before, it got no observable helpers, and the test failed with nextWith is not a function. Now it works.
If your own stream type happens to have both members, it now counts as an observable too. If that is wrong for your type, name the member in methodsToSpyOn or onlyMethodsToSpyOn.
2. DOM stubs and run diagnostics moved to their own subpaths
Thirteen helpers left every entry that re-exports the core: vitest-auto-spy, /bun, /bun-angular, /node, /react, /vue and /svelte. Importing from /react instead of the root does not avoid the change. The helpers themselves are the same; only the import path changed:
| Was | Is now |
|---|---|
stubIntersectionObserver, stubResizeObserver, stubMutationObserver, stubObserver, intersectionEntry, resizeEntry, mutationRecord, stubMediaElement, stubAbortController | vitest-auto-spy/dom-stubs |
compareTestRuns, summarizeTestRun, formatTestRunComparison, diffByField | vitest-auto-spy/diagnostics |
mockConstructor, stubConstructor and createSpyClass stay on the root: they are about new, not about the DOM. restoreMockedProps() and setupAutoSpy() still undo everything the moved stubs patch.
Their types moved with them:
- from
/dom-stubs:ObserverStub,ObserverStubOptions,ObserverGlobal,ObserverInstance,IntersectionObserverStubOptions,MutationRecordInit,ResizeEntryRect,MediaElementState,MediaElementStub,MediaElementStubOptions; - from
/diagnostics:TestRunComparison,TestRunReport,TestRunSummary.
// before
import { createSpyFromClass, stubIntersectionObserver } from 'vitest-auto-spy';
// after
import { createSpyFromClass } from 'vitest-auto-spy';
import { stubIntersectionObserver } from 'vitest-auto-spy/dom-stubs';The compiler finds every place for you. npx vitest-auto-spy codemod rewrites the imports. It reads the exports map of the installed package, so each name goes to the entry that really has it.
Why. A test runner does not tree-shake spec files, and an ESM re-export always loads its module. So every spec file, even in a Node service with no DOM, ran 27 kB of observer, media-element and AbortController code to get createSpyFromClass. The only fix is to stop exporting it from the root.
Measured on the built package, one process per sample, medians of 40 paired runs:
| change | |
|---|---|
| a spec file that does not import them | −0.159 ms |
a spec file that does import /dom-stubs | +0.155 ms |
a spec file that does import /diagnostics | +0.069 ms |
dist | −20.3 kB of JS, −3.8 kB of declarations |
Both new entries are built as single files, so importing one adds one module, not four. Split into chunks, the second row read +0.62 ms. If fewer than half of your spec files import the DOM stubs, your whole run gets faster.
3. Every ESLint rule is an error
configs.recommended used to set its nineteen rules to error, warn or off. Now all of them are error. One rule added later is the exception: prefer-render-shallow is a warn, because it reports a cost, not a bug. The plugin page explains why.
A build does not stop on a warn. In a repository where nobody reads lint output, warn works like off. Which findings block a merge is your project's choice, and it is one line of config. So the default is strict, and Tuning it for your project shows how to relax it.
Two rules changed more than their severity:
prefer-native-spy-apiwasoffbefore 4.0. It is on now because most projects are not in the middle of a migration. A project that is turns it off with one line (below).jasmine-namespace-without-entryno longer reportsspy.mock.calls[0]. It used to report this common Vitest read atwarn, while barespy.mock.callspassed. Anything under.mockis never reported now.
If the first run is red, look at these three rules first. Each can report correct code:
import autoSpy from 'vitest-auto-spy/eslint-plugin';
export default [
{
files: ['**/*.spec.ts'],
...autoSpy.configs.recommended,
rules: {
...autoSpy.configs.recommended.rules, // spread it, or a bare `rules` key replaces the whole map
// the jasmine layer is installed in a setup file no spec imports: name that file
'vitest-auto-spy/jasmine-namespace-without-entry': ['error', { setupModules: ['./test-setup'] }],
// reports working bridge code: off until the move off jasmine-auto-spies is finished
'vitest-auto-spy/prefer-native-spy-api': 'off',
// no option; it is silent wherever it cannot read all of a file's providers.
// Turn it off only in files where a helper you call registers the provider.
// 'vitest-auto-spy/no-unregistered-inject-spy': 'off',
},
},
];On a large existing codebase, start with every rule as a warning and fix in batches. The recipe is in Tuning it for your project.
Also changed in 3.x
If you come from an early 3.x release, these changes reach you too.
- 3.1.0:
gettersToSpyOnandsettersToSpyOnaccept a getter that returns aSignal. Before, a signal getter could not be named. The error wasType 'string' is not assignable to type 'never', with nothing about signals in it. - 3.5.0: naming one half of a get/set pair spies both halves. This happens only when the class declares both. Before, a write went to a no-op setter and was lost, and
accessorSpies.setters.xwasundefined. - 3.5.0: an observable spy no longer carries values into the next test. A
nextWith(value)from one test used to reach the next one ahead of itsthrowWith(error). The code under test then ran the success branch on old data. Aftererror()orcomplete(), laternextWithcalls emitted nothing.resetAutoSpynow clears both. For a spy shared across tests, callresetAutoSpy(spy)inbeforeEach. - 3.5.0:
expectEmissionandexpectEmissionsinfer the emitted type.expectEmission(of(1))is aPromise<number>. Before, both helpers returnedPromise<unknown>, and the loss showed only when a spec read a field of the result. - 3.5.0:
createAutoMockandmockDeepdoubles answer three operations correctly.- The four
mock*Prophelpers work on them, andrestoreMockedProps()undoes the patch. Before, the patch was silently ignored. delete mock.optionalMethodremoves the member until something writes it again. Before, the next read created a new spy.of(mock)emits the double. Before, rxjs took the double for a scheduler, and the stream stayed empty.
- The four
- 3.5.0:
asInstanceaccepts amockDeepresult. Before, a deep mock could not be passed whereTwas expected. - 3.5.0:
prefer-inject-spyreads the two-step form. It used to report onlyvi.spyOn(TestBed.inject(X), 'm')written inline. - 3.5.0:
provideAutoSpyaccepts abstract class tokens.abstract class LocalStorageprovided as{ provide: LocalStorage, useClass: BrowserLocalStorage }used to fail twice: the bare call gave an empty spy, and the config form did not compile (TS2345: Cannot assign an abstract constructor type to a non-abstract constructor type). Both work now. The hand-written{ provide: LocalStorage, useValue: createAutoMock<LocalStorage>() }is no longer needed. - 3.5.0: both provider helpers take
returns,overridesandobservablePropsToSpyOn. Before,provideAutoSpyForTokentook only property values andprovideAutoSpyonly method configuration, so a spy needing both was finished in abeforeEach. - 3.5.0:
overrideProvider(X, provideAutoSpy(X))works. Older docs said it was a silent no-op.overrideProviderreadsuseValueand ignores the extraprovide, so the spy is installed.overrideAutoSpyis still clearer. - 3.7.0:
stable()gives the fixture 2000 ms and then throws the cause. Before, a fixture that never settled hung until Vitest reported a 5 s file-level timeout that named neither the helper nor the fixture. - 3.13.0:
nextWithValuesemits falsy values.{ value: false },{ value: 0 },{ value: '' },{ value: null }and a falsyerrorValueused to emit nothing. A boolean or counter stream stayed silent. - 3.13.0: a spied method accepts only the arguments the real method accepts. Before,
read(1),read('ok', 'extra')andread()all compiled on a spy ofread(key: string). The mock surface is nowMockInstance, which has no call signature of its own. - 3.7.0: the docs stop showing
httpResource()withflushEventLoopUntil. That example never worked. The helper takes event-loop turns and never ticks, so the resource made no request and the wait timed out. UsesettleResource()fromvitest-auto-spy/angularinstead. - Docs correction (2026-08-29):
vi.mock('@angular/core')works under@angular/build:unit-test. Earlier docs said it could not be mocked at all. Measured on 11 spec files collected in one run, on@angular/build21.2.16 and 22.1.6, it does work; the real rule is narrower.
What did not change
- No helper was removed, renamed or deprecated. Two groups moved to new import paths, and that is all.
- No lint rule was added or removed. Apart from the
.mockfix injasmine-namespace-without-entry, none changed what it reports. Only the severity changed. nextWith,nextWithValues,nextOneTimeWith,throwWith,complete,observablePropsToSpyOnand thecalledWithchains behave the same.rxjsstays an optional peer dependency. Onlyvitest-auto-spy/rxjsandvitest-auto-spy/observer-spyimport it, now in the types as well as at runtime. The build fails if any other entry imports it.- The Angular, NestJS, React, Vue, Svelte, Bun and
node:testentry points are unchanged.
Coming from further back
- Upgrading to 3.0 is one line in
package.json: thevitestpeer range now matches what the types need. - Upgrading to 2.0 matters if you are still on 1.x.
methodsToSpyOnstopped removing spies, which took one migrated component from 147 failing tests to 4. Lazy method spies cut a wide test run from 257 ms / 425 MB to 27 ms / 35 MB.