React Junior
208 subscribers
37 photos
462 links
Изучение React с нуля
Download Telegram
Testing Library. Вызов событий. fireEvent

В тестах часто требуется проверить как элементы реагируют на действия пользователя, например, что при клике на кнопку отправились данные формы.

Testing Library предоставляет несколько способов эмулировать такие события. Самый простой - метод fireEvent из пакета @testing-library/dom.

Документация нам говорит, что в большинстве случаев мы будем использовать другой пакет - @testing-library/user-event, но пока посмотрим на этот.

Метод fireEvent принимает первым аргументом элемент, на котором необходимо вызвать событие. Вторым - объект события.


fireEvent(input, new MouseEvent('click'))


Также есть несколько готовых методов для конкретных событий, которым можно передать объект с настройками:


fireEvent.change(input, { target: { value: 'hello' } })
fireEvent.keyDown(domNode, {key: 'Enter', code: 'Enter', charCode: 13})


Дока тут: https://testing-library.com/docs/dom-testing-library/api-events

Это особенно полезно для сложных событий ввода, например, для инпута с файлами, которому мы не можем вручную установить значение свойства files.

Пример: https://codesandbox.io/p/devbox/testing-library-global-jsdom-screen-react-junior-forked-7t9cmt

#тестирование #testinglibrary #документация #примерыкода
👍2
Testing Library. Асинхронщина. waitFor

В ряде случаев для тестирования нам нужно подождать, когда на странице что-то произойдет: появится/пропадет конкретный элемент, закончится какой-то таймер, юзер что-то кликнет и так далее. Библиотека предоставляет несколько способов для такого ожидания.

waitFor

Основная механика ожидания - это метод waitFor. Первым аргументом он принимает коллбэк, который собственно и должен проверить, произошло ли то, что нужно. Если произошло, нужно вернуть true, если нет - выбросить исключение. В этом случае коллбэк будет вызван для проверки снова спустя некоторое время, и так до тех пор, пока он не будет удовлетворен.

Мы можем даже использовать expect для проверки условий, так как при несоответствии он как раз выбрасывает ошибку:


await waitFor(() => expect(mockAPI).toHaveBeenCalledTimes(1))


Вместо коллбэка-функции можно передать промис, он не будет вызываться повторно.

Вторым аргументом можно передать объект с настройками:

- container: HTMLElement - по умолчанию document
Если мы ждем появления элемента внутри конкретного контейра, то можно передать его
- timeout - время ожидания
- interval - как часто вызывать коллбэк
- onTimeout: (error: Error) => Error - по умолчанию добавляет к ошибке текущее состояние элемента container
- mutationObsereverOptions - для настройки вызова коллбэка при изменениях контейнера

#тестирование #testinglibrary #документация #примерыкода
👍2👏1
Testing Library. Асинхронщина. findBy

Мы помним, что у нас есть три вида запросов - getBy, queryBy и findBy. Так вот findBy - это комбинация getBy и уже рассмотренного выше метода waitFor. Таким образом, запрос findBy по умолчанию ожидает, когда искомый элемент появится на странице.

Можно использовать в комбинации с await:


await screen.findByText('Clicked once')


#тестирование #testinglibrary #документация #примерыкода
👍1
Testing Library. Асинхронщина. waitForElementToBeRemoved

Есть еще одна утилита, позволяющая дождаться, когда конкретный элемент исчезнет со страницы: waitForElementToBeRemoved.

Первым параметром она принимает элемент или массив элементов. Кроме того, можно передать функцию-коллбэк, которая вернет элемент или массив элементов. Если элемент null или массив пустой, будет ошибка.


const el = document.querySelector('div.getOuttaHere')
await waitForElementToBeRemoved(el)


Вторым параметром можно передать объект с настройками, такой же как у функции waitFor.

#тестирование #testinglibrary #документация #примерыкода
👍1
Testing Library. Размышления о fireEvent

Хорошая статья в документации про особенности эмуляции событий: https://testing-library.com/docs/guide-events

Помня о главном принципе библиотеки, что "тестировать нужно максимально похоже на то, как действует реальный пользователь", мы тем не менее должны помнить о некоторых условностях, особенно в работе с событиями.

