// ============================================================ // ModelServices 页面测试 // ============================================================ 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 ModelServices from '@/pages/ModelServices' // ── Mock data ──────────────────────────────────────────────── const mockProviders = { items: [ { id: 'prov-001', name: 'openai', display_name: 'OpenAI', base_url: 'https://api.openai.com/v1', api_protocol: 'openai', enabled: true, rate_limit_rpm: 500, rate_limit_tpm: null, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-03-15T10:00:00Z', }, { id: 'prov-002', name: 'anthropic', display_name: 'Anthropic', base_url: 'https://api.anthropic.com', api_protocol: 'anthropic', enabled: false, rate_limit_rpm: 200, rate_limit_tpm: null, created_at: '2026-02-01T00:00:00Z', updated_at: '2026-03-01T00:00:00Z', }, { id: 'prov-003', name: 'deepseek', display_name: 'DeepSeek', base_url: 'https://api.deepseek.com/v1', api_protocol: 'openai', enabled: true, rate_limit_rpm: null, rate_limit_tpm: null, created_at: '2026-03-01T00:00:00Z', updated_at: '2026-03-01T00: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('ModelServices page', () => { it('renders page with provider table', async () => { server.use( http.get('*/api/v1/providers', () => { return HttpResponse.json(mockProviders) }), ) renderWithProviders() // "新建服务商" button is rendered by toolBarRender expect(screen.getByText('新建服务商')).toBeInTheDocument() }) it('fetches and displays providers', async () => { server.use( http.get('*/api/v1/providers', () => { return HttpResponse.json(mockProviders) }), ) renderWithProviders() await waitFor(() => { expect(screen.getByText('OpenAI')).toBeInTheDocument() }) expect(screen.getByText('Anthropic')).toBeInTheDocument() expect(screen.getByText('DeepSeek')).toBeInTheDocument() // Provider identifiers rendered as code // openai also appears in base_url, so use getAllByText expect(screen.getAllByText('openai').length).toBeGreaterThanOrEqual(1) expect(screen.getAllByText('anthropic').length).toBeGreaterThanOrEqual(1) expect(screen.getAllByText('deepseek').length).toBeGreaterThanOrEqual(1) }) it('shows loading spinner before data arrives', async () => { server.use( http.get('*/api/v1/providers', async () => { await new Promise((resolve) => setTimeout(resolve, 500)) return HttpResponse.json(mockProviders) }), ) renderWithProviders() const spinner = document.querySelector('.ant-spin') expect(spinner).toBeTruthy() // Wait for loading to complete so afterEach cleanup is clean await waitFor(() => { expect(screen.getByText('OpenAI')).toBeInTheDocument() }) }) it('renders provider status as tag', async () => { server.use( http.get('*/api/v1/providers', () => { return HttpResponse.json(mockProviders) }), ) renderWithProviders() await waitFor(() => { expect(screen.getByText('OpenAI')).toBeInTheDocument() }) // enabled: true -> "启用" tag, enabled: false -> "禁用" tag const enabledTags = screen.getAllByText('启用') expect(enabledTags.length).toBe(2) // openai + deepseek expect(screen.getByText('禁用')).toBeInTheDocument() // anthropic }) it('shows empty table on API failure', async () => { server.use( http.get('*/api/v1/providers', () => { return HttpResponse.json( { error: 'internal_error', message: '获取服务商列表失败' }, { status: 500 }, ) }), ) renderWithProviders() // "新建服务商" button should still render expect(screen.getByText('新建服务商')).toBeInTheDocument() // Provider names should NOT be rendered await waitFor(() => { expect(screen.queryByText('OpenAI')).not.toBeInTheDocument() }) }) })