Перейти к содержимому

Переход на 4.0 ​

Версия 4.0 делает пакет легче для вашего проекта. Ни один хелпер не удалён, спаи работают как раньше. Самое большее — вы поменяете несколько строк импорта:

ts
// было
import { createSpyFromClass, stubIntersectionObserver } from 'vitest-auto-spy';

// стало
import { createSpyFromClass } from 'vitest-auto-spy';
import { stubIntersectionObserver } from 'vitest-auto-spy/dom-stubs';

Что проверить

  1. Поднимите версию и запустите проверку типов, тесты и линтер.
  2. Если компилятор не находит DOM-заглушку или хелпер диагностики прогона, поменяйте импорт на vitest-auto-spy/dom-stubs или vitest-auto-spy/diagnostics. Это делает за вас npx vitest-auto-spy codemod. Подробно — раздел 2.
  3. Если вы аннотируете returnSubject() как Subject<T> из rxjs и это перестало компилироваться, добавьте import 'vitest-auto-spy/rxjs'; в файл, который включён в tsconfig. Подробно — раздел 1.
  4. Если линтер покраснел: теперь каждое правило — ошибка. Настройте три правила или сначала переведите всё в предупреждения. Подробно — раздел 3.

Зачем обновляться ​

Релиз не добавляет возможностей и не меняет поведение в рантайме. Каждая строка ниже — расход, который ваш проект перестаёт нести.

Что вы получаетеИзмерено
rxjs уходит из вашей TypeScript-программы. Раньше публикуемые типы упоминали Observable и Subject, и типы rxjs грузились в каждый проект.программа 303 → 114 файлов; файлов rxjs 189 → 0
Публикуемый .d.ts компилируется без установленного rxjsTS2307 при skipLibCheck: false без rxjs: воспроизводилось → исчезло
Каждый файл спеки, который не трогает DOM-глобалы, стартует быстрее−0,159 мс на файл; корневая точка входа 15,5 → 12,9 кБ min+gzip
Вторая копия rxjs больше не ломает observable-спаиnextWith is not a function без намёка на дубликат → хелперы на месте
Строгость правил линтера выбирает ваш проектвсе девятнадцать правил — error; на странице каждого правила сказано, как смягчить

Цена — два пути импорта плюс один импорт, если вы аннотируете returnSubject().

Что изменилось ​

Три ломающих изменения. Ни одно не меняет того, что делает спай:

  • ни один хелпер не удалён и не переименован;
  • ни один ключ конфигурации не поменял смысл;
  • поведение в рантайме прежнее.

Поменялось, откуда импортируются две группы хелперов, тип returnSubject() и строгость правил линтера.

Что делать
1. rxjs ушёл из декларацийобычно ничего; один импорт, если вы аннотируете returnSubject()
2. DOM-заглушки и диагностика прогона переехалипоменять импорт у тринадцати хелперов во всех точках входа, которые реэкспортируют ядро
3. Каждое правило линтера — ошибканичего или один блок конфигурации

1. rxjs ушёл из публикуемых деклараций ​

В рантайме rxjs всегда был необязательным: его импортирует только vitest-auto-spy/rxjs. С типами было иначе. Публикуемые декларации начинались со строки import { Observable, Subject } from 'rxjs'. TypeScript загружает импорт одних типов так же, как любой другой импорт. Поэтому:

  • каждый проект загружал типы rxjs, даже если rxjs не использовал;
  • проект без rxjs получал TS2307 внутри .d.ts пакета при skipLibCheck: false.

Замер на опубликованном пакете, для проекта, который только вызывает createSpyFromClass на сервисе с промисами:

3.184.0
файлов в программе TypeScript303114
из них .d.ts-файлов rxjs1890
TS2307 при skipLibCheck: false без установленного rxjsданет

Почему не помогает import type — на странице про rxjs.

Касается ли это вас? ​

Запустите проверку типов. Она может пожаловаться на две вещи; часто встречается только первая.

returnSubject() получает тип SubjectLike, если слоя rxjs нет в программе ​

returnSubject() и nextWithPerCall() теперь возвращают SubjectOf<T>:

  • Если vitest-auto-spy/rxjs входит в программу, в которой TypeScript проверяет спеки, SubjectOf<T> — это и есть Subject<T> из rxjs.
  • Если не входит — это структурный SubjectLike<T>: next, error, complete и asObservable.