Например, если мы вызываем метод fireEvent.click(element), он задиспатчит событие клика и сработает обработчик клика, если он есть. В большинстве случаев нам этого более чем достаточно. Однако когда настоящий юзер кликает на настоящий элемент, мы получаем гораздо больше событий: mouseOver, mouseMove, mouseDown, focus (если элемент focusable), mouseUp и только теперь, наконец, click.

Про это важно помнить, чтобы не столкнуться однажды с неожиданным и непонятным поведением.

Статья предлагает пару паттернов для эмуляции событий с помощью fireEvent. Например, вместо прямого вызова события keyDown на элементе, лучше сначала сфокусироваться на нем, а затем вызвать событие на document.activeElement:


getByText('click me')
fireEvent.keyDown(document.activeElement || document.body)


#тестирование #testinglibrary #документация #примерыкода
👍2
Testing Library. Тестирование доступности

Если возникла необходимость затестить доступность вашего приложения, Testing Library предлагает пару полезных утилит:

getRoles

Находит на странице все элементы, имеющие роли, и возвращает их в виде объекта

logRoles

То же самое, только выводит данные в консоль.

isInaccessible

Проверяет, исключен ли элемент из дерева доступности браузера.

Подробнее в документации: https://testing-library.com/docs/dom-testing-library/api-accessibility

#тестирование #testinglibrary #документация
👍1
Testing Library. Debugging

Библиотека предоставляет ряд возможностей для отладки

- Если запросы get или find не находят элементов, то они выбрасывают ошибку, а в консоль выводится DOM корневого элемента (screen или container)
- Утилита prettyDOM принимает элемент и возвращает его структуру
- Метод screen.debug() или screen.debug(element) также получает DOM элемента и выводит его в консоль
- Метод screen.logTestingPlaygroundURL() выводит урл песочницы, в которой уже будет сохранен ваш документ
- Утилита logRoles выводит все элементы, имеющие роли

Подробнее в документации: https://testing-library.com/docs/dom-testing-library/api-debugging

#тестирование #testinglibrary #документация
👍1
Testing Library. within и кастомные запросы

Библиотека также предоставляет ряд низкоуровневых функций для построения более точных/сложных запросов: https://testing-library.com/docs/dom-testing-library/api-custom-queries

И еще есть полезная функция within(element). Она оборачивает полученный элемент и возвращает объект со всеми уже известными нам методами (как у объекта screen). И все эти методы работают "в контексте" полученного элемента: https://testing-library.com/docs/dom-testing-library/api-within

#тестирование #testinglibrary #документация
👍3
Forwarded from Cat in Web
Валидация форм с react-hook-form в React-приложениях

Видео (рус.): https://youtu.be/Jxfun6Jnt5Q

Быстрое и понятное введение в работу с библиотекой react-hook-form. Достаточно подробно описано, как настроить валидацию для формы с неконтролируемыми полями.

#видео #react #forms
👍3
user-event

user-event - это библиотека для симуляции пользовательских событий, которую мы можем использовать вместо fireEvent. Можно импортировать из пакета @testing-library/user-event.

В чем разница между user-event и fireEvent?
fireEvent просто диспатчит события DOM, это лишь удобная обертка над методом dispatchEvent.
А user-event пытается имитировать полноценное взаимодействие, которое обычно состоит из нескольких событий.

Простой пример - изменение значения поля ввода.
fireEvent просто задиспатчит событие ‘change’ с нужной строкой, хотя в реальной жизни поле сначала получит фокус, а затем будет ряд событий клавиатуры. user-event пытается все это имитировать, а заодно делает кучу проверок: например, не получится ввести текст в заблокированный инпут.

Настоящий пользователь в браузере вызывает trusted-события, которые нельзя создать программно, а user-event подменяет UI-слой, с которым взаимодействует пользователь в браузере, чтобы это имитировать.

Настройка

Прежде чем использовать методы из этой библиотеки в наших тестах, ее нужно настроить с помощью метода setup. Документация рекомендует делать это до того, как вы отрендерили DOM:



const user = userEvent.setup() // инициализация
render(…)
await user.click(screen.getByRole('button', {name: /click me!/i}))


