Тестирование Suspense компонентов

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


Основы Suspense

Suspense используется для оборачивания компонентов, которые возвращают Promise или используют async setup. Пример простого компонента:

import { defineComponent, ref, onMounted } from 'vue';

export default defineComponent({
  setup() {
    const data = ref(null);

    const fetchData = async () => {
      const result = await new Promise(resolve => setTimeout(() => resolve('Loaded'), 1000));
      data.value = result;
    };

    onMounted(fetchData);

    return { data };
  },
  template: `<div>{{ data ?? 'Loading...' }}</div>`
});

При использовании Suspense:

<Suspense>
  <template #default>
    <AsyncComponent />
  </template>
  <template #fallback>
    <div>Загрузка...</div>
  </template>
</Suspense>

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


Монтирование Suspense компонентов

В Vue Test Utils (VTU) для тестирования Suspense компонентов используется метод mount или shallowMount с асинхронным ожиданием рендера. Ключевой момент — правильно обрабатывать асинхронный рендеринг:

import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import AsyncComponent from './AsyncComponent.vue';

test('Suspense показывает fallback и основной контент', async () => {
  const wrapper = mount({
    template: `
      <Suspense>
        <template #default>
          <AsyncComponent />
        </template>
        <template #fallback>
          <div class="fallback">Загрузка...</div>
        </template>
      </Suspense>
    `
  });

  // Проверка, что изначально отображается fallback
  expect(wrapper.html()).toContain('Загрузка...');

  // Ожидание завершения всех асинхронных операций
  await nextTick();
  await new Promise(resolve => setTimeout(resolve, 1000));
  await nextTick();

  // Проверка, что основной компонент рендерится
  expect(wrapper.html()).toContain('Loaded');
});

Ключевые моменты:

  • nextTick() позволяет дождаться завершения текущего цикла реактивности.
  • При асинхронных действиях может потребоваться ожидание с помощью Promise или flushPromises.
  • Проверки нужно делать до и после завершения асинхронного рендера.

Использование flushPromises

Для удобного тестирования асинхронного кода часто применяется утилита flush-promises. Она позволяет дождаться завершения всех промисов без использования setTimeout:

import flushPromises from 'flush-promises';

test('Suspense с flushPromises', async () => {
  const wrapper = mount({
    template: `
      <Suspense>
        <template #default>
          <AsyncComponent />
        </template>
        <template #fallback>
          <div class="fallback">Загрузка...</div>
        </template>
      </Suspense>
    `
  });

  expect(wrapper.html()).toContain('Загрузка...');
  await flushPromises();
  await nextTick();
  expect(wrapper.html()).toContain('Loaded');
});

Использование flushPromises делает код тестов более читаемым и надежным при множественных асинхронных вызовах.


Тестирование ошибок в Suspense

Suspense поддерживает обработку ошибок через errorCaptured или глобальные error-handlers. Для проверки ошибок в VTU:

import { defineComponent, h } from 'vue';

const FailingComponent = defineComponent({
  async setup() {
    throw new Error('Ошибка загрузки');
  },
  render() {
    return h('div', 'Этот текст не рендерится');
  }
});

test('Suspense обрабатывает ошибки', async () => {
  const wrapper = mount({
    template: `
      <Suspense>
        <template #default>
          <FailingComponent />
        </template>
        <template #fallback>
          <div class="fallback">Загрузка...</div>
        </template>
      </Suspense>
    `
  });

  await flushPromises();
  await nextTick();

  // Ошибки нужно ловить через errorCaptured или глобальный обработчик
  const error = wrapper.vm.$.appContext.config.errorHandler;
  expect(error).toBeDefined();
});

Советы по надежному тестированию Suspense

  1. Всегда тестировать fallback и основной контент отдельно. Асинхронный рендер может завершиться позже, чем основной тестовый код.
  2. Использовать flushPromises для ожидания всех асинхронных операций.
  3. Добавлять nextTick() после промисов, чтобы обновления реактивности точно отобразились.
  4. Тестировать обработку ошибок, особенно если компонент может выбросить исключение при загрузке данных.
  5. Избегать использования setTimeout для ожидания рендера, если возможно — использовать flushPromises, это делает тесты стабильнее.

Проверка содержимого и классов

Для Suspense компонентов можно использовать стандартные VTU методы:

expect(wrapper.find('.fallback').exists()).toBe(true);
expect(wrapper.text()).toContain('Loaded');

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

const dataWrapper = wrapper.findComponent(AsyncComponent);
expect(dataWrapper.vm.data).toBe('Loaded');

Интеграция с Jest и другими тест-раннерами

При работе с Jest важно помнить:

  • Настройки таймеров (jest.useFakeTimers()) могут влиять на асинхронные промисы.
  • Асинхронные тесты должны быть помечены async и использовать await.
  • Suspense компоненты, содержащие асинхронные операции, могут потребовать комбинации flushPromises + nextTick для корректного рендера.

Использование Vue Test Utils с Suspense позволяет создавать надежные и детализированные тесты, проверяя не только конечный результат, но и процесс асинхронной загрузки, отображение fallback-контента и обработку ошибок. Правильная организация ожиданий и применение утилит для промисов делают тесты стабильными и воспроизводимыми.