Мы (инженеры) видим все больше и больше анимаций в наших проектах. Если ваша страница TikTok For You чем-то похожа на мою, она содержит удивительное количество руководств по Figma и InDesign по созданию современных пользовательских интерфейсов, битком набитых анимацией. Это восхитительно для пользователей, но меня, как инженера, это немного пугает. В этом посте я расскажу, с чем столкнулся, пытаясь написать автоматические тесты для анимации, и какие обходные пути я нашел.

Как инженер полного стека, анимация далеко не моя повседневная работа. Однако в прошлом году у меня была возможность поработать над парой функций продукта Klaviyo Signup Forms, где я стряхнул пыль с навыков анимации, которыми не пользовался с тех пор, как начал веб-разработку.

В прошлом году Klaviyo выпустила функцию Form Teaser. Тизеры — это небольшие виджеты, которые появляются до и/или после более крупной формы на странице. Они дают посетителям сайтов наших клиентов ненавязчивый способ взаимодействия с формами.

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

Есть два основных типа анимации, которые вы встретите в веб-разработке: анимация css и анимация javascript. Есть статьи получше, в которых объясняются их различия и преимущества, но на высоком уровне хорошей идеей будет выбрать анимацию JavaScript, а не анимацию CSS, если вы хотите встроить более сложные эффекты, такие как остановка, пауза, перемотка назад, повторная перемотка. run, или если вам нужен контроль над тем, когда и что анимировать. Размышляя о том, как анимируются тизеры, мы делаем много таких вещей (остановка, запуск, повтор, перемотка назад), поэтому для гибкости мы выбрали анимацию javascript. Наша анимация javascript по-прежнему зависит от ключевых кадров CSS, чтобы сообщить компонентам, что делать во время анимации, но мы контролируем, когда анимация начинает использовать javascript.

В том, как мы создавали анимацию, нет ничего особенного. Мы сохраняем состояние того, анимируется ли тизер или форма в любой момент времени в хранилище Redux, поэтому мы можем запускать другие события на основе этого. Настройка этого состояния выполняется через API для анимации элементов:

onAnimationStart (оболочка реакции вокруг animationstart)Это событие срабатывает один раз, когда начинается анимация элемента, и мы используем его для установки различных состояний в нашем хранилище Redux, чтобы мы знали в других частях приложения, независимо от того, была ли форма или тизер в настоящее время в анимации.

onAnimationEnd (оболочка реакции вокруг animationend) — это событие срабатывает, когда данный элемент прекращает анимацию.

Большая часть этого хорошо документирована и поэтому проста в реализации. Однако при работе с интерфейсом иногда самое сложное — написание автоматизированных тестов.

Вот упрощенный фрагмент теста, который, как я думал, сразу же сработает. Цель этого теста — показать, что тизер, отображаемый на странице, виден после закрытия формы. Мы проверяем тизер, ища содержащийся в нем текст «Получите скидку 15%».

describe('A Teaser rendered after the form', () => {
  let form: RenderResult;
  const handlerMock = jest.fn();
  beforeAll(async () => {
    form = await utils.renderFormUsingWholeTriggeringSuite();
    // Wait for the form to render
    await form.findByPlaceholderText(EXPECTED_PLACEHOLDER);
  });
  it('switches to teaser when form is closed', async () => {
    // Expect the teaser to not be in the view
    expect(form.queryByText('Get 15% Off.')).not.toBeInTheDocument();
    const closeButton = await form.findByText(/Close form \d+/); // Close the form
    userEvent.click(closeButton);
    // Expect the teaser to be in the view
    await waitFor(() => {
      expect(screen.getByText('Get 15% Off.')).toBeVisible();
    });
    // Expect the form not to be in the view
    expect(
      screen.queryByPlaceholderText(EXPECTED_PLACEHOLDER)
    ).not.toBeVisible();
  });
});

Но тест не прошел на строке, которая ожидает, что «Получите скидку 15%». После небольшой отладки, установки точек останова и использования журналов консоли я пришел к выводу, что этот тест терпит неудачу, потому что анимация никогда не завершается должным образом.

На момент написания этого блога поиск по запросу «тестирование анимации javascript в Jest» выдает пару запросов о переполнении стека, вопрос на форуме с сайта, о котором я раньше не слышал, но затем он превращается в обычные вопросы о тестировании в Jest. и React, ничего особенного для анимации. Вот почему я пишу это!

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

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

  • Загрузить страницу
  • Проверить загрузку тизера после задержки на странице
  • Нажмите тизер
  • Убедитесь, что анимация тизера заканчивается, а анимация формы начинается.

Мы обошли другие проблемы с анимацией в других инструментах тестирования (смотрим на вас, Cypress), переопределив продолжительность анимации свойства css на 0 с для всех элементов на странице, но это работает только в том случае, если анимация, которую вы пытаетесь оценить, происходит на странице. загружаться или не запускается через JavaScript. Это одна из тех вещей, от которых вы ожидаете, что они «просто сработают», потому что это то, что делает обычный браузер, но в этих тестах это было не так.

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

Давайте проиллюстрируем это на каком-нибудь псевдокоде.

import { fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
// First we can get the teaser from the Jest DOM
const teaser = document.querySelector('#my-beautiful-teaser');
// Fire the click event on the teaser and the necessary animation events on the same element
userEvent.click(teaser);
fireEvent.animationStart(teaser);
fireEvent.animationEnd(teaser);
expect(teaser).not.toBeVisible();

Теперь вам может быть интересно, почему мы запустили два события? Нас действительно волнует, закончится ли анимация? И на самом деле вопрос здесь в том, что это зависит от того, что вы тестируете. В нашем случае мы не удаляем тизер из представления, пока анимация не завершится. В противном случае вы получите эту раздражающую странную анимацию на стороне конечного пользователя, где анимация начнется, а затем тизер просто исчезнет, ​​прежде чем он полностью исчезнет из поля зрения. Не хорошо. Однако, если вы тестируете какой-то другой элемент, который должен появляться или исчезать одновременно с началом анимации этого элемента, то все готово, и вам нужно только одно событие.

Давайте посмотрим, сможем ли мы применить те же принципы к тесту, который я упомянул выше:

describe('A Teaser rendered after the form', () => {
  let form: RenderResult;
  const handlerMock = jest.fn();
  beforeAll(async () => {
    form = await utils.renderFormUsingWholeTriggeringSuite();
    await form.findByPlaceholderText(EXPECTED_PLACEHOLDER);
  });
  it('switches to teaser when form is closed', async () => {
    // Expect the teaser to not be in the view
    expect(form.queryByText('Get 15% Off.')).not.toBeInTheDocument();
    const closeButton = await form.findByText(/Close form \d+/);
    // Get a reference to the form's animation div
    const animationDiv = form.getByTestId('FLYOUT');
    // Close the form
    userEvent.click(closeButton);
    // Run the div's onAnimationEnd event
    fireEvent.animationEnd(animationDiv);
    // Get a reference to the teaser's animation div
    const animatedTeaser = await form.findByTestId('animated-teaser');
    // Run the teaser's onAnimationEnd event
    fireEvent.animationEnd(animatedTeaser);
    // Expect the teaser to be in the view
    await waitFor(() => {
      expect(screen.getByText('Get 15% Off.')).toBeVisible();
    });
    // Expect the form not to be in the view
    expect(
      screen.queryByPlaceholderText(EXPECTED_PLACEHOLDER)
    ).not.toBeVisible();
  });
});

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

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

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