From ce2403e0db4e1cb809efd4f5f7943908789e6a7d Mon Sep 17 00:00:00 2001 From: CodingOnStar Date: Mon, 30 Mar 2026 14:42:17 +0800 Subject: [PATCH] test: add unit tests for app store and configuration components, enhancing coverage for state management and UI interactions --- .../components/app/__tests__/store.spec.ts | 84 ++ .../configuration/__tests__/index.spec.tsx | 225 ++++ .../components/configuration-debug-panel.tsx | 22 + .../configuration-debug-panel.utils.ts | 10 + .../configuration-header-actions.tsx | 61 + .../components/configuration-modals.tsx | 142 +++ .../agent/__tests__/prompt-editor.spec.tsx | 160 +++ .../__tests__/get-code-generator-res.spec.tsx | 267 ++++ .../hooks/use-configuration-controller.ts | 735 +++++++++++ .../hooks/use-configuration-initializer.ts | 320 +++++ .../hooks/use-configuration-publish.ts | 238 ++++ .../components/app/configuration/index.tsx | 1093 +---------------- web/app/components/app/configuration/types.ts | 7 + .../base/ui/__tests__/placement.spec.ts | 25 + .../upgrade-modal/__tests__/index.spec.tsx | 61 + .../user-avatar-list/__tests__/index.spec.tsx | 59 + .../develop/__tests__/use-doc-toc.spec.ts | 425 ------- .../hooks/__tests__/use-doc-toc.spec.ts | 175 +++ .../trigger/__tests__/marketplace.spec.tsx | 83 ++ .../trigger/__tests__/tool-selector.spec.tsx | 67 + .../use-app-inputs-form-schema.spec.ts | 185 +++ .../detail-header/__tests__/index.spec.tsx | 244 ++++ .../components/__tests__/modal-steps.spec.tsx | 210 ++++ .../sandbox-migration-storage.spec.ts | 43 + 24 files changed, 3446 insertions(+), 1495 deletions(-) create mode 100644 web/app/components/app/__tests__/store.spec.ts create mode 100644 web/app/components/app/configuration/__tests__/index.spec.tsx create mode 100644 web/app/components/app/configuration/components/configuration-debug-panel.tsx create mode 100644 web/app/components/app/configuration/components/configuration-debug-panel.utils.ts create mode 100644 web/app/components/app/configuration/components/configuration-header-actions.tsx create mode 100644 web/app/components/app/configuration/components/configuration-modals.tsx create mode 100644 web/app/components/app/configuration/config/agent/__tests__/prompt-editor.spec.tsx create mode 100644 web/app/components/app/configuration/config/code-generator/__tests__/get-code-generator-res.spec.tsx create mode 100644 web/app/components/app/configuration/hooks/use-configuration-controller.ts create mode 100644 web/app/components/app/configuration/hooks/use-configuration-initializer.ts create mode 100644 web/app/components/app/configuration/hooks/use-configuration-publish.ts create mode 100644 web/app/components/app/configuration/types.ts create mode 100644 web/app/components/base/ui/__tests__/placement.spec.ts create mode 100644 web/app/components/base/upgrade-modal/__tests__/index.spec.tsx create mode 100644 web/app/components/base/user-avatar-list/__tests__/index.spec.tsx delete mode 100644 web/app/components/develop/__tests__/use-doc-toc.spec.ts create mode 100644 web/app/components/develop/hooks/__tests__/use-doc-toc.spec.ts create mode 100644 web/app/components/plugins/marketplace/search-box/trigger/__tests__/marketplace.spec.tsx create mode 100644 web/app/components/plugins/marketplace/search-box/trigger/__tests__/tool-selector.spec.tsx create mode 100644 web/app/components/plugins/plugin-detail-panel/app-selector/hooks/__tests__/use-app-inputs-form-schema.spec.ts create mode 100644 web/app/components/plugins/plugin-detail-panel/detail-header/__tests__/index.spec.tsx create mode 100644 web/app/components/plugins/plugin-detail-panel/subscription-list/create/components/__tests__/modal-steps.spec.tsx create mode 100644 web/app/components/workflow-app/utils/__tests__/sandbox-migration-storage.spec.ts diff --git a/web/app/components/app/__tests__/store.spec.ts b/web/app/components/app/__tests__/store.spec.ts new file mode 100644 index 0000000000..e71afd9576 --- /dev/null +++ b/web/app/components/app/__tests__/store.spec.ts @@ -0,0 +1,84 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { useStore } from '../store' + +type AppDetailState = Parameters['setAppDetail']>[0] +type CurrentLogItemState = Parameters['setCurrentLogItem']>[0] + +const resetStore = () => { + useStore.setState({ + appDetail: undefined, + appSidebarExpand: '', + currentLogItem: undefined, + currentLogModalActiveTab: 'DETAIL', + showPromptLogModal: false, + showAgentLogModal: false, + showMessageLogModal: false, + showAppConfigureFeaturesModal: false, + needsRuntimeUpgrade: false, + }) +} + +describe('app store', () => { + beforeEach(() => { + resetStore() + }) + + it('should update each primitive flag via dedicated setters', () => { + const store = useStore.getState() + + store.setAppSidebarExpand('collapse') + store.setShowPromptLogModal(true) + store.setShowAgentLogModal(true) + store.setShowAppConfigureFeaturesModal(true) + store.setNeedsRuntimeUpgrade(true) + + const nextState = useStore.getState() + expect(nextState.appSidebarExpand).toBe('collapse') + expect(nextState.showPromptLogModal).toBe(true) + expect(nextState.showAgentLogModal).toBe(true) + expect(nextState.showAppConfigureFeaturesModal).toBe(true) + expect(nextState.needsRuntimeUpgrade).toBe(true) + }) + + it('should store app detail and current log item', () => { + const appDetail = { id: 'app-1', name: 'Demo App' } as AppDetailState + const logItem: Exclude = { + id: 'log-1', + content: 'hello', + isAnswer: true, + } + const store = useStore.getState() + + store.setAppDetail(appDetail) + store.setCurrentLogItem(logItem) + store.setCurrentLogModalActiveTab('TRACES') + + const nextState = useStore.getState() + expect(nextState.appDetail).toEqual(appDetail) + expect(nextState.currentLogItem).toEqual(logItem) + expect(nextState.currentLogModalActiveTab).toBe('TRACES') + }) + + it('should preserve currentLogModalActiveTab when opening message log modal', () => { + useStore.setState({ currentLogModalActiveTab: 'AGENT' }) + + useStore.getState().setShowMessageLogModal(true) + + const nextState = useStore.getState() + expect(nextState.showMessageLogModal).toBe(true) + expect(nextState.currentLogModalActiveTab).toBe('AGENT') + }) + + it('should reset currentLogModalActiveTab to DETAIL when closing message log modal', () => { + useStore.setState({ + currentLogModalActiveTab: 'PROMPT', + showMessageLogModal: true, + }) + + useStore.getState().setShowMessageLogModal(false) + + const nextState = useStore.getState() + expect(nextState.showMessageLogModal).toBe(false) + expect(nextState.currentLogModalActiveTab).toBe('DETAIL') + }) +}) diff --git a/web/app/components/app/configuration/__tests__/index.spec.tsx b/web/app/components/app/configuration/__tests__/index.spec.tsx new file mode 100644 index 0000000000..b11bd149e4 --- /dev/null +++ b/web/app/components/app/configuration/__tests__/index.spec.tsx @@ -0,0 +1,225 @@ +import type { ReactNode } from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import { useContext } from 'use-context-selector' +import ConfigContext from '@/context/debug-configuration' +import Configuration from '../index' + +type ControllerState = ReturnType +type ContextValue = ControllerState['contextValue'] + +const mockUseConfigurationController = vi.fn() +const mockPublish = vi.fn() + +let latestDebugPanelProps: Record | undefined +let latestModalProps: Record | undefined +let latestFeatures: unknown + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('../hooks/use-configuration-controller', () => ({ + useConfigurationController: () => mockUseConfigurationController(), +})) + +vi.mock('@/app/components/base/features', () => ({ + FeaturesProvider: ({ children, features }: { children: ReactNode, features: unknown }) => { + latestFeatures = features + return
{children}
+ }, +})) + +vi.mock('@/context/mitt-context-provider', () => ({ + MittProvider: ({ children }: { children: ReactNode }) =>
{children}
, +})) + +vi.mock('@/app/components/base/loading', () => ({ + default: () =>
loading
, +})) + +vi.mock('@/app/components/app/configuration/config', () => ({ + default: function MockConfig() { + const context = useContext(ConfigContext) + return
{context.modelConfig.model_id}
+ }, +})) + +vi.mock('@/app/components/app/configuration/components/configuration-header-actions', () => ({ + default: ({ publisherProps }: { publisherProps: { publishDisabled: boolean, onPublish: () => void } }) => { + return ( +
+ +
+ ) + }, +})) + +vi.mock('@/app/components/app/configuration/components/configuration-debug-panel', () => ({ + default: (props: Record) => { + latestDebugPanelProps = props + return
debug-panel
+ }, +})) + +vi.mock('@/app/components/app/configuration/components/configuration-modals', () => ({ + default: (props: { isMobile?: boolean, isShowDebugPanel?: boolean }) => { + latestModalProps = props + return
+ }, +})) + +const createContextValue = (overrides: Partial = {}): ContextValue => ({ + setPromptMode: vi.fn(async () => {}), + isAdvancedMode: false, + modelConfig: { + provider: 'langgenius/openai/openai', + model_id: 'gpt-4o-mini', + mode: 'chat', + configs: { + prompt_template: '', + prompt_variables: [], + }, + chat_prompt_config: null, + completion_prompt_config: null, + more_like_this: null, + opening_statement: '', + suggested_questions: [], + sensitive_word_avoidance: null, + speech_to_text: null, + text_to_speech: null, + file_upload: null, + suggested_questions_after_answer: null, + retriever_resource: null, + annotation_reply: null, + external_data_tools: [], + system_parameters: { + audio_file_size_limit: 0, + file_size_limit: 0, + image_file_size_limit: 0, + video_file_size_limit: 0, + workflow_file_upload_limit: 0, + }, + dataSets: [], + agentConfig: { + enabled: false, + strategy: 'react', + max_iteration: 5, + tools: [], + }, + }, + ...overrides, +} as ContextValue) + +const createControllerState = (overrides: Partial = {}): ControllerState => ({ + currentWorkspaceId: 'workspace-id', + featuresData: { + opening: { enabled: true, opening_statement: 'hello', suggested_questions: [] }, + }, + contextValue: createContextValue(), + debugPanelProps: { + isAPIKeySet: true, + }, + headerActionsProps: { + publisherProps: { + publishDisabled: false, + onPublish: mockPublish, + debugWithMultipleModel: false, + }, + }, + isLoading: false, + isLoadingCurrentWorkspace: false, + isMobile: false, + modalProps: { + isMobile: false, + }, + ...overrides, +} as ControllerState) + +describe('Configuration', () => { + beforeEach(() => { + vi.clearAllMocks() + latestDebugPanelProps = undefined + latestModalProps = undefined + latestFeatures = undefined + mockUseConfigurationController.mockReturnValue(createControllerState()) + }) + + it('should show loading until workspace and detail initialization finish', () => { + mockUseConfigurationController.mockReturnValue(createControllerState({ + isLoading: true, + })) + + render() + + expect(screen.getByTestId('loading')).toBeInTheDocument() + expect(screen.queryByTestId('config')).not.toBeInTheDocument() + }) + + it('should render initialized desktop configuration and forward publish state', () => { + const baseState = createControllerState() + mockUseConfigurationController.mockReturnValue({ + ...baseState, + contextValue: { + ...baseState.contextValue, + isAdvancedMode: true, + modelConfig: { + ...baseState.contextValue.modelConfig, + model_id: 'gpt-4.1', + }, + }, + headerActionsProps: { + ...baseState.headerActionsProps, + publisherProps: { + ...baseState.headerActionsProps.publisherProps, + publishDisabled: true, + onPublish: mockPublish, + debugWithMultipleModel: true, + }, + }, + }) + + render() + + expect(screen.getByText('orchestrate')).toBeInTheDocument() + expect(screen.getByText('promptMode.advanced')).toBeInTheDocument() + expect(screen.getByTestId('config')).toHaveTextContent('gpt-4.1') + expect(screen.getByTestId('debug-panel')).toBeInTheDocument() + expect(screen.getByTestId('header-actions')).toHaveAttribute('data-disabled', 'true') + expect(latestFeatures).toEqual(expect.objectContaining({ + opening: expect.objectContaining({ enabled: true }), + })) + + fireEvent.click(screen.getByRole('button', { name: 'publish' })) + + expect(mockPublish).toHaveBeenCalledTimes(1) + expect(latestDebugPanelProps).toEqual(expect.objectContaining({ + isAPIKeySet: true, + })) + }) + + it('should switch to the mobile modal flow without rendering the desktop debug panel', () => { + const baseState = createControllerState() + mockUseConfigurationController.mockReturnValue({ + ...baseState, + isMobile: true, + modalProps: { + ...baseState.modalProps, + isMobile: true, + isShowDebugPanel: true, + }, + }) + + render() + + expect(screen.queryByTestId('debug-panel')).not.toBeInTheDocument() + expect(screen.getByTestId('configuration-modals')).toHaveAttribute('data-mobile', 'true') + expect(latestModalProps).toEqual(expect.objectContaining({ + isShowDebugPanel: true, + isMobile: true, + })) + }) +}) diff --git a/web/app/components/app/configuration/components/configuration-debug-panel.tsx b/web/app/components/app/configuration/components/configuration-debug-panel.tsx new file mode 100644 index 0000000000..674cc41338 --- /dev/null +++ b/web/app/components/app/configuration/components/configuration-debug-panel.tsx @@ -0,0 +1,22 @@ +import type { ComponentProps } from 'react' +import Debug from '@/app/components/app/configuration/debug' + +type DebugProps = ComponentProps + +type ConfigurationDebugPanelProps = Omit & { + onOpenModelProvider: () => void +} + +const ConfigurationDebugPanel = ({ + onOpenModelProvider, + ...props +}: ConfigurationDebugPanelProps) => { + return ( + + ) +} + +export default ConfigurationDebugPanel diff --git a/web/app/components/app/configuration/components/configuration-debug-panel.utils.ts b/web/app/components/app/configuration/components/configuration-debug-panel.utils.ts new file mode 100644 index 0000000000..aa083f4dba --- /dev/null +++ b/web/app/components/app/configuration/components/configuration-debug-panel.utils.ts @@ -0,0 +1,10 @@ +import type { Dispatch, SetStateAction } from 'react' +import type { AccountSettingTab } from '@/app/components/header/account-setting/constants' +import type { ModalState } from '@/context/modal-context' +import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' + +export const getOpenModelProviderHandler = ( + setShowAccountSettingModal: Dispatch | null>>, +) => { + return () => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MODEL_PROVIDER }) +} diff --git a/web/app/components/app/configuration/components/configuration-header-actions.tsx b/web/app/components/app/configuration/components/configuration-header-actions.tsx new file mode 100644 index 0000000000..0e088b62ac --- /dev/null +++ b/web/app/components/app/configuration/components/configuration-header-actions.tsx @@ -0,0 +1,61 @@ +import type { ComponentProps } from 'react' +import { CodeBracketIcon } from '@heroicons/react/20/solid' +import { useTranslation } from 'react-i18next' +import AppPublisher from '@/app/components/app/app-publisher/features-wrapper' +import AgentSettingButton from '@/app/components/app/configuration/config/agent-setting-button' +import Button from '@/app/components/base/button' +import Divider from '@/app/components/base/divider' +import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' + +type HeaderActionsProps = { + isAgent: boolean + isFunctionCall: boolean + isMobile: boolean + showModelParameterModal: boolean + onShowDebugPanel: () => void + agentSettingButtonProps: ComponentProps + modelParameterModalProps: ComponentProps + publisherProps: ComponentProps +} + +const ConfigurationHeaderActions = ({ + isAgent, + isFunctionCall, + isMobile, + showModelParameterModal, + onShowDebugPanel, + agentSettingButtonProps, + modelParameterModalProps, + publisherProps, +}: HeaderActionsProps) => { + const { t } = useTranslation() + + return ( +
+ {isAgent && ( + + )} + + {showModelParameterModal && ( + <> + + + + )} + + {isMobile && ( + + )} + + +
+ ) +} + +export default ConfigurationHeaderActions diff --git a/web/app/components/app/configuration/components/configuration-modals.tsx b/web/app/components/app/configuration/components/configuration-modals.tsx new file mode 100644 index 0000000000..b58f24a282 --- /dev/null +++ b/web/app/components/app/configuration/components/configuration-modals.tsx @@ -0,0 +1,142 @@ +import type { ComponentProps } from 'react' +import type { OnFeaturesChange } from '@/app/components/base/features/types' +import type { PromptVariable } from '@/models/debug' +import { useTranslation } from 'react-i18next' +import EditHistoryModal from '@/app/components/app/configuration/config-prompt/conversation-history/edit-modal' +import SelectDataSet from '@/app/components/app/configuration/dataset-config/select-dataset' +import Drawer from '@/app/components/base/drawer' +import NewFeaturePanel from '@/app/components/base/features/new-feature-panel' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' +import PluginDependency from '@/app/components/workflow/plugin-dependency' +import ConfigurationDebugPanel from './configuration-debug-panel' + +type ConfigurationDebugPanelProps = ComponentProps + +type ConfigurationModalsProps = { + showUseGPT4Confirm: boolean + onConfirmUseGPT4: () => void + onCancelUseGPT4: () => void + isShowSelectDataSet: boolean + hideSelectDataSet: () => void + selectedIds: string[] + onSelectDataSet: ComponentProps['onSelect'] + isShowHistoryModal: boolean + hideHistoryModal: () => void + conversationHistoriesRole: ComponentProps['data'] + setConversationHistoriesRole: (data: ComponentProps['data']) => void + isMobile: boolean + isShowDebugPanel: boolean + hideDebugPanel: () => void + debugPanelProps: ConfigurationDebugPanelProps + showAppConfigureFeaturesModal: boolean + closeFeaturePanel: () => void + mode: string + handleFeaturesChange: OnFeaturesChange + promptVariables: PromptVariable[] + handleAddPromptVariable: (variables: PromptVariable[]) => void +} + +const ConfigurationModals = ({ + showUseGPT4Confirm, + onConfirmUseGPT4, + onCancelUseGPT4, + isShowSelectDataSet, + hideSelectDataSet, + selectedIds, + onSelectDataSet, + isShowHistoryModal, + hideHistoryModal, + conversationHistoriesRole, + setConversationHistoriesRole, + isMobile, + isShowDebugPanel, + hideDebugPanel, + debugPanelProps, + showAppConfigureFeaturesModal, + closeFeaturePanel, + mode, + handleFeaturesChange, + promptVariables, + handleAddPromptVariable, +}: ConfigurationModalsProps) => { + const { t } = useTranslation() + + return ( + <> + {showUseGPT4Confirm && ( + !open && onCancelUseGPT4()}> + +
+ + {t('trailUseGPT4Info.title', { ns: 'appDebug' })} + + + {t('trailUseGPT4Info.description', { ns: 'appDebug' })} + +
+ + {t('operation.cancel', { ns: 'common' })} + + {t('operation.confirm', { ns: 'common' })} + + +
+
+ )} + + {isShowSelectDataSet && ( + + )} + + {isShowHistoryModal && ( + { + setConversationHistoriesRole(data) + hideHistoryModal() + }} + /> + )} + + {isMobile && ( + + + + )} + + {showAppConfigureFeaturesModal && ( + + )} + + + + ) +} + +export default ConfigurationModals diff --git a/web/app/components/app/configuration/config/agent/__tests__/prompt-editor.spec.tsx b/web/app/components/app/configuration/config/agent/__tests__/prompt-editor.spec.tsx new file mode 100644 index 0000000000..b40f0404ff --- /dev/null +++ b/web/app/components/app/configuration/config/agent/__tests__/prompt-editor.spec.tsx @@ -0,0 +1,160 @@ +import type { ComponentProps } from 'react' +import type { PromptEditorProps as BasePromptEditorProps } from '@/app/components/base/prompt-editor' +import { fireEvent, render, screen } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { toast } from '@/app/components/base/ui/toast' +import ConfigContext from '@/context/debug-configuration' +import PromptEditor from '../prompt-editor' + +type ContextValue = ComponentProps['value'] + +const mockCopy = vi.fn() +const mockSetShowExternalDataToolModal = vi.fn() +const mockPromptEditor = vi.fn() +const mockSetExternalDataToolsConfig = vi.fn() + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, values?: Record) => values?.key ?? key, + }), +})) + +vi.mock('copy-to-clipboard', () => ({ + default: (...args: unknown[]) => mockCopy(...args), +})) + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + error: vi.fn(), + success: vi.fn(), + }, +})) + +vi.mock('@/context/modal-context', () => ({ + useModalContext: () => ({ + setShowExternalDataToolModal: mockSetShowExternalDataToolModal, + }), +})) + +vi.mock('@/utils/classnames', () => ({ + cn: (...args: Array) => args.filter(Boolean).join(' '), +})) + +vi.mock('@/app/components/base/icons/src/vender/line/files', () => ({ + Copy: ({ onClick }: { onClick?: () => void }) => , + CopyCheck: () =>
copied-icon
, +})) + +vi.mock('@/app/components/base/prompt-editor', () => ({ + default: (props: BasePromptEditorProps) => { + mockPromptEditor(props) + return ( +
+ +
+ ) + }, +})) + +const createContextValue = (overrides: Partial = {}): ContextValue => ({ + modelConfig: { + configs: { + prompt_variables: [ + { key: 'customer_name', name: 'Customer Name' }, + { key: '', name: 'Ignored' }, + ], + }, + }, + hasSetBlockStatus: { + context: false, + }, + dataSets: [ + { id: 'dataset-1', name: 'Knowledge Base', data_source_type: 'notion' }, + ], + showSelectDataSet: vi.fn(), + externalDataToolsConfig: [ + { label: 'Search API', variable: 'search_api', icon: 'icon.png', icon_background: '#fff' }, + ], + setExternalDataToolsConfig: mockSetExternalDataToolsConfig, + ...overrides, +} as ContextValue) + +const renderEditor = (contextOverrides: Partial = {}, props: Partial> = {}) => { + return render( + + + , + ) +} + +describe('agent prompt-editor', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should copy the current prompt and toggle the copied feedback', () => { + renderEditor() + + fireEvent.click(screen.getByRole('button', { name: 'copy-icon' })) + + expect(mockCopy).toHaveBeenCalledWith('Hello world') + expect(screen.getByText('copied-icon')).toBeInTheDocument() + }) + + it('should pass context, variable, and external tool blocks into the shared prompt editor', () => { + const showSelectDataSet = vi.fn() + renderEditor({ showSelectDataSet }) + + expect(mockPromptEditor).toHaveBeenCalledWith(expect.objectContaining({ + value: 'Hello world', + contextBlock: expect.objectContaining({ + show: true, + selectable: true, + datasets: [{ id: 'dataset-1', name: 'Knowledge Base', type: 'notion' }], + onAddContext: showSelectDataSet, + }), + variableBlock: { + show: true, + variables: [{ name: 'Customer Name', value: 'customer_name' }], + }, + externalToolBlock: expect.objectContaining({ + show: true, + externalTools: [{ name: 'Search API', variableName: 'search_api', icon: 'icon.png', icon_background: '#fff' }], + }), + })) + expect(screen.getByText('11')).toBeInTheDocument() + }) + + it('should reject duplicated external tool variables before save', () => { + renderEditor() + fireEvent.click(screen.getByRole('button', { name: 'add-external-tool' })) + + const modalPayload = mockSetShowExternalDataToolModal.mock.calls[0][0] + + expect(modalPayload.onValidateBeforeSaveCallback({ variable: 'customer_name' })).toBe(false) + expect(modalPayload.onValidateBeforeSaveCallback({ variable: 'search_api' })).toBe(false) + expect(toast.error).toHaveBeenNthCalledWith(1, 'customer_name') + expect(toast.error).toHaveBeenNthCalledWith(2, 'search_api') + }) + + it('should append a new external tool when validation passes', () => { + renderEditor() + fireEvent.click(screen.getByRole('button', { name: 'add-external-tool' })) + + const modalPayload = mockSetShowExternalDataToolModal.mock.calls[0][0] + const newTool = { label: 'CRM API', variable: 'crm_api' } + + expect(modalPayload.onValidateBeforeSaveCallback(newTool)).toBe(true) + modalPayload.onSaveCallback(newTool) + + expect(mockSetExternalDataToolsConfig).toHaveBeenCalledWith([ + { label: 'Search API', variable: 'search_api', icon: 'icon.png', icon_background: '#fff' }, + newTool, + ]) + }) +}) diff --git a/web/app/components/app/configuration/config/code-generator/__tests__/get-code-generator-res.spec.tsx b/web/app/components/app/configuration/config/code-generator/__tests__/get-code-generator-res.spec.tsx new file mode 100644 index 0000000000..8814e2c9df --- /dev/null +++ b/web/app/components/app/configuration/config/code-generator/__tests__/get-code-generator-res.spec.tsx @@ -0,0 +1,267 @@ +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' +import * as React from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { toast } from '@/app/components/base/ui/toast' +import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' +import { STORAGE_KEYS } from '@/config/storage-keys' +import { AppModeEnum } from '@/types/app' +import { GetCodeGeneratorResModal } from '../get-code-generator-res' + +const mockGenerateRule = vi.fn() +const mockStorageGet = vi.fn() +const mockStorageSet = vi.fn() + +type GeneratedResult = { + error?: string + message?: string + modified?: string +} + +let sessionInstruction = '' +let instructionTemplateResponse: { data: string } | undefined = { data: 'Template instruction' } +let defaultModelResponse = { + model: 'gpt-4.1-mini', + provider: { + provider: 'langgenius/openai/openai', + }, +} + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('ahooks', async () => { + const React = await import('react') + return { + useBoolean: (initial: boolean) => { + const [value, setValue] = React.useState(initial) + return [value, { setTrue: () => setValue(true), setFalse: () => setValue(false) }] + }, + useSessionStorageState: () => React.useState(sessionInstruction), + } +}) + +vi.mock('@/app/components/base/button', () => ({ + default: ({ children, onClick, disabled }: { children: React.ReactNode, onClick?: () => void, disabled?: boolean }) => ( + + ), +})) + +vi.mock('@/app/components/base/confirm', () => ({ + default: ({ isShow, onConfirm, onCancel }: { isShow: boolean, onConfirm: () => void, onCancel: () => void }) => { + if (!isShow) + return null + return ( +
+ + +
+ ) + }, +})) + +vi.mock('@/app/components/base/loading', () => ({ + default: () =>
loading
, +})) + +vi.mock('@/app/components/base/modal', () => ({ + default: ({ isShow, children }: { isShow: boolean, children: React.ReactNode }) => isShow ?
{children}
: null, +})) + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + info: vi.fn(), + }, +})) + +vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ + useModelListAndDefaultModelAndCurrentProviderAndModel: () => ({ + defaultModel: defaultModelResponse, + }), +})) + +vi.mock('@/app/components/header/account-setting/model-provider-page/model-parameter-modal', () => ({ + default: ({ modelId, provider }: { modelId: string, provider: string }) => { + return
+ }, +})) + +vi.mock('@/service/debug', () => ({ + generateRule: (...args: unknown[]) => mockGenerateRule(...args), +})) + +vi.mock('@/service/use-apps', () => ({ + useGenerateRuleTemplate: () => ({ + data: instructionTemplateResponse, + }), +})) + +vi.mock('@/utils/storage', () => ({ + storage: { + get: (...args: unknown[]) => mockStorageGet(...args), + set: (...args: unknown[]) => mockStorageSet(...args), + }, +})) + +vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor/index', () => ({ + languageMap: { + python3: 'python', + }, +})) + +vi.mock('@/app/components/app/configuration/config/automatic/idea-output', () => ({ + default: ({ value, onChange }: { value: string, onChange: (value: string) => void }) => ( + onChange(e.target.value)} /> + ), +})) + +vi.mock('@/app/components/app/configuration/config/automatic/instruction-editor-in-workflow', () => ({ + default: ({ value, onChange }: { value: string, onChange: (value: string) => void }) => ( +