Skip to content

Releases: ASDAlexey/vitest-auto-spy

v5.0.1

Choose a tag to compare

@github-actions github-actions released this 07 Sep 07:53

npm i -D vitest-auto-spy@5.0.1

Fixes

  • diagnostics: let explainSpy read a double from another entry point (5a061d0)

Build & packaging

  • measure coverage with istanbul, which does not lose blocks (973f202)
  • merge the workers' coverage in one call, not pair by pair (7b79c61)
  • say why the coverage step failed, and try three times (375fc10)
  • let the coverage retry fire on a runner, not just locally (d2fa52f)

Documentation

  • ru: reword the Angular range row on the 5.0 upgrade page (d5b1a4b)
  • readme: drop the trailing zero from the major headings (edcc587)

📖 Documentation · 📦 npm · 📝 CHANGELOG · 🤖 AGENTS.md

Full changelog: v5.0.0...v5.0.1

v5.0.0

Choose a tag to compare

@github-actions github-actions released this 07 Sep 05:22

npm i -D vitest-auto-spy@5.0.0

Why upgrade. Three peer and engine floors that this package can actually keep, and fifteen fixes
— most of them cases where a helper reported the wrong test, left the next one dirty, or edited the
wrong span of a file.

Changed — BREAKING

  • The Angular peer range is now >=20.0.0, and @angular/platform-browser is declared. The
    old >=16.0.0 was not a promise this package could keep, and the two ways it broke were both
    link-time rather than graceful:

    • ɵSIGNAL (lib/run-effect.ts) is a value import on the eager path of dist/angular.js,
      and Angular did not export it before 18. A missing named ESM export fails the link, so on
      Angular 16 or 17 the symptom was not "runEffect is unavailable" — the whole /angular entry
      was unavailable, provideAutoSpy included.
    • provideZonelessChangeDetection (src/bun-angular.ts) arrived in 20; in 18–19 the symbol
      was provideExperimentalZonelessChangeDetection, and in 16–17 there was none.

    Verified by downloading @angular/core, @angular/common and @angular/platform-browser 16
    through 22 and parsing their real export lists, not by reading release notes. The same check
    corrected a belief this repository held in the other direction: platformBrowserTesting and
    BrowserTestingModule are exported identically from @angular/platform-browser/testing in
    every major from 16 — what changed in 20 is that platform-browser-dynamic left the picture.

    Nothing supported is dropped. On Angular's own policy — 6 months active plus 12 months LTS — 16,
    17, 18 and 19 are all past end of life (19's LTS ended 2026-05-19); 20 is the oldest major still
    receiving fixes, and it is exactly this package's technical floor.

    @angular/platform-browser was never declared at all, although lib/directive-matchers.ts
    imports By from it as a value and the /angular entry needs it at runtime. Under npm's hoisted
    layout that worked by accident; under pnpm's isolated one it did not resolve. It is now an
    optional peer with the same range.

    There is deliberately no upper bound. A bounded range would force a release for every Angular
    major and hand consumers ERESOLVE for upgrading first. The real fragility is ɵSIGNAL, a
    private symbol, and a range cannot protect against it — reading it structurally can.

  • The rxjs peer range is now >=7.2.0, and the operators come from the root entry.
    lib/observable-spy.ts imported six operators from rxjs/operators, the legacy deep path that
    rxjs 8 removes; the open-ended >=7.0.0 therefore promised a version it could not serve.
    rxjs re-exported every one of them from rxjs itself in 7.2 (verified against 7.2.0, not
    assumed), so the import moved and the floor moved with it. No Angular consumer pays anything:
    Angular 16 through 22 all peer on ^6.5.3 || ^7.4.0, so an Angular project already has more than
    this asks.

  • engines.node is now >=22. The old >=18 outlived both versions it named: Node 18 went
    end of life 2025-04-30 and Node 20 followed on 2026-04-30, so the floor promised maintenance
    nobody was giving. Node 22 is the oldest release still in Active LTS, and it is what every number
    on the performance page is measured on. engines is advisory in npm's default configuration — an
    install on an older runtime warns rather than fails — but a consumer running --engine-strict,
    and every CI image pinned by this field, will see it.

  • flushEffects() calls TestBed.tick() directly. The ApplicationRef.tick() fallback existed
    for Angular below 20 and is now unreachable, and with it goes the spec that deleted TestBed.tick
    at runtime purely to drive that branch to full coverage.

Unchanged, and worth saying because both were checked rather than assumed: vitest stays at
>=2.1.0 — every runner API this package uses unguarded exists in 2.1.0, and the one later
addition, vi.defineHelper (4.1), is feature-probed. zone.js stays absent from the peer list —
/zone reads globalThis.Zone and imports nothing from it.

Fixed

  • stubAbortController() fires onabort exactly once under happy-dom as well as jsdom.
    happy-dom's EventTarget invokes on<type> properties itself and jsdom's does not, so the stub's
    own this.signal.onabort?.(event) was a second call on one runtime and the only call on the other.
    The property is parked for the dispatch and run from a { once: true } listener instead, which is
    one call on both. The DOM stubs now have a suite of their own under real happy-dom
    (npm run test:happy-dom, in the gate and in CI) — until this release every one of them was
    verified against jsdom alone and a environment: 'happy-dom' project was taking them on trust.
  • A signal's reason is the DOMException the platform produces, not new Error('AbortError').
    Code under test that branches on signal.reason.name === 'AbortError' — the shape the platform
    documents, and what an AbortError-vs-TimeoutError distinction is written against — took the
    else against the stub while passing in the browser.
  • mockReadonlyProp / mockReadonlyPropGetter no longer leave the real setter live.
    Object.defineProperty over an existing accessor pair keeps whatever the new descriptor does not
    name, so defining get alone left the object's own set in place: a write the code under test
    made went into the real setter, silently, and the getter kept answering with the stub value. The
    descriptor now names set: undefined, which is what "readonly" was supposed to mean.
  • mockDeep keeps the call state attached when .mock is read before the first call. The spy
    surface was read with the Proxy as the receiver, so the fast engine's lazy mock getter wrote its
    state onto the node while the raw spy recorded calls into its own — and
    const state = api.getName.mock taken at the top of a test stayed empty however often the method
    ran. Members are now read with the spy as the receiver.
  • A delay on resolveWith counts from the call, not from the line that configured it. The
    delayed promise was built at configuration time, so vi.advanceTimersByTime() in the test body
    raced a timer that had already been running since beforeEach; a per-call entry now builds its
    promise when the call happens.
  • A spy's recorded result no longer settles on any thenable. settleInto treated anything with
    a .then as a promise to await, and a lazy query builder's .then is the query — so reading the
    result of one call executed it, and then being a member another spy owned recorded a call nobody
    made. Only a real Promise is settled, which is the test Vitest's own runner applies.
  • A failing Angular teardown check leaves the next test a clean TestBed. A throwing afterEach
    makes Vitest skip that test's remaining afterEach hooks — the framework's own module teardown
    among them — so the report named the next spec, dying on a stale module, instead of the one that
    leaked. Both provideHttpTesting()'s verification and
    enableAngularDiagnostics({ pendingRequests }) now reset before rethrowing.
  • provideHttpTesting() keeps verifying after the first test of a file. The teardown check was
    armed one shot per call, so hoisting the providers to a module constant — the ordinary
    optimisation once a suite uses the helper in a dozen places — verified test one and let every later
    test leak in silence. The setting is now the suite's policy, and each test's afterEach decides
    from whether that test's TestBed has an HttpTestingController at all.
  • overrideComponentProvider no longer fails the next test with a stale queue. A test that
    queued an override and never rendered — an @if branch not taken, a spec asserting only on the
    service — left its entry and the createComponent wrapper installed across resetTestingModule;
    the next test that did render verified the previous test's entry against its own fixture and
    failed with a false the override did not apply. The queue and the wrapper are now dropped when
    the framework resets the module.
  • extendWithAutoSpies({ providers }) wins over the generated spy, as its own documentation
    said.
    The listed providers went in before the generated ones and Angular resolves duplicates
    last-one-wins, so naming a token to override it had exactly no effect.
  • An until predicate that throws fails the assertion instead of the timeout. rxjs routes a
    throw from a subscriber callback to reportUnhandledError on a fresh macrotask: the run collected
    an unhandled error, the subscription and the watchdog stayed alive, and the eventual message
    blamed the silence — did not emit within 5000 ms — rather than the predicate. The throw is now
    caught where it happens and rejects with the predicate, the emission index and the cause.
  • The codemod stops rewriting prose inside comments and strings. Its mask scanned with one
    String.replace, and the regex-literal alternative could swallow a span that began at a
    division — running to the next /, taking an unseen comment or quote with it. The span was
    handed back "unchanged" but consumed, so its contents stayed in the code mask and the transforms
    edited the sentence inside. The scan now backs up to the slash and resumes one character later, and
    a / is read as opening a regular expression only after a keyword or an operator.
  • The codemod keeps a comment inside an import clause attached to its specifier. The clause was
    split on raw commas, so a comment containing one produced a specifier nobody exported; and the
    rewritten statement joined every name onto one line, which put the closing } and the whole
    from '…' inside a line comment — emitting a statement that no longer parses while the residue
    check, now also commented out, reported the file as f...
