Вложенные компоненты со слотами

Основные понятия

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

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

  • Слот — область в компоненте, куда можно вставить произвольное содержимое из родительского компонента. Слоты бывают:

    • Default — основной слот.
    • Named — именованные слоты.
    • Scoped — слоты с передачей данных из дочернего компонента в родительский.

Vue Test Utils предоставляет инструменты для рендеринга компонентов с слотами и проверки их поведения.


Монтирование компонентов с вложенными компонентами

Для тестирования компонента с дочерними компонентами используется функция mount или shallowMount:

  • mount полностью рендерит компонент с его шаблоном и всеми дочерними компонентами.
  • shallowMount заменяет дочерние компоненты на заглушки (stubs), что ускоряет тест и изолирует компонент.

Пример:

import { mount, shallowMount } from '@vue/test-utils';
import ParentComponent from '@/components/ParentComponent.vue';
import ChildComponent from '@/components/ChildComponent.vue';

describe('ParentComponent', () => {
  it('рендерит дочерний компонент', () => {
    const wrapper = mount(ParentComponent);
    expect(wrapper.findComponent(ChildComponent).exists()).toBe(true);
  });

  it('использует shallowMount для изоляции', () => {
    const wrapper = shallowMount(ParentComponent);
    expect(wrapper.findComponent(ChildComponent).exists()).toBe(true);
    expect(wrapper.findComponent(ChildComponent).isVisible()).toBe(false);
  });
});
  • findComponent позволяет искать вложенный компонент по самому компоненту или селектору.
  • isVisible проверяет видимость элемента на странице.

Работа со слотами

Слоты передаются в mount через опцию slots. Это позволяет подставлять контент при тестировании.

Пример с default-слотом:

import { mount } from '@vue/test-utils';
import CardComponent from '@/components/CardComponent.vue';

describe('CardComponent', () => {
  it('рендерит контент в default-слоте', () => {
    const wrapper = mount(CardComponent, {
      slots: {
        default: '<p>Тестовый контент</p>'
      }
    });
    expect(wrapper.html()).toContain('Тестовый контент');
  });
});

Пример с именованными слотами:

import { mount } from '@vue/test-utils';
import LayoutComponent from '@/components/LayoutComponent.vue';

describe('LayoutComponent', () => {
  it('рендерит контент в named slot', () => {
    const wrapper = mount(LayoutComponent, {
      slots: {
        header: '<h1>Заголовок</h1>',
        footer: '<footer>Футер</footer>'
      }
    });
    expect(wrapper.find('h1').text()).toBe('Заголовок');
    expect(wrapper.find('footer').text()).toBe('Футер');
  });
});

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

import { mount } from '@vue/test-utils';
import ListComponent from '@/components/ListComponent.vue';

describe('ListComponent', () => {
  it('рендерит scoped slot с данными', () => {
    const wrapper = mount(ListComponent, {
      slots: {
        item: `<template #item="{ item }"><span>{{ item.name }}</span></template>`
      },
      props: {
        items: [{ name: 'Элемент 1' }, { name: 'Элемент 2' }]
      }
    });

    const items = wrapper.findAll('span');
    expect(items[0].text()).toBe('Элемент 1');
    expect(items[1].text()).toBe('Элемент 2');
  });
});

Заглушки для вложенных компонентов

Для упрощения тестирования больших компонентов удобно использовать stubs. Они позволяют заменить реальный компонент на упрощённый шаблон.

import { mount } from '@vue/test-utils';
import ParentComponent from '@/components/ParentComponent.vue';
import ChildComponent from '@/components/ChildComponent.vue';

describe('ParentComponent с stub', () => {
  it('заменяет дочерний компонент заглушкой', () => {
    const wrapper = mount(ParentComponent, {
      global: {
        stubs: {
          ChildComponent: '<div class="stub-child"></div>'
        }
      }
    });
    expect(wrapper.find('.stub-child').exists()).toBe(true);
  });
});
  • global.stubs можно использовать для любого количества вложенных компонентов.
  • Заглушки упрощают проверку взаимодействия родителя и дочернего компонента без рендеринга полной структуры.

Проверка событий и передачи пропсов

При тестировании компонентов со слотами и вложенными компонентами важно проверять props и события:

import { mount } from '@vue/test-utils';
import ChildComponent from '@/components/ChildComponent.vue';
import ParentComponent from '@/components/ParentComponent.vue';

describe('ParentComponent', () => {
  it('передаёт пропсы в дочерний компонент', () => {
    const wrapper = mount(ParentComponent);
    const child = wrapper.findComponent(ChildComponent);
    expect(child.props('title')).toBe('Заголовок');
  });

  it('обрабатывает событие от дочернего компонента', async () => {
    const wrapper = mount(ParentComponent);
    const child = wrapper.findComponent(ChildComponent);

    await child.vm.$emit('custom-event', 123);
    expect(wrapper.emitted('child-event')[0]).toEqual([123]);
  });
});
  • props() возвращает объект пропсов компонента.
  • emitted() позволяет проверить, были ли вызваны события и с какими данными.

Рекомендации при тестировании сложных компонентов

  • Использовать shallowMount для изоляции тестируемого компонента от сложных вложенных компонентов.
  • Передавать контент через слоты при необходимости проверки рендеринга.
  • Применять stubs для компонентов, не влияющих на основную логику.
  • Проверять как визуальный рендеринг, так и события/props для полной проверки взаимодействия компонентов.
  • Для scoped слотов всегда использовать шаблон <template #slotName="{ data }"> и передавать данные через props дочернего компонента.

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