Files
dify/web/app/components/tools/mcp/hooks/use-mcp-service-card.spec.ts
Coding On Star c58647d39c refactor(web): extract MCP components and add comprehensive tests (#31517)
Co-authored-by: CodingOnStar <hanxujiang@dify.ai>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
2026-01-27 11:05:59 +08:00

452 lines
13 KiB
TypeScript

import type { ReactNode } from 'react'
import type { AppDetailResponse } from '@/models/app'
import type { AppSSO } from '@/types/app'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, renderHook } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AppModeEnum } from '@/types/app'
import { useMCPServiceCardState } from './use-mcp-service-card'
// Mutable mock data for MCP server detail
let mockMCPServerDetailData: {
id: string
status: string
server_code: string
description: string
parameters: Record<string, unknown>
} | undefined = {
id: 'server-123',
status: 'active',
server_code: 'abc123',
description: 'Test server',
parameters: {},
}
// Mock service hooks
vi.mock('@/service/use-tools', () => ({
useUpdateMCPServer: () => ({
mutateAsync: vi.fn().mockResolvedValue({}),
}),
useRefreshMCPServerCode: () => ({
mutateAsync: vi.fn().mockResolvedValue({}),
isPending: false,
}),
useMCPServerDetail: () => ({
data: mockMCPServerDetailData,
}),
useInvalidateMCPServerDetail: () => vi.fn(),
}))
// Mock workflow hook
vi.mock('@/service/use-workflow', () => ({
useAppWorkflow: (appId: string) => ({
data: appId
? {
graph: {
nodes: [
{ data: { type: 'start', variables: [{ variable: 'input', label: 'Input' }] } },
],
},
}
: undefined,
}),
}))
// Mock app context
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
isCurrentWorkspaceManager: true,
isCurrentWorkspaceEditor: true,
}),
}))
// Mock apps service
vi.mock('@/service/apps', () => ({
fetchAppDetail: vi.fn().mockResolvedValue({
model_config: {
updated_at: '2024-01-01',
user_input_form: [],
},
}),
}))
describe('useMCPServiceCardState', () => {
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
return ({ children }: { children: ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children)
}
const createMockAppInfo = (mode: AppModeEnum = AppModeEnum.CHAT): AppDetailResponse & Partial<AppSSO> => ({
id: 'app-123',
name: 'Test App',
mode,
api_base_url: 'https://api.example.com/v1',
} as AppDetailResponse & Partial<AppSSO>)
beforeEach(() => {
// Reset mock data to default (published server)
mockMCPServerDetailData = {
id: 'server-123',
status: 'active',
server_code: 'abc123',
description: 'Test server',
parameters: {},
}
})
describe('Initialization', () => {
it('should initialize with correct default values for basic app', () => {
const appInfo = createMockAppInfo(AppModeEnum.CHAT)
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
expect(result.current.serverPublished).toBe(true)
expect(result.current.serverActivated).toBe(true)
expect(result.current.showConfirmDelete).toBe(false)
expect(result.current.showMCPServerModal).toBe(false)
})
it('should initialize with correct values for workflow app', () => {
const appInfo = createMockAppInfo(AppModeEnum.WORKFLOW)
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
expect(result.current.isLoading).toBe(false)
})
it('should initialize with correct values for advanced chat app', () => {
const appInfo = createMockAppInfo(AppModeEnum.ADVANCED_CHAT)
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
expect(result.current.isLoading).toBe(false)
})
})
describe('Server URL Generation', () => {
it('should generate correct server URL when published', () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
expect(result.current.serverURL).toBe('https://api.example.com/mcp/server/abc123/mcp')
})
})
describe('Permission Flags', () => {
it('should have isCurrentWorkspaceManager as true', () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
expect(result.current.isCurrentWorkspaceManager).toBe(true)
})
it('should have toggleDisabled false when editor has permissions', () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
// Toggle is not disabled when user has permissions and app is published
expect(typeof result.current.toggleDisabled).toBe('boolean')
})
it('should have toggleDisabled true when triggerModeDisabled is true', () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, true),
{ wrapper: createWrapper() },
)
expect(result.current.toggleDisabled).toBe(true)
})
})
describe('UI State Actions', () => {
it('should open confirm delete modal', () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
expect(result.current.showConfirmDelete).toBe(false)
act(() => {
result.current.openConfirmDelete()
})
expect(result.current.showConfirmDelete).toBe(true)
})
it('should close confirm delete modal', () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
act(() => {
result.current.openConfirmDelete()
})
expect(result.current.showConfirmDelete).toBe(true)
act(() => {
result.current.closeConfirmDelete()
})
expect(result.current.showConfirmDelete).toBe(false)
})
it('should open server modal', () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
expect(result.current.showMCPServerModal).toBe(false)
act(() => {
result.current.openServerModal()
})
expect(result.current.showMCPServerModal).toBe(true)
})
it('should handle server modal hide', () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
act(() => {
result.current.openServerModal()
})
expect(result.current.showMCPServerModal).toBe(true)
let hideResult: { shouldDeactivate: boolean } | undefined
act(() => {
hideResult = result.current.handleServerModalHide(false)
})
expect(result.current.showMCPServerModal).toBe(false)
expect(hideResult?.shouldDeactivate).toBe(true)
})
it('should not deactivate when wasActivated is true', () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
let hideResult: { shouldDeactivate: boolean } | undefined
act(() => {
hideResult = result.current.handleServerModalHide(true)
})
expect(hideResult?.shouldDeactivate).toBe(false)
})
})
describe('Handler Functions', () => {
it('should have handleGenCode function', () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
expect(typeof result.current.handleGenCode).toBe('function')
})
it('should call handleGenCode and invalidate server detail', async () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
await act(async () => {
await result.current.handleGenCode()
})
// handleGenCode should complete without error
expect(result.current.genLoading).toBe(false)
})
it('should have handleStatusChange function', () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
expect(typeof result.current.handleStatusChange).toBe('function')
})
it('should have invalidateBasicAppConfig function', () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
expect(typeof result.current.invalidateBasicAppConfig).toBe('function')
})
it('should call invalidateBasicAppConfig', () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
// Call the function - should not throw
act(() => {
result.current.invalidateBasicAppConfig()
})
// Function should exist and be callable
expect(typeof result.current.invalidateBasicAppConfig).toBe('function')
})
})
describe('Status Change', () => {
it('should return activated state when status change succeeds', async () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
let statusResult: { activated: boolean } | undefined
await act(async () => {
statusResult = await result.current.handleStatusChange(true)
})
expect(statusResult?.activated).toBe(true)
})
it('should return deactivated state when disabling', async () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
let statusResult: { activated: boolean } | undefined
await act(async () => {
statusResult = await result.current.handleStatusChange(false)
})
expect(statusResult?.activated).toBe(false)
})
})
describe('Unpublished Server', () => {
it('should open modal and return not activated when enabling unpublished server', async () => {
// Set mock to return undefined (unpublished server)
mockMCPServerDetailData = undefined
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
// Verify server is not published
expect(result.current.serverPublished).toBe(false)
let statusResult: { activated: boolean } | undefined
await act(async () => {
statusResult = await result.current.handleStatusChange(true)
})
// Should open modal and return not activated
expect(result.current.showMCPServerModal).toBe(true)
expect(statusResult?.activated).toBe(false)
})
})
describe('Loading States', () => {
it('should have genLoading state', () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
expect(typeof result.current.genLoading).toBe('boolean')
})
it('should have isLoading state for basic app', () => {
const appInfo = createMockAppInfo(AppModeEnum.CHAT)
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
// Basic app doesn't need workflow, so isLoading should be false
expect(result.current.isLoading).toBe(false)
})
})
describe('Detail Data', () => {
it('should return detail data when available', () => {
const appInfo = createMockAppInfo()
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
expect(result.current.detail).toBeDefined()
expect(result.current.detail?.id).toBe('server-123')
expect(result.current.detail?.status).toBe('active')
})
})
describe('Latest Params', () => {
it('should return latestParams for workflow app', () => {
const appInfo = createMockAppInfo(AppModeEnum.WORKFLOW)
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
expect(Array.isArray(result.current.latestParams)).toBe(true)
})
it('should return latestParams for basic app', () => {
const appInfo = createMockAppInfo(AppModeEnum.CHAT)
const { result } = renderHook(
() => useMCPServiceCardState(appInfo, false),
{ wrapper: createWrapper() },
)
expect(Array.isArray(result.current.latestParams)).toBe(true)
})
})
})