Read more

v4.6.1

Choose a tag to compare

@github-actions github-actions released this 06 Sep 09:21

npm i -D vitest-auto-spy@4.6.1

Fixes


📖 Documentation · 📦 npm · 📝 CHANGELOG · 🤖 AGENTS.md

Full changelog: v4.6.0...v4.6.1

v4.6.0

Choose a tag to compare

@github-actions github-actions released this 06 Sep 09:03

npm i -D vitest-auto-spy@4.6.0

Features

  • site: mirror the docs under /ru/ and rework both landings (d50997e)

Fixes

  • keep spies and matchers working on Vitest 5 (b9bfb19)
  • eslint-plugin: two wrong prefer-render-shallow reports (bc9cee2)
  • docs: search on the Russian landing uses the English index (ac438c5)

Build & packaging

  • extract the blocks jscpd 5 reports as duplicates (b95cfca)
  • move to Angular 22 and TypeScript 6 (145124a)

Documentation

  • state Vitest 5 support on every surface (4075c45)

📖 Documentation · 📦 npm · 📝 CHANGELOG · 🤖 AGENTS.md

Full changelog: v4.5.1...v4.6.0

v4.5.1

Choose a tag to compare

@github-actions github-actions released this 05 Sep 18:51

npm i -D vitest-auto-spy@4.5.1

