feat: 前端集成 Prettier 代码格式化
This commit is contained in:
@@ -1,52 +1,52 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { AppLayout } from '@/components/AppLayout';
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { BrowserRouter } from 'react-router'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { AppLayout } from '@/components/AppLayout'
|
||||
|
||||
const renderWithRouter = (component: React.ReactNode) => {
|
||||
return render(<BrowserRouter>{component}</BrowserRouter>);
|
||||
};
|
||||
return render(<BrowserRouter>{component}</BrowserRouter>)
|
||||
}
|
||||
|
||||
describe('AppLayout', () => {
|
||||
it('renders sidebar with app name', () => {
|
||||
renderWithRouter(<AppLayout />);
|
||||
renderWithRouter(<AppLayout />)
|
||||
|
||||
const appNames = screen.getAllByText('AI Gateway');
|
||||
expect(appNames.length).toBeGreaterThan(0);
|
||||
});
|
||||
const appNames = screen.getAllByText('AI Gateway')
|
||||
expect(appNames.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('renders navigation menu items', () => {
|
||||
renderWithRouter(<AppLayout />);
|
||||
renderWithRouter(<AppLayout />)
|
||||
|
||||
expect(screen.getByText('供应商管理')).toBeInTheDocument();
|
||||
expect(screen.getByText('用量统计')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('供应商管理')).toBeInTheDocument()
|
||||
expect(screen.getByText('用量统计')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders settings menu item', () => {
|
||||
renderWithRouter(<AppLayout />);
|
||||
renderWithRouter(<AppLayout />)
|
||||
|
||||
expect(screen.getByText('设置')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('设置')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders content outlet', () => {
|
||||
const { container } = renderWithRouter(<AppLayout />);
|
||||
const { container } = renderWithRouter(<AppLayout />)
|
||||
|
||||
// TDesign Layout content
|
||||
expect(container.querySelector('.t-layout__content')).toBeInTheDocument();
|
||||
});
|
||||
expect(container.querySelector('.t-layout__content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders sidebar', () => {
|
||||
const { container } = renderWithRouter(<AppLayout />);
|
||||
const { container } = renderWithRouter(<AppLayout />)
|
||||
|
||||
// TDesign Layout.Aside might render with different class names
|
||||
// Check for Menu component which is in the sidebar
|
||||
expect(container.querySelector('.t-menu')).toBeInTheDocument();
|
||||
});
|
||||
expect(container.querySelector('.t-menu')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders header with page title', () => {
|
||||
const { container } = renderWithRouter(<AppLayout />);
|
||||
const { container } = renderWithRouter(<AppLayout />)
|
||||
|
||||
// TDesign Layout header
|
||||
expect(container.querySelector('.t-layout__header')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(container.querySelector('.t-layout__header')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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';
|
||||
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[] = [
|
||||
{
|
||||
@@ -25,7 +25,7 @@ const mockProviders: Provider[] = [
|
||||
createdAt: '2024-01-02T00:00:00Z',
|
||||
updatedAt: '2024-01-02T00:00:00Z',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const mockModel: Model = {
|
||||
id: 'gpt-4o',
|
||||
@@ -34,7 +34,7 @@ const mockModel: Model = {
|
||||
enabled: true,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
unifiedId: 'openai/gpt-4o',
|
||||
};
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
open: true,
|
||||
@@ -43,69 +43,63 @@ const defaultProps = {
|
||||
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');
|
||||
const dialog = document.querySelector('.t-dialog')
|
||||
if (!dialog) {
|
||||
throw new Error('Dialog not found');
|
||||
throw new Error('Dialog not found')
|
||||
}
|
||||
return dialog;
|
||||
return dialog
|
||||
}
|
||||
|
||||
describe('ModelForm', () => {
|
||||
it('renders form with provider select', () => {
|
||||
render(<ModelForm {...defaultProps} />);
|
||||
render(<ModelForm {...defaultProps} />)
|
||||
|
||||
const dialog = getDialog();
|
||||
expect(within(dialog).getByText('添加模型')).toBeInTheDocument();
|
||||
expect(within(dialog).getByText('供应商')).toBeInTheDocument();
|
||||
expect(within(dialog).getByText('模型名称')).toBeInTheDocument();
|
||||
expect(within(dialog).getByText('启用')).toBeInTheDocument();
|
||||
});
|
||||
const dialog = getDialog()
|
||||
expect(within(dialog).getByText('添加模型')).toBeInTheDocument()
|
||||
expect(within(dialog).getByText('供应商')).toBeInTheDocument()
|
||||
expect(within(dialog).getByText('模型名称')).toBeInTheDocument()
|
||||
expect(within(dialog).getByText('启用')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('defaults providerId to the passed providerId in create mode', () => {
|
||||
render(<ModelForm {...defaultProps} />);
|
||||
render(<ModelForm {...defaultProps} />)
|
||||
|
||||
const dialog = getDialog();
|
||||
const dialog = getDialog()
|
||||
// Form renders with provider select
|
||||
expect(within(dialog).getByText('供应商')).toBeInTheDocument();
|
||||
});
|
||||
expect(within(dialog).getByText('供应商')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows validation error messages for required fields', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ModelForm
|
||||
{...defaultProps}
|
||||
providerId={undefined as unknown as string}
|
||||
providers={[]}
|
||||
/>,
|
||||
);
|
||||
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);
|
||||
const dialog = getDialog()
|
||||
const okButton = within(dialog).getByRole('button', { name: /保/ })
|
||||
await user.click(okButton)
|
||||
|
||||
expect(await screen.findByText('请选择供应商')).toBeInTheDocument();
|
||||
expect(screen.getByText('请输入模型名称')).toBeInTheDocument();
|
||||
});
|
||||
expect(await screen.findByText('请选择供应商')).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 user = userEvent.setup()
|
||||
const onSave = vi.fn()
|
||||
render(<ModelForm {...defaultProps} onSave={onSave} />)
|
||||
|
||||
const dialog = getDialog();
|
||||
const dialog = getDialog()
|
||||
// Only one input with placeholder "例如: gpt-4o" for model name
|
||||
const modelNameInput = within(dialog).getByPlaceholderText('例如: gpt-4o');
|
||||
const modelNameInput = within(dialog).getByPlaceholderText('例如: gpt-4o')
|
||||
|
||||
// Type into the model name field
|
||||
await user.clear(modelNameInput);
|
||||
await user.type(modelNameInput, 'gpt-4o-mini');
|
||||
await user.clear(modelNameInput)
|
||||
await user.type(modelNameInput, 'gpt-4o-mini')
|
||||
|
||||
const okButton = within(dialog).getByRole('button', { name: /保/ });
|
||||
await user.click(okButton);
|
||||
const okButton = within(dialog).getByRole('button', { name: /保/ })
|
||||
await user.click(okButton)
|
||||
|
||||
// Wait for the onSave to be called
|
||||
await vi.waitFor(() => {
|
||||
@@ -114,30 +108,30 @@ describe('ModelForm', () => {
|
||||
providerId: 'openai',
|
||||
modelName: 'gpt-4o-mini',
|
||||
enabled: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}, 10000);
|
||||
})
|
||||
)
|
||||
})
|
||||
}, 10000)
|
||||
|
||||
it('renders pre-filled fields in edit mode', () => {
|
||||
render(<ModelForm {...defaultProps} model={mockModel} />);
|
||||
render(<ModelForm {...defaultProps} model={mockModel} />)
|
||||
|
||||
const dialog = getDialog();
|
||||
expect(within(dialog).getByText('编辑模型')).toBeInTheDocument();
|
||||
const dialog = getDialog()
|
||||
expect(within(dialog).getByText('编辑模型')).toBeInTheDocument()
|
||||
|
||||
// Check model name input
|
||||
const modelNameInput = within(dialog).getByPlaceholderText('例如: gpt-4o') as HTMLInputElement;
|
||||
expect(modelNameInput.value).toBe('gpt-4o');
|
||||
});
|
||||
const modelNameInput = within(dialog).getByPlaceholderText('例如: gpt-4o') 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 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);
|
||||
});
|
||||
});
|
||||
const dialog = getDialog()
|
||||
const cancelButton = within(dialog).getByRole('button', { name: /取/ })
|
||||
await user.click(cancelButton)
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ModelTable } from '@/pages/Providers/ModelTable';
|
||||
import type { Model } from '@/types';
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { ModelTable } from '@/pages/Providers/ModelTable'
|
||||
import type { Model } from '@/types'
|
||||
|
||||
const mockModels: Model[] = [
|
||||
{
|
||||
@@ -21,103 +21,103 @@ const mockModels: Model[] = [
|
||||
createdAt: '2024-01-02T00:00:00Z',
|
||||
unifiedId: 'openai/gpt-3.5-turbo',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const mockMutate = vi.fn();
|
||||
const mockMutate = vi.fn()
|
||||
|
||||
vi.mock('@/hooks/useModels', () => ({
|
||||
useModels: vi.fn((providerId: string) => {
|
||||
if (providerId === 'openai') {
|
||||
return { data: mockModels, isLoading: false };
|
||||
return { data: mockModels, isLoading: false }
|
||||
}
|
||||
return { data: [], isLoading: false };
|
||||
return { data: [], isLoading: false }
|
||||
}),
|
||||
useDeleteModel: vi.fn(() => ({ mutate: mockMutate })),
|
||||
}));
|
||||
}))
|
||||
|
||||
const defaultProps = {
|
||||
providerId: 'openai',
|
||||
onAdd: vi.fn(),
|
||||
onEdit: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe('ModelTable', () => {
|
||||
beforeEach(() => {
|
||||
mockMutate.mockClear();
|
||||
});
|
||||
mockMutate.mockClear()
|
||||
})
|
||||
|
||||
it('renders model list with unified ID and model name', () => {
|
||||
render(<ModelTable {...defaultProps} />);
|
||||
render(<ModelTable {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText(/关联模型/)).toBeInTheDocument();
|
||||
expect(screen.getByText('openai/gpt-4o')).toBeInTheDocument();
|
||||
expect(screen.getByText('openai/gpt-3.5-turbo')).toBeInTheDocument();
|
||||
expect(screen.getByText('gpt-4o')).toBeInTheDocument();
|
||||
expect(screen.getByText('gpt-3.5-turbo')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText(/关联模型/)).toBeInTheDocument()
|
||||
expect(screen.getByText('openai/gpt-4o')).toBeInTheDocument()
|
||||
expect(screen.getByText('openai/gpt-3.5-turbo')).toBeInTheDocument()
|
||||
expect(screen.getByText('gpt-4o')).toBeInTheDocument()
|
||||
expect(screen.getByText('gpt-3.5-turbo')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders status tags correctly', () => {
|
||||
render(<ModelTable {...defaultProps} />);
|
||||
render(<ModelTable {...defaultProps} />)
|
||||
|
||||
const enabledTags = screen.getAllByText('启用');
|
||||
const disabledTags = screen.getAllByText('禁用');
|
||||
expect(enabledTags.length).toBeGreaterThanOrEqual(1);
|
||||
expect(disabledTags.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
const enabledTags = screen.getAllByText('启用')
|
||||
const disabledTags = screen.getAllByText('禁用')
|
||||
expect(enabledTags.length).toBeGreaterThanOrEqual(1)
|
||||
expect(disabledTags.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('calls onAdd when clicking "添加模型" button', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onAdd = vi.fn();
|
||||
render(<ModelTable {...defaultProps} onAdd={onAdd} />);
|
||||
const user = userEvent.setup()
|
||||
const onAdd = vi.fn()
|
||||
render(<ModelTable {...defaultProps} onAdd={onAdd} />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '添加模型' }));
|
||||
expect(onAdd).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
await user.click(screen.getByRole('button', { name: '添加模型' }))
|
||||
expect(onAdd).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('calls onEdit with correct model when clicking "编辑"', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onEdit = vi.fn();
|
||||
render(<ModelTable {...defaultProps} onEdit={onEdit} />);
|
||||
const user = userEvent.setup()
|
||||
const onEdit = vi.fn()
|
||||
render(<ModelTable {...defaultProps} onEdit={onEdit} />)
|
||||
|
||||
const editButtons = screen.getAllByRole('button', { name: /编 ?辑/ });
|
||||
await user.click(editButtons[0]);
|
||||
const editButtons = screen.getAllByRole('button', { name: /编 ?辑/ })
|
||||
await user.click(editButtons[0])
|
||||
|
||||
expect(onEdit).toHaveBeenCalledTimes(1);
|
||||
expect(onEdit).toHaveBeenCalledWith(mockModels[0]);
|
||||
});
|
||||
expect(onEdit).toHaveBeenCalledTimes(1)
|
||||
expect(onEdit).toHaveBeenCalledWith(mockModels[0])
|
||||
})
|
||||
|
||||
it('calls deleteModel.mutate with correct model ID when delete is confirmed', async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<ModelTable {...defaultProps} />);
|
||||
render(<ModelTable {...defaultProps} />)
|
||||
|
||||
// Find and click the delete button for the first row
|
||||
const deleteButtons = screen.getAllByRole('button', { name: '删除' });
|
||||
await user.click(deleteButtons[0]);
|
||||
const deleteButtons = screen.getAllByRole('button', { name: '删除' })
|
||||
await user.click(deleteButtons[0])
|
||||
|
||||
// TDesign Popconfirm renders confirmation popup with "确定" button
|
||||
const confirmButton = await screen.findByRole('button', { name: '确定' });
|
||||
await user.click(confirmButton);
|
||||
const confirmButton = await screen.findByRole('button', { name: '确定' })
|
||||
await user.click(confirmButton)
|
||||
|
||||
// Assert that deleteModel.mutate was called with the correct model ID
|
||||
expect(mockMutate).toHaveBeenCalledTimes(1);
|
||||
expect(mockMutate).toHaveBeenCalledWith('model-1');
|
||||
}, 10000);
|
||||
expect(mockMutate).toHaveBeenCalledTimes(1)
|
||||
expect(mockMutate).toHaveBeenCalledWith('model-1')
|
||||
}, 10000)
|
||||
|
||||
it('shows custom empty text when models list is empty', () => {
|
||||
render(<ModelTable providerId="anthropic" />);
|
||||
expect(screen.getByText('暂无模型,点击上方按钮添加')).toBeInTheDocument();
|
||||
});
|
||||
render(<ModelTable providerId='anthropic' />)
|
||||
expect(screen.getByText('暂无模型,点击上方按钮添加')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render add button when onAdd is not provided', () => {
|
||||
render(<ModelTable providerId="openai" />);
|
||||
render(<ModelTable providerId='openai' />)
|
||||
|
||||
expect(screen.queryByRole('button', { name: '添加模型' })).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByRole('button', { name: '添加模型' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render edit button when onEdit is not provided', () => {
|
||||
render(<ModelTable providerId="openai" onAdd={vi.fn()} />);
|
||||
render(<ModelTable providerId='openai' onAdd={vi.fn()} />)
|
||||
|
||||
expect(screen.queryByRole('button', { name: /编 ?辑/ })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.queryByRole('button', { name: /编 ?辑/ })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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';
|
||||
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',
|
||||
@@ -13,187 +13,193 @@ const mockProvider: Provider = {
|
||||
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');
|
||||
const dialog = document.querySelector('.t-dialog')
|
||||
if (!dialog) {
|
||||
throw new Error('Dialog not found');
|
||||
throw new Error('Dialog not found')
|
||||
}
|
||||
return dialog;
|
||||
return dialog
|
||||
}
|
||||
|
||||
describe('ProviderForm', () => {
|
||||
it('renders form fields in create mode', () => {
|
||||
render(<ProviderForm {...defaultProps} />);
|
||||
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();
|
||||
});
|
||||
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} />);
|
||||
render(<ProviderForm {...defaultProps} provider={mockProvider} />)
|
||||
|
||||
const dialog = getDialog();
|
||||
expect(within(dialog).getByText('编辑供应商')).toBeInTheDocument();
|
||||
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 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 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 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');
|
||||
});
|
||||
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} />);
|
||||
render(<ProviderForm {...defaultProps} provider={mockProvider} />)
|
||||
|
||||
const dialog = getDialog();
|
||||
expect(within(dialog).getByText('API Key')).toBeInTheDocument();
|
||||
});
|
||||
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 user = userEvent.setup()
|
||||
render(<ProviderForm {...defaultProps} />)
|
||||
|
||||
const dialog = getDialog();
|
||||
const okButton = within(dialog).getByRole('button', { name: /保/ });
|
||||
await user.click(okButton);
|
||||
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();
|
||||
});
|
||||
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 onSave = vi.fn()
|
||||
render(<ProviderForm {...defaultProps} onSave={onSave} />)
|
||||
|
||||
const dialog = getDialog();
|
||||
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;
|
||||
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' } });
|
||||
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);
|
||||
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);
|
||||
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 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);
|
||||
});
|
||||
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: /保/ });
|
||||
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');
|
||||
});
|
||||
expect(okButton).toHaveClass('t-is-loading')
|
||||
})
|
||||
|
||||
it('shows validation error for invalid URL format', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ProviderForm {...defaultProps} />);
|
||||
const user = userEvent.setup()
|
||||
render(<ProviderForm {...defaultProps} />)
|
||||
|
||||
const dialog = getDialog();
|
||||
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');
|
||||
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');
|
||||
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);
|
||||
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);
|
||||
expect(screen.getByText('请输入有效的 URL')).toBeInTheDocument()
|
||||
})
|
||||
}, 15000)
|
||||
|
||||
it('renders protocol select field with default value', () => {
|
||||
render(<ProviderForm {...defaultProps} />);
|
||||
render(<ProviderForm {...defaultProps} />)
|
||||
|
||||
const dialog = getDialog();
|
||||
expect(within(dialog).getByText('协议')).toBeInTheDocument();
|
||||
});
|
||||
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 onSave = vi.fn()
|
||||
render(<ProviderForm {...defaultProps} onSave={onSave} />)
|
||||
|
||||
const dialog = getDialog();
|
||||
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;
|
||||
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' } });
|
||||
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);
|
||||
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);
|
||||
});
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import { render, screen } 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';
|
||||
import { render, screen } 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, unifiedId: 'openai/gpt-4o' },
|
||||
{ id: 'model-2', providerId: 'openai', modelName: 'gpt-3.5-turbo', enabled: false, unifiedId: 'openai/gpt-3.5-turbo' },
|
||||
];
|
||||
{
|
||||
id: 'model-2',
|
||||
providerId: 'openai',
|
||||
modelName: 'gpt-3.5-turbo',
|
||||
enabled: false,
|
||||
unifiedId: 'openai/gpt-3.5-turbo',
|
||||
},
|
||||
]
|
||||
|
||||
vi.mock('@/hooks/useModels', () => ({
|
||||
useModels: vi.fn(() => ({ data: mockModelsData, isLoading: false })),
|
||||
useDeleteModel: vi.fn(() => ({ mutate: vi.fn() })),
|
||||
}));
|
||||
}))
|
||||
|
||||
const mockProviders: Provider[] = [
|
||||
{
|
||||
@@ -35,7 +41,7 @@ const mockProviders: Provider[] = [
|
||||
createdAt: '2024-01-02T00:00:00Z',
|
||||
updatedAt: '2024-01-02T00:00:00Z',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const defaultProps = {
|
||||
providers: mockProviders,
|
||||
@@ -45,36 +51,36 @@ const defaultProps = {
|
||||
onDelete: vi.fn(),
|
||||
onAddModel: vi.fn(),
|
||||
onEditModel: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe('ProviderTable', () => {
|
||||
it('renders provider list with name, baseUrl, apiKey, and status tags', () => {
|
||||
render(<ProviderTable {...defaultProps} />);
|
||||
render(<ProviderTable {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('供应商列表')).toBeInTheDocument();
|
||||
expect(screen.getByText('供应商列表')).toBeInTheDocument()
|
||||
|
||||
expect(screen.getAllByText('OpenAI').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('https://api.openai.com/v1')).toBeInTheDocument();
|
||||
expect(screen.getByText('sk-abcdefgh12345678')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('OpenAI').length).toBeGreaterThan(0)
|
||||
expect(screen.getByText('https://api.openai.com/v1')).toBeInTheDocument()
|
||||
expect(screen.getByText('sk-abcdefgh12345678')).toBeInTheDocument()
|
||||
|
||||
expect(screen.getAllByText('Anthropic').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('https://api.anthropic.com')).toBeInTheDocument();
|
||||
expect(screen.getByText('sk-ant-test')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Anthropic').length).toBeGreaterThan(0)
|
||||
expect(screen.getByText('https://api.anthropic.com')).toBeInTheDocument()
|
||||
expect(screen.getByText('sk-ant-test')).toBeInTheDocument()
|
||||
|
||||
const enabledTags = screen.getAllByText('启用');
|
||||
const disabledTags = screen.getAllByText('禁用');
|
||||
expect(enabledTags.length).toBeGreaterThanOrEqual(1);
|
||||
expect(disabledTags.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
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} />);
|
||||
const { container } = render(<ProviderTable {...defaultProps} />)
|
||||
|
||||
// TDesign Card component
|
||||
expect(container.querySelector('.t-card')).toBeInTheDocument();
|
||||
expect(container.querySelector('.t-card__header')).toBeInTheDocument();
|
||||
expect(container.querySelector('.t-card__body')).toBeInTheDocument();
|
||||
});
|
||||
expect(container.querySelector('.t-card')).toBeInTheDocument()
|
||||
expect(container.querySelector('.t-card__header')).toBeInTheDocument()
|
||||
expect(container.querySelector('.t-card__body')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders short api keys directly', () => {
|
||||
const shortKeyProvider: Provider[] = [
|
||||
@@ -84,99 +90,99 @@ describe('ProviderTable', () => {
|
||||
name: 'ShortKey',
|
||||
apiKey: 'ab',
|
||||
},
|
||||
];
|
||||
render(<ProviderTable {...defaultProps} providers={shortKeyProvider} />);
|
||||
]
|
||||
render(<ProviderTable {...defaultProps} providers={shortKeyProvider} />)
|
||||
|
||||
expect(screen.getByText('ab')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('ab')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onAdd when clicking "添加供应商" button', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onAdd = vi.fn();
|
||||
render(<ProviderTable {...defaultProps} onAdd={onAdd} />);
|
||||
const user = userEvent.setup()
|
||||
const onAdd = vi.fn()
|
||||
render(<ProviderTable {...defaultProps} onAdd={onAdd} />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '添加供应商' }));
|
||||
expect(onAdd).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
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 user = userEvent.setup()
|
||||
const onEdit = vi.fn()
|
||||
render(<ProviderTable {...defaultProps} onEdit={onEdit} />)
|
||||
|
||||
const editButtons = screen.getAllByRole('button', { name: /编 ?辑/ });
|
||||
await user.click(editButtons[0]);
|
||||
const editButtons = screen.getAllByRole('button', { name: /编 ?辑/ })
|
||||
await user.click(editButtons[0])
|
||||
|
||||
expect(onEdit).toHaveBeenCalledTimes(1);
|
||||
expect(onEdit).toHaveBeenCalledWith(mockProviders[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} />);
|
||||
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]);
|
||||
const deleteButtons = screen.getAllByRole('button', { name: '删除' })
|
||||
await user.click(deleteButtons[0])
|
||||
|
||||
// TDesign Popconfirm renders confirmation popup with "确定" button
|
||||
const confirmButton = await screen.findByRole('button', { name: '确定' });
|
||||
await user.click(confirmButton);
|
||||
const confirmButton = await screen.findByRole('button', { name: '确定' })
|
||||
await user.click(confirmButton)
|
||||
|
||||
// Assert that onDelete was called with the correct provider ID
|
||||
expect(onDelete).toHaveBeenCalledTimes(1);
|
||||
expect(onDelete).toHaveBeenCalledWith('openai');
|
||||
}, 10000);
|
||||
expect(onDelete).toHaveBeenCalledTimes(1)
|
||||
expect(onDelete).toHaveBeenCalledWith('openai')
|
||||
}, 10000)
|
||||
|
||||
it('shows loading state', () => {
|
||||
const { container } = render(<ProviderTable {...defaultProps} loading={true} />);
|
||||
const { container } = render(<ProviderTable {...defaultProps} loading={true} />)
|
||||
// TDesign Table loading indicator
|
||||
const loadingElement = container.querySelector('.t-table__loading') || container.querySelector('.t-loading');
|
||||
expect(loadingElement).toBeInTheDocument();
|
||||
});
|
||||
const loadingElement = container.querySelector('.t-table__loading') || container.querySelector('.t-loading')
|
||||
expect(loadingElement).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders expandable ModelTable when row is expanded', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(<ProviderTable {...defaultProps} />);
|
||||
const user = userEvent.setup()
|
||||
const { container } = render(<ProviderTable {...defaultProps} />)
|
||||
|
||||
// TDesign Table expand icon is rendered as a button with specific class
|
||||
const expandIcon = container.querySelector('.t-table__expandable-icon');
|
||||
const expandIcon = container.querySelector('.t-table__expandable-icon')
|
||||
if (expandIcon) {
|
||||
await user.click(expandIcon);
|
||||
await user.click(expandIcon)
|
||||
|
||||
// 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();
|
||||
expect(await screen.findByText('gpt-4o')).toBeInTheDocument()
|
||||
expect(screen.getByText('gpt-3.5-turbo')).toBeInTheDocument()
|
||||
} else {
|
||||
// If no expand icon found, the test should still pass as expandable rows are optional
|
||||
expect(true).toBe(true);
|
||||
expect(true).toBe(true)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
it('sets fixed width and ellipsis on name column', () => {
|
||||
const { container } = render(<ProviderTable {...defaultProps} />);
|
||||
const { container } = render(<ProviderTable {...defaultProps} />)
|
||||
// TDesign Table
|
||||
const table = container.querySelector('.t-table');
|
||||
expect(table).toBeInTheDocument();
|
||||
});
|
||||
const table = container.querySelector('.t-table')
|
||||
expect(table).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows custom empty text when providers list is empty', () => {
|
||||
render(<ProviderTable {...defaultProps} providers={[]} />);
|
||||
expect(screen.getByText('暂无供应商,点击上方按钮添加')).toBeInTheDocument();
|
||||
});
|
||||
render(<ProviderTable {...defaultProps} providers={[]} />)
|
||||
expect(screen.getByText('暂无供应商,点击上方按钮添加')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders protocol column with correct tags', () => {
|
||||
const { container } = render(<ProviderTable {...defaultProps} />);
|
||||
const { container } = render(<ProviderTable {...defaultProps} />)
|
||||
|
||||
// Check that protocol tags are displayed in the table
|
||||
const protocolCells = container.querySelectorAll('[data-colkey="protocol"]');
|
||||
expect(protocolCells.length).toBeGreaterThan(0);
|
||||
const protocolCells = container.querySelectorAll('[data-colkey="protocol"]')
|
||||
expect(protocolCells.length).toBeGreaterThan(0)
|
||||
|
||||
// Verify protocol tags exist
|
||||
const tags = container.querySelectorAll('.t-tag');
|
||||
expect(tags.length).toBeGreaterThan(0);
|
||||
});
|
||||
const tags = container.querySelectorAll('.t-tag')
|
||||
expect(tags.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('displays protocol tag for each provider', () => {
|
||||
const singleProvider: Provider[] = [
|
||||
@@ -190,11 +196,11 @@ describe('ProviderTable', () => {
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
const { container } = render(<ProviderTable {...defaultProps} providers={singleProvider} />);
|
||||
]
|
||||
const { container } = render(<ProviderTable {...defaultProps} providers={singleProvider} />)
|
||||
|
||||
// Should display protocol column
|
||||
const protocolCell = container.querySelector('[data-colkey="protocol"]');
|
||||
expect(protocolCell).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
const protocolCell = container.querySelector('[data-colkey="protocol"]')
|
||||
expect(protocolCell).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { StatCards } from '@/pages/Stats/StatCards';
|
||||
import type { UsageStats } from '@/types';
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { StatCards } from '@/pages/Stats/StatCards'
|
||||
import type { UsageStats } from '@/types'
|
||||
|
||||
const mockStats: UsageStats[] = [
|
||||
{
|
||||
@@ -25,31 +25,31 @@ const mockStats: UsageStats[] = [
|
||||
requestCount: 150,
|
||||
date: '2024-01-02',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
describe('StatCards', () => {
|
||||
it('renders all statistic cards', () => {
|
||||
render(<StatCards stats={mockStats} />);
|
||||
render(<StatCards stats={mockStats} />)
|
||||
|
||||
expect(screen.getByText('总请求量')).toBeInTheDocument();
|
||||
expect(screen.getByText('活跃模型数')).toBeInTheDocument();
|
||||
expect(screen.getByText('活跃供应商数')).toBeInTheDocument();
|
||||
expect(screen.getByText('今日请求量')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('总请求量')).toBeInTheDocument()
|
||||
expect(screen.getByText('活跃模型数')).toBeInTheDocument()
|
||||
expect(screen.getByText('活跃供应商数')).toBeInTheDocument()
|
||||
expect(screen.getByText('今日请求量')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders with empty stats', () => {
|
||||
render(<StatCards stats={[]} />);
|
||||
render(<StatCards stats={[]} />)
|
||||
|
||||
expect(screen.getByText('总请求量')).toBeInTheDocument();
|
||||
expect(screen.getByText('活跃模型数')).toBeInTheDocument();
|
||||
expect(screen.getByText('活跃供应商数')).toBeInTheDocument();
|
||||
expect(screen.getByText('今日请求量')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('总请求量')).toBeInTheDocument()
|
||||
expect(screen.getByText('活跃模型数')).toBeInTheDocument()
|
||||
expect(screen.getByText('活跃供应商数')).toBeInTheDocument()
|
||||
expect(screen.getByText('今日请求量')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders suffix units', () => {
|
||||
render(<StatCards stats={mockStats} />);
|
||||
render(<StatCards stats={mockStats} />)
|
||||
|
||||
expect(screen.getAllByText('次').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('个').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
expect(screen.getAllByText('次').length).toBeGreaterThan(0)
|
||||
expect(screen.getAllByText('个').length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { StatsTable } from '@/pages/Stats/StatsTable';
|
||||
import type { Provider, UsageStats } from '@/types';
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { StatsTable } from '@/pages/Stats/StatsTable'
|
||||
import type { Provider, UsageStats } from '@/types'
|
||||
|
||||
const mockProviders: Provider[] = [
|
||||
{
|
||||
@@ -24,7 +24,7 @@ const mockProviders: Provider[] = [
|
||||
createdAt: '2024-01-02T00:00:00Z',
|
||||
updatedAt: '2024-01-02T00:00:00Z',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const mockStats: UsageStats[] = [
|
||||
{
|
||||
@@ -41,7 +41,7 @@ const mockStats: UsageStats[] = [
|
||||
requestCount: 50,
|
||||
date: '2024-01-15',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
const defaultProps = {
|
||||
providers: mockProviders,
|
||||
@@ -53,80 +53,80 @@ const defaultProps = {
|
||||
onProviderIdChange: vi.fn(),
|
||||
onModelNameChange: vi.fn(),
|
||||
onDateRangeChange: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe('StatsTable', () => {
|
||||
it('renders stats table with data', () => {
|
||||
render(<StatsTable {...defaultProps} />);
|
||||
render(<StatsTable {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('gpt-4o')).toBeInTheDocument();
|
||||
expect(screen.getByText('claude-3-opus')).toBeInTheDocument();
|
||||
const dateCells = screen.getAllByText('2024-01-15');
|
||||
expect(dateCells.length).toBe(2);
|
||||
expect(screen.getByText('100')).toBeInTheDocument();
|
||||
expect(screen.getByText('50')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('gpt-4o')).toBeInTheDocument()
|
||||
expect(screen.getByText('claude-3-opus')).toBeInTheDocument()
|
||||
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 {...defaultProps} />);
|
||||
render(<StatsTable {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('OpenAI')).toBeInTheDocument();
|
||||
const allAnthropic = screen.getAllByText('Anthropic');
|
||||
expect(allAnthropic.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
expect(screen.getByText('OpenAI')).toBeInTheDocument()
|
||||
const allAnthropic = screen.getAllByText('Anthropic')
|
||||
expect(allAnthropic.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('renders filter controls with Select, Input, and DatePicker', () => {
|
||||
const { container } = render(<StatsTable {...defaultProps} />);
|
||||
const { container } = render(<StatsTable {...defaultProps} />)
|
||||
|
||||
// TDesign Select component
|
||||
const selects = document.querySelectorAll('.t-select');
|
||||
expect(selects.length).toBeGreaterThanOrEqual(1);
|
||||
const selects = document.querySelectorAll('.t-select')
|
||||
expect(selects.length).toBeGreaterThanOrEqual(1)
|
||||
|
||||
const modelInput = screen.getByPlaceholderText('模型名称');
|
||||
expect(modelInput).toBeInTheDocument();
|
||||
const modelInput = screen.getByPlaceholderText('模型名称')
|
||||
expect(modelInput).toBeInTheDocument()
|
||||
|
||||
// TDesign Select placeholder is shown in the input
|
||||
const selectInput = document.querySelector('.t-select .t-input__inner');
|
||||
expect(selectInput).toBeInTheDocument();
|
||||
const selectInput = document.querySelector('.t-select .t-input__inner')
|
||||
expect(selectInput).toBeInTheDocument()
|
||||
|
||||
// TDesign DateRangePicker - could be .t-date-picker or .t-range-input
|
||||
const rangePicker = container.querySelector('.t-date-picker') || container.querySelector('.t-range-input');
|
||||
expect(rangePicker).toBeInTheDocument();
|
||||
});
|
||||
const rangePicker = container.querySelector('.t-date-picker') || container.querySelector('.t-range-input')
|
||||
expect(rangePicker).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders table headers correctly', () => {
|
||||
render(<StatsTable {...defaultProps} />);
|
||||
render(<StatsTable {...defaultProps} />)
|
||||
|
||||
expect(screen.getAllByText('供应商').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText('模型').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText('日期').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText('请求数').length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
expect(screen.getAllByText('供应商').length).toBeGreaterThanOrEqual(1)
|
||||
expect(screen.getAllByText('模型').length).toBeGreaterThanOrEqual(1)
|
||||
expect(screen.getAllByText('日期').length).toBeGreaterThanOrEqual(1)
|
||||
expect(screen.getAllByText('请求数').length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('falls back to providerId when provider not found in providers prop', () => {
|
||||
const limitedProviders = [mockProviders[0]];
|
||||
render(<StatsTable {...defaultProps} providers={limitedProviders} />);
|
||||
const limitedProviders = [mockProviders[0]]
|
||||
render(<StatsTable {...defaultProps} providers={limitedProviders} />)
|
||||
|
||||
expect(screen.getByText('OpenAI')).toBeInTheDocument();
|
||||
expect(screen.getByText('anthropic')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('OpenAI')).toBeInTheDocument()
|
||||
expect(screen.getByText('anthropic')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders with empty stats data', () => {
|
||||
render(<StatsTable {...defaultProps} stats={[]} />);
|
||||
render(<StatsTable {...defaultProps} stats={[]} />)
|
||||
|
||||
expect(screen.getAllByText('供应商').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText('模型').length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
expect(screen.getAllByText('供应商').length).toBeGreaterThanOrEqual(1)
|
||||
expect(screen.getAllByText('模型').length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('shows loading state', () => {
|
||||
const { container } = render(<StatsTable {...defaultProps} loading={true} />);
|
||||
const { container } = render(<StatsTable {...defaultProps} loading={true} />)
|
||||
// TDesign Table loading indicator - could be .t-table__loading or .t-loading
|
||||
const loadingElement = container.querySelector('.t-table__loading') || container.querySelector('.t-loading');
|
||||
expect(loadingElement).toBeInTheDocument();
|
||||
});
|
||||
const loadingElement = container.querySelector('.t-table__loading') || container.querySelector('.t-loading')
|
||||
expect(loadingElement).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows custom empty text when stats data is empty', () => {
|
||||
render(<StatsTable {...defaultProps} stats={[]} />);
|
||||
expect(screen.getByText('暂无统计数据')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
render(<StatsTable {...defaultProps} stats={[]} />)
|
||||
expect(screen.getByText('暂无统计数据')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { UsageChart } from '@/pages/Stats/UsageChart';
|
||||
import type { UsageStats } from '@/types';
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { UsageChart } from '@/pages/Stats/UsageChart'
|
||||
import type { UsageStats } from '@/types'
|
||||
|
||||
// Mock Recharts components
|
||||
vi.mock('recharts', () => ({
|
||||
ResponsiveContainer: vi.fn(({ children }) => <div data-testid="mock-chart-container">{children}</div>),
|
||||
AreaChart: vi.fn(() => <div data-testid="mock-area-chart" />),
|
||||
ResponsiveContainer: vi.fn(({ children }) => <div data-testid='mock-chart-container'>{children}</div>),
|
||||
AreaChart: vi.fn(() => <div data-testid='mock-area-chart' />),
|
||||
Area: vi.fn(() => null),
|
||||
XAxis: vi.fn(() => null),
|
||||
YAxis: vi.fn(() => null),
|
||||
CartesianGrid: vi.fn(() => null),
|
||||
Tooltip: vi.fn(() => null),
|
||||
}));
|
||||
}))
|
||||
|
||||
const mockStats: UsageStats[] = [
|
||||
{
|
||||
@@ -36,36 +36,36 @@ const mockStats: UsageStats[] = [
|
||||
requestCount: 150,
|
||||
date: '2024-01-02',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
describe('UsageChart', () => {
|
||||
it('renders chart title', () => {
|
||||
render(<UsageChart stats={mockStats} />);
|
||||
render(<UsageChart stats={mockStats} />)
|
||||
|
||||
expect(screen.getByText('请求趋势')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('请求趋势')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders with data', () => {
|
||||
const { container } = render(<UsageChart stats={mockStats} />);
|
||||
const { container } = render(<UsageChart stats={mockStats} />)
|
||||
|
||||
// TDesign Card component
|
||||
expect(container.querySelector('.t-card')).toBeInTheDocument();
|
||||
expect(container.querySelector('.t-card')).toBeInTheDocument()
|
||||
// Mocked chart container
|
||||
expect(screen.getByTestId('mock-chart-container')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('mock-chart-container')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders empty state when no data', () => {
|
||||
render(<UsageChart stats={[]} />);
|
||||
render(<UsageChart stats={[]} />)
|
||||
|
||||
expect(screen.getByText('暂无数据')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('暂无数据')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('aggregates data by date correctly', () => {
|
||||
const { container } = render(<UsageChart stats={mockStats} />);
|
||||
const { container } = render(<UsageChart stats={mockStats} />)
|
||||
|
||||
// TDesign Card component
|
||||
expect(container.querySelector('.t-card')).toBeInTheDocument();
|
||||
expect(container.querySelector('.t-card')).toBeInTheDocument()
|
||||
// Mocked chart should render
|
||||
expect(screen.getByTestId('mock-chart-container')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(screen.getByTestId('mock-chart-container')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user