dify/web/app/components/tools/provider/__tests__/custom-create-card.spec.tsx

355 lines
12 KiB
TypeScript

import type { CustomCollectionBackend } from '../../types'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { render } from '@/test/console/render'
import { AuthType } from '../../types'
import CustomCreateCard, { NewCustomToolButton } from '../custom-create-card'
const mockConsoleState = vi.hoisted(() => ({
workspacePermissionKeys: ['tool.manage'] as string[],
}))
vi.mock('@/context/permission-state', async () => {
const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture')
return createPermissionStateModuleMock(() => ({
workspacePermissionKeys: mockConsoleState.workspacePermissionKeys,
}))
})
// Mock useLocale and useDocLink
vi.mock('@/context/i18n', () => ({
useLocale: () => 'en-US',
useDocLink: () => (path?: string) =>
`https://docs.dify.ai/en${path?.startsWith('/use-dify/') ? `/cloud${path}` : path || ''}`,
}))
// Mock getLanguage
vi.mock('@/i18n-config/language', () => ({
getLanguage: () => 'en-US',
}))
// Mock createCustomCollection service
const mockCreateCustomCollection = vi.fn()
vi.mock('@/service/tools', () => ({
createCustomCollection: (data: CustomCollectionBackend) => mockCreateCustomCollection(data),
}))
// Track modal state
let mockModalVisible = false
// Mock EditCustomToolModal - complex component
vi.mock('@/app/components/tools/edit-custom-collection-modal', () => ({
default: ({
payload,
onHide,
onAdd,
}: {
payload: null
onHide: () => void
onAdd: (data: CustomCollectionBackend) => void
}) => {
mockModalVisible = true
void onAdd // Keep reference to avoid lint warning about unused param
return (
<div data-testid="edit-custom-collection-modal">
<span data-testid="modal-payload">{payload === null ? 'null' : 'not-null'}</span>
<button data-testid="close-modal" onClick={onHide}>
Close
</button>
<button
data-testid="submit-modal"
onClick={() => {
onAdd({
provider: 'test-provider',
credentials: { auth_type: AuthType.none },
icon: { background: '#000', content: '🔧' },
schema_type: 'json',
schema: '{}',
privacy_policy: '',
custom_disclaimer: '',
id: 'test-id',
labels: [],
})
}}
>
Submit
</button>
</div>
)
},
}))
// Mock toast
const mockToastSuccess = vi.fn()
vi.mock('@langgenius/dify-ui/toast', () => ({
toast: {
success: (title: string) => mockToastSuccess(title),
},
}))
describe('CustomCreateCard', () => {
const mockOnRefreshData = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
mockConsoleState.workspacePermissionKeys = ['tool.manage']
mockModalVisible = false
mockCreateCustomCollection.mockResolvedValue({})
})
describe('tool.manage conditional rendering', () => {
it('should render card when user has tool.manage', () => {
render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
expect(screen.getByText(/createSwaggerAPIAsTool/i)).toBeInTheDocument()
})
it('should not render anything when user does not have tool.manage', () => {
mockConsoleState.workspacePermissionKeys = []
const { container } = render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
expect(container.firstChild).toBeNull()
})
})
// Tests for card rendering and styling
describe('Card Rendering', () => {
it('should render add icon', () => {
render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
// RiAddCircleFill icon should be present
const iconContainer = document.querySelector('.size-10')
expect(iconContainer).toBeInTheDocument()
})
it('should have proper card styling', () => {
render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
const card = screen.getByText('tools.createSwaggerAPIAsTool').closest('.col-span-1')
expect(card).toBeInTheDocument()
expect(card).toHaveClass(
'h-[120px]',
'border-[0.5px]',
'border-components-panel-border',
'shadow-md',
)
expect(card).toHaveClass('min-w-0')
expect(card).not.toHaveClass('flex-1')
})
it('should render documentation link with Swagger API as Tool text', () => {
render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
const docLink = screen.getByText('tools.swaggerAPIAsToolTip').closest('a')
expect(docLink).toHaveAttribute(
'href',
'https://docs.dify.ai/en/cloud/use-dify/workspace/tools#swagger-api',
)
expect(docLink).toHaveAttribute('target', '_blank')
expect(docLink).toHaveAttribute('rel', 'noopener noreferrer')
})
})
describe('Toolbar Button Rendering', () => {
it('should render toolbar add button when user has tool.manage', () => {
render(<NewCustomToolButton onRefreshData={mockOnRefreshData} />)
expect(
screen.getByRole('button', { name: /tools\.addSwaggerAPIAsTool/i }),
).toBeInTheDocument()
})
it('should not render toolbar add button when user does not have tool.manage', () => {
mockConsoleState.workspacePermissionKeys = []
const { container } = render(<NewCustomToolButton onRefreshData={mockOnRefreshData} />)
expect(container.firstChild).toBeNull()
})
it('should open modal when toolbar add button is clicked', () => {
render(<NewCustomToolButton onRefreshData={mockOnRefreshData} />)
fireEvent.click(screen.getByRole('button', { name: /tools\.addSwaggerAPIAsTool/i }))
expect(screen.getByTestId('edit-custom-collection-modal')).toBeInTheDocument()
})
})
// Tests for modal interaction
describe('Modal Interaction', () => {
it('should open modal when card is clicked', () => {
render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
// Click on the card area (the group div)
const cardClickArea = screen.getByRole('button', { name: 'tools.createSwaggerAPIAsTool' })
fireEvent.click(cardClickArea!)
expect(screen.getByTestId('edit-custom-collection-modal')).toBeInTheDocument()
expect(mockModalVisible).toBe(true)
})
it('should pass null payload to modal', () => {
render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
const cardClickArea = screen.getByRole('button', { name: 'tools.createSwaggerAPIAsTool' })
fireEvent.click(cardClickArea!)
expect(screen.getByTestId('modal-payload')).toHaveTextContent('null')
})
it('should close modal when onHide is called', () => {
render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
// Open modal
const cardClickArea = screen.getByRole('button', { name: 'tools.createSwaggerAPIAsTool' })
fireEvent.click(cardClickArea!)
expect(screen.getByTestId('edit-custom-collection-modal')).toBeInTheDocument()
// Close modal
fireEvent.click(screen.getByTestId('close-modal'))
expect(screen.queryByTestId('edit-custom-collection-modal')).not.toBeInTheDocument()
})
})
// Tests for custom collection creation
describe('Custom Collection Creation', () => {
it('should call createCustomCollection when form is submitted', async () => {
render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
// Open modal
const cardClickArea = screen.getByRole('button', { name: 'tools.createSwaggerAPIAsTool' })
fireEvent.click(cardClickArea!)
// Submit form
fireEvent.click(screen.getByTestId('submit-modal'))
await waitFor(() => {
expect(mockCreateCustomCollection).toHaveBeenCalledTimes(1)
})
})
it('should show success toast after successful creation', async () => {
render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
// Open modal
const cardClickArea = screen.getByRole('button', { name: 'tools.createSwaggerAPIAsTool' })
fireEvent.click(cardClickArea!)
// Submit form
fireEvent.click(screen.getByTestId('submit-modal'))
await waitFor(() => {
expect(mockToastSuccess).toHaveBeenCalledWith(expect.any(String))
})
})
it('should close modal after successful creation', async () => {
render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
// Open modal
const cardClickArea = screen.getByRole('button', { name: 'tools.createSwaggerAPIAsTool' })
fireEvent.click(cardClickArea!)
expect(screen.getByTestId('edit-custom-collection-modal')).toBeInTheDocument()
// Submit form
fireEvent.click(screen.getByTestId('submit-modal'))
await waitFor(() => {
expect(screen.queryByTestId('edit-custom-collection-modal')).not.toBeInTheDocument()
})
})
it('should call onRefreshData after successful creation', async () => {
render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
// Open modal
const cardClickArea = screen.getByRole('button', { name: 'tools.createSwaggerAPIAsTool' })
fireEvent.click(cardClickArea!)
// Submit form
fireEvent.click(screen.getByTestId('submit-modal'))
await waitFor(() => {
expect(mockOnRefreshData).toHaveBeenCalledTimes(1)
})
})
it('should pass correct data to createCustomCollection', async () => {
render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
// Open modal
const cardClickArea = screen.getByRole('button', { name: 'tools.createSwaggerAPIAsTool' })
fireEvent.click(cardClickArea!)
// Submit form
fireEvent.click(screen.getByTestId('submit-modal'))
await waitFor(() => {
expect(mockCreateCustomCollection).toHaveBeenCalledWith(
expect.objectContaining({
provider: 'test-provider',
schema_type: 'json',
}),
)
})
})
})
// Tests for edge cases
describe('Edge Cases', () => {
it('should call createCustomCollection and handle successful response', async () => {
mockCreateCustomCollection.mockResolvedValue({ success: true })
render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
// Open modal
const cardClickArea = screen.getByRole('button', { name: 'tools.createSwaggerAPIAsTool' })
fireEvent.click(cardClickArea!)
// Submit form
fireEvent.click(screen.getByTestId('submit-modal'))
// The API should be called
await waitFor(() => {
expect(mockCreateCustomCollection).toHaveBeenCalled()
})
// And refresh should be triggered
await waitFor(() => {
expect(mockOnRefreshData).toHaveBeenCalled()
})
})
it('should not call onRefreshData if modal is just closed without submitting', () => {
render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
// Open modal
const cardClickArea = screen.getByRole('button', { name: 'tools.createSwaggerAPIAsTool' })
fireEvent.click(cardClickArea!)
// Close modal without submitting
fireEvent.click(screen.getByTestId('close-modal'))
expect(mockOnRefreshData).not.toHaveBeenCalled()
})
it('should handle rapid open/close of modal', () => {
render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
const cardClickArea = screen.getByRole('button', { name: 'tools.createSwaggerAPIAsTool' })
// Rapid open/close
fireEvent.click(cardClickArea!)
fireEvent.click(screen.getByTestId('close-modal'))
fireEvent.click(cardClickArea!)
expect(screen.getByTestId('edit-custom-collection-modal')).toBeInTheDocument()
})
})
// Tests for hover styling
})