test(web): add comprehensive unit and integration tests for plugins and tools modules (#32220)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
Coding On Star
2026-02-12 10:04:56 +08:00
committed by GitHub
parent 10f85074e8
commit d6b025e91e
195 changed files with 12219 additions and 7840 deletions

View File

@ -0,0 +1,221 @@
import type { ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import NewMCPCard from '../create-card'
// Track the mock functions
const mockCreateMCP = vi.fn().mockResolvedValue({ id: 'new-mcp-id', name: 'New MCP' })
// Mock the service
vi.mock('@/service/use-tools', () => ({
useCreateMCP: () => ({
mutateAsync: mockCreateMCP,
}),
}))
// Mock the MCP Modal
type MockMCPModalProps = {
show: boolean
onConfirm: (info: { name: string, server_url: string }) => void
onHide: () => void
}
vi.mock('../modal', () => ({
default: ({ show, onConfirm, onHide }: MockMCPModalProps) => {
if (!show)
return null
return (
<div data-testid="mcp-modal">
<span>tools.mcp.modal.title</span>
<button data-testid="confirm-btn" onClick={() => onConfirm({ name: 'Test MCP', server_url: 'https://test.com' })}>
Confirm
</button>
<button data-testid="close-btn" onClick={onHide}>
Close
</button>
</div>
)
},
}))
// Mutable workspace manager state
let mockIsCurrentWorkspaceManager = true
// Mock the app context
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager,
isCurrentWorkspaceEditor: true,
}),
}))
// Mock the plugins service
vi.mock('@/service/use-plugins', () => ({
useInstalledPluginList: () => ({
data: { pages: [] },
hasNextPage: false,
isFetchingNextPage: false,
fetchNextPage: vi.fn(),
isLoading: false,
isSuccess: true,
}),
}))
// Mock common service
vi.mock('@/service/common', () => ({
uploadRemoteFileInfo: vi.fn().mockResolvedValue({ url: 'https://example.com/icon.png' }),
}))
describe('NewMCPCard', () => {
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
return ({ children }: { children: ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children)
}
const defaultProps = {
handleCreate: vi.fn(),
}
beforeEach(() => {
mockCreateMCP.mockClear()
mockIsCurrentWorkspaceManager = true
})
describe('Rendering', () => {
it('should render without crashing', () => {
render(<NewMCPCard {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.create.cardTitle')).toBeInTheDocument()
})
it('should render card title', () => {
render(<NewMCPCard {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.create.cardTitle')).toBeInTheDocument()
})
it('should render documentation link', () => {
render(<NewMCPCard {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.create.cardLink')).toBeInTheDocument()
})
it('should render add icon', () => {
render(<NewMCPCard {...defaultProps} />, { wrapper: createWrapper() })
const svgElements = document.querySelectorAll('svg')
expect(svgElements.length).toBeGreaterThan(0)
})
})
describe('User Interactions', () => {
it('should open modal when card is clicked', async () => {
render(<NewMCPCard {...defaultProps} />, { wrapper: createWrapper() })
const cardTitle = screen.getByText('tools.mcp.create.cardTitle')
const clickableArea = cardTitle.closest('.group')
if (clickableArea) {
fireEvent.click(clickableArea)
await waitFor(() => {
expect(screen.getByText('tools.mcp.modal.title')).toBeInTheDocument()
})
}
})
it('should have documentation link with correct target', () => {
render(<NewMCPCard {...defaultProps} />, { wrapper: createWrapper() })
const docLink = screen.getByText('tools.mcp.create.cardLink').closest('a')
expect(docLink).toHaveAttribute('target', '_blank')
expect(docLink).toHaveAttribute('rel', 'noopener noreferrer')
})
})
describe('Non-Manager User', () => {
it('should not render card when user is not workspace manager', () => {
mockIsCurrentWorkspaceManager = false
render(<NewMCPCard {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.queryByText('tools.mcp.create.cardTitle')).not.toBeInTheDocument()
})
})
describe('Styling', () => {
it('should have correct card structure', () => {
render(<NewMCPCard {...defaultProps} />, { wrapper: createWrapper() })
const card = document.querySelector('.rounded-xl')
expect(card).toBeInTheDocument()
})
it('should have clickable cursor style', () => {
render(<NewMCPCard {...defaultProps} />, { wrapper: createWrapper() })
const card = document.querySelector('.cursor-pointer')
expect(card).toBeInTheDocument()
})
})
describe('Modal Interactions', () => {
it('should call create function when modal confirms', async () => {
const handleCreate = vi.fn()
render(<NewMCPCard handleCreate={handleCreate} />, { wrapper: createWrapper() })
// Open the modal
const cardTitle = screen.getByText('tools.mcp.create.cardTitle')
const clickableArea = cardTitle.closest('.group')
if (clickableArea) {
fireEvent.click(clickableArea)
await waitFor(() => {
expect(screen.getByTestId('mcp-modal')).toBeInTheDocument()
})
// Click confirm
const confirmBtn = screen.getByTestId('confirm-btn')
fireEvent.click(confirmBtn)
await waitFor(() => {
expect(mockCreateMCP).toHaveBeenCalledWith({
name: 'Test MCP',
server_url: 'https://test.com',
})
expect(handleCreate).toHaveBeenCalled()
})
}
})
it('should close modal when close button is clicked', async () => {
render(<NewMCPCard {...defaultProps} />, { wrapper: createWrapper() })
// Open the modal
const cardTitle = screen.getByText('tools.mcp.create.cardTitle')
const clickableArea = cardTitle.closest('.group')
if (clickableArea) {
fireEvent.click(clickableArea)
await waitFor(() => {
expect(screen.getByTestId('mcp-modal')).toBeInTheDocument()
})
// Click close
const closeBtn = screen.getByTestId('close-btn')
fireEvent.click(closeBtn)
await waitFor(() => {
expect(screen.queryByTestId('mcp-modal')).not.toBeInTheDocument()
})
}
})
})
})

View File

@ -0,0 +1,245 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import HeadersInput from '../headers-input'
describe('HeadersInput', () => {
const defaultProps = {
headersItems: [],
onChange: vi.fn(),
}
describe('Empty State', () => {
it('should render no headers message when empty', () => {
render(<HeadersInput {...defaultProps} />)
expect(screen.getByText('tools.mcp.modal.noHeaders')).toBeInTheDocument()
})
it('should render add header button when empty and not readonly', () => {
render(<HeadersInput {...defaultProps} />)
expect(screen.getByText('tools.mcp.modal.addHeader')).toBeInTheDocument()
})
it('should not render add header button when empty and readonly', () => {
render(<HeadersInput {...defaultProps} readonly={true} />)
expect(screen.queryByText('tools.mcp.modal.addHeader')).not.toBeInTheDocument()
})
it('should call onChange with new item when add button is clicked', () => {
const onChange = vi.fn()
render(<HeadersInput {...defaultProps} onChange={onChange} />)
const addButton = screen.getByText('tools.mcp.modal.addHeader')
fireEvent.click(addButton)
expect(onChange).toHaveBeenCalledWith([
expect.objectContaining({
key: '',
value: '',
}),
])
})
})
describe('With Headers', () => {
const headersItems = [
{ id: '1', key: 'Authorization', value: 'Bearer token123' },
{ id: '2', key: 'Content-Type', value: 'application/json' },
]
it('should render header items', () => {
render(<HeadersInput {...defaultProps} headersItems={headersItems} />)
expect(screen.getByDisplayValue('Authorization')).toBeInTheDocument()
expect(screen.getByDisplayValue('Bearer token123')).toBeInTheDocument()
expect(screen.getByDisplayValue('Content-Type')).toBeInTheDocument()
expect(screen.getByDisplayValue('application/json')).toBeInTheDocument()
})
it('should render table headers', () => {
render(<HeadersInput {...defaultProps} headersItems={headersItems} />)
expect(screen.getByText('tools.mcp.modal.headerKey')).toBeInTheDocument()
expect(screen.getByText('tools.mcp.modal.headerValue')).toBeInTheDocument()
})
it('should render delete buttons for each item when not readonly', () => {
render(<HeadersInput {...defaultProps} headersItems={headersItems} />)
// Should have delete buttons for each header
const deleteButtons = document.querySelectorAll('[class*="text-text-destructive"]')
expect(deleteButtons.length).toBe(headersItems.length)
})
it('should not render delete buttons when readonly', () => {
render(<HeadersInput {...defaultProps} headersItems={headersItems} readonly={true} />)
const deleteButtons = document.querySelectorAll('[class*="text-text-destructive"]')
expect(deleteButtons.length).toBe(0)
})
it('should render add button at bottom when not readonly', () => {
render(<HeadersInput {...defaultProps} headersItems={headersItems} />)
expect(screen.getByText('tools.mcp.modal.addHeader')).toBeInTheDocument()
})
it('should not render add button when readonly', () => {
render(<HeadersInput {...defaultProps} headersItems={headersItems} readonly={true} />)
expect(screen.queryByText('tools.mcp.modal.addHeader')).not.toBeInTheDocument()
})
})
describe('Masked Headers', () => {
const headersItems = [{ id: '1', key: 'Secret', value: '***' }]
it('should show masked headers tip when isMasked is true', () => {
render(<HeadersInput {...defaultProps} headersItems={headersItems} isMasked={true} />)
expect(screen.getByText('tools.mcp.modal.maskedHeadersTip')).toBeInTheDocument()
})
it('should not show masked headers tip when isMasked is false', () => {
render(<HeadersInput {...defaultProps} headersItems={headersItems} isMasked={false} />)
expect(screen.queryByText('tools.mcp.modal.maskedHeadersTip')).not.toBeInTheDocument()
})
})
describe('Item Interactions', () => {
const headersItems = [
{ id: '1', key: 'Header1', value: 'Value1' },
]
it('should call onChange when key is changed', () => {
const onChange = vi.fn()
render(<HeadersInput {...defaultProps} headersItems={headersItems} onChange={onChange} />)
const keyInput = screen.getByDisplayValue('Header1')
fireEvent.change(keyInput, { target: { value: 'NewHeader' } })
expect(onChange).toHaveBeenCalledWith([
{ id: '1', key: 'NewHeader', value: 'Value1' },
])
})
it('should call onChange when value is changed', () => {
const onChange = vi.fn()
render(<HeadersInput {...defaultProps} headersItems={headersItems} onChange={onChange} />)
const valueInput = screen.getByDisplayValue('Value1')
fireEvent.change(valueInput, { target: { value: 'NewValue' } })
expect(onChange).toHaveBeenCalledWith([
{ id: '1', key: 'Header1', value: 'NewValue' },
])
})
it('should remove item when delete button is clicked', () => {
const onChange = vi.fn()
render(<HeadersInput {...defaultProps} headersItems={headersItems} onChange={onChange} />)
const deleteButton = document.querySelector('[class*="text-text-destructive"]')?.closest('button')
if (deleteButton) {
fireEvent.click(deleteButton)
expect(onChange).toHaveBeenCalledWith([])
}
})
it('should add new item when add button is clicked', () => {
const onChange = vi.fn()
render(<HeadersInput {...defaultProps} headersItems={headersItems} onChange={onChange} />)
const addButton = screen.getByText('tools.mcp.modal.addHeader')
fireEvent.click(addButton)
expect(onChange).toHaveBeenCalledWith([
{ id: '1', key: 'Header1', value: 'Value1' },
expect.objectContaining({ key: '', value: '' }),
])
})
})
describe('Multiple Headers', () => {
const headersItems = [
{ id: '1', key: 'Header1', value: 'Value1' },
{ id: '2', key: 'Header2', value: 'Value2' },
{ id: '3', key: 'Header3', value: 'Value3' },
]
it('should render all headers', () => {
render(<HeadersInput {...defaultProps} headersItems={headersItems} />)
expect(screen.getByDisplayValue('Header1')).toBeInTheDocument()
expect(screen.getByDisplayValue('Header2')).toBeInTheDocument()
expect(screen.getByDisplayValue('Header3')).toBeInTheDocument()
})
it('should update correct item when changed', () => {
const onChange = vi.fn()
render(<HeadersInput {...defaultProps} headersItems={headersItems} onChange={onChange} />)
const header2Input = screen.getByDisplayValue('Header2')
fireEvent.change(header2Input, { target: { value: 'UpdatedHeader2' } })
expect(onChange).toHaveBeenCalledWith([
{ id: '1', key: 'Header1', value: 'Value1' },
{ id: '2', key: 'UpdatedHeader2', value: 'Value2' },
{ id: '3', key: 'Header3', value: 'Value3' },
])
})
it('should remove correct item when deleted', () => {
const onChange = vi.fn()
render(<HeadersInput {...defaultProps} headersItems={headersItems} onChange={onChange} />)
// Find all delete buttons and click the second one
const deleteButtons = document.querySelectorAll('[class*="text-text-destructive"]')
const secondDeleteButton = deleteButtons[1]?.closest('button')
if (secondDeleteButton) {
fireEvent.click(secondDeleteButton)
expect(onChange).toHaveBeenCalledWith([
{ id: '1', key: 'Header1', value: 'Value1' },
{ id: '3', key: 'Header3', value: 'Value3' },
])
}
})
})
describe('Readonly Mode', () => {
const headersItems = [{ id: '1', key: 'ReadOnly', value: 'Value' }]
it('should make inputs readonly when readonly is true', () => {
render(<HeadersInput {...defaultProps} headersItems={headersItems} readonly={true} />)
const keyInput = screen.getByDisplayValue('ReadOnly')
const valueInput = screen.getByDisplayValue('Value')
expect(keyInput).toHaveAttribute('readonly')
expect(valueInput).toHaveAttribute('readonly')
})
it('should not make inputs readonly when readonly is false', () => {
render(<HeadersInput {...defaultProps} headersItems={headersItems} readonly={false} />)
const keyInput = screen.getByDisplayValue('ReadOnly')
const valueInput = screen.getByDisplayValue('Value')
expect(keyInput).not.toHaveAttribute('readonly')
expect(valueInput).not.toHaveAttribute('readonly')
})
})
describe('Edge Cases', () => {
it('should handle empty key and value', () => {
const headersItems = [{ id: '1', key: '', value: '' }]
render(<HeadersInput {...defaultProps} headersItems={headersItems} />)
const inputs = screen.getAllByRole('textbox')
expect(inputs.length).toBe(2)
})
it('should handle special characters in header key', () => {
const headersItems = [{ id: '1', key: 'X-Custom-Header', value: 'value' }]
render(<HeadersInput {...defaultProps} headersItems={headersItems} />)
expect(screen.getByDisplayValue('X-Custom-Header')).toBeInTheDocument()
})
it('should handle JSON value', () => {
const headersItems = [{ id: '1', key: 'Data', value: '{"key":"value"}' }]
render(<HeadersInput {...defaultProps} headersItems={headersItems} />)
expect(screen.getByDisplayValue('{"key":"value"}')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,344 @@
import { act, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import MCPList from '../index'
type MockProvider = {
id: string
name: string | Record<string, string>
type: string
}
type MockDetail = MockProvider | undefined
// Mock dependencies
const mockRefetch = vi.fn()
let mockProviders: MockProvider[] = []
vi.mock('@/service/use-tools', () => ({
useAllToolProviders: () => ({
data: mockProviders,
refetch: mockRefetch,
}),
}))
// Mock child components
vi.mock('../create-card', () => ({
default: ({ handleCreate }: { handleCreate: (provider: { id: string, name: string }) => void }) => (
<div data-testid="create-card" onClick={() => handleCreate({ id: 'new-id', name: 'New Provider' })}>
Create Card
</div>
),
}))
vi.mock('../provider-card', () => ({
default: ({ data, handleSelect, onUpdate, onDeleted }: { data: MockProvider, handleSelect: (id: string) => void, onUpdate: (id: string) => void, onDeleted: () => void }) => {
const displayName = typeof data.name === 'string' ? data.name : Object.values(data.name)[0]
return (
<div data-testid={`provider-card-${data.id}`}>
<span onClick={() => handleSelect(data.id)}>{displayName}</span>
<button data-testid={`update-btn-${data.id}`} onClick={() => onUpdate(data.id)}>Update</button>
<button data-testid={`delete-btn-${data.id}`} onClick={onDeleted}>Delete</button>
</div>
)
},
}))
vi.mock('../detail/provider-detail', () => ({
default: ({ detail, onHide, onUpdate, isTriggerAuthorize, onFirstCreate }: { detail: MockDetail, onHide: () => void, onUpdate: () => void, isTriggerAuthorize: boolean, onFirstCreate: () => void }) => {
const displayName = detail?.name
? (typeof detail.name === 'string' ? detail.name : Object.values(detail.name)[0])
: ''
return (
<div data-testid="detail-panel">
<div data-testid="detail-name">{displayName}</div>
<div data-testid="trigger-authorize">{isTriggerAuthorize ? 'true' : 'false'}</div>
<button data-testid="close-detail" onClick={onHide}>Close</button>
<button data-testid="update-detail" onClick={onUpdate}>Update List</button>
<button data-testid="first-create-done" onClick={onFirstCreate}>First Create Done</button>
</div>
)
},
}))
describe('MCPList', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
mockProviders = []
mockRefetch.mockResolvedValue(undefined)
})
afterEach(() => {
vi.useRealTimers()
})
describe('Rendering', () => {
it('should render without crashing', () => {
render(<MCPList searchText="" />)
expect(screen.getByTestId('create-card')).toBeInTheDocument()
})
it('should render create card', () => {
render(<MCPList searchText="" />)
expect(screen.getByTestId('create-card')).toBeInTheDocument()
})
it('should render default skeleton cards when list is empty', () => {
render(<MCPList searchText="" />)
// Should render skeleton cards when no providers
const container = document.querySelector('.grid')
expect(container).toBeInTheDocument()
// Check for skeleton cards (36 of them)
const skeletonCards = document.querySelectorAll('.h-\\[111px\\]')
expect(skeletonCards.length).toBe(36)
})
it('should not render skeleton cards when providers exist', () => {
mockProviders = [
{ id: '1', name: 'Provider 1', type: 'mcp' },
]
render(<MCPList searchText="" />)
const skeletonCards = document.querySelectorAll('.h-\\[111px\\]')
expect(skeletonCards.length).toBe(0)
})
})
describe('With Providers', () => {
beforeEach(() => {
mockProviders = [
{ id: '1', name: 'Provider 1', type: 'mcp' },
{ id: '2', name: 'Provider 2', type: 'mcp' },
{ id: '3', name: 'API Tool', type: 'api' },
]
})
it('should render provider cards for MCP type providers', () => {
render(<MCPList searchText="" />)
expect(screen.getByTestId('provider-card-1')).toBeInTheDocument()
expect(screen.getByTestId('provider-card-2')).toBeInTheDocument()
// API type should not be rendered (only MCP type)
expect(screen.queryByTestId('provider-card-3')).not.toBeInTheDocument()
})
it('should show detail panel when provider is selected', async () => {
render(<MCPList searchText="" />)
const providerName = screen.getByText('Provider 1')
await act(async () => {
fireEvent.click(providerName)
vi.advanceTimersByTime(10)
})
expect(screen.getByTestId('detail-panel')).toBeInTheDocument()
expect(screen.getByTestId('detail-name')).toHaveTextContent('Provider 1')
})
it('should hide detail panel when close is clicked', async () => {
render(<MCPList searchText="" />)
const providerName = screen.getByText('Provider 1')
await act(async () => {
fireEvent.click(providerName)
vi.advanceTimersByTime(10)
})
expect(screen.getByTestId('detail-panel')).toBeInTheDocument()
const closeBtn = screen.getByTestId('close-detail')
await act(async () => {
fireEvent.click(closeBtn)
vi.advanceTimersByTime(10)
})
expect(screen.queryByTestId('detail-panel')).not.toBeInTheDocument()
})
})
describe('Search Filtering', () => {
beforeEach(() => {
mockProviders = [
{ id: '1', name: { 'en-US': 'Search Tool' }, type: 'mcp' },
{ id: '2', name: { 'en-US': 'Another Provider' }, type: 'mcp' },
]
})
it('should filter providers based on search text', () => {
render(<MCPList searchText="search" />)
expect(screen.getByTestId('provider-card-1')).toBeInTheDocument()
expect(screen.queryByTestId('provider-card-2')).not.toBeInTheDocument()
})
it('should filter case-insensitively', () => {
render(<MCPList searchText="SEARCH" />)
expect(screen.getByTestId('provider-card-1')).toBeInTheDocument()
})
it('should show all MCP type providers when search is empty', () => {
mockProviders = [
{ id: '1', name: 'Provider 1', type: 'mcp' },
{ id: '2', name: 'Provider 2', type: 'mcp' },
]
render(<MCPList searchText="" />)
expect(screen.getByTestId('provider-card-1')).toBeInTheDocument()
expect(screen.getByTestId('provider-card-2')).toBeInTheDocument()
})
})
describe('Create Provider', () => {
beforeEach(() => {
mockProviders = []
})
it('should call refetch and set provider after create', async () => {
render(<MCPList searchText="" />)
const createCard = screen.getByTestId('create-card')
await act(async () => {
fireEvent.click(createCard)
vi.advanceTimersByTime(10)
await Promise.resolve()
})
expect(mockRefetch).toHaveBeenCalled()
})
it('should show detail panel with trigger authorize after create', async () => {
mockProviders = [{ id: 'new-id', name: 'New Provider', type: 'mcp' }]
render(<MCPList searchText="" />)
const createCard = screen.getByTestId('create-card')
await act(async () => {
fireEvent.click(createCard)
vi.advanceTimersByTime(10)
await Promise.resolve()
})
expect(screen.getByTestId('detail-panel')).toBeInTheDocument()
expect(screen.getByTestId('trigger-authorize')).toHaveTextContent('true')
})
it('should reset trigger authorize when onFirstCreate is called', async () => {
mockProviders = [{ id: 'new-id', name: 'New Provider', type: 'mcp' }]
render(<MCPList searchText="" />)
const createCard = screen.getByTestId('create-card')
await act(async () => {
fireEvent.click(createCard)
vi.advanceTimersByTime(10)
await Promise.resolve()
})
expect(screen.getByTestId('trigger-authorize')).toHaveTextContent('true')
const firstCreateDone = screen.getByTestId('first-create-done')
await act(async () => {
fireEvent.click(firstCreateDone)
vi.advanceTimersByTime(10)
})
expect(screen.getByTestId('trigger-authorize')).toHaveTextContent('false')
})
})
describe('Update Provider', () => {
beforeEach(() => {
mockProviders = [
{ id: '1', name: 'Provider 1', type: 'mcp' },
]
})
it('should call refetch and set provider after update', async () => {
render(<MCPList searchText="" />)
const updateBtn = screen.getByTestId('update-btn-1')
await act(async () => {
fireEvent.click(updateBtn)
vi.advanceTimersByTime(10)
await Promise.resolve()
})
expect(mockRefetch).toHaveBeenCalled()
})
it('should show detail panel with trigger authorize after update', async () => {
render(<MCPList searchText="" />)
const updateBtn = screen.getByTestId('update-btn-1')
await act(async () => {
fireEvent.click(updateBtn)
vi.advanceTimersByTime(10)
await Promise.resolve()
})
expect(screen.getByTestId('detail-panel')).toBeInTheDocument()
expect(screen.getByTestId('trigger-authorize')).toHaveTextContent('true')
})
})
describe('Delete Provider', () => {
beforeEach(() => {
mockProviders = [
{ id: '1', name: 'Provider 1', type: 'mcp' },
]
})
it('should call refetch after delete', async () => {
render(<MCPList searchText="" />)
const deleteBtn = screen.getByTestId('delete-btn-1')
await act(async () => {
fireEvent.click(deleteBtn)
vi.advanceTimersByTime(10)
})
expect(mockRefetch).toHaveBeenCalled()
})
})
describe('Grid Layout', () => {
it('should have responsive grid layout', () => {
render(<MCPList searchText="" />)
const grid = document.querySelector('.grid')
expect(grid).toHaveClass('grid-cols-1')
expect(grid).toHaveClass('md:grid-cols-2')
expect(grid).toHaveClass('xl:grid-cols-4')
})
it('should have overflow hidden when list is empty', () => {
mockProviders = []
render(<MCPList searchText="" />)
const grid = document.querySelector('.grid')
expect(grid).toHaveClass('overflow-hidden')
})
it('should not have overflow hidden when list has providers', () => {
mockProviders = [{ id: '1', name: 'Provider 1', type: 'mcp' }]
render(<MCPList searchText="" />)
const grid = document.querySelector('.grid')
expect(grid).not.toHaveClass('overflow-hidden')
})
})
})

View File

@ -0,0 +1,361 @@
import type { ReactNode } from 'react'
import type { MCPServerDetail } from '@/app/components/tools/types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { describe, expect, it, vi } from 'vitest'
import MCPServerModal from '../mcp-server-modal'
// Mock the services
vi.mock('@/service/use-tools', () => ({
useCreateMCPServer: () => ({
mutateAsync: vi.fn().mockResolvedValue({ result: 'success' }),
isPending: false,
}),
useUpdateMCPServer: () => ({
mutateAsync: vi.fn().mockResolvedValue({ result: 'success' }),
isPending: false,
}),
useInvalidateMCPServerDetail: () => vi.fn(),
}))
describe('MCPServerModal', () => {
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
return ({ children }: { children: ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children)
}
const defaultProps = {
appID: 'app-123',
show: true,
onHide: vi.fn(),
}
describe('Rendering', () => {
it('should render without crashing', () => {
render(<MCPServerModal {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.server.modal.addTitle')).toBeInTheDocument()
})
it('should render add title when no data is provided', () => {
render(<MCPServerModal {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.server.modal.addTitle')).toBeInTheDocument()
})
it('should render edit title when data is provided', () => {
const mockData = {
id: 'server-1',
description: 'Existing description',
parameters: {},
} as unknown as MCPServerDetail
render(<MCPServerModal {...defaultProps} data={mockData} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.server.modal.editTitle')).toBeInTheDocument()
})
it('should render description label', () => {
render(<MCPServerModal {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.server.modal.description')).toBeInTheDocument()
})
it('should render required indicator', () => {
render(<MCPServerModal {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('*')).toBeInTheDocument()
})
it('should render description textarea', () => {
render(<MCPServerModal {...defaultProps} />, { wrapper: createWrapper() })
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.descriptionPlaceholder')
expect(textarea).toBeInTheDocument()
})
it('should render cancel button', () => {
render(<MCPServerModal {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.modal.cancel')).toBeInTheDocument()
})
it('should render confirm button in add mode', () => {
render(<MCPServerModal {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.server.modal.confirm')).toBeInTheDocument()
})
it('should render save button in edit mode', () => {
const mockData = {
id: 'server-1',
description: 'Existing description',
parameters: {},
} as unknown as MCPServerDetail
render(<MCPServerModal {...defaultProps} data={mockData} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.modal.save')).toBeInTheDocument()
})
it('should render close icon', () => {
render(<MCPServerModal {...defaultProps} />, { wrapper: createWrapper() })
const closeButton = document.querySelector('.cursor-pointer svg')
expect(closeButton).toBeInTheDocument()
})
})
describe('Parameters Section', () => {
it('should not render parameters section when no latestParams', () => {
render(<MCPServerModal {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.queryByText('tools.mcp.server.modal.parameters')).not.toBeInTheDocument()
})
it('should render parameters section when latestParams is provided', () => {
const latestParams = [
{ variable: 'param1', label: 'Parameter 1', type: 'string' },
]
render(<MCPServerModal {...defaultProps} latestParams={latestParams} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.server.modal.parameters')).toBeInTheDocument()
})
it('should render parameters tip', () => {
const latestParams = [
{ variable: 'param1', label: 'Parameter 1', type: 'string' },
]
render(<MCPServerModal {...defaultProps} latestParams={latestParams} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.server.modal.parametersTip')).toBeInTheDocument()
})
it('should render parameter items', () => {
const latestParams = [
{ variable: 'param1', label: 'Parameter 1', type: 'string' },
{ variable: 'param2', label: 'Parameter 2', type: 'number' },
]
render(<MCPServerModal {...defaultProps} latestParams={latestParams} />, { wrapper: createWrapper() })
expect(screen.getByText('Parameter 1')).toBeInTheDocument()
expect(screen.getByText('Parameter 2')).toBeInTheDocument()
})
})
describe('Form Interactions', () => {
it('should update description when typing', () => {
render(<MCPServerModal {...defaultProps} />, { wrapper: createWrapper() })
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.descriptionPlaceholder')
fireEvent.change(textarea, { target: { value: 'New description' } })
expect(textarea).toHaveValue('New description')
})
it('should call onHide when cancel button is clicked', () => {
const onHide = vi.fn()
render(<MCPServerModal {...defaultProps} onHide={onHide} />, { wrapper: createWrapper() })
const cancelButton = screen.getByText('tools.mcp.modal.cancel')
fireEvent.click(cancelButton)
expect(onHide).toHaveBeenCalledTimes(1)
})
it('should call onHide when close icon is clicked', () => {
const onHide = vi.fn()
render(<MCPServerModal {...defaultProps} onHide={onHide} />, { wrapper: createWrapper() })
const closeButton = document.querySelector('.cursor-pointer')
if (closeButton) {
fireEvent.click(closeButton)
expect(onHide).toHaveBeenCalled()
}
})
it('should disable confirm button when description is empty', () => {
render(<MCPServerModal {...defaultProps} />, { wrapper: createWrapper() })
const confirmButton = screen.getByText('tools.mcp.server.modal.confirm')
expect(confirmButton).toBeDisabled()
})
it('should enable confirm button when description is filled', () => {
render(<MCPServerModal {...defaultProps} />, { wrapper: createWrapper() })
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.descriptionPlaceholder')
fireEvent.change(textarea, { target: { value: 'Valid description' } })
const confirmButton = screen.getByText('tools.mcp.server.modal.confirm')
expect(confirmButton).not.toBeDisabled()
})
})
describe('Edit Mode', () => {
const mockData = {
id: 'server-1',
description: 'Existing description',
parameters: { param1: 'existing value' },
} as unknown as MCPServerDetail
it('should populate description with existing value', () => {
render(<MCPServerModal {...defaultProps} data={mockData} />, { wrapper: createWrapper() })
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.descriptionPlaceholder')
expect(textarea).toHaveValue('Existing description')
})
it('should populate parameters with existing values', () => {
const latestParams = [
{ variable: 'param1', label: 'Parameter 1', type: 'string' },
]
render(
<MCPServerModal {...defaultProps} data={mockData} latestParams={latestParams} />,
{ wrapper: createWrapper() },
)
const paramInput = screen.getByPlaceholderText('tools.mcp.server.modal.parametersPlaceholder')
expect(paramInput).toHaveValue('existing value')
})
})
describe('Form Submission', () => {
it('should submit form with description', async () => {
const onHide = vi.fn()
render(<MCPServerModal {...defaultProps} onHide={onHide} />, { wrapper: createWrapper() })
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.descriptionPlaceholder')
fireEvent.change(textarea, { target: { value: 'Test description' } })
const confirmButton = screen.getByText('tools.mcp.server.modal.confirm')
fireEvent.click(confirmButton)
await waitFor(() => {
expect(onHide).toHaveBeenCalled()
})
})
})
describe('With App Info', () => {
it('should use appInfo description as default when no data', () => {
const appInfo = { description: 'App default description' }
render(<MCPServerModal {...defaultProps} appInfo={appInfo} />, { wrapper: createWrapper() })
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.descriptionPlaceholder')
expect(textarea).toHaveValue('App default description')
})
it('should prefer data description over appInfo description', () => {
const appInfo = { description: 'App default description' }
const mockData = {
id: 'server-1',
description: 'Data description',
parameters: {},
} as unknown as MCPServerDetail
render(
<MCPServerModal {...defaultProps} data={mockData} appInfo={appInfo} />,
{ wrapper: createWrapper() },
)
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.descriptionPlaceholder')
expect(textarea).toHaveValue('Data description')
})
})
describe('Not Shown State', () => {
it('should not render modal content when show is false', () => {
render(<MCPServerModal {...defaultProps} show={false} />, { wrapper: createWrapper() })
expect(screen.queryByText('tools.mcp.server.modal.addTitle')).not.toBeInTheDocument()
})
})
describe('Update Mode Submission', () => {
it('should submit update when data is provided', async () => {
const onHide = vi.fn()
const mockData = {
id: 'server-1',
description: 'Existing description',
parameters: { param1: 'value1' },
} as unknown as MCPServerDetail
render(
<MCPServerModal {...defaultProps} data={mockData} onHide={onHide} />,
{ wrapper: createWrapper() },
)
// Change description
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.descriptionPlaceholder')
fireEvent.change(textarea, { target: { value: 'Updated description' } })
// Click save button
const saveButton = screen.getByText('tools.mcp.modal.save')
fireEvent.click(saveButton)
await waitFor(() => {
expect(onHide).toHaveBeenCalled()
})
})
})
describe('Parameter Handling', () => {
it('should update parameter value when changed', async () => {
const latestParams = [
{ variable: 'param1', label: 'Parameter 1', type: 'string' },
{ variable: 'param2', label: 'Parameter 2', type: 'string' },
]
render(
<MCPServerModal {...defaultProps} latestParams={latestParams} />,
{ wrapper: createWrapper() },
)
// Fill description first
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.descriptionPlaceholder')
fireEvent.change(textarea, { target: { value: 'Test description' } })
// Get all parameter inputs
const paramInputs = screen.getAllByPlaceholderText('tools.mcp.server.modal.parametersPlaceholder')
// Change the first parameter value
fireEvent.change(paramInputs[0], { target: { value: 'new param value' } })
expect(paramInputs[0]).toHaveValue('new param value')
})
it('should submit with parameter values', async () => {
const onHide = vi.fn()
const latestParams = [
{ variable: 'param1', label: 'Parameter 1', type: 'string' },
]
render(
<MCPServerModal {...defaultProps} latestParams={latestParams} onHide={onHide} />,
{ wrapper: createWrapper() },
)
// Fill description
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.descriptionPlaceholder')
fireEvent.change(textarea, { target: { value: 'Test description' } })
// Fill parameter
const paramInput = screen.getByPlaceholderText('tools.mcp.server.modal.parametersPlaceholder')
fireEvent.change(paramInput, { target: { value: 'param value' } })
// Submit
const confirmButton = screen.getByText('tools.mcp.server.modal.confirm')
fireEvent.click(confirmButton)
await waitFor(() => {
expect(onHide).toHaveBeenCalled()
})
})
it('should handle empty description submission', async () => {
const onHide = vi.fn()
render(<MCPServerModal {...defaultProps} onHide={onHide} />, { wrapper: createWrapper() })
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.descriptionPlaceholder')
fireEvent.change(textarea, { target: { value: '' } })
// Button should be disabled
const confirmButton = screen.getByText('tools.mcp.server.modal.confirm')
expect(confirmButton).toBeDisabled()
})
})
})

View File

@ -0,0 +1,165 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import MCPServerParamItem from '../mcp-server-param-item'
describe('MCPServerParamItem', () => {
const defaultProps = {
data: {
label: 'Test Label',
variable: 'test_variable',
type: 'string',
},
value: '',
onChange: vi.fn(),
}
describe('Rendering', () => {
it('should render without crashing', () => {
render(<MCPServerParamItem {...defaultProps} />)
expect(screen.getByText('Test Label')).toBeInTheDocument()
})
it('should display label', () => {
render(<MCPServerParamItem {...defaultProps} />)
expect(screen.getByText('Test Label')).toBeInTheDocument()
})
it('should display variable name', () => {
render(<MCPServerParamItem {...defaultProps} />)
expect(screen.getByText('test_variable')).toBeInTheDocument()
})
it('should display type', () => {
render(<MCPServerParamItem {...defaultProps} />)
expect(screen.getByText('string')).toBeInTheDocument()
})
it('should display separator dot', () => {
render(<MCPServerParamItem {...defaultProps} />)
expect(screen.getByText('·')).toBeInTheDocument()
})
it('should render textarea with placeholder', () => {
render(<MCPServerParamItem {...defaultProps} />)
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.parametersPlaceholder')
expect(textarea).toBeInTheDocument()
})
})
describe('Value Display', () => {
it('should display empty value by default', () => {
render(<MCPServerParamItem {...defaultProps} />)
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.parametersPlaceholder')
expect(textarea).toHaveValue('')
})
it('should display provided value', () => {
render(<MCPServerParamItem {...defaultProps} value="test value" />)
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.parametersPlaceholder')
expect(textarea).toHaveValue('test value')
})
it('should display long text value', () => {
const longValue = 'This is a very long text value that might span multiple lines'
render(<MCPServerParamItem {...defaultProps} value={longValue} />)
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.parametersPlaceholder')
expect(textarea).toHaveValue(longValue)
})
})
describe('User Interactions', () => {
it('should call onChange when text is entered', () => {
const onChange = vi.fn()
render(<MCPServerParamItem {...defaultProps} onChange={onChange} />)
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.parametersPlaceholder')
fireEvent.change(textarea, { target: { value: 'new value' } })
expect(onChange).toHaveBeenCalledWith('new value')
})
it('should call onChange with empty string when cleared', () => {
const onChange = vi.fn()
render(<MCPServerParamItem {...defaultProps} value="existing" onChange={onChange} />)
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.parametersPlaceholder')
fireEvent.change(textarea, { target: { value: '' } })
expect(onChange).toHaveBeenCalledWith('')
})
it('should handle multiple changes', () => {
const onChange = vi.fn()
render(<MCPServerParamItem {...defaultProps} onChange={onChange} />)
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.parametersPlaceholder')
fireEvent.change(textarea, { target: { value: 'first' } })
fireEvent.change(textarea, { target: { value: 'second' } })
fireEvent.change(textarea, { target: { value: 'third' } })
expect(onChange).toHaveBeenCalledTimes(3)
expect(onChange).toHaveBeenLastCalledWith('third')
})
})
describe('Different Data Types', () => {
it('should display number type', () => {
const props = {
...defaultProps,
data: { label: 'Count', variable: 'count', type: 'number' },
}
render(<MCPServerParamItem {...props} />)
expect(screen.getByText('number')).toBeInTheDocument()
})
it('should display boolean type', () => {
const props = {
...defaultProps,
data: { label: 'Enabled', variable: 'enabled', type: 'boolean' },
}
render(<MCPServerParamItem {...props} />)
expect(screen.getByText('boolean')).toBeInTheDocument()
})
it('should display array type', () => {
const props = {
...defaultProps,
data: { label: 'Items', variable: 'items', type: 'array' },
}
render(<MCPServerParamItem {...props} />)
expect(screen.getByText('array')).toBeInTheDocument()
})
})
describe('Edge Cases', () => {
it('should handle special characters in label', () => {
const props = {
...defaultProps,
data: { label: 'Test <Label> & "Special"', variable: 'test', type: 'string' },
}
render(<MCPServerParamItem {...props} />)
expect(screen.getByText('Test <Label> & "Special"')).toBeInTheDocument()
})
it('should handle empty data object properties', () => {
const props = {
...defaultProps,
data: { label: '', variable: '', type: '' },
}
render(<MCPServerParamItem {...props} />)
// Should render without crashing
expect(screen.getByText('·')).toBeInTheDocument()
})
it('should handle unicode characters in value', () => {
const onChange = vi.fn()
render(<MCPServerParamItem {...defaultProps} onChange={onChange} />)
const textarea = screen.getByPlaceholderText('tools.mcp.server.modal.parametersPlaceholder')
fireEvent.change(textarea, { target: { value: '你好世界 🌍' } })
expect(onChange).toHaveBeenCalledWith('你好世界 🌍')
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,745 @@
import type { ReactNode } from 'react'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { describe, expect, it, vi } from 'vitest'
import MCPModal from '../modal'
// Mock the service API
vi.mock('@/service/common', () => ({
uploadRemoteFileInfo: vi.fn().mockResolvedValue({ url: 'https://example.com/icon.png' }),
}))
// Mock the AppIconPicker component
type IconPayload = {
type: string
icon: string
background: string
}
type AppIconPickerProps = {
onSelect: (payload: IconPayload) => void
onClose: () => void
}
vi.mock('@/app/components/base/app-icon-picker', () => ({
default: ({ onSelect, onClose }: AppIconPickerProps) => (
<div data-testid="app-icon-picker">
<button data-testid="select-emoji-btn" onClick={() => onSelect({ type: 'emoji', icon: '🎉', background: '#FF0000' })}>
Select Emoji
</button>
<button data-testid="close-picker-btn" onClick={onClose}>
Close Picker
</button>
</div>
),
}))
// Mock the plugins service to avoid React Query issues from TabSlider
vi.mock('@/service/use-plugins', () => ({
useInstalledPluginList: () => ({
data: { pages: [] },
hasNextPage: false,
isFetchingNextPage: false,
fetchNextPage: vi.fn(),
isLoading: false,
isSuccess: true,
}),
}))
describe('MCPModal', () => {
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
return ({ children }: { children: ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children)
}
const defaultProps = {
show: true,
onConfirm: vi.fn(),
onHide: vi.fn(),
}
describe('Rendering', () => {
it('should render without crashing', () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.modal.title')).toBeInTheDocument()
})
it('should not render when show is false', () => {
render(<MCPModal {...defaultProps} show={false} />, { wrapper: createWrapper() })
expect(screen.queryByText('tools.mcp.modal.title')).not.toBeInTheDocument()
})
it('should render create title when no data is provided', () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.modal.title')).toBeInTheDocument()
})
it('should render edit title when data is provided', () => {
const mockData = {
id: 'test-id',
name: 'Test Server',
server_url: 'https://example.com/mcp',
server_identifier: 'test-server',
icon: { content: '🔗', background: '#6366F1' },
} as unknown as ToolWithProvider
render(<MCPModal {...defaultProps} data={mockData} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.modal.editTitle')).toBeInTheDocument()
})
})
describe('Form Fields', () => {
it('should render server URL input', () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.modal.serverUrl')).toBeInTheDocument()
})
it('should render name input', () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.modal.name')).toBeInTheDocument()
})
it('should render server identifier input', () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.modal.serverIdentifier')).toBeInTheDocument()
})
it('should render auth method tabs', () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.modal.authentication')).toBeInTheDocument()
expect(screen.getByText('tools.mcp.modal.headers')).toBeInTheDocument()
expect(screen.getByText('tools.mcp.modal.configurations')).toBeInTheDocument()
})
})
describe('Form Interactions', () => {
it('should update URL input value', () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
const urlInput = screen.getByPlaceholderText('tools.mcp.modal.serverUrlPlaceholder')
fireEvent.change(urlInput, { target: { value: 'https://test.com/mcp' } })
expect(urlInput).toHaveValue('https://test.com/mcp')
})
it('should update name input value', () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
const nameInput = screen.getByPlaceholderText('tools.mcp.modal.namePlaceholder')
fireEvent.change(nameInput, { target: { value: 'My Server' } })
expect(nameInput).toHaveValue('My Server')
})
it('should update server identifier input value', () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
const identifierInput = screen.getByPlaceholderText('tools.mcp.modal.serverIdentifierPlaceholder')
fireEvent.change(identifierInput, { target: { value: 'my-server' } })
expect(identifierInput).toHaveValue('my-server')
})
})
describe('Tab Navigation', () => {
it('should show authentication section by default', () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.modal.useDynamicClientRegistration')).toBeInTheDocument()
})
it('should switch to headers section when clicked', async () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
const headersTab = screen.getByText('tools.mcp.modal.headers')
fireEvent.click(headersTab)
await waitFor(() => {
expect(screen.getByText('tools.mcp.modal.headersTip')).toBeInTheDocument()
})
})
it('should switch to configurations section when clicked', async () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
const configTab = screen.getByText('tools.mcp.modal.configurations')
fireEvent.click(configTab)
await waitFor(() => {
expect(screen.getByText('tools.mcp.modal.timeout')).toBeInTheDocument()
expect(screen.getByText('tools.mcp.modal.sseReadTimeout')).toBeInTheDocument()
})
})
})
describe('Action Buttons', () => {
it('should render confirm button', () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.modal.confirm')).toBeInTheDocument()
})
it('should render save button in edit mode', () => {
const mockData = {
id: 'test-id',
name: 'Test',
icon: { content: '🔗', background: '#6366F1' },
} as unknown as ToolWithProvider
render(<MCPModal {...defaultProps} data={mockData} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.modal.save')).toBeInTheDocument()
})
it('should render cancel button', () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('tools.mcp.modal.cancel')).toBeInTheDocument()
})
it('should call onHide when cancel is clicked', () => {
const onHide = vi.fn()
render(<MCPModal {...defaultProps} onHide={onHide} />, { wrapper: createWrapper() })
const cancelButton = screen.getByText('tools.mcp.modal.cancel')
fireEvent.click(cancelButton)
expect(onHide).toHaveBeenCalledTimes(1)
})
it('should call onHide when close icon is clicked', () => {
const onHide = vi.fn()
render(<MCPModal {...defaultProps} onHide={onHide} />, { wrapper: createWrapper() })
// Find the close button by its parent div with cursor-pointer class
const closeButtons = document.querySelectorAll('.cursor-pointer')
const closeButton = Array.from(closeButtons).find(el =>
el.querySelector('svg'),
)
if (closeButton) {
fireEvent.click(closeButton)
expect(onHide).toHaveBeenCalled()
}
})
it('should have confirm button disabled when form is empty', () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
const confirmButton = screen.getByText('tools.mcp.modal.confirm')
expect(confirmButton).toBeDisabled()
})
it('should enable confirm button when required fields are filled', () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
// Fill required fields
const urlInput = screen.getByPlaceholderText('tools.mcp.modal.serverUrlPlaceholder')
const nameInput = screen.getByPlaceholderText('tools.mcp.modal.namePlaceholder')
const identifierInput = screen.getByPlaceholderText('tools.mcp.modal.serverIdentifierPlaceholder')
fireEvent.change(urlInput, { target: { value: 'https://example.com/mcp' } })
fireEvent.change(nameInput, { target: { value: 'Test Server' } })
fireEvent.change(identifierInput, { target: { value: 'test-server' } })
const confirmButton = screen.getByText('tools.mcp.modal.confirm')
expect(confirmButton).not.toBeDisabled()
})
})
describe('Form Submission', () => {
it('should call onConfirm with correct data when form is submitted', async () => {
const onConfirm = vi.fn().mockResolvedValue(undefined)
render(<MCPModal {...defaultProps} onConfirm={onConfirm} />, { wrapper: createWrapper() })
// Fill required fields
const urlInput = screen.getByPlaceholderText('tools.mcp.modal.serverUrlPlaceholder')
const nameInput = screen.getByPlaceholderText('tools.mcp.modal.namePlaceholder')
const identifierInput = screen.getByPlaceholderText('tools.mcp.modal.serverIdentifierPlaceholder')
fireEvent.change(urlInput, { target: { value: 'https://example.com/mcp' } })
fireEvent.change(nameInput, { target: { value: 'Test Server' } })
fireEvent.change(identifierInput, { target: { value: 'test-server' } })
const confirmButton = screen.getByText('tools.mcp.modal.confirm')
fireEvent.click(confirmButton)
await waitFor(() => {
expect(onConfirm).toHaveBeenCalledWith(
expect.objectContaining({
name: 'Test Server',
server_url: 'https://example.com/mcp',
server_identifier: 'test-server',
}),
)
})
})
it('should not call onConfirm with invalid URL', async () => {
const onConfirm = vi.fn()
render(<MCPModal {...defaultProps} onConfirm={onConfirm} />, { wrapper: createWrapper() })
// Fill fields with invalid URL
const urlInput = screen.getByPlaceholderText('tools.mcp.modal.serverUrlPlaceholder')
const nameInput = screen.getByPlaceholderText('tools.mcp.modal.namePlaceholder')
const identifierInput = screen.getByPlaceholderText('tools.mcp.modal.serverIdentifierPlaceholder')
fireEvent.change(urlInput, { target: { value: 'not-a-valid-url' } })
fireEvent.change(nameInput, { target: { value: 'Test Server' } })
fireEvent.change(identifierInput, { target: { value: 'test-server' } })
const confirmButton = screen.getByText('tools.mcp.modal.confirm')
fireEvent.click(confirmButton)
// Wait a bit and verify onConfirm was not called
await new Promise(resolve => setTimeout(resolve, 100))
expect(onConfirm).not.toHaveBeenCalled()
})
it('should not call onConfirm with invalid server identifier', async () => {
const onConfirm = vi.fn()
render(<MCPModal {...defaultProps} onConfirm={onConfirm} />, { wrapper: createWrapper() })
// Fill fields with invalid server identifier
const urlInput = screen.getByPlaceholderText('tools.mcp.modal.serverUrlPlaceholder')
const nameInput = screen.getByPlaceholderText('tools.mcp.modal.namePlaceholder')
const identifierInput = screen.getByPlaceholderText('tools.mcp.modal.serverIdentifierPlaceholder')
fireEvent.change(urlInput, { target: { value: 'https://example.com/mcp' } })
fireEvent.change(nameInput, { target: { value: 'Test Server' } })
fireEvent.change(identifierInput, { target: { value: 'Invalid Server ID!' } })
const confirmButton = screen.getByText('tools.mcp.modal.confirm')
fireEvent.click(confirmButton)
// Wait a bit and verify onConfirm was not called
await new Promise(resolve => setTimeout(resolve, 100))
expect(onConfirm).not.toHaveBeenCalled()
})
})
describe('Edit Mode', () => {
const mockData = {
id: 'test-id',
name: 'Existing Server',
server_url: 'https://existing.com/mcp',
server_identifier: 'existing-server',
icon: { content: '🚀', background: '#FF0000' },
configuration: {
timeout: 60,
sse_read_timeout: 600,
},
masked_headers: {
Authorization: '***',
},
is_dynamic_registration: false,
authentication: {
client_id: 'client-123',
client_secret: 'secret-456',
},
} as unknown as ToolWithProvider
it('should populate form with existing data', () => {
render(<MCPModal {...defaultProps} data={mockData} />, { wrapper: createWrapper() })
expect(screen.getByDisplayValue('https://existing.com/mcp')).toBeInTheDocument()
expect(screen.getByDisplayValue('Existing Server')).toBeInTheDocument()
expect(screen.getByDisplayValue('existing-server')).toBeInTheDocument()
})
it('should show warning when URL is changed', () => {
render(<MCPModal {...defaultProps} data={mockData} />, { wrapper: createWrapper() })
const urlInput = screen.getByDisplayValue('https://existing.com/mcp')
fireEvent.change(urlInput, { target: { value: 'https://new.com/mcp' } })
expect(screen.getByText('tools.mcp.modal.serverUrlWarning')).toBeInTheDocument()
})
it('should show warning when server identifier is changed', () => {
render(<MCPModal {...defaultProps} data={mockData} />, { wrapper: createWrapper() })
const identifierInput = screen.getByDisplayValue('existing-server')
fireEvent.change(identifierInput, { target: { value: 'new-server' } })
expect(screen.getByText('tools.mcp.modal.serverIdentifierWarning')).toBeInTheDocument()
})
})
describe('Form Key Reset', () => {
it('should reset form when switching from create to edit mode', () => {
const { rerender } = render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
// Fill some data in create mode
const nameInput = screen.getByPlaceholderText('tools.mcp.modal.namePlaceholder')
fireEvent.change(nameInput, { target: { value: 'New Server' } })
// Switch to edit mode with different data
const mockData = {
id: 'edit-id',
name: 'Edit Server',
icon: { content: '🔗', background: '#6366F1' },
} as unknown as ToolWithProvider
rerender(<MCPModal {...defaultProps} data={mockData} />)
// Should show edit mode data
expect(screen.getByDisplayValue('Edit Server')).toBeInTheDocument()
})
})
describe('URL Blur Handler', () => {
it('should trigger URL blur handler when URL input loses focus', () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
const urlInput = screen.getByPlaceholderText('tools.mcp.modal.serverUrlPlaceholder')
fireEvent.change(urlInput, { target: { value: ' https://test.com/mcp ' } })
fireEvent.blur(urlInput)
// The blur handler trims the value
expect(urlInput).toHaveValue(' https://test.com/mcp ')
})
it('should handle URL blur with empty value', () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
const urlInput = screen.getByPlaceholderText('tools.mcp.modal.serverUrlPlaceholder')
fireEvent.change(urlInput, { target: { value: '' } })
fireEvent.blur(urlInput)
expect(urlInput).toHaveValue('')
})
})
describe('App Icon', () => {
it('should render app icon with default emoji', () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
// The app icon should be rendered
const appIcons = document.querySelectorAll('[class*="rounded-2xl"]')
expect(appIcons.length).toBeGreaterThan(0)
})
it('should render app icon in edit mode with custom icon', () => {
const mockData = {
id: 'test-id',
name: 'Test Server',
server_url: 'https://example.com/mcp',
server_identifier: 'test-server',
icon: { content: '🚀', background: '#FF0000' },
} as unknown as ToolWithProvider
render(<MCPModal {...defaultProps} data={mockData} />, { wrapper: createWrapper() })
// The app icon should be rendered
const appIcons = document.querySelectorAll('[class*="rounded-2xl"]')
expect(appIcons.length).toBeGreaterThan(0)
})
})
describe('Form Submission with Headers', () => {
it('should submit form with headers data', async () => {
const onConfirm = vi.fn().mockResolvedValue(undefined)
render(<MCPModal {...defaultProps} onConfirm={onConfirm} />, { wrapper: createWrapper() })
// Fill required fields
const urlInput = screen.getByPlaceholderText('tools.mcp.modal.serverUrlPlaceholder')
const nameInput = screen.getByPlaceholderText('tools.mcp.modal.namePlaceholder')
const identifierInput = screen.getByPlaceholderText('tools.mcp.modal.serverIdentifierPlaceholder')
fireEvent.change(urlInput, { target: { value: 'https://example.com/mcp' } })
fireEvent.change(nameInput, { target: { value: 'Test Server' } })
fireEvent.change(identifierInput, { target: { value: 'test-server' } })
// Switch to headers tab and add a header
const headersTab = screen.getByText('tools.mcp.modal.headers')
fireEvent.click(headersTab)
await waitFor(() => {
expect(screen.getByText('tools.mcp.modal.headersTip')).toBeInTheDocument()
})
const confirmButton = screen.getByText('tools.mcp.modal.confirm')
fireEvent.click(confirmButton)
await waitFor(() => {
expect(onConfirm).toHaveBeenCalledWith(
expect.objectContaining({
name: 'Test Server',
server_url: 'https://example.com/mcp',
server_identifier: 'test-server',
}),
)
})
})
it('should submit with authentication data', async () => {
const onConfirm = vi.fn().mockResolvedValue(undefined)
render(<MCPModal {...defaultProps} onConfirm={onConfirm} />, { wrapper: createWrapper() })
// Fill required fields
const urlInput = screen.getByPlaceholderText('tools.mcp.modal.serverUrlPlaceholder')
const nameInput = screen.getByPlaceholderText('tools.mcp.modal.namePlaceholder')
const identifierInput = screen.getByPlaceholderText('tools.mcp.modal.serverIdentifierPlaceholder')
fireEvent.change(urlInput, { target: { value: 'https://example.com/mcp' } })
fireEvent.change(nameInput, { target: { value: 'Test Server' } })
fireEvent.change(identifierInput, { target: { value: 'test-server' } })
// Submit form
const confirmButton = screen.getByText('tools.mcp.modal.confirm')
fireEvent.click(confirmButton)
await waitFor(() => {
expect(onConfirm).toHaveBeenCalledWith(
expect.objectContaining({
authentication: expect.objectContaining({
client_id: '',
client_secret: '',
}),
}),
)
})
})
it('should format headers correctly when submitting with header keys', async () => {
const onConfirm = vi.fn().mockResolvedValue(undefined)
const mockData = {
id: 'test-id',
name: 'Test Server',
server_url: 'https://example.com/mcp',
server_identifier: 'test-server',
icon: { content: '🔗', background: '#6366F1' },
masked_headers: {
'Authorization': 'Bearer token',
'X-Custom': 'value',
},
} as unknown as ToolWithProvider
render(<MCPModal {...defaultProps} data={mockData} onConfirm={onConfirm} />, { wrapper: createWrapper() })
// Switch to headers tab
const headersTab = screen.getByText('tools.mcp.modal.headers')
fireEvent.click(headersTab)
await waitFor(() => {
expect(screen.getByText('tools.mcp.modal.headersTip')).toBeInTheDocument()
})
// Submit form
const saveButton = screen.getByText('tools.mcp.modal.save')
fireEvent.click(saveButton)
await waitFor(() => {
expect(onConfirm).toHaveBeenCalledWith(
expect.objectContaining({
headers: expect.objectContaining({
Authorization: expect.any(String),
}),
}),
)
})
})
})
describe('Edit Mode Submission', () => {
it('should send hidden URL when URL is unchanged in edit mode', async () => {
const onConfirm = vi.fn().mockResolvedValue(undefined)
const mockData = {
id: 'test-id',
name: 'Existing Server',
server_url: 'https://existing.com/mcp',
server_identifier: 'existing-server',
icon: { content: '🚀', background: '#FF0000' },
} as unknown as ToolWithProvider
render(<MCPModal {...defaultProps} data={mockData} onConfirm={onConfirm} />, { wrapper: createWrapper() })
// Don't change the URL, just submit
const saveButton = screen.getByText('tools.mcp.modal.save')
fireEvent.click(saveButton)
await waitFor(() => {
expect(onConfirm).toHaveBeenCalledWith(
expect.objectContaining({
server_url: '[__HIDDEN__]',
}),
)
})
})
it('should send new URL when URL is changed in edit mode', async () => {
const onConfirm = vi.fn().mockResolvedValue(undefined)
const mockData = {
id: 'test-id',
name: 'Existing Server',
server_url: 'https://existing.com/mcp',
server_identifier: 'existing-server',
icon: { content: '🚀', background: '#FF0000' },
} as unknown as ToolWithProvider
render(<MCPModal {...defaultProps} data={mockData} onConfirm={onConfirm} />, { wrapper: createWrapper() })
// Change the URL
const urlInput = screen.getByDisplayValue('https://existing.com/mcp')
fireEvent.change(urlInput, { target: { value: 'https://new.com/mcp' } })
const saveButton = screen.getByText('tools.mcp.modal.save')
fireEvent.click(saveButton)
await waitFor(() => {
expect(onConfirm).toHaveBeenCalledWith(
expect.objectContaining({
server_url: 'https://new.com/mcp',
}),
)
})
})
})
describe('Configuration Section', () => {
it('should submit with default timeout values', async () => {
const onConfirm = vi.fn().mockResolvedValue(undefined)
render(<MCPModal {...defaultProps} onConfirm={onConfirm} />, { wrapper: createWrapper() })
// Fill required fields
const urlInput = screen.getByPlaceholderText('tools.mcp.modal.serverUrlPlaceholder')
const nameInput = screen.getByPlaceholderText('tools.mcp.modal.namePlaceholder')
const identifierInput = screen.getByPlaceholderText('tools.mcp.modal.serverIdentifierPlaceholder')
fireEvent.change(urlInput, { target: { value: 'https://example.com/mcp' } })
fireEvent.change(nameInput, { target: { value: 'Test Server' } })
fireEvent.change(identifierInput, { target: { value: 'test-server' } })
const confirmButton = screen.getByText('tools.mcp.modal.confirm')
fireEvent.click(confirmButton)
await waitFor(() => {
expect(onConfirm).toHaveBeenCalledWith(
expect.objectContaining({
configuration: expect.objectContaining({
timeout: 30,
sse_read_timeout: 300,
}),
}),
)
})
})
it('should submit with custom timeout values', async () => {
const onConfirm = vi.fn().mockResolvedValue(undefined)
render(<MCPModal {...defaultProps} onConfirm={onConfirm} />, { wrapper: createWrapper() })
// Fill required fields
const urlInput = screen.getByPlaceholderText('tools.mcp.modal.serverUrlPlaceholder')
const nameInput = screen.getByPlaceholderText('tools.mcp.modal.namePlaceholder')
const identifierInput = screen.getByPlaceholderText('tools.mcp.modal.serverIdentifierPlaceholder')
fireEvent.change(urlInput, { target: { value: 'https://example.com/mcp' } })
fireEvent.change(nameInput, { target: { value: 'Test Server' } })
fireEvent.change(identifierInput, { target: { value: 'test-server' } })
// Switch to configurations tab
const configTab = screen.getByText('tools.mcp.modal.configurations')
fireEvent.click(configTab)
await waitFor(() => {
expect(screen.getByText('tools.mcp.modal.timeout')).toBeInTheDocument()
})
const confirmButton = screen.getByText('tools.mcp.modal.confirm')
fireEvent.click(confirmButton)
await waitFor(() => {
expect(onConfirm).toHaveBeenCalled()
})
})
})
describe('Dynamic Registration', () => {
it('should toggle dynamic registration', async () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
// Find the switch for dynamic registration
const switchElements = screen.getAllByRole('switch')
expect(switchElements.length).toBeGreaterThan(0)
// Click the first switch (dynamic registration)
fireEvent.click(switchElements[0])
// The switch should toggle
expect(switchElements[0]).toBeInTheDocument()
})
})
describe('App Icon Picker Interactions', () => {
it('should open app icon picker when app icon is clicked', async () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
// Find the app icon container with cursor-pointer and rounded-2xl classes
const appIconContainer = document.querySelector('[class*="rounded-2xl"][class*="cursor-pointer"]')
if (appIconContainer) {
fireEvent.click(appIconContainer)
// The mocked AppIconPicker should now be visible
await waitFor(() => {
expect(screen.getByTestId('app-icon-picker')).toBeInTheDocument()
})
}
})
it('should close app icon picker and update icon when selecting an icon', async () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
// Open the icon picker
const appIconContainer = document.querySelector('[class*="rounded-2xl"][class*="cursor-pointer"]')
if (appIconContainer) {
fireEvent.click(appIconContainer)
await waitFor(() => {
expect(screen.getByTestId('app-icon-picker')).toBeInTheDocument()
})
// Click the select emoji button
const selectBtn = screen.getByTestId('select-emoji-btn')
fireEvent.click(selectBtn)
// The picker should be closed
await waitFor(() => {
expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument()
})
}
})
it('should close app icon picker and reset icon when close button is clicked', async () => {
render(<MCPModal {...defaultProps} />, { wrapper: createWrapper() })
// Open the icon picker
const appIconContainer = document.querySelector('[class*="rounded-2xl"][class*="cursor-pointer"]')
if (appIconContainer) {
fireEvent.click(appIconContainer)
await waitFor(() => {
expect(screen.getByTestId('app-icon-picker')).toBeInTheDocument()
})
// Click the close button
const closeBtn = screen.getByTestId('close-picker-btn')
fireEvent.click(closeBtn)
// The picker should be closed
await waitFor(() => {
expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument()
})
}
})
})
})

View File

@ -0,0 +1,524 @@
import type { ReactNode } from 'react'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import MCPCard from '../provider-card'
// Mutable mock functions
const mockUpdateMCP = vi.fn().mockResolvedValue({ result: 'success' })
const mockDeleteMCP = vi.fn().mockResolvedValue({ result: 'success' })
// Mock the services
vi.mock('@/service/use-tools', () => ({
useUpdateMCP: () => ({
mutateAsync: mockUpdateMCP,
}),
useDeleteMCP: () => ({
mutateAsync: mockDeleteMCP,
}),
}))
// Mock the MCPModal
type MCPModalForm = {
name: string
server_url: string
}
type MCPModalProps = {
show: boolean
onConfirm: (form: MCPModalForm) => void
onHide: () => void
}
vi.mock('../modal', () => ({
default: ({ show, onConfirm, onHide }: MCPModalProps) => {
if (!show)
return null
return (
<div data-testid="mcp-modal">
<button data-testid="modal-confirm-btn" onClick={() => onConfirm({ name: 'Updated MCP', server_url: 'https://updated.com' })}>
Confirm
</button>
<button data-testid="modal-close-btn" onClick={onHide}>
Close
</button>
</div>
)
},
}))
// Mock the Confirm dialog
type ConfirmDialogProps = {
isShow: boolean
onConfirm: () => void
onCancel: () => void
isLoading: boolean
}
vi.mock('@/app/components/base/confirm', () => ({
default: ({ isShow, onConfirm, onCancel, isLoading }: ConfirmDialogProps) => {
if (!isShow)
return null
return (
<div data-testid="confirm-dialog">
<button data-testid="confirm-delete-btn" onClick={onConfirm} disabled={isLoading}>
{isLoading ? 'Deleting...' : 'Confirm Delete'}
</button>
<button data-testid="cancel-delete-btn" onClick={onCancel}>
Cancel
</button>
</div>
)
},
}))
// Mock the OperationDropdown
type OperationDropdownProps = {
onEdit: () => void
onRemove: () => void
onOpenChange: (open: boolean) => void
}
vi.mock('../detail/operation-dropdown', () => ({
default: ({ onEdit, onRemove, onOpenChange }: OperationDropdownProps) => (
<div data-testid="operation-dropdown">
<button
data-testid="edit-btn"
onClick={() => {
onOpenChange(true)
onEdit()
}}
>
Edit
</button>
<button
data-testid="remove-btn"
onClick={() => {
onOpenChange(true)
onRemove()
}}
>
Remove
</button>
</div>
),
}))
// Mock the app context
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
isCurrentWorkspaceManager: true,
isCurrentWorkspaceEditor: true,
}),
}))
// Mock the format time hook
vi.mock('@/hooks/use-format-time-from-now', () => ({
useFormatTimeFromNow: () => ({
formatTimeFromNow: (_timestamp: number) => '2 hours ago',
}),
}))
// Mock the plugins service
vi.mock('@/service/use-plugins', () => ({
useInstalledPluginList: () => ({
data: { pages: [] },
hasNextPage: false,
isFetchingNextPage: false,
fetchNextPage: vi.fn(),
isLoading: false,
isSuccess: true,
}),
}))
// Mock common service
vi.mock('@/service/common', () => ({
uploadRemoteFileInfo: vi.fn().mockResolvedValue({ url: 'https://example.com/icon.png' }),
}))
describe('MCPCard', () => {
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
return ({ children }: { children: ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children)
}
const createMockData = (overrides = {}): ToolWithProvider => ({
id: 'mcp-1',
name: 'Test MCP Server',
server_identifier: 'test-server',
icon: { content: '🔧', background: '#FF0000' },
tools: [
{ name: 'tool1', description: 'Tool 1' },
{ name: 'tool2', description: 'Tool 2' },
],
is_team_authorization: true,
updated_at: Date.now() / 1000,
...overrides,
} as unknown as ToolWithProvider)
const defaultProps = {
data: createMockData(),
handleSelect: vi.fn(),
onUpdate: vi.fn(),
onDeleted: vi.fn(),
}
beforeEach(() => {
mockUpdateMCP.mockClear()
mockDeleteMCP.mockClear()
mockUpdateMCP.mockResolvedValue({ result: 'success' })
mockDeleteMCP.mockResolvedValue({ result: 'success' })
})
describe('Rendering', () => {
it('should render without crashing', () => {
render(<MCPCard {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('Test MCP Server')).toBeInTheDocument()
})
it('should display MCP name', () => {
render(<MCPCard {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('Test MCP Server')).toBeInTheDocument()
})
it('should display server identifier', () => {
render(<MCPCard {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText('test-server')).toBeInTheDocument()
})
it('should display tools count', () => {
render(<MCPCard {...defaultProps} />, { wrapper: createWrapper() })
// The tools count uses i18n with count parameter
expect(screen.getByText(/tools.mcp.toolsCount/)).toBeInTheDocument()
})
it('should display update time', () => {
render(<MCPCard {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByText(/tools.mcp.updateTime/)).toBeInTheDocument()
})
})
describe('No Tools State', () => {
it('should show no tools message when tools array is empty', () => {
const dataWithNoTools = createMockData({ tools: [] })
render(
<MCPCard {...defaultProps} data={dataWithNoTools} />,
{ wrapper: createWrapper() },
)
expect(screen.getByText('tools.mcp.noTools')).toBeInTheDocument()
})
it('should show not configured badge when not authorized', () => {
const dataNotAuthorized = createMockData({ is_team_authorization: false })
render(
<MCPCard {...defaultProps} data={dataNotAuthorized} />,
{ wrapper: createWrapper() },
)
expect(screen.getByText('tools.mcp.noConfigured')).toBeInTheDocument()
})
it('should show not configured badge when no tools', () => {
const dataWithNoTools = createMockData({ tools: [], is_team_authorization: true })
render(
<MCPCard {...defaultProps} data={dataWithNoTools} />,
{ wrapper: createWrapper() },
)
expect(screen.getByText('tools.mcp.noConfigured')).toBeInTheDocument()
})
})
describe('Selected State', () => {
it('should apply selected styles when current provider matches', () => {
render(
<MCPCard {...defaultProps} currentProvider={defaultProps.data} />,
{ wrapper: createWrapper() },
)
const card = document.querySelector('[class*="border-components-option-card-option-selected-border"]')
expect(card).toBeInTheDocument()
})
it('should not apply selected styles when different provider', () => {
const differentProvider = createMockData({ id: 'different-id' })
render(
<MCPCard {...defaultProps} currentProvider={differentProvider} />,
{ wrapper: createWrapper() },
)
const card = document.querySelector('[class*="border-components-option-card-option-selected-border"]')
expect(card).not.toBeInTheDocument()
})
})
describe('User Interactions', () => {
it('should call handleSelect when card is clicked', () => {
const handleSelect = vi.fn()
render(
<MCPCard {...defaultProps} handleSelect={handleSelect} />,
{ wrapper: createWrapper() },
)
const card = screen.getByText('Test MCP Server').closest('[class*="cursor-pointer"]')
if (card) {
fireEvent.click(card)
expect(handleSelect).toHaveBeenCalledWith('mcp-1')
}
})
})
describe('Card Icon', () => {
it('should render card icon', () => {
render(<MCPCard {...defaultProps} />, { wrapper: createWrapper() })
// Icon component is rendered
const iconContainer = document.querySelector('[class*="rounded-xl"][class*="border"]')
expect(iconContainer).toBeInTheDocument()
})
})
describe('Status Indicator', () => {
it('should show green indicator when authorized and has tools', () => {
const data = createMockData({ is_team_authorization: true, tools: [{ name: 'tool1' }] })
render(
<MCPCard {...defaultProps} data={data} />,
{ wrapper: createWrapper() },
)
// Should have green indicator (not showing red badge)
expect(screen.queryByText('tools.mcp.noConfigured')).not.toBeInTheDocument()
})
it('should show red indicator when not configured', () => {
const data = createMockData({ is_team_authorization: false })
render(
<MCPCard {...defaultProps} data={data} />,
{ wrapper: createWrapper() },
)
expect(screen.getByText('tools.mcp.noConfigured')).toBeInTheDocument()
})
})
describe('Edge Cases', () => {
it('should handle long MCP name', () => {
const longName = 'A'.repeat(100)
const data = createMockData({ name: longName })
render(
<MCPCard {...defaultProps} data={data} />,
{ wrapper: createWrapper() },
)
expect(screen.getByText(longName)).toBeInTheDocument()
})
it('should handle special characters in name', () => {
const data = createMockData({ name: 'Test <Script> & "Quotes"' })
render(
<MCPCard {...defaultProps} data={data} />,
{ wrapper: createWrapper() },
)
expect(screen.getByText('Test <Script> & "Quotes"')).toBeInTheDocument()
})
it('should handle undefined currentProvider', () => {
render(
<MCPCard {...defaultProps} currentProvider={undefined} />,
{ wrapper: createWrapper() },
)
expect(screen.getByText('Test MCP Server')).toBeInTheDocument()
})
})
describe('Operation Dropdown', () => {
it('should render operation dropdown for workspace managers', () => {
render(<MCPCard {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByTestId('operation-dropdown')).toBeInTheDocument()
})
it('should stop propagation when clicking on dropdown container', () => {
const handleSelect = vi.fn()
render(<MCPCard {...defaultProps} handleSelect={handleSelect} />, { wrapper: createWrapper() })
// Click on the dropdown area (which should stop propagation)
const dropdown = screen.getByTestId('operation-dropdown')
const dropdownContainer = dropdown.closest('[class*="absolute"]')
if (dropdownContainer) {
fireEvent.click(dropdownContainer)
// handleSelect should NOT be called because stopPropagation
expect(handleSelect).not.toHaveBeenCalled()
}
})
})
describe('Update Modal', () => {
it('should open update modal when edit button is clicked', async () => {
render(<MCPCard {...defaultProps} />, { wrapper: createWrapper() })
// Click the edit button
const editBtn = screen.getByTestId('edit-btn')
fireEvent.click(editBtn)
// Modal should be shown
await waitFor(() => {
expect(screen.getByTestId('mcp-modal')).toBeInTheDocument()
})
})
it('should close update modal when close button is clicked', async () => {
render(<MCPCard {...defaultProps} />, { wrapper: createWrapper() })
// Open the modal
const editBtn = screen.getByTestId('edit-btn')
fireEvent.click(editBtn)
await waitFor(() => {
expect(screen.getByTestId('mcp-modal')).toBeInTheDocument()
})
// Close the modal
const closeBtn = screen.getByTestId('modal-close-btn')
fireEvent.click(closeBtn)
await waitFor(() => {
expect(screen.queryByTestId('mcp-modal')).not.toBeInTheDocument()
})
})
it('should call updateMCP and onUpdate when form is confirmed', async () => {
const onUpdate = vi.fn()
render(<MCPCard {...defaultProps} onUpdate={onUpdate} />, { wrapper: createWrapper() })
// Open the modal
const editBtn = screen.getByTestId('edit-btn')
fireEvent.click(editBtn)
await waitFor(() => {
expect(screen.getByTestId('mcp-modal')).toBeInTheDocument()
})
// Confirm the form
const confirmBtn = screen.getByTestId('modal-confirm-btn')
fireEvent.click(confirmBtn)
await waitFor(() => {
expect(mockUpdateMCP).toHaveBeenCalledWith({
name: 'Updated MCP',
server_url: 'https://updated.com',
provider_id: 'mcp-1',
})
expect(onUpdate).toHaveBeenCalledWith('mcp-1')
})
})
it('should not call onUpdate when updateMCP fails', async () => {
mockUpdateMCP.mockResolvedValue({ result: 'error' })
const onUpdate = vi.fn()
render(<MCPCard {...defaultProps} onUpdate={onUpdate} />, { wrapper: createWrapper() })
// Open the modal
const editBtn = screen.getByTestId('edit-btn')
fireEvent.click(editBtn)
await waitFor(() => {
expect(screen.getByTestId('mcp-modal')).toBeInTheDocument()
})
// Confirm the form
const confirmBtn = screen.getByTestId('modal-confirm-btn')
fireEvent.click(confirmBtn)
await waitFor(() => {
expect(mockUpdateMCP).toHaveBeenCalled()
})
// onUpdate should not be called because result is not 'success'
expect(onUpdate).not.toHaveBeenCalled()
})
})
describe('Delete Confirm', () => {
it('should open delete confirm when remove button is clicked', async () => {
render(<MCPCard {...defaultProps} />, { wrapper: createWrapper() })
// Click the remove button
const removeBtn = screen.getByTestId('remove-btn')
fireEvent.click(removeBtn)
// Confirm dialog should be shown
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
})
})
it('should close delete confirm when cancel button is clicked', async () => {
render(<MCPCard {...defaultProps} />, { wrapper: createWrapper() })
// Open the confirm dialog
const removeBtn = screen.getByTestId('remove-btn')
fireEvent.click(removeBtn)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
})
// Cancel
const cancelBtn = screen.getByTestId('cancel-delete-btn')
fireEvent.click(cancelBtn)
await waitFor(() => {
expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument()
})
})
it('should call deleteMCP and onDeleted when delete is confirmed', async () => {
const onDeleted = vi.fn()
render(<MCPCard {...defaultProps} onDeleted={onDeleted} />, { wrapper: createWrapper() })
// Open the confirm dialog
const removeBtn = screen.getByTestId('remove-btn')
fireEvent.click(removeBtn)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
})
// Confirm delete
const confirmBtn = screen.getByTestId('confirm-delete-btn')
fireEvent.click(confirmBtn)
await waitFor(() => {
expect(mockDeleteMCP).toHaveBeenCalledWith('mcp-1')
expect(onDeleted).toHaveBeenCalled()
})
})
it('should not call onDeleted when deleteMCP fails', async () => {
mockDeleteMCP.mockResolvedValue({ result: 'error' })
const onDeleted = vi.fn()
render(<MCPCard {...defaultProps} onDeleted={onDeleted} />, { wrapper: createWrapper() })
// Open the confirm dialog
const removeBtn = screen.getByTestId('remove-btn')
fireEvent.click(removeBtn)
await waitFor(() => {
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
})
// Confirm delete
const confirmBtn = screen.getByTestId('confirm-delete-btn')
fireEvent.click(confirmBtn)
await waitFor(() => {
expect(mockDeleteMCP).toHaveBeenCalled()
})
// onDeleted should not be called because result is not 'success'
expect(onDeleted).not.toHaveBeenCalled()
})
})
})