В метод можно передать ряд настроек: https://testing-library.com/docs/user-event/options

В результате мы получаем объект user с кучей полезных методов для создания интерактивности.

До
ступные API и методы

👉 type

Для ввода значения в поле ввода.
Первым аргументом принимает само поле, вторым - текст, третим - объект с настройками.

user.type(input, ' World!')


👉 clear

Очистка editable-элемента (фокус, выделение контента, удаление)

user.clear(screen.getByRole('textbox'))


👉 selectOptions/deselectOptions

Работа с селектами или listbox.


user.selectOptions(screen.getByRole('listbox'), ['1', 'C'])


👉 upload

Для загрузки файлов через input[type=«file»]


user.upload(input, new File(['hello'], 'hello.png', {type: 'image/png'}))


👉 clipboard: copy, cut, paste

Работа с буфером обмена. Библиотека подменяет реальный window.navigator.clipboard собственным стабом.
Дока: https://testing-library.com/docs/user-event/clipboard

👉 keyboard

Для симуляции событий клавиатуры, ввода текста. Символы можно указать просто текстом, а также по значениею KeyboardEvent.key или KeyboardEvent.code
Дока: https://testing-library.com/docs/user-event/keyboard

У этого метода также есть шорткат tab (https://testing-library.com/docs/user-event/convenience)


👉 pointer

Работа указателем. Некоторые возможности этого метода не очень актуальны, если вы работаете с jsdom, так как в этом случае реальная отрисовка страницы не происходит.
Дока: https://testing-library.com/docs/user-event/pointer

Это сложный метод, у которого есть много шорткатов (подробнее тут https://testing-library.com/docs/user-event/convenience):

⁃ click
⁃ dblClick
⁃ tripleClick
⁃ hover
⁃ unhover

#тестирование #testinglibrary #документация
👍2
React Testing Library. Первая проба

Наконец-то то, ради чего мы тут и собрались в общем-то. Как использовать Testing Library в React-проектах?

Очень просто, они нам предоставляют обертку @testing-library/react, которая реализует методы для удобной работы с компонентами React.

👀 Тестируем компонент Button вживую: https://codesandbox.io/p/devbox/react-testing-library-react-junior-2yqhll

Сложнее всего тут - настроить окружение, а сами тесты пишутся очень просто и логично.

Итак, нам понадобится:

👉 @testing-library/react - это обертка над @testing-library/dom, поэтому dom нам не нужен.
👉 @testing-library/jest-dom - для добавления полезных утверждений
👉 @testing-library/user-event - для эмуляции пользовательских событий
👉 а также компонент, который нужно протестировать - Button

Тесты в примере очень простые, чтобы разобраться, как это дело заводится.

Первым тестом мы проверяем, что наш компонент в принципе рендерится. Для этого используем метод render из @testing-library/react. Он возвращает объект с уже знакомыми нам методами типа getByRole, findByText и так далее. Соответственно все они работают именно с тем фрагментом, который мы отрендерили.
Находим нашу кнопку по ее тексту (можно и по роли). И утверждаем, что она есть в документе (`expect(button).toBeInTheDocument`).

Второй тест чуть сложнее, он проверяет, что при клике на кнопку вызывает функция-обработчик. Для этого мы создаем поддельную функцию (`jest.fn()`) и передаем ее в компонент в пропе onClick. Опять рендерим, находим кнопку и кликаем на нее, используя метод click из библиотеки user-event (хотя тут можно и нативным кликом обойтись). Наконец, утверждаем, что наша мокнутая функция-обработчик была вызвана (`expect(onClick).toHaveBeenCalled()`).

Все логично, методы удобные, ничего больше не мешает писать тесты))

#тестирование #testinglibrary #документация #примерыкода
👍3
React Testing Library. Пример с сетевыми запросами

Еще один пример тестирования с использованием React Testing Library: https://codesandbox.io/p/sandbox/react-testing-library-fetch-react-junior-vc465v

У нас есть компонент с кнопкой, при клике на которую из публичного api загружается и отображается картинка, то есть уходит сетевой запрос.

Нам нужно это протестировать: что при клике загружается и отображается картинка.

Алгоритм такой:

