import type { WorkflowToolModalPayload } from './index' import type { WorkflowToolProviderResponse } from '@/app/components/tools/types' import type { InputVar, Variable } from '@/app/components/workflow/types' import { act, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' import { InputVarType, VarType } from '@/app/components/workflow/types' import WorkflowToolConfigureButton from './configure-button' import WorkflowToolAsModal from './index' import MethodSelector from './method-selector' // Mock Next.js navigation const mockPush = vi.fn() vi.mock('next/navigation', () => ({ useRouter: () => ({ push: mockPush, replace: vi.fn(), prefetch: vi.fn(), }), usePathname: () => '/app/workflow-app-id', useSearchParams: () => new URLSearchParams(), })) // Mock app context const mockIsCurrentWorkspaceManager = vi.fn(() => true) vi.mock('@/context/app-context', () => ({ useAppContext: () => ({ isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager(), }), })) // Mock API services - only mock external services const mockFetchWorkflowToolDetailByAppID = vi.fn() const mockCreateWorkflowToolProvider = vi.fn() const mockSaveWorkflowToolProvider = vi.fn() vi.mock('@/service/tools', () => ({ fetchWorkflowToolDetailByAppID: (...args: unknown[]) => mockFetchWorkflowToolDetailByAppID(...args), createWorkflowToolProvider: (...args: unknown[]) => mockCreateWorkflowToolProvider(...args), saveWorkflowToolProvider: (...args: unknown[]) => mockSaveWorkflowToolProvider(...args), })) // Mock invalidate workflow tools hook const mockInvalidateAllWorkflowTools = vi.fn() vi.mock('@/service/use-tools', () => ({ useInvalidateAllWorkflowTools: () => mockInvalidateAllWorkflowTools, })) // Mock Toast - need to verify notification calls const mockToastNotify = vi.fn() vi.mock('@/app/components/base/toast', () => ({ default: { notify: (options: { type: string, message: string }) => mockToastNotify(options), }, })) // Mock useTags hook used by LabelSelector - returns empty tags for testing vi.mock('@/app/components/plugins/hooks', () => ({ useTags: () => ({ tags: [ { name: 'label1', label: 'Label 1' }, { name: 'label2', label: 'Label 2' }, ], }), })) // Mock Drawer - simplified for testing, preserves behavior vi.mock('@/app/components/base/drawer-plus', () => ({ default: ({ isShow, onHide, title, body }: { isShow: boolean, onHide: () => void, title: string, body: React.ReactNode }) => { if (!isShow) return null return (
{title}
{body}
) }, })) // Mock EmojiPicker - simplified for testing vi.mock('@/app/components/base/emoji-picker', () => ({ default: ({ onSelect, onClose }: { onSelect: (icon: string, background: string) => void, onClose: () => void }) => (
), })) // Mock AppIcon - simplified for testing vi.mock('@/app/components/base/app-icon', () => ({ default: ({ onClick, icon, background }: { onClick?: () => void, icon: string, background: string }) => (
{icon}
), })) // Mock LabelSelector - simplified for testing vi.mock('@/app/components/tools/labels/selector', () => ({ default: ({ value, onChange }: { value: string[], onChange: (labels: string[]) => void }) => (
{value.join(',')}
), })) // Mock PortalToFollowElem for dropdown tests let mockPortalOpenState = false vi.mock('@/app/components/base/portal-to-follow-elem', () => ({ PortalToFollowElem: ({ children, open, onOpenChange }: { children: React.ReactNode, open: boolean, onOpenChange: (open: boolean) => void }) => { mockPortalOpenState = open return (
onOpenChange(!open)}> {children}
) }, PortalToFollowElemTrigger: ({ children, onClick, className }: { children: React.ReactNode, onClick: () => void, className?: string }) => (
{children}
), PortalToFollowElemContent: ({ children, className }: { children: React.ReactNode, className?: string }) => { if (!mockPortalOpenState) return null return
{children}
}, })) // Test data factories const createMockEmoji = (overrides = {}) => ({ content: 'πŸ”§', background: '#ffffff', ...overrides, }) const createMockInputVar = (overrides: Partial = {}): InputVar => ({ variable: 'test_var', label: 'Test Variable', type: InputVarType.textInput, required: true, max_length: 100, options: [], ...overrides, } as InputVar) const createMockVariable = (overrides: Partial = {}): Variable => ({ variable: 'output_var', value_type: 'string', ...overrides, } as Variable) const createMockWorkflowToolDetail = (overrides: Partial = {}): WorkflowToolProviderResponse => ({ workflow_app_id: 'workflow-app-123', workflow_tool_id: 'workflow-tool-456', label: 'Test Tool', name: 'test_tool', icon: createMockEmoji(), description: 'A test workflow tool', synced: true, tool: { author: 'test-author', name: 'test_tool', label: { en_US: 'Test Tool', zh_Hans: 'ζ΅‹θ―•ε·₯ε…·' }, description: { en_US: 'Test description', zh_Hans: '桋试描述' }, labels: ['label1', 'label2'], parameters: [ { name: 'test_var', label: { en_US: 'Test Variable', zh_Hans: 'ζ΅‹θ―•ε˜ι‡' }, human_description: { en_US: 'A test variable', zh_Hans: 'ζ΅‹θ―•ε˜ι‡' }, type: 'string', form: 'llm', llm_description: 'Test variable description', required: true, default: '', }, ], output_schema: { type: 'object', properties: { output_var: { type: 'string', description: 'Output description', }, }, }, }, privacy_policy: 'https://example.com/privacy', ...overrides, }) const createDefaultConfigureButtonProps = (overrides = {}) => ({ disabled: false, published: false, detailNeedUpdate: false, workflowAppId: 'workflow-app-123', icon: createMockEmoji(), name: 'Test Workflow', description: 'Test workflow description', inputs: [createMockInputVar()], outputs: [createMockVariable()], handlePublish: vi.fn().mockResolvedValue(undefined), onRefreshData: vi.fn(), ...overrides, }) const createDefaultModalPayload = (overrides: Partial = {}): WorkflowToolModalPayload => ({ icon: createMockEmoji(), label: 'Test Tool', name: 'test_tool', description: 'Test description', parameters: [ { name: 'param1', description: 'Parameter 1', form: 'llm', required: true, type: 'string', }, ], outputParameters: [ { name: 'output1', description: 'Output 1', }, ], labels: ['label1'], privacy_policy: '', workflow_app_id: 'workflow-app-123', ...overrides, }) // ============================================================================ // WorkflowToolConfigureButton Tests // ============================================================================ describe('WorkflowToolConfigureButton', () => { beforeEach(() => { vi.clearAllMocks() mockPortalOpenState = false mockIsCurrentWorkspaceManager.mockReturnValue(true) mockFetchWorkflowToolDetailByAppID.mockResolvedValue(createMockWorkflowToolDetail()) }) // Rendering Tests (REQUIRED) describe('Rendering', () => { it('should render without crashing', () => { // Arrange const props = createDefaultConfigureButtonProps() // Act render() // Assert expect(screen.getByText('workflow.common.workflowAsTool')).toBeInTheDocument() }) it('should render configure required badge when not published', () => { // Arrange const props = createDefaultConfigureButtonProps({ published: false }) // Act render() // Assert expect(screen.getByText('workflow.common.configureRequired')).toBeInTheDocument() }) it('should not render configure required badge when published', async () => { // Arrange const props = createDefaultConfigureButtonProps({ published: true }) // Act render() // Assert await waitFor(() => { expect(screen.queryByText('workflow.common.configureRequired')).not.toBeInTheDocument() }) }) it('should render disabled state with cursor-not-allowed', () => { // Arrange const props = createDefaultConfigureButtonProps({ disabled: true }) // Act render() // Assert const container = document.querySelector('.cursor-not-allowed') expect(container).toBeInTheDocument() }) it('should render disabledReason when provided', () => { // Arrange const props = createDefaultConfigureButtonProps({ disabledReason: 'Please save the workflow first', }) // Act render() // Assert expect(screen.getByText('Please save the workflow first')).toBeInTheDocument() }) it('should render loading state when published and fetching details', async () => { // Arrange mockFetchWorkflowToolDetailByAppID.mockImplementation(() => new Promise(() => {})) // Never resolves const props = createDefaultConfigureButtonProps({ published: true }) // Act render() // Assert await waitFor(() => { const loadingElement = document.querySelector('.pt-2') expect(loadingElement).toBeInTheDocument() }) }) it('should render configure and manage buttons when published', async () => { // Arrange const props = createDefaultConfigureButtonProps({ published: true }) // Act render() // Assert await waitFor(() => { expect(screen.getByText('workflow.common.configure')).toBeInTheDocument() expect(screen.getByText('workflow.common.manageInTools')).toBeInTheDocument() }) }) it('should render different UI for non-workspace manager', () => { // Arrange mockIsCurrentWorkspaceManager.mockReturnValue(false) const props = createDefaultConfigureButtonProps() // Act render() // Assert const textElement = screen.getByText('workflow.common.workflowAsTool') expect(textElement).toHaveClass('text-text-tertiary') }) }) // Props Testing (REQUIRED) describe('Props', () => { it('should handle all required props', () => { // Arrange const props = createDefaultConfigureButtonProps() // Act & Assert - should not throw expect(() => render()).not.toThrow() }) it('should handle undefined inputs and outputs', () => { // Arrange const props = createDefaultConfigureButtonProps({ inputs: undefined, outputs: undefined, }) // Act & Assert expect(() => render()).not.toThrow() }) it('should handle empty inputs and outputs arrays', () => { // Arrange const props = createDefaultConfigureButtonProps({ inputs: [], outputs: [], }) // Act & Assert expect(() => render()).not.toThrow() }) it('should call handlePublish when updating workflow tool', async () => { // Arrange const user = userEvent.setup() const handlePublish = vi.fn().mockResolvedValue(undefined) mockSaveWorkflowToolProvider.mockResolvedValue({}) const props = createDefaultConfigureButtonProps({ published: true, handlePublish }) // Act render() await waitFor(() => { expect(screen.getByText('workflow.common.configure')).toBeInTheDocument() }) await user.click(screen.getByText('workflow.common.configure')) // Fill required fields and save await waitFor(() => { expect(screen.getByTestId('drawer')).toBeInTheDocument() }) const saveButton = screen.getByText('common.operation.save') await user.click(saveButton) // Confirm in modal await waitFor(() => { expect(screen.getByText('tools.createTool.confirmTitle')).toBeInTheDocument() }) await user.click(screen.getByText('common.operation.confirm')) // Assert await waitFor(() => { expect(handlePublish).toHaveBeenCalled() }) }) }) // State Management Tests describe('State Management', () => { it('should fetch detail when published and mount', async () => { // Arrange const props = createDefaultConfigureButtonProps({ published: true }) // Act render() // Assert await waitFor(() => { expect(mockFetchWorkflowToolDetailByAppID).toHaveBeenCalledWith('workflow-app-123') }) }) it('should refetch detail when detailNeedUpdate changes to true', async () => { // Arrange const props = createDefaultConfigureButtonProps({ published: true, detailNeedUpdate: false }) // Act const { rerender } = render() await waitFor(() => { expect(mockFetchWorkflowToolDetailByAppID).toHaveBeenCalledTimes(1) }) // Rerender with detailNeedUpdate true rerender() // Assert await waitFor(() => { expect(mockFetchWorkflowToolDetailByAppID).toHaveBeenCalledTimes(2) }) }) it('should toggle modal visibility', async () => { // Arrange const user = userEvent.setup() const props = createDefaultConfigureButtonProps() // Act render() // Click to open modal const triggerArea = screen.getByText('workflow.common.workflowAsTool').closest('.flex') await user.click(triggerArea!) // Assert await waitFor(() => { expect(screen.getByTestId('drawer')).toBeInTheDocument() }) }) it('should not open modal when disabled', async () => { // Arrange const user = userEvent.setup() const props = createDefaultConfigureButtonProps({ disabled: true }) // Act render() const triggerArea = screen.getByText('workflow.common.workflowAsTool').closest('.flex') await user.click(triggerArea!) // Assert expect(screen.queryByTestId('drawer')).not.toBeInTheDocument() }) it('should not open modal when published (use configure button instead)', async () => { // Arrange const user = userEvent.setup() const props = createDefaultConfigureButtonProps({ published: true }) // Act render() await waitFor(() => { expect(screen.getByText('workflow.common.configure')).toBeInTheDocument() }) // Click the main area (should not open modal) const mainArea = screen.getByText('workflow.common.workflowAsTool').closest('.flex') await user.click(mainArea!) // Should not open modal from main click expect(screen.queryByTestId('drawer')).not.toBeInTheDocument() // Click configure button await user.click(screen.getByText('workflow.common.configure')) // Assert await waitFor(() => { expect(screen.getByTestId('drawer')).toBeInTheDocument() }) }) }) // Memoization Tests describe('Memoization - outdated detection', () => { it('should detect outdated when parameter count differs', async () => { // Arrange const detail = createMockWorkflowToolDetail() mockFetchWorkflowToolDetailByAppID.mockResolvedValue(detail) const props = createDefaultConfigureButtonProps({ published: true, inputs: [ createMockInputVar({ variable: 'test_var' }), createMockInputVar({ variable: 'extra_var' }), ], }) // Act render() // Assert - should show outdated warning await waitFor(() => { expect(screen.getByText('workflow.common.workflowAsToolTip')).toBeInTheDocument() }) }) it('should detect outdated when parameter not found', async () => { // Arrange const detail = createMockWorkflowToolDetail() mockFetchWorkflowToolDetailByAppID.mockResolvedValue(detail) const props = createDefaultConfigureButtonProps({ published: true, inputs: [createMockInputVar({ variable: 'different_var' })], }) // Act render() // Assert await waitFor(() => { expect(screen.getByText('workflow.common.workflowAsToolTip')).toBeInTheDocument() }) }) it('should detect outdated when required property differs', async () => { // Arrange const detail = createMockWorkflowToolDetail() mockFetchWorkflowToolDetailByAppID.mockResolvedValue(detail) const props = createDefaultConfigureButtonProps({ published: true, inputs: [createMockInputVar({ variable: 'test_var', required: false })], // Detail has required: true }) // Act render() // Assert await waitFor(() => { expect(screen.getByText('workflow.common.workflowAsToolTip')).toBeInTheDocument() }) }) it('should not show outdated when parameters match', async () => { // Arrange const detail = createMockWorkflowToolDetail() mockFetchWorkflowToolDetailByAppID.mockResolvedValue(detail) const props = createDefaultConfigureButtonProps({ published: true, inputs: [createMockInputVar({ variable: 'test_var', required: true, type: InputVarType.textInput })], }) // Act render() // Assert await waitFor(() => { expect(screen.getByText('workflow.common.configure')).toBeInTheDocument() }) expect(screen.queryByText('workflow.common.workflowAsToolTip')).not.toBeInTheDocument() }) }) // User Interactions Tests describe('User Interactions', () => { it('should navigate to tools page when manage button clicked', async () => { // Arrange const user = userEvent.setup() const props = createDefaultConfigureButtonProps({ published: true }) // Act render() await waitFor(() => { expect(screen.getByText('workflow.common.manageInTools')).toBeInTheDocument() }) await user.click(screen.getByText('workflow.common.manageInTools')) // Assert expect(mockPush).toHaveBeenCalledWith('/tools?category=workflow') }) it('should create workflow tool provider on first publish', async () => { // Arrange const user = userEvent.setup() mockCreateWorkflowToolProvider.mockResolvedValue({}) const props = createDefaultConfigureButtonProps() // Act render() // Open modal const triggerArea = screen.getByText('workflow.common.workflowAsTool').closest('.flex') await user.click(triggerArea!) await waitFor(() => { expect(screen.getByTestId('drawer')).toBeInTheDocument() }) // Fill in required name field const nameInput = screen.getByPlaceholderText('tools.createTool.nameForToolCallPlaceHolder') await user.type(nameInput, 'my_tool') // Click save await user.click(screen.getByText('common.operation.save')) // Assert await waitFor(() => { expect(mockCreateWorkflowToolProvider).toHaveBeenCalled() }) }) it('should show success toast after creating workflow tool', async () => { // Arrange const user = userEvent.setup() mockCreateWorkflowToolProvider.mockResolvedValue({}) const props = createDefaultConfigureButtonProps() // Act render() const triggerArea = screen.getByText('workflow.common.workflowAsTool').closest('.flex') await user.click(triggerArea!) await waitFor(() => { expect(screen.getByTestId('drawer')).toBeInTheDocument() }) const nameInput = screen.getByPlaceholderText('tools.createTool.nameForToolCallPlaceHolder') await user.type(nameInput, 'my_tool') await user.click(screen.getByText('common.operation.save')) // Assert await waitFor(() => { expect(mockToastNotify).toHaveBeenCalledWith({ type: 'success', message: 'common.api.actionSuccess', }) }) }) it('should show error toast when create fails', async () => { // Arrange const user = userEvent.setup() mockCreateWorkflowToolProvider.mockRejectedValue(new Error('Create failed')) const props = createDefaultConfigureButtonProps() // Act render() const triggerArea = screen.getByText('workflow.common.workflowAsTool').closest('.flex') await user.click(triggerArea!) await waitFor(() => { expect(screen.getByTestId('drawer')).toBeInTheDocument() }) const nameInput = screen.getByPlaceholderText('tools.createTool.nameForToolCallPlaceHolder') await user.type(nameInput, 'my_tool') await user.click(screen.getByText('common.operation.save')) // Assert await waitFor(() => { expect(mockToastNotify).toHaveBeenCalledWith({ type: 'error', message: 'Create failed', }) }) }) it('should call onRefreshData after successful create', async () => { // Arrange const user = userEvent.setup() const onRefreshData = vi.fn() mockCreateWorkflowToolProvider.mockResolvedValue({}) const props = createDefaultConfigureButtonProps({ onRefreshData }) // Act render() const triggerArea = screen.getByText('workflow.common.workflowAsTool').closest('.flex') await user.click(triggerArea!) await waitFor(() => { expect(screen.getByTestId('drawer')).toBeInTheDocument() }) const nameInput = screen.getByPlaceholderText('tools.createTool.nameForToolCallPlaceHolder') await user.type(nameInput, 'my_tool') await user.click(screen.getByText('common.operation.save')) // Assert await waitFor(() => { expect(onRefreshData).toHaveBeenCalled() }) }) it('should invalidate all workflow tools after successful create', async () => { // Arrange const user = userEvent.setup() mockCreateWorkflowToolProvider.mockResolvedValue({}) const props = createDefaultConfigureButtonProps() // Act render() const triggerArea = screen.getByText('workflow.common.workflowAsTool').closest('.flex') await user.click(triggerArea!) await waitFor(() => { expect(screen.getByTestId('drawer')).toBeInTheDocument() }) const nameInput = screen.getByPlaceholderText('tools.createTool.nameForToolCallPlaceHolder') await user.type(nameInput, 'my_tool') await user.click(screen.getByText('common.operation.save')) // Assert await waitFor(() => { expect(mockInvalidateAllWorkflowTools).toHaveBeenCalled() }) }) }) // Edge Cases (REQUIRED) describe('Edge Cases', () => { it('should handle API returning undefined', async () => { // Arrange - API returns undefined (simulating empty response or handled error) mockFetchWorkflowToolDetailByAppID.mockResolvedValue(undefined) const props = createDefaultConfigureButtonProps({ published: true }) // Act render() // Assert - should not crash and wait for API call await waitFor(() => { expect(mockFetchWorkflowToolDetailByAppID).toHaveBeenCalled() }) // Component should still render without crashing expect(screen.getByText('workflow.common.workflowAsTool')).toBeInTheDocument() }) it('should handle rapid publish/unpublish state changes', async () => { // Arrange const props = createDefaultConfigureButtonProps({ published: false }) // Act const { rerender } = render() // Toggle published state rapidly await act(async () => { rerender() }) await act(async () => { rerender() }) await act(async () => { rerender() }) // Assert - should not crash expect(mockFetchWorkflowToolDetailByAppID).toHaveBeenCalled() }) it('should handle detail with empty parameters', async () => { // Arrange const detail = createMockWorkflowToolDetail() detail.tool.parameters = [] mockFetchWorkflowToolDetailByAppID.mockResolvedValue(detail) const props = createDefaultConfigureButtonProps({ published: true, inputs: [] }) // Act render() // Assert await waitFor(() => { expect(screen.getByText('workflow.common.configure')).toBeInTheDocument() }) }) it('should handle detail with undefined output_schema', async () => { // Arrange const detail = createMockWorkflowToolDetail() // @ts-expect-error - testing undefined case detail.tool.output_schema = undefined mockFetchWorkflowToolDetailByAppID.mockResolvedValue(detail) const props = createDefaultConfigureButtonProps({ published: true }) // Act & Assert expect(() => render()).not.toThrow() }) it('should handle paragraph type input conversion', async () => { // Arrange const user = userEvent.setup() const props = createDefaultConfigureButtonProps({ inputs: [createMockInputVar({ variable: 'test_var', type: InputVarType.paragraph })], }) // Act render() const triggerArea = screen.getByText('workflow.common.workflowAsTool').closest('.flex') await user.click(triggerArea!) // Assert - should render without error await waitFor(() => { expect(screen.getByTestId('drawer')).toBeInTheDocument() }) }) }) // Accessibility Tests describe('Accessibility', () => { it('should have accessible buttons when published', async () => { // Arrange const props = createDefaultConfigureButtonProps({ published: true }) // Act render() // Assert await waitFor(() => { const buttons = screen.getAllByRole('button') expect(buttons.length).toBeGreaterThan(0) }) }) it('should disable configure button when not workspace manager', async () => { // Arrange mockIsCurrentWorkspaceManager.mockReturnValue(false) const props = createDefaultConfigureButtonProps({ published: true }) // Act render() // Assert await waitFor(() => { const configureButton = screen.getByText('workflow.common.configure') expect(configureButton).toBeDisabled() }) }) }) }) // ============================================================================ // WorkflowToolAsModal Tests // ============================================================================ describe('WorkflowToolAsModal', () => { beforeEach(() => { vi.clearAllMocks() mockPortalOpenState = false }) // Rendering Tests (REQUIRED) describe('Rendering', () => { it('should render drawer with correct title', () => { // Arrange const props = { isAdd: true, payload: createDefaultModalPayload(), onHide: vi.fn(), } // Act render() // Assert expect(screen.getByTestId('drawer-title')).toHaveTextContent('workflow.common.workflowAsTool') }) it('should render name input field', () => { // Arrange const props = { isAdd: true, payload: createDefaultModalPayload(), onHide: vi.fn(), } // Act render() // Assert expect(screen.getByPlaceholderText('tools.createTool.toolNamePlaceHolder')).toBeInTheDocument() }) it('should render name for tool call input', () => { // Arrange const props = { isAdd: true, payload: createDefaultModalPayload(), onHide: vi.fn(), } // Act render() // Assert expect(screen.getByPlaceholderText('tools.createTool.nameForToolCallPlaceHolder')).toBeInTheDocument() }) it('should render description textarea', () => { // Arrange const props = { isAdd: true, payload: createDefaultModalPayload(), onHide: vi.fn(), } // Act render() // Assert expect(screen.getByPlaceholderText('tools.createTool.descriptionPlaceholder')).toBeInTheDocument() }) it('should render tool input table', () => { // Arrange const props = { isAdd: true, payload: createDefaultModalPayload(), onHide: vi.fn(), } // Act render() // Assert expect(screen.getByText('tools.createTool.toolInput.title')).toBeInTheDocument() }) it('should render tool output table', () => { // Arrange const props = { isAdd: true, payload: createDefaultModalPayload(), onHide: vi.fn(), } // Act render() // Assert expect(screen.getByText('tools.createTool.toolOutput.title')).toBeInTheDocument() }) it('should render reserved output parameters', () => { // Arrange const props = { isAdd: true, payload: createDefaultModalPayload(), onHide: vi.fn(), } // Act render() // Assert expect(screen.getByText('text')).toBeInTheDocument() expect(screen.getByText('files')).toBeInTheDocument() expect(screen.getByText('json')).toBeInTheDocument() }) it('should render label selector', () => { // Arrange const props = { isAdd: true, payload: createDefaultModalPayload(), onHide: vi.fn(), } // Act render() // Assert expect(screen.getByTestId('label-selector')).toBeInTheDocument() }) it('should render privacy policy input', () => { // Arrange const props = { isAdd: true, payload: createDefaultModalPayload(), onHide: vi.fn(), } // Act render() // Assert expect(screen.getByPlaceholderText('tools.createTool.privacyPolicyPlaceholder')).toBeInTheDocument() }) it('should render delete button when editing and onRemove provided', () => { // Arrange const props = { isAdd: false, payload: createDefaultModalPayload({ workflow_tool_id: 'tool-123' }), onHide: vi.fn(), onRemove: vi.fn(), } // Act render() // Assert expect(screen.getByText('common.operation.delete')).toBeInTheDocument() }) it('should not render delete button when adding', () => { // Arrange const props = { isAdd: true, payload: createDefaultModalPayload(), onHide: vi.fn(), onRemove: vi.fn(), } // Act render() // Assert expect(screen.queryByText('common.operation.delete')).not.toBeInTheDocument() }) }) // Props Testing (REQUIRED) describe('Props', () => { it('should initialize state from payload', () => { // Arrange const payload = createDefaultModalPayload({ label: 'Custom Label', name: 'custom_name', description: 'Custom description', }) const props = { isAdd: true, payload, onHide: vi.fn(), } // Act render() // Assert expect(screen.getByDisplayValue('Custom Label')).toBeInTheDocument() expect(screen.getByDisplayValue('custom_name')).toBeInTheDocument() expect(screen.getByDisplayValue('Custom description')).toBeInTheDocument() }) it('should pass labels to label selector', () => { // Arrange const payload = createDefaultModalPayload({ labels: ['tag1', 'tag2'] }) const props = { isAdd: true, payload, onHide: vi.fn(), } // Act render() // Assert expect(screen.getByTestId('label-values')).toHaveTextContent('tag1,tag2') }) }) // State Management Tests describe('State Management', () => { it('should update label state on input change', async () => { // Arrange const user = userEvent.setup() const props = { isAdd: true, payload: createDefaultModalPayload({ label: '' }), onHide: vi.fn(), } // Act render() const labelInput = screen.getByPlaceholderText('tools.createTool.toolNamePlaceHolder') await user.type(labelInput, 'New Label') // Assert expect(labelInput).toHaveValue('New Label') }) it('should update name state on input change', async () => { // Arrange const user = userEvent.setup() const props = { isAdd: true, payload: createDefaultModalPayload({ name: '' }), onHide: vi.fn(), } // Act render() const nameInput = screen.getByPlaceholderText('tools.createTool.nameForToolCallPlaceHolder') await user.type(nameInput, 'new_name') // Assert expect(nameInput).toHaveValue('new_name') }) it('should update description state on textarea change', async () => { // Arrange const user = userEvent.setup() const props = { isAdd: true, payload: createDefaultModalPayload({ description: '' }), onHide: vi.fn(), } // Act render() const descInput = screen.getByPlaceholderText('tools.createTool.descriptionPlaceholder') await user.type(descInput, 'New description') // Assert expect(descInput).toHaveValue('New description') }) it('should show emoji picker on icon click', async () => { // Arrange const user = userEvent.setup() const props = { isAdd: true, payload: createDefaultModalPayload(), onHide: vi.fn(), } // Act render() const iconButton = screen.getByTestId('app-icon') await user.click(iconButton) // Assert expect(screen.getByTestId('emoji-picker')).toBeInTheDocument() }) it('should update emoji on selection', async () => { // Arrange const user = userEvent.setup() const props = { isAdd: true, payload: createDefaultModalPayload(), onHide: vi.fn(), } // Act render() // Open emoji picker const iconButton = screen.getByTestId('app-icon') await user.click(iconButton) // Select emoji await user.click(screen.getByTestId('select-emoji')) // Assert const updatedIcon = screen.getByTestId('app-icon') expect(updatedIcon).toHaveAttribute('data-icon', 'πŸš€') expect(updatedIcon).toHaveAttribute('data-background', '#f0f0f0') }) it('should close emoji picker on close button', async () => { // Arrange const user = userEvent.setup() const props = { isAdd: true, payload: createDefaultModalPayload(), onHide: vi.fn(), } // Act render() const iconButton = screen.getByTestId('app-icon') await user.click(iconButton) expect(screen.getByTestId('emoji-picker')).toBeInTheDocument() await user.click(screen.getByTestId('close-emoji-picker')) // Assert expect(screen.queryByTestId('emoji-picker')).not.toBeInTheDocument() }) it('should update labels when label selector changes', async () => { // Arrange const user = userEvent.setup() const props = { isAdd: true, payload: createDefaultModalPayload({ labels: ['initial'] }), onHide: vi.fn(), } // Act render() await user.click(screen.getByTestId('add-label')) // Assert expect(screen.getByTestId('label-values')).toHaveTextContent('initial,new-label') }) it('should update privacy policy on input change', async () => { // Arrange const user = userEvent.setup() const props = { isAdd: true, payload: createDefaultModalPayload({ privacy_policy: '' }), onHide: vi.fn(), } // Act render() const privacyInput = screen.getByPlaceholderText('tools.createTool.privacyPolicyPlaceholder') await user.type(privacyInput, 'https://example.com/privacy') // Assert expect(privacyInput).toHaveValue('https://example.com/privacy') }) }) // User Interactions Tests describe('User Interactions', () => { it('should call onHide when cancel button clicked', async () => { // Arrange const user = userEvent.setup() const onHide = vi.fn() const props = { isAdd: true, payload: createDefaultModalPayload(), onHide, } // Act render() await user.click(screen.getByText('common.operation.cancel')) // Assert expect(onHide).toHaveBeenCalledTimes(1) }) it('should call onHide when drawer close button clicked', async () => { // Arrange const user = userEvent.setup() const onHide = vi.fn() const props = { isAdd: true, payload: createDefaultModalPayload(), onHide, } // Act render() await user.click(screen.getByTestId('drawer-close')) // Assert expect(onHide).toHaveBeenCalledTimes(1) }) it('should call onRemove when delete button clicked', async () => { // Arrange const user = userEvent.setup() const onRemove = vi.fn() const props = { isAdd: false, payload: createDefaultModalPayload({ workflow_tool_id: 'tool-123' }), onHide: vi.fn(), onRemove, } // Act render() await user.click(screen.getByText('common.operation.delete')) // Assert expect(onRemove).toHaveBeenCalledTimes(1) }) it('should call onCreate when save clicked in add mode', async () => { // Arrange const user = userEvent.setup() const onCreate = vi.fn() const props = { isAdd: true, payload: createDefaultModalPayload(), onHide: vi.fn(), onCreate, } // Act render() await user.click(screen.getByText('common.operation.save')) // Assert expect(onCreate).toHaveBeenCalledWith(expect.objectContaining({ name: 'test_tool', workflow_app_id: 'workflow-app-123', })) }) it('should show confirm modal when save clicked in edit mode', async () => { // Arrange const user = userEvent.setup() const props = { isAdd: false, payload: createDefaultModalPayload({ workflow_tool_id: 'tool-123' }), onHide: vi.fn(), onSave: vi.fn(), } // Act render() await user.click(screen.getByText('common.operation.save')) // Assert expect(screen.getByText('tools.createTool.confirmTitle')).toBeInTheDocument() }) it('should call onSave after confirm in edit mode', async () => { // Arrange const user = userEvent.setup() const onSave = vi.fn() const props = { isAdd: false, payload: createDefaultModalPayload({ workflow_tool_id: 'tool-123' }), onHide: vi.fn(), onSave, } // Act render() await user.click(screen.getByText('common.operation.save')) await user.click(screen.getByText('common.operation.confirm')) // Assert expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ workflow_tool_id: 'tool-123', })) }) it('should update parameter description on input', async () => { // Arrange const user = userEvent.setup() const props = { isAdd: true, payload: createDefaultModalPayload({ parameters: [{ name: 'param1', description: '', // Start with empty description form: 'llm', required: true, type: 'string', }], }), onHide: vi.fn(), } // Act render() const descInput = screen.getByPlaceholderText('tools.createTool.toolInput.descriptionPlaceholder') await user.type(descInput, 'New parameter description') // Assert expect(descInput).toHaveValue('New parameter description') }) }) // Validation Tests describe('Validation', () => { it('should show error when label is empty', async () => { // Arrange const user = userEvent.setup() const props = { isAdd: true, payload: createDefaultModalPayload({ label: '' }), onHide: vi.fn(), onCreate: vi.fn(), } // Act render() await user.click(screen.getByText('common.operation.save')) // Assert expect(mockToastNotify).toHaveBeenCalledWith({ type: 'error', message: expect.any(String), }) }) it('should show error when name is empty', async () => { // Arrange const user = userEvent.setup() const props = { isAdd: true, payload: createDefaultModalPayload({ label: 'Test', name: '' }), onHide: vi.fn(), onCreate: vi.fn(), } // Act render() await user.click(screen.getByText('common.operation.save')) // Assert expect(mockToastNotify).toHaveBeenCalledWith({ type: 'error', message: expect.any(String), }) }) it('should show validation error for invalid name format', async () => { // Arrange const user = userEvent.setup() const props = { isAdd: true, payload: createDefaultModalPayload({ name: '' }), onHide: vi.fn(), } // Act render() const nameInput = screen.getByPlaceholderText('tools.createTool.nameForToolCallPlaceHolder') await user.type(nameInput, 'invalid name with spaces') // Assert expect(screen.getByText('tools.createTool.nameForToolCallTip')).toBeInTheDocument() }) it('should accept valid name format', async () => { // Arrange const user = userEvent.setup() const props = { isAdd: true, payload: createDefaultModalPayload({ name: '' }), onHide: vi.fn(), } // Act render() const nameInput = screen.getByPlaceholderText('tools.createTool.nameForToolCallPlaceHolder') await user.type(nameInput, 'valid_name_123') // Assert expect(screen.queryByText('tools.createTool.nameForToolCallTip')).not.toBeInTheDocument() }) }) // Edge Cases (REQUIRED) describe('Edge Cases', () => { it('should handle empty parameters array', () => { // Arrange const props = { isAdd: true, payload: createDefaultModalPayload({ parameters: [] }), onHide: vi.fn(), } // Act & Assert expect(() => render()).not.toThrow() }) it('should handle empty output parameters', () => { // Arrange const props = { isAdd: true, payload: createDefaultModalPayload({ outputParameters: [] }), onHide: vi.fn(), } // Act & Assert expect(() => render()).not.toThrow() }) it('should handle parameter with __image name specially', () => { // Arrange const props = { isAdd: true, payload: createDefaultModalPayload({ parameters: [{ name: '__image', description: 'Image parameter', form: 'llm', required: true, type: 'file', }], }), onHide: vi.fn(), } // Act render() // Assert - __image should show method as text, not selector expect(screen.getByText('tools.createTool.toolInput.methodParameter')).toBeInTheDocument() }) it('should show warning for reserved output parameter name collision', () => { // Arrange const props = { isAdd: true, payload: createDefaultModalPayload({ outputParameters: [{ name: 'text', // Collides with reserved description: 'Custom text output', type: VarType.string, }], }), onHide: vi.fn(), } // Act render() // Assert - should show both reserved and custom with warning icon const textElements = screen.getAllByText('text') expect(textElements.length).toBe(2) }) it('should handle undefined onSave gracefully', async () => { // Arrange const user = userEvent.setup() const props = { isAdd: false, payload: createDefaultModalPayload({ workflow_tool_id: 'tool-123' }), onHide: vi.fn(), // onSave is undefined } // Act render() await user.click(screen.getByText('common.operation.save')) // Show confirm modal await waitFor(() => { expect(screen.getByText('tools.createTool.confirmTitle')).toBeInTheDocument() }) // Assert - should not crash await user.click(screen.getByText('common.operation.confirm')) }) it('should handle undefined onCreate gracefully', async () => { // Arrange const user = userEvent.setup() const props = { isAdd: true, payload: createDefaultModalPayload(), onHide: vi.fn(), // onCreate is undefined } // Act & Assert - should not crash render() await user.click(screen.getByText('common.operation.save')) }) it('should close confirm modal on close button', async () => { // Arrange const user = userEvent.setup() const props = { isAdd: false, payload: createDefaultModalPayload({ workflow_tool_id: 'tool-123' }), onHide: vi.fn(), onSave: vi.fn(), } // Act render() await user.click(screen.getByText('common.operation.save')) await waitFor(() => { expect(screen.getByText('tools.createTool.confirmTitle')).toBeInTheDocument() }) // Click cancel in confirm modal const cancelButtons = screen.getAllByText('common.operation.cancel') await user.click(cancelButtons[cancelButtons.length - 1]) // Assert await waitFor(() => { expect(screen.queryByText('tools.createTool.confirmTitle')).not.toBeInTheDocument() }) }) }) }) // ============================================================================ // MethodSelector Tests // ============================================================================ describe('MethodSelector', () => { beforeEach(() => { vi.clearAllMocks() mockPortalOpenState = false }) // Rendering Tests (REQUIRED) describe('Rendering', () => { it('should render without crashing', () => { // Arrange const props = { value: 'llm', onChange: vi.fn(), } // Act render() // Assert expect(screen.getByTestId('portal-trigger')).toBeInTheDocument() }) it('should display parameter method text when value is llm', () => { // Arrange const props = { value: 'llm', onChange: vi.fn(), } // Act render() // Assert expect(screen.getByText('tools.createTool.toolInput.methodParameter')).toBeInTheDocument() }) it('should display setting method text when value is form', () => { // Arrange const props = { value: 'form', onChange: vi.fn(), } // Act render() // Assert expect(screen.getByText('tools.createTool.toolInput.methodSetting')).toBeInTheDocument() }) it('should display setting method text when value is undefined', () => { // Arrange const props = { value: undefined, onChange: vi.fn(), } // Act render() // Assert expect(screen.getByText('tools.createTool.toolInput.methodSetting')).toBeInTheDocument() }) }) // User Interactions Tests describe('User Interactions', () => { it('should open dropdown on trigger click', async () => { // Arrange const user = userEvent.setup() const props = { value: 'llm', onChange: vi.fn(), } // Act render() await user.click(screen.getByTestId('portal-trigger')) // Assert expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) it('should call onChange with llm when parameter option clicked', async () => { // Arrange const user = userEvent.setup() const onChange = vi.fn() const props = { value: 'form', onChange, } // Act render() await user.click(screen.getByTestId('portal-trigger')) const paramOption = screen.getAllByText('tools.createTool.toolInput.methodParameter')[0] await user.click(paramOption) // Assert expect(onChange).toHaveBeenCalledWith('llm') }) it('should call onChange with form when setting option clicked', async () => { // Arrange const user = userEvent.setup() const onChange = vi.fn() const props = { value: 'llm', onChange, } // Act render() await user.click(screen.getByTestId('portal-trigger')) const settingOption = screen.getByText('tools.createTool.toolInput.methodSetting') await user.click(settingOption) // Assert expect(onChange).toHaveBeenCalledWith('form') }) it('should toggle dropdown state on multiple clicks', async () => { // Arrange const user = userEvent.setup() const props = { value: 'llm', onChange: vi.fn(), } // Act render() // First click - open await user.click(screen.getByTestId('portal-trigger')) expect(screen.getByTestId('portal-content')).toBeInTheDocument() // Second click - close await user.click(screen.getByTestId('portal-trigger')) expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument() }) }) // Props Tests (REQUIRED) describe('Props', () => { it('should show check icon for selected llm value', async () => { // Arrange const user = userEvent.setup() const props = { value: 'llm', onChange: vi.fn(), } // Act render() await user.click(screen.getByTestId('portal-trigger')) // Assert - the first option (llm) should have a check icon container const content = screen.getByTestId('portal-content') expect(content).toBeInTheDocument() }) it('should show check icon for selected form value', async () => { // Arrange const user = userEvent.setup() const props = { value: 'form', onChange: vi.fn(), } // Act render() await user.click(screen.getByTestId('portal-trigger')) // Assert const content = screen.getByTestId('portal-content') expect(content).toBeInTheDocument() }) }) // Edge Cases (REQUIRED) describe('Edge Cases', () => { it('should handle rapid value changes', async () => { // Arrange const onChange = vi.fn() const props = { value: 'llm', onChange, } // Act const { rerender } = render() rerender() rerender() rerender() // Assert - should not crash expect(screen.getByText('tools.createTool.toolInput.methodSetting')).toBeInTheDocument() }) it('should handle empty string value', () => { // Arrange const props = { value: '', onChange: vi.fn(), } // Act & Assert expect(() => render()).not.toThrow() }) }) }) // ============================================================================ // Integration Tests // ============================================================================ describe('Integration Tests', () => { beforeEach(() => { vi.clearAllMocks() mockPortalOpenState = false mockIsCurrentWorkspaceManager.mockReturnValue(true) mockFetchWorkflowToolDetailByAppID.mockResolvedValue(createMockWorkflowToolDetail()) }) // Complete workflow: open modal -> fill form -> save describe('Complete Workflow', () => { it('should complete full create workflow', async () => { // Arrange const user = userEvent.setup() mockCreateWorkflowToolProvider.mockResolvedValue({}) const onRefreshData = vi.fn() const props = createDefaultConfigureButtonProps({ onRefreshData }) // Act render() // Open modal const triggerArea = screen.getByText('workflow.common.workflowAsTool').closest('.flex') await user.click(triggerArea!) await waitFor(() => { expect(screen.getByTestId('drawer')).toBeInTheDocument() }) // Fill form const labelInput = screen.getByPlaceholderText('tools.createTool.toolNamePlaceHolder') await user.clear(labelInput) await user.type(labelInput, 'My Custom Tool') const nameInput = screen.getByPlaceholderText('tools.createTool.nameForToolCallPlaceHolder') await user.type(nameInput, 'my_custom_tool') const descInput = screen.getByPlaceholderText('tools.createTool.descriptionPlaceholder') await user.clear(descInput) await user.type(descInput, 'A custom tool for testing') // Save await user.click(screen.getByText('common.operation.save')) // Assert await waitFor(() => { expect(mockCreateWorkflowToolProvider).toHaveBeenCalledWith( expect.objectContaining({ name: 'my_custom_tool', label: 'My Custom Tool', description: 'A custom tool for testing', }), ) }) await waitFor(() => { expect(onRefreshData).toHaveBeenCalled() }) }) it('should complete full update workflow', async () => { // Arrange const user = userEvent.setup() const handlePublish = vi.fn().mockResolvedValue(undefined) mockSaveWorkflowToolProvider.mockResolvedValue({}) const props = createDefaultConfigureButtonProps({ published: true, handlePublish, }) // Act render() // Wait for detail to load await waitFor(() => { expect(screen.getByText('workflow.common.configure')).toBeInTheDocument() }) // Open modal await user.click(screen.getByText('workflow.common.configure')) await waitFor(() => { expect(screen.getByTestId('drawer')).toBeInTheDocument() }) // Modify description const descInput = screen.getByPlaceholderText('tools.createTool.descriptionPlaceholder') await user.clear(descInput) await user.type(descInput, 'Updated description') // Save await user.click(screen.getByText('common.operation.save')) // Confirm await waitFor(() => { expect(screen.getByText('tools.createTool.confirmTitle')).toBeInTheDocument() }) await user.click(screen.getByText('common.operation.confirm')) // Assert await waitFor(() => { expect(handlePublish).toHaveBeenCalled() expect(mockSaveWorkflowToolProvider).toHaveBeenCalled() }) }) }) // Test callbacks and state synchronization describe('Callback Stability', () => { it('should maintain callback references across rerenders', async () => { // Arrange const handlePublish = vi.fn().mockResolvedValue(undefined) const onRefreshData = vi.fn() const props = createDefaultConfigureButtonProps({ handlePublish, onRefreshData, }) // Act const { rerender } = render() rerender() rerender() // Assert - component should not crash and callbacks should be stable expect(screen.getByText('workflow.common.workflowAsTool')).toBeInTheDocument() }) }) })