mirror of
https://github.com/jeffusion/gitea-ai-assistant.git
synced 2026-03-27 10:05:50 +00:00
test(ui): add frontend component tests for LLM management UI (7 tests)
Component tests for all LLM management UI elements using vitest and @testing-library/react with happy-dom: - LLMProviders: Tab container rendering - ModelCombobox: API/recommended/custom tag display, selection, custom input - ProviderList: Async data loading, enable switches, status indicators - RoleAssignment: Role card rendering, Radix Select interaction - TestResultDialog: Success/error state rendering Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
20
frontend/src/components/llm/__tests__/LLMProviders.test.tsx
Normal file
20
frontend/src/components/llm/__tests__/LLMProviders.test.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { LLMProviders } from '../LLMProviders';
|
||||
|
||||
vi.mock('../ProviderList', () => ({
|
||||
ProviderList: () => <div>提供商区域</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../RoleAssignment', () => ({
|
||||
RoleAssignment: () => <div>角色区域</div>,
|
||||
}));
|
||||
|
||||
describe('LLMProviders', () => {
|
||||
it('renders providers and roles sections', () => {
|
||||
render(<LLMProviders />);
|
||||
|
||||
expect(screen.getByText('提供商区域')).toBeInTheDocument();
|
||||
expect(screen.getByText('角色区域')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
71
frontend/src/components/llm/__tests__/ModelCombobox.test.tsx
Normal file
71
frontend/src/components/llm/__tests__/ModelCombobox.test.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { ReactNode } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ModelCombobox } from '../ModelCombobox';
|
||||
import { fetchModels } from '@/services/llmProviderService';
|
||||
|
||||
vi.mock('@/services/llmProviderService', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/services/llmProviderService')>('@/services/llmProviderService');
|
||||
return {
|
||||
...actual,
|
||||
fetchModels: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
function renderWithQuery(ui: ReactNode) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
});
|
||||
return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>);
|
||||
}
|
||||
|
||||
describe('ModelCombobox', () => {
|
||||
it('shows API tag and selects API model', async () => {
|
||||
vi.mocked(fetchModels).mockResolvedValueOnce(['api-model-1']);
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
|
||||
renderWithQuery(
|
||||
<ModelCombobox providerId="p1" providerType="openai_compatible" value="" onChange={onChange} />,
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText('选择或输入模型...');
|
||||
await user.click(input);
|
||||
|
||||
expect(await screen.findByText('api-model-1')).toBeInTheDocument();
|
||||
expect(screen.getByText('API')).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByText('api-model-1'));
|
||||
expect(onChange).toHaveBeenCalledWith('api-model-1');
|
||||
});
|
||||
|
||||
it('shows 推荐 and 自定义 tags and supports custom input', async () => {
|
||||
vi.mocked(fetchModels).mockResolvedValueOnce([]);
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
|
||||
renderWithQuery(
|
||||
<ModelCombobox providerId="p2" providerType="openai_compatible" value="" onChange={onChange} />,
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText('选择或输入模型...');
|
||||
await user.click(input);
|
||||
|
||||
expect((await screen.findAllByText('推荐')).length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('gpt-4o')).toBeInTheDocument();
|
||||
|
||||
await user.clear(input);
|
||||
await user.type(input, 'my-custom-model');
|
||||
|
||||
expect(await screen.findByText('自定义')).toBeInTheDocument();
|
||||
await user.click(screen.getByText('my-custom-model'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledWith('my-custom-model');
|
||||
});
|
||||
});
|
||||
});
|
||||
90
frontend/src/components/llm/__tests__/ProviderList.test.tsx
Normal file
90
frontend/src/components/llm/__tests__/ProviderList.test.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ProviderList } from '../ProviderList';
|
||||
import {
|
||||
fetchProviders,
|
||||
fetchRoles,
|
||||
updateProvider,
|
||||
deleteProvider,
|
||||
testProvider,
|
||||
} from '@/services/llmProviderService';
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/services/llmProviderService', () => ({
|
||||
fetchProviders: vi.fn(),
|
||||
fetchRoles: vi.fn(),
|
||||
updateProvider: vi.fn(),
|
||||
deleteProvider: vi.fn(),
|
||||
testProvider: vi.fn(),
|
||||
}));
|
||||
|
||||
function renderWithQuery(ui: ReactNode) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>);
|
||||
}
|
||||
|
||||
describe('ProviderList', () => {
|
||||
it('renders providers, enable states and hasKey indicators', async () => {
|
||||
vi.mocked(fetchProviders).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'p1',
|
||||
name: 'OpenAI 官方',
|
||||
type: 'openai_responses',
|
||||
baseUrl: null,
|
||||
defaultModel: 'gpt-4o',
|
||||
isEnabled: true,
|
||||
hasKey: true,
|
||||
extraConfig: {},
|
||||
createdAt: '2026-01-01',
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
name: '本地兼容服务',
|
||||
type: 'openai_compatible',
|
||||
baseUrl: 'https://example.com/v1',
|
||||
defaultModel: 'qwen-plus',
|
||||
isEnabled: false,
|
||||
hasKey: false,
|
||||
extraConfig: {},
|
||||
createdAt: '2026-01-01',
|
||||
},
|
||||
]);
|
||||
vi.mocked(fetchRoles).mockResolvedValueOnce([]);
|
||||
vi.mocked(updateProvider).mockResolvedValue({} as never);
|
||||
vi.mocked(deleteProvider).mockResolvedValue(undefined);
|
||||
vi.mocked(testProvider).mockResolvedValue({ success: true });
|
||||
|
||||
renderWithQuery(<ProviderList />);
|
||||
|
||||
expect(await screen.findByText('模型提供商')).toBeInTheDocument();
|
||||
expect(await screen.findByText('OpenAI 官方')).toBeInTheDocument();
|
||||
expect(await screen.findByText('本地兼容服务')).toBeInTheDocument();
|
||||
expect(screen.getByText('OpenAI Responses')).toBeInTheDocument();
|
||||
expect(screen.getByText('OpenAI 兼容')).toBeInTheDocument();
|
||||
expect(screen.getByText('就绪')).toBeInTheDocument();
|
||||
expect(screen.getByText('无 Key')).toBeInTheDocument();
|
||||
|
||||
const switches = screen.getAllByRole('switch');
|
||||
expect(switches).toHaveLength(2);
|
||||
expect(switches[0]).toHaveAttribute('data-state', 'checked');
|
||||
expect(switches[1]).toHaveAttribute('data-state', 'unchecked');
|
||||
|
||||
const testButtons = screen.getAllByTitle('测试连接');
|
||||
expect(testButtons).toHaveLength(2);
|
||||
expect(testButtons[0]).toBeEnabled();
|
||||
expect(testButtons[1]).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { ReactNode } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { RoleAssignment } from '../RoleAssignment';
|
||||
import { fetchProviders, fetchRoles, setRole } from '@/services/llmProviderService';
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/services/llmProviderService', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/services/llmProviderService')>('@/services/llmProviderService');
|
||||
return {
|
||||
...actual,
|
||||
fetchProviders: vi.fn(),
|
||||
fetchRoles: vi.fn(),
|
||||
setRole: vi.fn(),
|
||||
fetchModels: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
});
|
||||
|
||||
function renderWithQuery(ui: ReactNode) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>);
|
||||
}
|
||||
|
||||
describe('RoleAssignment', () => {
|
||||
it('renders role cards and supports provider/model editing', async () => {
|
||||
vi.mocked(fetchProviders).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'p1',
|
||||
name: 'OpenAI',
|
||||
type: 'openai_responses',
|
||||
baseUrl: null,
|
||||
defaultModel: 'gpt-4o-mini',
|
||||
isEnabled: true,
|
||||
hasKey: true,
|
||||
extraConfig: {},
|
||||
createdAt: '2026-01-01',
|
||||
},
|
||||
]);
|
||||
|
||||
vi.mocked(fetchRoles).mockResolvedValueOnce([
|
||||
{
|
||||
role: 'legacy',
|
||||
providerId: 'p1',
|
||||
providerName: 'OpenAI',
|
||||
providerType: 'openai_responses',
|
||||
model: 'gpt-4o',
|
||||
},
|
||||
]);
|
||||
|
||||
vi.mocked(setRole).mockResolvedValue({
|
||||
role: 'planner',
|
||||
providerId: 'p1',
|
||||
providerName: 'OpenAI',
|
||||
providerType: 'openai_responses',
|
||||
model: 'custom-planner-model',
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderWithQuery(<RoleAssignment />);
|
||||
|
||||
expect(await screen.findByText('角色分配')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Legacy 审查')).toBeInTheDocument();
|
||||
expect(await screen.findByText('规划器 Planner')).toBeInTheDocument();
|
||||
|
||||
// Radix Select renders placeholder in a span with pointer-events: none.
|
||||
// Click the trigger button (parent) instead of the placeholder text.
|
||||
const providerPlaceholders = screen.getAllByText('选择提供商');
|
||||
const triggerButton = providerPlaceholders[0].closest('button')!;
|
||||
await user.click(triggerButton);
|
||||
await user.click(await screen.findByRole('option', { name: /OpenAI/ }));
|
||||
|
||||
const modelInputs = screen.getAllByPlaceholderText('选择或输入模型...') as HTMLInputElement[];
|
||||
await waitFor(() => {
|
||||
expect(modelInputs[1].value).toBe('gpt-4o-mini');
|
||||
});
|
||||
|
||||
await user.clear(modelInputs[1]);
|
||||
await user.type(modelInputs[1], 'custom-planner-model');
|
||||
expect(modelInputs[1].value).toBe('custom-planner-model');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { TestResultDialog } from '../TestResultDialog';
|
||||
|
||||
describe('TestResultDialog', () => {
|
||||
it('renders success state with latency, model and message', () => {
|
||||
render(
|
||||
<TestResultDialog
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
providerName="DeepSeek"
|
||||
result={{
|
||||
success: true,
|
||||
latencyMs: 123,
|
||||
model: 'deepseek-chat',
|
||||
message: '连接已建立',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('测试结果 - DeepSeek')).toBeInTheDocument();
|
||||
expect(screen.getByText('连接成功')).toBeInTheDocument();
|
||||
expect(screen.getByText('延迟:')).toBeInTheDocument();
|
||||
expect(screen.getByText('123 ms')).toBeInTheDocument();
|
||||
expect(screen.getByText('模型:')).toBeInTheDocument();
|
||||
expect(screen.getByText('deepseek-chat')).toBeInTheDocument();
|
||||
expect(screen.getByText('AI 响应:')).toBeInTheDocument();
|
||||
expect(screen.getByText('连接已建立')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders error state and closes via button', async () => {
|
||||
const onOpenChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<TestResultDialog
|
||||
open
|
||||
onOpenChange={onOpenChange}
|
||||
providerName="OpenAI"
|
||||
result={{
|
||||
success: false,
|
||||
latencyMs: 789,
|
||||
error: '认证失败',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('测试失败')).toBeInTheDocument();
|
||||
expect(screen.getByText('延迟:')).toBeInTheDocument();
|
||||
expect(screen.getByText('789 ms')).toBeInTheDocument();
|
||||
expect(screen.getByText('错误:')).toBeInTheDocument();
|
||||
expect(screen.getByText('认证失败')).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '关闭' }));
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user