Зарегистрируйтесь, чтобы продолжить обучение

Testing Library Dom Тестирование фронтенда

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

theme: gaia class:

  • lead
  • invert paginate: true ---

Dom Testing Library

Hexlet


Экосистема Testing Library

  • dom testing library — основная библиотека
  • user-event — имитация браузерных событий
  • jest-dom — кастомные матчеры Jest
  • eslint-plugin-testing-library — плагин ESLint для Testing Library
  • eslint-plugin-jest-dom — плагин ESLint для Jest DOM
  • Версии для фреймворков
    • react testing library
    • angular testing library
    • svelte testing library

Ключевой руководящий принцип

Чем лучше ваши тесты имитируют реальное использование вашего приложения, тем больше уверенности они могут вам дать


  • тестируйте UI компоненты с точки зрения пользователя
  • избегайте проверки деталей реализации
  • не полагайтесь на конкретный фреймворк для тестирования
  • не используйте test runner
  • DOM Testing Library работает с любым окружением, где есть DOM API
    • Jest, Mocha
    • JSDOM
    • реальный браузер

test('greeting', () => {
  const h1 = queryByText(/Hello/i)
  expect(h1).toHaveTextContent('Hello username!')
  expect(queryByText('Goodbye!')).not.toBeInTheDocument()
})

Запросы

  • get, getAll
  • query, queryAll
  • find, findAll

No Match1 Match1+ MatchAwait?
getBythrowreturnthrowNo
findBythrowreturnthrowYes
queryBynullreturnthrowNo
getAllBythrowarrayarrayNo
findAllBythrowarrayarrayYes
queryAllBy[]arrayarrayNo

ByRole

  • Селектор элементов по роли
  • getByRole, getAllByRole
  • queryByRole queryAllByRole
  • findByRole, findAllByRole
  • Доступные роли: link, button, form, heading, document, img, checkbox, radio, listitem, main, navigation, table, textbox
  • getByRole(expectedRole, { name: /submit/i })
  • getByRole('checkbox', { checked: true })
  • getAllByRole('button', { hidden: true })

test('show a required field warning', async () => {
  const loginButton = getByRole(container, 'button', { name: 'Login' })
  loginButton.click()

  expect(await findByText(container, 'User Name Required')).toBeVisible()
  expect(await findByText(container, 'Password Required')).toBeVisible()
})

ByText


<a href="/about">About ℹ️</a>

import { getByText } from '@testing-library/dom'
const aboutLinkNode = getByText(document.body, /about/i)

// ИЛИ
import { screen } from '@testing-library/dom'
const aboutLinkNode = screen.getByText(/about/i)

TextMatch

  • как строка
    • screen.getByText("Hello World") - поиск по полной строке
    • screen.getByText('llo worl', { exact: false }) — поиск по подстроке, игнорируется регистр
  • как регулярное выражение
    • screen.getByText(/world/i) — поиск по подстроке, игнорируется регистр
    • screen.getByText(/^hello world$/i) — поиск по полной строке, игнорируется регистр
  • как функция
    • screen.getByText((content, element) => content.startsWith("Hello"))

  • screen.getByLabelText('Username') ищет элемент с соответствющим label
  • getByLabelText хорошо подходит для полей формы
  • getByPlaceholderText ищет по атрибуту placeholder
  • getByTitle ищет по атрибуту title

<div id="app">
  <label for="username-input">Username</label>
  <input id="username-input" />
</div>
import { getByLabelText } from '@testing-library/dom'

const container = document.querySelector('#app')
const inputNode = getByLabelText(container, 'Username')

// ИЛИ
import { screen } from '@testing-library/dom'

const inputNode = screen.getByLabelText('Username')

ByTestId

<div data-testid="mailbox" />
const element = document.body.querySelector(`[data-testid="mailbox"]`)

// OR
import { screen } from '@testing-library/dom'
const element = screen.getByTestId('mailbox')
  • configure({testIdAttribute: 'data-my-test-attribute'})

Расширение для браузеров Chrome и Firefox Testing Playground


Вызов событий


fireEvent(node: HTMLElement, event: Event)


<button>Submit</button>
fireEvent(
  screen.getByText('Submit'),
  new MouseEvent('click', {
    bubbles: true,
    cancelable: true,
  }),
)

fireEvent[eventName](node: HTMLElement, eventProperties: Object)


fireEvent.change(getByPlaceholderText(/username/i), { target: { value: 'ruslan' } })

fireEvent.keyDown(getByRole('button'), { key: 'Enter', code: 'Enter' })

Асинхронность

const button = screen.getByRole('button', { name: 'Login' })
fireEvent.click(button)
await screen.findByText('Hello')

findBy = getBy + waitFor


waitFor

function waitFor<T>(
  callback: () => T | Promise<T>,
  options?: {
    container?: HTMLElement // document по умолчанию
    timeout?: number // 1000мс по умолчанию
    interval?: number // 50мс по умолчанию
    onTimeout?: (error: Error) => Error // ошибка и состояние контейнера по умолчанию
    mutationObserverOptions?: MutationObserverInit // вызов колбека при изменениях
  }
): Promise<T>

  • await waitFor(() => screen.getByRole('alert'))

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

  • WaitForElementToBeRemoved

  • wrapper on waitFor


const button = document.querySelector('button');

waitForElementToBeRemoved(document.querySelector('button')).then(() =>
  console.log('Element button no longer in DOM');
);

// изменения игнорируются
button.setAttribute('data-is-submit', true);

// логирует строку 'Element button no longer in DOM'
button.parentElement.removeChild(button);

  • fireEvent подходит для большинства сценариев, НО
  • fireEvent.click не порождает другие события:
    • fireEvent.mouseOver(element)
    • fireEvent.mouseMove(element)
    • fireEvent.mouseDown(element)
    • element.focus() (если элемент допускает это)
    • fireEvent.mouseUp(element)
    • fireEvent.click(element)

Рефакторинг

// Было
fireEvent.focus(getByText('focus me'))
// Стало
getByText('focus me').focus()

user-event

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


test('types inside textarea', () => {
  document.body.innerHTML = `<textarea />`

  userEvent.type(screen.getByRole('textbox'), 'Hello, World!')
  expect(screen.getByRole('textbox')).toHaveValue('Hello, World!')
})

test('click', () => {
  render(`
    <div>
      <label for="checkbox">Check</label>
      <input id="checkbox" type="checkbox" />
    </div>
  `)

  userEvent.click(screen.getByText('Check'))
  expect(screen.getByLabelText('Check')).toBeChecked()
})

user-event API

  • click / dbclick
  • hover / unhover
  • type
  • upload
  • selectOptions / deselectOptions
  • tab
  • paste

Click

click(element, eventInit, options)


Клики по элементу, могут иметь различные побочные эффекты в зависимости от элемента

<div>
  <label htmlFor="checkbox">Check</label>
  <input id="checkbox" type="checkbox" />
</div>
userEvent.click(screen.getByText('Check'))
expect(screen.getByLabelText('Check')).toBeChecked()

  • Клик вызовет сначала событие hover Его можно отключить установивtrue для параметра skipHover
  • Можно также вызывать клик с зажатыми клавишаи, ctrlClick / shiftClick / пр.
    • userEvent.click(elem, { ctrlKey: true, shiftKey: true })

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

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

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff