Тестирование
Помощники для тестов приложения, границы jsdom и правила, без которых тест зеленеет на сломанном коде.
Раннер и способ монтирования — ваши: подпуть @feugene/granularity/testing не
зависит ни от vitest, ни от @vue/test-utils и отдаёт только данные и
действия над DOM. Обёрток над фейковыми таймерами и моками там нет намеренно —
это API вашего инструмента, и пакет не должен ломаться от смены его мажора.
Окружение компонентов
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. Проверять
этим сам провайдер нельзя — для этого его монтируют.
Уборка между тестами
import { resetGranularityDom } from '@feugene/granularity/testing'
afterEach(resetGranularityDom)Это не гигиена, а условие работы. Одного document.body.innerHTML = '' мало:
очередь тостов — модульное состояние и по замыслу переживает размонтирование,
так что GrToaster следующего теста покажет чужие тосты, а отложенные записи
объявителя из прошлого теста продолжают тикать. resetGranularityDom гасит
очередь тостов, снимает хост живого региона вместе с таймерами, убирает корень
портала и только потом чистит body.
Жесты
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
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 не выставлял
первый; пакет читает композицию по обоим признакам, и проверять половину
предиката смысла нет.
Геометрия
import { mockRect, stackRects } from '@feugene/granularity/testing'
mockRect(track, { width: 200 })
stackRects(rows, { size: 20 })Раскладки в jsdom нет: getBoundingClientRect всегда нулевой. Всё, что считает
попадание указателя или измеряет соседей — шкала слайдера, строки перестановки,
ширины и смещения колонок, — на нулевом прямоугольнике не проверяется.
Живой регион
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') проверяет обработчик, но не то, что до него
вообще можно добраться с клавиатуры. Клавиатурный контракт поэтому проверяется в
живом браузере — и в самой библиотеке, и в вашем приложении это стоит делать
так же.