// ============================================================ // Config 页面测试 // ============================================================ 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 Config from '@/pages/Config' // ── Mock data ──────────────────────────────────────────────── const mockConfigItems = [ { id: 'cfg-001', category: 'general', key_path: 'general.app_name', value_type: 'string', current_value: 'ZCLAW', default_value: 'ZCLAW', source: 'database', description: '应用程序名称', requires_restart: false, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', }, { id: 'cfg-002', category: 'general', key_path: 'general.debug_mode', value_type: 'boolean', current_value: 'false', default_value: 'false', source: 'default', description: '调试模式开关', requires_restart: true, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', }, { id: 'cfg-003', category: 'general', key_path: 'general.max_connections', value_type: 'integer', current_value: null, default_value: '100', source: 'default', description: '最大连接数', requires_restart: false, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', }, ] const mockResponse = { items: mockConfigItems, total: 3, page: 1, page_size: 50, } // ── 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('Config page', () => { it('renders page header', async () => { server.use( http.get('*/api/v1/config/items', () => { return HttpResponse.json(mockResponse) }), ) renderWithProviders() expect(screen.getByText('系统配置')).toBeInTheDocument() expect(screen.getByText('管理系统运行参数和功能开关')).toBeInTheDocument() }) it('fetches and displays config items', async () => { server.use( http.get('*/api/v1/config/items', () => { return HttpResponse.json(mockResponse) }), ) renderWithProviders() await waitFor(() => { expect(screen.getByText('general.app_name')).toBeInTheDocument() }) expect(screen.getByText('general.debug_mode')).toBeInTheDocument() }) it('shows loading spinner while fetching', async () => { server.use( http.get('*/api/v1/config/items', async () => { await new Promise((resolve) => setTimeout(resolve, 500)) return HttpResponse.json(mockResponse) }), ) 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('general.app_name')).toBeInTheDocument() }) }) it('shows error state on API failure', async () => { server.use( http.get('*/api/v1/config/items', () => { return HttpResponse.json( { error: 'internal_error', message: '服务器内部错误' }, { status: 500 }, ) }), ) renderWithProviders() // Config page does not have a dedicated ErrorState; the ProTable simply // renders empty when the query fails. We verify the page header is still // rendered and the table body has no data rows (shows "暂无数据"). await waitFor(() => { const emptyElements = screen.queryAllByText('暂无数据') expect(emptyElements.length).toBeGreaterThanOrEqual(1) }) // Page header is still present even on error expect(screen.getByText('系统配置')).toBeInTheDocument() }) it('renders config key_path and current_value columns', async () => { server.use( http.get('*/api/v1/config/items', () => { return HttpResponse.json(mockResponse) }), ) renderWithProviders() // key_path values are rendered in elements await waitFor(() => { expect(screen.getByText('general.app_name')).toBeInTheDocument() }) expect(screen.getByText('general.debug_mode')).toBeInTheDocument() // current_value "ZCLAW" appears in both the current_value column and default_value column const zclawElements = screen.getAllByText('ZCLAW') expect(zclawElements.length).toBeGreaterThanOrEqual(1) }) it('renders requires_restart column with tags', async () => { server.use( http.get('*/api/v1/config/items', () => { return HttpResponse.json(mockResponse) }), ) renderWithProviders() await waitFor(() => { expect(screen.getByText('general.app_name')).toBeInTheDocument() }) // requires_restart=true renders "是" (orange tag) expect(screen.getByText('是')).toBeInTheDocument() // requires_restart=false renders "否" (may appear multiple times for two items) const noTags = screen.getAllByText('否') expect(noTags.length).toBeGreaterThanOrEqual(1) }) it('renders category tabs', async () => { server.use( http.get('*/api/v1/config/items', () => { return HttpResponse.json(mockResponse) }), ) renderWithProviders() expect(screen.getByText('通用')).toBeInTheDocument() expect(screen.getByText('认证')).toBeInTheDocument() expect(screen.getByText('中转')).toBeInTheDocument() expect(screen.getByText('模型')).toBeInTheDocument() }) })