// ============================================================ // Dashboard 页面测试 // ============================================================ 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 Dashboard from '@/pages/Dashboard' // ── Mock data ──────────────────────────────────────────────── const mockStats = { total_accounts: 12, active_accounts: 8, tasks_today: 156, active_providers: 3, active_models: 7, tokens_today_input: 24000, tokens_today_output: 8500, } const mockLogs = { items: [ { id: 1, account_id: 'acc-001', action: 'login', target_type: 'account', target_id: 'acc-001', details: null, ip_address: '192.168.1.1', created_at: '2026-03-30T10:00:00Z', }, { id: 2, account_id: 'acc-002', action: 'create_provider', target_type: 'provider', target_id: 'prov-001', details: { name: 'OpenAI' }, ip_address: '10.0.0.1', created_at: '2026-03-30T09:30:00Z', }, ], total: 2, page: 1, page_size: 10, } // ── 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('Dashboard page', () => { it('renders page header', async () => { server.use( http.get('*/api/v1/stats/dashboard', () => { return HttpResponse.json(mockStats) }), http.get('*/api/v1/logs/operations', () => { return HttpResponse.json(mockLogs) }), ) renderWithProviders() expect(screen.getByText('仪表盘')).toBeInTheDocument() expect(screen.getByText('系统概览与最近活动')).toBeInTheDocument() }) it('renders stat cards with correct values', async () => { server.use( http.get('*/api/v1/stats/dashboard', () => { return HttpResponse.json(mockStats) }), http.get('*/api/v1/logs/operations', () => { return HttpResponse.json(mockLogs) }), ) renderWithProviders() await waitFor(() => { expect(screen.getByText('12')).toBeInTheDocument() }) // Stat titles expect(screen.getByText('总账号')).toBeInTheDocument() expect(screen.getByText('活跃服务商')).toBeInTheDocument() expect(screen.getByText('活跃模型')).toBeInTheDocument() expect(screen.getByText('今日请求')).toBeInTheDocument() expect(screen.getByText('今日 Token')).toBeInTheDocument() // Token total: 24000 + 8500 = 32500 expect(screen.getByText('32,500')).toBeInTheDocument() }) it('renders recent logs table with action labels', async () => { server.use( http.get('*/api/v1/stats/dashboard', () => { return HttpResponse.json(mockStats) }), http.get('*/api/v1/logs/operations', () => { return HttpResponse.json(mockLogs) }), ) renderWithProviders() // Wait for action labels from constants/status.ts await waitFor(() => { expect(screen.getByText('登录')).toBeInTheDocument() }) expect(screen.getByText('创建服务商')).toBeInTheDocument() }) it('renders target types in logs table', async () => { server.use( http.get('*/api/v1/stats/dashboard', () => { return HttpResponse.json(mockStats) }), http.get('*/api/v1/logs/operations', () => { return HttpResponse.json(mockLogs) }), ) renderWithProviders() await waitFor(() => { expect(screen.getByText('登录')).toBeInTheDocument() }) expect(screen.getByText('account')).toBeInTheDocument() expect(screen.getByText('provider')).toBeInTheDocument() }) it('shows loading spinner before stats load', async () => { server.use( http.get('*/api/v1/stats/dashboard', async () => { await new Promise((resolve) => setTimeout(resolve, 500)) return HttpResponse.json(mockStats) }), http.get('*/api/v1/logs/operations', () => { return HttpResponse.json(mockLogs) }), ) 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 error state when stats request fails', async () => { server.use( http.get('*/api/v1/stats/dashboard', () => { return HttpResponse.json( { error: 'internal_error', message: '服务器内部错误' }, { status: 500 }, ) }), http.get('*/api/v1/logs/operations', () => { return HttpResponse.json(mockLogs) }), ) renderWithProviders() await waitFor(() => { expect(screen.getByText('服务器内部错误')).toBeInTheDocument() }) }) it('renders stat cards with zero values when stats are null', async () => { server.use( http.get('*/api/v1/stats/dashboard', () => { return HttpResponse.json({}) }), http.get('*/api/v1/logs/operations', () => { return HttpResponse.json({ items: [], total: 0, page: 1, page_size: 10 }) }), ) renderWithProviders() // All stats should fallback to 0 await waitFor(() => { const zeros = screen.getAllByText('0') expect(zeros.length).toBeGreaterThanOrEqual(2) }) }) it('renders recent logs section header', async () => { server.use( http.get('*/api/v1/stats/dashboard', () => { return HttpResponse.json(mockStats) }), http.get('*/api/v1/logs/operations', () => { return HttpResponse.json(mockLogs) }), ) renderWithProviders() await waitFor(() => { expect(screen.getByText('最近操作日志')).toBeInTheDocument() }) }) })