Skip to content

v5.0.0

Choose a tag to compare

@github-actions github-actions released this 07 Sep 05:22
· 11 commits to master since this release

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 fully migrated. Comments ride their specifier,
    and a clause carrying one is emitted one name per line.
  • init never overwrites a file it did not write. .cursor/rules/vitest-auto-spy.mdc and
    .claude/skills/vitest-auto-spy/SKILL.md are exactly the paths a team plausibly authored before
    discovering this CLI; the owned-file plan rewrote them regardless, and --uninstall then deleted
    the replacement. A file present without the managed markers is now left byte for byte, and named
    in the warnings.
  • codemod says so when the repository scan stopped at its safety cap. Past 50 000 files the
    scan truncates, and Nothing left to migrate off a truncated list is a claim about a tree the
    tool never looked at.

Added

  • AbortSignal.abort(), AbortSignal.timeout() and AbortSignal.any() on the stub. The three
    statics are how modern code makes a signal without a controller — fetch(url, { signal: AbortSignal.timeout(5_000) }) most of all — and the stub had none of them, so a spec that called
    stubAbortController() to fix the jsdom brand-check broke the code it was trying to test.
    timeout() aborts through setTimeout, so vi.useFakeTimers() drives it exactly as it drives the
    platform's, and with a TimeoutError rather than an AbortError because that is the distinction
    the platform draws.
  • video.currentTime = 0 reaches the record and fires timeupdate. media.set() was the only
    way in, and a player restarting itself assigns the field directly — the component's own
    timeupdate handler stayed unrun while the assertion read the new value, which looks like a bug in
    the component. The stub's currentTime is now a get/set pair, and media.set() is unchanged.
  • VITEST_AUTO_SPY_SCAN_CAP raises the CLI's 50 000-file scan cap. The truncation warning above
    tells the reader to raise the cap; this is the cap.

Size and memory

/dom-stubs is +219 B (5 030 → 5 249 B min+gzip, +4.4 %), the one entry past the 200 B
allowance, and it is all stub surface rather than machinery: 42 B for the happy-dom onabort
repair, and 177 B for the three AbortSignal statics, the DOMException reasons and the
currentTime setter — measured by building the entry with those two files at their previous
revision. The module graph is unchanged at 2 modules, so nothing new is pulled in; the bytes are
code this entry already had to have to stand in for the platform it stands in for. Every other entry
moved under 1 %: /angular +192 B (+1.0 %) for the override queue and the HTTP verification policy,
. / /react / /vue / /svelte +76 B (+0.5 %), /bun +69 B, /node +67 B, /bun-angular
+46 B, /setup +4 B, /eslint-plugin and /diagnostics unchanged, /rxjs −12 B.


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

Full changelog: v4.6.1...v5.0.0