Во втором случае такая аннотация перестаёт компилироваться:

ts
const subject: Subject<Product[]> = service.getProducts$.returnSubject();
//    ~~~~~~~ Type 'SubjectLike<Product[]>' is not assignable to type 'Subject<Product[]>'

Чинится одной строкой в файле, который включён в tsconfig:

ts
import 'vitest-auto-spy/rxjs';

Этот импорт и так нужен observable-хелперам в рантайме. Если тесты вызывают nextWith, он у вас где-то есть. Ошибка значит, что этот файл не видит компилятор. Обычно это файл из setupFiles Vitest, который не входит ни в один tsconfig. Добавьте его в include или перенесите импорт в спеку.

Под @angular/build:unit-test 22.2.0 и новее одного include мало. Билдер компилирует только спеки, providersFile, setupFiles и .d.ts-файлы из tsconfig.spec.json. Поставьте импорт в setup-файл, в спеку или в .d.ts.

Если вы не аннотируете результат (const subject = spy.load.returnSubject()), для вас ничего не меняется.

Распознавание структурное, так что продублированный rxjs теперь подходит ​

Член класса считается observable, если в его типе есть subscribe и forEach(next), который возвращает промис.

  • Подходят: Observable из rxjs, любой Subject, EventEmitter из Angular.
  • Не подходят: Promise, массивы, Signal, OutputEmitterRef из Angular.

Поэтому обычный спай случайно не станет observable-спаем.

Один случай стал лучше: Observable из второй копии rxjs в node_modules. Раньше он оставался без observable-хелперов, и тест падал с nextWith is not a function. Теперь всё работает.

Если у вашего собственного типа потока случайно есть оба члена, он тоже считается observable. Если для вашего типа это неверно, назовите член в methodsToSpyOn или onlyMethodsToSpyOn.

2. DOM-заглушки и диагностика прогона переехали на свои подпути ​

Тринадцать хелперов ушли из всех точек входа, которые реэкспортируют ядро: vitest-auto-spy, /bun, /bun-angular, /node, /react, /vue и /svelte. Импорт из /react вместо корня от этого не спасает. Сами хелперы не изменились — поменялся только путь импорта:

БылоСтало
stubIntersectionObserver, stubResizeObserver, stubMutationObserver, stubObserver, intersectionEntry, resizeEntry, mutationRecord, stubMediaElement, stubAbortControllervitest-auto-spy/dom-stubs
compareTestRuns, summarizeTestRun, formatTestRunComparison, diffByFieldvitest-auto-spy/diagnostics

mockConstructor, stubConstructor и createSpyClass остались в корне: они про new, а не про DOM. restoreMockedProps() и setupAutoSpy() по-прежнему откатывают всё, что патчат перенесённые заглушки.

Их типы переехали вместе с ними:

  • из /dom-stubs: ObserverStub, ObserverStubOptions, ObserverGlobal, ObserverInstance, IntersectionObserverStubOptions, MutationRecordInit, ResizeEntryRect, MediaElementState, MediaElementStub, MediaElementStubOptions;
  • из /diagnostics: TestRunComparison, TestRunReport, TestRunSummary.
ts
// было
import { createSpyFromClass, stubIntersectionObserver } from 'vitest-auto-spy';

// стало
import { createSpyFromClass } from 'vitest-auto-spy';
import { stubIntersectionObserver } from 'vitest-auto-spy/dom-stubs';

Компилятор найдёт все такие места. npx vitest-auto-spy codemod перепишет импорты. Он читает карту exports установленного пакета, поэтому каждое имя попадает в ту точку входа, где оно на самом деле есть.

Почему. Раннер не делает tree-shaking файлов спек, а реэкспорт в ESM всегда загружает свой модуль. Поэтому каждый файл спеки — даже в Node-сервисе без DOM — исполнял 27 кБ кода про observer'ы, медиаэлементы и AbortController, чтобы получить createSpyFromClass. Единственное решение — убрать этот код из корня.

Замер на собранном пакете, отдельный процесс на каждый замер, медианы 40 парных прогонов:

изменение
файл спеки, который их не импортирует−0,159 мс
файл спеки, который импортирует /dom-stubs+0,155 мс
файл спеки, который импортирует /diagnostics+0,069 мс
dist−20,3 кБ JS, −3,8 кБ деклараций

