React Junior
207 subscribers
37 photos
462 links
Изучение React с нуля
加入频道
Testing Library. Вступление

Незапланированно и стремительно перехожу к рассмотрению Testing Library и ее субпакетов.

Вот тут сайтик с документацией и примерами: https://testing-library.com/

Что это вообще за штука такая?

По сути это набор утилит для тестирования DOM, то есть нашего с вами фронтенда. Они работают с любым DOM-подобным ресурсом и помогают находить элементы и эмулировать события, как это мог бы делать пользователь.

Философия Testing Library:
«Чем больше тесты похожи на реальное использование вашего продукта, тем больше уверенности они дают».

Эта фраза преследует вас по всей доке. Она означает, что мы максимально отходим от реализации и работаем только с тем, что есть на странице - что видит пользователь. Тесты ничего не знают о вашем коде, а значит, они не сломаются, если код изменится.

Testing Library может работать с реальным DOM из браузера, а также с его эмуляциями (JSDOM, Jest).

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

Итак, Testing Library это большой набор хелперов для работы с DOM в стиле реального пользователя. Ее удобно использовать в тестах, но это не тест-раннер и не полноценный фреймворк для тестирования - просто хелпер, не привязанный к другим технологиям (только к DOM).

Утверждается, что мы можем использовать Testing Library для любых видов тестов, от юнитов до e2e (есть даже интеграция с cypress).

Итак, основной принцип: мы работаем с DOM, а не с инстансами компонентов.

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

Основная задача при работе с DOM - поиск нужных элементов. За это у нас отвечает пакет @testing-library/dom.

Что искать?

Testing Library придерживается философии, что на элементы нужно смотреть с точки зрения пользователя, а не программиста.

⁃ Пользователь ищет элемент не по классу или testid, а прежде всего по его роли: например, поле ввода или кнопка.
⁃ Элементы формы можно искать по тексту связанного с ними элементу label или по плейсхолдеру. В крайнем случае по значению, которое находится в поле в данный момент.
⁃ Неинтерактивные элементы (параграфы, дивы) можно искать по тексту.
⁃ Есть еще варианты искать по тексту атрибутов alt и title, но он уже менее желательный.
⁃ И наконец в конце списка идет поиск по testid.

Как искать?

Библиотека предоставляет целую кучу методов для поиска, разделенных на три группы - отличаются мелкими деталями.

⁃ getBy… и getAllBy… (например, `getByRole`)
⁃ queryBy… и queryAllBy…
⁃ findBy… и findAllBy…

Очевидно, что методы с префиксами getBy, queryBy и findBy ищут и возвращают один элемент. При этом если найдется несколько, то все они выбросят ошибку. Методы getAllBy, queryAllBy и findAllBy ищут все подходящие элементы и возвращают массив.

Первое отличие состоит в том, как этим методы реагируют на неудачу - если не нашлось ничего. Методы группы query не очень драматизируют по этому поводу и возвращают либо null, либо пустой массив. Остальные сразу кидают ошибку.

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

Пробуем

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

Рабочий пример: https://codesandbox.io/p/devbox/compassionate-mestorf-68kddy

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

Итак, в Testing Library есть куча методов для поиска элементов в DOM, но они все требуют передавать им первым аргументом HTML-контейнер.

Есть другой подход, более удобный. Библиотека экспортирует объект screen, в котором тоже есть все эти методы, но уже с предустановленным контейнером. Как нетрудно догадаться, в его роли выступает document.body.

Однако как будто нет способа установить свой собственный body для screen, он берется из глобального скоупа. То есть у нас должен быть глобальный document, но так как мы не работаем с браузером, его нет.
Можно попробовать установить его искусственно:


const dom = new JSDOM(‘’)

global.window = dom.window;
global.document = dom.window.document;


Но у меня что-то не получилось, все равно кидает ошибку, что document.body недоступно.

Поэтому мы возьмем вместо jsdom другой пакет, который сделает это за нас (возьмет jsdom и закинет его на глобальный уровень, как будто мы в браузере). Дальше можно будет работать как будто внутри браузера.

Пакет называется global-jsdom, а чтобы все завелось нужно подключить модуль ‘global-jsdom/register’.


require('global-jsdom/register')

// теперь можно взаимодействовать с DOM глобально
document.body.innerHTML = ''


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

Работающий пример тут: https://codesandbox.io/p/devbox/testing-library-global-jsdom-screen-react-junior-forked-8hrw8y

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

Есть прикольное расширение, которое помогает выбрать подходящий метод для поиска элемента на странице: https://chromewebstore.google.com/detail/testing-playground/hejbmebodbijjdhflfknehhcgaklhano
Устанавливаем его, в DevTools появляется новая вкладка Testing Playground.
Открываем ее, наводим курсор на нужный элемент, и расширение пытается подобрать самый подходящий метод с учетом всех best practices.

