Динамическое добавление и удаление полей

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


Поиск и взаимодействие с динамическими элементами

React Testing Library (RTL) ориентирована на работу с пользовательским интерфейсом, а не с внутренними реализациями компонента. Поэтому все действия строятся вокруг query, get и find функций. Для динамических полей важно:

  1. Использовать getBy* для элементов, которые обязательно должны существовать на момент рендера.
  2. Использовать findBy* для элементов, появление которых асинхронное или зависит от действий пользователя.
  3. Использовать queryBy* для проверки отсутствия элементов после удаления.

Пример поиска динамического поля:

const input = screen.getByPlaceholderText(/Введите имя/i);

Если поле появляется после клика:

const addButton = screen.getByText(/Добавить поле/i);
fireEvent.click(addButton);

const newInput = await screen.findByPlaceholderText(/Новое поле/i);

Добавление полей

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

Пример теста для добавления поля:

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import DynamicForm from './DynamicForm';

test('добавляет новое поле после клика', async () => {
  render(<DynamicForm />);

  const addButton = screen.getByText(/Добавить поле/i);
  await userEvent.click(addButton);

  const newInput = await screen.findByPlaceholderText(/Новое поле/i);
  expect(newInput).toBeInTheDocument();
});

Ключевой момент: findBy* возвращает промис и автоматически ожидает появления элемента, что идеально для асинхронных изменений DOM.


Удаление полей

Удаление полей тестируется через имитацию клика по кнопке удаления, связанной с конкретным элементом. После этого проверяется отсутствие элемента в DOM с помощью queryBy*:

test('удаляет поле при клике на кнопку удаления', async () => {
  render(<DynamicForm />);

  const addButton = screen.getByText(/Добавить поле/i);
  await userEvent.click(addButton);

  const newInput = await screen.findByPlaceholderText(/Новое поле/i);
  const removeButton = screen.getByRole('button', { name: /Удалить/i });

  await userEvent.click(removeButton);

  expect(screen.queryByPlaceholderText(/Новое поле/i)).not.toBeInTheDocument();
});

queryBy* возвращает null, если элемент отсутствует, что идеально для проверки удаления.


Проверка количества динамических элементов

Иногда важно убедиться, что количество полей соответствует ожидаемому после серии действий:

test('добавляет и удаляет несколько полей', async () => {
  render(<DynamicForm />);

  const addButton = screen.getByText(/Добавить поле/i);
  await userEvent.click(addButton);
  await userEvent.click(addButton);

  let inputs = screen.getAllByPlaceholderText(/Новое поле/i);
  expect(inputs).toHaveLength(2);

  const removeButtons = screen.getAllByRole('button', { name: /Удалить/i });
  await userEvent.click(removeButtons[0]);

  inputs = screen.getAllByPlaceholderText(/Новое поле/i);
  expect(inputs).toHaveLength(1);
});

Использование getAllBy* позволяет работать с множественными элементами сразу и проверять их количество.


Асинхронные сценарии

Динамическое добавление может происходить через API или асинхронные операции. Для таких случаев RTL предоставляет ожидания через findBy* и waitFor:

import { waitFor } from '@testing-library/react';

test('добавляет поле после асинхронного запроса', async () => {
  render(<DynamicForm />);

  const addButton = screen.getByText(/Добавить поле/i);
  await userEvent.click(addButton);

  await waitFor(() => {
    expect(screen.getByPlaceholderText(/Новое поле/i)).toBeInTheDocument();
  });
});

waitFor позволяет тесту корректно подождать изменения DOM, даже если они происходят с задержкой.


Важные практики

  • Тестировать поведение, а не реализацию: проверять наличие и взаимодействие с элементами, а не внутреннее состояние компонента.
  • Использовать роли и тексты: getByRole, getByText, getByPlaceholderText делают тесты более стабильными.
  • Разделять добавление и удаление: отдельные тесты для добавления, удаления и проверки количества полей делают тесты читаемыми и надежными.
  • Асинхронное ожидание: для динамических элементов всегда использовать findBy* или waitFor.

Интеграция с формами и submit

Для полноценного тестирования формы важно проверять не только динамическое изменение полей, но и успешную отправку данных:

test('отправка формы с динамическими полями', async () => {
  const handleSubmit = jest.fn();
  render(<DynamicForm onSub mit={handleSubmit} />);

  const addButton = screen.getByText(/Добавить поле/i);
  await userEvent.click(addButton);

  const input = await screen.findByPlaceholderText(/Новое поле/i);
  await userEvent.type(input, 'Тестовое значение');

  const submitButton = screen.getByText(/Отправить/i);
  await userEvent.click(submitButton);

  expect(handleSubmit).toHaveBeenCalledWith([{ value: 'Тестовое значение' }]);
});

Это гарантирует, что динамически добавленные поля корректно интегрируются с логикой формы и не теряются при submit.


Динамическое добавление и удаление полей требует тщательного выбора методов поиска и ожидания элементов. Комбинация getBy*, findBy*, queryBy* и userEvent позволяет создавать надежные и читаемые тесты для любых интерактивных форм.