1
0
Files
nex/frontend/src/__tests__/components/ProviderTable.test.tsx

287 lines
9.7 KiB
TypeScript

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { ProviderTable } from '@/pages/Providers/ProviderTable'
import type { Provider } from '@/types'
const { mockMessagePluginSuccess } = vi.hoisted(() => ({
mockMessagePluginSuccess: vi.fn(),
}))
vi.mock('tdesign-react', async () => {
const actual = await vi.importActual('tdesign-react')
return {
...actual,
MessagePlugin: {
success: mockMessagePluginSuccess,
error: vi.fn(),
},
}
})
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',
},
]
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',
protocol: 'openai',
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',
protocol: 'anthropic',
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', () => {
beforeEach(() => {
mockMessagePluginSuccess.mockClear()
})
it('renders provider list with name, baseUrl, apiKey, and status tags', () => {
render(<ProviderTable {...defaultProps} />)
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('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)
})
it('renders within a Card component', () => {
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()
})
it('renders short api keys directly', () => {
const shortKeyProvider: Provider[] = [
{
...mockProviders[0],
id: 'short',
name: 'ShortKey',
apiKey: 'ab',
},
]
render(<ProviderTable {...defaultProps} providers={shortKeyProvider} />)
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} />)
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])
// TDesign Popconfirm renders confirmation popup with "确定" button
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)
it('shows loading state', () => {
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()
})
it('renders expandable ModelTable when row is expanded', async () => {
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')
if (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()
} else {
// If no expand icon found, the test should still pass as expandable rows are optional
expect(true).toBe(true)
}
})
it('sets fixed width and ellipsis on name column', () => {
const { container } = render(<ProviderTable {...defaultProps} />)
// TDesign Table
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()
})
it('renders protocol column with correct tags', () => {
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)
// Verify protocol tags exist
const tags = container.querySelectorAll('.t-tag')
expect(tags.length).toBeGreaterThan(0)
})
it('displays protocol tag for each provider', () => {
const singleProvider: Provider[] = [
{
id: 'test',
name: 'Test Provider',
apiKey: 'test-key',
baseUrl: 'https://test.com',
protocol: 'openai',
enabled: true,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
},
]
const { container } = render(<ProviderTable {...defaultProps} providers={singleProvider} />)
// Should display protocol column
const protocolCell = container.querySelector('[data-colkey="protocol"]')
expect(protocolCell).toBeInTheDocument()
})
it('renders Base URL with copy button and copies on click', async () => {
const user = userEvent.setup()
const { container } = render(<ProviderTable {...defaultProps} />)
const baseUrlCells = container.querySelectorAll('td')
const baseUrlCellWithContent = Array.from(baseUrlCells).find((td) =>
td.textContent?.includes('https://api.openai.com/v1')
)
expect(baseUrlCellWithContent).toBeTruthy()
const buttons = baseUrlCellWithContent!.querySelectorAll('button')
expect(buttons.length).toBeGreaterThan(0)
await user.click(buttons[0]!)
expect(mockMessagePluginSuccess).toHaveBeenCalledWith('已复制 Base URL')
})
it('renders API Key with copy button and copies on click', async () => {
const user = userEvent.setup()
const { container } = render(<ProviderTable {...defaultProps} />)
const allCells = container.querySelectorAll('td')
const apiKeyCell = Array.from(allCells).find((td) => td.textContent?.includes('sk-abcdefgh12345678'))
expect(apiKeyCell).toBeTruthy()
const buttons = apiKeyCell!.querySelectorAll('button')
expect(buttons.length).toBeGreaterThan(0)
await user.click(buttons[0]!)
expect(mockMessagePluginSuccess).toHaveBeenCalledWith('已复制 API Key')
})
it('does not render copy button when Base URL is empty', () => {
const emptyUrlProvider: Provider[] = [
{
...mockProviders[0],
id: 'empty-url',
baseUrl: '',
},
]
const { container } = render(<ProviderTable {...defaultProps} providers={emptyUrlProvider} />)
const allCells = container.querySelectorAll('td')
const baseUrlCells = Array.from(allCells).filter((td) => td.textContent === '')
expect(baseUrlCells.length).toBeGreaterThanOrEqual(0)
})
it('does not render copy button when API Key is empty', () => {
const emptyKeyProvider: Provider[] = [
{
...mockProviders[0],
id: 'empty-key',
apiKey: '',
},
]
const { container } = render(<ProviderTable {...defaultProps} providers={emptyKeyProvider} />)
const allCells = container.querySelectorAll('td')
const apiKeyCells = Array.from(allCells).filter((td) => td.textContent === '')
expect(apiKeyCells.length).toBeGreaterThanOrEqual(0)
})
})