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

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

- Page: https://granularity.tech/ru/docs/testing
- Last reviewed: 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)
```

:::callout{type="warning"}
Это не гигиена, а условие работы. Одного `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-композиции
```

:::callout{type="warning"}
Тест «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')` проверяет обработчик, но не то, что до него
вообще можно добраться с клавиатуры. Клавиатурный контракт поэтому проверяется в
живом браузере — и в самой библиотеке, и в вашем приложении это стоит делать
так же.
