1
0
Files
nex/frontend/src/__tests__/hooks/useModels.test.tsx
lanyuanxiaoyao 2b1c5e96c3 refactor: 迁移 UI 组件库从 Ant Design 至 TDesign
- 替换 antd 为 tdesign-react 作为主要 UI 组件库
- 引入 Recharts 替代 @ant-design/charts 实现图表功能
- 移除主题系统相关代码(ThemeContext、themes 目录)
- 更新所有组件以适配 TDesign 组件 API
- 更新测试用例以匹配新的组件实现
- 新增 TDesign 和 Recharts 集成规范文档
2026-04-17 18:22:13 +08:00

287 lines
7.9 KiB
TypeScript

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<string, unknown>;
return HttpResponse.json({
...mockCreatedModel,
...body,
});
}),
http.put('/api/models/:id', async ({ request, params }) => {
const body = await request.json() as Record<string, unknown>;
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 (
<QueryClientProvider client={testQueryClient}>
{children}
</QueryClientProvider>
);
};
}
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 (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
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 (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
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 (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
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();
});
});