mirror of
https://github.com/langgenius/dify.git
synced 2026-03-30 10:30:16 +08:00
test(mcp): enhance unit tests for MCP components and improve mock handling
- Added beforeEach hooks to reset mock states in and for better test isolation. - Refactored mock data handling in to streamline test setup. - Improved error handling in to gracefully manage JSON parsing errors. - Updated to utilize a pending status for optimistic updates, enhancing user experience during state changes. These changes aim to improve the reliability and maintainability of tests across MCP components.
This commit is contained in:
@ -1,9 +1,8 @@
|
||||
/* eslint-disable react/no-unnecessary-use-prefix */
|
||||
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 { describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import NewMCPCard from './create-card'
|
||||
|
||||
// Track the mock functions
|
||||
@ -41,10 +40,13 @@ vi.mock('./modal', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// Mutable workspace manager state
|
||||
let mockIsCurrentWorkspaceManager = true
|
||||
|
||||
// Mock the app context
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
isCurrentWorkspaceManager: true,
|
||||
isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager,
|
||||
isCurrentWorkspaceEditor: true,
|
||||
}),
|
||||
}))
|
||||
@ -83,6 +85,11 @@ describe('NewMCPCard', () => {
|
||||
handleCreate: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockCreateMCP.mockClear()
|
||||
mockIsCurrentWorkspaceManager = true
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<NewMCPCard {...defaultProps} />, { wrapper: createWrapper() })
|
||||
@ -133,19 +140,11 @@ describe('NewMCPCard', () => {
|
||||
|
||||
describe('Non-Manager User', () => {
|
||||
it('should not render card when user is not workspace manager', () => {
|
||||
// Override the mock for this test
|
||||
vi.doMock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
isCurrentWorkspaceManager: false,
|
||||
isCurrentWorkspaceEditor: false,
|
||||
}),
|
||||
}))
|
||||
mockIsCurrentWorkspaceManager = false
|
||||
|
||||
// The component should not render
|
||||
render(<NewMCPCard {...defaultProps} />, { wrapper: createWrapper() })
|
||||
// Since isCurrentWorkspaceManager is mocked as true at module level,
|
||||
// the card will still render. This test documents the expected behavior.
|
||||
expect(screen.getByText('tools.mcp.create.cardTitle')).toBeInTheDocument()
|
||||
|
||||
expect(screen.queryByText('tools.mcp.create.cardTitle')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -166,10 +165,6 @@ describe('NewMCPCard', () => {
|
||||
})
|
||||
|
||||
describe('Modal Interactions', () => {
|
||||
beforeEach(() => {
|
||||
mockCreateMCP.mockClear()
|
||||
})
|
||||
|
||||
it('should call create function when modal confirms', async () => {
|
||||
const handleCreate = vi.fn()
|
||||
render(<NewMCPCard handleCreate={handleCreate} />, { wrapper: createWrapper() })
|
||||
|
||||
@ -126,9 +126,19 @@ export const useMCPModalForm = (data?: ToolWithProvider) => {
|
||||
}
|
||||
catch (e) {
|
||||
let errorMessage = 'Failed to fetch remote icon'
|
||||
const errorData = await (e as Response).json()
|
||||
if (errorData?.code)
|
||||
errorMessage = `Upload failed: ${errorData.code}`
|
||||
if (e instanceof Response) {
|
||||
try {
|
||||
const errorData = await e.json()
|
||||
if (errorData?.code)
|
||||
errorMessage = `Upload failed: ${errorData.code}`
|
||||
}
|
||||
catch {
|
||||
// Ignore JSON parsing errors
|
||||
}
|
||||
}
|
||||
else if (e instanceof Error) {
|
||||
errorMessage = e.message
|
||||
}
|
||||
console.error('Failed to fetch remote icon:', e)
|
||||
Toast.notify({ type: 'warning', message: errorMessage })
|
||||
}
|
||||
|
||||
@ -1,14 +1,28 @@
|
||||
/* eslint-disable react/no-unnecessary-use-prefix */
|
||||
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 { describe, expect, it, vi } from 'vitest'
|
||||
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: () => ({
|
||||
@ -18,16 +32,8 @@ vi.mock('@/service/use-tools', () => ({
|
||||
mutateAsync: vi.fn().mockResolvedValue({}),
|
||||
isPending: false,
|
||||
}),
|
||||
useMCPServerDetail: (appId: string) => ({
|
||||
data: appId
|
||||
? {
|
||||
id: 'server-123',
|
||||
status: 'active',
|
||||
server_code: 'abc123',
|
||||
description: 'Test server',
|
||||
parameters: {},
|
||||
}
|
||||
: undefined,
|
||||
useMCPServerDetail: () => ({
|
||||
data: mockMCPServerDetailData,
|
||||
}),
|
||||
useInvalidateMCPServerDetail: () => vi.fn(),
|
||||
}))
|
||||
@ -85,6 +91,17 @@ describe('useMCPServiceCardState', () => {
|
||||
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)
|
||||
@ -350,38 +367,26 @@ describe('useMCPServiceCardState', () => {
|
||||
|
||||
describe('Unpublished Server', () => {
|
||||
it('should open modal and return not activated when enabling unpublished server', async () => {
|
||||
// Override mock to return no data (unpublished server)
|
||||
vi.doMock('@/service/use-tools', () => ({
|
||||
useUpdateMCPServer: () => ({
|
||||
mutateAsync: vi.fn().mockResolvedValue({}),
|
||||
}),
|
||||
useRefreshMCPServerCode: () => ({
|
||||
mutateAsync: vi.fn().mockResolvedValue({}),
|
||||
isPending: false,
|
||||
}),
|
||||
useMCPServerDetail: () => ({
|
||||
data: undefined, // No server data = unpublished
|
||||
}),
|
||||
useInvalidateMCPServerDetail: () => vi.fn(),
|
||||
}))
|
||||
// Set mock to return undefined (unpublished server)
|
||||
mockMCPServerDetailData = undefined
|
||||
|
||||
// For this test, we need to test the scenario where server is not published
|
||||
// Since we can't easily change the mock, we verify the behavior with the current mock
|
||||
const appInfo = createMockAppInfo()
|
||||
const { result } = renderHook(
|
||||
() => useMCPServiceCardState(appInfo, false),
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
|
||||
// The current mock returns published server, so status change works normally
|
||||
// This test documents the expected behavior
|
||||
// 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)
|
||||
})
|
||||
|
||||
// With published server, activation succeeds
|
||||
expect(statusResult?.activated).toBe(true)
|
||||
// Should open modal and return not activated
|
||||
expect(result.current.showMCPServerModal).toBe(true)
|
||||
expect(statusResult?.activated).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -9,102 +9,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import MCPServiceCard from './mcp-service-card'
|
||||
|
||||
// Mutable mock state for dynamic testing
|
||||
type MockServerDetail = {
|
||||
id: string
|
||||
status: string
|
||||
server_code: string
|
||||
description: string
|
||||
parameters?: Record<string, unknown>
|
||||
}
|
||||
|
||||
type MockAppContext = {
|
||||
isCurrentWorkspaceManager: boolean
|
||||
isCurrentWorkspaceEditor: boolean
|
||||
}
|
||||
|
||||
type MockWorkflowData = {
|
||||
graph: {
|
||||
nodes: Array<{
|
||||
data: {
|
||||
type: string
|
||||
variables?: Array<{ variable: string, label: string }>
|
||||
}
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
type MockBasicAppConfig = {
|
||||
updated_at?: string
|
||||
user_input_form?: unknown[]
|
||||
}
|
||||
|
||||
let mockServerDetail: MockServerDetail | undefined = {
|
||||
id: 'server-123',
|
||||
status: 'active',
|
||||
server_code: 'abc123',
|
||||
description: 'Test server',
|
||||
parameters: {},
|
||||
}
|
||||
|
||||
let mockAppContext: MockAppContext = {
|
||||
isCurrentWorkspaceManager: true,
|
||||
isCurrentWorkspaceEditor: true,
|
||||
}
|
||||
|
||||
let mockWorkflowData: MockWorkflowData = {
|
||||
graph: {
|
||||
nodes: [
|
||||
{ data: { type: 'start', variables: [{ variable: 'input', label: 'Input' }] } },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
let mockBasicAppConfig: MockBasicAppConfig = {
|
||||
updated_at: '2024-01-01',
|
||||
user_input_form: [],
|
||||
}
|
||||
|
||||
const mockUpdateMCPServer = vi.fn().mockResolvedValue({})
|
||||
const mockRefreshMCPServerCode = vi.fn().mockResolvedValue({})
|
||||
const mockInvalidateMCPServerDetail = vi.fn()
|
||||
|
||||
// Mock service hooks
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useUpdateMCPServer: () => ({
|
||||
mutateAsync: mockUpdateMCPServer,
|
||||
}),
|
||||
useRefreshMCPServerCode: () => ({
|
||||
mutateAsync: mockRefreshMCPServerCode,
|
||||
isPending: false,
|
||||
}),
|
||||
useMCPServerDetail: (appId: string) => ({
|
||||
data: appId ? mockServerDetail : undefined,
|
||||
}),
|
||||
useInvalidateMCPServerDetail: () => mockInvalidateMCPServerDetail,
|
||||
}))
|
||||
|
||||
// Mock workflow hook
|
||||
vi.mock('@/service/use-workflow', () => ({
|
||||
useAppWorkflow: (appId: string) => ({
|
||||
data: appId ? mockWorkflowData : undefined,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock app context
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => mockAppContext,
|
||||
}))
|
||||
|
||||
// Mock apps service
|
||||
vi.mock('@/service/apps', () => ({
|
||||
fetchAppDetail: vi.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
model_config: mockBasicAppConfig,
|
||||
}),
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock MCPServerModal
|
||||
vi.mock('@/app/components/tools/mcp/mcp-server-modal', () => ({
|
||||
default: ({ show, onHide }: { show: boolean, onHide: () => void }) => {
|
||||
@ -133,15 +37,39 @@ vi.mock('@/app/components/base/confirm', () => ({
|
||||
}))
|
||||
|
||||
// Mutable mock handlers for hook
|
||||
let mockHandleStatusChange = vi.fn().mockResolvedValue({ activated: true })
|
||||
let mockHandleServerModalHide = vi.fn().mockReturnValue({ shouldDeactivate: false })
|
||||
let mockHandleGenCode = vi.fn()
|
||||
let mockOpenConfirmDelete = vi.fn()
|
||||
let mockCloseConfirmDelete = vi.fn()
|
||||
let mockOpenServerModal = vi.fn()
|
||||
const mockHandleStatusChange = vi.fn().mockResolvedValue({ activated: true })
|
||||
const mockHandleServerModalHide = vi.fn().mockReturnValue({ shouldDeactivate: false })
|
||||
const mockHandleGenCode = vi.fn()
|
||||
const mockOpenConfirmDelete = vi.fn()
|
||||
const mockCloseConfirmDelete = vi.fn()
|
||||
const mockOpenServerModal = vi.fn()
|
||||
|
||||
// Mutable hook state
|
||||
let mockHookState = {
|
||||
// Type for mock hook state
|
||||
type MockHookState = {
|
||||
genLoading: boolean
|
||||
isLoading: boolean
|
||||
serverPublished: boolean
|
||||
serverActivated: boolean
|
||||
serverURL: string
|
||||
detail: {
|
||||
id: string
|
||||
status: string
|
||||
server_code: string
|
||||
description: string
|
||||
parameters: Record<string, unknown>
|
||||
} | undefined
|
||||
isCurrentWorkspaceManager: boolean
|
||||
toggleDisabled: boolean
|
||||
isMinimalState: boolean
|
||||
appUnpublished: boolean
|
||||
missingStartNode: boolean
|
||||
showConfirmDelete: boolean
|
||||
showMCPServerModal: boolean
|
||||
latestParams: Array<unknown>
|
||||
}
|
||||
|
||||
// Default hook state factory - creates fresh state for each test
|
||||
const createDefaultHookState = (): MockHookState => ({
|
||||
genLoading: false,
|
||||
isLoading: false,
|
||||
serverPublished: true,
|
||||
@ -162,9 +90,12 @@ let mockHookState = {
|
||||
showConfirmDelete: false,
|
||||
showMCPServerModal: false,
|
||||
latestParams: [],
|
||||
}
|
||||
})
|
||||
|
||||
// Mock the hook
|
||||
// Mutable hook state - modify this in tests to change component behavior
|
||||
let mockHookState = createDefaultHookState()
|
||||
|
||||
// Mock the hook - uses mockHookState which can be modified per test
|
||||
vi.mock('./hooks/use-mcp-service-card', () => ({
|
||||
useMCPServiceCardState: () => ({
|
||||
...mockHookState,
|
||||
@ -198,63 +129,16 @@ describe('MCPServiceCard', () => {
|
||||
} as AppDetailResponse & Partial<AppSSO>)
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mock states
|
||||
mockServerDetail = {
|
||||
id: 'server-123',
|
||||
status: 'active',
|
||||
server_code: 'abc123',
|
||||
description: 'Test server',
|
||||
parameters: {},
|
||||
}
|
||||
mockAppContext = {
|
||||
isCurrentWorkspaceManager: true,
|
||||
isCurrentWorkspaceEditor: true,
|
||||
}
|
||||
mockWorkflowData = {
|
||||
graph: {
|
||||
nodes: [
|
||||
{ data: { type: 'start', variables: [{ variable: 'input', label: 'Input' }] } },
|
||||
],
|
||||
},
|
||||
}
|
||||
mockBasicAppConfig = {
|
||||
updated_at: '2024-01-01',
|
||||
user_input_form: [],
|
||||
}
|
||||
mockUpdateMCPServer.mockClear()
|
||||
mockRefreshMCPServerCode.mockClear()
|
||||
mockInvalidateMCPServerDetail.mockClear()
|
||||
// Reset hook state to defaults before each test
|
||||
mockHookState = createDefaultHookState()
|
||||
|
||||
// Reset hook mocks
|
||||
mockHandleStatusChange = vi.fn().mockResolvedValue({ activated: true })
|
||||
mockHandleServerModalHide = vi.fn().mockReturnValue({ shouldDeactivate: false })
|
||||
mockHandleGenCode = vi.fn()
|
||||
mockOpenConfirmDelete = vi.fn()
|
||||
mockCloseConfirmDelete = vi.fn()
|
||||
mockOpenServerModal = vi.fn()
|
||||
|
||||
mockHookState = {
|
||||
genLoading: false,
|
||||
isLoading: false,
|
||||
serverPublished: true,
|
||||
serverActivated: true,
|
||||
serverURL: 'https://api.example.com/mcp/server/abc123/mcp',
|
||||
detail: {
|
||||
id: 'server-123',
|
||||
status: 'active',
|
||||
server_code: 'abc123',
|
||||
description: 'Test server',
|
||||
parameters: {},
|
||||
},
|
||||
isCurrentWorkspaceManager: true,
|
||||
toggleDisabled: false,
|
||||
isMinimalState: false,
|
||||
appUnpublished: false,
|
||||
missingStartNode: false,
|
||||
showConfirmDelete: false,
|
||||
showMCPServerModal: false,
|
||||
latestParams: [],
|
||||
}
|
||||
// Reset all mock function call history
|
||||
mockHandleStatusChange.mockClear().mockResolvedValue({ activated: true })
|
||||
mockHandleServerModalHide.mockClear().mockReturnValue({ shouldDeactivate: false })
|
||||
mockHandleGenCode.mockClear()
|
||||
mockOpenConfirmDelete.mockClear()
|
||||
mockCloseConfirmDelete.mockClear()
|
||||
mockOpenServerModal.mockClear()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
@ -502,7 +386,15 @@ describe('MCPServiceCard', () => {
|
||||
|
||||
describe('Server Not Published', () => {
|
||||
beforeEach(() => {
|
||||
mockServerDetail = undefined
|
||||
// Modify hookState to simulate unpublished server
|
||||
mockHookState = {
|
||||
...createDefaultHookState(),
|
||||
serverPublished: false,
|
||||
serverActivated: false,
|
||||
serverURL: '***********',
|
||||
detail: undefined,
|
||||
isMinimalState: true,
|
||||
}
|
||||
})
|
||||
|
||||
it('should show add description button when server is not published', () => {
|
||||
@ -541,12 +433,17 @@ describe('MCPServiceCard', () => {
|
||||
|
||||
describe('Inactive Server', () => {
|
||||
beforeEach(() => {
|
||||
mockServerDetail = {
|
||||
id: 'server-123',
|
||||
status: 'inactive',
|
||||
server_code: 'abc123',
|
||||
description: 'Test server',
|
||||
parameters: {},
|
||||
// Modify hookState to simulate inactive server
|
||||
mockHookState = {
|
||||
...createDefaultHookState(),
|
||||
serverActivated: false,
|
||||
detail: {
|
||||
id: 'server-123',
|
||||
status: 'inactive',
|
||||
server_code: 'abc123',
|
||||
description: 'Test server',
|
||||
parameters: {},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@ -575,9 +472,10 @@ describe('MCPServiceCard', () => {
|
||||
|
||||
describe('Non-Manager User', () => {
|
||||
beforeEach(() => {
|
||||
mockAppContext = {
|
||||
// Modify hookState to simulate non-manager user
|
||||
mockHookState = {
|
||||
...createDefaultHookState(),
|
||||
isCurrentWorkspaceManager: false,
|
||||
isCurrentWorkspaceEditor: true,
|
||||
}
|
||||
})
|
||||
|
||||
@ -593,7 +491,7 @@ describe('MCPServiceCard', () => {
|
||||
describe('Non-Editor User', () => {
|
||||
it('should show disabled styling for non-editor switch', () => {
|
||||
mockHookState = {
|
||||
...mockHookState,
|
||||
...createDefaultHookState(),
|
||||
toggleDisabled: true,
|
||||
}
|
||||
|
||||
@ -690,24 +588,31 @@ describe('MCPServiceCard', () => {
|
||||
})
|
||||
|
||||
it('should deactivate switch when modal closes without previous activation', async () => {
|
||||
mockServerDetail = undefined // Unpublished server
|
||||
// Simulate unpublished server state
|
||||
mockHookState = {
|
||||
...createDefaultHookState(),
|
||||
serverPublished: false,
|
||||
serverActivated: false,
|
||||
detail: undefined,
|
||||
showMCPServerModal: true,
|
||||
}
|
||||
|
||||
const appInfo = createMockAppInfo()
|
||||
render(<MCPServiceCard appInfo={appInfo} />, { wrapper: createWrapper() })
|
||||
|
||||
const switchElement = screen.getByRole('switch')
|
||||
fireEvent.click(switchElement)
|
||||
// Modal should be visible
|
||||
const modal = screen.getByTestId('mcp-server-modal')
|
||||
expect(modal).toBeInTheDocument()
|
||||
|
||||
const closeBtn = screen.getByTestId('close-modal-btn')
|
||||
fireEvent.click(closeBtn)
|
||||
|
||||
await waitFor(() => {
|
||||
const modal = screen.queryByTestId('mcp-server-modal')
|
||||
if (modal) {
|
||||
expect(modal).toBeInTheDocument()
|
||||
const closeBtn = screen.getByTestId('close-modal-btn')
|
||||
fireEvent.click(closeBtn)
|
||||
}
|
||||
expect(mockHandleServerModalHide).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Switch should be off after closing modal without activation
|
||||
const switchElement = screen.getByRole('switch')
|
||||
expect(switchElement).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -715,7 +620,7 @@ describe('MCPServiceCard', () => {
|
||||
describe('Unpublished App', () => {
|
||||
it('should show minimal state for unpublished app', () => {
|
||||
mockHookState = {
|
||||
...mockHookState,
|
||||
...createDefaultHookState(),
|
||||
appUnpublished: true,
|
||||
isMinimalState: true,
|
||||
}
|
||||
@ -728,7 +633,7 @@ describe('MCPServiceCard', () => {
|
||||
|
||||
it('should show disabled styling for unpublished app switch', () => {
|
||||
mockHookState = {
|
||||
...mockHookState,
|
||||
...createDefaultHookState(),
|
||||
appUnpublished: true,
|
||||
toggleDisabled: true,
|
||||
}
|
||||
@ -746,7 +651,7 @@ describe('MCPServiceCard', () => {
|
||||
describe('Workflow App Without Start Node', () => {
|
||||
it('should show minimal state for workflow without start node', () => {
|
||||
mockHookState = {
|
||||
...mockHookState,
|
||||
...createDefaultHookState(),
|
||||
missingStartNode: true,
|
||||
isMinimalState: true,
|
||||
}
|
||||
@ -759,7 +664,7 @@ describe('MCPServiceCard', () => {
|
||||
|
||||
it('should show disabled styling for workflow without start node', () => {
|
||||
mockHookState = {
|
||||
...mockHookState,
|
||||
...createDefaultHookState(),
|
||||
missingStartNode: true,
|
||||
toggleDisabled: true,
|
||||
}
|
||||
@ -777,7 +682,7 @@ describe('MCPServiceCard', () => {
|
||||
describe('Loading State', () => {
|
||||
it('should return null when isLoading is true', () => {
|
||||
mockHookState = {
|
||||
...mockHookState,
|
||||
...createDefaultHookState(),
|
||||
isLoading: true,
|
||||
}
|
||||
|
||||
@ -790,7 +695,7 @@ describe('MCPServiceCard', () => {
|
||||
|
||||
it('should render content when isLoading is false', () => {
|
||||
mockHookState = {
|
||||
...mockHookState,
|
||||
...createDefaultHookState(),
|
||||
isLoading: false,
|
||||
}
|
||||
|
||||
@ -829,10 +734,10 @@ describe('MCPServiceCard', () => {
|
||||
it('should call handleStatusChange with false when turning off', async () => {
|
||||
// Start with server activated
|
||||
mockHookState = {
|
||||
...mockHookState,
|
||||
...createDefaultHookState(),
|
||||
serverActivated: true,
|
||||
}
|
||||
mockHandleStatusChange = vi.fn().mockResolvedValue({ activated: false })
|
||||
mockHandleStatusChange.mockResolvedValue({ activated: false })
|
||||
|
||||
const appInfo = createMockAppInfo()
|
||||
render(<MCPServiceCard appInfo={appInfo} />, { wrapper: createWrapper() })
|
||||
@ -850,10 +755,10 @@ describe('MCPServiceCard', () => {
|
||||
it('should call handleStatusChange with true when turning on', async () => {
|
||||
// Start with server deactivated
|
||||
mockHookState = {
|
||||
...mockHookState,
|
||||
...createDefaultHookState(),
|
||||
serverActivated: false,
|
||||
}
|
||||
mockHandleStatusChange = vi.fn().mockResolvedValue({ activated: true })
|
||||
mockHandleStatusChange.mockResolvedValue({ activated: true })
|
||||
|
||||
const appInfo = createMockAppInfo()
|
||||
render(<MCPServiceCard appInfo={appInfo} />, { wrapper: createWrapper() })
|
||||
@ -871,12 +776,12 @@ describe('MCPServiceCard', () => {
|
||||
it('should set local activated to false when handleStatusChange returns activated: false and state is true', async () => {
|
||||
// Simulate unpublished server scenario where enabling opens modal
|
||||
mockHookState = {
|
||||
...mockHookState,
|
||||
...createDefaultHookState(),
|
||||
serverActivated: false,
|
||||
serverPublished: false,
|
||||
}
|
||||
// Handler returns activated: false (modal opened instead)
|
||||
mockHandleStatusChange = vi.fn().mockResolvedValue({ activated: false })
|
||||
mockHandleStatusChange.mockResolvedValue({ activated: false })
|
||||
|
||||
const appInfo = createMockAppInfo()
|
||||
render(<MCPServiceCard appInfo={appInfo} />, { wrapper: createWrapper() })
|
||||
@ -899,11 +804,11 @@ describe('MCPServiceCard', () => {
|
||||
it('should deactivate when handleServerModalHide returns shouldDeactivate: true', async () => {
|
||||
// Set up to show modal
|
||||
mockHookState = {
|
||||
...mockHookState,
|
||||
...createDefaultHookState(),
|
||||
showMCPServerModal: true,
|
||||
serverActivated: false, // Server was not activated
|
||||
}
|
||||
mockHandleServerModalHide = vi.fn().mockReturnValue({ shouldDeactivate: true })
|
||||
mockHandleServerModalHide.mockReturnValue({ shouldDeactivate: true })
|
||||
|
||||
const appInfo = createMockAppInfo()
|
||||
render(<MCPServiceCard appInfo={appInfo} />, { wrapper: createWrapper() })
|
||||
@ -919,11 +824,11 @@ describe('MCPServiceCard', () => {
|
||||
|
||||
it('should not deactivate when handleServerModalHide returns shouldDeactivate: false', async () => {
|
||||
mockHookState = {
|
||||
...mockHookState,
|
||||
...createDefaultHookState(),
|
||||
showMCPServerModal: true,
|
||||
serverActivated: true, // Server was already activated
|
||||
}
|
||||
mockHandleServerModalHide = vi.fn().mockReturnValue({ shouldDeactivate: false })
|
||||
mockHandleServerModalHide.mockReturnValue({ shouldDeactivate: false })
|
||||
|
||||
const appInfo = createMockAppInfo()
|
||||
render(<MCPServiceCard appInfo={appInfo} />, { wrapper: createWrapper() })
|
||||
@ -942,7 +847,7 @@ describe('MCPServiceCard', () => {
|
||||
it('should call handleGenCode and closeConfirmDelete when confirm is clicked', async () => {
|
||||
// Set up to show confirm dialog
|
||||
mockHookState = {
|
||||
...mockHookState,
|
||||
...createDefaultHookState(),
|
||||
showConfirmDelete: true,
|
||||
}
|
||||
|
||||
@ -965,7 +870,7 @@ describe('MCPServiceCard', () => {
|
||||
|
||||
it('should call closeConfirmDelete when cancel is clicked', async () => {
|
||||
mockHookState = {
|
||||
...mockHookState,
|
||||
...createDefaultHookState(),
|
||||
showConfirmDelete: true,
|
||||
}
|
||||
|
||||
@ -984,7 +889,13 @@ describe('MCPServiceCard', () => {
|
||||
|
||||
describe('getTooltipContent Function', () => {
|
||||
it('should show publish tip when app is unpublished', () => {
|
||||
mockBasicAppConfig = {} // Unpublished
|
||||
// Modify hookState to simulate unpublished app
|
||||
mockHookState = {
|
||||
...createDefaultHookState(),
|
||||
appUnpublished: true,
|
||||
toggleDisabled: true,
|
||||
isMinimalState: true,
|
||||
}
|
||||
|
||||
const appInfo = createMockAppInfo()
|
||||
render(<MCPServiceCard appInfo={appInfo} />, { wrapper: createWrapper() })
|
||||
@ -994,10 +905,12 @@ describe('MCPServiceCard', () => {
|
||||
})
|
||||
|
||||
it('should show missing start node tooltip for workflow without start node', () => {
|
||||
mockWorkflowData = {
|
||||
graph: {
|
||||
nodes: [{ data: { type: 'end' } }],
|
||||
},
|
||||
// Modify hookState to simulate missing start node
|
||||
mockHookState = {
|
||||
...createDefaultHookState(),
|
||||
missingStartNode: true,
|
||||
toggleDisabled: true,
|
||||
isMinimalState: true,
|
||||
}
|
||||
|
||||
const appInfo = createMockAppInfo(AppModeEnum.WORKFLOW)
|
||||
|
||||
@ -187,27 +187,23 @@ const MCPServiceCard: FC<IAppCardProps> = ({
|
||||
openServerModal,
|
||||
} = useMCPServiceCardState(appInfo, triggerModeDisabled)
|
||||
|
||||
// Local UI state for optimistic update
|
||||
const [activated, setActivated] = useState(serverActivated)
|
||||
|
||||
// Sync with server state when it changes
|
||||
if (activated !== serverActivated && !showMCPServerModal) {
|
||||
setActivated(serverActivated)
|
||||
}
|
||||
// Pending status for optimistic updates (null means use server state)
|
||||
const [pendingStatus, setPendingStatus] = useState<boolean | null>(null)
|
||||
const activated = pendingStatus ?? serverActivated
|
||||
|
||||
const onChangeStatus = async (state: boolean) => {
|
||||
setActivated(state)
|
||||
setPendingStatus(state)
|
||||
const result = await handleStatusChange(state)
|
||||
if (!result.activated && state) {
|
||||
// Server modal was opened instead, keep local state false
|
||||
setActivated(false)
|
||||
// Server modal was opened instead, clear pending status
|
||||
setPendingStatus(null)
|
||||
}
|
||||
}
|
||||
|
||||
const onServerModalHide = () => {
|
||||
const result = handleServerModalHide(serverActivated)
|
||||
if (result.shouldDeactivate)
|
||||
setActivated(false)
|
||||
handleServerModalHide(serverActivated)
|
||||
// Clear pending status when modal closes to sync with server state
|
||||
setPendingStatus(null)
|
||||
}
|
||||
|
||||
const onConfirmRegenerate = () => {
|
||||
|
||||
Reference in New Issue
Block a user