⁃ рендерим компонент
⁃ находим кнопку
⁃ кликаем
⁃ ждем, когда на странице появится картинка
⁃ проверяем ее адрес

Но для этого нам нужно перехватить и подменить запрос. Для этого существуют специальные утилиты, типа msw или nock. Они очень простые и отлично работают. Однако у меня не получилось завести их на демонстрационной площадке, поэтому пришлось использовать корявую подмену метода fetch.

В тесте стоит обратить внимание на две вещи:

👉 мы ждем, когда изображение появится на странице, используя await и find-запрос, который возвращает промис
👉 для утверждения используем метод toHaveAttribute, добавленный библиотекой @testing-library/jest-dom

#тестирование #testinglibrary #документация #примерыкода
👍2
React Testing Library. Кастомный рендер

Мы можем настроить функцию render, передав вторым аргументом объект конфигурации. Например, можно добавить какую-то общую обертку с провайдерми для наших рендеров - параметр wrapper.

#тестирование #testinglibrary #документация #примерыкода
👍2
Render Hook

Помимо render у нас есть еще один метод - renderHook. Как можно догадаться, он предназначен для работы с хуками.


import {renderHook} from '@testing-library/react'

test('returns logged in user', () => {
const {result, rerender} = renderHook((props = {}) => props, {
initialProps: {name: 'Alice'},
})
expect(result.current).toEqual({name: 'Alice'})
rerender()
expect(result.current).toEqual({name: undefined})
})


Метод принимает два аргумента:
- рендер-функцию, которая должна вызвать хук и вернуть результат его работы
- объект конфигурации (опционально)

Результат работы хукуа будет доступен в поле result.current.

#тестирование #testinglibrary #документация #примерыкода
👍2
Руководство по React Testing Library

Статья (англ.): https://www.robinwieruch.de/react-testing-library/

Robin Wieruch по полочкам разложил, что такое RTL и как ей пользоваться.

1. RTL сама по себе не предназначена для организации тестов, поэтому нам в любом случае понадобится один тест-раннеров - Jest или Vitest. С их помощью напишем обертки тестов, а для конкретных действий уже будем использовать RTL.

2. В начале теста рендерим необходимые React-компоненты и сразу же перестаем о них думать как о React-компонентах. Мы должны тестировать обычный DOM, который видит пользователь.

3. RTL предоставляет огромное количество методов для поиска нужных элементов в отрендеренном фрагменте. Все они довольно подробно разобраны, кроме того, описано, когда и что предпочтительно использовать.

Запросы в testing-library

4. Пакет @testing-library/jest-dom добавляет множество удобных утверждений, вроде toBeInTheDocument.

5. Пользовательские события можно эмулировать с помощью fireEvent и user-event.

fireEvent
user-event

6. Разбираемся, как тестировать обработчики событий с помощью мокнутых функций.

7. И наконец учимся работать с асинхронными изменениями, например, сетевыми запросами, с помощью find-методов и waitFor. Не забываем мокать собственно методы, отправляющие запрос (axios/fetch/etc)

waitFor
пример тестирования компонента, выполняющего сетевой запрос


#ссылки #тестирование #testinglibrary
👍2
Testing Library. Полезные ресурсы

- документация Testing Library
- все существующие ARIA-роли для метода getByRole
- список утверждений, которые добавляются библиотекой @testing-library/jest-dom

#ссылки #тестирование #testinglibrary
👍2
Простое руководство по тестированию взаимодействия с пользователем с помощью библиотеки тестирования React

Статья (рус.): https://reddeveloper.ru/blog/363/prostoye-rukovodstvo-po-testirovaniyu-vzaimodeistviya-s-pol-zovatelem-s-pomoshch-yu-biblioteki-testirovaniya-react

Из этой статьи мы возьмем несколько примеров того, что в принципе можно тестировать, так как у меня по-прежнему есть сомнения и вопросы в этой области.

1. Поле ввода работают правильно

Имеем селект. Тестируем, что он работает правильно и выбранное пользователем значение действительно выбирается.

Полезные методы:
- getByRole('combobox')
- userEvent.selectOptiions
- expect(select).toHaveValue

2. При вводе/выборе значения оно корректно отображается в интерфейсе

