Настройка Jest-тестов для Vue/React компонентов 1С-Битрикс
Vue и React компоненты в Битрикс-проектах — часть, которую проще всего протестировать изолированно. Компонент принимает пропсы, отдаёт DOM — чистая функция. Jest с Testing Library даёт возможность тестировать компоненты без браузера, быстро, в Node.js-окружении. Для Битрикс-проекта это означает: тесты фронтенда запускаются за секунды, не требуют запущенного сервера, интегрируются в CI без Docker.
Настройка Jest-тестов для Vue/React компонентов 1С-Битрикс
Структура тестов в Битрикс-проекте
/local/templates/my_site/
src/
components/
catalog/
ProductCard.vue <- компонент
ProductCard.test.ts <- тест рядом с компонентом
cart/
CartItem.tsx
CartItem.test.tsx
composables/
useCart.ts
useCart.test.ts
jest.config.ts
package.json
Тесты рядом с компонентами — удобнее, чем отдельная папка __tests__/: при рефакторинге перемещение компонента и его тестов происходит вместе.
jest.config.ts для Vue + TypeScript
// jest.config.ts
import type { Config } from 'jest';
const config: Config = {
testEnvironment: 'jsdom',
transform: {
'^.+\\.vue$': ['@vue/vue3-jest', { tsConfig: 'tsconfig.json' }],
'^.+\\.(ts|tsx|js|jsx)$': ['ts-jest', { tsconfig: 'tsconfig.json' }],
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
// Мокируем статические ресурсы
'\\.(css|scss|png|jpg|svg)$': '<rootDir>/src/__mocks__/fileMock.ts',
// Глобальный объект BX — мокируем
'^bx-globals$': '<rootDir>/src/__mocks__/bx.ts',
},
moduleFileExtensions: ['ts', 'tsx', 'vue', 'js', 'json'],
coverageDirectory: 'coverage',
collectCoverageFrom: [
'src/components/**/*.{vue,ts,tsx}',
'src/composables/**/*.ts',
'!src/**/*.test.{ts,tsx}',
],
setupFilesAfterFramework: ['<rootDir>/src/test-setup.ts'],
};
export default config;
Мок объекта BX и BX24
// src/__mocks__/bx.ts
// Глобальный объект Битрикс — мокируем для изолированных тестов
global.BX = {
bitrix_sessid: () => 'test-sessid-12345',
message: (params: Record<string, string>) => params,
bind: jest.fn(),
Event: { add: jest.fn() },
};
global.BX24 = {
init: (cb: () => void) => cb(),
isAdmin: () => false,
callMethod: jest.fn(),
callBatch: jest.fn(),
resizeWindow: jest.fn(),
};
Тест Vue-компонента карточки товара
// src/components/catalog/ProductCard.test.ts
import { mount } from '@vue/test-utils';
import { describe, it, expect, vi, beforeEach } from 'vitest'; // или jest.fn()
import ProductCard from './ProductCard.vue';
import * as cartApi from '@/api/cart';
const mockProduct = {
id: '42',
name: 'Дрель Bosch GSB 21-2 RCT',
price: '8990',
currency: 'RUB',
img: '/upload/test.jpg',
inStock: true,
};
describe('ProductCard', () => {
it('отображает название и цену товара', () => {
const wrapper = mount(ProductCard, {
props: { product: mockProduct },
});
expect(wrapper.find('.product-name').text()).toBe(mockProduct.name);
expect(wrapper.find('.product-price').text()).toContain('8 990');
});
it('показывает кнопку «В корзину» для товара в наличии', () => {
const wrapper = mount(ProductCard, {
props: { product: mockProduct },
});
expect(wrapper.find('[data-action="add-to-cart"]').exists()).toBe(true);
expect(wrapper.find('.out-of-stock').exists()).toBe(false);
});
it('скрывает кнопку «В корзину» для товара не в наличии', () => {
const wrapper = mount(ProductCard, {
props: { product: { ...mockProduct, inStock: false } },
});
expect(wrapper.find('[data-action="add-to-cart"]').exists()).toBe(false);
expect(wrapper.find('.out-of-stock').exists()).toBe(true);
});
it('вызывает API корзины при клике «В корзину»', async () => {
const addToCart = vi.spyOn(cartApi, 'addToCart').mockResolvedValue({
items: [],
totalPrice: 8990,
totalCount: 1,
currency: 'RUB',
});
const wrapper = mount(ProductCard, {
props: { product: mockProduct },
});
await wrapper.find('[data-action="add-to-cart"]').trigger('click');
await wrapper.vm.$nextTick();
expect(addToCart).toHaveBeenCalledWith({
productId: 42,
quantity: 1,
});
});
});
Тест React-компонента (TSX)
// src/components/cart/CartItem.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CartItem from './CartItem';
import * as cartApi from '@/api/cart';
const mockItem = {
id: 1,
name: 'Перфоратор Makita HR2630',
price: 12490,
quantity: 2,
img: null,
};
describe('CartItem', () => {
it('отображает название и суммарную стоимость', () => {
render(<CartItem item={mockItem} onRemove={jest.fn()} onQuantityChange={jest.fn()} />);
expect(screen.getByText('Перфоратор Makita HR2630')).toBeInTheDocument();
expect(screen.getByText('24 980 ₽')).toBeInTheDocument(); // 12490 * 2
});
it('вызывает onQuantityChange при изменении количества', async () => {
const onQuantityChange = jest.fn();
const user = userEvent.setup();
render(<CartItem item={mockItem} onRemove={jest.fn()} onQuantityChange={onQuantityChange} />);
const plusBtn = screen.getByRole('button', { name: '+' });
await user.click(plusBtn);
expect(onQuantityChange).toHaveBeenCalledWith(mockItem.id, 3);
});
});
Тест composable (Vue Composition API)
// src/composables/useCart.test.ts
import { setActivePinia, createPinia } from 'pinia';
import { useCart } from './useCart';
import * as cartApi from '@/api/cart';
vi.mock('@/api/cart');
describe('useCart', () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it('инициализируется с пустой корзиной', () => {
const { items, totalCount, totalPrice } = useCart();
expect(items.value).toEqual([]);
expect(totalCount.value).toBe(0);
expect(totalPrice.value).toBe(0);
});
it('добавляет товар и обновляет состояние', async () => {
vi.mocked(cartApi.addToCart).mockResolvedValue({
items: [{ id: 1, name: 'Test', price: 100, quantity: 1, img: null }],
totalPrice: 100,
totalCount: 1,
currency: 'RUB',
});
const { addItem, totalCount } = useCart();
await addItem(42, 1);
expect(totalCount.value).toBe(1);
});
});
Запуск тестов
# Все тесты
npx jest
# С покрытием
npx jest --coverage
# Watch-mode при разработке
npx jest --watch
# Конкретный файл
npx jest ProductCard
Сроки
| Задача | Сроки |
|---|---|
| Настройка Jest + @vue/test-utils или @testing-library/react | 4–8 часов |
| Базовые тесты для 5–10 компонентов | 1–2 дня |
| Тесты composables / хуков с Pinia/Zustand | 1 день |
| Интеграция в CI, конфигурация coverage-репорта | 4 часа |







