feat(web): заготовка веб-клиента на Vite + React 19 + TypeScript strict
- стек по §5.2: React 19, React Router, TanStack Query, Zustand, Tailwind CSS 4, i18next (ru/en), Vitest + Testing Library - дизайн-токены §3 docs/client-web.md (тёмная тема по умолчанию, светлая, accent) - типизированный API-клиент §8.1: единый формат ошибок, ApiError с кодом - App: экран состояния фундамента с readz/meta, локализация ошибок по code - тесты: клиент API, форматирование, рендер App (успех/ошибка кода/сеть) - eslint + prettier + tsc strict; initial JS 97.6 KB gzip
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ApiError, request } from '@/api/client';
|
||||
|
||||
function jsonResponse(body: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
describe('api client', () => {
|
||||
it('returns the parsed payload on success', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ api_version: 'v1' }));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
await expect(request<{ api_version: string }>('/meta')).resolves.toEqual({
|
||||
api_version: 'v1',
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/v1/meta',
|
||||
expect.objectContaining({ method: 'GET', credentials: 'same-origin' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('maps the server error envelope to ApiError', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
jsonResponse({ error: { code: 'perm.denied', message: 'no access' } }, 403),
|
||||
),
|
||||
);
|
||||
|
||||
await expect(request('/guilds/1')).rejects.toMatchObject({
|
||||
name: 'ApiError',
|
||||
code: 'perm.denied',
|
||||
status: 403,
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to internal.error when the body is not an envelope', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue(new Response('gateway timeout', { status: 504 })),
|
||||
);
|
||||
|
||||
const error = await request('/meta').catch((reason: unknown) => reason);
|
||||
expect(error).toBeInstanceOf(ApiError);
|
||||
expect((error as ApiError).code).toBe('internal.error');
|
||||
expect((error as ApiError).status).toBe(504);
|
||||
});
|
||||
|
||||
it('sends a json body for write requests', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ ok: true }));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
await request('/guilds', { method: 'POST', body: { name: 'test' } });
|
||||
|
||||
const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
|
||||
expect(init.body).toBe('{"name":"test"}');
|
||||
expect((init.headers as Record<string, string>)['Content-Type']).toBe('application/json');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user