import { renderHook, waitFor } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import React from 'react'; import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; import { useModels, useCreateModel, useUpdateModel, useDeleteModel } from '@/hooks/useModels'; import type { Model, CreateModelInput, UpdateModelInput } from '@/types'; import { MessagePlugin } from 'tdesign-react'; // Mock MessagePlugin vi.mock('tdesign-react', () => ({ MessagePlugin: { success: vi.fn(), error: vi.fn(), }, })); // Test data const mockModels: Model[] = [ { id: 'model-1', providerId: 'provider-1', modelName: 'gpt-4o', enabled: true, createdAt: '2026-01-01T00:00:00Z', }, { id: 'model-2', providerId: 'provider-1', modelName: 'gpt-4o-mini', enabled: true, createdAt: '2026-01-02T00:00:00Z', }, ]; const mockFilteredModels: Model[] = [ { id: 'model-3', providerId: 'provider-2', modelName: 'claude-sonnet-4-5', enabled: true, createdAt: '2026-02-01T00:00:00Z', }, ]; const mockCreatedModel: Model = { id: 'model-4', providerId: 'provider-1', modelName: 'gpt-4.1', enabled: true, createdAt: '2026-03-01T00:00:00Z', }; // MSW handlers const handlers = [ http.get('/api/models', ({ request }) => { const url = new URL(request.url); const providerId = url.searchParams.get('provider_id'); if (providerId === 'provider-2') { return HttpResponse.json(mockFilteredModels); } return HttpResponse.json(mockModels); }), http.post('/api/models', async ({ request }) => { const body = await request.json() as Record; return HttpResponse.json({ ...mockCreatedModel, ...body, }); }), http.put('/api/models/:id', async ({ request, params }) => { const body = await request.json() as Record; const existing = mockModels.find((m) => m.id === params['id']); return HttpResponse.json({ ...existing, ...body }); }), http.delete('/api/models/:id', () => { return new HttpResponse(null, { status: 204 }); }), ]; const server = setupServer(...handlers); function createTestQueryClient() { return new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); } function createWrapper() { const testQueryClient = createTestQueryClient(); return function Wrapper({ children }: { children: React.ReactNode }) { return ( {children} ); }; } beforeAll(() => server.listen()); afterEach(() => { server.resetHandlers(); vi.clearAllMocks(); }); afterAll(() => server.close()); describe('useModels', () => { it('fetches model list', async () => { const { result } = renderHook(() => useModels(), { wrapper: createWrapper(), }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data).toEqual(mockModels); expect(result.current.data).toHaveLength(2); expect(result.current.data![0]!.modelName).toBe('gpt-4o'); }); it('with providerId passes it to API and returns filtered models', async () => { const { result } = renderHook(() => useModels('provider-2'), { wrapper: createWrapper(), }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data).toEqual(mockFilteredModels); expect(result.current.data).toHaveLength(1); expect(result.current.data![0]!.modelName).toBe('claude-sonnet-4-5'); }); }); describe('useCreateModel', () => { it('calls API and invalidates model queries', async () => { const queryClient = createTestQueryClient(); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); function Wrapper({ children }: { children: React.ReactNode }) { return ( {children} ); } const { result } = renderHook(() => useCreateModel(), { wrapper: Wrapper, }); const input: CreateModelInput = { id: 'model-4', providerId: 'provider-1', modelName: 'gpt-4.1', enabled: true, }; result.current.mutate(input); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data).toMatchObject({ id: 'model-4', modelName: 'gpt-4.1', }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['models'] }); expect(MessagePlugin.success).toHaveBeenCalledWith('模型创建成功'); }); it('calls message.error on failure', async () => { server.use( http.post('/api/models', () => { return HttpResponse.json({ message: '创建失败' }, { status: 500 }); }), ); const { result } = renderHook(() => useCreateModel(), { wrapper: createWrapper(), }); const input: CreateModelInput = { id: 'model-4', providerId: 'provider-1', modelName: 'gpt-4.1', enabled: true, }; result.current.mutate(input); await waitFor(() => expect(result.current.isError).toBe(true)); expect(MessagePlugin.error).toHaveBeenCalled(); }); }); describe('useUpdateModel', () => { it('calls API and invalidates model queries', async () => { const queryClient = createTestQueryClient(); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); function Wrapper({ children }: { children: React.ReactNode }) { return ( {children} ); } const { result } = renderHook(() => useUpdateModel(), { wrapper: Wrapper, }); const input: UpdateModelInput = { modelName: 'gpt-4o-updated' }; result.current.mutate({ id: 'model-1', input }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data).toMatchObject({ modelName: 'gpt-4o-updated', }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['models'] }); expect(MessagePlugin.success).toHaveBeenCalledWith('模型更新成功'); }); it('calls message.error on failure', async () => { server.use( http.put('/api/models/:id', () => { return HttpResponse.json({ message: '更新失败' }, { status: 500 }); }), ); const { result } = renderHook(() => useUpdateModel(), { wrapper: createWrapper(), }); result.current.mutate({ id: 'model-1', input: { modelName: 'Updated' } }); await waitFor(() => expect(result.current.isError).toBe(true)); expect(MessagePlugin.error).toHaveBeenCalled(); }); }); describe('useDeleteModel', () => { it('calls API and invalidates model queries', async () => { const queryClient = createTestQueryClient(); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); function Wrapper({ children }: { children: React.ReactNode }) { return ( {children} ); } const { result } = renderHook(() => useDeleteModel(), { wrapper: Wrapper, }); result.current.mutate('model-1'); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['models'] }); expect(MessagePlugin.success).toHaveBeenCalledWith('模型删除成功'); }); it('calls message.error on failure', async () => { server.use( http.delete('/api/models/:id', () => { return HttpResponse.json({ message: '删除失败' }, { status: 500 }); }), ); const { result } = renderHook(() => useDeleteModel(), { wrapper: createWrapper(), }); result.current.mutate('model-1'); await waitFor(() => expect(result.current.isError).toBe(true)); expect(MessagePlugin.error).toHaveBeenCalled(); }); });