From 6afa8f268e51f10778fde58bd14e2157bd2c1fb7 Mon Sep 17 00:00:00 2001 From: iqubik Date: Sun, 29 Mar 2026 23:49:43 +0300 Subject: [PATCH] 75+ fronttest --- .gitignore | 1 + client/test/pages/DomainsPage.spec.tsx | 175 +++++++++++++++++++ client/test/pages/SubscriptionsPage.spec.tsx | 124 +++++++++++++ 3 files changed, 300 insertions(+) diff --git a/.gitignore b/.gitignore index 0675713..0686b91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ checker/ client/.env +client/coverage/ diff --git a/client/test/pages/DomainsPage.spec.tsx b/client/test/pages/DomainsPage.spec.tsx index 37b2695..56a492a 100644 --- a/client/test/pages/DomainsPage.spec.tsx +++ b/client/test/pages/DomainsPage.spec.tsx @@ -444,5 +444,180 @@ describe('DomainsPage', () => { })) }) }) + + it('должен очищать результаты сканирования', async () => { + setupMockGet({ + capabilities: { scannerAvailable: true }, + settings: { xui_ip: '1.2.3.4' } + }) + mockPost.mockResolvedValue({ data: { runId: '123', foundCount: 2, domains: ['a.com', 'b.com'] } }) + + renderDomainsPage() + + const scanButton = await screen.findByText('Сканировать') + fireEvent.click(scanButton) + + const clearButton = await screen.findByText('Очистить предварительный') + fireEvent.click(clearButton) + + await waitFor(() => { + expect(mockGet).toHaveBeenCalled() + }) + }) + + it('должен изменять настройки сканирования', async () => { + setupMockGet({ + capabilities: { scannerAvailable: true }, + settings: { xui_ip: '1.2.3.4' } + }) + + renderDomainsPage() + + const secondsInput = await screen.findByLabelText('Секунд скана') + fireEvent.change(secondsInput, { target: { value: '120' } }) + expect(secondsInput).toHaveValue(120) + + const threadInput = screen.getByLabelText('Потоков') + fireEvent.change(threadInput, { target: { value: '8' } }) + expect(threadInput).toHaveValue(8) + + const timeoutInput = screen.getByLabelText('Таймаут, сек') + fireEvent.change(timeoutInput, { target: { value: '10' } }) + expect(timeoutInput).toHaveValue(10) + }) + + it('должен разворачивать/сворачивать аккордеон сканера', async () => { + setupMockGet({ + capabilities: { scannerAvailable: true } + }) + + renderDomainsPage() + + const accordionSummary = await screen.findByText('Автопоиск SNI (backend scanner)') + + fireEvent.click(accordionSummary) + + await waitFor(() => { + const secondsInput = screen.getByLabelText('Секунд скана') + expect(secondsInput).toBeInTheDocument() + }) + }) + + it('должен показывать сообщение при пустом списке кандидатов после сканирования', async () => { + setupMockGet({ + capabilities: { scannerAvailable: true }, + settings: { xui_ip: '1.2.3.4' } + }) + mockPost.mockResolvedValue({ data: { runId: '123', foundCount: 0, domains: [] } }) + + renderDomainsPage() + + const scanButton = await screen.findByText('Сканировать') + fireEvent.click(scanButton) + + await waitFor(() => { + expect(screen.getByText('Домены не найдены')).toBeInTheDocument() + }) + }) + + it('должен сохранять состояние сканера в localStorage', async () => { + const mockSetItem = vi.fn() + const mockLocalStorage = { + getItem: vi.fn().mockReturnValue(null), + setItem: mockSetItem, + removeItem: vi.fn(), + clear: vi.fn(), + } + Object.defineProperty(window, 'localStorage', { value: mockLocalStorage, writable: true }) + + setupMockGet({ + capabilities: { scannerAvailable: true }, + settings: { xui_ip: '1.2.3.4' } + }) + + renderDomainsPage() + + const addrInput = await screen.findByLabelText('IP/домен VPS') + fireEvent.change(addrInput, { target: { value: 'new-addr.com' } }) + + await waitFor(() => { + expect(mockSetItem).toHaveBeenCalledWith( + 'domains_scan_state_v1', + expect.any(String) + ) + }) + }) + + it('должен показывать кнопку удаления для домена в основном списке', async () => { + setupMockGet({ + domains: { data: [{ id: 1, name: 'test.com' }], total: 1 } + }) + + renderDomainsPage() + + const deleteButtons = await screen.findAllByTestId('icon-Delete') + expect(deleteButtons.length).toBeGreaterThan(0) + }) + + it('должен подтверждать удаление всех доменов', async () => { + setupMockGet({ + domains: { data: [{ id: 1, name: 'test.com' }], total: 1 } + }) + mockDelete.mockResolvedValue({}) + + renderDomainsPage() + + const deleteAllButton = await screen.findByText('Удалить все') + fireEvent.click(deleteAllButton) + + const confirmButton = await screen.findByText('Удалить') + fireEvent.click(confirmButton) + + await waitFor(() => { + expect(mockDelete).toHaveBeenCalledWith('/domains/all') + }) + }) + + it('должен показывать успех после удаления всех доменов', async () => { + setupMockGet({ + domains: { data: [{ id: 1, name: 'test.com' }], total: 1 } + }) + mockDelete.mockResolvedValue({}) + + renderDomainsPage() + + const deleteAllButton = await screen.findByText('Удалить все') + fireEvent.click(deleteAllButton) + + const confirmButton = await screen.findByText('Удалить') + fireEvent.click(confirmButton) + + await waitFor(() => { + const alerts = screen.getAllByRole('alert') + const successAlert = alerts.find(alert => alert.textContent?.includes('Все домены удалены')) + expect(successAlert).toBeInTheDocument() + }) + }) + + it('должен показывать ошибку при неудачном удалении всех доменов', async () => { + setupMockGet({ + domains: { data: [{ id: 1, name: 'test.com' }], total: 1 } + }) + mockDelete.mockRejectedValue({ response: { status: 500 } }) + + renderDomainsPage() + + const deleteAllButton = await screen.findByText('Удалить все') + fireEvent.click(deleteAllButton) + + const confirmButton = await screen.findByText('Удалить') + fireEvent.click(confirmButton) + + await waitFor(() => { + const alerts = screen.getAllByRole('alert') + const errorAlert = alerts.find(alert => alert.textContent?.includes('Ошибка удаления')) + expect(errorAlert).toBeInTheDocument() + }) + }) }) }) diff --git a/client/test/pages/SubscriptionsPage.spec.tsx b/client/test/pages/SubscriptionsPage.spec.tsx index 0fa57bf..1feb936 100644 --- a/client/test/pages/SubscriptionsPage.spec.tsx +++ b/client/test/pages/SubscriptionsPage.spec.tsx @@ -697,4 +697,128 @@ describe('SubscriptionsPage', () => { }) }) }) + + describe('Утилитные функции', () => { + it('должен генерировать уникальный ID', async () => { + setupMockGet() + renderSubscriptionsPage() + + const createButton = await screen.findByText('Создать') + fireEvent.click(createButton) + + // Проверяем что ID генерируется (просто что форма открывается) + 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', link: 'vless://test' }], isAutoRotationEnabled: true } + ] + setupMockGet({ subscriptions: mockSubs }) + + renderSubscriptionsPage() + + const menuButton = await screen.findByTestId('icon-MoreVert') + fireEvent.click(menuButton) + + const linksOption = await screen.findByText('Показать конфиги') + fireEvent.click(linksOption) + + 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', link: 'vless://test' }], isAutoRotationEnabled: true } + ] + setupMockGet({ subscriptions: mockSubs }) + + renderSubscriptionsPage() + + const menuButton = await screen.findByTestId('icon-MoreVert') + fireEvent.click(menuButton) + + const linksOption = await screen.findByText('Показать конфиги') + fireEvent.click(linksOption) + + const closeButton = await screen.findByText('Закрыть') + fireEvent.click(closeButton) + + await waitFor(() => { + expect(screen.queryByText('Активные ссылки')).not.toBeInTheDocument() + }) + }) + + it('должен копировать ссылку при клике на кнопку копирования', async () => { + const mockSubs = [ + { id: '1', name: 'Test Sub', uuid: 'abc-123', inbounds: [{ type: 'vless', port: 443, sni: 'example.com', link: 'vless://test' }], isAutoRotationEnabled: true } + ] + setupMockGet({ subscriptions: mockSubs }) + mockClipboardWriteText.mockResolvedValue(undefined) + + renderSubscriptionsPage() + + const menuButton = await screen.findByTestId('icon-MoreVert') + fireEvent.click(menuButton) + + const linksOption = await screen.findByText('Показать конфиги') + fireEvent.click(linksOption) + + const copyButton = await screen.findByText('Копировать все') + fireEvent.click(copyButton) + + await waitFor(() => { + expect(mockClipboardWriteText).toHaveBeenCalled() + }) + }) + + it('должен открывать ссылку в новой вкладке при клике на иконку открытия', async () => { + const mockSubs = [ + { id: '1', name: 'Test Sub', uuid: 'abc-123', inbounds: [{ type: 'vless', port: 443, sni: 'example.com', link: 'vless://test' }], isAutoRotationEnabled: true } + ] + setupMockGet({ subscriptions: mockSubs }) + + renderSubscriptionsPage() + + const menuButton = await screen.findByTestId('icon-MoreVert') + fireEvent.click(menuButton) + + const linksOption = await screen.findByText('Показать конфиги') + fireEvent.click(linksOption) + + const openButtons = await screen.findAllByTestId('icon-OpenInNew') + if (openButtons.length > 0) { + fireEvent.click(openButtons[0]) + } + + await waitFor(() => { + expect(mockWindowOpen).toHaveBeenCalled() + }) + }) + + 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 linksOption = await screen.findByText('Показать конфиги') + fireEvent.click(linksOption) + + await waitFor(() => { + expect(screen.getByText('Нет активных ссылок (ждите ротации)')).toBeInTheDocument() + }) + }) + }) })