// ============================================================ // Usage 页面测试 // ============================================================ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { render, screen, waitFor } from '@testing-library/react' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import Usage from '@/pages/Usage' // ── Mock data ──────────────────────────────────────────────── const mockDailyStats = [ { day: '2026-03-28', request_count: 120, input_tokens: 24000, output_tokens: 8000, unique_devices: 5, }, { day: '2026-03-29', request_count: 80, input_tokens: 16000, output_tokens: 5000, unique_devices: 3, }, { day: '2026-03-30', request_count: 200, input_tokens: 40000, output_tokens: 12000, unique_devices: 7, }, ] const mockModelStats = [ { model_id: 'gpt-4o', request_count: 300, input_tokens: 60000, output_tokens: 18000, avg_latency_ms: 450.3, success_rate: 0.98, }, { model_id: 'claude-sonnet-4-20250514', request_count: 100, input_tokens: 20000, output_tokens: 7000, avg_latency_ms: 620.7, success_rate: 0.95, }, ] // ── MSW server ─────────────────────────────────────────────── const server = setupServer() beforeEach(() => { server.listen({ onUnhandledRequest: 'bypass' }) }) afterEach(() => { server.close() }) // ── Helper: render with QueryClient ────────────────────────── function renderWithProviders(ui: React.ReactElement) { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, }, }) return render( {ui} , ) } // ── Tests ──────────────────────────────────────────────────── describe('Usage page', () => { it('renders page title and summary cards', async () => { server.use( http.get('*/api/v1/telemetry/daily', () => { return HttpResponse.json(mockDailyStats) }), http.get('*/api/v1/telemetry/stats', () => { return HttpResponse.json(mockModelStats) }), ) renderWithProviders() expect(screen.getByText('用量统计')).toBeInTheDocument() expect(screen.getByText('查看模型使用情况、Token 消耗和用户转化')).toBeInTheDocument() // Summary card titles expect(screen.getByText('总请求数')).toBeInTheDocument() expect(screen.getByText('总 Token 数')).toBeInTheDocument() // Total requests: 120 + 80 + 200 = 400 await waitFor(() => { expect(screen.getByText('400')).toBeInTheDocument() }) // Total tokens: (24000+8000) + (16000+5000) + (40000+12000) = 105,000 expect(screen.getByText('105,000')).toBeInTheDocument() }) it('fetches and displays daily stats table', async () => { server.use( http.get('*/api/v1/telemetry/daily', () => { return HttpResponse.json(mockDailyStats) }), http.get('*/api/v1/telemetry/stats', () => { return HttpResponse.json(mockModelStats) }), ) renderWithProviders() // Table column headers expect(screen.getByText('每日统计')).toBeInTheDocument() // Wait for data rows to render await waitFor(() => { expect(screen.getByText('2026-03-28')).toBeInTheDocument() }) // Formatted request counts expect(screen.getByText('120')).toBeInTheDocument() expect(screen.getByText('80')).toBeInTheDocument() expect(screen.getByText('200')).toBeInTheDocument() // Device counts expect(screen.getByText('5')).toBeInTheDocument() }) it('fetches and displays model stats table', async () => { server.use( http.get('*/api/v1/telemetry/daily', () => { return HttpResponse.json(mockDailyStats) }), http.get('*/api/v1/telemetry/stats', () => { return HttpResponse.json(mockModelStats) }), ) renderWithProviders() expect(screen.getByText('按模型统计')).toBeInTheDocument() await waitFor(() => { expect(screen.getByText('gpt-4o')).toBeInTheDocument() }) expect(screen.getByText('claude-sonnet-4-20250514')).toBeInTheDocument() // Success rate: 0.98 -> "98.0%" expect(screen.getByText('98.0%')).toBeInTheDocument() // Avg latency: 450.3 -> "450ms" expect(screen.getByText('450ms')).toBeInTheDocument() }) it('shows loading spinner before data loads', async () => { server.use( http.get('*/api/v1/telemetry/daily', async () => { await new Promise((resolve) => setTimeout(resolve, 500)) return HttpResponse.json(mockDailyStats) }), http.get('*/api/v1/telemetry/stats', async () => { await new Promise((resolve) => setTimeout(resolve, 500)) return HttpResponse.json(mockModelStats) }), ) renderWithProviders() // Ant Design Spin component renders a .ant-spin element const spinner = document.querySelector('.ant-spin') expect(spinner).toBeTruthy() // Wait for loading to complete so afterEach cleanup is clean await waitFor(() => { expect(screen.getByText('用量统计')).toBeInTheDocument() }) }) it('shows ErrorState when daily stats request fails', async () => { server.use( http.get('*/api/v1/telemetry/daily', () => { return HttpResponse.json( { error: 'internal_error', message: '服务器内部错误' }, { status: 500 }, ) }), http.get('*/api/v1/telemetry/stats', () => { return HttpResponse.json(mockModelStats) }), ) renderWithProviders() await waitFor(() => { expect(screen.getByText('服务器内部错误')).toBeInTheDocument() }) // ErrorState renders a retry button (antd v6 may split Chinese characters) expect(screen.getByRole('button', { name: /重.*试/ })).toBeInTheDocument() }) it('calculates totals correctly from daily data', async () => { server.use( http.get('*/api/v1/telemetry/daily', () => { return HttpResponse.json([ { day: '2026-03-30', request_count: 1500, input_tokens: 10000, output_tokens: 3000, unique_devices: 2, }, ]) }), http.get('*/api/v1/telemetry/stats', () => { return HttpResponse.json([]) }), ) renderWithProviders() // Total requests: 1500 (formatted as "1,500" by Statistic) await waitFor(() => { const elements = screen.getAllByText('1,500') expect(elements.length).toBeGreaterThanOrEqual(1) }) // Total tokens: 10000 + 3000 = 13,000 expect(screen.getAllByText('13,000').length).toBeGreaterThanOrEqual(1) }) })