Имеем селект города. Выводим выбранный город где-то в интерфейсе Тестируем, что при выборе города в селекте он изменяется и в интерфейсе.

Полезные методы:
- поиск элемента по тексту getByText
- поиск элемента другими методами, но с уточнением его текста (в настройках метода), например, getByRole('heading', { name: /moscow/i })

3. При ховере появляется элемент

Имеем тултип, который должен появляться при наведении на элемент. Тестируем, что он появляется.

Полезные методы:
- userEvent.hover
- expect(tooltip).toBeInTheDocument

4. Загрузка файлов

Проверяем, что загрузчик файлов видит загруженные файлы.

Полезные методы:
- new File(['hello'], 'hello.png', {type: 'image/png'})
- userEvent.upload(filePicker, file)
- expect(filePicker.files[0]).toEqual(file)

5. Отправка формы

Имеем форму. Тестируем, что при правильном заполнении полей форма успешно отправляется.

Также тестируем все варианты валидации:
- пустые поля
- некорректно заполненные поля

Полезные методы:
- userEvent.type(field, text)
- userEvent.selectOptions(select, option)
- userEvent.click(element)
- getByPlaceholderText(text)
- getByLabelText(text)

#ссылки #тестирование #testinglibrary
👍2
Разбираемся в React Concurrency

Статья (англ.): https://www.bbss.dev/posts/react-concurrency/

Конкурентный режим в React - фича уже не новая, но наверно еще не очень широко используемая. В статье дается хороший разбор.

Основная идея в том, что React научился прерывать рендеринг. Если у нас рендериттся что-то большое и тяжелое, пользователь не может взаимодействовать со страницей. Конкурентный режим как раз должен решать эту проблему.

Как именно эта фича реализована под капотом, в статье подробно не рассказывается. Упомянуто только, что в этом задействован пакет scheduler, а также, что используется requestAnimationFrame.

В общем, "конкурентного режима" как такового у нас нет, потому что его введение многое поломает. Вместо этого нам дали несколько "конкурентных фич", которые дают возможность избирательно запустить конкуррентный рендеринг.

useTransition

Хук useTransition возвращает метод startTransition. Он запускает transition, внутри которого все изменения будут считаться прерываемыми.

Если мы изменим стейт компонента внутри такого transition, изменения начнут применяться, но могут быть прерваны, если найдется более важное дело - например, действия пользователя.

Очевидно, что этот хук нам нужен только если мы контролируем изменения (сами их запускаем). Но как быть, если у нас нет возможности обернуть изменения в transition? Например, если мы получаем некоторое изменяющееся значение через проп.

useDeferredValue

Для этого случая есть хук useDeferredValue, который оборачивает это изменяющееся значение, но об изменениях сообщает нам только тогда, когда это ничему не мешает.

***

Таким образом, новые фичи позволяют нам явно помечать изменения как "неблокирующие". Также они работают с Suspense-компонентами.

#ссылки #concurrentmode
👍5
7 бесплатных шаблонов React для разработки проектов

Статья (рус.): https://nuancesprog.ru/p/15488/

Полноценные заготовки для проектов - чтобы не начинать работу с нуля. Есть шаблоны для лендингов, портфолио, блога, админ-панели...

#ссылки #инструменты
👍4👏2
useEffect vs useLayoutEffect

Статья (англ.): https://kentcdodds.com/blog/useeffect-vs-uselayouteffect

Маленькое напоминание о разнице между двумя хуками.

useEffect срабатывает асинхронно после рендера компонента - пользователь уже увидел обновленный интерфейс, и только потом исполняется ваш код.

useLayoutEffect срабатывает синхронно. Новый DOM уже подготовлен, но браузер еще не перерисовал страницу для пользователя. В этот момент исполняется ваш код.

Обычно нам нужен useEffect, так как он неблокирующий и более эффективный, а у useLayoutEffect есть вполне конкретные кейсы:

- снять метрики (позиция скролла)
- обновить DOM до перерисовки так, чтобы пользователь не увидел мерцания
- установить какие-то актуальные значения, чтобы весь последующий код из useEffect мог их использовать

#ссылки #жизненныйциклкомпонента #хуки
👍51