mirror of
https://github.com/langgenius/dify.git
synced 2026-02-09 23:20:12 -05:00
test: create some hooks and utils test script, modified clipboard test script (#27928)
This commit is contained in:
@@ -1,3 +1,13 @@
|
||||
/**
|
||||
* Test suite for clipboard utilities
|
||||
*
|
||||
* This module provides cross-browser clipboard functionality with automatic fallback:
|
||||
* 1. Modern Clipboard API (navigator.clipboard.writeText) - preferred method
|
||||
* 2. Legacy execCommand('copy') - fallback for older browsers
|
||||
*
|
||||
* The implementation ensures clipboard operations work across all supported browsers
|
||||
* while gracefully handling permissions and API availability.
|
||||
*/
|
||||
import { writeTextToClipboard } from './clipboard'
|
||||
|
||||
describe('Clipboard Utilities', () => {
|
||||
@@ -6,6 +16,10 @@ describe('Clipboard Utilities', () => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
/**
|
||||
* Test modern Clipboard API usage
|
||||
* When navigator.clipboard is available, should use the modern API
|
||||
*/
|
||||
it('should use navigator.clipboard.writeText when available', async () => {
|
||||
const mockWriteText = jest.fn().mockResolvedValue(undefined)
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
@@ -18,6 +32,11 @@ describe('Clipboard Utilities', () => {
|
||||
expect(mockWriteText).toHaveBeenCalledWith('test text')
|
||||
})
|
||||
|
||||
/**
|
||||
* Test fallback to legacy execCommand method
|
||||
* When Clipboard API is unavailable, should use document.execCommand('copy')
|
||||
* This involves creating a temporary textarea element
|
||||
*/
|
||||
it('should fallback to execCommand when clipboard API not available', async () => {
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: undefined,
|
||||
@@ -38,6 +57,10 @@ describe('Clipboard Utilities', () => {
|
||||
expect(removeChildSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
/**
|
||||
* Test error handling when execCommand returns false
|
||||
* execCommand returns false when the operation fails
|
||||
*/
|
||||
it('should handle execCommand failure', async () => {
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: undefined,
|
||||
@@ -51,6 +74,10 @@ describe('Clipboard Utilities', () => {
|
||||
await expect(writeTextToClipboard('fail text')).rejects.toThrow()
|
||||
})
|
||||
|
||||
/**
|
||||
* Test error handling when execCommand throws an exception
|
||||
* Should propagate the error to the caller
|
||||
*/
|
||||
it('should handle execCommand exception', async () => {
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: undefined,
|
||||
@@ -66,6 +93,10 @@ describe('Clipboard Utilities', () => {
|
||||
await expect(writeTextToClipboard('error text')).rejects.toThrow('execCommand error')
|
||||
})
|
||||
|
||||
/**
|
||||
* Test proper cleanup of temporary DOM elements
|
||||
* The temporary textarea should be removed after copying
|
||||
*/
|
||||
it('should clean up textarea after fallback', async () => {
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: undefined,
|
||||
@@ -81,6 +112,10 @@ describe('Clipboard Utilities', () => {
|
||||
expect(removeChildSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
/**
|
||||
* Test copying empty strings
|
||||
* Should handle edge case of empty clipboard content
|
||||
*/
|
||||
it('should handle empty string', async () => {
|
||||
const mockWriteText = jest.fn().mockResolvedValue(undefined)
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
@@ -93,6 +128,10 @@ describe('Clipboard Utilities', () => {
|
||||
expect(mockWriteText).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
/**
|
||||
* Test copying text with special characters
|
||||
* Should preserve newlines, tabs, quotes, unicode, and emojis
|
||||
*/
|
||||
it('should handle special characters', async () => {
|
||||
const mockWriteText = jest.fn().mockResolvedValue(undefined)
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
|
||||
253
web/utils/context.spec.ts
Normal file
253
web/utils/context.spec.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Test suite for React context creation utilities
|
||||
*
|
||||
* This module provides helper functions to create React contexts with better type safety
|
||||
* and automatic error handling when context is used outside of its provider.
|
||||
*
|
||||
* Two variants are provided:
|
||||
* - createCtx: Standard React context using useContext/createContext
|
||||
* - createSelectorCtx: Context with selector support using use-context-selector library
|
||||
*/
|
||||
import React from 'react'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { createCtx, createSelectorCtx } from './context'
|
||||
|
||||
describe('Context Utilities', () => {
|
||||
describe('createCtx', () => {
|
||||
/**
|
||||
* Test that createCtx creates a valid context with provider and hook
|
||||
* The function should return a tuple with [Provider, useContextValue, Context]
|
||||
* plus named properties for easier access
|
||||
*/
|
||||
it('should create context with provider and hook', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const [Provider, useTestContext, Context] = createCtx<TestContextValue>({
|
||||
name: 'Test',
|
||||
})
|
||||
|
||||
expect(Provider).toBeDefined()
|
||||
expect(useTestContext).toBeDefined()
|
||||
expect(Context).toBeDefined()
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that the context hook returns the provided value correctly
|
||||
* when used within the context provider
|
||||
*/
|
||||
it('should provide and consume context value', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const [Provider, useTestContext] = createCtx<TestContextValue>({
|
||||
name: 'Test',
|
||||
})
|
||||
|
||||
const testValue = { value: 'test-value' }
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
||||
React.createElement(Provider, { value: testValue }, children)
|
||||
|
||||
const { result } = renderHook(() => useTestContext(), { wrapper })
|
||||
|
||||
expect(result.current).toEqual(testValue)
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that accessing context outside of provider throws an error
|
||||
* This ensures developers are notified when they forget to wrap components
|
||||
*/
|
||||
it('should throw error when used outside provider', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const [, useTestContext] = createCtx<TestContextValue>({
|
||||
name: 'Test',
|
||||
})
|
||||
|
||||
// Suppress console.error for this test
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => { /* suppress error */ })
|
||||
|
||||
expect(() => {
|
||||
renderHook(() => useTestContext())
|
||||
}).toThrow('No Test context found.')
|
||||
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that context works with default values
|
||||
* When a default value is provided, it should be accessible without a provider
|
||||
*/
|
||||
it('should use default value when provided', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const defaultValue = { value: 'default' }
|
||||
const [, useTestContext] = createCtx<TestContextValue>({
|
||||
name: 'Test',
|
||||
defaultValue,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useTestContext())
|
||||
|
||||
expect(result.current).toEqual(defaultValue)
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that the returned tuple has named properties for convenience
|
||||
* This allows destructuring or property access based on preference
|
||||
*/
|
||||
it('should expose named properties', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const result = createCtx<TestContextValue>({ name: 'Test' })
|
||||
|
||||
expect(result.provider).toBe(result[0])
|
||||
expect(result.useContextValue).toBe(result[1])
|
||||
expect(result.context).toBe(result[2])
|
||||
})
|
||||
|
||||
/**
|
||||
* Test context with complex data types
|
||||
* Ensures type safety is maintained with nested objects and arrays
|
||||
*/
|
||||
it('should handle complex context values', () => {
|
||||
type ComplexContext = {
|
||||
user: { id: string; name: string }
|
||||
settings: { theme: string; locale: string }
|
||||
actions: Array<() => void>
|
||||
}
|
||||
|
||||
const [Provider, useComplexContext] = createCtx<ComplexContext>({
|
||||
name: 'Complex',
|
||||
})
|
||||
|
||||
const complexValue: ComplexContext = {
|
||||
user: { id: '123', name: 'Test User' },
|
||||
settings: { theme: 'dark', locale: 'en-US' },
|
||||
actions: [
|
||||
() => { /* empty action 1 */ },
|
||||
() => { /* empty action 2 */ },
|
||||
],
|
||||
}
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
||||
React.createElement(Provider, { value: complexValue }, children)
|
||||
|
||||
const { result } = renderHook(() => useComplexContext(), { wrapper })
|
||||
|
||||
expect(result.current).toEqual(complexValue)
|
||||
expect(result.current.user.id).toBe('123')
|
||||
expect(result.current.settings.theme).toBe('dark')
|
||||
expect(result.current.actions).toHaveLength(2)
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that context updates propagate to consumers
|
||||
* When provider value changes, hooks should receive the new value
|
||||
*/
|
||||
it('should update when context value changes', () => {
|
||||
type TestContextValue = { count: number }
|
||||
const [Provider, useTestContext] = createCtx<TestContextValue>({
|
||||
name: 'Test',
|
||||
})
|
||||
|
||||
let value = { count: 0 }
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
||||
React.createElement(Provider, { value }, children)
|
||||
|
||||
const { result, rerender } = renderHook(() => useTestContext(), { wrapper })
|
||||
|
||||
expect(result.current.count).toBe(0)
|
||||
|
||||
value = { count: 5 }
|
||||
rerender()
|
||||
|
||||
expect(result.current.count).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createSelectorCtx', () => {
|
||||
/**
|
||||
* Test that createSelectorCtx creates a valid context with selector support
|
||||
* This variant uses use-context-selector for optimized re-renders
|
||||
*/
|
||||
it('should create selector context with provider and hook', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const [Provider, useTestContext, Context] = createSelectorCtx<TestContextValue>({
|
||||
name: 'SelectorTest',
|
||||
})
|
||||
|
||||
expect(Provider).toBeDefined()
|
||||
expect(useTestContext).toBeDefined()
|
||||
expect(Context).toBeDefined()
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that selector context provides and consumes values correctly
|
||||
* The API should be identical to createCtx for basic usage
|
||||
*/
|
||||
it('should provide and consume context value with selector', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const [Provider, useTestContext] = createSelectorCtx<TestContextValue>({
|
||||
name: 'SelectorTest',
|
||||
})
|
||||
|
||||
const testValue = { value: 'selector-test' }
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
||||
React.createElement(Provider, { value: testValue }, children)
|
||||
|
||||
const { result } = renderHook(() => useTestContext(), { wrapper })
|
||||
|
||||
expect(result.current).toEqual(testValue)
|
||||
})
|
||||
|
||||
/**
|
||||
* Test error handling for selector context
|
||||
* Should throw error when used outside provider, same as createCtx
|
||||
*/
|
||||
it('should throw error when used outside provider', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const [, useTestContext] = createSelectorCtx<TestContextValue>({
|
||||
name: 'SelectorTest',
|
||||
})
|
||||
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => { /* suppress error */ })
|
||||
|
||||
expect(() => {
|
||||
renderHook(() => useTestContext())
|
||||
}).toThrow('No SelectorTest context found.')
|
||||
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that selector context works with default values
|
||||
*/
|
||||
it('should use default value when provided', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const defaultValue = { value: 'selector-default' }
|
||||
const [, useTestContext] = createSelectorCtx<TestContextValue>({
|
||||
name: 'SelectorTest',
|
||||
defaultValue,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useTestContext())
|
||||
|
||||
expect(result.current).toEqual(defaultValue)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Context without name', () => {
|
||||
/**
|
||||
* Test that contexts can be created without a name
|
||||
* The error message should use a generic fallback
|
||||
*/
|
||||
it('should create context without name and show generic error', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const [, useTestContext] = createCtx<TestContextValue>()
|
||||
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => { /* suppress error */ })
|
||||
|
||||
expect(() => {
|
||||
renderHook(() => useTestContext())
|
||||
}).toThrow('No related context found.')
|
||||
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
})
|
||||
})
|
||||
819
web/utils/model-config.spec.ts
Normal file
819
web/utils/model-config.spec.ts
Normal file
@@ -0,0 +1,819 @@
|
||||
/**
|
||||
* Test suite for model configuration transformation utilities
|
||||
*
|
||||
* This module handles the conversion between two different representations of user input forms:
|
||||
* 1. UserInputFormItem: The form structure used in the UI
|
||||
* 2. PromptVariable: The variable structure used in prompts and model configuration
|
||||
*
|
||||
* Key functions:
|
||||
* - userInputsFormToPromptVariables: Converts UI form items to prompt variables
|
||||
* - promptVariablesToUserInputsForm: Converts prompt variables back to form items
|
||||
* - formatBooleanInputs: Ensures boolean inputs are properly typed
|
||||
*/
|
||||
import {
|
||||
formatBooleanInputs,
|
||||
promptVariablesToUserInputsForm,
|
||||
userInputsFormToPromptVariables,
|
||||
} from './model-config'
|
||||
import type { UserInputFormItem } from '@/types/app'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
|
||||
describe('Model Config Utilities', () => {
|
||||
describe('userInputsFormToPromptVariables', () => {
|
||||
/**
|
||||
* Test handling of null or undefined input
|
||||
* Should return empty array when no inputs provided
|
||||
*/
|
||||
it('should return empty array for null input', () => {
|
||||
const result = userInputsFormToPromptVariables(null)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
/**
|
||||
* Test conversion of text-input (string) type
|
||||
* Text inputs are the most common form field type
|
||||
*/
|
||||
it('should convert text-input to string prompt variable', () => {
|
||||
const userInputs: UserInputFormItem[] = [
|
||||
{
|
||||
'text-input': {
|
||||
label: 'User Name',
|
||||
variable: 'user_name',
|
||||
required: true,
|
||||
max_length: 100,
|
||||
default: '',
|
||||
hide: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const result = userInputsFormToPromptVariables(userInputs)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toEqual({
|
||||
key: 'user_name',
|
||||
name: 'User Name',
|
||||
required: true,
|
||||
type: 'string',
|
||||
max_length: 100,
|
||||
options: [],
|
||||
is_context_var: false,
|
||||
hide: false,
|
||||
default: '',
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test conversion of paragraph type
|
||||
* Paragraphs are multi-line text inputs
|
||||
*/
|
||||
it('should convert paragraph to paragraph prompt variable', () => {
|
||||
const userInputs: UserInputFormItem[] = [
|
||||
{
|
||||
paragraph: {
|
||||
label: 'Description',
|
||||
variable: 'description',
|
||||
required: false,
|
||||
max_length: 500,
|
||||
default: '',
|
||||
hide: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const result = userInputsFormToPromptVariables(userInputs)
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
key: 'description',
|
||||
name: 'Description',
|
||||
required: false,
|
||||
type: 'paragraph',
|
||||
max_length: 500,
|
||||
options: [],
|
||||
is_context_var: false,
|
||||
hide: false,
|
||||
default: '',
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test conversion of number type
|
||||
* Number inputs should preserve numeric constraints
|
||||
*/
|
||||
it('should convert number input to number prompt variable', () => {
|
||||
const userInputs: UserInputFormItem[] = [
|
||||
{
|
||||
number: {
|
||||
label: 'Age',
|
||||
variable: 'age',
|
||||
required: true,
|
||||
default: '',
|
||||
hide: false,
|
||||
},
|
||||
} as any,
|
||||
]
|
||||
|
||||
const result = userInputsFormToPromptVariables(userInputs)
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
key: 'age',
|
||||
name: 'Age',
|
||||
required: true,
|
||||
type: 'number',
|
||||
options: [],
|
||||
hide: false,
|
||||
default: '',
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test conversion of checkbox (boolean) type
|
||||
* Checkboxes are converted to 'checkbox' type in prompt variables
|
||||
*/
|
||||
it('should convert checkbox to checkbox prompt variable', () => {
|
||||
const userInputs: UserInputFormItem[] = [
|
||||
{
|
||||
checkbox: {
|
||||
label: 'Accept Terms',
|
||||
variable: 'accept_terms',
|
||||
required: true,
|
||||
default: '',
|
||||
hide: false,
|
||||
},
|
||||
} as any,
|
||||
]
|
||||
|
||||
const result = userInputsFormToPromptVariables(userInputs)
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
key: 'accept_terms',
|
||||
name: 'Accept Terms',
|
||||
required: true,
|
||||
type: 'checkbox',
|
||||
options: [],
|
||||
hide: false,
|
||||
default: '',
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test conversion of select (dropdown) type
|
||||
* Select inputs include options array
|
||||
*/
|
||||
it('should convert select input to select prompt variable', () => {
|
||||
const userInputs: UserInputFormItem[] = [
|
||||
{
|
||||
select: {
|
||||
label: 'Country',
|
||||
variable: 'country',
|
||||
required: true,
|
||||
options: ['USA', 'Canada', 'Mexico'],
|
||||
default: 'USA',
|
||||
hide: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const result = userInputsFormToPromptVariables(userInputs)
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
key: 'country',
|
||||
name: 'Country',
|
||||
required: true,
|
||||
type: 'select',
|
||||
options: ['USA', 'Canada', 'Mexico'],
|
||||
is_context_var: false,
|
||||
hide: false,
|
||||
default: 'USA',
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test conversion of file upload type
|
||||
* File inputs include configuration for allowed types and upload methods
|
||||
*/
|
||||
it('should convert file input to file prompt variable', () => {
|
||||
const userInputs: UserInputFormItem[] = [
|
||||
{
|
||||
file: {
|
||||
label: 'Profile Picture',
|
||||
variable: 'profile_pic',
|
||||
required: false,
|
||||
allowed_file_types: ['image'],
|
||||
allowed_file_extensions: ['.jpg', '.png'],
|
||||
allowed_file_upload_methods: ['local_file', 'remote_url'],
|
||||
default: '',
|
||||
hide: false,
|
||||
},
|
||||
} as any,
|
||||
]
|
||||
|
||||
const result = userInputsFormToPromptVariables(userInputs)
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
key: 'profile_pic',
|
||||
name: 'Profile Picture',
|
||||
required: false,
|
||||
type: 'file',
|
||||
config: {
|
||||
allowed_file_types: ['image'],
|
||||
allowed_file_extensions: ['.jpg', '.png'],
|
||||
allowed_file_upload_methods: ['local_file', 'remote_url'],
|
||||
number_limits: 1,
|
||||
},
|
||||
hide: false,
|
||||
default: '',
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test conversion of file-list type
|
||||
* File lists allow multiple file uploads with a max_length constraint
|
||||
*/
|
||||
it('should convert file-list input to file-list prompt variable', () => {
|
||||
const userInputs: UserInputFormItem[] = [
|
||||
{
|
||||
'file-list': {
|
||||
label: 'Documents',
|
||||
variable: 'documents',
|
||||
required: true,
|
||||
allowed_file_types: ['document'],
|
||||
allowed_file_extensions: ['.pdf', '.docx'],
|
||||
allowed_file_upload_methods: ['local_file'],
|
||||
max_length: 5,
|
||||
default: '',
|
||||
hide: false,
|
||||
},
|
||||
} as any,
|
||||
]
|
||||
|
||||
const result = userInputsFormToPromptVariables(userInputs)
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
key: 'documents',
|
||||
name: 'Documents',
|
||||
required: true,
|
||||
type: 'file-list',
|
||||
config: {
|
||||
allowed_file_types: ['document'],
|
||||
allowed_file_extensions: ['.pdf', '.docx'],
|
||||
allowed_file_upload_methods: ['local_file'],
|
||||
number_limits: 5,
|
||||
},
|
||||
hide: false,
|
||||
default: '',
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test conversion of external_data_tool type
|
||||
* External data tools have custom configuration and icons
|
||||
*/
|
||||
it('should convert external_data_tool to prompt variable', () => {
|
||||
const userInputs: UserInputFormItem[] = [
|
||||
{
|
||||
external_data_tool: {
|
||||
label: 'API Data',
|
||||
variable: 'api_data',
|
||||
type: 'api',
|
||||
enabled: true,
|
||||
required: false,
|
||||
config: { endpoint: 'https://api.example.com' },
|
||||
icon: 'api-icon',
|
||||
icon_background: '#FF5733',
|
||||
hide: false,
|
||||
},
|
||||
} as any,
|
||||
]
|
||||
|
||||
const result = userInputsFormToPromptVariables(userInputs)
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
key: 'api_data',
|
||||
name: 'API Data',
|
||||
required: false,
|
||||
type: 'api',
|
||||
enabled: true,
|
||||
config: { endpoint: 'https://api.example.com' },
|
||||
icon: 'api-icon',
|
||||
icon_background: '#FF5733',
|
||||
is_context_var: false,
|
||||
hide: false,
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test handling of dataset_query_variable
|
||||
* When a variable matches the dataset_query_variable, is_context_var should be true
|
||||
*/
|
||||
it('should mark variable as context var when matching dataset_query_variable', () => {
|
||||
const userInputs: UserInputFormItem[] = [
|
||||
{
|
||||
'text-input': {
|
||||
label: 'Query',
|
||||
variable: 'query',
|
||||
required: true,
|
||||
max_length: 200,
|
||||
default: '',
|
||||
hide: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const result = userInputsFormToPromptVariables(userInputs, 'query')
|
||||
|
||||
expect(result[0].is_context_var).toBe(true)
|
||||
})
|
||||
|
||||
/**
|
||||
* Test conversion of multiple mixed input types
|
||||
* Should handle an array with different input types correctly
|
||||
*/
|
||||
it('should convert multiple mixed input types', () => {
|
||||
const userInputs: UserInputFormItem[] = [
|
||||
{
|
||||
'text-input': {
|
||||
label: 'Name',
|
||||
variable: 'name',
|
||||
required: true,
|
||||
max_length: 50,
|
||||
default: '',
|
||||
hide: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
number: {
|
||||
label: 'Age',
|
||||
variable: 'age',
|
||||
required: false,
|
||||
default: '',
|
||||
hide: false,
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
select: {
|
||||
label: 'Gender',
|
||||
variable: 'gender',
|
||||
required: true,
|
||||
options: ['Male', 'Female', 'Other'],
|
||||
default: '',
|
||||
hide: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const result = userInputsFormToPromptVariables(userInputs)
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0].type).toBe('string')
|
||||
expect(result[1].type).toBe('number')
|
||||
expect(result[2].type).toBe('select')
|
||||
})
|
||||
})
|
||||
|
||||
describe('promptVariablesToUserInputsForm', () => {
|
||||
/**
|
||||
* Test conversion of string prompt variable back to text-input
|
||||
*/
|
||||
it('should convert string prompt variable to text-input', () => {
|
||||
const promptVariables: PromptVariable[] = [
|
||||
{
|
||||
key: 'user_name',
|
||||
name: 'User Name',
|
||||
required: true,
|
||||
type: 'string',
|
||||
max_length: 100,
|
||||
options: [],
|
||||
},
|
||||
]
|
||||
|
||||
const result = promptVariablesToUserInputsForm(promptVariables)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toEqual({
|
||||
'text-input': {
|
||||
label: 'User Name',
|
||||
variable: 'user_name',
|
||||
required: true,
|
||||
max_length: 100,
|
||||
default: '',
|
||||
hide: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test conversion of paragraph prompt variable
|
||||
*/
|
||||
it('should convert paragraph prompt variable to paragraph input', () => {
|
||||
const promptVariables: PromptVariable[] = [
|
||||
{
|
||||
key: 'description',
|
||||
name: 'Description',
|
||||
required: false,
|
||||
type: 'paragraph',
|
||||
max_length: 500,
|
||||
options: [],
|
||||
},
|
||||
]
|
||||
|
||||
const result = promptVariablesToUserInputsForm(promptVariables)
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
paragraph: {
|
||||
label: 'Description',
|
||||
variable: 'description',
|
||||
required: false,
|
||||
max_length: 500,
|
||||
default: '',
|
||||
hide: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test conversion of number prompt variable
|
||||
*/
|
||||
it('should convert number prompt variable to number input', () => {
|
||||
const promptVariables: PromptVariable[] = [
|
||||
{
|
||||
key: 'age',
|
||||
name: 'Age',
|
||||
required: true,
|
||||
type: 'number',
|
||||
options: [],
|
||||
},
|
||||
]
|
||||
|
||||
const result = promptVariablesToUserInputsForm(promptVariables)
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
number: {
|
||||
label: 'Age',
|
||||
variable: 'age',
|
||||
required: true,
|
||||
default: '',
|
||||
hide: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test conversion of checkbox prompt variable
|
||||
*/
|
||||
it('should convert checkbox prompt variable to checkbox input', () => {
|
||||
const promptVariables: PromptVariable[] = [
|
||||
{
|
||||
key: 'accept_terms',
|
||||
name: 'Accept Terms',
|
||||
required: true,
|
||||
type: 'checkbox',
|
||||
options: [],
|
||||
},
|
||||
]
|
||||
|
||||
const result = promptVariablesToUserInputsForm(promptVariables)
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
checkbox: {
|
||||
label: 'Accept Terms',
|
||||
variable: 'accept_terms',
|
||||
required: true,
|
||||
default: '',
|
||||
hide: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test conversion of select prompt variable
|
||||
*/
|
||||
it('should convert select prompt variable to select input', () => {
|
||||
const promptVariables: PromptVariable[] = [
|
||||
{
|
||||
key: 'country',
|
||||
name: 'Country',
|
||||
required: true,
|
||||
type: 'select',
|
||||
options: ['USA', 'Canada', 'Mexico'],
|
||||
default: 'USA',
|
||||
},
|
||||
]
|
||||
|
||||
const result = promptVariablesToUserInputsForm(promptVariables)
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
select: {
|
||||
label: 'Country',
|
||||
variable: 'country',
|
||||
required: true,
|
||||
options: ['USA', 'Canada', 'Mexico'],
|
||||
default: 'USA',
|
||||
hide: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test filtering of invalid prompt variables
|
||||
* Variables without key or name should be filtered out
|
||||
*/
|
||||
it('should filter out variables with empty key or name', () => {
|
||||
const promptVariables: PromptVariable[] = [
|
||||
{
|
||||
key: '',
|
||||
name: 'Empty Key',
|
||||
required: true,
|
||||
type: 'string',
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
key: 'valid',
|
||||
name: '',
|
||||
required: true,
|
||||
type: 'string',
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
key: ' ',
|
||||
name: 'Whitespace Key',
|
||||
required: true,
|
||||
type: 'string',
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
key: 'valid_key',
|
||||
name: 'Valid Name',
|
||||
required: true,
|
||||
type: 'string',
|
||||
options: [],
|
||||
},
|
||||
]
|
||||
|
||||
const result = promptVariablesToUserInputsForm(promptVariables)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect((result[0] as any)['text-input']?.variable).toBe('valid_key')
|
||||
})
|
||||
|
||||
/**
|
||||
* Test conversion of external data tool prompt variable
|
||||
*/
|
||||
it('should convert external data tool prompt variable', () => {
|
||||
const promptVariables: PromptVariable[] = [
|
||||
{
|
||||
key: 'api_data',
|
||||
name: 'API Data',
|
||||
required: false,
|
||||
type: 'api',
|
||||
enabled: true,
|
||||
config: { endpoint: 'https://api.example.com' },
|
||||
icon: 'api-icon',
|
||||
icon_background: '#FF5733',
|
||||
},
|
||||
]
|
||||
|
||||
const result = promptVariablesToUserInputsForm(promptVariables)
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
external_data_tool: {
|
||||
label: 'API Data',
|
||||
variable: 'api_data',
|
||||
enabled: true,
|
||||
type: 'api',
|
||||
config: { endpoint: 'https://api.example.com' },
|
||||
required: false,
|
||||
icon: 'api-icon',
|
||||
icon_background: '#FF5733',
|
||||
hide: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that required defaults to true when not explicitly set to false
|
||||
*/
|
||||
it('should default required to true when not false', () => {
|
||||
const promptVariables: PromptVariable[] = [
|
||||
{
|
||||
key: 'test1',
|
||||
name: 'Test 1',
|
||||
required: undefined,
|
||||
type: 'string',
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
key: 'test2',
|
||||
name: 'Test 2',
|
||||
required: false,
|
||||
type: 'string',
|
||||
options: [],
|
||||
},
|
||||
]
|
||||
|
||||
const result = promptVariablesToUserInputsForm(promptVariables)
|
||||
|
||||
expect((result[0] as any)['text-input']?.required).toBe(true)
|
||||
expect((result[1] as any)['text-input']?.required).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatBooleanInputs', () => {
|
||||
/**
|
||||
* Test that null or undefined inputs are handled gracefully
|
||||
*/
|
||||
it('should return inputs unchanged when useInputs is null', () => {
|
||||
const inputs = { key1: 'value1', key2: 'value2' }
|
||||
const result = formatBooleanInputs(null, inputs)
|
||||
expect(result).toEqual(inputs)
|
||||
})
|
||||
|
||||
it('should return inputs unchanged when useInputs is undefined', () => {
|
||||
const inputs = { key1: 'value1', key2: 'value2' }
|
||||
const result = formatBooleanInputs(undefined, inputs)
|
||||
expect(result).toEqual(inputs)
|
||||
})
|
||||
|
||||
/**
|
||||
* Test conversion of boolean input values to actual boolean type
|
||||
* This is important for proper type handling in the backend
|
||||
* Note: checkbox inputs are converted to type 'checkbox' by userInputsFormToPromptVariables
|
||||
*/
|
||||
it('should convert boolean inputs to boolean type', () => {
|
||||
const useInputs: PromptVariable[] = [
|
||||
{
|
||||
key: 'accept_terms',
|
||||
name: 'Accept Terms',
|
||||
required: true,
|
||||
type: 'checkbox',
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
key: 'subscribe',
|
||||
name: 'Subscribe',
|
||||
required: false,
|
||||
type: 'checkbox',
|
||||
options: [],
|
||||
},
|
||||
]
|
||||
|
||||
const inputs = {
|
||||
accept_terms: 'true',
|
||||
subscribe: '',
|
||||
other_field: 'value',
|
||||
}
|
||||
|
||||
const result = formatBooleanInputs(useInputs, inputs)
|
||||
|
||||
expect(result).toEqual({
|
||||
accept_terms: true,
|
||||
subscribe: false,
|
||||
other_field: 'value',
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that non-boolean inputs are not affected
|
||||
*/
|
||||
it('should not modify non-boolean inputs', () => {
|
||||
const useInputs: PromptVariable[] = [
|
||||
{
|
||||
key: 'name',
|
||||
name: 'Name',
|
||||
required: true,
|
||||
type: 'string',
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
key: 'age',
|
||||
name: 'Age',
|
||||
required: true,
|
||||
type: 'number',
|
||||
options: [],
|
||||
},
|
||||
]
|
||||
|
||||
const inputs = {
|
||||
name: 'John Doe',
|
||||
age: 30,
|
||||
}
|
||||
|
||||
const result = formatBooleanInputs(useInputs, inputs)
|
||||
|
||||
expect(result).toEqual(inputs)
|
||||
})
|
||||
|
||||
/**
|
||||
* Test handling of truthy and falsy values for boolean conversion
|
||||
* Note: checkbox inputs are converted to type 'checkbox' by userInputsFormToPromptVariables
|
||||
*/
|
||||
it('should handle various truthy and falsy values', () => {
|
||||
const useInputs: PromptVariable[] = [
|
||||
{
|
||||
key: 'bool1',
|
||||
name: 'Bool 1',
|
||||
required: true,
|
||||
type: 'checkbox',
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
key: 'bool2',
|
||||
name: 'Bool 2',
|
||||
required: true,
|
||||
type: 'checkbox',
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
key: 'bool3',
|
||||
name: 'Bool 3',
|
||||
required: true,
|
||||
type: 'checkbox',
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
key: 'bool4',
|
||||
name: 'Bool 4',
|
||||
required: true,
|
||||
type: 'checkbox',
|
||||
options: [],
|
||||
},
|
||||
]
|
||||
|
||||
const inputs = {
|
||||
bool1: 1,
|
||||
bool2: 0,
|
||||
bool3: 'yes',
|
||||
bool4: null as any,
|
||||
}
|
||||
|
||||
const result = formatBooleanInputs(useInputs, inputs)
|
||||
|
||||
expect(result?.bool1).toBe(true)
|
||||
expect(result?.bool2).toBe(false)
|
||||
expect(result?.bool3).toBe(true)
|
||||
expect(result?.bool4).toBe(false)
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that the function creates a new object and doesn't mutate the original
|
||||
* Note: checkbox inputs are converted to type 'checkbox' by userInputsFormToPromptVariables
|
||||
*/
|
||||
it('should not mutate original inputs object', () => {
|
||||
const useInputs: PromptVariable[] = [
|
||||
{
|
||||
key: 'flag',
|
||||
name: 'Flag',
|
||||
required: true,
|
||||
type: 'checkbox',
|
||||
options: [],
|
||||
},
|
||||
]
|
||||
|
||||
const inputs = { flag: 'true', other: 'value' }
|
||||
const originalInputs = { ...inputs }
|
||||
|
||||
formatBooleanInputs(useInputs, inputs)
|
||||
|
||||
expect(inputs).toEqual(originalInputs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Round-trip conversion', () => {
|
||||
/**
|
||||
* Test that converting from UserInputForm to PromptVariable and back
|
||||
* preserves the essential data (though some fields may have defaults applied)
|
||||
*/
|
||||
it('should preserve data through round-trip conversion', () => {
|
||||
const originalUserInputs: UserInputFormItem[] = [
|
||||
{
|
||||
'text-input': {
|
||||
label: 'Name',
|
||||
variable: 'name',
|
||||
required: true,
|
||||
max_length: 50,
|
||||
default: '',
|
||||
hide: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
select: {
|
||||
label: 'Type',
|
||||
variable: 'type',
|
||||
required: false,
|
||||
options: ['A', 'B', 'C'],
|
||||
default: 'A',
|
||||
hide: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const promptVars = userInputsFormToPromptVariables(originalUserInputs)
|
||||
const backToUserInputs = promptVariablesToUserInputsForm(promptVars)
|
||||
|
||||
expect(backToUserInputs).toHaveLength(2)
|
||||
expect((backToUserInputs[0] as any)['text-input']?.variable).toBe('name')
|
||||
expect((backToUserInputs[1] as any).select?.variable).toBe('type')
|
||||
expect((backToUserInputs[1] as any).select?.options).toEqual(['A', 'B', 'C'])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -200,7 +200,7 @@ export const formatBooleanInputs = (useInputs?: PromptVariable[] | null, inputs?
|
||||
return inputs
|
||||
const res = { ...inputs }
|
||||
useInputs.forEach((item) => {
|
||||
const isBooleanInput = item.type === 'boolean'
|
||||
const isBooleanInput = item.type === 'checkbox'
|
||||
if (isBooleanInput) {
|
||||
// Convert boolean inputs to boolean type
|
||||
res[item.key] = !!res[item.key]
|
||||
|
||||
Reference in New Issue
Block a user