А еще есть песочница, где можно ввести любой кусок HTML и поиграться с запросами - https://testing-playground.com/

#тестирование #testinglibrary #документация
👍3
Forwarded from Cat in Web
React Reconciliation: как работает и зачем нам это знать

Статья (англ.): https://www.developerway.com/posts/reconciliation-in-react
Перевод: https://gist.github.com/zagazat/db926ec7ab69061934246a55b64913c3

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

Идея в том, чтобы найти оптимальный способ перерендеринга элементов в реальном DOM, чтобы улучшить производительность. React ищет все возможности, чтобы не создавать новые элементы, а переиспользовать уже существующие, просто внеся в них минимальные изменения.

В статье Надя Макаревич рассматривает некоторые неочевидные подводные камни такой оптимизации и способы их решения. Например, в некоторых ситуациях React переиспользует старый элемент (включая его стейт), даже если мы этого не хотим.

В качестве решения проблемы рассматриваются два подхода: с позицией элемента в массиве и с атрибутом key.

#react
👍3
byRole

Итак, начнем с подробного разбора самого главного запроса в Testing Library: byRole. Это методы getByRole, getAllByRole, queryByRole, queryAllByRole, findByRole, findAllByRole.

Документация: https://testing-library.com/docs/queries/byrole

Собственно роль

Первым параметром метод принимает собственно роль нужного элемента в виде строки. Это может быть button, heading, switch и так далее.

Речь идет не об атрибуте role, а прежде всего о дефолтной (встроенной) роли элементов. Например, элемент button имеет роль button, даже без явного указания на это. Вот тут можно почитать про роли элементов.

Опции

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

Тут будет много опций для поиска по aria- атрибутам.

🔸hidden: boolean

По умолчанию значение равно false - из-за этого в поиск не включаются "недоступные" элементы (display: none, aria-hidden, role=none).

🔸name: TextMatch

Для элемента формы - это текст лейбла, для кнопки - собственно текст кнопки. Также может использоваться значение атрибута aria-label.

Что за тип такой TextMatch? Это составной тип, который может быть обычной строкой, регулярным выражением или даже функцией.

Функция принимает два аргумента: content: string (собственно текстовый контент, по которому производится поиск) и element (DOMElement) и должна вернуть true или false.


screen.getByText((content, element) => content.startsWith('Hello'))


🔸description: TextMatch

Это для атрибута aria-describedby.

🔸selected: boolean

Отбор по значению атрибута aria-selected.

🔸busy: boolean

Отбор по значению атрибута aria-busy.

🔸checked: boolean

Отбор по значению атрибута aria-checked.

🔸pressed: boolean

Отбор по значению атрибута aria-pressed.

🔸suggest: boolean

Эта настройка для того, чтобы Testing Library предлагала вам запросы получше, чем тот, что написали вы.

🔸current: boolean | string

Отбор по значению атрибута aria-current.

🔸expanded: boolean

Отбор по значению атрибута aria-expanded.

🔸queryFallbacks: boolean

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

🔸level: number

Уровень заголовка для роли heading. Учитывает как семантику тега, так и атрибут aria-level.

🔸value

Это для группы атрибутов aria-value, например, aria-valuemin, aria-valuetext. Указывается в виде объекта:


screen.getByRole('spinbutton', { value: { min: 5, max: 10 }})


#тестирование #testinglibrary #документация
👍2
ByLabelText, ByPlaceholderText

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

ByLabelText

Поле ввода может быть связано с лейблом разными способами:

- через атрибуты for и id
- через атрибут aria-labelledby
- если поле находится внутри label
- лейбл можно указать в атрибуте aria-label

Сигнатура:


screen.getByLabelText(
text: TextMatch,
options: {
selector?: string = '*',
exact?: boolean = true,
normalizer?: NormalizeFn
}
)


Текст лейбла можно задать первым параметром в виде строки, регулярки или функции (TextMatch).

Вторым параметром идет объект настроект:
- selector - можно дополнительно указать селектор нужного элемента
- exact - если текст задан в виде строки, то этот параметр определяет должен ли поиск быть точным (по полной строке, с учетом регистра символов) или нет
- normalizer - по умолчанию Testing Library нормализует текст (убирает лишние пробелы). Можно передать собственный нормализатор

ByPlaceholderText

Очень похожий метод, только ищет элемент по тексту плейсхолдера. Менее предпочтительный, следует использовать только если элемент нельзя найти по лейблу или роли.

Сигнатура:


screen.getByPlaceholderText(
text: TextMatch,
options: {
exact?: boolean = true,
normalizer?: NormalizerFn
}
)


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

