Тестирование

  • Интеграции
  • 5 мин
  • кода: 6

Помощники для тестов приложения, границы jsdom и правила, без которых тест зеленеет на сломанном коде.

Ревизия 2026-10-15

На этой странице

Раннер и способ монтирования — ваши: подпуть @feugene/granularity/testing не зависит ни от vitest, ни от @vue/test-utils и отдаёт только данные и действия над DOM. Обёрток над фейковыми таймерами и моками там нет намеренно — это API вашего инструмента, и пакет не должен ломаться от смены его мажора.

Окружение компонентов

ts
import { granularityGlobal } from '@feugene/granularity/testing'

mount(GrSelect, { props, global: granularityGlobal({ size: 'sm' }) })

mount(GrButton, {
  global: granularityGlobal({
    componentDefaults: { GrButton: { variant: 'outline' } },
    i18n: { 'gr.select.loading': 'Loading…' },
  }),
})

Помощник раздаёт ровно то, что GrConfigProvider кладёт в provide. Проверять этим сам провайдер нельзя — для этого его монтируют.

Уборка между тестами

ts
import { resetGranularityDom } from '@feugene/granularity/testing'

afterEach(resetGranularityDom)

Это не гигиена, а условие работы. Одного document.body.innerHTML = '' мало: очередь тостов — модульное состояние и по замыслу переживает размонтирование, так что GrToaster следующего теста покажет чужие тосты, а отложенные записи объявителя из прошлого теста продолжают тикать. resetGranularityDom гасит очередь тостов, снимает хост живого региона вместе с таймерами, убирает корень портала и только потом чистит body.

Жесты

ts
import { drag, move, press, release } from '@feugene/granularity/testing'

press(handle, { clientY: 10 })
move({ clientY: 60 }) // в window, а не в элемент
release()

drag(handle, { clientY: 10 }, { clientY: 60 }) // то же одной строкой

Две причины не писать это руками:

  • движение и отпускание слушает window. Так устроен жест у слайдера, сплиттера, перестановки строк и колонок, смахивания тоста: нажатие приходит от разметки, дальше жест ведётся глобально — иначе он обрывался бы на выходе курсора за элемент. Тест, отправивший pointermove в тот же элемент, не проверяет ничего и делает это молча;
  • trigger('pointerdown', { clientY }) падает: @vue/test-utils создаёт настоящий PointerEvent, а потом пытается присвоить ему clientY — свойство, у которого есть только геттер. Помощники собирают событие MouseEvent-ом с нужным type и отправляют его через dispatchEvent.

cancelPointer() проверяет отдельную ветку: обрыв возвращает состояние, а не дожимает жест.

Клавиатура и IME

ts
import { composingKeydown, keydown } from '@feugene/granularity/testing'

keydown(input, 'Enter')
composingKeydown(input, 'Enter') // то же во время IME-композиции

Тест «Enter во время композиции ничего не коммитит» проверяет предикат пакета, а у него два признака: isComposing и keyCode: 229. composingKeydown ставит оба и называет проверку по имени; через trigger('keydown', …) их приходится перечислять на каждом вызове, и забытый признак превращает тест в проверку половины предиката.

Флаг ставится вместе с keyCode: 229, потому что Safari до 16.4 не выставлял первый; пакет читает композицию по обоим признакам, и проверять половину предиката смысла нет.

Геометрия

ts
import { mockRect, stackRects } from '@feugene/granularity/testing'

mockRect(track, { width: 200 })
stackRects(rows, { size: 20 })

Раскладки в jsdom нет: getBoundingClientRect всегда нулевой. Всё, что считает попадание указателя или измеряет соседей — шкала слайдера, строки перестановки, ширины и смещения колонок, — на нулевом прямоугольнике не проверяется.

Живой регион

ts
import { announced } from '@feugene/granularity/testing'

expect(await announced()).toBe('13 August 2026')

Ждать обязательно: объявление пишется в регион отложенным макротаском, и это условие работы — регион, в котором текст появился в том же кадре, что и сам регион, часть скринридеров не объявляет вовсе.

Где проходит граница пакетов

ГдеЧто там
@feugene/granularity/testingТо, что ходит во внутренности ядра: конфиг, объявитель, корень портала
@feugene/granularity-test-kitВсё остальное тестовое: ./gates (контрактные гейты пакетов семейства), ./vue, ./a11y (axe в jsdom), ./e2e (обход клавиатурой и axe в Playwright)

Граница проходит по знанию о пакете, а не по слову «тест». Уедь granularityGlobal и resetGranularityDom в тест-кит — он стал бы зависеть от ядра, а ядро зовёт его фабрики: зависимость замкнулась бы в цикл.

Чего в jsdom не проверить

Ни перемещения фокуса по Tab, ни активации кнопки по Enter, ни ResizeObserver. trigger('click') проверяет обработчик, но не то, что до него вообще можно добраться с клавиатуры. Клавиатурный контракт поэтому проверяется в живом браузере — и в самой библиотеке, и в вашем приложении это стоит делать так же.