Обе новые точки входа собраны одним файлом, поэтому импорт добавляет один модуль, а не четыре. При сборке чанками вторая строка показывала +0,62 мс. Если DOM-заглушки импортирует меньше половины ваших файлов спек, весь прогон становится быстрее.

3. Каждое правило ESLint — ошибка ​

Раньше configs.recommended ставил своим девятнадцати правилам error, warn или off. Теперь все они error. Исключение — правило, добавленное позже: prefer-render-shallow приходит как warn, потому что сообщает о цене, а не об ошибке. Почему — на странице плагина.

На warn сборка не останавливается. В репозитории, где вывод линтера никто не читает, warn работает как off. Какие находки блокируют мерж — решение вашего проекта, и это одна строка конфигурации. Поэтому по умолчанию всё строго, а как ослабить — в разделе Настройка под ваш проект.

У двух правил поменялась не только строгость:

  • До 4.0 prefer-native-spy-api было выключено (off). Теперь оно включено, потому что большинство проектов не находятся посреди переезда. Проект, который переезжает, выключает его одной строкой (ниже).
  • jasmine-namespace-without-entry больше не жалуется на spy.mock.calls[0]. Раньше оно отмечало это обычное для Vitest чтение на уровне warn, хотя голый spy.mock.calls проходил. Теперь всё, что находится под .mock, правило не трогает.

Если первый прогон красный, сначала посмотрите на эти три правила. Каждое может пожаловаться на правильный код:

js
import autoSpy from 'vitest-auto-spy/eslint-plugin';

export default [
  {
    files: ['**/*.spec.ts'],
    ...autoSpy.configs.recommended,
    rules: {
      ...autoSpy.configs.recommended.rules, // разверните спредом, иначе голый ключ `rules` заменит всю карту
      // слой jasmine ставится в setup-файле, который не импортирует ни одна спека: назовите этот файл
      'vitest-auto-spy/jasmine-namespace-without-entry': ['error', { setupModules: ['./test-setup'] }],
      // жалуется на рабочий переходный код: выключено, пока переезд с jasmine-auto-spies не закончен
      'vitest-auto-spy/prefer-native-spy-api': 'off',
      // опций нет; правило молчит там, где не может прочитать все провайдеры файла.
      // Выключайте его только в файлах, где провайдер регистрирует вызываемый вами хелпер.
      // 'vitest-auto-spy/no-unregistered-inject-spy': 'off',
    },
  },
];

В большом существующем проекте начните с того, что все правила — предупреждения, и чините партиями. Рецепт — в разделе Настройка под ваш проект.

Что ещё изменилось в 3.x ​