Еще один запрос для интерактивных элементов, у которых может быть value (input, textarea, select). Соответственно поиск происходит по текущему значению value - это текст, который отображается в инпуте. Настроек у запроса немного:


screen.getByDisplayValue(
value: TextMatch,
options?: {
exact?: boolean = true,
normalizer?: NormalizerFn,
}
)


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

Есть возможность искать элементы по текстовому контенту. Это сработает для всех элементов, у которых есть textContent, а также для инпутов с типом submit или button.

Настройки у запроса стандартные:


getByText(
text: TextMatch,
options?: {
selector?: string = '*',
exact?: boolean = true,
ignore?: string|boolean = 'script, style',
normalizer?: NormalizerFn,
})

Новая для нас настройка - ignore, она указывает, какие селекторы игнорировать при поиске.

#тестирование #testinglibrary #документация #примерыкода
👍1
ByAltText, ByTitle

Еще два запроса для поиска по значению атрибутов:

- alt - в основном для изображений
- title


screen.getByAltText(
text: TextMatch,
options?: {
exact?: boolean = true,
normalizer?: NormalizerFn,
})

screen.getByTitle(
title: TextMatch,
options?: {
exact?: boolean = true,
normalizer?: NormalizerFn,
})


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

И наконец последний, запасной запрос, который рекомендуется использовать только в самом крайнем случае, если ничего другое вам не подошло - поиск по атрибуту data-testid (название атрибута можно изменить в настройках).


screen.getByTestId(
text: TextMatch,
options?: {
exact?: boolean = true,
normalizer?: NormalizerFn,
})


#тестирование #testinglibrary #документация #примерыкода
👍1
Родители (parents) и владельцы (owners) в React

Статьи (англ.):
Поток данных https://julesblom.com/writing/parents-owners-data-flow
Производительность рендеринга https://julesblom.com/writing/parents-owners-performance

Статьи очень подробно и с интерактивными примерами рассказывают о двух способах построения дерева рендеринга в React (Parent Hierarchy и Owner Hierarchy). И о том, как понимание разницы между ними позволяет нам избежать prop drilling, а также улучшить производительность рендеринга без использования memo.

Пример


const App = () => {
const [user, setUser] = useState({ name: ‘John’ })

return <div>
<Dashboard user={user} />
</div>
}

const Dashboard = ({ user }) => {
return <div>
<WelcomeMessage user={user} />
</div>
}

const WelcomeMessage = ({ user }) => {
return <div>Hello, {user.name}</div>
}


Компонент App рендерит Dashboard, а тот в свою очередь рендерит WelcomeMessage. В компоненте App определяется стейт user, который используется в WelcomeMessage. Нам приходится пробрасывать его сквозь Dashboard, хотя самому дашборду он совсем не нужен.

Компоненты со слотами

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


const App = () => {
const [user, setUser] = useState({ name: ‘John’ })

return <div>
<Dashboard>
<WelcomeMessage user={user} />
</Dashboard>
</div>
}

const Dashboard = ({ children }) => {
return <div>{children}</div>
}


Тут Dashboard реализован как компонент «со слотом». Он рендерит кусок приложения, не имея представления о том, что в нем происходит. Это инверсия контроля, когда ответственность за этот фрагмент переходит к родителю Dashboard.

Благодаря такой структуре мы избегаем пробрасывания пропсов через незаинтересованные компоненты.

На этом примере легко понять разницу между родителем (непосредственная обертка) и владельцем (компонент, который контролирует рендеринг). Для WelcomeMessage родителем будет Dashboard, но владеет им App.

Производительность

Важно понимать, что ответственность за перерендеринг компонента извне несет его владелец, а не родитель.

Добавим в компонент Dashboard собственное состояние:


const Dashboard = ({ children }) => {
const [data, setData] = useState(‘hello’)

return <div>
{data}
{children}
</div>
}


Если изменится состояние внутри Dashboard, компонент WelcomeMessage не будет перерендерен, несмотря на то, что он находится внутри дашборда. (Помню, как меня это удивило в первый раз).

Живой пример можно посмотреть здесь: https://codesandbox.io/p/sandbox/parents-vs-owners-fjtr35. В консоли видно, какие компоненты обновляются при изменении состояния.

Мы можем использовать этот момент, чтобы избежать ненужных перерисовок.

Вот плохой пример:


const App = () => {
const [counter, setCounter] = useState(1)
const increase = () => {
setCounter(prev => prev + 1)
}

return <div>
{counter}
<button onClick={increase)>Increase</button>
<VeryExpensiveComponent />
</div>
}


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

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


const App = () => {
return <Counter>
<VeryExpensiveComponent />
</Counter>
}


Не злоупотребляем

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

#паттерны #примерыкода #ссылки
👍5🔥2
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