// ============================================================ // Logs 页面测试 // ============================================================ 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 Logs from '@/pages/Logs' // ── Mock data ──────────────────────────────────────────────── 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', }, { id: 3, account_id: 'acc-001', action: 'delete_model', target_type: 'model', target_id: 'mdl-001', details: null, ip_address: '192.168.1.1', created_at: '2026-03-29T14:00:00Z', }, ], total: 3, page: 1, page_size: 20, } // ── 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('Logs page', () => { it('renders page header', async () => { server.use( http.get('*/api/v1/logs/operations', () => { return HttpResponse.json(mockLogs) }), ) renderWithProviders() expect(screen.getByText('操作日志')).toBeInTheDocument() expect(screen.getByText('系统审计与操作记录')).toBeInTheDocument() }) it('fetches and displays log entries', async () => { server.use( http.get('*/api/v1/logs/operations', () => { return HttpResponse.json(mockLogs) }), ) renderWithProviders() // Wait for action labels rendered from constants/status.ts await waitFor(() => { expect(screen.getByText('登录')).toBeInTheDocument() }) expect(screen.getByText('创建服务商')).toBeInTheDocument() expect(screen.getByText('删除模型')).toBeInTheDocument() }) it('shows loading spinner while fetching', async () => { server.use( http.get('*/api/v1/logs/operations', async () => { await new Promise((resolve) => setTimeout(resolve, 500)) 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 ErrorState on API failure with retry button', async () => { server.use( http.get('*/api/v1/logs/operations', () => { return HttpResponse.json( { error: 'internal_error', message: '服务器内部错误' }, { status: 500 }, ) }), ) renderWithProviders() // ErrorState renders the error message await waitFor(() => { expect(screen.getByText('服务器内部错误')).toBeInTheDocument() }) // Ant Design Button splits two-character text with a space: "重 试" const retryButton = screen.getByRole('button', { name: /重.?试/ }) expect(retryButton).toBeInTheDocument() }) it('renders action as a colored tag', async () => { server.use( http.get('*/api/v1/logs/operations', () => { return HttpResponse.json(mockLogs) }), ) renderWithProviders() await waitFor(() => { expect(screen.getByText('登录')).toBeInTheDocument() }) // Verify the action tags have the correct Ant Design color classes const loginTag = screen.getByText('登录').closest('.ant-tag') expect(loginTag).toBeTruthy() // actionColors.login = 'green' → Ant Design renders ant-tag-green or ant-tag-color-green expect(loginTag?.className).toMatch(/green/) }) it('renders IP address column', async () => { server.use( http.get('*/api/v1/logs/operations', () => { return HttpResponse.json(mockLogs) }), ) renderWithProviders() await waitFor(() => { expect(screen.getByText('登录')).toBeInTheDocument() }) // 192.168.1.1 appears twice (two log entries from the same IP) const ip1Elements = screen.getAllByText('192.168.1.1') expect(ip1Elements.length).toBeGreaterThanOrEqual(1) expect(screen.getByText('10.0.0.1')).toBeInTheDocument() }) it('renders target_type column', async () => { server.use( 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() expect(screen.getByText('model')).toBeInTheDocument() }) })