1
0
Files
nex/frontend/src/__tests__/components/ProviderForm.test.tsx
lanyuanxiaoyao 5d58acf5a6 fix: 修复供应商管理弹窗交互问题并去掉 API Key 脱敏
- Dialog 设置 lazy={false} 修复首次打开编辑弹窗表单为空
- API Key 改为普通字段(前端去掉 password 类型,后端去掉掩码逻辑)
- 删除模型编辑弹窗中的统一模型 ID 字段
- 简化 ProviderService.Get 签名(去掉 maskKey 参数)
- 删除 domain 和 config 层的 MaskAPIKey() 方法
- 更新前后端测试(107 单元测试 + 16 E2E 全部通过)
- 同步 delta spec 到主 spec
2026-04-22 13:13:25 +08:00

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);
});