Fixes

  • exempt a createDirectiveHost harness from the strict policy (59694fe)
  • add NO_ERRORS_SCHEMA only where a schema can apply (623a9ea)

Documentation

  • describe the render rule on every surface (c122f89)

📖 Documentation · 📦 npm · 📝 CHANGELOG · 🤖 AGENTS.md

Full changelog: v4.5.0...v4.5.1

v4.5.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 16:50

npm i -D vitest-auto-spy@4.5.0

Features

  • eslint-plugin: report a render nothing in the file reads (a9f0e83)

Fixes

  • site: show the language menu between 960px and 1280px (6ff3407)

Documentation

  • document prefer-render-shallow on every surface (3f4b551)
  • contributing: add the Russian landing to the surfaces table (68b2335)
  • site: add a Russian landing behind a ru locale (a266d65)

📖 Documentation · 📦 npm · 📝 CHANGELOG · 🤖 AGENTS.md

Full changelog: v4.4.0...v4.5.0

v4.4.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 13:52

npm i -D vitest-auto-spy@4.4.0

Features

  • setup: restore the Web Storage Vitest never copies over (0994be3)

Documentation

  • performance: republish the bundle-size table from the baseline (686bd5d)
  • document restoreWebStorage on every surface (7ee758f)

📖 Documentation · 📦 npm · 📝 CHANGELOG · 🤖 AGENTS.md

Full changelog: v4.3.0...v4.4.0

v4.3.0

Choose a tag to compare

@github-actions github-actions released this 04 Sep 20:57

npm i -D vitest-auto-spy@4.3.0

Features

  • cli: add doctor checks that resolve a name, not a file (7e1bcc5)
  • diagnostics: add explainSpy (c7f9503)
  • spy: add createSpyFromInstance (9d936fc)

