- Home »

Как тестировать React приложение с Jest и React Testing Library
Фронтенд-разработка без тестирования — это как настройка сервера без мониторинга. Вроде бы всё работает, но когда что-то ломается, начинается хаос. Если вы деплоите React-приложения на свои серверы или настраиваете CI/CD пайплайны, то автоматизированное тестирование — это не роскошь, а необходимость. Jest и React Testing Library — это золотой стандарт для тестирования React-компонентов, который поможет вам спать спокойно, зная, что ваше приложение не развалится после очередного деплоя.
В этой статье разберём, как правильно настроить тестирование React-приложений с нуля, чтобы ваши тесты были быстрыми, надёжными и не превращались в кошмар поддержки. Покажу практические примеры, подводные камни и лайфхаки, которые сэкономят вам кучу времени.
🔍 Как это работает: архитектура тестирования
Jest — это test runner и assertion library в одном флаконе. Он запускает тесты, предоставляет API для проверок и генерирует отчёты. React Testing Library — это набор утилит для рендеринга и взаимодействия с React-компонентами в тестовой среде.
Ключевая философия React Testing Library — тестировать не implementation details, а поведение компонентов так, как их видит пользователь. Никаких проверок internal state или вызовов методов — только взаимодействие через DOM.
Архитектура выглядит так:
- Jest — тестовый фреймворк и test runner
- React Testing Library — утилиты для рендеринга компонентов
- jsdom — виртуальная DOM-среда для Node.js
- @testing-library/jest-dom — дополнительные матчеры для Jest
⚙️ Быстрая настройка: от нуля до первого теста
Если вы создаёте приложение с Create React App, то Jest и React Testing Library уже настроены. Для кастомной настройки или если вы используете другие бандлеры, делаем всё руками.
Установка зависимостей
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event
# Для TypeScript проектов
npm install --save-dev @types/jest
Конфигурация Jest
Создайте файл jest.config.js
в корне проекта:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['/src/setupTests.js'],
moduleNameMapping: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'\\.(gif|ttf|eot|svg|png)$': '/src/__mocks__/fileMock.js'
},
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest'
},
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.js',
'!src/reportWebVitals.js'
],
testMatch: [
'/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
'/src/**/*.{spec,test}.{js,jsx,ts,tsx}'
]
};
Файл setupTests.js
import '@testing-library/jest-dom';
// Глобальные моки для API, которые не поддерживаются в jsdom
global.fetch = require('jest-fetch-mock');
// Мок для localStorage
Object.defineProperty(window, 'localStorage', {
value: {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
},
writable: true,
});
// Мок для window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
Мок для статических файлов
Создайте файл src/__mocks__/fileMock.js
:
module.exports = 'test-file-stub';
Настройка package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --watchAll=false --coverage --passWithNoTests"
}
}
🧪 Практические примеры и кейсы
Базовый тест компонента
Начнём с простого компонента кнопки:
// Button.jsx
import React from 'react';
const Button = ({ onClick, children, disabled = false, variant = 'primary' }) => {
return (
);
};
export default Button;
Тест для этого компонента:
// Button.test.jsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from './Button';
describe('Button Component', () => {
test('renders button with correct text', () => {
render();
const button = screen.getByRole('button', { name: /click me/i });
expect(button).toBeInTheDocument();
});
test('calls onClick when clicked', async () => {
const user = userEvent.setup();
const handleClick = jest.fn();
render();
const button = screen.getByRole('button');
await user.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
test('is disabled when disabled prop is true', () => {
render();
const button = screen.getByRole('button');
expect(button).toBeDisabled();
});
test('has correct CSS class based on variant', () => {
render();
const button = screen.getByRole('button');
expect(button).toHaveClass('btn-secondary');
});
});
Тестирование формы с валидацией
// LoginForm.jsx
import React, { useState } from 'react';
const LoginForm = ({ onSubmit }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});
const validate = () => {
const newErrors = {};
if (!email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(email)) {
newErrors.email = 'Email is invalid';
}
if (!password) {
newErrors.password = 'Password is required';
} else if (password.length < 6) {
newErrors.password = 'Password must be at least 6 characters';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate()) {
onSubmit({ email, password });
}
};
return (
);
};
export default LoginForm;
Тест для формы:
// LoginForm.test.jsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
describe('LoginForm', () => {
const mockOnSubmit = jest.fn();
beforeEach(() => {
mockOnSubmit.mockClear();
});
test('submits form with valid data', async () => {
const user = userEvent.setup();
render( );
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /login/i });
await user.type(emailInput, 'test@example.com');
await user.type(passwordInput, 'password123');
await user.click(submitButton);
expect(mockOnSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
test('shows validation errors for invalid data', async () => {
const user = userEvent.setup();
render( );
const submitButton = screen.getByRole('button', { name: /login/i });
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
});
expect(mockOnSubmit).not.toHaveBeenCalled();
});
test('validates email format', async () => {
const user = userEvent.setup();
render( );
const emailInput = screen.getByLabelText(/email/i);
const submitButton = screen.getByRole('button', { name: /login/i });
await user.type(emailInput, 'invalid-email');
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/email is invalid/i)).toBeInTheDocument();
});
});
test('validates password length', async () => {
const user = userEvent.setup();
render( );
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /login/i });
await user.type(passwordInput, '12345');
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/password must be at least 6 characters/i)).toBeInTheDocument();
});
});
});
Тестирование компонентов с асинхронными операциями
// UserProfile.jsx
import React, { useState, useEffect } from 'react';
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
if (userId) {
fetchUser();
}
}, [userId]);
if (loading) return Loading...;
if (error) return Error: {error};
if (!user) return No user found;
return (
{user.name}
Email: {user.email}
Role: {user.role}
);
};
export default UserProfile;
Тест с мокированием fetch:
// UserProfile.test.jsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';
// Мок для fetch
global.fetch = jest.fn();
describe('UserProfile', () => {
beforeEach(() => {
fetch.mockClear();
});
test('displays loading state initially', () => {
fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ name: 'John Doe', email: 'john@example.com', role: 'user' })
});
render( );
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
test('displays user data after successful fetch', async () => {
const mockUser = {
name: 'John Doe',
email: 'john@example.com',
role: 'admin'
};
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUser
});
render( );
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('Email: john@example.com')).toBeInTheDocument();
expect(screen.getByText('Role: admin')).toBeInTheDocument();
});
expect(fetch).toHaveBeenCalledWith('/api/users/123');
});
test('displays error message when fetch fails', async () => {
fetch.mockRejectedValueOnce(new Error('Network error'));
render( );
await waitFor(() => {
expect(screen.getByText(/error: network error/i)).toBeInTheDocument();
});
});
test('displays error when API returns error response', async () => {
fetch.mockResolvedValueOnce({
ok: false,
status: 404
});
render( );
await waitFor(() => {
expect(screen.getByText(/error: failed to fetch user/i)).toBeInTheDocument();
});
});
});
📊 Сравнение подходов тестирования
Подход | Преимущества | Недостатки | Когда использовать |
---|---|---|---|
React Testing Library |
• Тестирует поведение, а не реализацию • Отличная поддержка accessibility • Простой API • Хорошая документация |
• Сложно тестировать implementation details • Требует переосмысления подхода к тестированию |
Unit и integration тесты компонентов |
Enzyme |
• Детальный контроль над компонентами • Возможность тестировать state и props • Shallow рендеринг |
• Нет официальной поддержки React 18+ • Тестирует implementation details • Хрупкие тесты |
Legacy проекты (не рекомендуется для новых) |
Cypress |
• End-to-end тестирование • Реальный браузер • Отличные инструменты отладки • Временные путешествия |
• Медленные тесты • Сложная настройка • Требует больше ресурсов |
E2E тесты критичных user flows |
🚀 Продвинутые техники и оптимизация
Создание кастомных матчеров
// testUtils.js
import { expect } from '@jest/globals';
expect.extend({
toHaveValidationError(received, expectedError) {
const errorElement = received.querySelector('[role="alert"]');
const pass = errorElement && errorElement.textContent.includes(expectedError);
if (pass) {
return {
message: () => `expected element not to have validation error "${expectedError}"`,
pass: true,
};
} else {
return {
message: () => `expected element to have validation error "${expectedError}"`,
pass: false,
};
}
},
});
// Использование
const form = render( );
expect(form.container).toHaveValidationError('Email is required');
Создание render wrapper для провайдеров
// test-utils.jsx
import React from 'react';
import { render } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ThemeProvider } from 'styled-components';
import { theme } from '../src/theme';
const AllTheProviders = ({ children }) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
return (
{children}
);
};
const customRender = (ui, options) =>
render(ui, { wrapper: AllTheProviders, ...options });
// re-export everything
export * from '@testing-library/react';
// override render method
export { customRender as render };
Тестирование хуков с React Testing Library
// useCounter.js
import { useState, useCallback } from 'react';
export const useCounter = (initialValue = 0) => {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => {
setCount(prev => prev + 1);
}, []);
const decrement = useCallback(() => {
setCount(prev => prev - 1);
}, []);
const reset = useCallback(() => {
setCount(initialValue);
}, [initialValue]);
return { count, increment, decrement, reset };
};
// useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
test('should initialize with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
test('should initialize with provided value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('should decrement counter', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
test('should reset counter', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(12);
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(10);
});
});
🔧 Интеграция с CI/CD и автоматизация
GitHub Actions для тестирования
# .github/workflows/test.yml
name: Test
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:ci
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
Dockerfile для тестирования
# Dockerfile.test
FROM node:18-alpine
WORKDIR /app
# Копируем package.json и package-lock.json
COPY package*.json ./
# Устанавливаем зависимости
RUN npm ci --only=production=false
# Копируем исходный код
COPY . .
# Запускаем тесты
CMD ["npm", "run", "test:ci"]
# Для локального запуска:
# docker build -f Dockerfile.test -t my-app-tests .
# docker run --rm my-app-tests
Скрипт для автоматизации тестирования
#!/bin/bash
# test-runner.sh
set -e
echo "🧪 Starting test suite..."
# Проверяем, что все зависимости установлены
if [ ! -d "node_modules" ]; then
echo "📦 Installing dependencies..."
npm ci
fi
# Запускаем линтер
echo "🔍 Running linter..."
npm run lint
# Запускаем типизацию (если TypeScript)
if [ -f "tsconfig.json" ]; then
echo "📝 Checking TypeScript..."
npx tsc --noEmit
fi
# Запускаем тесты с покрытием
echo "🎯 Running tests with coverage..."
npm run test:coverage
# Проверяем покрытие
echo "📊 Checking coverage thresholds..."
npx jest --coverage --passWithNoTests
# Генерируем отчёт
echo "📈 Generating test report..."
npm run test:report
echo "✅ All tests passed!"
# Если нужно деплоить на сервер
if [ "$1" = "--deploy" ]; then
echo "🚀 Deploying to server..."
npm run build
rsync -avz --delete dist/ user@your-server:/var/www/html/
fi
📈 Мониторинг и метрики тестирования
Настройка отчётов о покрытии
// jest.config.js (дополнение)
module.exports = {
// ... другие настройки
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
coverageReporters: ['text', 'lcov', 'html', 'json'],
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.js',
'!src/reportWebVitals.js',
'!src/**/*.stories.{js,jsx,ts,tsx}',
'!src/**/*.test.{js,jsx,ts,tsx}'
]
};
Интеграция с SonarQube
# sonar-project.properties
sonar.projectKey=my-react-app
sonar.projectName=My React App
sonar.projectVersion=1.0
sonar.sources=src
sonar.tests=src
sonar.test.inclusions=**/*.test.js,**/*.test.jsx,**/*.test.ts,**/*.test.tsx
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.testExecutionReportPaths=coverage/test-report.xml
🎭 Полезные утилиты и расширения
MSW (Mock Service Worker) для API мокирования
npm install --save-dev msw
// src/mocks/handlers.js
import { rest } from 'msw';
export const handlers = [
rest.get('/api/users/:id', (req, res, ctx) => {
const { id } = req.params;
return res(
ctx.json({
id,
name: 'John Doe',
email: 'john@example.com',
role: 'user'
})
);
}),
rest.post('/api/login', (req, res, ctx) => {
const { email, password } = req.body;
if (email === 'test@example.com' && password === 'password') {
return res(
ctx.json({
token: 'fake-jwt-token',
user: { id: 1, email, name: 'Test User' }
})
);
}
return res(
ctx.status(401),
ctx.json({ error: 'Invalid credentials' })
);
})
];
// src/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// src/setupTests.js (дополнение)
import { server } from './mocks/server';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Storybook для визуального тестирования
# Установка Storybook
npx storybook@latest init
# Интеграция с тестированием
npm install --save-dev @storybook/test-runner @storybook/jest
# package.json
{
"scripts": {
"test-storybook": "test-storybook",
"test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"npm run storybook -- --ci\" \"wait-on tcp:6006 && npm run test-storybook\""
}
}
💡 Интересные факты и нестандартные применения
Знали ли вы, что:
- Jest может запускать тесты в параллельном режиме, используя все доступные CPU ядра. Это особенно полезно при развёртывании на мощных серверах.
- React Testing Library автоматически очищает DOM после каждого теста, что предотвращает memory leaks в длительных тестовых прогонах.
- Можно настроить Jest для работы с модулями ES6 без Babel, используя экспериментальный флаг
--experimental-vm-modules
в Node.js. - Testing Library поддерживает не только React, но и Vue, Angular, Svelte — можно использовать единый подход для всех фреймворков.
Нестандартные применения:
- Тестирование производительности: Можно измерять время рендеринга компонентов и отслеживать регрессии производительности.
- Визуальное регрессионное тестирование: Интеграция с Chromatic или Percy для автоматического выявления визуальных изменений.
- Accessibility тестирование: Использование jest-axe для автоматической проверки доступности.
- Cross-browser тестирование: Запуск тестов в разных версиях jsdom или интеграция с Playwright.
Интеграция с инструментами разработки
// Webhook для отправки результатов тестов в Slack
// webhook-reporter.js
class WebhookReporter {
constructor(globalConfig, options) {
this._globalConfig = globalConfig;
this._options = options;
}
onRunComplete(contexts, results) {
const { numFailedTests, numPassedTests, numTotalTests } = results;
if (numFailedTests > 0) {
fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `🚨 Tests failed: ${numFailedTests}/${numTotalTests} tests failed`,
attachments: [{
color: 'danger',
fields: [{
title: 'Test Results',
value: `Passed: ${numPassedTests}\nFailed: ${numFailedTests}`,
short: true
}]
}]
})
});
}
}
}
module.exports = WebhookReporter;
🌟 Автоматизация и DevOps интеграция
Для серверных админов особенно важна интеграция тестирования в процессы деплоя. Если вы используете VPS для разработки или выделенный сервер для продакшена, то правильная настройка тестирования может сэкономить кучу времени на отладку в продакшене.
Мониторинг тестов в реальном времени
// test-monitor.js
const WebSocket = require('ws');
const fs = require('fs');
class TestMonitor {
constructor() {
this.wss = new WebSocket.Server({ port: 8080 });
this.clients = new Set();
this.wss.on('connection', (ws) => {
this.clients.add(ws);
ws.on('close', () => this.clients.delete(ws));
});
}
broadcast(data) {
this.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(data));
}
});
}
onTestResult(test, result) {
this.broadcast({
type: 'test_result',
test: test.path,
status: result.status,
duration: result.duration
});
}
onRunComplete(results) {
this.broadcast({
type: 'run_complete',
summary: {
total: results.numTotalTests,
passed: results.numPassedTests,
failed: results.numFailedTests,
duration: results.runTime
}
});
}
}
module.exports = TestMonitor;
Кеширование тестов для ускорения CI
# .github/workflows/test-with-cache.yml
name: Test with Cache
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Cache Node modules
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Cache Jest cache
uses: actions/cache@v3
with:
path: .jest-cache
key: ${{ runner.os }}-jest-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-jest-
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:ci -- --cacheDirectory=.jest-cache
🎯 Заключение и рекомендации
Тестирование React-приложений с Jest и React Testing Library — это не просто хорошая практика, это необходимость для любого серьёзного проекта. Особенно если вы управляете серверной инфраструктурой и отвечаете за стабильность продакшена.
Основные выводы:
- Начинайте с простого: Настройте базовое тестирование и постепенно добавляйте сложные сценарии
- Автоматизируйте всё: Интегрируйте тесты в CI/CD, настройте автоматические проверки перед деплоем
- Следите за метриками: Покрытие кода, время выполнения тестов, количество падений в продакшене
- Инвестируйте в инфраструктуру: Хороший сервер для CI/CD окупится экономией времени на отладку
Когда использовать Jest + React Testing Library:
- Unit тестирование React компонентов
- Integration тестирование взаимодействия компонентов
- Тестирование пользовательских сценариев
- Валидация форм и обработка ошибок
- Проверка доступности (accessibility)
Альтернативы рассмотрите когда:
- Cypress/Playwright: Для end-to-end тестирования критичных пользовательских сценариев
- Storybook: Для изолированной разработки и тестирования компонентов
- Vitest: Если используете Vite и нужна максимальная скорость
Помните: хорошие тесты — это инвестиция в будущее. Потратьте время на правильную настройку сейчас, и вы сэкономите недели отладки в продакшене потом. А если вам нужна надёжная инфраструктура для CI/CD, обратите внимание на качественные серверные решения — стабильность тестирования напрямую зависит от стабильности инфраструктуры.
В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.
Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.