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:
144
frontend/src/__tests__/components/ModelForm.test.tsx
Normal file
144
frontend/src/__tests__/components/ModelForm.test.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { ModelForm } from '@/pages/Providers/ModelForm';
|
||||
import type { Provider, Model } 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 mockModel: Model = {
|
||||
id: 'gpt-4o',
|
||||
providerId: 'openai',
|
||||
modelName: 'gpt-4o',
|
||||
enabled: true,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
open: true,
|
||||
providerId: 'openai',
|
||||
providers: mockProviders,
|
||||
onSave: vi.fn(),
|
||||
onCancel: vi.fn(),
|
||||
loading: false,
|
||||
};
|
||||
|
||||
function getDialog() {
|
||||
return screen.getByRole('dialog');
|
||||
}
|
||||
|
||||
describe('ModelForm', () => {
|
||||
it('renders form with provider select', () => {
|
||||
render(<ModelForm {...defaultProps} />);
|
||||
|
||||
const dialog = getDialog();
|
||||
expect(within(dialog).getByText('添加模型')).toBeInTheDocument();
|
||||
expect(within(dialog).getByText('ID')).toBeInTheDocument();
|
||||
expect(within(dialog).getByText('供应商')).toBeInTheDocument();
|
||||
expect(within(dialog).getByText('模型名称')).toBeInTheDocument();
|
||||
expect(within(dialog).getByText('启用')).toBeInTheDocument();
|
||||
|
||||
// The selected provider (OpenAI) is shown; Anthropic is not rendered until dropdown opens
|
||||
expect(within(dialog).getByText('OpenAI')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('defaults providerId to the passed providerId in create mode', () => {
|
||||
render(<ModelForm {...defaultProps} />);
|
||||
|
||||
const dialog = getDialog();
|
||||
const selectionItem = dialog.querySelector('.ant-select-selection-item');
|
||||
expect(selectionItem).toBeInTheDocument();
|
||||
expect(selectionItem?.textContent).toBe('OpenAI');
|
||||
});
|
||||
|
||||
it('shows validation error messages for required fields', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ModelForm
|
||||
{...defaultProps}
|
||||
providerId={undefined as unknown as string}
|
||||
providers={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const dialog = getDialog();
|
||||
const okButton = within(dialog).getByRole('button', { name: /保/ });
|
||||
await user.click(okButton);
|
||||
|
||||
expect(await screen.findByText('请输入模型 ID')).toBeInTheDocument();
|
||||
expect(screen.getByText('请选择供应商')).toBeInTheDocument();
|
||||
expect(screen.getByText('请输入模型名称')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onSave with form values on successful submission', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSave = vi.fn();
|
||||
render(<ModelForm {...defaultProps} onSave={onSave} />);
|
||||
|
||||
const dialog = getDialog();
|
||||
// There are two inputs with placeholder "例如: gpt-4o": ID field (index 0) and model name (index 1)
|
||||
const inputs = within(dialog).getAllByPlaceholderText('例如: gpt-4o');
|
||||
|
||||
// Type into the ID field
|
||||
await user.type(inputs[0], 'gpt-4o-mini');
|
||||
// Type into the model name field
|
||||
await user.type(inputs[1], 'gpt-4o-mini');
|
||||
|
||||
const okButton = within(dialog).getByRole('button', { name: /保/ });
|
||||
await user.click(okButton);
|
||||
|
||||
expect(onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'gpt-4o-mini',
|
||||
providerId: 'openai',
|
||||
modelName: 'gpt-4o-mini',
|
||||
enabled: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders pre-filled fields in edit mode', () => {
|
||||
render(<ModelForm {...defaultProps} model={mockModel} />);
|
||||
|
||||
const dialog = getDialog();
|
||||
expect(within(dialog).getByText('编辑模型')).toBeInTheDocument();
|
||||
|
||||
const inputs = within(dialog).getAllByPlaceholderText('例如: gpt-4o');
|
||||
const idInput = inputs[0] as HTMLInputElement;
|
||||
expect(idInput.value).toBe('gpt-4o');
|
||||
expect(idInput).toBeDisabled();
|
||||
|
||||
const modelNameInput = inputs[1] as HTMLInputElement;
|
||||
expect(modelNameInput.value).toBe('gpt-4o');
|
||||
});
|
||||
|
||||
it('calls onCancel when clicking cancel button', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCancel = vi.fn();
|
||||
render(<ModelForm {...defaultProps} onCancel={onCancel} />);
|
||||
|
||||
const dialog = getDialog();
|
||||
const cancelButton = within(dialog).getByRole('button', { name: /取/ });
|
||||
await user.click(cancelButton);
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
147
frontend/src/__tests__/components/ProviderForm.test.tsx
Normal file
147
frontend/src/__tests__/components/ProviderForm.test.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { ProviderForm } from '@/pages/Providers/ProviderForm';
|
||||
import type { Provider } from '@/types';
|
||||
|
||||
const mockProvider: Provider = {
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
apiKey: 'sk-old-key',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
enabled: true,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
open: true,
|
||||
onSave: vi.fn(),
|
||||
onCancel: vi.fn(),
|
||||
loading: false,
|
||||
};
|
||||
|
||||
function getDialog() {
|
||||
return screen.getByRole('dialog');
|
||||
}
|
||||
|
||||
describe('ProviderForm', () => {
|
||||
it('renders form fields in create mode', () => {
|
||||
render(<ProviderForm {...defaultProps} />);
|
||||
|
||||
const dialog = getDialog();
|
||||
expect(within(dialog).getByText('添加供应商')).toBeInTheDocument();
|
||||
expect(within(dialog).getByText('ID')).toBeInTheDocument();
|
||||
expect(within(dialog).getByText('名称')).toBeInTheDocument();
|
||||
expect(within(dialog).getByText('API Key')).toBeInTheDocument();
|
||||
expect(within(dialog).getByText('Base URL')).toBeInTheDocument();
|
||||
expect(within(dialog).getByText('启用')).toBeInTheDocument();
|
||||
expect(within(dialog).getByPlaceholderText('例如: openai')).toBeInTheDocument();
|
||||
expect(within(dialog).getByPlaceholderText('例如: OpenAI')).toBeInTheDocument();
|
||||
expect(within(dialog).getByPlaceholderText('例如: https://api.openai.com/v1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders pre-filled fields in edit mode', () => {
|
||||
render(<ProviderForm {...defaultProps} provider={mockProvider} />);
|
||||
|
||||
const dialog = getDialog();
|
||||
expect(within(dialog).getByText('编辑供应商')).toBeInTheDocument();
|
||||
|
||||
const idInput = within(dialog).getByPlaceholderText('例如: openai') as HTMLInputElement;
|
||||
expect(idInput.value).toBe('openai');
|
||||
expect(idInput).toBeDisabled();
|
||||
|
||||
const nameInput = within(dialog).getByPlaceholderText('例如: OpenAI') as HTMLInputElement;
|
||||
expect(nameInput.value).toBe('OpenAI');
|
||||
|
||||
const baseUrlInput = within(dialog).getByPlaceholderText('例如: https://api.openai.com/v1') as HTMLInputElement;
|
||||
expect(baseUrlInput.value).toBe('https://api.openai.com/v1');
|
||||
});
|
||||
|
||||
it('shows API Key label variant in edit mode', () => {
|
||||
render(<ProviderForm {...defaultProps} provider={mockProvider} />);
|
||||
|
||||
const dialog = getDialog();
|
||||
expect(within(dialog).getByText('API Key(留空则不修改)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows validation error messages for required fields', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ProviderForm {...defaultProps} />);
|
||||
|
||||
const dialog = getDialog();
|
||||
const okButton = within(dialog).getByRole('button', { name: /保/ });
|
||||
await user.click(okButton);
|
||||
|
||||
// Wait for validation messages to appear
|
||||
expect(await screen.findByText('请输入供应商 ID')).toBeInTheDocument();
|
||||
expect(screen.getByText('请输入名称')).toBeInTheDocument();
|
||||
expect(screen.getByText('请输入 API Key')).toBeInTheDocument();
|
||||
expect(screen.getByText('请输入 Base URL')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onSave with form values on successful submission', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSave = vi.fn();
|
||||
render(<ProviderForm {...defaultProps} onSave={onSave} />);
|
||||
|
||||
const dialog = getDialog();
|
||||
await user.type(within(dialog).getByPlaceholderText('例如: openai'), 'test-provider');
|
||||
await user.type(within(dialog).getByPlaceholderText('例如: OpenAI'), 'Test Provider');
|
||||
await user.type(within(dialog).getByPlaceholderText('sk-...'), 'sk-test-key');
|
||||
await user.type(within(dialog).getByPlaceholderText('例如: https://api.openai.com/v1'), 'https://api.test.com/v1');
|
||||
|
||||
const okButton = within(dialog).getByRole('button', { name: /保/ });
|
||||
await user.click(okButton);
|
||||
|
||||
expect(onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'test-provider',
|
||||
name: 'Test Provider',
|
||||
apiKey: 'sk-test-key',
|
||||
baseUrl: 'https://api.test.com/v1',
|
||||
enabled: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('calls onCancel when clicking cancel button', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCancel = vi.fn();
|
||||
render(<ProviderForm {...defaultProps} onCancel={onCancel} />);
|
||||
|
||||
const dialog = getDialog();
|
||||
const cancelButton = within(dialog).getByRole('button', { name: /取/ });
|
||||
await user.click(cancelButton);
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('shows confirm loading state', () => {
|
||||
render(<ProviderForm {...defaultProps} loading={true} />);
|
||||
const dialog = getDialog();
|
||||
const okButton = within(dialog).getByRole('button', { name: /保/ });
|
||||
expect(okButton).toHaveClass('ant-btn-loading');
|
||||
});
|
||||
|
||||
it('shows validation error for invalid URL format', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ProviderForm {...defaultProps} />);
|
||||
|
||||
const dialog = getDialog();
|
||||
|
||||
// Fill in required fields
|
||||
await user.type(within(dialog).getByPlaceholderText('例如: openai'), 'test-provider');
|
||||
await user.type(within(dialog).getByPlaceholderText('例如: OpenAI'), 'Test Provider');
|
||||
await user.type(within(dialog).getByPlaceholderText('sk-...'), 'sk-test-key');
|
||||
|
||||
// Enter an invalid URL in the Base URL field
|
||||
await user.type(within(dialog).getByPlaceholderText('例如: https://api.openai.com/v1'), 'not-a-url');
|
||||
|
||||
// Submit the form
|
||||
const okButton = within(dialog).getByRole('button', { name: /保/ });
|
||||
await user.click(okButton);
|
||||
|
||||
// Verify that a URL validation error message appears
|
||||
expect(await screen.findByText('请输入有效的 URL')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
140
frontend/src/__tests__/components/ProviderTable.test.tsx
Normal file
140
frontend/src/__tests__/components/ProviderTable.test.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { ProviderTable } from '@/pages/Providers/ProviderTable';
|
||||
import type { Provider } from '@/types';
|
||||
|
||||
const mockModelsData = [
|
||||
{ id: 'model-1', providerId: 'openai', modelName: 'gpt-4o', enabled: true },
|
||||
{ id: 'model-2', providerId: 'openai', modelName: 'gpt-3.5-turbo', enabled: false },
|
||||
];
|
||||
|
||||
vi.mock('@/hooks/useModels', () => ({
|
||||
useModels: vi.fn(() => ({ data: mockModelsData, isLoading: false })),
|
||||
useDeleteModel: vi.fn(() => ({ mutate: vi.fn() })),
|
||||
}));
|
||||
|
||||
const mockProviders: Provider[] = [
|
||||
{
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
apiKey: 'sk-abcdefgh12345678',
|
||||
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: false,
|
||||
createdAt: '2024-01-02T00:00:00Z',
|
||||
updatedAt: '2024-01-02T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
const defaultProps = {
|
||||
providers: mockProviders,
|
||||
loading: false,
|
||||
onAdd: vi.fn(),
|
||||
onEdit: vi.fn(),
|
||||
onDelete: vi.fn(),
|
||||
onAddModel: vi.fn(),
|
||||
onEditModel: vi.fn(),
|
||||
};
|
||||
|
||||
describe('ProviderTable', () => {
|
||||
it('renders provider list with name, baseUrl, masked apiKey, and status tags', () => {
|
||||
render(<ProviderTable {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('供应商列表')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('OpenAI')).toBeInTheDocument();
|
||||
expect(screen.getByText('https://api.openai.com/v1')).toBeInTheDocument();
|
||||
expect(screen.getByText('****5678')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Anthropic')).toBeInTheDocument();
|
||||
expect(screen.getByText('https://api.anthropic.com')).toBeInTheDocument();
|
||||
expect(screen.getByText('****test')).toBeInTheDocument();
|
||||
|
||||
const enabledTags = screen.getAllByText('启用');
|
||||
const disabledTags = screen.getAllByText('禁用');
|
||||
expect(enabledTags.length).toBeGreaterThanOrEqual(1);
|
||||
expect(disabledTags.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('renders short api keys fully masked', () => {
|
||||
const shortKeyProvider: Provider[] = [
|
||||
{
|
||||
...mockProviders[0],
|
||||
id: 'short',
|
||||
name: 'ShortKey',
|
||||
apiKey: 'ab',
|
||||
},
|
||||
];
|
||||
render(<ProviderTable {...defaultProps} providers={shortKeyProvider} />);
|
||||
|
||||
expect(screen.getByText('****')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onAdd when clicking "添加供应商" button', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onAdd = vi.fn();
|
||||
render(<ProviderTable {...defaultProps} onAdd={onAdd} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '添加供应商' }));
|
||||
expect(onAdd).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls onEdit with correct provider when clicking "编辑"', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onEdit = vi.fn();
|
||||
render(<ProviderTable {...defaultProps} onEdit={onEdit} />);
|
||||
|
||||
const editButtons = screen.getAllByRole('button', { name: '编辑' });
|
||||
await user.click(editButtons[0]);
|
||||
|
||||
expect(onEdit).toHaveBeenCalledTimes(1);
|
||||
expect(onEdit).toHaveBeenCalledWith(mockProviders[0]);
|
||||
});
|
||||
|
||||
it('calls onDelete with correct provider ID when delete is confirmed', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onDelete = vi.fn();
|
||||
render(<ProviderTable {...defaultProps} onDelete={onDelete} />);
|
||||
|
||||
// Find and click the delete button for the first row
|
||||
const deleteButtons = screen.getAllByRole('button', { name: '删除' });
|
||||
await user.click(deleteButtons[0]);
|
||||
|
||||
// Find and click the "确 定" confirm button in the Popconfirm popup
|
||||
// antd renders the text with spaces between Chinese characters
|
||||
const confirmButtons = await screen.findAllByText('确 定');
|
||||
await user.click(confirmButtons[0]);
|
||||
|
||||
// Assert that onDelete was called with the correct provider ID
|
||||
expect(onDelete).toHaveBeenCalledTimes(1);
|
||||
expect(onDelete).toHaveBeenCalledWith('openai');
|
||||
});
|
||||
|
||||
it('shows loading state', () => {
|
||||
render(<ProviderTable {...defaultProps} loading={true} />);
|
||||
expect(document.querySelector('.ant-spin')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders expandable ModelTable when row is expanded', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ProviderTable {...defaultProps} />);
|
||||
|
||||
// Find and click the expand button for the first row
|
||||
const expandButtons = screen.getAllByRole('button', { name: /expand/i });
|
||||
expect(expandButtons.length).toBeGreaterThanOrEqual(1);
|
||||
await user.click(expandButtons[0]);
|
||||
|
||||
// Verify that ModelTable content is rendered with data from mocked useModels
|
||||
expect(await screen.findByText('gpt-4o')).toBeInTheDocument();
|
||||
expect(screen.getByText('gpt-3.5-turbo')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
159
frontend/src/__tests__/components/StatsTable.test.tsx
Normal file
159
frontend/src/__tests__/components/StatsTable.test.tsx
Normal 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',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user