import { render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import App, { copyTextToClipboard } from './App'; import { fallbackDashboardSnapshot } from './data/mockDashboard'; import { MockWebSocket } from './test/setup'; async function loginIntoPanel(user: ReturnType) { await user.type(screen.getByLabelText(/login/i), 'admin'); await user.type(screen.getByLabelText(/password/i), 'proxy-ui-demo'); await user.click(screen.getByRole('button', { name: /open panel/i })); } function mockClipboardWriteText(writeText: ReturnType) { Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText, }, }); } beforeEach(() => { document.documentElement.dataset.theme = ''; window.sessionStorage.clear(); window.localStorage.clear(); window.history.replaceState(null, '', '/'); }); describe('App login gate', () => { it('rejects wrong hardcoded credentials and keeps the panel locked', async () => { const user = userEvent.setup(); render(); await user.type(screen.getByLabelText(/login/i), 'admin'); await user.type(screen.getByLabelText(/password/i), 'wrong-pass'); await user.click(screen.getByRole('button', { name: /open panel/i })); expect(screen.getByText(/wrong panel credentials/i)).toBeInTheDocument(); expect(screen.queryByRole('navigation', { name: /primary/i })).not.toBeInTheDocument(); }); it('unlocks the panel with the configured credentials', async () => { const user = userEvent.setup(); render(); await loginIntoPanel(user); expect(screen.getByRole('navigation', { name: /primary/i })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: /3proxy ui/i })).toBeInTheDocument(); }); it('restores the panel session from sessionStorage after a remount', async () => { const user = userEvent.setup(); const firstRender = render(); await loginIntoPanel(user); expect(screen.getByRole('navigation', { name: /primary/i })).toBeInTheDocument(); firstRender.unmount(); render(); expect(screen.getByRole('navigation', { name: /primary/i })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /open panel/i })).not.toBeInTheDocument(); }); it('stores panel language in localStorage and restores it after a remount', async () => { const user = userEvent.setup(); const firstRender = render(); await loginIntoPanel(user); await user.click(screen.getByRole('button', { name: /settings/i })); await user.selectOptions(screen.getByLabelText(/panel language/i), 'ru'); expect(screen.getByRole('button', { name: /панель/i })).toBeInTheDocument(); firstRender.unmount(); render(); expect(screen.getByRole('button', { name: /панель/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /^настройки$/i })).toBeInTheDocument(); }); it('stores panel theme in localStorage and restores it after a remount', async () => { const user = userEvent.setup(); const firstRender = render(); await loginIntoPanel(user); await user.click(screen.getByRole('button', { name: /settings/i })); await user.selectOptions(screen.getByLabelText(/panel style/i), 'dark'); expect(document.documentElement.dataset.theme).toBe('dark'); firstRender.unmount(); render(); expect(document.documentElement.dataset.theme).toBe('dark'); }); it('keeps tab navigation in the hash and restores the active tab after remount', async () => { const user = userEvent.setup(); const firstRender = render(); await loginIntoPanel(user); await user.click(screen.getByRole('button', { name: /users/i })); expect(window.location.hash).toBe('#users'); expect(screen.getByRole('button', { name: /new user/i })).toBeInTheDocument(); firstRender.unmount(); render(); expect(window.location.hash).toBe('#users'); expect(screen.getByRole('button', { name: /new user/i })).toBeInTheDocument(); }); it('can stop the runtime from the dashboard', async () => { const user = userEvent.setup(); render(); await loginIntoPanel(user); await user.click(screen.getByRole('button', { name: /stop/i })); expect(screen.getAllByText(/^idle$/i).length).toBeGreaterThan(0); }); it('shows online data in the users table and keeps proxy copy inside actions', async () => { const user = userEvent.setup(); render(); await loginIntoPanel(user); await user.click(screen.getByRole('button', { name: /users/i })); expect(screen.getByRole('columnheader', { name: /online/i })).toBeInTheDocument(); expect(screen.queryByRole('columnheader', { name: /^proxy$/i })).not.toBeInTheDocument(); expect(screen.getAllByRole('button', { name: /copy proxy link/i }).length).toBeGreaterThan(0); expect(screen.getAllByText(/^now$/i).length).toBeGreaterThan(0); }); it('copies the proxy link via the Clipboard API when available', async () => { const clipboardWriteText = vi.fn().mockResolvedValue(undefined); mockClipboardWriteText(clipboardWriteText); await copyTextToClipboard('socks5://night-shift:kettle!23@edge.example.net:1080'); expect(clipboardWriteText).toHaveBeenCalledWith('socks5://night-shift:kettle!23@edge.example.net:1080'); }); it('falls back to execCommand copy when the Clipboard API is unavailable', async () => { Object.defineProperty(navigator, 'clipboard', { configurable: true, value: undefined, }); const execCommand = vi.fn(() => true); Object.defineProperty(document, 'execCommand', { configurable: true, value: execCommand, }); await copyTextToClipboard('socks5://night-shift:kettle!23@edge.example.net:1080'); expect(execCommand).toHaveBeenCalledWith('copy'); }); it('opens add-user flow in a modal and closes it on escape', async () => { const user = userEvent.setup(); render(); await loginIntoPanel(user); await user.click(screen.getByRole('button', { name: /users/i })); await user.click(screen.getByRole('button', { name: /new user/i })); const dialog = screen.getByRole('dialog', { name: /add user/i }); expect(dialog).toBeInTheDocument(); expect(String((screen.getByLabelText(/username/i) as HTMLInputElement).value)).toMatch(/^user-/i); expect(String((screen.getByLabelText(/password/i) as HTMLInputElement).value)).toMatch(/^pw-/i); expect(screen.getByLabelText(/service/i)).toBeInTheDocument(); expect(screen.queryByLabelText(/port/i)).not.toBeInTheDocument(); expect(within(dialog).getByText(/edge\.example\.net:1080/i)).toBeInTheDocument(); await user.keyboard('{Escape}'); expect(screen.queryByRole('dialog', { name: /add user/i })).not.toBeInTheDocument(); }); it('pauses and resumes a user profile from the table', async () => { const user = userEvent.setup(); render(); await loginIntoPanel(user); await user.click(screen.getByRole('button', { name: /users/i })); await user.click(screen.getAllByRole('button', { name: /pause user/i })[0]); expect(screen.getByText(/^paused$/i)).toBeInTheDocument(); expect(screen.getAllByRole('button', { name: /resume user/i })).toHaveLength(1); await user.click(screen.getByRole('button', { name: /resume user/i })); expect(screen.queryByText(/^paused$/i)).not.toBeInTheDocument(); }); it('confirms before deleting a user and removes the row after approval', async () => { const user = userEvent.setup(); render(); await loginIntoPanel(user); await user.click(screen.getByRole('button', { name: /users/i })); expect(screen.getByText(/night-shift/i)).toBeInTheDocument(); await user.click(screen.getAllByRole('button', { name: /^delete user$/i })[0]); const dialog = screen.getByRole('dialog', { name: /delete user/i }); expect(dialog).toBeInTheDocument(); expect(within(dialog).getByText(/remove profile/i)).toBeInTheDocument(); await user.click(within(dialog).getByRole('button', { name: /delete user/i })); expect(screen.queryByText(/night-shift/i)).not.toBeInTheDocument(); expect(screen.queryByRole('dialog', { name: /delete user/i })).not.toBeInTheDocument(); }); it('uses a combined service type field in settings and applies saved proxy endpoint values to the local fallback state', async () => { const user = userEvent.setup(); render(); await loginIntoPanel(user); await user.click(screen.getByRole('button', { name: /settings/i })); expect(screen.getByRole('heading', { name: /panel settings/i })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: /^services$/i })).toBeInTheDocument(); expect(screen.getByLabelText(/proxy endpoint/i)).toBeInTheDocument(); expect(screen.queryByLabelText(/command/i)).not.toBeInTheDocument(); expect(screen.queryByLabelText(/protocol/i)).not.toBeInTheDocument(); expect(screen.getAllByLabelText(/type/i)).toHaveLength(3); expect(screen.queryByRole('option', { name: /admin/i })).not.toBeInTheDocument(); await user.clear(screen.getByLabelText(/proxy endpoint/i)); await user.type(screen.getByLabelText(/proxy endpoint/i), 'gw.example.net'); const firstPortInput = screen.getAllByLabelText(/port/i)[0]; await user.clear(firstPortInput); await user.type(firstPortInput, '1180'); await user.click(screen.getByRole('button', { name: /save settings/i })); await user.click(screen.getByRole('button', { name: /users/i })); expect(screen.getAllByText(/gw\.example\.net:1180/i).length).toBeGreaterThan(0); }); it('opens edit-user flow with existing credentials instead of generating new ones', async () => { const user = userEvent.setup(); render(); await loginIntoPanel(user); await user.click(screen.getByRole('button', { name: /users/i })); await user.click(screen.getAllByRole('button', { name: /edit user/i })[0]); const dialog = screen.getByRole('dialog', { name: /edit user/i }); expect(within(dialog).getByLabelText(/username/i)).toHaveValue('night-shift'); expect(within(dialog).getByLabelText(/password/i)).toHaveValue('kettle!23'); await user.clear(within(dialog).getByLabelText(/username/i)); await user.type(within(dialog).getByLabelText(/username/i), 'night-shift-updated'); await user.click(within(dialog).getByRole('button', { name: /save user/i })); expect(screen.getByText(/night-shift-updated/i)).toBeInTheDocument(); expect(screen.queryByRole('dialog', { name: /edit user/i })).not.toBeInTheDocument(); }); it('does not overwrite dirty system settings when a websocket patch arrives', async () => { const user = userEvent.setup(); render(); await loginIntoPanel(user); await waitFor(() => expect(MockWebSocket.instances.length).toBeGreaterThan(0)); const socket = MockWebSocket.instances[0]; await user.click(screen.getByRole('button', { name: /settings/i })); const endpointInput = screen.getByLabelText(/proxy endpoint/i); await user.clear(endpointInput); await user.type(endpointInput, 'draft.example.net'); socket.emitMessage({ type: 'snapshot.patch', patch: { system: { ...fallbackDashboardSnapshot.system, publicHost: 'server-sync.example.net', }, }, }); expect(screen.getByLabelText(/proxy endpoint/i)).toHaveValue('draft.example.net'); }); it('shows a connection notice and exits the panel after websocket and api retries are exhausted', async () => { vi.useFakeTimers(); MockWebSocket.connectMode = 'close'; window.sessionStorage.setItem( '3proxy-ui-panel-session', JSON.stringify({ token: 'local-ui-fallback', expiresAt: new Date(Date.now() + 60_000).toISOString(), }), ); render(); await vi.advanceTimersByTimeAsync(2_100); expect(screen.getByText(/reconnecting websocket|переподключаем websocket/i)).toBeInTheDocument(); await vi.advanceTimersByTimeAsync(25_000); expect(screen.getByRole('button', { name: /open panel/i })).toBeInTheDocument(); }, 10_000); it('warns before deleting a service and removes linked users after confirmation', async () => { const user = userEvent.setup(); render(); await loginIntoPanel(user); await user.click(screen.getByRole('button', { name: /settings/i })); await user.click(screen.getAllByRole('button', { name: /^remove$/i })[0]); const dialog = screen.getByRole('dialog', { name: /delete service/i }); expect(dialog).toBeInTheDocument(); expect(within(dialog).getByText(/linked users to be removed/i)).toBeInTheDocument(); expect(within(dialog).getByText(/night-shift, ops-east/i)).toBeInTheDocument(); await user.click(within(dialog).getByRole('button', { name: /^remove$/i })); await user.click(screen.getByRole('button', { name: /save settings/i })); await user.click(screen.getByRole('button', { name: /users/i })); expect(screen.queryByText(/night-shift/i)).not.toBeInTheDocument(); expect(screen.queryByText(/ops-east/i)).not.toBeInTheDocument(); }); });