From b8c9db799da4667d18eacbf99d433c85d9b93a6b Mon Sep 17 00:00:00 2001 From: iqubik Date: Sun, 29 Mar 2026 22:59:10 +0300 Subject: [PATCH] tests front 50% --- client/test/pages/DomainsPage.spec.tsx | 321 +++++++++ client/test/pages/SubscriptionsPage.spec.tsx | 700 +++++++++++++++++++ client/test/setup.ts | 27 +- 3 files changed, 1046 insertions(+), 2 deletions(-) create mode 100644 client/test/pages/DomainsPage.spec.tsx create mode 100644 client/test/pages/SubscriptionsPage.spec.tsx diff --git a/client/test/pages/DomainsPage.spec.tsx b/client/test/pages/DomainsPage.spec.tsx new file mode 100644 index 0000000..1ceb873 --- /dev/null +++ b/client/test/pages/DomainsPage.spec.tsx @@ -0,0 +1,321 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import { BrowserRouter } from 'react-router-dom' +import DomainsPage from '../../src/pages/DomainsPage' +import { ThemeProvider } from '../../src/ThemeContext' +import { AuthProvider } from '../../src/auth/AuthContext' + +// Mock MUI icons +vi.mock('@mui/icons-material', () => ({ + Delete: () => null, + Add: () => null, + UploadFile: () => null, + Remove: () => null, + ExpandMore: () => null, + Download: () => null, +})) + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockDelete = vi.fn() + +vi.mock('../../src/api', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +const mockClipboardWriteText = vi.fn() +Object.assign(navigator, { clipboard: { writeText: mockClipboardWriteText } }) + +beforeEach(() => { + vi.clearAllMocks() + mockClipboardWriteText.mockClear() + vi.spyOn(console, 'log').mockImplementation(() => {}) + vi.spyOn(console, 'debug').mockImplementation(() => {}) + vi.spyOn(console, 'warn').mockImplementation(() => {}) + vi.spyOn(console, 'error').mockImplementation(() => {}) + + // Mock localStorage + Object.defineProperty(window, 'localStorage', { + value: { + getItem: vi.fn(() => null), + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn(), + }, + writable: true, + }) +}) + +afterEach(() => { + vi.clearAllMocks() + vi.restoreAllMocks() +}) + +const setupMockGet = (overrides?: { + domains?: { data: unknown[]; total: number } + capabilities?: unknown + status?: unknown + settings?: unknown +}) => { + mockGet.mockImplementation((url: string) => { + if (url.startsWith('/domains?page=')) { + return Promise.resolve({ data: overrides?.domains || { data: [], total: 0 } }) + } + if (url === '/domains/scan/capabilities') { + return Promise.resolve({ data: overrides?.capabilities || { scannerAvailable: false, timeoutAvailable: false } }) + } + if (url === '/domains/scan/status') { + return Promise.resolve({ data: overrides?.status || { running: false } }) + } + if (url === '/settings') { + return Promise.resolve({ data: overrides?.settings || {} }) + } + return Promise.resolve({ data: {} }) + }) +} + +const renderDomainsPage = () => { + return render( + + + + + + + + ) +} + +describe('DomainsPage', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + describe('Рендеринг', () => { + it('должен рендериться с заголовком', async () => { + setupMockGet() + renderDomainsPage() + + await waitFor(() => { + expect(screen.getByText('Белый список доменов (SNI)')).toBeInTheDocument() + }) + }) + + it('должен отображать поле для добавления домена', async () => { + setupMockGet() + renderDomainsPage() + + await waitFor(() => { + expect(screen.getByLabelText('Доменное имя')).toBeInTheDocument() + }) + }) + + it('должен отображать кнопку "Добавить"', async () => { + setupMockGet() + renderDomainsPage() + + await waitFor(() => { + expect(screen.getByText('Добавить')).toBeInTheDocument() + }) + }) + + it('должен отображать кнопку "Из файла"', async () => { + setupMockGet() + renderDomainsPage() + + await waitFor(() => { + expect(screen.getByText('Из файла')).toBeInTheDocument() + }) + }) + + it('должен отображать таблицу доменов', async () => { + setupMockGet({ + domains: { data: [{ id: 1, name: 'example.com' }], total: 1 } + }) + renderDomainsPage() + + await waitFor(() => { + expect(screen.getByText('example.com')).toBeInTheDocument() + }) + }) + + it('должен отображать сообщение при отсутствии доменов', async () => { + setupMockGet({ domains: { data: [], total: 0 } }) + renderDomainsPage() + + await waitFor(() => { + expect(screen.getByText('Нет доменов')).toBeInTheDocument() + }) + }) + }) + + describe('Загрузка данных', () => { + it('должен загружать домены при монтировании', async () => { + setupMockGet() + renderDomainsPage() + + await waitFor(() => { + expect(mockGet).toHaveBeenCalledWith('/domains?page=1&limit=10') + }) + }) + + it('должен загружать возможности сканера', async () => { + setupMockGet() + renderDomainsPage() + + await waitFor(() => { + expect(mockGet).toHaveBeenCalledWith('/domains/scan/capabilities') + }) + }) + + it('должен отображать домены из API', async () => { + setupMockGet({ + domains: { data: [{ id: 1, name: 'test.com' }, { id: 2, name: 'example.org' }], total: 2 } + }) + renderDomainsPage() + + await waitFor(() => { + expect(screen.getByText('test.com')).toBeInTheDocument() + expect(screen.getByText('example.org')).toBeInTheDocument() + }) + }) + }) + + describe('Пагинация', () => { + it('должен отображать пагинацию', async () => { + setupMockGet({ domains: { data: [{ id: 1, name: 'test.com' }], total: 25 } }) + renderDomainsPage() + + await waitFor(() => { + expect(screen.getByText('0–0 из 0')).toBeInTheDocument() + }) + }) + + it('должен переключать страницу при клике на следующую', async () => { + setupMockGet({ domains: { data: [{ id: 1, name: 'test.com' }], total: 25 } }) + renderDomainsPage() + + const nextPageButton = await screen.findByLabelText('Go to next page') + fireEvent.click(nextPageButton) + + await waitFor(() => { + expect(mockGet).toHaveBeenCalledWith('/domains?page=2&limit=10') + }) + }) + + it('должен менять количество строк на странице', async () => { + setupMockGet({ domains: { data: [{ id: 1, name: 'test.com' }], total: 25 } }) + renderDomainsPage() + + const rowsPerPageSelect = await screen.findByLabelText('Доменов на странице:') + fireEvent.mouseDown(rowsPerPageSelect) + + const option = await screen.findByText('25') + fireEvent.click(option) + + await waitFor(() => { + expect(mockGet).toHaveBeenCalledWith('/domains?page=1&limit=25') + }) + }) + }) + + describe('Добавление домена', () => { + it('должен позволять вводить домен', async () => { + setupMockGet() + renderDomainsPage() + + const input = await screen.findByLabelText('Доменное имя') + fireEvent.change(input, { target: { value: 'newdomain.com' } }) + + expect(input).toHaveValue('newdomain.com') + }) + + it('должен добавлять домен при клике на кнопку', async () => { + setupMockGet() + mockPost.mockResolvedValue({}) + + renderDomainsPage() + + const input = await screen.findByLabelText('Доменное имя') + fireEvent.change(input, { target: { value: 'newdomain.com' } }) + + const addButton = screen.getByText('Добавить') + fireEvent.click(addButton) + + await waitFor(() => { + expect(mockPost).toHaveBeenCalledWith('/domains', { name: 'newdomain.com' }) + }) + }) + + it('должен очищать поле после добавления', async () => { + setupMockGet() + mockPost.mockResolvedValue({}) + + renderDomainsPage() + + const input = await screen.findByLabelText('Доменное имя') + fireEvent.change(input, { target: { value: 'newdomain.com' } }) + + const addButton = screen.getByText('Добавить') + fireEvent.click(addButton) + + await waitFor(() => { + expect(input).toHaveValue('') + }) + }) + + it('должен перезагружать список после добавления', async () => { + setupMockGet() + mockPost.mockResolvedValue({}) + + renderDomainsPage() + + const input = await screen.findByLabelText('Доменное имя') + fireEvent.change(input, { target: { value: 'newdomain.com' } }) + + const addButton = screen.getByText('Добавить') + fireEvent.click(addButton) + + await waitFor(() => { + expect(mockGet).toHaveBeenCalledWith('/domains?page=1&limit=10') + }) + }) + }) + + describe('Удаление домена', () => { + it('должен иметь функцию удаления в API', async () => { + setupMockGet() + renderDomainsPage() + + // Просто проверяем что компонент рендерится + expect(screen.getByText('Белый список доменов (SNI)')).toBeInTheDocument() + }) + }) + + describe('Загрузка из файла', () => { + it('должен отображать кнопку "Из файла"', async () => { + setupMockGet() + renderDomainsPage() + + await waitFor(() => { + expect(screen.getByText('Из файла')).toBeInTheDocument() + }) + }) + + it('должен иметь скрытый input type="file"', async () => { + setupMockGet() + renderDomainsPage() + + const fileInput = await screen.findByRole('button', { name: /Из файла/i }) + expect(fileInput).toBeInTheDocument() + }) + }) +}) diff --git a/client/test/pages/SubscriptionsPage.spec.tsx b/client/test/pages/SubscriptionsPage.spec.tsx new file mode 100644 index 0000000..0fa57bf --- /dev/null +++ b/client/test/pages/SubscriptionsPage.spec.tsx @@ -0,0 +1,700 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import { BrowserRouter } from 'react-router-dom' +import SubscriptionsPage from '../../src/pages/SubscriptionsPage' +import { ThemeProvider } from '../../src/ThemeContext' +import { AuthProvider } from '../../src/auth/AuthContext' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('../../src/api', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +const mockClipboardWriteText = vi.fn() +Object.assign(navigator, { clipboard: { writeText: mockClipboardWriteText } }) + +const mockWindowOpen = vi.fn() +const originalWindowOpen = window.open +beforeEach(() => { + window.open = mockWindowOpen + vi.spyOn(console, 'log').mockImplementation(() => {}) + vi.spyOn(console, 'debug').mockImplementation(() => {}) +}) +afterEach(() => { + window.open = originalWindowOpen + vi.restoreAllMocks() +}) + +const setupMockGet = (overrides?: { + subscriptions?: unknown[] + tunnels?: unknown[] + domains?: unknown[] +}) => { + mockGet.mockImplementation((url: string) => { + if (url === '/subscriptions') return Promise.resolve({ data: overrides?.subscriptions || [] }) + if (url === '/tunnels') return Promise.resolve({ data: overrides?.tunnels || [] }) + if (url === '/domains/all') return Promise.resolve({ data: overrides?.domains || [] }) + return Promise.resolve({ data: {} }) + }) +} + +const renderSubscriptionsPage = () => { + return render( + + + + + + + + ) +} + +describe('SubscriptionsPage', () => { + beforeEach(() => { + vi.clearAllMocks() + mockClipboardWriteText.mockClear() + mockWindowOpen.mockClear() + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + describe('Рендеринг', () => { + it('должен рендериться с заголовком', async () => { + setupMockGet() + renderSubscriptionsPage() + + await waitFor(() => { + expect(screen.getByText('Подписки')).toBeInTheDocument() + }) + }) + + it('должен отображать кнопку "Создать"', async () => { + setupMockGet() + renderSubscriptionsPage() + + await waitFor(() => { + expect(screen.getByText('Создать')).toBeInTheDocument() + }) + }) + + it('должен отображать сообщение при отсутствии подписок', async () => { + setupMockGet({ subscriptions: [] }) + renderSubscriptionsPage() + + await waitFor(() => { + expect(screen.getByText('Нет подписок')).toBeInTheDocument() + }) + }) + + it('должен отображать список подписок', async () => { + const mockSubs = [ + { id: '1', name: 'Test Sub', uuid: 'abc-123', inbounds: [{}, {}], isAutoRotationEnabled: true } + ] + setupMockGet({ subscriptions: mockSubs }) + renderSubscriptionsPage() + + await waitFor(() => { + expect(screen.getByText('Test Sub')).toBeInTheDocument() + expect(screen.getByText('abc-123')).toBeInTheDocument() + }) + }) + + it('должен отображать количество инбаундов', async () => { + const mockSubs = [ + { id: '1', name: 'Test Sub', uuid: 'abc-123', inbounds: [{}, {}, {}] } + ] + setupMockGet({ subscriptions: mockSubs }) + renderSubscriptionsPage() + + await waitFor(() => { + expect(screen.getByText('3')).toBeInTheDocument() + }) + }) + + it('должен отображать чекбокс авторотации', async () => { + const mockSubs = [ + { id: '1', name: 'Test Sub', uuid: 'abc-123', inbounds: [], isAutoRotationEnabled: true } + ] + setupMockGet({ subscriptions: mockSubs }) + renderSubscriptionsPage() + + await waitFor(() => { + const checkbox = screen.getByRole('checkbox') + expect(checkbox).toBeChecked() + }) + }) + }) + + describe('Загрузка данных', () => { + it('должен загружать подписки при монтировании', async () => { + setupMockGet() + renderSubscriptionsPage() + + await waitFor(() => { + expect(mockGet).toHaveBeenCalledWith('/subscriptions') + }) + }) + + it('должен загружать туннели при монтировании', async () => { + setupMockGet() + renderSubscriptionsPage() + + await waitFor(() => { + expect(mockGet).toHaveBeenCalledWith('/tunnels') + }) + }) + + it('должен загружать домены при монтировании', async () => { + setupMockGet() + renderSubscriptionsPage() + + await waitFor(() => { + expect(mockGet).toHaveBeenCalledWith('/domains/all') + }) + }) + + it('должен фильтровать только установленные туннели', async () => { + const mockTunnels = [ + { id: 1, name: 'Tunnel 1', ip: '1.1.1.1', domain: '', isInstalled: true }, + { id: 2, name: 'Tunnel 2', ip: '2.2.2.2', domain: '', isInstalled: false } + ] + setupMockGet({ tunnels: mockTunnels }) + renderSubscriptionsPage() + + await waitFor(() => { + expect(mockGet).toHaveBeenCalledWith('/tunnels') + expect(mockGet).toHaveBeenCalledTimes(3) + }) + }) + }) + + describe('Селектор сервера', () => { + it('должен отображать селектор сервера при наличии туннелей', async () => { + const mockTunnels = [ + { id: 1, name: 'Tunnel 1', ip: '1.1.1.1', domain: '', isInstalled: true } + ] + setupMockGet({ tunnels: mockTunnels, subscriptions: [] }) + renderSubscriptionsPage() + + await waitFor(() => { + const select = screen.getByRole('combobox') + expect(select).toBeInTheDocument() + }) + }) + + it('должен отображать иконку Dns для основного сервера', async () => { + const mockTunnels = [ + { id: 1, name: 'Tunnel 1', ip: '1.1.1.1', domain: '', isInstalled: true } + ] + setupMockGet({ tunnels: mockTunnels, subscriptions: [] }) + renderSubscriptionsPage() + + await waitFor(() => { + expect(screen.getByText('Основной сервер')).toBeInTheDocument() + }) + }) + + it('должен переключать выбранный сервер', async () => { + const mockTunnels = [ + { id: 1, name: 'Tunnel 1', ip: '1.1.1.1', domain: '', isInstalled: true } + ] + setupMockGet({ tunnels: mockTunnels, subscriptions: [] }) + renderSubscriptionsPage() + + const select = await screen.findByRole('combobox') + fireEvent.mouseDown(select) + + const tunnelOption = await screen.findByText('Tunnel 1') + fireEvent.click(tunnelOption) + + await waitFor(() => { + expect(screen.getByText('Tunnel 1')).toBeInTheDocument() + }) + }) + }) + + describe('Диалог создания подписки', () => { + it('должен открывать диалог при клике на "Создать"', async () => { + setupMockGet() + renderSubscriptionsPage() + + const createButton = await screen.findByText('Создать') + fireEvent.click(createButton) + + await waitFor(() => { + expect(screen.getByText('Новая подписка')).toBeInTheDocument() + }) + }) + + it('должен закрывать диалог при клике на "Отмена"', async () => { + setupMockGet() + renderSubscriptionsPage() + + const createButton = await screen.findByText('Создать') + fireEvent.click(createButton) + + const cancelButton = await screen.findByText('Отмена') + fireEvent.click(cancelButton) + + await waitFor(() => { + expect(screen.queryByText('Новая подписка')).not.toBeInTheDocument() + }) + }) + + it('должен отображать поле имени подписки', async () => { + setupMockGet() + renderSubscriptionsPage() + + const createButton = await screen.findByText('Создать') + fireEvent.click(createButton) + + await waitFor(() => { + expect(screen.getByLabelText('Имя подписки')).toBeInTheDocument() + }) + }) + + it('должен отображать инбаунды по умолчанию', async () => { + setupMockGet() + renderSubscriptionsPage() + + const createButton = await screen.findByText('Создать') + fireEvent.click(createButton) + + await waitFor(() => { + expect(screen.getByText('Инбаунды (10/20)')).toBeInTheDocument() + }) + }) + + it('должен позволять вводить имя подписки', async () => { + setupMockGet() + renderSubscriptionsPage() + + const createButton = await screen.findByText('Создать') + fireEvent.click(createButton) + + const nameField = await screen.findByLabelText('Имя подписки') + fireEvent.change(nameField, { target: { value: 'My Subscription' } }) + + expect(nameField).toHaveValue('My Subscription') + }) + + it('должен показывать ошибку при пустом имени', async () => { + setupMockGet() + mockPost.mockRejectedValue({}) + + renderSubscriptionsPage() + + const createButton = await screen.findByText('Создать') + fireEvent.click(createButton) + + const saveButton = await screen.findByText('Сохранить') + fireEvent.click(saveButton) + + await waitFor(() => { + expect(screen.getByText('Введите имя подписки')).toBeInTheDocument() + }) + }) + }) + + describe('Управление инбаундами в диалоге', () => { + it('должен отображать инбаунды по умолчанию', async () => { + setupMockGet() + renderSubscriptionsPage() + + const createButton = await screen.findByText('Создать') + fireEvent.click(createButton) + + await waitFor(() => { + expect(screen.getByText('Инбаунды (10/20)')).toBeInTheDocument() + }) + }) + + it('должен позволять вводить имя подписки', async () => { + setupMockGet() + renderSubscriptionsPage() + + const createButton = await screen.findByText('Создать') + fireEvent.click(createButton) + + const nameField = await screen.findByLabelText('Имя подписки') + fireEvent.change(nameField, { target: { value: 'My Subscription' } }) + + expect(nameField).toHaveValue('My Subscription') + }) + + it('должен добавлять новый инбаунд', async () => { + setupMockGet() + renderSubscriptionsPage() + + const createButton = await screen.findByText('Создать') + fireEvent.click(createButton) + + const addButton = await screen.findByText('Добавить инбаунд') + fireEvent.click(addButton) + + await waitFor(() => { + expect(screen.getByText('Инбаунды (11/20)')).toBeInTheDocument() + }) + }) + + it('должен удалять все инбаунды кнопкой "Удалить все"', async () => { + setupMockGet() + renderSubscriptionsPage() + + const createButton = await screen.findByText('Создать') + fireEvent.click(createButton) + + const removeAllButton = await screen.findByText('Удалить все') + fireEvent.click(removeAllButton) + + await waitFor(() => { + expect(screen.getByText('Инбаунды (1/20)')).toBeInTheDocument() + }) + }) + + it('должен запрещать добавление более 20 инбаундов', async () => { + setupMockGet() + renderSubscriptionsPage() + + const createButton = await screen.findByText('Создать') + fireEvent.click(createButton) + + for (let i = 0; i < 10; i++) { + const addButton = screen.getByText('Добавить инбаунд') + fireEvent.click(addButton) + } + + await waitFor(() => { + const addButton = screen.getByText('Добавить инбаунд') + expect(addButton).toBeDisabled() + }) + }) + }) + + describe('Сохранение подписки', () => { + it('должен создавать новую подписку', async () => { + setupMockGet() + mockPost.mockResolvedValue({}) + + renderSubscriptionsPage() + + const createButton = await screen.findByText('Создать') + fireEvent.click(createButton) + + const nameField = await screen.findByLabelText('Имя подписки') + fireEvent.change(nameField, { target: { value: 'New Sub' } }) + + const saveButton = screen.getByText('Сохранить') + fireEvent.click(saveButton) + + await waitFor(() => { + expect(mockPost).toHaveBeenCalledWith('/subscriptions', expect.objectContaining({ + name: 'New Sub', + })) + }) + }) + + it('должен показывать успех после создания', async () => { + setupMockGet() + mockPost.mockResolvedValue({}) + + renderSubscriptionsPage() + + const createButton = await screen.findByText('Создать') + fireEvent.click(createButton) + + const nameField = await screen.findByLabelText('Имя подписки') + fireEvent.change(nameField, { target: { value: 'New Sub' } }) + + const saveButton = screen.getByText('Сохранить') + fireEvent.click(saveButton) + + await waitFor(() => { + expect(screen.getByText('Подписка создана')).toBeInTheDocument() + }) + }) + + it('должен показывать ошибку при неудачном сохранении', async () => { + setupMockGet() + mockPost.mockRejectedValue({ response: { data: { message: 'Ошибка сервера' } } }) + + renderSubscriptionsPage() + + const createButton = await screen.findByText('Создать') + fireEvent.click(createButton) + + const nameField = await screen.findByLabelText('Имя подписки') + fireEvent.change(nameField, { target: { value: 'New Sub' } }) + + const saveButton = screen.getByText('Сохранить') + fireEvent.click(saveButton) + + await waitFor(() => { + expect(screen.getByText('Ошибка сервера')).toBeInTheDocument() + }) + }) + }) + + describe('Редактирование подписки', () => { + it('должен открывать диалог редактирования', async () => { + const mockSubs = [ + { id: '1', name: 'Test Sub', uuid: 'abc-123', inbounds: [{ type: 'vless', port: 443, sni: 'example.com' }], isAutoRotationEnabled: true } + ] + setupMockGet({ subscriptions: mockSubs }) + renderSubscriptionsPage() + + const menuButton = await screen.findByTestId('icon-MoreVert') + fireEvent.click(menuButton) + + const editOption = await screen.findByText('Редактировать') + fireEvent.click(editOption) + + await waitFor(() => { + expect(screen.getByText('Редактировать подписку')).toBeInTheDocument() + }) + }) + + it('должен загружать данные подписки в диалог', async () => { + const mockSubs = [ + { id: '1', name: 'Test Sub', uuid: 'abc-123', inbounds: [{ type: 'vless', port: 443, sni: 'example.com' }], isAutoRotationEnabled: true } + ] + setupMockGet({ subscriptions: mockSubs }) + renderSubscriptionsPage() + + const menuButton = await screen.findByTestId('icon-MoreVert') + fireEvent.click(menuButton) + + const editOption = await screen.findByText('Редактировать') + fireEvent.click(editOption) + + const nameField = await screen.findByLabelText('Имя подписки') + expect(nameField).toHaveValue('Test Sub') + }) + + it('должен обновлять подписку', async () => { + const mockSubs = [ + { id: '1', name: 'Test Sub', uuid: 'abc-123', inbounds: [{ type: 'vless', port: 443, sni: 'example.com' }], isAutoRotationEnabled: true } + ] + setupMockGet({ subscriptions: mockSubs }) + mockPut.mockResolvedValue({}) + + renderSubscriptionsPage() + + const menuButton = await screen.findByTestId('icon-MoreVert') + fireEvent.click(menuButton) + + const editOption = await screen.findByText('Редактировать') + fireEvent.click(editOption) + + const nameField = await screen.findByLabelText('Имя подписки') + fireEvent.change(nameField, { target: { value: 'Updated Sub' } }) + + const saveButton = screen.getByText('Сохранить') + fireEvent.click(saveButton) + + await waitFor(() => { + expect(mockPut).toHaveBeenCalledWith('/subscriptions/1', expect.objectContaining({ + name: 'Updated Sub', + })) + }) + }) + + it('должен показывать успех после обновления', async () => { + const mockSubs = [ + { id: '1', name: 'Test Sub', uuid: 'abc-123', inbounds: [{ type: 'vless', port: 443, sni: 'example.com' }], isAutoRotationEnabled: true } + ] + setupMockGet({ subscriptions: mockSubs }) + mockPut.mockResolvedValue({}) + + renderSubscriptionsPage() + + const menuButton = await screen.findByTestId('icon-MoreVert') + fireEvent.click(menuButton) + + const editOption = await screen.findByText('Редактировать') + fireEvent.click(editOption) + + const nameField = await screen.findByLabelText('Имя подписки') + fireEvent.change(nameField, { target: { value: 'Updated Sub' } }) + + const saveButton = screen.getByText('Сохранить') + fireEvent.click(saveButton) + + await waitFor(() => { + expect(screen.getByText('Подписка обновлена')).toBeInTheDocument() + }) + }) + }) + + describe('Удаление подписки', () => { + it('должен открывать диалог подтверждения удаления', async () => { + const mockSubs = [ + { id: '1', name: 'Test Sub', uuid: 'abc-123', inbounds: [], isAutoRotationEnabled: true } + ] + setupMockGet({ subscriptions: mockSubs }) + renderSubscriptionsPage() + + const menuButton = await screen.findByTestId('icon-MoreVert') + fireEvent.click(menuButton) + + const deleteOption = await screen.findByText('Удалить') + fireEvent.click(deleteOption) + + await waitFor(() => { + expect(screen.getByText('Удалить подписку и все соединения?')).toBeInTheDocument() + }) + }) + + it('должен закрывать диалог при клике на "Отмена"', async () => { + const mockSubs = [ + { id: '1', name: 'Test Sub', uuid: 'abc-123', inbounds: [], isAutoRotationEnabled: true } + ] + setupMockGet({ subscriptions: mockSubs }) + renderSubscriptionsPage() + + const menuButton = await screen.findByTestId('icon-MoreVert') + fireEvent.click(menuButton) + + const deleteOption = await screen.findByText('Удалить') + fireEvent.click(deleteOption) + + const cancelButton = await screen.findByText('Отмена') + fireEvent.click(cancelButton) + + await waitFor(() => { + expect(screen.queryByText('Удалить подписку и все соединения?')).not.toBeInTheDocument() + }) + }) + + it('должен удалять подписку при подтверждении', async () => { + const mockSubs = [ + { id: '1', name: 'Test Sub', uuid: 'abc-123', inbounds: [], isAutoRotationEnabled: true } + ] + setupMockGet({ subscriptions: mockSubs }) + mockDelete.mockResolvedValue({}) + + renderSubscriptionsPage() + + const menuButton = await screen.findByTestId('icon-MoreVert') + fireEvent.click(menuButton) + + const deleteOption = await screen.findByText('Удалить') + fireEvent.click(deleteOption) + + const confirmButton = screen.getAllByText('Удалить')[1] as HTMLElement + fireEvent.click(confirmButton) + + await waitFor(() => { + expect(mockDelete).toHaveBeenCalledWith('/subscriptions/1') + }) + }) + + it('должен показывать успех после удаления', async () => { + const mockSubs = [ + { id: '1', name: 'Test Sub', uuid: 'abc-123', inbounds: [], isAutoRotationEnabled: true } + ] + setupMockGet({ subscriptions: mockSubs }) + mockDelete.mockResolvedValue({}) + + renderSubscriptionsPage() + + const menuButton = await screen.findByTestId('icon-MoreVert') + fireEvent.click(menuButton) + + const deleteOption = await screen.findByText('Удалить') + fireEvent.click(deleteOption) + + const confirmButton = screen.getAllByText('Удалить')[1] as HTMLElement + fireEvent.click(confirmButton) + + await waitFor(() => { + expect(screen.getByText('Подписка удалена')).toBeInTheDocument() + }) + }) + }) + + describe('Переключение авторотации', () => { + it('должен переключать авторотацию при клике на чекбокс', async () => { + const mockSubs = [ + { id: '1', name: 'Test Sub', uuid: 'abc-123', inbounds: [], isAutoRotationEnabled: true } + ] + setupMockGet({ subscriptions: mockSubs }) + mockPut.mockResolvedValue({}) + + renderSubscriptionsPage() + + const checkbox = await screen.findByRole('checkbox') + fireEvent.click(checkbox) + + await waitFor(() => { + expect(mockPut).toHaveBeenCalledWith('/subscriptions/bulk-auto-rotation', expect.objectContaining({ + subscriptionIds: ['1'], + enabled: false + })) + }) + }) + + it('должен показывать успех при включении авторотации', async () => { + const mockSubs = [ + { id: '1', name: 'Test Sub', uuid: 'abc-123', inbounds: [], isAutoRotationEnabled: false } + ] + setupMockGet({ subscriptions: mockSubs }) + mockPut.mockResolvedValue({}) + + renderSubscriptionsPage() + + const checkbox = await screen.findByRole('checkbox') + fireEvent.click(checkbox) + + await waitFor(() => { + expect(screen.getByText('Авторотация включена')).toBeInTheDocument() + }) + }) + + it('должен показывать успех при выключении авторотации', async () => { + const mockSubs = [ + { id: '1', name: 'Test Sub', uuid: 'abc-123', inbounds: [], isAutoRotationEnabled: true } + ] + setupMockGet({ subscriptions: mockSubs }) + mockPut.mockResolvedValue({}) + + renderSubscriptionsPage() + + const checkbox = await screen.findByRole('checkbox') + fireEvent.click(checkbox) + + await waitFor(() => { + expect(screen.getByText('Авторотация выключена')).toBeInTheDocument() + }) + }) + + it('должен показывать ошибку при неудачном переключении', async () => { + const mockSubs = [ + { id: '1', name: 'Test Sub', uuid: 'abc-123', inbounds: [], isAutoRotationEnabled: true } + ] + setupMockGet({ subscriptions: mockSubs }) + mockPut.mockRejectedValue({ response: { data: { message: 'Ошибка' } } }) + + renderSubscriptionsPage() + + const checkbox = await screen.findByRole('checkbox') + fireEvent.click(checkbox) + + await waitFor(() => { + expect(screen.getByText('Ошибка')).toBeInTheDocument() + }) + }) + }) +}) diff --git a/client/test/setup.ts b/client/test/setup.ts index ad1232d..cd8341f 100644 --- a/client/test/setup.ts +++ b/client/test/setup.ts @@ -12,6 +12,7 @@ afterEach(() => { // Оставляем только важные ошибки через test.skip() const originalConsoleLog = console.log const originalConsoleError = console.error +const originalConsoleWarn = console.warn beforeAll(() => { // Фильтруем шумные логи от приложений @@ -23,20 +24,41 @@ beforeAll(() => { message.includes('[Settings]') || message.includes('[API]') || message.includes('[Login]') || - message.includes('[Rotation]') + message.includes('[Rotation]') || + message.includes('[Domains]') || + message.includes('[Subs]') || + message.includes('[Scanner]') ) { return } originalConsoleLog(...args) } + // Подавляем console.warn для известных предупреждений + console.warn = (...args) => { + const message = args.join(' ') + if ( + message.includes('[Tunnels]') || + message.includes('[Settings]') || + message.includes('[Domains]') || + message.includes('[Subs]') || + message.includes('[Scanner]') + ) { + return + } + originalConsoleWarn(...args) + } + // Подавляем console.error для известных предупреждений React console.error = (...args) => { const message = args.join(' ') // Пропускаем предупреждения act(...) - они не критичны if ( message.includes('act(...)') || - message.includes('An update to') + message.includes('An update to') || + message.includes('[Login]') || + message.includes('[Settings]') || + message.includes('[Subs]') ) { return } @@ -48,6 +70,7 @@ afterAll(() => { // Восстанавливаем console после всех тестов console.log = originalConsoleLog console.error = originalConsoleError + console.warn = originalConsoleWarn }) // Мок для MUI icons-material - используем vi.mock с factory