mirror of
https://github.com/langgenius/dify.git
synced 2026-01-19 11:45:05 +08:00
- Refactored and improved test cases for the Icon component, ensuring proper rendering and behavior for various src configurations. - Updated AppPicker tests to utilize React context for managing portal open state, enhancing test reliability and clarity. - Adjusted type definitions in several components to use more specific types, improving type safety and code maintainability.
2710 lines
82 KiB
TypeScript
2710 lines
82 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
import type { Node } from 'reactflow'
|
|
import type { Collection } from '@/app/components/tools/types'
|
|
import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
|
|
import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types'
|
|
import type { NodeOutPutVar, ToolWithProvider } from '@/app/components/workflow/types'
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
import { act, fireEvent, render, renderHook, screen, waitFor } from '@testing-library/react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { CollectionType } from '@/app/components/tools/types'
|
|
import { VarKindType } from '@/app/components/workflow/nodes/_base/types'
|
|
import { Type } from '@/app/components/workflow/nodes/llm/types'
|
|
import {
|
|
SchemaModal,
|
|
ToolAuthorizationSection,
|
|
ToolBaseForm,
|
|
ToolCredentialsForm,
|
|
ToolItem,
|
|
ToolSettingsPanel,
|
|
ToolTrigger,
|
|
} from './components'
|
|
import { usePluginInstalledCheck, useToolSelectorState } from './hooks'
|
|
import ToolSelector from './index'
|
|
|
|
// ==================== Mock Setup ====================
|
|
|
|
// Mock service hooks - use let so we can modify in tests
|
|
// Allow undefined for testing fallback behavior
|
|
let mockBuildInTools: ToolWithProvider[] | undefined = []
|
|
let mockCustomTools: ToolWithProvider[] | undefined = []
|
|
let mockWorkflowTools: ToolWithProvider[] | undefined = []
|
|
let mockMcpTools: ToolWithProvider[] | undefined = []
|
|
|
|
vi.mock('@/service/use-tools', () => ({
|
|
useAllBuiltInTools: () => ({ data: mockBuildInTools }),
|
|
useAllCustomTools: () => ({ data: mockCustomTools }),
|
|
useAllWorkflowTools: () => ({ data: mockWorkflowTools }),
|
|
useAllMCPTools: () => ({ data: mockMcpTools }),
|
|
useInvalidateAllBuiltInTools: () => vi.fn(),
|
|
}))
|
|
|
|
// Track manifest mock state
|
|
let mockManifestData: Record<string, unknown> | null = null
|
|
|
|
vi.mock('@/service/use-plugins', () => ({
|
|
usePluginManifestInfo: () => ({ data: mockManifestData }),
|
|
useInvalidateInstalledPluginList: () => vi.fn(),
|
|
}))
|
|
|
|
// Mock tool credential services
|
|
const mockFetchBuiltInToolCredentialSchema = vi.fn().mockResolvedValue([
|
|
{ name: 'api_key', type: 'string', required: false, label: { en_US: 'API Key' } },
|
|
])
|
|
const mockFetchBuiltInToolCredential = vi.fn().mockResolvedValue({})
|
|
|
|
vi.mock('@/service/tools', () => ({
|
|
fetchBuiltInToolCredentialSchema: (...args: unknown[]) => mockFetchBuiltInToolCredentialSchema(...args),
|
|
fetchBuiltInToolCredential: (...args: unknown[]) => mockFetchBuiltInToolCredential(...args),
|
|
}))
|
|
|
|
// Mock form schema utils - necessary for controlling test data
|
|
vi.mock('@/app/components/tools/utils/to-form-schema', () => ({
|
|
generateFormValue: vi.fn().mockReturnValue({}),
|
|
getPlainValue: vi.fn().mockImplementation(v => v),
|
|
getStructureValue: vi.fn().mockImplementation(v => v),
|
|
toolParametersToFormSchemas: vi.fn().mockReturnValue([]),
|
|
toolCredentialToFormSchemas: vi.fn().mockImplementation(schemas => schemas.map((s: { required?: boolean }) => ({
|
|
...s,
|
|
required: s.required || false,
|
|
}))),
|
|
addDefaultValue: vi.fn().mockImplementation((credential, _schemas) => credential),
|
|
}))
|
|
|
|
// Mock complex child components that need controlled interaction
|
|
vi.mock('@/app/components/workflow/block-selector/tool-picker', () => ({
|
|
default: ({
|
|
onSelect,
|
|
onSelectMultiple,
|
|
trigger,
|
|
}: {
|
|
onSelect: (tool: ToolDefaultValue) => void
|
|
onSelectMultiple?: (tools: ToolDefaultValue[]) => void
|
|
trigger: ReactNode
|
|
}) => {
|
|
const mockToolDefault = {
|
|
provider_id: 'test-provider/tool',
|
|
provider_type: 'builtin',
|
|
provider_name: 'Test Provider',
|
|
tool_name: 'test-tool',
|
|
tool_label: 'Test Tool',
|
|
tool_description: 'A test tool',
|
|
title: 'Test Tool Title',
|
|
is_team_authorization: true,
|
|
params: {},
|
|
paramSchemas: [],
|
|
}
|
|
return (
|
|
<div data-testid="tool-picker">
|
|
{trigger}
|
|
<button
|
|
data-testid="select-tool-btn"
|
|
onClick={() => onSelect(mockToolDefault as ToolDefaultValue)}
|
|
>
|
|
Select Tool
|
|
</button>
|
|
<button
|
|
data-testid="select-multiple-btn"
|
|
onClick={() => onSelectMultiple?.([mockToolDefault as ToolDefaultValue])}
|
|
>
|
|
Select Multiple
|
|
</button>
|
|
</div>
|
|
)
|
|
},
|
|
}))
|
|
|
|
vi.mock('@/app/components/workflow/nodes/tool/components/tool-form', () => ({
|
|
default: ({
|
|
onChange,
|
|
value,
|
|
}: {
|
|
onChange: (v: Record<string, unknown>) => void
|
|
value: Record<string, unknown>
|
|
}) => (
|
|
<div data-testid="tool-form">
|
|
<span data-testid="tool-form-value">{JSON.stringify(value)}</span>
|
|
<button
|
|
data-testid="change-settings-btn"
|
|
onClick={() => onChange({ setting1: 'new-value' })}
|
|
>
|
|
Change Settings
|
|
</button>
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
vi.mock('@/app/components/plugins/plugin-auth', () => ({
|
|
AuthCategory: { tool: 'tool' },
|
|
PluginAuthInAgent: ({
|
|
onAuthorizationItemClick,
|
|
}: {
|
|
onAuthorizationItemClick: (id: string) => void
|
|
}) => (
|
|
<div data-testid="plugin-auth-in-agent">
|
|
<button
|
|
data-testid="auth-item-click-btn"
|
|
onClick={() => onAuthorizationItemClick('credential-123')}
|
|
>
|
|
Select Credential
|
|
</button>
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
// Portal components need mocking for controlled positioning in tests
|
|
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
|
|
PortalToFollowElem: ({
|
|
children,
|
|
open,
|
|
}: {
|
|
children: ReactNode
|
|
open?: boolean
|
|
}) => (
|
|
<div data-testid="portal-to-follow-elem" data-open={open}>
|
|
{children}
|
|
</div>
|
|
),
|
|
PortalToFollowElemTrigger: ({
|
|
children,
|
|
onClick,
|
|
}: {
|
|
children: ReactNode
|
|
onClick?: () => void
|
|
}) => (
|
|
<div data-testid="portal-trigger" onClick={onClick}>
|
|
{children}
|
|
</div>
|
|
),
|
|
PortalToFollowElemContent: ({ children }: { children: ReactNode }) => (
|
|
<div data-testid="portal-content">{children}</div>
|
|
),
|
|
}))
|
|
|
|
vi.mock('../../../readme-panel/entrance', () => ({
|
|
ReadmeEntrance: () => <div data-testid="readme-entrance" />,
|
|
}))
|
|
|
|
vi.mock('./components/reasoning-config-form', () => ({
|
|
default: ({
|
|
onChange,
|
|
value,
|
|
}: {
|
|
onChange: (v: Record<string, unknown>) => void
|
|
value: Record<string, unknown>
|
|
}) => (
|
|
<div data-testid="reasoning-config-form">
|
|
<span data-testid="params-value">{JSON.stringify(value)}</span>
|
|
<button
|
|
data-testid="change-params-btn"
|
|
onClick={() => onChange({ param1: 'new-param' })}
|
|
>
|
|
Change Params
|
|
</button>
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
// Track MCP availability mock state
|
|
let mockMCPToolAllowed = true
|
|
|
|
vi.mock('@/app/components/workflow/nodes/_base/components/mcp-tool-availability', () => ({
|
|
useMCPToolAvailability: () => ({ allowed: mockMCPToolAllowed }),
|
|
}))
|
|
|
|
vi.mock('@/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip', () => ({
|
|
default: () => <div data-testid="mcp-not-support-tooltip" />,
|
|
}))
|
|
|
|
vi.mock('@/app/components/workflow/nodes/_base/components/install-plugin-button', () => ({
|
|
InstallPluginButton: ({
|
|
onSuccess,
|
|
onClick,
|
|
}: {
|
|
onSuccess?: () => void
|
|
onClick?: (e: React.MouseEvent) => void
|
|
}) => (
|
|
<button
|
|
data-testid="install-plugin-btn"
|
|
onClick={(e) => {
|
|
onClick?.(e)
|
|
onSuccess?.()
|
|
}}
|
|
>
|
|
Install
|
|
</button>
|
|
),
|
|
}))
|
|
|
|
vi.mock('@/app/components/workflow/nodes/_base/components/switch-plugin-version', () => ({
|
|
SwitchPluginVersion: ({
|
|
onChange,
|
|
}: {
|
|
onChange?: () => void
|
|
}) => (
|
|
<button data-testid="switch-version-btn" onClick={onChange}>
|
|
Switch Version
|
|
</button>
|
|
),
|
|
}))
|
|
|
|
vi.mock('@/app/components/workflow/block-icon', () => ({
|
|
default: () => <div data-testid="block-icon" />,
|
|
}))
|
|
|
|
// Mock Modal - headlessui Dialog has complex behavior
|
|
vi.mock('@/app/components/base/modal', () => ({
|
|
default: ({ children, isShow }: { children: ReactNode, isShow: boolean }) => (
|
|
isShow ? <div data-testid="modal">{children}</div> : null
|
|
),
|
|
}))
|
|
|
|
// Mock VisualEditor - complex component with many dependencies
|
|
vi.mock('@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor', () => ({
|
|
default: () => <div data-testid="visual-editor" />,
|
|
}))
|
|
|
|
vi.mock('@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context', () => ({
|
|
MittProvider: ({ children }: { children: ReactNode }) => <>{children}</>,
|
|
VisualEditorContextProvider: ({ children }: { children: ReactNode }) => <>{children}</>,
|
|
}))
|
|
|
|
// Mock Form - complex model provider form
|
|
vi.mock('@/app/components/header/account-setting/model-provider-page/model-modal/Form', () => ({
|
|
default: ({
|
|
onChange,
|
|
value,
|
|
fieldMoreInfo,
|
|
}: {
|
|
onChange: (v: Record<string, unknown>) => void
|
|
value: Record<string, unknown>
|
|
fieldMoreInfo?: (item: { url?: string | null }) => ReactNode
|
|
}) => (
|
|
<div data-testid="credential-form">
|
|
<input
|
|
data-testid="form-input"
|
|
value={JSON.stringify(value)}
|
|
onChange={e => onChange(JSON.parse(e.target.value || '{}'))}
|
|
/>
|
|
{fieldMoreInfo && (
|
|
<div data-testid="field-more-info">
|
|
{fieldMoreInfo({ url: 'https://example.com' })}
|
|
{fieldMoreInfo({ url: null })}
|
|
</div>
|
|
)}
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
// Mock Toast - need to track notify calls for assertions
|
|
const mockToastNotify = vi.fn()
|
|
vi.mock('@/app/components/base/toast', () => ({
|
|
default: { notify: (...args: unknown[]) => mockToastNotify(...args) },
|
|
}))
|
|
|
|
// ==================== Test Utilities ====================
|
|
|
|
const createTestQueryClient = () =>
|
|
new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
retry: false,
|
|
gcTime: 0,
|
|
},
|
|
},
|
|
})
|
|
|
|
const createWrapper = () => {
|
|
const testQueryClient = createTestQueryClient()
|
|
return ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={testQueryClient}>
|
|
{children}
|
|
</QueryClientProvider>
|
|
)
|
|
}
|
|
|
|
// Factory functions for test data
|
|
const createToolValue = (overrides: Partial<ToolValue> = {}): ToolValue => ({
|
|
provider_name: 'test-provider/tool',
|
|
provider_show_name: 'Test Provider',
|
|
tool_name: 'test-tool',
|
|
tool_label: 'Test Tool',
|
|
tool_description: 'A test tool',
|
|
settings: {},
|
|
parameters: {},
|
|
enabled: true,
|
|
extra: { description: 'Test description' },
|
|
...overrides,
|
|
})
|
|
|
|
const createToolDefaultValue = (overrides: Partial<ToolDefaultValue> = {}): ToolDefaultValue => ({
|
|
provider_id: 'test-provider/tool',
|
|
provider_type: CollectionType.builtIn,
|
|
provider_name: 'Test Provider',
|
|
tool_name: 'test-tool',
|
|
tool_label: 'Test Tool',
|
|
tool_description: 'A test tool',
|
|
title: 'Test Tool Title',
|
|
is_team_authorization: true,
|
|
params: {},
|
|
paramSchemas: [],
|
|
...overrides,
|
|
} as ToolDefaultValue)
|
|
|
|
// Helper to create mock ToolFormSchema for testing
|
|
const createMockFormSchema = (name: string) => ({
|
|
name,
|
|
variable: name,
|
|
label: { en_US: name, zh_Hans: name },
|
|
type: 'text-input',
|
|
_type: 'string',
|
|
form: 'llm',
|
|
required: false,
|
|
show_on: [],
|
|
})
|
|
|
|
const createToolWithProvider = (overrides: Record<string, unknown> = {}): ToolWithProvider => ({
|
|
id: 'test-provider/tool',
|
|
name: 'test-provider',
|
|
type: CollectionType.builtIn,
|
|
icon: 'test-icon',
|
|
is_team_authorization: true,
|
|
allow_delete: true,
|
|
tools: [
|
|
{
|
|
name: 'test-tool',
|
|
label: { en_US: 'Test Tool' },
|
|
description: { en_US: 'A test tool' },
|
|
parameters: [
|
|
{ name: 'setting1', form: 'user', type: 'string' },
|
|
{ name: 'param1', form: 'llm', type: 'string' },
|
|
],
|
|
},
|
|
],
|
|
...overrides,
|
|
} as unknown as ToolWithProvider)
|
|
|
|
const defaultProps = {
|
|
onSelect: vi.fn(),
|
|
nodeOutputVars: [] as NodeOutPutVar[],
|
|
availableNodes: [] as Node[],
|
|
}
|
|
|
|
// ==================== Hook Tests ====================
|
|
|
|
describe('usePluginInstalledCheck Hook', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('should return inMarketPlace as false when manifest is null', () => {
|
|
const { result } = renderHook(
|
|
() => usePluginInstalledCheck('test-provider/tool'),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(result.current.inMarketPlace).toBe(false)
|
|
expect(result.current.manifest).toBeUndefined()
|
|
})
|
|
|
|
it('should handle empty provider name', () => {
|
|
const { result } = renderHook(
|
|
() => usePluginInstalledCheck(''),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(result.current.inMarketPlace).toBe(false)
|
|
})
|
|
|
|
it('should extract pluginID from provider name correctly', () => {
|
|
const { result } = renderHook(
|
|
() => usePluginInstalledCheck('org/plugin/extra'),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// The hook should parse "org/plugin" from "org/plugin/extra"
|
|
expect(result.current.inMarketPlace).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('useToolSelectorState Hook', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Initial State', () => {
|
|
it('should initialize with correct default values', () => {
|
|
const onSelect = vi.fn()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(result.current.isShow).toBe(false)
|
|
expect(result.current.isShowChooseTool).toBe(false)
|
|
expect(result.current.currType).toBe('settings')
|
|
expect(result.current.currentProvider).toBeUndefined()
|
|
expect(result.current.currentTool).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('State Setters', () => {
|
|
it('should update isShow state', () => {
|
|
const onSelect = vi.fn()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
act(() => {
|
|
result.current.setIsShow(true)
|
|
})
|
|
|
|
expect(result.current.isShow).toBe(true)
|
|
})
|
|
|
|
it('should update isShowChooseTool state', () => {
|
|
const onSelect = vi.fn()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
act(() => {
|
|
result.current.setIsShowChooseTool(true)
|
|
})
|
|
|
|
expect(result.current.isShowChooseTool).toBe(true)
|
|
})
|
|
|
|
it('should update currType state', () => {
|
|
const onSelect = vi.fn()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
act(() => {
|
|
result.current.setCurrType('params')
|
|
})
|
|
|
|
expect(result.current.currType).toBe('params')
|
|
})
|
|
})
|
|
|
|
describe('Event Handlers', () => {
|
|
it('should call onSelect when handleDescriptionChange is triggered', () => {
|
|
const onSelect = vi.fn()
|
|
const value = createToolValue()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ value, onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
act(() => {
|
|
result.current.handleDescriptionChange({
|
|
target: { value: 'new description' },
|
|
} as React.ChangeEvent<HTMLTextAreaElement>)
|
|
})
|
|
|
|
expect(onSelect).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
extra: expect.objectContaining({ description: 'new description' }),
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('should call onSelect when handleEnabledChange is triggered', () => {
|
|
const onSelect = vi.fn()
|
|
const value = createToolValue({ enabled: false })
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ value, onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
act(() => {
|
|
result.current.handleEnabledChange(true)
|
|
})
|
|
|
|
expect(onSelect).toHaveBeenCalledWith(
|
|
expect.objectContaining({ enabled: true }),
|
|
)
|
|
})
|
|
|
|
it('should call onSelect when handleAuthorizationItemClick is triggered', () => {
|
|
const onSelect = vi.fn()
|
|
const value = createToolValue()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ value, onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
act(() => {
|
|
result.current.handleAuthorizationItemClick('credential-123')
|
|
})
|
|
|
|
expect(onSelect).toHaveBeenCalledWith(
|
|
expect.objectContaining({ credential_id: 'credential-123' }),
|
|
)
|
|
})
|
|
|
|
it('should call onSelect when handleSettingsFormChange is triggered', () => {
|
|
const onSelect = vi.fn()
|
|
const value = createToolValue()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ value, onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
act(() => {
|
|
result.current.handleSettingsFormChange({ key: { type: VarKindType.constant, value: 'value' } })
|
|
})
|
|
|
|
expect(onSelect).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
settings: expect.any(Object),
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('should call onSelect when handleParamsFormChange is triggered', () => {
|
|
const onSelect = vi.fn()
|
|
const value = createToolValue()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ value, onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
act(() => {
|
|
result.current.handleParamsFormChange({ param: { value: { type: VarKindType.constant, value: 'value' } } })
|
|
})
|
|
|
|
expect(onSelect).toHaveBeenCalledWith(
|
|
expect.objectContaining({ parameters: { param: { value: { type: VarKindType.constant, value: 'value' } } } }),
|
|
)
|
|
})
|
|
|
|
it('should call onSelectMultiple when handleSelectMultipleTool is triggered', () => {
|
|
const onSelect = vi.fn()
|
|
const onSelectMultiple = vi.fn()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ onSelect, onSelectMultiple }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
act(() => {
|
|
result.current.handleSelectMultipleTool([createToolDefaultValue()])
|
|
})
|
|
|
|
expect(onSelectMultiple).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('Computed Values', () => {
|
|
it('should return empty settings value when no settings', () => {
|
|
const onSelect = vi.fn()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
expect(result.current.getSettingsValue()).toEqual({})
|
|
})
|
|
|
|
it('should compute showTabSlider correctly', () => {
|
|
const onSelect = vi.fn()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Without currentProvider, should be false
|
|
expect(result.current.showTabSlider).toBe(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
// ==================== Component Tests ====================
|
|
|
|
describe('ToolTrigger Component', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
render(<ToolTrigger open={false} />)
|
|
expect(screen.getByText(/placeholder|configureTool/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show placeholder text when no value', () => {
|
|
render(<ToolTrigger open={false} />)
|
|
// Should show placeholder text from i18n
|
|
expect(screen.getByText(/placeholder|configureTool/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show tool name when value is provided', () => {
|
|
const value = { provider_name: 'test', tool_name: 'My Tool' }
|
|
const provider = createToolWithProvider()
|
|
|
|
render(<ToolTrigger open={false} value={value} provider={provider} />)
|
|
expect(screen.getByText('My Tool')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show configure icon when isConfigure is true', () => {
|
|
render(<ToolTrigger open={false} isConfigure />)
|
|
// RiEqualizer2Line should be present
|
|
const container = screen.getByText(/configureTool/i).parentElement
|
|
expect(container).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show arrow icon when isConfigure is false', () => {
|
|
render(<ToolTrigger open={false} isConfigure={false} />)
|
|
// RiArrowDownSLine should be present
|
|
const container = screen.getByText(/placeholder/i).parentElement
|
|
expect(container).toBeInTheDocument()
|
|
})
|
|
|
|
it('should apply open state styling', () => {
|
|
const { rerender, container } = render(<ToolTrigger open={false} />)
|
|
expect(container.querySelector('.group')).toBeInTheDocument()
|
|
|
|
rerender(<ToolTrigger open={true} />)
|
|
// When open is true, the root div should have the hover-alt background
|
|
const updatedTriggerDiv = container.querySelector('.bg-state-base-hover-alt')
|
|
expect(updatedTriggerDiv).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('ToolItem Component', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
const { container } = render(<ToolItem open={false} />)
|
|
expect(container.querySelector('.group')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should display provider name and tool label', () => {
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
providerName="org/provider"
|
|
toolLabel="My Tool"
|
|
/>,
|
|
)
|
|
expect(screen.getByText('provider')).toBeInTheDocument()
|
|
expect(screen.getByText('My Tool')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show MCP provider show name for MCP tools', () => {
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
isMCPTool
|
|
providerShowName="MCP Provider"
|
|
toolLabel="My Tool"
|
|
/>,
|
|
)
|
|
expect(screen.getByText('MCP Provider')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render string icon correctly', () => {
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
icon="https://example.com/icon.png"
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
const iconElement = document.querySelector('[style*="background-image"]')
|
|
expect(iconElement).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render object icon correctly', () => {
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
icon={{ content: '🔧', background: '#fff' }}
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
// AppIcon should be rendered
|
|
expect(document.querySelector('.rounded-lg')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render default icon when no icon provided', () => {
|
|
render(<ToolItem open={false} toolLabel="Tool" />)
|
|
// Group icon should be rendered
|
|
expect(document.querySelector('.opacity-35')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call onDelete when delete button is clicked', async () => {
|
|
const onDelete = vi.fn()
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
onDelete={onDelete}
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
|
|
// Find the delete button (hidden by default, shown on hover)
|
|
const deleteBtn = document.querySelector('[class*="hover:text-text-destructive"]')
|
|
if (deleteBtn) {
|
|
fireEvent.click(deleteBtn)
|
|
expect(onDelete).toHaveBeenCalled()
|
|
}
|
|
})
|
|
|
|
it('should call onSwitchChange when switch is toggled', () => {
|
|
const onSwitchChange = vi.fn()
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
showSwitch
|
|
switchValue={false}
|
|
onSwitchChange={onSwitchChange}
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
|
|
// The switch should be rendered
|
|
const switchContainer = document.querySelector('.mr-1')
|
|
expect(switchContainer).toBeInTheDocument()
|
|
})
|
|
|
|
it('should stop propagation on delete click', () => {
|
|
const onDelete = vi.fn()
|
|
const parentClick = vi.fn()
|
|
|
|
render(
|
|
<div onClick={parentClick}>
|
|
<ToolItem
|
|
open={false}
|
|
onDelete={onDelete}
|
|
toolLabel="Tool"
|
|
/>
|
|
</div>,
|
|
)
|
|
|
|
const deleteBtn = document.querySelector('[class*="hover:text-text-destructive"]')
|
|
if (deleteBtn) {
|
|
fireEvent.click(deleteBtn)
|
|
expect(parentClick).not.toHaveBeenCalled()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('Conditional Rendering', () => {
|
|
it('should show switch only when showSwitch is true and no errors', () => {
|
|
const { rerender } = render(
|
|
<ToolItem open={false} showSwitch={false} toolLabel="Tool" />,
|
|
)
|
|
expect(document.querySelector('.mr-1')).not.toBeInTheDocument()
|
|
|
|
rerender(
|
|
<ToolItem open={false} showSwitch toolLabel="Tool" />,
|
|
)
|
|
expect(document.querySelector('.mr-1')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show not authorized button when noAuth is true', () => {
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
noAuth
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
expect(screen.getByText(/notAuthorized/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show auth removed button when authRemoved is true', () => {
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
authRemoved
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
expect(screen.getByText(/authRemoved/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show install button when uninstalled', () => {
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
uninstalled
|
|
installInfo="plugin@1.0.0"
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
expect(screen.getByTestId('install-plugin-btn')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show version switch when versionMismatch', () => {
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
versionMismatch
|
|
installInfo="plugin@1.0.0"
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
expect(screen.getByTestId('switch-version-btn')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show error icon when isError is true', () => {
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
isError
|
|
errorTip="Error occurred"
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
// RiErrorWarningFill should be rendered
|
|
expect(document.querySelector('.text-text-destructive')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should apply opacity when transparent states are true', () => {
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
uninstalled
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
expect(document.querySelector('.opacity-50')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show MCP tooltip when isMCPTool is true and MCP not allowed', () => {
|
|
// Set MCP tool not allowed
|
|
mockMCPToolAllowed = false
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
isMCPTool
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
// McpToolNotSupportTooltip should be rendered (line 128)
|
|
expect(screen.getByTestId('mcp-not-support-tooltip')).toBeInTheDocument()
|
|
// Reset
|
|
mockMCPToolAllowed = true
|
|
})
|
|
|
|
it('should apply opacity-30 to icon when isMCPTool and not allowed with string icon', () => {
|
|
mockMCPToolAllowed = false
|
|
const { container } = render(
|
|
<ToolItem
|
|
open={false}
|
|
isMCPTool
|
|
icon="https://example.com/icon.png"
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
// Should have opacity-30 class on the icon container (line 80)
|
|
const iconContainer = container.querySelector('.shrink-0.opacity-30')
|
|
expect(iconContainer).toBeInTheDocument()
|
|
mockMCPToolAllowed = true
|
|
})
|
|
|
|
it('should not have opacity-30 on icon when isMCPTool is false', () => {
|
|
mockMCPToolAllowed = true
|
|
const { container } = render(
|
|
<ToolItem
|
|
open={false}
|
|
isMCPTool={false}
|
|
icon="https://example.com/icon.png"
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
// Should NOT have opacity-30 when isShowCanNotChooseMCPTip is false
|
|
const iconContainer = container.querySelector('.shrink-0')
|
|
expect(iconContainer).toBeInTheDocument()
|
|
expect(iconContainer).not.toHaveClass('opacity-30')
|
|
})
|
|
|
|
it('should not have opacity-30 on icon when MCP allowed', () => {
|
|
mockMCPToolAllowed = true
|
|
const { container } = render(
|
|
<ToolItem
|
|
open={false}
|
|
isMCPTool={true}
|
|
icon="https://example.com/icon.png"
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
// Should NOT have opacity-30 when MCP is allowed
|
|
const iconContainer = container.querySelector('.shrink-0')
|
|
expect(iconContainer).toBeInTheDocument()
|
|
expect(iconContainer).not.toHaveClass('opacity-30')
|
|
})
|
|
|
|
it('should apply opacity-30 to default icon when isMCPTool and not allowed without icon', () => {
|
|
mockMCPToolAllowed = false
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
isMCPTool
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
// Should have opacity-30 class on default icon container (lines 89-97)
|
|
expect(document.querySelector('.opacity-30')).toBeInTheDocument()
|
|
mockMCPToolAllowed = true
|
|
})
|
|
|
|
it('should show switch when showSwitch is true without MCP tip', () => {
|
|
const { container } = render(
|
|
<ToolItem
|
|
open={false}
|
|
showSwitch
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
// Switch wrapper should be rendered when showSwitch is true and no MCP tip
|
|
expect(container.querySelector('.mr-1')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show MCP tooltip instead of switch when isMCPTool and not allowed', () => {
|
|
mockMCPToolAllowed = false
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
showSwitch
|
|
isMCPTool
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
// MCP tooltip should be rendered
|
|
expect(screen.getByTestId('mcp-not-support-tooltip')).toBeInTheDocument()
|
|
mockMCPToolAllowed = true
|
|
})
|
|
})
|
|
|
|
describe('Install/Upgrade Actions', () => {
|
|
it('should call onInstall when install button is clicked', () => {
|
|
const onInstall = vi.fn()
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
uninstalled
|
|
installInfo="plugin@1.0.0"
|
|
onInstall={onInstall}
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('install-plugin-btn'))
|
|
expect(onInstall).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should call onInstall when version switch is clicked', () => {
|
|
const onInstall = vi.fn()
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
versionMismatch
|
|
installInfo="plugin@1.0.0"
|
|
onInstall={onInstall}
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('switch-version-btn'))
|
|
expect(onInstall).toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('ToolAuthorizationSection Component', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render null when currentProvider is undefined', () => {
|
|
const { container } = render(
|
|
<ToolAuthorizationSection
|
|
onAuthorizationItemClick={vi.fn()}
|
|
/>,
|
|
)
|
|
expect(container.firstChild).toBeNull()
|
|
})
|
|
|
|
it('should render null when provider type is not builtIn', () => {
|
|
const provider = createToolWithProvider({ type: CollectionType.custom })
|
|
const { container } = render(
|
|
<ToolAuthorizationSection
|
|
currentProvider={provider}
|
|
onAuthorizationItemClick={vi.fn()}
|
|
/>,
|
|
)
|
|
expect(container.firstChild).toBeNull()
|
|
})
|
|
|
|
it('should render null when allow_delete is false', () => {
|
|
const provider = createToolWithProvider({ allow_delete: false })
|
|
const { container } = render(
|
|
<ToolAuthorizationSection
|
|
currentProvider={provider}
|
|
onAuthorizationItemClick={vi.fn()}
|
|
/>,
|
|
)
|
|
expect(container.firstChild).toBeNull()
|
|
})
|
|
|
|
it('should render when all conditions are met', () => {
|
|
const provider = createToolWithProvider({
|
|
type: CollectionType.builtIn,
|
|
allow_delete: true,
|
|
})
|
|
render(
|
|
<ToolAuthorizationSection
|
|
currentProvider={provider}
|
|
onAuthorizationItemClick={vi.fn()}
|
|
/>,
|
|
)
|
|
expect(screen.getByTestId('plugin-auth-in-agent')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call onAuthorizationItemClick when credential is selected', () => {
|
|
const onAuthorizationItemClick = vi.fn()
|
|
const provider = createToolWithProvider({
|
|
type: CollectionType.builtIn,
|
|
allow_delete: true,
|
|
})
|
|
|
|
render(
|
|
<ToolAuthorizationSection
|
|
currentProvider={provider}
|
|
onAuthorizationItemClick={onAuthorizationItemClick}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('auth-item-click-btn'))
|
|
expect(onAuthorizationItemClick).toHaveBeenCalledWith('credential-123')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('ToolSettingsPanel Component', () => {
|
|
const defaultSettingsPanelProps = {
|
|
nodeId: 'node-1',
|
|
currType: 'settings' as const,
|
|
settingsFormSchemas: [createMockFormSchema('setting1')],
|
|
paramsFormSchemas: [],
|
|
settingsValue: {},
|
|
showTabSlider: false,
|
|
userSettingsOnly: true,
|
|
reasoningConfigOnly: false,
|
|
nodeOutputVars: [] as NodeOutPutVar[],
|
|
availableNodes: [] as Node[],
|
|
onCurrTypeChange: vi.fn(),
|
|
onSettingsFormChange: vi.fn(),
|
|
onParamsFormChange: vi.fn(),
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render null when no schemas and no authorization', () => {
|
|
const { container } = render(
|
|
<ToolSettingsPanel
|
|
{...defaultSettingsPanelProps}
|
|
settingsFormSchemas={[]}
|
|
paramsFormSchemas={[]}
|
|
/>,
|
|
)
|
|
expect(container.firstChild).toBeNull()
|
|
})
|
|
|
|
it('should render null when not team authorized', () => {
|
|
const provider = createToolWithProvider({ is_team_authorization: false })
|
|
const { container } = render(
|
|
<ToolSettingsPanel
|
|
{...defaultSettingsPanelProps}
|
|
currentProvider={provider}
|
|
/>,
|
|
)
|
|
expect(container.firstChild).toBeNull()
|
|
})
|
|
|
|
it('should render settings form when has settings schemas', () => {
|
|
const provider = createToolWithProvider({ is_team_authorization: true })
|
|
render(
|
|
<ToolSettingsPanel
|
|
{...defaultSettingsPanelProps}
|
|
currentProvider={provider}
|
|
/>,
|
|
)
|
|
expect(screen.getByTestId('tool-form')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render tab slider when both settings and params exist', () => {
|
|
const provider = createToolWithProvider({ is_team_authorization: true })
|
|
const { container } = render(
|
|
<ToolSettingsPanel
|
|
{...defaultSettingsPanelProps}
|
|
currentProvider={provider}
|
|
settingsFormSchemas={[createMockFormSchema('setting1')]}
|
|
paramsFormSchemas={[createMockFormSchema('param1')]}
|
|
showTabSlider={true}
|
|
userSettingsOnly={false}
|
|
/>,
|
|
)
|
|
// Tab slider should be rendered (px-4 is a common class in TabSlider)
|
|
expect(container.querySelector('.px-4')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render reasoning config form when params tab is active', () => {
|
|
const provider = createToolWithProvider({ is_team_authorization: true })
|
|
render(
|
|
<ToolSettingsPanel
|
|
{...defaultSettingsPanelProps}
|
|
currentProvider={provider}
|
|
currType="params"
|
|
paramsFormSchemas={[createMockFormSchema('param1')]}
|
|
reasoningConfigOnly={true}
|
|
userSettingsOnly={false}
|
|
/>,
|
|
)
|
|
expect(screen.getByTestId('reasoning-config-form')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call onSettingsFormChange when settings form changes', () => {
|
|
const onSettingsFormChange = vi.fn()
|
|
const provider = createToolWithProvider({ is_team_authorization: true })
|
|
|
|
render(
|
|
<ToolSettingsPanel
|
|
{...defaultSettingsPanelProps}
|
|
currentProvider={provider}
|
|
onSettingsFormChange={onSettingsFormChange}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('change-settings-btn'))
|
|
expect(onSettingsFormChange).toHaveBeenCalledWith({ setting1: 'new-value' })
|
|
})
|
|
|
|
it('should call onParamsFormChange when params form changes', () => {
|
|
const onParamsFormChange = vi.fn()
|
|
const provider = createToolWithProvider({ is_team_authorization: true })
|
|
|
|
render(
|
|
<ToolSettingsPanel
|
|
{...defaultSettingsPanelProps}
|
|
currentProvider={provider}
|
|
currType="params"
|
|
paramsFormSchemas={[createMockFormSchema('param1')]}
|
|
reasoningConfigOnly={true}
|
|
userSettingsOnly={false}
|
|
onParamsFormChange={onParamsFormChange}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('change-params-btn'))
|
|
expect(onParamsFormChange).toHaveBeenCalledWith({ param1: 'new-param' })
|
|
})
|
|
})
|
|
|
|
describe('Tab Navigation', () => {
|
|
it('should show params tips when params tab is active', () => {
|
|
const provider = createToolWithProvider({ is_team_authorization: true })
|
|
render(
|
|
<ToolSettingsPanel
|
|
{...defaultSettingsPanelProps}
|
|
currentProvider={provider}
|
|
currType="params"
|
|
settingsFormSchemas={[createMockFormSchema('setting1')]}
|
|
paramsFormSchemas={[createMockFormSchema('param1')]}
|
|
showTabSlider={true}
|
|
userSettingsOnly={false}
|
|
/>,
|
|
)
|
|
// Params tips should be shown
|
|
expect(screen.getByText(/paramsTip1/i)).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('ToolBaseForm Component', () => {
|
|
const defaultBaseFormProps = {
|
|
isShowChooseTool: false,
|
|
hasTrigger: false,
|
|
onShowChange: vi.fn(),
|
|
onSelectTool: vi.fn(),
|
|
onSelectMultipleTool: vi.fn(),
|
|
onDescriptionChange: vi.fn(),
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
render(<ToolBaseForm {...defaultBaseFormProps} />)
|
|
expect(screen.getByTestId('tool-picker')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render tool label text', () => {
|
|
render(<ToolBaseForm {...defaultBaseFormProps} />)
|
|
expect(screen.getByText(/toolLabel/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render description label text', () => {
|
|
render(<ToolBaseForm {...defaultBaseFormProps} />)
|
|
expect(screen.getByText(/descriptionLabel/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render tool picker component', () => {
|
|
render(<ToolBaseForm {...defaultBaseFormProps} />)
|
|
expect(screen.getByTestId('tool-picker')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render textarea for description', () => {
|
|
render(<ToolBaseForm {...defaultBaseFormProps} />)
|
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Props Handling', () => {
|
|
it('should display description value in textarea', () => {
|
|
const value = createToolValue({ extra: { description: 'Test description' } })
|
|
render(<ToolBaseForm {...defaultBaseFormProps} value={value} />)
|
|
|
|
expect(screen.getByRole('textbox')).toHaveValue('Test description')
|
|
})
|
|
|
|
it('should disable textarea when no provider_name', () => {
|
|
const value = createToolValue({ provider_name: '' })
|
|
render(<ToolBaseForm {...defaultBaseFormProps} value={value} />)
|
|
|
|
expect(screen.getByRole('textbox')).toBeDisabled()
|
|
})
|
|
|
|
it('should enable textarea when provider_name exists', () => {
|
|
const value = createToolValue({ provider_name: 'test-provider' })
|
|
render(<ToolBaseForm {...defaultBaseFormProps} value={value} />)
|
|
|
|
expect(screen.getByRole('textbox')).not.toBeDisabled()
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call onDescriptionChange when textarea changes', async () => {
|
|
const onDescriptionChange = vi.fn()
|
|
const value = createToolValue()
|
|
|
|
render(
|
|
<ToolBaseForm
|
|
{...defaultBaseFormProps}
|
|
value={value}
|
|
onDescriptionChange={onDescriptionChange}
|
|
/>,
|
|
)
|
|
|
|
const textarea = screen.getByRole('textbox')
|
|
fireEvent.change(textarea, { target: { value: 'new description' } })
|
|
|
|
expect(onDescriptionChange).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should call onSelectTool when tool is selected', () => {
|
|
const onSelectTool = vi.fn()
|
|
render(
|
|
<ToolBaseForm
|
|
{...defaultBaseFormProps}
|
|
onSelectTool={onSelectTool}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('select-tool-btn'))
|
|
expect(onSelectTool).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should call onSelectMultipleTool when multiple tools are selected', () => {
|
|
const onSelectMultipleTool = vi.fn()
|
|
render(
|
|
<ToolBaseForm
|
|
{...defaultBaseFormProps}
|
|
onSelectMultipleTool={onSelectMultipleTool}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('select-multiple-btn'))
|
|
expect(onSelectMultipleTool).toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('ToolSelector Component', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
render(<ToolSelector {...defaultProps} />, { wrapper: createWrapper() })
|
|
expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render ToolTrigger when no value and no trigger', () => {
|
|
const { container } = render(<ToolSelector {...defaultProps} />, { wrapper: createWrapper() })
|
|
// ToolTrigger should be rendered with its group class
|
|
expect(container.querySelector('.group')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render custom trigger when provided', () => {
|
|
render(
|
|
<ToolSelector
|
|
{...defaultProps}
|
|
trigger={<button data-testid="custom-trigger">Custom Trigger</button>}
|
|
/>,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
expect(screen.getByTestId('custom-trigger')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render panel content', () => {
|
|
render(<ToolSelector {...defaultProps} />, { wrapper: createWrapper() })
|
|
expect(screen.getByTestId('portal-content')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render tool base form in panel', () => {
|
|
render(<ToolSelector {...defaultProps} />, { wrapper: createWrapper() })
|
|
expect(screen.getByTestId('tool-picker')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Props', () => {
|
|
it('should apply isEdit mode title', () => {
|
|
render(
|
|
<ToolSelector {...defaultProps} isEdit />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
expect(screen.getByText(/toolSetting/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should apply default title when not in edit mode', () => {
|
|
render(
|
|
<ToolSelector {...defaultProps} isEdit={false} />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
expect(screen.getByText(/title/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should pass nodeId to settings panel', () => {
|
|
render(
|
|
<ToolSelector {...defaultProps} nodeId="test-node-id" />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
// The component should receive and use the nodeId
|
|
expect(screen.getByTestId('portal-content')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Controlled Mode', () => {
|
|
it('should use controlledState when trigger is provided', () => {
|
|
const onControlledStateChange = vi.fn()
|
|
render(
|
|
<ToolSelector
|
|
{...defaultProps}
|
|
trigger={<button>Trigger</button>}
|
|
controlledState={true}
|
|
onControlledStateChange={onControlledStateChange}
|
|
/>,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
expect(screen.getByTestId('portal-to-follow-elem')).toHaveAttribute('data-open', 'true')
|
|
})
|
|
|
|
it('should use internal state when no trigger', () => {
|
|
render(
|
|
<ToolSelector {...defaultProps} />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
expect(screen.getByTestId('portal-to-follow-elem')).toHaveAttribute('data-open', 'false')
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call onSelect when tool is selected', () => {
|
|
const onSelect = vi.fn()
|
|
render(
|
|
<ToolSelector {...defaultProps} onSelect={onSelect} />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('select-tool-btn'))
|
|
expect(onSelect).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should call onSelectMultiple when multiple tools are selected', () => {
|
|
const onSelectMultiple = vi.fn()
|
|
render(
|
|
<ToolSelector {...defaultProps} onSelectMultiple={onSelectMultiple} />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('select-multiple-btn'))
|
|
expect(onSelectMultiple).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should pass onDelete prop to ToolItem', () => {
|
|
const onDelete = vi.fn()
|
|
const value = createToolValue()
|
|
|
|
const { container } = render(
|
|
<ToolSelector
|
|
{...defaultProps}
|
|
value={value}
|
|
onDelete={onDelete}
|
|
/>,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// ToolItem should be rendered (it has a group class)
|
|
// The delete functionality is tested in ToolItem tests
|
|
expect(container.querySelector('.group')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not trigger when disabled', () => {
|
|
const onSelect = vi.fn()
|
|
render(
|
|
<ToolSelector {...defaultProps} disabled onSelect={onSelect} />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Click on portal trigger
|
|
fireEvent.click(screen.getByTestId('portal-trigger'))
|
|
// State should not change when disabled
|
|
expect(screen.getByTestId('portal-to-follow-elem')).toHaveAttribute('data-open', 'false')
|
|
})
|
|
})
|
|
|
|
describe('Component Memoization', () => {
|
|
it('should be memoized with React.memo', () => {
|
|
// ToolSelector is wrapped with React.memo
|
|
// This test verifies the component doesn't re-render unnecessarily
|
|
const onSelect = vi.fn()
|
|
const { rerender } = render(
|
|
<ToolSelector {...defaultProps} onSelect={onSelect} />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Re-render with same props
|
|
rerender(<ToolSelector {...defaultProps} onSelect={onSelect} />)
|
|
|
|
// Component should not trigger unnecessary re-renders
|
|
expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ==================== Edge Cases ====================
|
|
|
|
describe('Edge Cases', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('ToolSelector with undefined values', () => {
|
|
it('should handle undefined value prop', () => {
|
|
render(
|
|
<ToolSelector {...defaultProps} value={undefined} />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle undefined selectedTools', () => {
|
|
render(
|
|
<ToolSelector {...defaultProps} selectedTools={undefined} />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle empty nodeOutputVars', () => {
|
|
render(
|
|
<ToolSelector {...defaultProps} nodeOutputVars={[]} />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle empty availableNodes', () => {
|
|
render(
|
|
<ToolSelector {...defaultProps} availableNodes={[]} />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('ToolItem with edge case props', () => {
|
|
it('should handle all error states combined', () => {
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
isError
|
|
uninstalled
|
|
versionMismatch
|
|
noAuth
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
// Should show error state (highest priority)
|
|
expect(document.querySelector('.text-text-destructive')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle empty provider name', () => {
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
providerName=""
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
expect(screen.getByText('Tool')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle special characters in tool label', () => {
|
|
render(
|
|
<ToolItem
|
|
open={false}
|
|
toolLabel="Tool <script>alert('xss')</script>"
|
|
/>,
|
|
)
|
|
// Should render safely without XSS
|
|
expect(screen.getByText(/Tool/)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('ToolBaseForm with edge case props', () => {
|
|
it('should handle undefined extra in value', () => {
|
|
const value = createToolValue({ extra: undefined })
|
|
render(
|
|
<ToolBaseForm
|
|
value={value}
|
|
isShowChooseTool={false}
|
|
hasTrigger={false}
|
|
onShowChange={vi.fn()}
|
|
onSelectTool={vi.fn()}
|
|
onSelectMultipleTool={vi.fn()}
|
|
onDescriptionChange={vi.fn()}
|
|
/>,
|
|
)
|
|
expect(screen.getByRole('textbox')).toHaveValue('')
|
|
})
|
|
|
|
it('should handle empty description', () => {
|
|
const value = createToolValue({ extra: { description: '' } })
|
|
render(
|
|
<ToolBaseForm
|
|
value={value}
|
|
isShowChooseTool={false}
|
|
hasTrigger={false}
|
|
onShowChange={vi.fn()}
|
|
onSelectTool={vi.fn()}
|
|
onSelectMultipleTool={vi.fn()}
|
|
onDescriptionChange={vi.fn()}
|
|
/>,
|
|
)
|
|
expect(screen.getByRole('textbox')).toHaveValue('')
|
|
})
|
|
})
|
|
|
|
describe('ToolSettingsPanel with edge case props', () => {
|
|
it('should handle empty schemas arrays', () => {
|
|
const { container } = render(
|
|
<ToolSettingsPanel
|
|
nodeId=""
|
|
currType="settings"
|
|
settingsFormSchemas={[]}
|
|
paramsFormSchemas={[]}
|
|
settingsValue={{}}
|
|
showTabSlider={false}
|
|
userSettingsOnly={false}
|
|
reasoningConfigOnly={false}
|
|
nodeOutputVars={[]}
|
|
availableNodes={[]}
|
|
onCurrTypeChange={vi.fn()}
|
|
onSettingsFormChange={vi.fn()}
|
|
onParamsFormChange={vi.fn()}
|
|
/>,
|
|
)
|
|
expect(container.firstChild).toBeNull()
|
|
})
|
|
|
|
it('should handle undefined currentProvider', () => {
|
|
const { container } = render(
|
|
<ToolSettingsPanel
|
|
currentProvider={undefined}
|
|
nodeId="node-1"
|
|
currType="settings"
|
|
settingsFormSchemas={[createMockFormSchema('setting1')]}
|
|
paramsFormSchemas={[]}
|
|
settingsValue={{}}
|
|
showTabSlider={false}
|
|
userSettingsOnly={true}
|
|
reasoningConfigOnly={false}
|
|
nodeOutputVars={[]}
|
|
availableNodes={[]}
|
|
onCurrTypeChange={vi.fn()}
|
|
onSettingsFormChange={vi.fn()}
|
|
onParamsFormChange={vi.fn()}
|
|
/>,
|
|
)
|
|
expect(container.firstChild).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('Hook edge cases', () => {
|
|
it('useToolSelectorState should handle undefined onSelectMultiple', () => {
|
|
const onSelect = vi.fn()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ onSelect, onSelectMultiple: undefined }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Should not throw when calling handleSelectMultipleTool
|
|
act(() => {
|
|
result.current.handleSelectMultipleTool([createToolDefaultValue()])
|
|
})
|
|
|
|
// Should complete without error
|
|
expect(result.current.isShow).toBe(false)
|
|
})
|
|
|
|
it('useToolSelectorState should handle empty description change', () => {
|
|
const onSelect = vi.fn()
|
|
const value = createToolValue()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ value, onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
act(() => {
|
|
result.current.handleDescriptionChange({
|
|
target: { value: '' },
|
|
} as React.ChangeEvent<HTMLTextAreaElement>)
|
|
})
|
|
|
|
expect(onSelect).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
extra: expect.objectContaining({ description: '' }),
|
|
}),
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
// ==================== SchemaModal Tests ====================
|
|
|
|
describe('SchemaModal Component', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render modal with schema content', () => {
|
|
const mockSchema: SchemaRoot = {
|
|
type: Type.object,
|
|
properties: {
|
|
name: { type: Type.string },
|
|
},
|
|
additionalProperties: false,
|
|
}
|
|
|
|
render(
|
|
<SchemaModal
|
|
isShow={true}
|
|
schema={mockSchema}
|
|
rootName="TestSchema"
|
|
onClose={vi.fn()}
|
|
/>,
|
|
)
|
|
|
|
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render when isShow is false', () => {
|
|
const mockSchema: SchemaRoot = { type: Type.object, properties: {}, additionalProperties: false }
|
|
|
|
render(
|
|
<SchemaModal
|
|
isShow={false}
|
|
schema={mockSchema}
|
|
rootName="TestSchema"
|
|
onClose={vi.fn()}
|
|
/>,
|
|
)
|
|
|
|
expect(screen.queryByTestId('modal')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should call onClose when close button is clicked', () => {
|
|
const onClose = vi.fn()
|
|
const mockSchema: SchemaRoot = { type: Type.object, properties: {}, additionalProperties: false }
|
|
|
|
render(
|
|
<SchemaModal
|
|
isShow={true}
|
|
schema={mockSchema}
|
|
rootName="TestSchema"
|
|
onClose={onClose}
|
|
/>,
|
|
)
|
|
|
|
// Find and click close button (the one with absolute positioning)
|
|
const closeBtn = document.querySelector('.absolute')
|
|
if (closeBtn) {
|
|
fireEvent.click(closeBtn)
|
|
expect(onClose).toHaveBeenCalled()
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
// ==================== ToolCredentialsForm Tests ====================
|
|
|
|
describe('ToolCredentialsForm Component', () => {
|
|
const mockCollection: Partial<Collection> = {
|
|
name: 'test-collection',
|
|
label: { en_US: 'Test Collection', zh_Hans: '测试集合' },
|
|
type: CollectionType.builtIn,
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render loading state initially', () => {
|
|
render(
|
|
<ToolCredentialsForm
|
|
collection={mockCollection as Collection}
|
|
onCancel={vi.fn()}
|
|
onSaved={vi.fn()}
|
|
/>,
|
|
)
|
|
|
|
// Should show loading initially (using role="status" from Loading component)
|
|
expect(screen.getByRole('status')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should render form after loading', async () => {
|
|
render(
|
|
<ToolCredentialsForm
|
|
collection={mockCollection as Collection}
|
|
onCancel={vi.fn()}
|
|
onSaved={vi.fn()}
|
|
/>,
|
|
)
|
|
|
|
// Wait for loading to complete
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('credential-form')).toBeInTheDocument()
|
|
}, { timeout: 2000 })
|
|
})
|
|
|
|
it('should call onCancel when cancel button is clicked', async () => {
|
|
const onCancel = vi.fn()
|
|
|
|
render(
|
|
<ToolCredentialsForm
|
|
collection={mockCollection as Collection}
|
|
onCancel={onCancel}
|
|
onSaved={vi.fn()}
|
|
/>,
|
|
)
|
|
|
|
// Wait for loading to complete and click cancel
|
|
await waitFor(() => {
|
|
const cancelBtn = screen.queryByText(/cancel/i)
|
|
if (cancelBtn) {
|
|
fireEvent.click(cancelBtn)
|
|
expect(onCancel).toHaveBeenCalled()
|
|
}
|
|
}, { timeout: 2000 })
|
|
})
|
|
|
|
it('should call onSaved when save button is clicked with valid data', async () => {
|
|
const onSaved = vi.fn()
|
|
|
|
render(
|
|
<ToolCredentialsForm
|
|
collection={mockCollection as Collection}
|
|
onCancel={vi.fn()}
|
|
onSaved={onSaved}
|
|
/>,
|
|
)
|
|
|
|
// Wait for loading to complete
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('credential-form')).toBeInTheDocument()
|
|
}, { timeout: 2000 })
|
|
|
|
// Click save
|
|
const saveBtn = screen.getByText(/save/i)
|
|
fireEvent.click(saveBtn)
|
|
|
|
// onSaved should be called
|
|
expect(onSaved).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should render fieldMoreInfo with url', async () => {
|
|
render(
|
|
<ToolCredentialsForm
|
|
collection={mockCollection as Collection}
|
|
onCancel={vi.fn()}
|
|
onSaved={vi.fn()}
|
|
/>,
|
|
)
|
|
|
|
// Wait for loading to complete
|
|
await waitFor(() => {
|
|
const fieldMoreInfo = screen.queryByTestId('field-more-info')
|
|
if (fieldMoreInfo) {
|
|
// Should render link for item with url
|
|
expect(fieldMoreInfo.querySelector('a')).toBeInTheDocument()
|
|
}
|
|
}, { timeout: 2000 })
|
|
})
|
|
|
|
it('should update form value when onChange is called', async () => {
|
|
render(
|
|
<ToolCredentialsForm
|
|
collection={mockCollection as Collection}
|
|
onCancel={vi.fn()}
|
|
onSaved={vi.fn()}
|
|
/>,
|
|
)
|
|
|
|
// Wait for form to load
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('credential-form')).toBeInTheDocument()
|
|
}, { timeout: 2000 })
|
|
|
|
// Trigger onChange via mock form
|
|
const formInput = screen.getByTestId('form-input')
|
|
fireEvent.change(formInput, { target: { value: '{"api_key":"test"}' } })
|
|
|
|
// Verify form updated
|
|
expect(formInput).toHaveValue('{"api_key":"test"}')
|
|
})
|
|
|
|
it('should show error toast when required field is missing', async () => {
|
|
// Clear previous calls
|
|
mockToastNotify.mockClear()
|
|
|
|
// Setup mock to return required field
|
|
mockFetchBuiltInToolCredentialSchema.mockResolvedValueOnce([
|
|
{ name: 'api_key', type: 'string', required: true, label: { en_US: 'API Key' } },
|
|
])
|
|
mockFetchBuiltInToolCredential.mockResolvedValueOnce({})
|
|
|
|
const onSaved = vi.fn()
|
|
|
|
render(
|
|
<ToolCredentialsForm
|
|
collection={mockCollection as Collection}
|
|
onCancel={vi.fn()}
|
|
onSaved={onSaved}
|
|
/>,
|
|
)
|
|
|
|
// Wait for form to load
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('credential-form')).toBeInTheDocument()
|
|
}, { timeout: 2000 })
|
|
|
|
// Click save without filling required field
|
|
const saveBtn = screen.getByText(/save/i)
|
|
fireEvent.click(saveBtn)
|
|
|
|
// Toast.notify should have been called with error (lines 49-50)
|
|
expect(mockToastNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }))
|
|
// onSaved should not be called because validation fails
|
|
expect(onSaved).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should call onSaved when all required fields are filled', async () => {
|
|
// Setup mock to return required field with value
|
|
mockFetchBuiltInToolCredentialSchema.mockResolvedValueOnce([
|
|
{ name: 'api_key', type: 'string', required: true, label: { en_US: 'API Key' } },
|
|
])
|
|
mockFetchBuiltInToolCredential.mockResolvedValueOnce({ api_key: 'test-key' })
|
|
|
|
const onSaved = vi.fn()
|
|
|
|
render(
|
|
<ToolCredentialsForm
|
|
collection={mockCollection as Collection}
|
|
onCancel={vi.fn()}
|
|
onSaved={onSaved}
|
|
/>,
|
|
)
|
|
|
|
// Wait for form to load
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('credential-form')).toBeInTheDocument()
|
|
}, { timeout: 2000 })
|
|
|
|
// Click save
|
|
const saveBtn = screen.getByText(/save/i)
|
|
fireEvent.click(saveBtn)
|
|
|
|
// onSaved should be called with credential data
|
|
expect(onSaved).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should iterate through all credential schema fields on save', async () => {
|
|
// Setup mock with multiple fields including required ones
|
|
mockFetchBuiltInToolCredentialSchema.mockResolvedValueOnce([
|
|
{ name: 'api_key', type: 'string', required: true, label: { en_US: 'API Key' } },
|
|
{ name: 'secret', type: 'string', required: true, label: { en_US: 'Secret' } },
|
|
{ name: 'optional_field', type: 'string', required: false, label: { en_US: 'Optional' } },
|
|
])
|
|
mockFetchBuiltInToolCredential.mockResolvedValueOnce({ api_key: 'key', secret: 'secret' })
|
|
|
|
const onSaved = vi.fn()
|
|
|
|
render(
|
|
<ToolCredentialsForm
|
|
collection={mockCollection as Collection}
|
|
onCancel={vi.fn()}
|
|
onSaved={onSaved}
|
|
/>,
|
|
)
|
|
|
|
// Wait for form to load and click save
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('credential-form')).toBeInTheDocument()
|
|
}, { timeout: 2000 })
|
|
|
|
const saveBtn = screen.getByText(/save/i)
|
|
fireEvent.click(saveBtn)
|
|
|
|
// onSaved should be called since all required fields are filled
|
|
await waitFor(() => {
|
|
expect(onSaved).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should handle form onChange and update tempCredential state', async () => {
|
|
mockFetchBuiltInToolCredentialSchema.mockResolvedValueOnce([
|
|
{ name: 'api_key', type: 'string', required: false, label: { en_US: 'API Key' } },
|
|
])
|
|
mockFetchBuiltInToolCredential.mockResolvedValueOnce({})
|
|
|
|
render(
|
|
<ToolCredentialsForm
|
|
collection={mockCollection as Collection}
|
|
onCancel={vi.fn()}
|
|
onSaved={vi.fn()}
|
|
/>,
|
|
)
|
|
|
|
// Wait for form to load
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('credential-form')).toBeInTheDocument()
|
|
}, { timeout: 2000 })
|
|
|
|
// Trigger onChange via mock form
|
|
const formInput = screen.getByTestId('form-input')
|
|
fireEvent.change(formInput, { target: { value: '{"api_key":"new-value"}' } })
|
|
|
|
// The form should have updated
|
|
expect(formInput).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ==================== Additional Coverage Tests ====================
|
|
|
|
describe('Additional Coverage Tests', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('ToolItem Mouse Events', () => {
|
|
it('should set deleting state on mouse over', () => {
|
|
const { container } = render(
|
|
<ToolItem
|
|
open={false}
|
|
onDelete={vi.fn()}
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
|
|
const deleteBtn = container.querySelector('[class*="hover:text-text-destructive"]')
|
|
if (deleteBtn) {
|
|
fireEvent.mouseOver(deleteBtn)
|
|
// After mouseOver, the parent should have destructive border
|
|
// This tests line 113
|
|
const parentDiv = container.querySelector('.group')
|
|
expect(parentDiv).toBeInTheDocument()
|
|
}
|
|
})
|
|
|
|
it('should reset deleting state on mouse leave', () => {
|
|
const { container } = render(
|
|
<ToolItem
|
|
open={false}
|
|
onDelete={vi.fn()}
|
|
toolLabel="Tool"
|
|
/>,
|
|
)
|
|
|
|
const deleteBtn = container.querySelector('[class*="hover:text-text-destructive"]')
|
|
if (deleteBtn) {
|
|
fireEvent.mouseOver(deleteBtn)
|
|
fireEvent.mouseLeave(deleteBtn)
|
|
// After mouseLeave, should reset
|
|
// This tests line 114
|
|
const parentDiv = container.querySelector('.group')
|
|
expect(parentDiv).toBeInTheDocument()
|
|
}
|
|
})
|
|
|
|
it('should stop propagation on install button click', () => {
|
|
const onInstall = vi.fn()
|
|
const parentClick = vi.fn()
|
|
|
|
render(
|
|
<div onClick={parentClick}>
|
|
<ToolItem
|
|
open={false}
|
|
uninstalled
|
|
installInfo="plugin@1.0.0"
|
|
onInstall={onInstall}
|
|
toolLabel="Tool"
|
|
/>
|
|
</div>,
|
|
)
|
|
|
|
// The InstallPluginButton mock handles onClick with stopPropagation
|
|
fireEvent.click(screen.getByTestId('install-plugin-btn'))
|
|
expect(onInstall).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should stop propagation on switch click', () => {
|
|
const parentClick = vi.fn()
|
|
const onSwitchChange = vi.fn()
|
|
|
|
render(
|
|
<div onClick={parentClick}>
|
|
<ToolItem
|
|
open={false}
|
|
showSwitch
|
|
switchValue={true}
|
|
onSwitchChange={onSwitchChange}
|
|
toolLabel="Tool"
|
|
/>
|
|
</div>,
|
|
)
|
|
|
|
// Find and click on switch container
|
|
const switchContainer = document.querySelector('.mr-1')
|
|
expect(switchContainer).toBeInTheDocument()
|
|
if (switchContainer) {
|
|
fireEvent.click(switchContainer)
|
|
// Parent should not be called due to stopPropagation (line 120)
|
|
expect(parentClick).not.toHaveBeenCalled()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('useToolSelectorState with Provider Data', () => {
|
|
it('should compute currentToolSettings when provider exists', () => {
|
|
// Setup mock data with tools
|
|
const mockProvider = createToolWithProvider({
|
|
id: 'test-provider/tool',
|
|
tools: [
|
|
{
|
|
name: 'test-tool',
|
|
parameters: [
|
|
{ name: 'setting1', form: 'user', label: { en_US: 'Setting 1', zh_Hans: '设置1' }, human_description: { en_US: '', zh_Hans: '' }, type: 'string', llm_description: '', required: false, multiple: false, default: '' },
|
|
{ name: 'param1', form: 'llm', label: { en_US: 'Param 1', zh_Hans: '参数1' }, human_description: { en_US: '', zh_Hans: '' }, type: 'string', llm_description: '', required: false, multiple: false, default: '' },
|
|
],
|
|
},
|
|
],
|
|
})
|
|
|
|
// Temporarily modify mock data
|
|
mockBuildInTools!.push(mockProvider)
|
|
|
|
const onSelect = vi.fn()
|
|
const value = createToolValue({ provider_name: 'test-provider/tool', tool_name: 'test-tool' })
|
|
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ value, onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Clean up
|
|
mockBuildInTools!.pop()
|
|
|
|
expect(result.current.currentToolSettings).toBeDefined()
|
|
})
|
|
|
|
it('should call handleInstall and invalidate caches', async () => {
|
|
const onSelect = vi.fn()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
await act(async () => {
|
|
await result.current.handleInstall()
|
|
})
|
|
|
|
// handleInstall should complete without error
|
|
expect(result.current.isShow).toBe(false)
|
|
})
|
|
|
|
it('should return empty manifestIcon when manifest is null', () => {
|
|
mockManifestData = null
|
|
const onSelect = vi.fn()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Without manifest, should return empty string
|
|
expect(result.current.manifestIcon).toBe('')
|
|
})
|
|
|
|
it('should return manifestIcon URL when manifest exists', () => {
|
|
// Set manifest data
|
|
mockManifestData = {
|
|
data: {
|
|
plugin: {
|
|
plugin_id: 'test-plugin-id',
|
|
latest_package_identifier: 'test@1.0.0',
|
|
},
|
|
},
|
|
}
|
|
|
|
const onSelect = vi.fn()
|
|
const value = createToolValue({ provider_name: 'test/plugin' })
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ value, onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// With manifest, should return icon URL - this covers line 103
|
|
expect(result.current.manifest).toBeDefined()
|
|
|
|
// Reset mock
|
|
mockManifestData = null
|
|
})
|
|
|
|
it('should handle tool selection with paramSchemas filtering', () => {
|
|
const onSelect = vi.fn()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
const toolWithSchemas: ToolDefaultValue = {
|
|
...createToolDefaultValue(),
|
|
paramSchemas: [
|
|
{ name: 'setting1', form: 'user', label: { en_US: 'Setting 1' }, human_description: { en_US: '' }, type: 'string', llm_description: '', required: false, multiple: false, default: '' },
|
|
{ name: 'param1', form: 'llm', label: { en_US: 'Param 1' }, human_description: { en_US: '' }, type: 'string', llm_description: '', required: false, multiple: false, default: '' },
|
|
],
|
|
}
|
|
|
|
act(() => {
|
|
result.current.handleSelectTool(toolWithSchemas)
|
|
})
|
|
|
|
expect(onSelect).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should merge all tool types including customTools, workflowTools and mcpTools', () => {
|
|
// Setup all tool type mocks to cover lines 52-55
|
|
const buildInProvider = createToolWithProvider({
|
|
id: 'builtin-provider/tool',
|
|
name: 'builtin-provider',
|
|
type: CollectionType.builtIn,
|
|
tools: [{ name: 'builtin-tool', parameters: [] }],
|
|
})
|
|
|
|
const customProvider = createToolWithProvider({
|
|
id: 'custom-provider/tool',
|
|
name: 'custom-provider',
|
|
type: CollectionType.custom,
|
|
tools: [{ name: 'custom-tool', parameters: [] }],
|
|
})
|
|
|
|
const workflowProvider = createToolWithProvider({
|
|
id: 'workflow-provider/tool',
|
|
name: 'workflow-provider',
|
|
type: CollectionType.workflow,
|
|
tools: [{ name: 'workflow-tool', parameters: [] }],
|
|
})
|
|
|
|
const mcpProvider = createToolWithProvider({
|
|
id: 'mcp-provider/tool',
|
|
name: 'mcp-provider',
|
|
type: CollectionType.mcp,
|
|
tools: [{ name: 'mcp-tool', parameters: [] }],
|
|
})
|
|
|
|
// Set all mocks
|
|
mockBuildInTools = [buildInProvider]
|
|
mockCustomTools = [customProvider]
|
|
mockWorkflowTools = [workflowProvider]
|
|
mockMcpTools = [mcpProvider]
|
|
|
|
const onSelect = vi.fn()
|
|
const value = createToolValue({ provider_name: 'builtin-provider/tool', tool_name: 'builtin-tool' })
|
|
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ value, onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Should find the builtin provider
|
|
expect(result.current.currentProvider).toBeDefined()
|
|
|
|
// Clean up
|
|
mockBuildInTools = []
|
|
mockCustomTools = []
|
|
mockWorkflowTools = []
|
|
mockMcpTools = []
|
|
})
|
|
|
|
it('should filter parameters correctly for settings and params', () => {
|
|
// Setup mock with tool that has both user and llm parameters
|
|
const mockProvider = createToolWithProvider({
|
|
id: 'test-provider/tool',
|
|
name: 'test-provider',
|
|
tools: [
|
|
{
|
|
name: 'test-tool',
|
|
label: { en_US: 'Test Tool' },
|
|
parameters: [
|
|
{ name: 'setting1', form: 'user' },
|
|
{ name: 'setting2', form: 'user' },
|
|
{ name: 'param1', form: 'llm' },
|
|
{ name: 'param2', form: 'llm' },
|
|
],
|
|
},
|
|
],
|
|
})
|
|
|
|
mockBuildInTools = [mockProvider]
|
|
|
|
const onSelect = vi.fn()
|
|
const value = createToolValue({ provider_name: 'test-provider/tool', tool_name: 'test-tool' })
|
|
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ value, onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Verify currentToolSettings filters to user form only (lines 69-72)
|
|
expect(result.current.currentToolSettings).toBeDefined()
|
|
// Verify currentToolParams filters to llm form only (lines 78-81)
|
|
expect(result.current.currentToolParams).toBeDefined()
|
|
|
|
// Clean up
|
|
mockBuildInTools = []
|
|
})
|
|
|
|
it('should return empty arrays when currentProvider is undefined', () => {
|
|
const onSelect = vi.fn()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Without a provider, settings and params should be empty
|
|
expect(result.current.currentToolSettings).toEqual([])
|
|
expect(result.current.currentToolParams).toEqual([])
|
|
})
|
|
|
|
it('should handle null/undefined tool arrays with fallback', () => {
|
|
// Clear all mocks to undefined
|
|
mockBuildInTools = undefined
|
|
mockCustomTools = undefined
|
|
mockWorkflowTools = undefined
|
|
mockMcpTools = undefined
|
|
|
|
const onSelect = vi.fn()
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Should not crash and currentProvider should be undefined
|
|
expect(result.current.currentProvider).toBeUndefined()
|
|
|
|
// Reset mocks
|
|
mockBuildInTools = []
|
|
mockCustomTools = []
|
|
mockWorkflowTools = []
|
|
mockMcpTools = []
|
|
})
|
|
|
|
it('should handle tool not found in provider', () => {
|
|
// Setup mock with provider but wrong tool name
|
|
const mockProvider = {
|
|
id: 'test-provider/tool',
|
|
name: 'test-provider',
|
|
type: CollectionType.builtIn,
|
|
icon: 'icon',
|
|
is_team_authorization: true,
|
|
allow_delete: true,
|
|
tools: [
|
|
{
|
|
name: 'different-tool',
|
|
label: { en_US: 'Different Tool' },
|
|
parameters: [{ name: 'setting1', form: 'user' }],
|
|
},
|
|
],
|
|
} as unknown as ToolWithProvider
|
|
|
|
mockBuildInTools = [mockProvider]
|
|
|
|
const onSelect = vi.fn()
|
|
// Use a tool_name that doesn't exist in the provider
|
|
const value = createToolValue({ provider_name: 'test-provider/tool', tool_name: 'non-existent-tool' })
|
|
|
|
const { result } = renderHook(
|
|
() => useToolSelectorState({ value, onSelect }),
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Provider should be found but tool should not
|
|
expect(result.current.currentProvider).toBeDefined()
|
|
expect(result.current.currentTool).toBeUndefined()
|
|
// Parameters should fallback to empty arrays due to || []
|
|
expect(result.current.currentToolSettings).toEqual([])
|
|
expect(result.current.currentToolParams).toEqual([])
|
|
|
|
// Clean up
|
|
mockBuildInTools = []
|
|
})
|
|
})
|
|
|
|
describe('ToolSettingsPanel Tab Change', () => {
|
|
it('should call onCurrTypeChange when tab is switched', () => {
|
|
const onCurrTypeChange = vi.fn()
|
|
const provider = createToolWithProvider({ is_team_authorization: true })
|
|
|
|
render(
|
|
<ToolSettingsPanel
|
|
currentProvider={provider}
|
|
nodeId="node-1"
|
|
currType="settings"
|
|
settingsFormSchemas={[createMockFormSchema('setting1')]}
|
|
paramsFormSchemas={[createMockFormSchema('param1')]}
|
|
settingsValue={{}}
|
|
showTabSlider={true}
|
|
userSettingsOnly={false}
|
|
reasoningConfigOnly={false}
|
|
nodeOutputVars={[]}
|
|
availableNodes={[]}
|
|
onCurrTypeChange={onCurrTypeChange}
|
|
onSettingsFormChange={vi.fn()}
|
|
onParamsFormChange={vi.fn()}
|
|
/>,
|
|
)
|
|
|
|
// The TabSlider component should render
|
|
expect(document.querySelector('.space-x-6')).toBeInTheDocument()
|
|
|
|
// Find and click on the params tab to trigger onChange (line 87)
|
|
const paramsTab = screen.getByText(/params/i)
|
|
fireEvent.click(paramsTab)
|
|
expect(onCurrTypeChange).toHaveBeenCalledWith('params')
|
|
})
|
|
|
|
it('should handle tab change with different currType values', () => {
|
|
const onCurrTypeChange = vi.fn()
|
|
const provider = createToolWithProvider({ is_team_authorization: true })
|
|
|
|
const { rerender } = render(
|
|
<ToolSettingsPanel
|
|
currentProvider={provider}
|
|
nodeId="node-1"
|
|
currType="settings"
|
|
settingsFormSchemas={[createMockFormSchema('setting1')]}
|
|
paramsFormSchemas={[createMockFormSchema('param1')]}
|
|
settingsValue={{}}
|
|
showTabSlider={true}
|
|
userSettingsOnly={false}
|
|
reasoningConfigOnly={false}
|
|
nodeOutputVars={[]}
|
|
availableNodes={[]}
|
|
onCurrTypeChange={onCurrTypeChange}
|
|
onSettingsFormChange={vi.fn()}
|
|
onParamsFormChange={vi.fn()}
|
|
/>,
|
|
)
|
|
|
|
// Rerender with params currType
|
|
rerender(
|
|
<ToolSettingsPanel
|
|
currentProvider={provider}
|
|
nodeId="node-1"
|
|
currType="params"
|
|
settingsFormSchemas={[createMockFormSchema('setting1')]}
|
|
paramsFormSchemas={[createMockFormSchema('param1')]}
|
|
settingsValue={{}}
|
|
showTabSlider={true}
|
|
userSettingsOnly={false}
|
|
reasoningConfigOnly={false}
|
|
nodeOutputVars={[]}
|
|
availableNodes={[]}
|
|
onCurrTypeChange={onCurrTypeChange}
|
|
onSettingsFormChange={vi.fn()}
|
|
onParamsFormChange={vi.fn()}
|
|
/>,
|
|
)
|
|
|
|
// Now params tips should be visible
|
|
expect(screen.getByText(/paramsTip1/i)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('ToolSelector Trigger Click Behavior', () => {
|
|
beforeEach(() => {
|
|
// Reset mock tools
|
|
mockBuildInTools = []
|
|
})
|
|
|
|
it('should not set isShow when disabled', () => {
|
|
render(
|
|
<ToolSelector {...defaultProps} disabled />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Click on the trigger
|
|
const trigger = screen.getByTestId('portal-trigger')
|
|
fireEvent.click(trigger)
|
|
|
|
// Should still be closed because disabled
|
|
expect(screen.getByTestId('portal-to-follow-elem')).toHaveAttribute('data-open', 'false')
|
|
})
|
|
|
|
it('should handle trigger click when provider and tool exist', () => {
|
|
// This requires mocking the tools data
|
|
render(
|
|
<ToolSelector {...defaultProps} />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Without provider/tool, clicking should not open
|
|
const trigger = screen.getByTestId('portal-trigger')
|
|
fireEvent.click(trigger)
|
|
|
|
expect(screen.getByTestId('portal-to-follow-elem')).toHaveAttribute('data-open', 'false')
|
|
})
|
|
|
|
it('should early return from handleTriggerClick when disabled', () => {
|
|
// Test to ensure disabled state prevents opening
|
|
const { rerender } = render(
|
|
<ToolSelector {...defaultProps} disabled={false} />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Rerender with disabled=true
|
|
rerender(<ToolSelector {...defaultProps} disabled={true} />)
|
|
|
|
const trigger = screen.getByTestId('portal-trigger')
|
|
fireEvent.click(trigger)
|
|
|
|
// Verify it stays closed
|
|
expect(screen.getByTestId('portal-to-follow-elem')).toHaveAttribute('data-open', 'false')
|
|
})
|
|
|
|
it('should set isShow when clicked with valid provider and tool', () => {
|
|
// Setup mock data to have matching provider/tool
|
|
const mockProvider = {
|
|
id: 'test-provider/tool',
|
|
name: 'test-provider',
|
|
type: CollectionType.builtIn,
|
|
icon: 'test-icon',
|
|
is_team_authorization: true,
|
|
allow_delete: true,
|
|
tools: [
|
|
{
|
|
name: 'test-tool',
|
|
label: { en_US: 'Test Tool' },
|
|
parameters: [],
|
|
},
|
|
],
|
|
} as unknown as ToolWithProvider
|
|
|
|
mockBuildInTools = [mockProvider]
|
|
|
|
const value = createToolValue({
|
|
provider_name: 'test-provider/tool',
|
|
tool_name: 'test-tool',
|
|
})
|
|
|
|
render(
|
|
<ToolSelector {...defaultProps} value={value} disabled={false} />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Click on the trigger - this should call handleTriggerClick
|
|
const trigger = screen.getByTestId('portal-trigger')
|
|
fireEvent.click(trigger)
|
|
|
|
// Now that we have provider and tool, the click should work
|
|
// This tests lines 106-108 and 148
|
|
expect(screen.getByTestId('portal-to-follow-elem')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not open when disabled is true even with valid provider', () => {
|
|
const mockProvider = {
|
|
id: 'test-provider/tool',
|
|
name: 'test-provider',
|
|
type: CollectionType.builtIn,
|
|
icon: 'test-icon',
|
|
is_team_authorization: true,
|
|
allow_delete: true,
|
|
tools: [
|
|
{
|
|
name: 'test-tool',
|
|
label: { en_US: 'Test Tool' },
|
|
parameters: [],
|
|
},
|
|
],
|
|
} as unknown as ToolWithProvider
|
|
|
|
mockBuildInTools = [mockProvider]
|
|
|
|
const value = createToolValue({
|
|
provider_name: 'test-provider/tool',
|
|
tool_name: 'test-tool',
|
|
})
|
|
|
|
render(
|
|
<ToolSelector {...defaultProps} value={value} disabled={true} />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Click should not open because disabled=true
|
|
const trigger = screen.getByTestId('portal-trigger')
|
|
fireEvent.click(trigger)
|
|
|
|
// Verify it stays closed due to disabled
|
|
expect(screen.getByTestId('portal-to-follow-elem')).toHaveAttribute('data-open', 'false')
|
|
})
|
|
})
|
|
|
|
describe('ToolTrigger Configure Mode', () => {
|
|
it('should show different icon based on isConfigure prop', () => {
|
|
const { rerender, container } = render(<ToolTrigger open={false} isConfigure={true} />)
|
|
|
|
// Should have equalizer icon when isConfigure is true
|
|
expect(container.querySelector('svg')).toBeInTheDocument()
|
|
|
|
rerender(<ToolTrigger open={false} isConfigure={false} />)
|
|
// Should have arrow down icon when isConfigure is false
|
|
expect(container.querySelector('svg')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ==================== Integration Tests ====================
|
|
|
|
describe('Integration Tests', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Full Flow: Tool Selection', () => {
|
|
it('should complete full tool selection flow', async () => {
|
|
const onSelect = vi.fn()
|
|
render(
|
|
<ToolSelector {...defaultProps} onSelect={onSelect} />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Click to select a tool
|
|
fireEvent.click(screen.getByTestId('select-tool-btn'))
|
|
|
|
// Verify onSelect was called with tool value
|
|
expect(onSelect).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
provider_name: expect.any(String),
|
|
tool_name: expect.any(String),
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('should complete full multiple tool selection flow', async () => {
|
|
const onSelectMultiple = vi.fn()
|
|
render(
|
|
<ToolSelector {...defaultProps} onSelectMultiple={onSelectMultiple} />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Click to select multiple tools
|
|
fireEvent.click(screen.getByTestId('select-multiple-btn'))
|
|
|
|
// Verify onSelectMultiple was called
|
|
expect(onSelectMultiple).toHaveBeenCalledWith(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
provider_name: expect.any(String),
|
|
}),
|
|
]),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('Full Flow: Description Update', () => {
|
|
it('should update description through the form', async () => {
|
|
const onSelect = vi.fn()
|
|
const value = createToolValue()
|
|
|
|
render(
|
|
<ToolSelector {...defaultProps} value={value} onSelect={onSelect} />,
|
|
{ wrapper: createWrapper() },
|
|
)
|
|
|
|
// Find and change the description textarea
|
|
const textarea = screen.getByRole('textbox')
|
|
fireEvent.change(textarea, { target: { value: 'Updated description' } })
|
|
|
|
// Verify onSelect was called with updated description
|
|
expect(onSelect).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
extra: expect.objectContaining({
|
|
description: 'Updated description',
|
|
}),
|
|
}),
|
|
)
|
|
})
|
|
})
|
|
})
|