Fixes

  • node: name node:test spies after the method (995516c)
  • rxjs: report emission failures at the caller's line (2c8b7f2)

Performance

  • build: pin only the emission timeout cell in the shared chunk (a06a348)

Documentation

  • document the new helpers on every surface (9e22eac)
  • add the @testing-library/angular migration page (9efe420)

📖 Documentation · 📦 npm · 📝 CHANGELOG · 🤖 AGENTS.md

Full changelog: v4.2.0...v4.3.0

v4.2.0

Choose a tag to compare

@github-actions github-actions released this 04 Sep 00:13

npm i -D vitest-auto-spy@4.2.0

Features

  • bench: gate benchmark ratios against a committed baseline (f90ddf6)
  • perf: add a cold-import harness with a module-graph gate (38df7e2)
  • size: gate the min+gzip weight of every entry point (d2209a9)

Performance

  • bench: measure a spied call against a batched plain call (d3347bb)

Build & packaging

  • register the new gates as npm scripts and CI steps (6cdffa4)
  • refresh the lockfile and gate on drift and formatting (39018f5)

Documentation

  • republish every performance number from a fresh measurement (8583a65)
  • bench: document the ratio gate and the batching it needs (42ea50f)

📖 Documentation · 📦 npm · 📝 CHANGELOG · 🤖 AGENTS.md

Full changelog: v4.1.0...v4.2.0

v4.1.0

Choose a tag to compare

@github-actions github-actions released this 03 Sep 21:43

npm i -D vitest-auto-spy@4.1.0

Why upgrade. A method spy is no longer a vi.fn(), and that is the whole release. The library
builds its own mock function — one shared prototype carrying the Mock surface, call state
allocated on the first call rather than at creation, no entry in any global registry — and every
table in the head-to-head benchmark changed hands.

Case (micro, median p75 of seven runs) 4.0.1 4.1.0 best other arm lead
small project — 6 methods, 1 called 1.42 µs 6.83 µs hand-written 4.82×
medium project — 14 methods, 2 called 2.67 µs 15.92 µs hand-written 5.97×
large project — 45 methods, 2 called 5.79 µs 52.79 µs hand-written 9.11×
10 methods, all 10 called → 14 methods, all 14 18.92 µs (0.66× — a loss) 8.17 µs 17.92 µs hand-written 2.19×
40 methods, all 40 called → 45 methods, all 45 75.33 µs (0.71× — a loss) 26.12 µs 62.04 µs hand-written 2.37×
double from a type — 2 members 3.58 µs (0.77× — a loss) 1.00 µs 2.79 µs vitest-mock-extended 2.79×
double from a type — 40 members 72.88 µs (0.80× — a loss) 18.92 µs 56.79 µs vitest-mock-extended 3.00×
deep double, 3 levels, leaf called 8.83 µs (0.61× — a loss) 2.29 µs 5.46 µs vitest-mock-extended 2.38×
configure a return + 3 calls — from a type 2.08 µs (0.76× — a loss) 0.71 µs 1.58 µs @golevelup 2.24×
calledWith dispatch, 2 configured + 1 miss 0.54 µs (parity) 0.17 µs 0.54 µs vitest-mock-extended 3.25×

Six of those rows were losses and one was parity; the narrowest margin now is 2.19×. The class cases
changed shape in the same release (see below), so the two left-hand columns are the same operation
on a slightly different class where the row says so. Everything is the median p75 of seven
independent runs on one machine, npm run bench:vs:precise.

At suite scale the same change is worth a few per cent, not a multiple, and that row is published
too: npm run bench:suite on a 20-method class under isolate: true puts hand-written vi.fn()
doubles about 5 % ahead of this library at the median across 1 000 / 3 000 / 10 000 tests, where
they were 10-15 % ahead before — two runs of three rounds, individual rounds 0.81-1.01×. Building a
double is on the order of one per cent of what a test costs — a 10× win on the double cannot be worth
more than that on the run, and anybody quoting the micro-benchmark as a suite-level claim is quoting
it wrong. Against @bugsplat/vitest-auto-spies the same runs measure 1.63-1.72×.

