import { act, fireEvent, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { render } from '@/test/console/render' import MCPList from '../index' type MockProvider = { id: string name: string | Record type: string } type MockDetail = MockProvider | undefined // Mock dependencies const mockRefetch = vi.fn() const mockUseAllToolProviders = vi.fn() let mockProviders: MockProvider[] = [] let mockIsLoadingToolProviders = false const mockConsoleState = vi.hoisted(() => ({ workspacePermissionKeys: ['mcp.manage'] as string[], })) vi.mock('@/service/use-tools', () => ({ useAllToolProviders: (enabled?: boolean) => { mockUseAllToolProviders(enabled) return { data: mockProviders, isLoading: mockIsLoadingToolProviders, refetch: mockRefetch, } }, })) vi.mock('@/context/permission-state', async () => { const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture') return createPermissionStateModuleMock(() => ({ workspacePermissionKeys: mockConsoleState.workspacePermissionKeys, })) }) vi.mock('@/app/components/tools/provider/tool-card-skeleton', () => ({ default: ({ variant }: { variant?: string }) => ( <> {Array.from({ length: 6 }, (_, index) => (
Loading MCP
))} ), })) // Mock child components vi.mock('../create-card', () => ({ default: ({ handleCreate, }: { handleCreate: (provider: { id: string; name: string }) => void }) => ( ), })) 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 (
) }, })) 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 (
{displayName}
{isTriggerAuthorize ? 'true' : 'false'}
) }, })) describe('MCPList', () => { beforeEach(() => { vi.clearAllMocks() vi.useFakeTimers() mockProviders = [] mockIsLoadingToolProviders = false mockConsoleState.workspacePermissionKeys = ['mcp.manage'] mockRefetch.mockResolvedValue(undefined) }) afterEach(() => { vi.useRealTimers() }) describe('Rendering', () => { it('should render create card', () => { render() expect(screen.getByTestId('create-card')).toBeInTheDocument() }) it('should render providers read-only when user lacks mcp.manage', () => { mockConsoleState.workspacePermissionKeys = [] mockProviders = [{ id: '1', name: 'Provider 1', type: 'mcp' }] render() expect(mockUseAllToolProviders).toHaveBeenCalledWith(undefined) expect(screen.getByTestId('provider-card-1')).toBeInTheDocument() expect(screen.queryByTestId('create-card')).not.toBeInTheDocument() }) it('should hide create card when parent moves creation into the toolbar', () => { mockProviders = [{ id: '1', name: 'Provider 1', type: 'mcp' }] render() expect(screen.queryByTestId('create-card')).not.toBeInTheDocument() expect(screen.getByTestId('provider-card-1')).toBeInTheDocument() }) it('should render card skeletons while tool providers are loading', () => { mockIsLoadingToolProviders = true render() expect(screen.getAllByTestId('mcp-card-skeleton')).toHaveLength(6) expect(screen.getAllByTestId('mcp-card-skeleton')[0]).toHaveAttribute('data-variant', 'mcp') expect(screen.queryByTestId('create-card')).not.toBeInTheDocument() expect(screen.queryByTestId('provider-card-1')).not.toBeInTheDocument() }) it('should not render card skeletons when the loaded list is empty', () => { render() expect(screen.queryByTestId('mcp-card-skeleton')).not.toBeInTheDocument() }) it('should not render skeleton cards when providers exist', () => { mockProviders = [{ id: '1', name: 'Provider 1', type: 'mcp' }] render() expect(screen.queryByTestId('mcp-card-skeleton')).not.toBeInTheDocument() }) }) 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() 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() 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() 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' }, { id: '3', name: { 'en-US': 'Search API Tool' }, type: 'api' }, ] }) it('should filter providers based on search text', () => { render() expect(screen.getByTestId('provider-card-1')).toBeInTheDocument() expect(screen.queryByTestId('provider-card-2')).not.toBeInTheDocument() expect(screen.queryByTestId('provider-card-3')).not.toBeInTheDocument() }) it('should filter case-insensitively', () => { render() 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() 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() 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() 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() 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') }) it('should refetch and open detail when provider is created from the toolbar', async () => { mockProviders = [{ id: 'toolbar-id', name: 'Toolbar Provider', type: 'mcp' }] const onCreatedProviderHandled = vi.fn() await act(async () => { render( , ) await Promise.resolve() }) expect(mockRefetch).toHaveBeenCalled() expect(screen.getByTestId('detail-panel')).toBeInTheDocument() expect(screen.getByTestId('detail-name')).toHaveTextContent('Toolbar Provider') expect(screen.getByTestId('trigger-authorize')).toHaveTextContent('true') expect(onCreatedProviderHandled).toHaveBeenCalled() }) }) describe('Update Provider', () => { beforeEach(() => { mockProviders = [{ id: '1', name: 'Provider 1', type: 'mcp' }] }) it('should call refetch and set provider after update', async () => { render() 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() 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() const deleteBtn = screen.getByTestId('delete-btn-1') await act(async () => { fireEvent.click(deleteBtn) vi.advanceTimersByTime(10) }) expect(mockRefetch).toHaveBeenCalled() }) }) describe('Grid Layout', () => { it('should keep MCP cards to three columns at desktop width and above', () => { render() const grid = document.querySelector('.grid') expect(grid).toHaveClass('grid-cols-1', 'sm:grid-cols-2', 'md:grid-cols-3') expect(grid).not.toHaveClass('xl:grid-cols-4') expect(grid).not.toHaveClass('2xl:grid-cols-5') expect(grid).not.toHaveClass('2k:grid-cols-6') }) it('should have overflow hidden while loading', () => { mockProviders = [] mockIsLoadingToolProviders = true render() const grid = document.querySelector('.grid') expect(grid).toHaveClass('overflow-hidden') }) it('should not have overflow hidden when loading is complete', () => { mockProviders = [{ id: '1', name: 'Provider 1', type: 'mcp' }] render() const grid = document.querySelector('.grid') expect(grid).not.toHaveClass('overflow-hidden') }) it('should use compact content inset when requested by parent layout', () => { render() const grid = document.querySelector('.grid') expect(grid).toHaveClass('px-6') expect(grid).toHaveClass('max-w-[1600px]') expect(grid).not.toHaveClass('px-12') }) }) })