- 采用 Ant Design 5 作为 UI 组件库,替换自定义组件 - 集成 React Router v7 提供路由导航 - 使用 TanStack Query v5 管理数据获取和缓存 - 建立 Vitest + React Testing Library 测试体系 - 添加 Playwright E2E 测试覆盖 - 使用 MSW mock API 响应 - 配置 TypeScript strict 模式 - 采用 SCSS Modules 组织样式 - 更新 OpenSpec 规格以反映前端架构变更 - 归档 frontend-refactor 变更记录
141 lines
4.0 KiB
TypeScript
141 lines
4.0 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 { useStats } from '@/hooks/useStats';
|
|
import type { UsageStats, StatsQueryParams } from '@/types';
|
|
|
|
// Test data
|
|
const mockStats: UsageStats[] = [
|
|
{
|
|
id: 1,
|
|
providerId: 'provider-1',
|
|
modelName: 'gpt-4o',
|
|
requestCount: 100,
|
|
date: '2026-04-01',
|
|
},
|
|
{
|
|
id: 2,
|
|
providerId: 'provider-1',
|
|
modelName: 'gpt-4o-mini',
|
|
requestCount: 50,
|
|
date: '2026-04-01',
|
|
},
|
|
];
|
|
|
|
const mockFilteredStats: UsageStats[] = [
|
|
{
|
|
id: 3,
|
|
providerId: 'provider-2',
|
|
modelName: 'claude-sonnet-4-5',
|
|
requestCount: 200,
|
|
date: '2026-04-01',
|
|
},
|
|
];
|
|
|
|
// Track the request URL for assertions
|
|
let capturedUrl: URL | null = null;
|
|
|
|
// MSW handlers
|
|
const handlers = [
|
|
http.get('/api/stats', ({ request }) => {
|
|
capturedUrl = new URL(request.url);
|
|
const providerId = capturedUrl.searchParams.get('provider_id');
|
|
if (providerId === 'provider-2') {
|
|
return HttpResponse.json(mockFilteredStats);
|
|
}
|
|
return HttpResponse.json(mockStats);
|
|
}),
|
|
];
|
|
|
|
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();
|
|
capturedUrl = null;
|
|
});
|
|
afterAll(() => server.close());
|
|
|
|
describe('useStats', () => {
|
|
it('fetches stats without params', async () => {
|
|
const { result } = renderHook(() => useStats(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
expect(result.current.data).toEqual(mockStats);
|
|
expect(result.current.data).toHaveLength(2);
|
|
expect(result.current.data![0]!.modelName).toBe('gpt-4o');
|
|
expect(result.current.data![1]!.requestCount).toBe(50);
|
|
|
|
// Verify no query params were sent
|
|
expect(capturedUrl!.search).toBe('');
|
|
});
|
|
|
|
it('with filter params passes them correctly', async () => {
|
|
const params: StatsQueryParams = {
|
|
providerId: 'provider-2',
|
|
modelName: 'claude-sonnet-4-5',
|
|
startDate: '2026-04-01',
|
|
endDate: '2026-04-15',
|
|
};
|
|
|
|
const { result } = renderHook(() => useStats(params), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
expect(result.current.data).toEqual(mockFilteredStats);
|
|
expect(result.current.data).toHaveLength(1);
|
|
expect(result.current.data![0]!.modelName).toBe('claude-sonnet-4-5');
|
|
|
|
// Verify query params were passed correctly (snake_case)
|
|
expect(capturedUrl!.searchParams.get('provider_id')).toBe('provider-2');
|
|
expect(capturedUrl!.searchParams.get('model_name')).toBe('claude-sonnet-4-5');
|
|
expect(capturedUrl!.searchParams.get('start_date')).toBe('2026-04-01');
|
|
expect(capturedUrl!.searchParams.get('end_date')).toBe('2026-04-15');
|
|
});
|
|
|
|
it('with partial filter params only sends provided params', async () => {
|
|
const params: StatsQueryParams = {
|
|
providerId: 'provider-1',
|
|
};
|
|
|
|
const { result } = renderHook(() => useStats(params), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
// Verify only provider_id was sent
|
|
expect(capturedUrl!.searchParams.get('provider_id')).toBe('provider-1');
|
|
expect(capturedUrl!.searchParams.get('model_name')).toBeNull();
|
|
expect(capturedUrl!.searchParams.get('start_date')).toBeNull();
|
|
expect(capturedUrl!.searchParams.get('end_date')).toBeNull();
|
|
});
|
|
});
|