Если вы переходите с ранней 3.x, вас коснутся и эти изменения.

  • 3.1.0: gettersToSpyOn и settersToSpyOn принимают геттер, который возвращает Signal. Раньше такой геттер нельзя было назвать. Ошибка звучала как Type 'string' is not assignable to type 'never', и о сигналах в ней не было ни слова.
  • 3.5.0: если назвать одну половину пары get/set, спай ставится на обе. Так происходит, только когда класс объявляет обе. Раньше запись уходила в пустой сеттер и терялась, а accessorSpies.setters.x был undefined.
  • 3.5.0: observable-спай больше не переносит значения в следующий тест. nextWith(value) из одного теста раньше попадал в следующий тест прежде его throwWith(error). Код под тестом тогда шёл по ветке успеха со старыми данными. После error() или complete() следующие вызовы nextWith ничего не выпускали. Теперь resetAutoSpy сбрасывает и то и другое. Для спая, общего для нескольких тестов, вызывайте resetAutoSpy(spy) в beforeEach.
  • 3.5.0: expectEmission и expectEmissions выводят тип эмиссии. expectEmission(of(1)) — это Promise<number>. Раньше оба хелпера возвращали Promise<unknown>, и потеря типа всплывала, только когда спека читала поле результата.
  • 3.5.0: подмены из createAutoMock и mockDeep правильно отвечают на три операции.
    • Четыре хелпера mock*Prop работают на них, а restoreMockedProps() откатывает патч. Раньше патч молча игнорировался.
    • delete mock.optionalMethod убирает член, пока в него снова что-нибудь не запишут. Раньше следующее чтение создавало новый спай.
    • of(mock) выпускает подмену. Раньше rxjs принимал подмену за планировщик, и поток оставался пустым.
  • 3.5.0: asInstance принимает результат mockDeep. Раньше глубокий мок нельзя было передать туда, где ждут T.
  • 3.5.0: prefer-inject-spy читает форму в две строки. Раньше оно отмечало только vi.spyOn(TestBed.inject(X), 'm'), записанный в одну строку.
  • 3.5.0: provideAutoSpy принимает абстрактные классы-токены. abstract class LocalStorage с провайдером { provide: LocalStorage, useClass: BrowserLocalStorage } раньше не работал дважды: вызов без настроек давал пустой спай, а вызов с настройками не компилировался (TS2345: Cannot assign an abstract constructor type to a non-abstract constructor type). Теперь работают оба. Самописный { provide: LocalStorage, useValue: createAutoMock<LocalStorage>() } больше не нужен.
  • 3.5.0: оба хелпера-провайдера принимают returns, overrides и observablePropsToSpyOn. Раньше provideAutoSpyForToken принимал только значения свойств, а provideAutoSpy — только настройку методов, так что спай, которому нужно и то и другое, донастраивали в beforeEach.
  • 3.5.0: overrideProvider(X, provideAutoSpy(X)) работает. Старая документация называла это молчаливым no-op. overrideProvider читает useValue и игнорирует лишний provide, так что спай ставится. overrideAutoSpy по-прежнему понятнее.
  • 3.7.0: stable() даёт фикстуре 2000 мс, а затем бросает ошибку с причиной. Раньше фикстура, которая не успокаивалась, висела, пока Vitest не сообщал о таймауте файла в 5 с, где не были названы ни хелпер, ни фикстура.
  • 3.13.0: nextWithValues выпускает ложные значения. { value: false }, { value: 0 }, { value: '' }, { value: null } и ложный errorValue раньше не выпускали ничего. Поток булевых значений или счётчика молчал.
  • 3.13.0: спай метода принимает только те аргументы, что и настоящий метод. Раньше на спае read(key: string) компилировались и read(1), и read('ok', 'extra'), и read(). Теперь API мока — MockInstance, у которого нет своей сигнатуры вызова.
  • 3.7.0: документация больше не показывает httpResource() с flushEventLoopUntil. Этот пример никогда не работал. Хелпер прокручивает цикл событий и никогда не делает tick, поэтому ресурс не отправлял запрос, и ожидание заканчивалось таймаутом. Используйте settleResource() из vitest-auto-spy/angular.
  • Исправление документации (2026-08-29): vi.mock('@angular/core') работает под @angular/build:unit-test. Раньше документация говорила, что его нельзя замокать вовсе. Замер на 11 файлах спек в одном прогоне, на @angular/build 21.2.16 и 22.1.6: работает; настоящее правило уже.

Что не изменилось ​

  • Ни один хелпер не удалён, не переименован и не помечен устаревшим. Две группы переехали на новые пути импорта, и это всё.
  • Ни одно правило линтера не добавлено и не удалено. Кроме исправления .mock в jasmine-namespace-without-entry, ни одно не поменяло, на что жалуется. Поменялась только строгость.
  • nextWith, nextWithValues, nextOneTimeWith, throwWith, complete, observablePropsToSpyOn и цепочки calledWith работают как раньше.
  • rxjs остаётся необязательной peer-зависимостью. Её импортируют только vitest-auto-spy/rxjs и vitest-auto-spy/observer-spy — теперь и в типах, и в рантайме. Сборка падает, если rxjs импортирует любая другая точка входа.
  • Точки входа для Angular, NestJS, React, Vue, Svelte, Bun и node:test не изменились.

Если вы идёте издалека ​

  • Переход на 3.0 — одна строка в package.json: peer-диапазон vitest теперь совпадает с тем, что нужно типам.
  • Переход на 2.0 важен, если вы ещё на 1.x. methodsToSpyOn перестал убирать спаи: в одном перенесённом компоненте падающих тестов стало 4 вместо 147. Ленивые спаи методов сократили большой прогон с 257 мс / 425 МБ до 27 мс / 35 МБ.

Опубликовано под лицензией MIT.