Files
grendervill 642d0f720e 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
2026-09-19 18:20:59 +03:00

66 lines
2.0 KiB
TypeScript

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');
});
});