1
0

feat: 完成前端重构,采用 Ant Design 5 和完整测试体系

- 采用 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 变更记录
This commit is contained in:
2026-04-16 11:21:48 +08:00
parent c17903dcbc
commit 9359ca7f62
61 changed files with 4588 additions and 1095 deletions

View File

@@ -0,0 +1,159 @@
import { render, screen, within, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi, afterEach } from 'vitest';
import { StatsTable } from '@/pages/Stats/StatsTable';
import type { Provider, UsageStats } from '@/types';
const mockProviders: Provider[] = [
{
id: 'openai',
name: 'OpenAI',
apiKey: 'sk-test',
baseUrl: 'https://api.openai.com/v1',
enabled: true,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
},
{
id: 'anthropic',
name: 'Anthropic',
apiKey: 'sk-ant-test',
baseUrl: 'https://api.anthropic.com',
enabled: true,
createdAt: '2024-01-02T00:00:00Z',
updatedAt: '2024-01-02T00:00:00Z',
},
];
const mockStats: UsageStats[] = [
{
id: 1,
providerId: 'openai',
modelName: 'gpt-4o',
requestCount: 100,
date: '2024-01-15',
},
{
id: 2,
providerId: 'anthropic',
modelName: 'claude-3-opus',
requestCount: 50,
date: '2024-01-15',
},
];
const mockUseStats = vi.fn(() => ({
data: mockStats,
isLoading: false,
}));
vi.mock('@/hooks/useStats', () => ({
useStats: (...args: unknown[]) => mockUseStats(...args),
}));
describe('StatsTable', () => {
afterEach(() => {
vi.clearAllMocks();
});
it('renders stats table with data', () => {
render(<StatsTable providers={mockProviders} />);
expect(screen.getByText('gpt-4o')).toBeInTheDocument();
expect(screen.getByText('claude-3-opus')).toBeInTheDocument();
// Both rows share the same date
const dateCells = screen.getAllByText('2024-01-15');
expect(dateCells.length).toBe(2);
expect(screen.getByText('100')).toBeInTheDocument();
expect(screen.getByText('50')).toBeInTheDocument();
});
it('shows provider name from providers prop instead of providerId', () => {
render(<StatsTable providers={mockProviders} />);
expect(screen.getByText('OpenAI')).toBeInTheDocument();
// "Anthropic" appears in both the provider column and the filter select options
const allAnthropic = screen.getAllByText('Anthropic');
expect(allAnthropic.length).toBeGreaterThanOrEqual(1);
});
it('renders filter controls with Select, Input, and DatePicker', () => {
render(<StatsTable providers={mockProviders} />);
// Check that the select element exists for provider filter
const selects = document.querySelectorAll('.ant-select');
expect(selects.length).toBeGreaterThanOrEqual(1);
// Check that the Input element exists for model name filter
const modelInput = screen.getByPlaceholderText('模型名称');
expect(modelInput).toBeInTheDocument();
// Verify placeholder text is rendered
expect(screen.getByText('所有供应商')).toBeInTheDocument();
const rangePicker = document.querySelector('.ant-picker-range');
expect(rangePicker).toBeInTheDocument();
});
it('renders table headers correctly', () => {
render(<StatsTable providers={mockProviders} />);
expect(screen.getByText('供应商')).toBeInTheDocument();
expect(screen.getByText('模型')).toBeInTheDocument();
expect(screen.getByText('日期')).toBeInTheDocument();
expect(screen.getByText('请求数')).toBeInTheDocument();
});
it('falls back to providerId when provider not found in providers prop', () => {
const limitedProviders = [mockProviders[0]]; // only OpenAI
render(<StatsTable providers={limitedProviders} />);
// OpenAI should show name
expect(screen.getByText('OpenAI')).toBeInTheDocument();
// Anthropic is not in providers list, so providerId "anthropic" should show
expect(screen.getByText('anthropic')).toBeInTheDocument();
});
it('renders with empty stats data', () => {
mockUseStats.mockReturnValueOnce({
data: [],
isLoading: false,
});
render(<StatsTable providers={mockProviders} />);
// Table should still be rendered, just empty
expect(screen.getByText('供应商')).toBeInTheDocument();
expect(screen.getByText('模型')).toBeInTheDocument();
});
it('updates provider filter when selecting a provider', () => {
render(<StatsTable providers={mockProviders} />);
// Initially useStats should be called with no providerId filter
expect(mockUseStats).toHaveBeenLastCalledWith(
expect.objectContaining({
providerId: undefined,
}),
);
// Find the provider Select and change its value
const selectElement = document.querySelector('.ant-select');
expect(selectElement).toBeInTheDocument();
// Open the select dropdown
fireEvent.mouseDown(selectElement!.querySelector('.ant-select-selector')!);
// Click on the "OpenAI" option from the dropdown
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toBeInTheDocument();
const openaiOption = within(dropdown as HTMLElement).getByText('OpenAI');
fireEvent.click(openaiOption);
// After selecting, useStats should be called with providerId set to 'openai'
expect(mockUseStats).toHaveBeenLastCalledWith(
expect.objectContaining({
providerId: 'openai',
}),
);
});
});