import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; import { AccountPage } from './AccountPage'; import { api } from '../api/client'; import { useAuthStore } from '../store/useStore'; vi.mock('qrcode', () => ({ default: { toDataURL: () => Promise.resolve('data:image/png;base64,qr') }, })); vi.mock('../api/client', () => ({ api: { getPlans: vi.fn(), checkout: vi.fn(), getMe: vi.fn(), getSubscription: vi.fn(), getDevices: vi.fn(), deleteDevice: vi.fn(), login: vi.fn(), signup: vi.fn(), }, ApiError: class ApiError extends Error { constructor(m: string, public status: number) { super(m); } }, setStoredTokens: vi.fn(), clearStoredTokens: vi.fn(), })); function renderAccount(authed = true) { if (authed) { useAuthStore.setState({ access: 'a', refresh: 'r', expiresAt: 9999999999, user: { username: 'alice', telegram_id: 'alice_id', referal_code: 'c' }, remember: true, }); } else { useAuthStore.setState({ access: null, refresh: null, expiresAt: null, user: null, remember: true, }); } return render( } /> login page} /> landing} /> , ); } const ME = { username: 'alice', telegram_id: 'alice_id', referal_code: 'c' }; const SUB = { devices: 6, expires_at: '2026-03-14T00:00:00Z', addon_ids: [], subscription_link: 'link', has_subscription: true, }; const DEVICES = [ { os: 'macos', model: 'MacBook Pro', client: 'Malenia', hwid: 'h1' }, { os: 'ios', model: 'iPhone 15', client: 'Malenia', hwid: 'h2' }, ]; describe('AccountPage', () => { beforeEach(() => { vi.mocked(api.getMe).mockReset(); vi.mocked(api.getSubscription).mockReset(); vi.mocked(api.getDevices).mockReset(); vi.mocked(api.deleteDevice).mockReset(); }); afterEach(() => vi.restoreAllMocks()); it('redirects to /login when not authenticated', async () => { renderAccount(false); await waitFor(() => expect(screen.getByText('login page')).toBeInTheDocument()); }); it('renders hero username and stat strip', async () => { vi.mocked(api.getMe).mockResolvedValue(ME); vi.mocked(api.getSubscription).mockResolvedValue(SUB); vi.mocked(api.getDevices).mockResolvedValue(DEVICES); renderAccount(); expect(await screen.findByText('alice')).toBeInTheDocument(); expect(screen.getByText('@alice_id')).toBeInTheDocument(); expect(screen.getAllByText(/iPhone 15/)).toHaveLength(1); }); it('does not render a subscription status before subscription data loads', async () => { vi.mocked(api.getMe).mockResolvedValue(ME); vi.mocked(api.getSubscription).mockResolvedValue(SUB); vi.mocked(api.getDevices).mockResolvedValue(DEVICES); renderAccount(); expect(screen.queryByText(/активная подписка отсутствует/i)).not.toBeInTheDocument(); expect(await screen.findByText(/подписка до/i)).toBeInTheDocument(); }); it('renders the subscription link and copy action', async () => { vi.mocked(api.getMe).mockResolvedValue(ME); vi.mocked(api.getSubscription).mockResolvedValue(SUB); vi.mocked(api.getDevices).mockResolvedValue(DEVICES); renderAccount(); expect(await screen.findByText(SUB.subscription_link)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /копировать ссылку подписки/i })).toBeEnabled(); }); it('renders a referral link with copy and QR actions', async () => { vi.mocked(api.getMe).mockResolvedValue(ME); vi.mocked(api.getSubscription).mockResolvedValue(SUB); vi.mocked(api.getDevices).mockResolvedValue(DEVICES); renderAccount(); const link = `${window.location.origin}/?ref=c`; expect(await screen.findByText(link)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /показать qr-код реферальной ссылки/i })).toBeEnabled(); }); it('opens and closes the subscription QR code dialog', async () => { vi.mocked(api.getMe).mockResolvedValue(ME); vi.mocked(api.getSubscription).mockResolvedValue(SUB); vi.mocked(api.getDevices).mockResolvedValue(DEVICES); const user = userEvent.setup(); renderAccount(); await screen.findByText(SUB.subscription_link); await user.click(screen.getByRole('button', { name: /показать qr-код ссылки подписки/i })); expect(await screen.findByRole('dialog', { name: /откройте на устройстве/i })).toBeInTheDocument(); expect(await screen.findByAltText(/qr-код ссылки подписки/i)).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: /закрыть/i })); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); it('renders empty state when no devices', async () => { vi.mocked(api.getMe).mockResolvedValue(ME); vi.mocked(api.getSubscription).mockResolvedValue(SUB); vi.mocked(api.getDevices).mockResolvedValue([]); renderAccount(); expect(await screen.findByText(/нет подключённых устройств/i)).toBeInTheDocument(); }); it('opens confirmation modal and deletes device on confirm', async () => { vi.mocked(api.getMe).mockResolvedValue(ME); vi.mocked(api.getSubscription).mockResolvedValue(SUB); vi.mocked(api.getDevices).mockResolvedValue(DEVICES); vi.mocked(api.deleteDevice).mockResolvedValue({ success: true }); const user = userEvent.setup(); renderAccount(); await screen.findByText(/iPhone 15/); await user.click(screen.getByRole('button', { name: /удалить iPhone 15/i })); expect(await screen.findByText(/удалить это устройство\?/i)).toBeInTheDocument(); const confirmBtns = screen.getAllByRole('button', { name: /^удалить$/i }); await user.click(confirmBtns[confirmBtns.length - 1]!); await waitFor(() => expect(api.deleteDevice).toHaveBeenCalledWith('h2')); await waitFor(() => expect(screen.queryByText(/iPhone 15/)).not.toBeInTheDocument()); }); it('signs out and navigates to landing', async () => { vi.mocked(api.getMe).mockResolvedValue(ME); vi.mocked(api.getSubscription).mockResolvedValue(SUB); vi.mocked(api.getDevices).mockResolvedValue(DEVICES); const user = userEvent.setup(); renderAccount(); await screen.findByText(/alice/); await user.click(screen.getByRole('button', { name: /выйти/i })); await waitFor(() => expect(screen.getByText('landing')).toBeInTheDocument()); expect(useAuthStore.getState().access).toBeNull(); }); });