What it costs. One thing, and it is namable: mock.invocationCallOrder counts on this library's
own scale, so toHaveBeenCalledBefore / toHaveBeenCalledAfter between an auto-spy and a
hand-written vi.fn()
compares two counters that never met. Everything else a spec can observe is
identical, and the suite pins it by putting a spy and a vi.fn() through the same steps and
comparing their recorded state: vi.isMockFunction, every matcher, the snapshot serialiser, the
whole mockReturnValue / mockResolvedValue / mockImplementation family, mock.calls /
.results / .settledResults / .instances / .contexts / .lastCall, mockClear / mockReset
/ mockRestore, using, vi.clearAllMocks(), vi.resetAllMocks() and the clearMocks /
mockReset config keys. Nothing was removed or renamed.

Added

  • setSpyEngine(engine) / getSpyEngine(), on vitest-auto-spy/setup. 'auto-spy' is the
    default; 'runner' builds every method spy from vi.fn(), method for method, exactly as every
    release before this one did. Doubles already built keep the engine they were built with. Vitest
    only — on Bun and node:test the runner's own matchers recognise only the runner's own mocks, so
    those entries keep using them.

Changed

  • Method spies come from this library's own mock function on Vitest. vi.fn() assigns some
    twenty-five closures as own properties of every mock it creates, allocates six arrays of call
    state up front, and registers the mock in a module-level strong Set — per method, on every
    double a spec builds. The replacement is one function object and one small config record; the six
    arrays appear on the first call, so a materialised method that is never called owns none of them.
  • A run-wide vi.clearAllMocks() reaches those spies through one registered vi.fn() of the
    library's own.
    Vitest clears mocks by walking a Set inside @vitest/spy that only vi.fn()
    and vi.spyOn() write to and that has no public API; the adapter registers a single mock whose
    mockClear sweeps this library's spies instead. clearMocks: true and mockReset: true in a
    config keep working untouched, because Vitest applies both through those same two functions.
  • What it weighs. The engine adds +1.4 kB min+gzip to every entry that builds Vitest spies —
    . 12.94 → 14.33 kB, /angular 16.84 → 18.23 kB, /setup 10.60 → 12.09 kB, /nestjs 9.54 →
    10.95 kB — which is the mock function itself and buys the tables above. The entries that build no
    Vitest spy are unchanged to the byte (/rxjs, /angular-http, /diagnostics, /jasmine-compat,
    /observer-spy, /zone, /eslint-plugin), and /node, /bun and /bun-angular pay +0.12 kB
    or less
    , not the full engine — the two predicates the settled-results polyfill needs moved out of
    the engine module so the runtimes that never build a fast spy stop bundling one.
  • A sweep is now O(1) and holds nothing alive. It bumps a counter; each spy compares its own
    stamp against it before it records or reports anything, and empties itself if it is behind. A
    state object a spec is holding answers with the emptied arrays too, which is what the runner's own
    state does.
  • calledWith(x) with a single primitive argument is looked up by value. It was rendered into a
    string key on every call of that spy — an array from map, a string per argument, a joined
    string, then a hash — for a lookup a Map does on the value with no allocation at all. Shapes
    where the two disagree (a symbol, which renders by description; -0, which renders apart from 0
    and is the same key under SameValueZero) stay on the string path and keep the answer they had.
  • The benchmark's class cases are now measured project profiles rather than round numbers.
    Across four private Angular suites — ~2 700 spec files, 2 742 doubles built from a class — the
    service a spec doubles has 5–8 methods at the median, 12–16 at the p75 and 32–44 at the p90, and
    the spec touches 1 of them at the median and 2 at the p90: 5–6 % of what it built. The cases
    are that survey's median, p75 and p90, plus two worst case blocks where every method is called.

Fixed

  • pruneMockRegistry() no longer drops the mock that carries the sweep. It would have taken the
    sentinel with everything else the file created, and vi.clearAllMocks() would then have gone
    silently nowhere for the rest of the run — found under isolate: false, where it reproduced.

📖 Documentation · 📦 npm · 📝 CHANGELOG · 🤖 AGENTS.md

Full changelog: v4.0.1...v4.1.0