Тестирование
Помощники для тестов приложения, границы 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 = '' мало:
корень портала и хост живого региона кэшированы модулями, и после такой очистки
указывают на узлы вне документа. Следующий монтаж телепортирует туда
оверлеи, тест их не находит — и не находит без единой ошибки в консоли.
Жесты
import { drag, move, press, release } from '@feugene/granularity/testing'
press(handle, { clientY: 10 })
move({ clientY: 60 }) // в window, а не в элемент
release()Две причины не писать это руками:
- движение и отпускание слушает
window. Так устроен жест у слайдера, сплиттера, перестановки строк и колонок, смахивания тоста: нажатие приходит от разметки, дальше жест ведётся глобально — иначе он обрывался бы на выходе курсора за элемент. Тест, отправившийpointermoveв тот же элемент, не проверяет ничего и делает это молча; PointerEventв jsdom не реализован, иtrigger('pointerdown', { clientY })падает на попытке записать координату в незнакомый тип события.
cancelPointer() проверяет отдельную ветку: обрыв возвращает состояние, а
не дожимает жест.
Клавиатура и IME
import { composingKeydown, keydown } from '@feugene/granularity/testing'
keydown(input, 'Enter')
composingKeydown(input, 'Enter') // то же во время IME-композицииisComposing не переживает trigger('keydown', …): свойство только для чтения,
и присвоение молча теряется. Тест «Enter во время композиции ничего не
коммитит», написанный через trigger, проверяет обычный Enter — то есть
зеленеет на сломанном коде.
Флаг ставится вместе с 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 | Всё остальное тестовое: ./vue, ./a11y (axe в jsdom), ./e2e (клавиатурный обход в Playwright) |
Граница проходит по знанию о пакете, а не по слову «тест». Уедь granularityGlobal
и resetGranularityDom в тест-кит — он стал бы зависеть от ядра, а ядро зовёт
его фабрики: зависимость замкнулась бы в цикл.
Чего в jsdom не проверить
Ни перемещения фокуса по Tab, ни активации кнопки по Enter, ни
ResizeObserver. trigger('click') проверяет обработчик, но не то, что до него
вообще можно добраться с клавиатуры. Клавиатурный контракт поэтому проверяется в
живом браузере — и в самой библиотеке, и в вашем приложении это стоит делать
так же.