Компонент Signup - это форма регистрации: поле email, кнопка создать аккаунт и сообщение об ошибке, если email неверный. Компонент под тестом - это отрисованный кусок интерфейса. Проверить его можно двумя взглядами: изнутри, как программист, знающий про useState и пропсы, или снаружи, как пользователь, который видит поле и жмёт кнопку. Выбор взгляда определяет, переживут ли тесты рефакторинг.
Наивный ход - лезть внутрь. Достать состояние компонента, проверить, что переменная isValid стала false, найти узел по className и убедиться, что появился div.error. Это естественно: данные под рукой. Кажется, что вы тестируете именно то, что написали.
Ломается это на первом же рефакторинге. Переименовали класс, заменили useState на useReducer, вынесли ошибку в другой компонент - поведение для пользователя не изменилось, а тесты покраснели. Бывает и обратное, опаснее: тест проверяет className и проходит, хотя кнопка на самом деле не кликается, а сообщение об ошибке невидимо для скринридера. Тест зелёный, интерфейс сломан.
Testing Library разворачивает взгляд. Её идея: чем больше тест похож на то, как софтом реально пользуются, тем больше доверия он даёт. Поэтому вы не заглядываете в состояние, а работаете с тем, что доступно пользователю - с ролями элементов и подписями. Тест перестаёт знать про внутренности и начинает описывать поведение: ввёл неверный email, нажал кнопку, увидел ошибку.
Для этого нужна среда и набор инструментов. jsdom - это браузерный DOM на чистом Node, чтобы компонент было где отрисовать без браузера; его даёт среда jest-environment-jsdom. @testing-library/react даёт render, который монтирует компонент. @testing-library/jest-dom добавляет матчеры вроде toHaveTextContent и toBeVisible. user-event имитирует действия пользователя. Матчеры подключают один раз в setup-файле.
Главный запрос - getByRole: он находит элемент по его роли в дереве доступности, а опция name отбирает по доступному имени, например getByRole('button', { name: /создать аккаунт/i }). Для полей формы удобен getByLabelText: пользователь находит поле по подписи. Это ещё и проверка доступности - если запрос по роли или подписи не находит элемент, то и скринридер его не увидит.
Действия выполняет userEvent. Сначала userEvent.setup создаёт сессию пользователя, затем await user.type вводит текст, а await user.click нажимает кнопку - и то и другое асинхронно, потому что за одним действием стоит цепочка событий фокуса и клавиатуры. Появление ошибки после отправки ловят через findByRole('alert'): этот запрос возвращает промис и повторяет попытки, пока узел не появится.
Отсюда растёт и предупреждение act. Jest ругается not wrapped in act(...), когда состояние React обновляется вне его ведома - обычно это признак, что вы не дождались асинхронного действия или запроса. Хорошая новость: userEvent и findBy уже оборачивают обновления в act сами, так что честный await убирает предупреждение. Глушить его случайной обёрткой act не нужно - это лечение симптома, а не причины.
Цена подхода - дисциплина в разметке: чтобы тест находил элементы по роли и подписи, интерфейс должен быть доступным. Но это не налог, а бонус - тот же код становится доступнее для реальных людей. А выигрыш виден в провальном сценарии из начала: тест по className промолчал бы о сломанной кнопке, тогда как тест глазами пользователя падает ровно тогда, когда пользователь не может нажать кнопку.
npm i -D jest-environment-jsdom @testing-library/react \
@testing-library/jest-dom @testing-library/user-event
// test/setup.ts
import '@testing-library/jest-dom'test('показывает ошибку при неверном email', async () => {
const user = userEvent.setup()
render(<Signup />)
await user.type(screen.getByLabelText(/email/i), 'wrong')
await user.click(screen.getByRole('button', { name: /создать аккаунт/i }))
expect(await screen.findByRole('alert')).toHaveTextContent(
'Введите корректный email',
)
})