- 重构 AppLayout 为可折叠侧边栏导航布局 - 实现统计仪表盘:统计摘要卡片 + 请求趋势图表 - Provider 页面使用 Card 包裹优化视觉层次 - 主题切换按钮移至侧边栏底部,支持折叠态 - Header 适配暗色主题,添加分隔线优化视觉过渡 - 添加全局样式重置(SCSS) - 完善组件测试和 E2E 测试覆盖 - 同步 OpenSpec 规范到主 specs
149 lines
5.2 KiB
TypeScript
149 lines
5.2 KiB
TypeScript
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 within a Card component', () => {
|
|
const { container } = render(<ProviderTable {...defaultProps} />);
|
|
|
|
expect(container.querySelector('.ant-card')).toBeInTheDocument();
|
|
expect(container.querySelector('.ant-card-head')).toBeInTheDocument();
|
|
expect(container.querySelector('.ant-card-body')).toBeInTheDocument();
|
|
});
|
|
|
|
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');
|
|
}, 10000);
|
|
|
|
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();
|
|
});
|
|
});
|