mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
152 lines
4.2 KiB
TypeScript
152 lines
4.2 KiB
TypeScript
|
|
import { render, screen } from '@testing-library/react'
|
|||
|
|
import userEvent from '@testing-library/user-event'
|
|||
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|||
|
|
|
|||
|
|
import { clearSimpleRestApiTokens } from 'infra/simple-rest-api'
|
|||
|
|
import { App } from './app'
|
|||
|
|
import { AppProviders } from './providers/app-providers'
|
|||
|
|
|
|||
|
|
const demoUser = {
|
|||
|
|
id: 'user-admin',
|
|||
|
|
email: 'admin@demo.local',
|
|||
|
|
name: 'Demo Admin',
|
|||
|
|
role: 'admin',
|
|||
|
|
avatarUrl: 'https://i.pravatar.cc/160?img=12'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const demoProduct = {
|
|||
|
|
id: 'product-keyboard',
|
|||
|
|
name: 'Mechanical Keyboard',
|
|||
|
|
slug: 'mechanical-keyboard',
|
|||
|
|
description: 'Mechanical Keyboard is a deterministic demo product used by frontend examples.',
|
|||
|
|
priceCents: 12990,
|
|||
|
|
currency: 'USD',
|
|||
|
|
categoryId: 'category-electronics',
|
|||
|
|
stock: 24,
|
|||
|
|
rating: 4.8,
|
|||
|
|
imageUrl: 'https://picsum.photos/seed/mechanical-keyboard/640/480',
|
|||
|
|
createdAt: '2026-07-10T09:00:00.000Z',
|
|||
|
|
version: 1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Создаёт JSON response, совместимый с generated HttpClient.
|
|||
|
|
*/
|
|||
|
|
const jsonResponse = (body: unknown, status = 200): Response => {
|
|||
|
|
return new Response(JSON.stringify(body), {
|
|||
|
|
status,
|
|||
|
|
headers: { 'Content-Type': 'application/json' }
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Эмулирует минимальный Simple API для app-level smoke-сценария.
|
|||
|
|
*/
|
|||
|
|
const createSimpleApiFetch = () => {
|
|||
|
|
return vi.fn(async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|||
|
|
const url = new URL(String(input))
|
|||
|
|
const method = init?.method ?? 'GET'
|
|||
|
|
|
|||
|
|
if (url.pathname === '/api/v1/auth/login' && method === 'POST') {
|
|||
|
|
return jsonResponse({
|
|||
|
|
data: {
|
|||
|
|
tokens: {
|
|||
|
|
accessToken: 'access-token',
|
|||
|
|
refreshToken: 'refresh-token',
|
|||
|
|
expiresIn: 60,
|
|||
|
|
tokenType: 'Bearer'
|
|||
|
|
},
|
|||
|
|
user: demoUser
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (url.pathname === '/api/v1/products' && method === 'GET') {
|
|||
|
|
return jsonResponse({
|
|||
|
|
data: [demoProduct],
|
|||
|
|
meta: { page: 1, limit: 6, total: 1, totalPages: 1 }
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (url.pathname === '/api/v1/categories' && method === 'GET') {
|
|||
|
|
return jsonResponse({
|
|||
|
|
data: [
|
|||
|
|
{
|
|||
|
|
id: 'category-electronics',
|
|||
|
|
name: 'Electronics',
|
|||
|
|
slug: 'electronics',
|
|||
|
|
productCount: 1
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (url.pathname === '/api/v1/orders' && method === 'GET') {
|
|||
|
|
return jsonResponse({
|
|||
|
|
data: [],
|
|||
|
|
meta: { page: 1, limit: 20, total: 0, totalPages: 0 }
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (url.pathname === '/api/v1/orders' && method === 'POST') {
|
|||
|
|
return jsonResponse(
|
|||
|
|
{
|
|||
|
|
data: {
|
|||
|
|
id: 'order-010',
|
|||
|
|
userId: 'user-admin',
|
|||
|
|
status: 'pending',
|
|||
|
|
items: [
|
|||
|
|
{
|
|||
|
|
productId: demoProduct.id,
|
|||
|
|
productName: demoProduct.name,
|
|||
|
|
quantity: 1,
|
|||
|
|
unitPriceCents: demoProduct.priceCents
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
totalCents: demoProduct.priceCents,
|
|||
|
|
currency: 'USD',
|
|||
|
|
createdAt: '2026-08-10T10:00:00.000Z'
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
201
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return jsonResponse({ code: 'NOT_FOUND', message: 'Not found' }, 404)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
afterEach(() => {
|
|||
|
|
clearSimpleRestApiTokens()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
describe('App', () => {
|
|||
|
|
it('authenticates, loads domain data and checks out a product', async () => {
|
|||
|
|
const user = userEvent.setup()
|
|||
|
|
const fetchMock = createSimpleApiFetch()
|
|||
|
|
vi.stubGlobal('fetch', fetchMock)
|
|||
|
|
|
|||
|
|
render(
|
|||
|
|
<AppProviders>
|
|||
|
|
<App />
|
|||
|
|
</AppProviders>
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
await user.click(await screen.findByRole('button', { name: 'Войти в магазин' }))
|
|||
|
|
|
|||
|
|
expect(await screen.findByRole('heading', { name: 'Mechanical Keyboard' })).toBeInTheDocument()
|
|||
|
|
expect(screen.getByText('Demo Admin')).toBeInTheDocument()
|
|||
|
|
|
|||
|
|
await user.click(screen.getByRole('button', { name: 'В заказ' }))
|
|||
|
|
expect(screen.getByText('Draft order')).toBeInTheDocument()
|
|||
|
|
|
|||
|
|
await user.click(screen.getByRole('button', { name: 'Оформить заказ' }))
|
|||
|
|
|
|||
|
|
expect(await screen.findByText('order-010')).toBeInTheDocument()
|
|||
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|||
|
|
'http://localhost:3001/api/v1/orders',
|
|||
|
|
expect.objectContaining({ method: 'POST' })
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
})
|