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

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

Раннер и способ монтирования — ваши: подпуть @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 = '' мало: корень портала и хост живого региона кэшированы модулями, и после такой очистки указывают на узлы вне документа. Следующий монтаж телепортирует туда оверлеи, тест их не находит — и не находит без единой ошибки в консоли.

Жесты

ts
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

ts
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 не выставлял первый; пакет читает композицию по обоим признакам, и проверять половину предиката смысла нет.

Геометрия

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Всё остальное тестовое: ./vue, ./a11y (axe в jsdom), ./e2e (клавиатурный обход в Playwright)

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

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

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

Последняя ревизия: 2026-09-01