This commit is contained in:
2026-08-10 12:01:03 +07:00
commit 265b1202ad
95 changed files with 27718 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
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('../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(
<MemoryRouter initialEntries={['/account']}>
<Routes>
<Route path="/account" element={<AccountPage />} />
<Route path="/login" element={<div>login page</div>} />
<Route path="/" element={<div>landing</div>} />
</Routes>
</MemoryRouter>,
);
}
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 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();
});
});