- Dialog 设置 lazy={false} 修复首次打开编辑弹窗表单为空
- API Key 改为普通字段(前端去掉 password 类型,后端去掉掩码逻辑)
- 删除模型编辑弹窗中的统一模型 ID 字段
- 简化 ProviderService.Get 签名(去掉 maskKey 参数)
- 删除 domain 和 config 层的 MaskAPIKey() 方法
- 更新前后端测试(107 单元测试 + 16 E2E 全部通过)
- 同步 delta spec 到主 spec
200 lines
8.0 KiB
TypeScript
200 lines
8.0 KiB
TypeScript
import { render, screen, within, fireEvent } 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',
|
|
protocol: 'openai',
|
|
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() {
|
|
// TDesign Dialog doesn't have role="dialog", use class selector
|
|
const dialog = document.querySelector('.t-dialog');
|
|
if (!dialog) {
|
|
throw new Error('Dialog not found');
|
|
}
|
|
return 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).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');
|
|
|
|
const apiKeyInput = within(dialog).getByPlaceholderText('sk-...') as HTMLInputElement;
|
|
expect(apiKeyInput.value).toBe('sk-old-key');
|
|
});
|
|
|
|
it('shows API Key label 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 onSave = vi.fn();
|
|
render(<ProviderForm {...defaultProps} onSave={onSave} />);
|
|
|
|
const dialog = getDialog();
|
|
|
|
// Get form instance and set values directly
|
|
const idInput = within(dialog).getByPlaceholderText('例如: openai') as HTMLInputElement;
|
|
const nameInput = within(dialog).getByPlaceholderText('例如: OpenAI') as HTMLInputElement;
|
|
const apiKeyInput = within(dialog).getByPlaceholderText('sk-...') as HTMLInputElement;
|
|
const baseUrlInput = within(dialog).getByPlaceholderText('例如: https://api.openai.com/v1') as HTMLInputElement;
|
|
|
|
// Simulate user input by directly setting values
|
|
fireEvent.change(idInput, { target: { value: 'test-provider' } });
|
|
fireEvent.change(nameInput, { target: { value: 'Test Provider' } });
|
|
fireEvent.change(apiKeyInput, { target: { value: 'sk-test-key' } });
|
|
fireEvent.change(baseUrlInput, { target: { value: 'https://api.test.com/v1' } });
|
|
|
|
const okButton = within(dialog).getByRole('button', { name: /保/ });
|
|
fireEvent.click(okButton);
|
|
|
|
// Wait for the onSave to be called
|
|
await vi.waitFor(() => {
|
|
expect(onSave).toHaveBeenCalled();
|
|
}, { timeout: 5000 });
|
|
}, 10000);
|
|
|
|
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: /保/ });
|
|
// TDesign uses t-is-loading class for loading state
|
|
expect(okButton).toHaveClass('t-is-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
|
|
await vi.waitFor(() => {
|
|
expect(screen.getByText('请输入有效的 URL')).toBeInTheDocument();
|
|
});
|
|
}, 15000);
|
|
|
|
it('renders protocol select field with default value', () => {
|
|
render(<ProviderForm {...defaultProps} />);
|
|
|
|
const dialog = getDialog();
|
|
expect(within(dialog).getByText('协议')).toBeInTheDocument();
|
|
});
|
|
|
|
it('includes protocol field in form submission', async () => {
|
|
const onSave = vi.fn();
|
|
render(<ProviderForm {...defaultProps} onSave={onSave} />);
|
|
|
|
const dialog = getDialog();
|
|
|
|
// Get form instance and set values directly
|
|
const idInput = within(dialog).getByPlaceholderText('例如: openai') as HTMLInputElement;
|
|
const nameInput = within(dialog).getByPlaceholderText('例如: OpenAI') as HTMLInputElement;
|
|
const apiKeyInput = within(dialog).getByPlaceholderText('sk-...') as HTMLInputElement;
|
|
const baseUrlInput = within(dialog).getByPlaceholderText('例如: https://api.openai.com/v1') as HTMLInputElement;
|
|
|
|
// Simulate user input by directly setting values
|
|
fireEvent.change(idInput, { target: { value: 'test-provider' } });
|
|
fireEvent.change(nameInput, { target: { value: 'Test Provider' } });
|
|
fireEvent.change(apiKeyInput, { target: { value: 'sk-test-key' } });
|
|
fireEvent.change(baseUrlInput, { target: { value: 'https://api.test.com/v1' } });
|
|
|
|
const okButton = within(dialog).getByRole('button', { name: /保/ });
|
|
fireEvent.click(okButton);
|
|
|
|
// Wait for the onSave to be called
|
|
await vi.waitFor(() => {
|
|
expect(onSave).toHaveBeenCalled();
|
|
// Verify that the saved data includes a protocol field
|
|
const savedData = onSave.mock.calls[0][0];
|
|
expect(savedData).toHaveProperty('protocol');
|
|
}, { timeout: 5000 });
|
|
}, 10000);
|
|
});
|