diff --git a/web/app/components/app/create-app-modal/index.spec.tsx b/web/app/components/app/create-app-modal/index.spec.tsx index c99dfd8c1a..ff6a697e83 100644 --- a/web/app/components/app/create-app-modal/index.spec.tsx +++ b/web/app/components/app/create-app-modal/index.spec.tsx @@ -12,6 +12,10 @@ import { AppModeEnum } from '@/types/app' import { getRedirection } from '@/utils/app-redirection' import CreateAppModal from './index' +const { mockNotify } = vi.hoisted(() => ({ + mockNotify: vi.fn(), +})) + vi.mock('ahooks', () => ({ useDebounceFn: unknown>(fn: T) => { const run = (...args: Parameters) => fn(...args) @@ -31,6 +35,12 @@ vi.mock('@/app/components/base/amplitude', () => ({ vi.mock('@/service/apps', () => ({ createApp: vi.fn(), })) +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + success: (message: string) => mockNotify({ type: 'success', message }), + error: (message: string) => mockNotify({ type: 'error', message }), + }, +})) vi.mock('@/utils/app-redirection', () => ({ getRedirection: vi.fn(), })) @@ -47,7 +57,6 @@ vi.mock('@/hooks/use-theme', () => ({ default: () => ({ theme: 'light' }), })) -const mockNotify = vi.fn() const mockUseRouter = vi.mocked(useRouter) const mockPush = vi.fn() const mockCreateApp = vi.mocked(createApp) diff --git a/web/app/components/datasets/create/website/jina-reader/__tests__/base.spec.tsx b/web/app/components/datasets/create/website/jina-reader/__tests__/base.spec.tsx index bcfcf39060..ab7b4019f4 100644 --- a/web/app/components/datasets/create/website/jina-reader/__tests__/base.spec.tsx +++ b/web/app/components/datasets/create/website/jina-reader/__tests__/base.spec.tsx @@ -264,7 +264,7 @@ describe('UrlInput', () => { render() const input = screen.getByRole('textbox') - await userEvent.type(input, longUrl) + fireEvent.change(input, { target: { value: longUrl } }) expect(input).toHaveValue(longUrl) }) @@ -275,7 +275,7 @@ describe('UrlInput', () => { render() const input = screen.getByRole('textbox') - await userEvent.type(input, unicodeUrl) + fireEvent.change(input, { target: { value: unicodeUrl } }) expect(input).toHaveValue(unicodeUrl) }) @@ -285,7 +285,10 @@ describe('UrlInput', () => { render() const input = screen.getByRole('textbox') - await userEvent.type(input, 'https://rapid.com', { delay: 1 }) + fireEvent.change(input, { target: { value: 'h' } }) + fireEvent.change(input, { target: { value: 'ht' } }) + fireEvent.change(input, { target: { value: 'https://' } }) + fireEvent.change(input, { target: { value: 'https://rapid.com' } }) expect(input).toHaveValue('https://rapid.com') }) diff --git a/web/app/components/goto-anything/hooks/use-goto-anything-results.ts b/web/app/components/goto-anything/hooks/use-goto-anything-results.ts index dabbd8039c..3ac20cd6fb 100644 --- a/web/app/components/goto-anything/hooks/use-goto-anything-results.ts +++ b/web/app/components/goto-anything/hooks/use-goto-anything-results.ts @@ -48,7 +48,7 @@ export const useGotoAnythingResults = ( const { data: searchResults = [], isLoading, isError, error } = useQuery( { - // eslint-disable-next-line @tanstack/query/exhaustive-deps -- Actions intentionally excluded: contains non-serializable functions; actionKeys provides stable representation + queryKey: [ 'goto-anything', 'search-result', diff --git a/web/app/components/header/account-dropdown/__tests__/index.spec.tsx b/web/app/components/header/account-dropdown/__tests__/index.spec.tsx index 9d4226c33a..737ab25d13 100644 --- a/web/app/components/header/account-dropdown/__tests__/index.spec.tsx +++ b/web/app/components/header/account-dropdown/__tests__/index.spec.tsx @@ -79,15 +79,19 @@ const { mockConfig, mockEnv } = vi.hoisted(() => ({ }, }, })) -vi.mock('@/config', () => ({ - get IS_CLOUD_EDITION() { return mockConfig.IS_CLOUD_EDITION }, - get AMPLITUDE_API_KEY() { return mockConfig.AMPLITUDE_API_KEY }, - get isAmplitudeEnabled() { return mockConfig.IS_CLOUD_EDITION && !!mockConfig.AMPLITUDE_API_KEY }, - get ZENDESK_WIDGET_KEY() { return mockConfig.ZENDESK_WIDGET_KEY }, - get SUPPORT_EMAIL_ADDRESS() { return mockConfig.SUPPORT_EMAIL_ADDRESS }, - IS_DEV: false, - IS_CE_EDITION: false, -})) +vi.mock('@/config', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + get IS_CLOUD_EDITION() { return mockConfig.IS_CLOUD_EDITION }, + get AMPLITUDE_API_KEY() { return mockConfig.AMPLITUDE_API_KEY }, + get isAmplitudeEnabled() { return mockConfig.IS_CLOUD_EDITION && !!mockConfig.AMPLITUDE_API_KEY }, + get ZENDESK_WIDGET_KEY() { return mockConfig.ZENDESK_WIDGET_KEY }, + get SUPPORT_EMAIL_ADDRESS() { return mockConfig.SUPPORT_EMAIL_ADDRESS }, + IS_DEV: false, + IS_CE_EDITION: false, + } +}) vi.mock('@/env', () => mockEnv) const baseAppContextValue: AppContextValue = { diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx index dee16e394e..17acf62af2 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx @@ -405,7 +405,7 @@ describe('Popup', () => { expect(onHide).toHaveBeenCalled() expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ - payload: 'provider', + payload: 'model-provider', }) }) @@ -425,7 +425,7 @@ describe('Popup', () => { fireEvent.click(screen.getByText(/modelProvider\.selector\.configure/)) expect(onHide).toHaveBeenCalled() expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ - payload: 'provider', + payload: 'model-provider', }) }) diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.spec.tsx index 882d53dd60..8daccb0794 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.spec.tsx @@ -1,7 +1,6 @@ import type { Model, ModelItem } from '../declarations' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, screen } from '@testing-library/react' -import { tooltipManager } from '@/app/components/base/tooltip/TooltipManager' import { ConfigurationMethodEnum, ModelFeatureEnum, @@ -29,12 +28,32 @@ vi.mock('@/utils/tool-call', () => ({ supportFunctionCall: mockSupportFunctionCall, })) +const mockMarketplacePlugins = vi.hoisted(() => ({ + current: [] as Array<{ plugin_id: string, latest_package_identifier: string }>, + isLoading: false, +})) +const mockContextModelProviders = vi.hoisted(() => ({ + current: [] as Array<{ + provider: string + label: Record + icon_small: Record + icon_small_dark?: Record + custom_configuration?: { status?: string } + system_configuration?: { enabled?: boolean } + }>, +})) +const mockTrialModels = vi.hoisted(() => ({ + current: ['test-openai', 'test-anthropic'] as string[], +})) vi.mock('../hooks', async () => { const actual = await vi.importActual('../hooks') return { ...actual, useLanguage: () => mockLanguage, - useMarketplaceAllPlugins: () => [], + useMarketplaceAllPlugins: () => ({ + plugins: mockMarketplacePlugins.current, + isLoading: mockMarketplacePlugins.isLoading, + }), } }) @@ -42,6 +61,74 @@ vi.mock('./popup-item', () => ({ default: ({ model }: { model: Model }) =>
{model.provider}
, })) +vi.mock('@/context/provider-context', () => ({ + useProviderContext: () => ({ modelProviders: mockContextModelProviders.current }), +})) + +vi.mock('@/context/global-public-context', () => ({ + useSystemFeaturesQuery: () => ({ + data: { trial_models: mockTrialModels.current }, + }), +})) + +const mockTrialCredits = vi.hoisted(() => ({ + credits: 200, + totalCredits: 200, + isExhausted: false, + isLoading: false, + nextCreditResetDate: undefined as number | undefined, +})) +vi.mock('../provider-added-card/use-trial-credits', () => ({ + useTrialCredits: () => mockTrialCredits, +})) + +vi.mock('../provider-added-card/model-auth-dropdown/credits-exhausted-alert', () => ({ + default: () =>
, +})) + +vi.mock('next-themes', () => ({ + useTheme: () => ({ theme: 'light' }), +})) + +const mockInstallMutateAsync = vi.hoisted(() => vi.fn()) +vi.mock('@/service/use-plugins', () => ({ + useInstallPackageFromMarketPlace: () => ({ mutateAsync: mockInstallMutateAsync }), +})) + +const mockRefreshPluginList = vi.hoisted(() => vi.fn()) +vi.mock('@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list', () => ({ + default: () => ({ refreshPluginList: mockRefreshPluginList }), +})) + +const mockCheck = vi.hoisted(() => vi.fn()) +vi.mock('@/app/components/plugins/install-plugin/base/check-task-status', () => ({ + default: () => ({ check: mockCheck }), +})) + +vi.mock('@/utils/var', () => ({ + getMarketplaceUrl: vi.fn(() => 'https://marketplace.example.com'), +})) + +vi.mock('../utils', async () => { + const actual = await vi.importActual('../utils') + return { + ...actual, + MODEL_PROVIDER_QUOTA_GET_PAID: ['test-openai', 'test-anthropic'], + providerIconMap: { + 'test-openai': ({ className }: { className?: string }) => OAI, + 'test-anthropic': ({ className }: { className?: string }) => ANT, + }, + modelNameMap: { + 'test-openai': 'TestOpenAI', + 'test-anthropic': 'TestAnthropic', + }, + providerKeyToPluginId: { + 'test-openai': 'langgenius/openai', + 'test-anthropic': 'langgenius/anthropic', + }, + } +}) + const makeModelItem = (overrides: Partial = {}): ModelItem => ({ model: 'gpt-4', label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' }, @@ -54,22 +141,30 @@ const makeModelItem = (overrides: Partial = {}): ModelItem => ({ }) const makeModel = (overrides: Partial = {}): Model => ({ - provider: 'openai', + provider: 'custom-provider', icon_small: { en_US: '', zh_Hans: '' }, - label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' }, + label: { en_US: 'Custom Provider', zh_Hans: 'Custom Provider' }, models: [makeModelItem()], status: ModelStatusEnum.active, ...overrides, }) describe('Popup', () => { - let closeActiveTooltipSpy: ReturnType - beforeEach(() => { vi.clearAllMocks() mockLanguage = 'en_US' mockSupportFunctionCall.mockReturnValue(true) - closeActiveTooltipSpy = vi.spyOn(tooltipManager, 'closeActiveTooltip') + mockMarketplacePlugins.current = [] + mockMarketplacePlugins.isLoading = false + mockContextModelProviders.current = [] + mockTrialModels.current = ['test-openai', 'test-anthropic'] + Object.assign(mockTrialCredits, { + credits: 200, + totalCredits: 200, + isExhausted: false, + isLoading: false, + nextCreditResetDate: undefined, + }) }) it('should filter models by search and allow clearing search', () => { @@ -81,7 +176,7 @@ describe('Popup', () => { />, ) - expect(screen.getByText('openai')).toBeInTheDocument() + expect(screen.getByText('custom-provider')).toBeInTheDocument() const input = screen.getByPlaceholderText('datasetSettings.form.searchModel') fireEvent.change(input, { target: { value: 'not-found' } }) @@ -89,7 +184,7 @@ describe('Popup', () => { fireEvent.change(input, { target: { value: '' } }) expect((input as HTMLInputElement).value).toBe('') - expect(screen.getByText('openai')).toBeInTheDocument() + expect(screen.getByText('custom-provider')).toBeInTheDocument() }) it('should filter by scope features including toolCall and non-toolCall checks', () => { @@ -120,7 +215,7 @@ describe('Popup', () => { scopeFeatures={[ModelFeatureEnum.toolCall, ModelFeatureEnum.vision]} />, ) - expect(screen.getByText('openai')).toBeInTheDocument() + expect(screen.getByText('custom-provider')).toBeInTheDocument() unmount2() const { unmount: unmount3 } = renderWithProviders( @@ -131,7 +226,7 @@ describe('Popup', () => { scopeFeatures={[ModelFeatureEnum.vision]} />, ) - expect(screen.getByText('openai')).toBeInTheDocument() + expect(screen.getByText('custom-provider')).toBeInTheDocument() // When features are missing, non-toolCall feature checks should fail. unmount3() @@ -162,7 +257,7 @@ describe('Popup', () => { { target: { value: 'gpt' } }, ) - expect(screen.getByText('openai')).toBeInTheDocument() + expect(screen.getByText('No model found for “gpt”')).toBeInTheDocument() }) it('should filter out model when features array exists but does not include required scopeFeature', () => { @@ -180,11 +275,11 @@ describe('Popup', () => { ) // The model item should be filtered out because it has toolCall but not vision - expect(screen.queryByText('openai')).not.toBeInTheDocument() + expect(screen.queryByText('custom-provider')).not.toBeInTheDocument() }) - it('should close tooltip on scroll', () => { - const { container } = renderWithProviders( + it('should render marketplace providers that are not installed yet', () => { + renderWithProviders( { />, ) - fireEvent.scroll(container.firstElementChild as HTMLElement) - expect(closeActiveTooltipSpy).toHaveBeenCalled() + expect(screen.getByText('TestOpenAI')).toBeInTheDocument() + expect(screen.getByText('TestAnthropic')).toBeInTheDocument() }) it('should open provider settings when clicking footer link', () => { @@ -205,7 +300,7 @@ describe('Popup', () => { />, ) - fireEvent.click(screen.getByText('common.model.settingsLink')) + fireEvent.click(screen.getByText('common.modelProvider.selector.modelProviderSettings')) expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'model-provider', @@ -222,7 +317,7 @@ describe('Popup', () => { />, ) - fireEvent.click(screen.getByText('common.model.settingsLink')) + fireEvent.click(screen.getByText('common.modelProvider.selector.modelProviderSettings')) expect(mockOnHide).toHaveBeenCalled() }) @@ -240,6 +335,6 @@ describe('Popup', () => { const input = screen.getByPlaceholderText('datasetSettings.form.searchModel') fireEvent.change(input, { target: { value: 'gpt' } }) - expect(screen.getByText('openai')).toBeInTheDocument() + expect(screen.getByText('No model found for “gpt”')).toBeInTheDocument() }) }) diff --git a/web/app/components/workflow-app/__tests__/index.spec.tsx b/web/app/components/workflow-app/__tests__/index.spec.tsx index 880a62ca8d..edba570fd6 100644 --- a/web/app/components/workflow-app/__tests__/index.spec.tsx +++ b/web/app/components/workflow-app/__tests__/index.spec.tsx @@ -8,16 +8,26 @@ const mockSetShowInputsPanel = vi.fn() const mockSetShowDebugAndPreviewPanel = vi.fn() const mockWorkflowStoreSetState = vi.fn() const mockDebouncedCancel = vi.fn() +const mockSetShowUpgradeRuntimeModal = vi.fn() +const mockSetNeedsRuntimeUpgrade = vi.fn() const mockFetchRunDetail = vi.fn() const mockInitialNodes = vi.fn() const mockInitialEdges = vi.fn() const mockGetWorkflowRunAndTraceUrl = vi.fn() +const mockSyncWorkflowDraftImmediately = vi.fn() +const mockUseSubscription = vi.fn() +const mockCollaborationSetNodes = vi.fn() +const mockCollaborationSetEdges = vi.fn() +const mockEmitGraphViewActive = vi.fn() let appStoreState: { appDetail?: { id: string mode: string + name?: string + runtime_type?: string } + setNeedsRuntimeUpgrade: typeof mockSetNeedsRuntimeUpgrade } let workflowInitState: { @@ -50,6 +60,12 @@ let appTriggersState: { } let searchParamsValue: string | null = null +const workflowUiState = { + appId: 'app-1', + isResponding: false, + showUpgradeRuntimeModal: false, + setShowUpgradeRuntimeModal: mockSetShowUpgradeRuntimeModal, +} const mockWorkflowStore = { setState: mockWorkflowStoreSetState, @@ -68,6 +84,7 @@ vi.mock('@/app/components/app/store', () => ({ })) vi.mock('@/app/components/workflow/store', () => ({ + useStore: (selector: (state: typeof workflowUiState) => T) => selector(workflowUiState), useWorkflowStore: () => mockWorkflowStore, })) @@ -87,6 +104,14 @@ vi.mock('@/next/navigation', () => ({ }), })) +vi.mock('nuqs', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useQueryState: () => [null, vi.fn()], + } +}) + vi.mock('@/service/log', () => ({ fetchRunDetail: (...args: unknown[]) => mockFetchRunDetail(...args), })) @@ -99,12 +124,38 @@ vi.mock('@/app/components/workflow-app/hooks/use-workflow-init', () => ({ useWorkflowInit: () => workflowInitState, })) +vi.mock('@/app/components/workflow-app/hooks/use-nodes-sync-draft', () => ({ + useNodesSyncDraft: () => ({ + syncWorkflowDraftImmediately: mockSyncWorkflowDraftImmediately, + }), +})) + vi.mock('@/app/components/workflow-app/hooks/use-get-run-and-trace-url', () => ({ useGetRunAndTraceUrl: () => ({ getWorkflowRunAndTraceUrl: mockGetWorkflowRunAndTraceUrl, }), })) +vi.mock('@/context/event-emitter', () => ({ + useEventEmitterContextContext: () => ({ + eventEmitter: { + useSubscription: mockUseSubscription, + }, + }), +})) + +vi.mock('@/app/components/workflow/collaboration', () => ({ + useCollaboration: () => undefined, +})) + +vi.mock('@/app/components/workflow/collaboration/core/collaboration-manager', () => ({ + collaborationManager: { + setNodes: (...args: unknown[]) => mockCollaborationSetNodes(...args), + setEdges: (...args: unknown[]) => mockCollaborationSetEdges(...args), + emitGraphViewActive: (...args: unknown[]) => mockEmitGraphViewActive(...args), + }, +})) + vi.mock('@/app/components/workflow/utils', async (importOriginal) => { const actual = await importOriginal() return { @@ -118,19 +169,25 @@ vi.mock('@/app/components/base/loading', () => ({ default: () =>
loading
, })) -vi.mock('@/app/components/base/features', () => ({ - FeaturesProvider: ({ - features, - children, - }: { - features: Record - children: ReactNode - }) => ( -
- {children} -
- ), -})) +vi.mock('@/app/components/base/features', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + FeaturesProvider: ({ + features, + children, + }: { + features: Record + children: ReactNode + }) => ( +
+ + {children} + +
+ ), + } +}) vi.mock('@/app/components/workflow', () => ({ default: ({ @@ -178,6 +235,26 @@ vi.mock('@/app/components/workflow-app/components/workflow-main', () => ({ ), })) +vi.mock('@/app/components/workflow/header', () => ({ + HeaderShell: ({ children }: { children: ReactNode }) =>
{children}
, +})) + +vi.mock('@/app/components/workflow/header/online-users', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/workflow/view-picker', () => ({ + default: () =>
, +})) + +vi.mock('../components/sandbox-migration-modal', () => ({ + default: () =>
, +})) + +vi.mock('../components/upgraded-from-banner', () => ({ + default: () =>
, +})) + describe('WorkflowApp', () => { beforeEach(() => { vi.clearAllMocks() @@ -185,7 +262,9 @@ describe('WorkflowApp', () => { appDetail: { id: 'app-1', mode: 'workflow', + name: 'Workflow App', }, + setNeedsRuntimeUpgrade: mockSetNeedsRuntimeUpgrade, } workflowInitState = { data: { @@ -213,6 +292,7 @@ describe('WorkflowApp', () => { mockInitialNodes.mockReturnValue([{ id: 'node-1' }]) mockInitialEdges.mockReturnValue([{ id: 'edge-1' }]) mockGetWorkflowRunAndTraceUrl.mockReturnValue({ runUrl: '/runs/run-1' }) + mockSyncWorkflowDraftImmediately.mockResolvedValue(undefined) }) it('should render the loading shell while workflow data is still loading', () => { diff --git a/web/app/components/workflow-app/components/__tests__/workflow-main.spec.tsx b/web/app/components/workflow-app/components/__tests__/workflow-main.spec.tsx index 381cc4b10d..af17db7bff 100644 --- a/web/app/components/workflow-app/components/__tests__/workflow-main.spec.tsx +++ b/web/app/components/workflow-app/components/__tests__/workflow-main.spec.tsx @@ -8,6 +8,16 @@ import WorkflowMain from '../workflow-main' const mockSetFeatures = vi.fn() const mockSetConversationVariables = vi.fn() const mockSetEnvironmentVariables = vi.fn() +const mockStartCursorTracking = vi.fn() +const mockStopCursorTracking = vi.fn() +const mockGetNodes = vi.fn() +const mockSetNodes = vi.fn() +const mockGetEdges = vi.fn() +const mockSetEdges = vi.fn() + +const workflowUiState = { + appId: 'app-1', +} const hookFns = { doSyncWorkflowDraft: vi.fn(), @@ -54,6 +64,7 @@ type MockWorkflowWithInnerContextProps = Pick ({ + useFeatures: (selector: (state: { features: { sandbox?: { enabled: boolean } } }) => T) => selector({ features: {} }), useFeaturesStore: () => ({ getState: () => ({ setFeatures: mockSetFeatures, @@ -62,6 +73,7 @@ vi.mock('@/app/components/base/features/hooks', () => ({ })) vi.mock('@/app/components/workflow/store', () => ({ + useStore: (selector: (state: typeof workflowUiState) => T) => selector(workflowUiState), useWorkflowStore: () => ({ getState: () => ({ setConversationVariables: mockSetConversationVariables, @@ -70,6 +82,35 @@ vi.mock('@/app/components/workflow/store', () => ({ }), })) +vi.mock('reactflow', () => ({ + useReactFlow: () => ({ + getNodes: mockGetNodes, + setNodes: mockSetNodes, + getEdges: mockGetEdges, + setEdges: mockSetEdges, + }), +})) + +vi.mock('@/app/components/workflow/collaboration', () => ({ + collaborationManager: { + onVarsAndFeaturesUpdate: vi.fn(() => vi.fn()), + onWorkflowUpdate: vi.fn(() => vi.fn()), + onSyncRequest: vi.fn(() => vi.fn()), + }, + useCollaboration: () => ({ + startCursorTracking: mockStartCursorTracking, + stopCursorTracking: mockStopCursorTracking, + onlineUsers: [], + cursors: {}, + isConnected: false, + isEnabled: false, + }), +})) + +vi.mock('@/app/components/workflow/block-selector/context/mcp-tool-availability-context', () => ({ + MCPToolAvailabilityProvider: ({ children }: { children: ReactNode, sandboxEnabled: boolean }) => <>{children}, +})) + vi.mock('@/app/components/workflow', () => ({ WorkflowWithInnerContext: ({ nodes, @@ -172,6 +213,8 @@ describe('WorkflowMain', () => { beforeEach(() => { vi.clearAllMocks() capturedContextProps = null + mockGetNodes.mockReturnValue([]) + mockGetEdges.mockReturnValue([]) }) it('should render the inner workflow context with children and forwarded graph props', () => { @@ -207,9 +250,18 @@ describe('WorkflowMain', () => { fireEvent.click(screen.getByRole('button', { name: /update-workflow-data/i })) - expect(mockSetFeatures).toHaveBeenCalledWith({ file: { enabled: true } }) - expect(mockSetConversationVariables).toHaveBeenCalledWith([{ id: 'conversation-1' }]) - expect(mockSetEnvironmentVariables).toHaveBeenCalledWith([{ id: 'env-1' }]) + expect(mockSetFeatures).toHaveBeenCalledWith(expect.objectContaining({ + file: expect.objectContaining({ + enabled: true, + }), + sandbox: { enabled: false }, + })) + expect(mockSetConversationVariables).toHaveBeenCalledWith([ + expect.objectContaining({ id: 'conversation-1' }), + ]) + expect(mockSetEnvironmentVariables).toHaveBeenCalledWith([ + expect.objectContaining({ id: 'env-1' }), + ]) }) it('should only update the workflow store slices present in the payload', () => { @@ -223,7 +275,9 @@ describe('WorkflowMain', () => { fireEvent.click(screen.getByRole('button', { name: /update-conversation-only/i })) - expect(mockSetConversationVariables).toHaveBeenCalledWith([{ id: 'conversation-only' }]) + expect(mockSetConversationVariables).toHaveBeenCalledWith([ + expect.objectContaining({ id: 'conversation-only' }), + ]) expect(mockSetFeatures).not.toHaveBeenCalled() expect(mockSetEnvironmentVariables).not.toHaveBeenCalled() }) diff --git a/web/app/components/workflow-app/hooks/__tests__/use-DSL.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-DSL.spec.ts index 0716d71dce..8337a5090a 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-DSL.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-DSL.spec.ts @@ -6,6 +6,7 @@ const mockNotify = vi.fn() const mockEmit = vi.fn() const mockDoSyncWorkflowDraft = vi.fn() const mockExportAppConfig = vi.fn() +const mockExportAppBundle = vi.fn() const mockFetchWorkflowDraft = vi.fn() const mockDownloadBlob = vi.fn() @@ -39,6 +40,7 @@ vi.mock('../use-nodes-sync-draft', () => ({ })) vi.mock('@/service/apps', () => ({ + exportAppBundle: (...args: unknown[]) => mockExportAppBundle(...args), exportAppConfig: (...args: unknown[]) => mockExportAppConfig(...args), })) @@ -69,6 +71,7 @@ describe('useDSL', () => { } mockDoSyncWorkflowDraft.mockResolvedValue(undefined) mockExportAppConfig.mockResolvedValue({ data: 'yaml-content' }) + mockExportAppBundle.mockResolvedValue(undefined) mockFetchWorkflowDraft.mockResolvedValue({ environment_variables: [] }) }) @@ -79,17 +82,19 @@ describe('useDSL', () => { await result.current.exportCheck() }) - expect(mockFetchWorkflowDraft).toHaveBeenCalledWith('/apps/app-1/workflows/draft') - expect(mockDoSyncWorkflowDraft).toHaveBeenCalled() - expect(mockExportAppConfig).toHaveBeenCalledWith({ - appID: 'app-1', - include: false, - workflowID: undefined, + await waitFor(() => { + expect(mockFetchWorkflowDraft).toHaveBeenCalledWith('/apps/app-1/workflows/draft') + expect(mockDoSyncWorkflowDraft).toHaveBeenCalled() + expect(mockExportAppConfig).toHaveBeenCalledWith({ + appID: 'app-1', + include: false, + workflowID: undefined, + }) + expect(mockDownloadBlob).toHaveBeenCalledWith(expect.objectContaining({ + data: expect.any(Blob), + fileName: 'Workflow App.yaml', + })) }) - expect(mockDownloadBlob).toHaveBeenCalledWith(expect.objectContaining({ - data: expect.any(Blob), - fileName: 'Workflow App.yml', - })) }) it('should forward include and workflow id arguments when exporting dsl directly', async () => { @@ -120,6 +125,7 @@ describe('useDSL', () => { type: DSL_EXPORT_CHECK, payload: { data: secretVars, + sandboxed: false, }, }) expect(mockExportAppConfig).not.toHaveBeenCalled() diff --git a/web/app/components/workflow-app/hooks/__tests__/use-available-nodes-meta-data.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-available-nodes-meta-data.spec.ts index c92e438cb3..c587865232 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-available-nodes-meta-data.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-available-nodes-meta-data.spec.ts @@ -3,11 +3,24 @@ import { BlockEnum } from '@/app/components/workflow/types' import { useAvailableNodesMetaData } from '../use-available-nodes-meta-data' const mockUseIsChatMode = vi.fn() +const appStoreState = { + appDetail: { + runtime_type: 'default', + }, +} vi.mock('@/app/components/workflow-app/hooks/use-is-chat-mode', () => ({ useIsChatMode: () => mockUseIsChatMode(), })) +vi.mock('@/app/components/app/store', () => ({ + useStore: (selector: (state: typeof appStoreState) => T) => selector(appStoreState), +})) + +vi.mock('@/app/components/base/features/hooks', () => ({ + useFeatures: (selector: (state: { features: { sandbox?: { enabled: boolean } } }) => T) => selector({ features: {} }), +})) + vi.mock('@/context/i18n', () => ({ useDocLink: () => (path: string) => `/docs${path}`, })) diff --git a/web/app/components/workflow-app/hooks/__tests__/use-nodes-sync-draft.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-nodes-sync-draft.spec.ts index c9fa535d51..2b25806db6 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-nodes-sync-draft.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-nodes-sync-draft.spec.ts @@ -70,8 +70,20 @@ vi.mock('@/service/workflow', () => ({ syncWorkflowDraft: (p: unknown) => mockSyncWorkflowDraft(p), })) -vi.mock('@/service/fetch', () => ({ postWithKeepalive: (...args: unknown[]) => mockPostWithKeepalive(...args) })) -vi.mock('@/config', () => ({ API_PREFIX: '/api' })) +vi.mock('@/service/fetch', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + postWithKeepalive: (...args: unknown[]) => mockPostWithKeepalive(...args), + } +}) +vi.mock('@/config', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + API_PREFIX: '/api', + } +}) const mockHandleRefreshWorkflowDraft = vi.fn() vi.mock('@/app/components/workflow-app/hooks', () => ({ @@ -207,9 +219,11 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () => await result.current.doSyncWorkflowDraft(false, callbacks) }) - expect(mockSyncWorkflowDraft).toHaveBeenCalledWith({ + expect(mockSyncWorkflowDraft).toHaveBeenCalledWith(expect.objectContaining({ url: '/apps/app-1/workflows/draft', - params: { + canNotSaveEmpty: true, + params: expect.objectContaining({ + _is_collaborative: false, graph: { nodes: [{ id: 'n1', position: { x: 0, y: 0 }, data: { type: 'start', label: 'Start' } }], edges: [{ id: 'edge-1', source: 'n1', target: 'n2', data: { stable: 'keep' } }], @@ -224,12 +238,13 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () => retriever_resource: { enabled: true }, sensitive_word_avoidance: { enabled: false }, file_upload: { enabled: true }, + sandbox: undefined, }, environment_variables: [{ id: 'env-1', value: 'env' }], conversation_variables: [{ id: 'conversation-1', value: 'conversation' }], hash: 'latest-hash', - }, - }) + }), + })) expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('new') expect(mockSetDraftUpdatedAt).toHaveBeenCalledWith(1) expect(callbacks.onSuccess).toHaveBeenCalled() diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-refresh-draft.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-refresh-draft.spec.ts index 209f9d9c0e..7b81ca7ac9 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-refresh-draft.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-refresh-draft.spec.ts @@ -52,7 +52,7 @@ describe('useWorkflowRefreshDraft — notUpdateCanvas parameter', () => { workflowStoreState = { appId: 'app-1', isWorkflowDataLoaded: true, - debouncedSyncWorkflowDraft: undefined, + debouncedSyncWorkflowDraft: { cancel: mockCancel }, setSyncWorkflowDraftHash: mockSetSyncWorkflowDraftHash, setIsSyncingWorkflowDraft: mockSetIsSyncingWorkflowDraft, setEnvironmentVariables: mockSetEnvironmentVariables, @@ -65,33 +65,38 @@ describe('useWorkflowRefreshDraft — notUpdateCanvas parameter', () => { it('should update canvas by default (notUpdateCanvas omitted)', async () => { const { result } = renderHook(() => useWorkflowRefreshDraft()) - await act(async () => { + act(() => { result.current.handleRefreshWorkflowDraft() }) - expect(mockHandleUpdateWorkflowCanvas).toHaveBeenCalledTimes(1) + await waitFor(() => { + expect(mockHandleUpdateWorkflowCanvas).toHaveBeenCalledTimes(1) + }) }) it('should update canvas when notUpdateCanvas=false', async () => { const { result } = renderHook(() => useWorkflowRefreshDraft()) - await act(async () => { + act(() => { result.current.handleRefreshWorkflowDraft(false) }) - expect(mockHandleUpdateWorkflowCanvas).toHaveBeenCalledTimes(1) + await waitFor(() => { + expect(mockHandleUpdateWorkflowCanvas).toHaveBeenCalledTimes(1) + }) }) it('should NOT update canvas when notUpdateCanvas=true', async () => { - // This is the key change: when called from a 409 error during editing, - // canvas must not be overwritten with server state. const { result } = renderHook(() => useWorkflowRefreshDraft()) - await act(async () => { + act(() => { result.current.handleRefreshWorkflowDraft(true) }) + await waitFor(() => { + expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('server-hash') + }) expect(mockHandleUpdateWorkflowCanvas).not.toHaveBeenCalled() }) it('should still update hash even when notUpdateCanvas=true', async () => { const { result } = renderHook(() => useWorkflowRefreshDraft()) - await act(async () => { + act(() => { result.current.handleRefreshWorkflowDraft(true) }) await waitFor(() => { diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-run-utils.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-run-utils.spec.ts index a83d2f55ee..f0940a9795 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-run-utils.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-run-utils.spec.ts @@ -76,8 +76,8 @@ describe('useWorkflowRun utils', () => { expect(validateWorkflowRunRequest(TriggerType.Schedule)).toBe('handleRun: schedule trigger run requires node id') expect(validateWorkflowRunRequest(TriggerType.Webhook)).toBe('handleRun: webhook trigger run requires node id') expect(validateWorkflowRunRequest(TriggerType.Plugin)).toBe('handleRun: plugin trigger run requires node id') - expect(validateWorkflowRunRequest(TriggerType.All)).toBe('') - expect(validateWorkflowRunRequest(TriggerType.All, { allNodeIds: [] })).toBe('') + expect(validateWorkflowRunRequest(TriggerType.All)).toBe('handleRun: all trigger run requires node ids') + expect(validateWorkflowRunRequest(TriggerType.All, { allNodeIds: [] })).toBe('handleRun: all trigger run requires node ids') }) it('should return empty trigger urls when app id is missing and keep user-input urls empty outside workflow debug', () => { diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-run.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-run.spec.ts index 7b54598774..a20d3af16d 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-run.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-run.spec.ts @@ -69,6 +69,7 @@ const mocks = vi.hoisted(() => { mockFetchInspectVars: vi.fn(), mockInvalidateAllLastRun: vi.fn(), mockInvalidateRunHistory: vi.fn(), + mockInvalidateSandboxFiles: vi.fn(), mockSsePost: vi.fn(), mockSseGet: vi.fn(), mockHandleStream: vi.fn(), @@ -181,6 +182,10 @@ vi.mock('@/service/workflow', () => ({ stopWorkflowRun: mocks.mockStopWorkflowRun, })) +vi.mock('@/service/use-sandbox-file', () => ({ + useInvalidateSandboxFiles: () => mocks.mockInvalidateSandboxFiles, +})) + vi.mock('@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars', () => ({ useSetWorkflowVarsWithValue: () => ({ fetchInspectVars: mocks.mockFetchInspectVars, @@ -340,6 +345,14 @@ describe('useWorkflowRun', () => { getAbortController: expect.any(Function), }), ) + + const baseCallbackFactoryContext = mocks.mockCreateBaseWorkflowRunCallbacks.mock.calls.at(-1)?.[0] as { + callbacks: { + onWorkflowFinished?: (params: { workflow_run_id: string }) => void + } + } + baseCallbackFactoryContext.callbacks.onWorkflowFinished?.({ workflow_run_id: 'run-1' }) + expect(mocks.mockInvalidateSandboxFiles).toHaveBeenCalledTimes(1) }) it.each([ @@ -546,15 +559,34 @@ describe('useWorkflowRun', () => { edges: [{ id: 'published-edge' }], viewport: { x: 10, y: 20, zoom: 0.8 }, }) - expect(mocks.featuresStoreSetState).toHaveBeenCalledWith({ + expect(mocks.featuresStoreSetState).toHaveBeenCalledWith(expect.objectContaining({ features: expect.objectContaining({ opening: expect.objectContaining({ enabled: true, opening_statement: 'hello', + suggested_questions: ['Q1'], + }), + suggested: { enabled: true }, + text2speech: { enabled: true }, + speech2text: { enabled: true }, + citation: { enabled: true }, + moderation: { enabled: true }, + annotationReply: { enabled: false }, + sandbox: { enabled: false }, + file: expect.objectContaining({ + enabled: true, + allowed_file_types: ['image'], + allowed_file_upload_methods: ['local_file', 'remote_url'], + number_limits: 3, + fileUploadConfig: undefined, + image: { + enabled: false, + number_limits: 3, + transfer_methods: ['local_file', 'remote_url'], + }, }), - file: { enabled: true }, }), - }) + })) expect(mocks.workflowStoreState.setEnvironmentVariables).toHaveBeenCalledWith([{ id: 'env-published', value: 'value' }]) }) @@ -581,12 +613,34 @@ describe('useWorkflowRun', () => { } as never) }) - expect(mocks.featuresStoreSetState).toHaveBeenCalledWith({ + expect(mocks.featuresStoreSetState).toHaveBeenCalledWith(expect.objectContaining({ features: expect.objectContaining({ - opening: expect.objectContaining({ enabled: false }), - file: { enabled: false }, + opening: { + enabled: false, + opening_statement: '', + suggested_questions: [], + }, + suggested: { enabled: false }, + text2speech: { enabled: false }, + speech2text: { enabled: false }, + citation: { enabled: false }, + moderation: { enabled: false }, + annotationReply: { enabled: false }, + sandbox: { enabled: false }, + file: expect.objectContaining({ + enabled: false, + allowed_file_types: ['image'], + allowed_file_upload_methods: ['local_file', 'remote_url'], + number_limits: 3, + fileUploadConfig: undefined, + image: { + enabled: false, + number_limits: 3, + transfer_methods: ['local_file', 'remote_url'], + }, + }), }), - }) + })) expect(mocks.workflowStoreState.setEnvironmentVariables).toHaveBeenCalledWith([]) }) }) diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-template.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-template.spec.ts index 5d492a3b35..fe86484e90 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-template.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-template.spec.ts @@ -76,7 +76,7 @@ describe('useWorkflowTemplate', () => { expect(generateNewNodeCalls[2].data).toMatchObject({ type: 'answer', title: 'workflow.blocks.answer', - answer: '{{#llm.text#}}', + answer: '{{#llm.generation#}}', }) }) }) diff --git a/web/app/components/workflow-app/hooks/use-workflow-run.ts b/web/app/components/workflow-app/hooks/use-workflow-run.ts index 99b317b27c..b6cefcec3d 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-run.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-run.ts @@ -20,8 +20,7 @@ import { useWorkflowUpdate } from '@/app/components/workflow/hooks/use-workflow- import { useWorkflowRunEvent } from '@/app/components/workflow/hooks/use-workflow-run-event/use-workflow-run-event' import { useWorkflowStore } from '@/app/components/workflow/store' import { usePathname } from '@/next/navigation' -import { handleStream, post, sseGet, ssePost } from '@/service/base' -import { ContentType } from '@/service/fetch' +import { ssePost } from '@/service/base' import { useInvalidateSandboxFiles } from '@/service/use-sandbox-file' import { useInvalidAllLastRun, useInvalidateWorkflowRunHistory } from '@/service/use-workflow' import { stopWorkflowRun } from '@/service/workflow' @@ -380,7 +379,6 @@ export const useWorkflowRun = () => { const handleStopRun = useCallback((taskId: string) => { const setStoppedState = () => { const { - workflowRunningData, setWorkflowRunningData, setIsListening, setShowVariableInspectPanel, diff --git a/web/app/components/workflow-app/index.tsx b/web/app/components/workflow-app/index.tsx index c83afdfe1d..cfab038e18 100644 --- a/web/app/components/workflow-app/index.tsx +++ b/web/app/components/workflow-app/index.tsx @@ -52,12 +52,12 @@ import { } from './hooks/use-workflow-init' import { parseAsViewType, WORKFLOW_VIEW_PARAM_KEY } from './search-params' import { createWorkflowSlice } from './store/workflow/workflow-slice' -import { getSandboxMigrationDismissed, setSandboxMigrationDismissed } from './utils/sandbox-migration-storage' import { buildInitialFeatures, buildTriggerStatusMap, coerceReplayUserInputs, } from './utils' +import { getSandboxMigrationDismissed, setSandboxMigrationDismissed } from './utils/sandbox-migration-storage' const SkillMain = dynamic(() => import('@/app/components/workflow/skill/main'), { ssr: false, diff --git a/web/app/components/workflow/__tests__/custom-edge.spec.tsx b/web/app/components/workflow/__tests__/custom-edge.spec.tsx index f8ff9a1a0e..a5eb189a18 100644 --- a/web/app/components/workflow/__tests__/custom-edge.spec.tsx +++ b/web/app/components/workflow/__tests__/custom-edge.spec.tsx @@ -9,6 +9,7 @@ const mockUseAvailableBlocks = vi.hoisted(() => vi.fn()) const mockUseNodesInteractions = vi.hoisted(() => vi.fn()) const mockBlockSelector = vi.hoisted(() => vi.fn()) const mockGradientRender = vi.hoisted(() => vi.fn()) +const mockUseHooksStore = vi.hoisted(() => vi.fn()) vi.mock('reactflow', () => ({ BaseEdge: (props: { @@ -44,6 +45,10 @@ vi.mock('@/app/components/workflow/hooks', () => ({ useNodesInteractions: () => mockUseNodesInteractions(), })) +vi.mock('../hooks-store', () => ({ + useHooksStore: (selector: (state: { interactionMode: string }) => unknown) => mockUseHooksStore(selector), +})) + vi.mock('@/app/components/workflow/block-selector', () => ({ __esModule: true, default: (props: { @@ -87,6 +92,9 @@ describe('CustomEdge', () => { beforeEach(() => { vi.clearAllMocks() + mockUseHooksStore.mockImplementation((selector: (state: { interactionMode: string }) => unknown) => selector({ + interactionMode: 'default', + })) mockUseNodesInteractions.mockReturnValue({ handleNodeAdd: mockHandleNodeAdd, }) diff --git a/web/app/components/workflow/__tests__/features.spec.tsx b/web/app/components/workflow/__tests__/features.spec.tsx index 8be40faea9..8510bbea56 100644 --- a/web/app/components/workflow/__tests__/features.spec.tsx +++ b/web/app/components/workflow/__tests__/features.spec.tsx @@ -10,6 +10,7 @@ import { renderWorkflowFlowComponent } from './workflow-test-env' const mockHandleSyncWorkflowDraft = vi.fn() const mockHandleAddVariable = vi.fn() +const mockUpdateFeatures = vi.hoisted(() => vi.fn()) let mockIsChatMode = true let mockNodesReadOnly = false @@ -34,6 +35,24 @@ vi.mock('../nodes/start/use-config', () => ({ }), })) +vi.mock('@/app/components/app/store', () => ({ + useStore: (selector: (state: { appDetail?: { runtime_type?: string } }) => unknown) => selector({ + appDetail: { + runtime_type: 'sandboxed', + }, + }), +})) + +vi.mock('@/service/workflow', () => ({ + updateFeatures: mockUpdateFeatures, +})) + +vi.mock('@/app/components/workflow/collaboration/core/websocket-manager', () => ({ + webSocketClient: { + getSocket: () => null, + }, +})) + vi.mock('@/app/components/base/features/new-feature-panel', () => ({ default: ({ show, @@ -112,21 +131,29 @@ const DelayedFeatures = () => { return } -const renderFeatures = (options?: Omit[1], 'nodes' | 'edges'>) => - renderWorkflowFlowComponent( +const renderFeatures = (options?: Omit[1]>, 'nodes' | 'edges'>) => { + const { initialStoreState, ...rest } = options ?? {} + + return renderWorkflowFlowComponent( , { nodes: [startNode], edges: [], - ...options, + initialStoreState: { + appId: 'app-1', + ...initialStoreState, + }, + ...rest, }, ) +} describe('Features', () => { beforeEach(() => { vi.clearAllMocks() mockIsChatMode = true mockNodesReadOnly = false + mockUpdateFeatures.mockResolvedValue(undefined) }) describe('Rendering', () => { diff --git a/web/app/components/workflow/__tests__/panel-contextmenu.spec.tsx b/web/app/components/workflow/__tests__/panel-contextmenu.spec.tsx index 914c1be617..18c1e0e94d 100644 --- a/web/app/components/workflow/__tests__/panel-contextmenu.spec.tsx +++ b/web/app/components/workflow/__tests__/panel-contextmenu.spec.tsx @@ -10,6 +10,8 @@ const mockUsePanelInteractions = vi.hoisted(() => vi.fn()) const mockUseWorkflowStartRun = vi.hoisted(() => vi.fn()) const mockUseOperator = vi.hoisted(() => vi.fn()) const mockUseDSL = vi.hoisted(() => vi.fn()) +const mockUseWorkflowMoveMode = vi.hoisted(() => vi.fn()) +const mockUseFeatures = vi.hoisted(() => vi.fn()) vi.mock('ahooks', () => ({ useClickAway: (...args: unknown[]) => mockUseClickAway(...args), @@ -32,12 +34,17 @@ vi.mock('@/app/components/workflow/hooks', () => ({ usePanelInteractions: () => mockUsePanelInteractions(), useWorkflowStartRun: () => mockUseWorkflowStartRun(), useDSL: () => mockUseDSL(), + useWorkflowMoveMode: () => mockUseWorkflowMoveMode(), })) vi.mock('@/app/components/workflow/operator/hooks', () => ({ useOperator: () => mockUseOperator(), })) +vi.mock('@/app/components/base/features/hooks', () => ({ + useFeatures: (selector: (state: { features: { sandbox?: { enabled?: boolean } } }) => unknown) => mockUseFeatures(selector), +})) + vi.mock('@/app/components/workflow/operator/add-block', () => ({ __esModule: true, default: ({ renderTrigger }: { renderTrigger: () => ReactNode }) => ( @@ -62,14 +69,19 @@ describe('PanelContextmenu', () => { const mockHandleAddNote = vi.fn() const mockExportCheck = vi.fn() const mockSetShowImportDSLModal = vi.fn() + const mockSetShowUpgradeRuntimeModal = vi.fn() + const mockSetCommentPlacing = vi.fn() + const mockSetCommentQuickAdd = vi.fn() let panelMenu: { left: number, top: number } | undefined let clipboardElements: unknown[] + let pipelineId: string | undefined let clickAwayHandler: (() => void) | undefined beforeEach(() => { vi.clearAllMocks() panelMenu = undefined clipboardElements = [] + pipelineId = 'pipeline-1' clickAwayHandler = undefined mockUseClickAway.mockImplementation((handler: () => void) => { @@ -82,10 +94,20 @@ describe('PanelContextmenu', () => { panelMenu?: { left: number, top: number } clipboardElements: unknown[] setShowImportDSLModal: (visible: boolean) => void + setShowUpgradeRuntimeModal: (visible: boolean) => void + pendingComment?: unknown + setCommentPlacing: (placing: boolean) => void + setCommentQuickAdd: (placing: boolean) => void + pipelineId?: string }) => unknown) => selector({ panelMenu, clipboardElements, setShowImportDSLModal: mockSetShowImportDSLModal, + setShowUpgradeRuntimeModal: mockSetShowUpgradeRuntimeModal, + pendingComment: undefined, + setCommentPlacing: mockSetCommentPlacing, + setCommentQuickAdd: mockSetCommentQuickAdd, + pipelineId, })) mockUseNodesInteractions.mockReturnValue({ handleNodesPaste: mockHandleNodesPaste, @@ -102,6 +124,16 @@ describe('PanelContextmenu', () => { mockUseDSL.mockReturnValue({ exportCheck: mockExportCheck, }) + mockUseWorkflowMoveMode.mockReturnValue({ + isCommentModeAvailable: false, + }) + mockUseFeatures.mockImplementation((selector: (state: { features: { sandbox?: { enabled?: boolean } } }) => unknown) => selector({ + features: { + sandbox: { + enabled: false, + }, + }, + })) }) it('should stay hidden when the panel menu is absent', () => { diff --git a/web/app/components/workflow/__tests__/workflow-test-env.spec.tsx b/web/app/components/workflow/__tests__/workflow-test-env.spec.tsx index de13828f2a..70b24acd5a 100644 --- a/web/app/components/workflow/__tests__/workflow-test-env.spec.tsx +++ b/web/app/components/workflow/__tests__/workflow-test-env.spec.tsx @@ -95,16 +95,9 @@ describe('renderWorkflowComponent', () => { expect(screen.getByTestId('hooks-reader')).toHaveTextContent('test-123') }) - it('should throw when HooksStoreContext is not provided', () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - try { - expect(() => { - renderWorkflowComponent(React.createElement(HooksStoreReader)) - }).toThrow('Missing HooksStoreContext.Provider') - } - finally { - consoleSpy.mockRestore() - } + it('should provide a default HooksStoreContext when hooksStoreProps are omitted', () => { + renderWorkflowComponent(React.createElement(HooksStoreReader)) + expect(screen.getByTestId('hooks-reader')).toHaveTextContent('none') }) it('should forward extra render options (container)', () => { diff --git a/web/app/components/workflow/__tests__/workflow-test-env.tsx b/web/app/components/workflow/__tests__/workflow-test-env.tsx index 1ee601317b..e7c5a89532 100644 --- a/web/app/components/workflow/__tests__/workflow-test-env.tsx +++ b/web/app/components/workflow/__tests__/workflow-test-env.tsx @@ -72,6 +72,7 @@ import * as React from 'react' import ReactFlow, { ReactFlowProvider } from 'reactflow' import { temporal } from 'zundo' import { create } from 'zustand' +import { FeaturesProvider } from '@/app/components/base/features/context' import { WorkflowContext } from '../context' import { HooksStoreContext } from '../hooks-store/provider' import { createHooksStore } from '../hooks-store/store' @@ -138,14 +139,12 @@ type WorkflowProviderOptions = { type StoreInstances = { store: WorkflowStore - hooksStore?: HooksStore + hooksStore: HooksStore } function createStoresFromOptions(options: WorkflowProviderOptions): StoreInstances { const store = createTestWorkflowStore(options.initialStoreState) - const hooksStore = options.hooksStoreProps !== undefined - ? createTestHooksStore(options.hooksStoreProps) - : undefined + const hooksStore = createTestHooksStore(options.hooksStoreProps) return { store, hooksStore } } @@ -175,21 +174,23 @@ function createWorkflowWrapper( ) } - if (stores.hooksStore) { - inner = React.createElement( - HooksStoreContext.Provider, - { value: stores.hooksStore }, - inner, - ) - } + inner = React.createElement( + HooksStoreContext.Provider, + { value: stores.hooksStore }, + inner, + ) return React.createElement( QueryClientProvider, { client: queryClient }, React.createElement( - WorkflowContext.Provider, - { value: stores.store }, - inner, + FeaturesProvider, + null, + React.createElement( + WorkflowContext.Provider, + { value: stores.store }, + inner, + ), ), ) } diff --git a/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx b/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx index 6fa934b57d..10c251e602 100644 --- a/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx +++ b/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx @@ -5,10 +5,10 @@ import { renderWorkflowComponent } from '../../__tests__/workflow-test-env' import { WorkflowVersion } from '../../types' import HeaderInRestoring from '../header-in-restoring' -const mockRestoreWorkflow = vi.fn() const mockInvalidAllLastRun = vi.fn() const mockHandleLoadBackupDraft = vi.fn() const mockHandleRefreshWorkflowDraft = vi.fn() +const mockRequestRestore = vi.fn() vi.mock('@/hooks/use-theme', () => ({ default: () => ({ @@ -30,9 +30,6 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({ vi.mock('@/service/use-workflow', () => ({ useInvalidAllLastRun: () => mockInvalidAllLastRun, - useRestoreWorkflow: () => ({ - mutateAsync: mockRestoreWorkflow, - }), })) vi.mock('../../hooks', () => ({ @@ -42,6 +39,18 @@ vi.mock('../../hooks', () => ({ useWorkflowRefreshDraft: () => ({ handleRefreshWorkflowDraft: mockHandleRefreshWorkflowDraft, }), + useLeaderRestore: () => ({ + requestRestore: mockRequestRestore, + }), +})) + +vi.mock('@/context/app-context', () => ({ + useSelector: (selector: (state: { userProfile: { id: string, name: string } }) => unknown) => selector({ + userProfile: { + id: 'user-1', + name: 'Alice', + }, + }), })) const createVersion = (overrides: Partial = {}): VersionHistory => ({ @@ -73,6 +82,10 @@ const createVersion = (overrides: Partial = {}): VersionHistory describe('HeaderInRestoring', () => { beforeEach(() => { vi.clearAllMocks() + mockRequestRestore.mockImplementation((_payload, callbacks) => { + callbacks?.onSuccess?.() + callbacks?.onSettled?.() + }) }) it('should disable restore when the flow id is not ready yet', () => { diff --git a/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx b/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx index d092e769d6..77ba009ee5 100644 --- a/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx +++ b/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx @@ -14,7 +14,7 @@ const mockHandleNodeSelect = vi.fn() const mockHandleRefreshWorkflowDraft = vi.fn() const mockCloseAllInputFieldPanels = vi.fn() const mockInvalidAllLastRun = vi.fn() -const mockRestoreWorkflow = vi.fn() +const mockRequestRestore = vi.fn() const mockNotify = vi.fn() const mockRunAndHistory = vi.fn() const mockViewHistory = vi.fn() @@ -39,6 +39,9 @@ vi.mock('../../hooks', () => ({ useWorkflowRefreshDraft: () => ({ handleRefreshWorkflowDraft: mockHandleRefreshWorkflowDraft, }), + useLeaderRestore: () => ({ + requestRestore: mockRequestRestore, + }), })) vi.mock('@/app/components/rag-pipeline/hooks', () => ({ @@ -55,8 +58,14 @@ vi.mock('@/hooks/use-theme', () => ({ vi.mock('@/service/use-workflow', () => ({ useInvalidAllLastRun: () => mockInvalidAllLastRun, - useRestoreWorkflow: () => ({ - mutateAsync: mockRestoreWorkflow, +})) + +vi.mock('@/context/app-context', () => ({ + useSelector: (selector: (state: { userProfile: { id: string, name: string } }) => unknown) => selector({ + userProfile: { + id: 'user-1', + name: 'Tester', + }, }), })) @@ -77,6 +86,10 @@ vi.mock('../scroll-to-selected-node-button', () => ({ default: () =>
scroll-button
, })) +vi.mock('../online-users', () => ({ + default: () =>
online-users
, +})) + vi.mock('../env-button', () => ({ default: ({ disabled }: { disabled: boolean }) =>
{`${disabled}`}
, })) @@ -162,7 +175,10 @@ describe('Header layout components', () => { mockNodesReadOnly = false mockTheme = 'light' mockUseNodes.mockReturnValue([]) - mockRestoreWorkflow.mockResolvedValue(undefined) + mockRequestRestore.mockImplementation((_payload, callbacks) => { + callbacks?.onSuccess?.() + callbacks?.onSettled?.() + }) }) describe('HeaderInNormal', () => { @@ -267,11 +283,18 @@ describe('Header layout components', () => { fireEvent.click(screen.getByRole('button', { name: 'workflow.common.restore' })) await waitFor(() => { - expect(mockRestoreWorkflow).toHaveBeenCalledWith('/apps/flow-1/workflows/version-1/restore') + expect(mockRequestRestore).toHaveBeenCalledWith(expect.objectContaining({ + versionId: 'version-1', + initiatorUserId: 'user-1', + initiatorName: 'Tester', + }), expect.objectContaining({ + onSuccess: expect.any(Function), + onError: expect.any(Function), + onSettled: expect.any(Function), + })) expect(store.getState().showWorkflowVersionHistoryPanel).toBe(false) expect(store.getState().isRestoring).toBe(false) expect(store.getState().backupDraft).toBeUndefined() - expect(mockHandleRefreshWorkflowDraft).toHaveBeenCalledTimes(1) expect(deleteAllInspectVars).toHaveBeenCalledTimes(1) expect(mockInvalidAllLastRun).toHaveBeenCalledTimes(1) expect(mockNotify).toHaveBeenCalledWith({ diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-store-only.spec.ts b/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-store-only.spec.ts index b41af1aef1..4857a933e5 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-store-only.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-workflow-run-event-store-only.spec.ts @@ -91,7 +91,7 @@ describe('useWorkflowFinished', () => { expect(state.resultText).toBe('hello') }) - it('should not activate result tab for multi-key outputs', () => { + it('should activate result tab for multi-key outputs', () => { const { result, store } = renderWorkflowHook(() => useWorkflowFinished(), { initialStoreState: { workflowRunningData: baseRunningData() }, }) @@ -100,7 +100,7 @@ describe('useWorkflowFinished', () => { data: { status: 'succeeded', outputs: { a: 'hello', b: 'world' } }, } as unknown as WorkflowFinishedResponse) - expect(store.getState().workflowRunningData!.resultTabActive).toBeFalsy() + expect(store.getState().workflowRunningData!.resultTabActive).toBe(true) }) }) diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/form-input-item.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/form-input-item.spec.tsx index 49fa4ea29f..12766fd888 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/form-input-item.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/form-input-item.spec.tsx @@ -41,7 +41,7 @@ const renderFormInputItem = (props: Partial renderWorkflowFlowComponent( { it('should build single-run forms from external vars and keep iterator state in a dedicated form', () => { const toVarInputs = vi.fn(() => [createInputVar('#start-node.answer#')]) - const { result } = renderHook(() => useSingleRunFormParams({ + const { result } = renderWorkflowHook(() => useSingleRunFormParams({ id: 'iteration-node', payload: createPayload(), runInputData: { @@ -134,7 +135,7 @@ describe('iteration/use-single-run-form-params', () => { it('should forward form updates and expose iterator dependencies', () => { const setRunInputData = vi.fn() - const { result } = renderHook(() => useSingleRunFormParams({ + const { result } = renderWorkflowHook(() => useSingleRunFormParams({ id: 'iteration-node', payload: createPayload({ iterator_selector: ['source-node', 'records'], diff --git a/web/app/components/workflow/nodes/loop/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/loop/__tests__/integration.spec.tsx index 99ce377b99..3b8a3811f7 100644 --- a/web/app/components/workflow/nodes/loop/__tests__/integration.spec.tsx +++ b/web/app/components/workflow/nodes/loop/__tests__/integration.spec.tsx @@ -82,6 +82,8 @@ vi.mock('@/app/components/workflow/block-selector', () => ({ })) vi.mock('../../loop-start', () => ({ + __esModule: true, + default: () =>
loop-start-node
, LoopStartNodeDumb: () =>
loop-start-node
, })) diff --git a/web/app/components/workflow/nodes/tool/__tests__/node.spec.tsx b/web/app/components/workflow/nodes/tool/__tests__/node.spec.tsx index cc7c6b4e0d..4131b23bfa 100644 --- a/web/app/components/workflow/nodes/tool/__tests__/node.spec.tsx +++ b/web/app/components/workflow/nodes/tool/__tests__/node.spec.tsx @@ -6,6 +6,16 @@ import Node from '../node' const mockUseNodePluginInstallation = vi.hoisted(() => vi.fn()) const mockUseCurrentToolCollection = vi.hoisted(() => vi.fn()) +const mockUseNodes = vi.hoisted(() => vi.fn()) +const mockUseNodesMetaData = vi.hoisted(() => vi.fn()) + +vi.mock('reactflow', () => ({ + useNodes: () => mockUseNodes(), +})) + +vi.mock('@/app/components/workflow/hooks', () => ({ + useNodesMetaData: () => mockUseNodesMetaData(), +})) vi.mock('@/app/components/workflow/hooks/use-node-plugin-installation', () => ({ useNodePluginInstallation: mockUseNodePluginInstallation, @@ -20,6 +30,14 @@ vi.mock('@/app/components/workflow/nodes/_base/components/install-plugin-button' InstallPluginButton: () => , })) +vi.mock('@/context/i18n', () => ({ + useGetLanguage: () => 'en-US', +})) + +vi.mock('@/service/use-strategy', () => ({ + useStrategyProviders: () => ({ data: undefined }), +})) + const createNodeData = (overrides: Partial = {}): ToolNodeType => ({ title: 'Google Search', desc: '', @@ -37,6 +55,8 @@ const createNodeData = (overrides: Partial = {}): ToolNodeType => describe('ToolNode', () => { beforeEach(() => { vi.clearAllMocks() + mockUseNodes.mockReturnValue([]) + mockUseNodesMetaData.mockReturnValue({ nodesMap: {} }) mockUseNodePluginInstallation.mockReturnValue({ isChecking: false, isMissing: false, diff --git a/web/app/components/workflow/nodes/variable-assigner/__tests__/hooks.spec.ts b/web/app/components/workflow/nodes/variable-assigner/__tests__/hooks.spec.ts index 0cbb98c96a..ca3940c12d 100644 --- a/web/app/components/workflow/nodes/variable-assigner/__tests__/hooks.spec.ts +++ b/web/app/components/workflow/nodes/variable-assigner/__tests__/hooks.spec.ts @@ -1,4 +1,6 @@ import { act, renderHook } from '@testing-library/react' +import * as React from 'react' +import { FeaturesProvider } from '@/app/components/base/features/context' import { VarType } from '../../../types' import { useGetAvailableVars, useVariableAssigner } from '../hooks' @@ -229,7 +231,9 @@ describe('variable-assigner/hooks', () => { getNodeAvailableVars, }) - const { result } = renderHook(() => useGetAvailableVars()) + const { result } = renderHook(() => useGetAvailableVars(), { + wrapper: ({ children }) => React.createElement(FeaturesProvider, null, children), + }) expect(result.current('current-node', 'target', () => true, true)).toEqual([{ isStartNode: true, diff --git a/web/app/components/workflow/panel/__tests__/workflow-preview.spec.tsx b/web/app/components/workflow/panel/__tests__/workflow-preview.spec.tsx index 860322d729..5b0a61c566 100644 --- a/web/app/components/workflow/panel/__tests__/workflow-preview.spec.tsx +++ b/web/app/components/workflow/panel/__tests__/workflow-preview.spec.tsx @@ -142,7 +142,7 @@ describe('WorkflowPreview', () => { it('should keep the input tab active, switch to result after running, and close the preview panel', async () => { const user = userEvent.setup() - const { container } = renderWorkflowComponent( + const { container, store } = renderWorkflowComponent( , { initialStoreState: { @@ -156,7 +156,20 @@ describe('WorkflowPreview', () => { expect(screen.getByRole('button', { name: 'run-inputs' })).toBeInTheDocument() await user.click(screen.getByRole('button', { name: 'run-inputs' })) - expect(screen.getByTestId('result-text')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'run-inputs' })).toBeInTheDocument() + + store.setState({ + workflowRunningData: createWorkflowRunningData({ + result: createWorkflowResult({ + status: WorkflowRunningStatus.Running, + files: [], + }), + }) as NonNullable, + }) + + await waitFor(() => { + expect(screen.getByTestId('result-text')).toBeInTheDocument() + }) await user.click(container.querySelector('.flex.items-center.justify-between .cursor-pointer.p-1') as HTMLElement) expect(mockHandleCancelDebugAndPreviewPanel).toHaveBeenCalledTimes(1) @@ -281,6 +294,7 @@ describe('WorkflowPreview', () => { }, ) + fireEvent.click(screen.getByText('runLog.tracing')) expect(screen.getByTestId('tracing-panel')).toHaveTextContent('0') expect(screen.getByRole('status', { name: 'appApi.loading' })).toBeInTheDocument() }) diff --git a/web/app/components/workflow/panel/chat-variable-panel/__tests__/index.spec.tsx b/web/app/components/workflow/panel/chat-variable-panel/__tests__/index.spec.tsx index 722c0c280e..3e91414c33 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/__tests__/index.spec.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/__tests__/index.spec.tsx @@ -1,10 +1,12 @@ import type { ConversationVariable, Node } from '@/app/components/workflow/types' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { updateConversationVariables } from '@/service/workflow' import ChatVariablePanel from '../index' import { ChatVarType } from '../type' type MockWorkflowStoreState = { + appId: string setShowChatVariablePanel: (value: boolean) => void conversationVariables: ConversationVariable[] setConversationVariables: (value: ConversationVariable[]) => void @@ -17,9 +19,6 @@ type MockFlowStore = { const mockSetShowChatVariablePanel = vi.fn() const mockSetConversationVariables = vi.fn() -const mockDoSyncWorkflowDraft = vi.fn((_sync: boolean, options?: { onSuccess?: () => void }) => { - options?.onSuccess?.() -}) const mockInvalidateConversationVarValues = vi.fn() const mockFindUsedVarNodes = vi.fn<(selector: string[], nodes: Node[]) => Node[]>() const mockUpdateNodeVars = vi.fn<(node: Node, current: string[], next: string[]) => Node>() @@ -61,16 +60,15 @@ vi.mock('reactflow', () => ({ vi.mock('@/app/components/workflow/store', () => ({ useStore: (selector: (state: MockWorkflowStoreState) => T) => selector({ + appId: 'app-1', setShowChatVariablePanel: mockSetShowChatVariablePanel, conversationVariables: mockConversationVariables, setConversationVariables: mockSetConversationVariables, }), })) -vi.mock('@/app/components/workflow/hooks/use-nodes-sync-draft', () => ({ - useNodesSyncDraft: () => ({ - doSyncWorkflowDraft: mockDoSyncWorkflowDraft, - }), +vi.mock('@/service/workflow', () => ({ + updateConversationVariables: vi.fn(), })) vi.mock('../../../hooks/use-inspect-vars-crud', () => ({ @@ -171,12 +169,15 @@ vi.mock('@/app/components/workflow/nodes/_base/components/remove-effect-var-conf })) describe('ChatVariablePanel', () => { + const mockUpdateConversationVariables = vi.mocked(updateConversationVariables) + beforeEach(() => { vi.clearAllMocks() mockConversationVariables = [createConversationVariable()] mockFlowNodes = [createNode('node-1'), createNode('node-2')] mockFindUsedVarNodes.mockReturnValue([]) mockUpdateNodeVars.mockImplementation((node: Node) => node) + mockUpdateConversationVariables.mockResolvedValue(undefined as never) }) it('should toggle the tips area and close the panel', async () => { @@ -208,7 +209,13 @@ describe('ChatVariablePanel', () => { createConversationVariable(), ]) }) - expect(mockDoSyncWorkflowDraft).toHaveBeenCalledTimes(1) + expect(mockUpdateConversationVariables).toHaveBeenCalledWith({ + appId: 'app-1', + conversationVariables: [ + expect.objectContaining({ id: 'var-added', name: 'fresh_var' }), + createConversationVariable(), + ], + }) expect(mockInvalidateConversationVarValues).toHaveBeenCalledTimes(1) }) diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.helpers.spec.ts b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.helpers.spec.ts index 86b46cc869..de75df8de0 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.helpers.spec.ts +++ b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.helpers.spec.ts @@ -77,7 +77,7 @@ describe('variable-modal helpers', () => { objectValue: [], type: ChatVarType.Boolean, value: undefined, - })).toBe(true) + })).toBe(false) expect(formatChatVariableValue({ editInJSON: false, diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/components.spec.tsx b/web/app/components/workflow/panel/debug-and-preview/__tests__/components.spec.tsx index b7ab773836..dca144f609 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/components.spec.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/components.spec.tsx @@ -109,7 +109,7 @@ vi.mock('@/app/components/base/chat/chat', () => ({ }, })) -vi.mock('../hooks', () => ({ +vi.mock('../hooks/use-chat', () => ({ useChat: (...args: unknown[]) => mockUseChat(...args), })) diff --git a/web/app/components/workflow/skill/hooks/file-tree/operations/use-file-operations.spec.tsx b/web/app/components/workflow/skill/hooks/file-tree/operations/use-file-operations.spec.tsx index 68a77bde5d..3bbebc26bc 100644 --- a/web/app/components/workflow/skill/hooks/file-tree/operations/use-file-operations.spec.tsx +++ b/web/app/components/workflow/skill/hooks/file-tree/operations/use-file-operations.spec.tsx @@ -220,6 +220,7 @@ describe('useFileOperations', () => { nodeId: 'node-from-node', node, treeRef, + nodeType: 'file', appId: 'app-1', storeApi: mocks.workflowStore, treeData: mocks.treeData, diff --git a/web/app/components/workflow/update-dsl-modal.tsx b/web/app/components/workflow/update-dsl-modal.tsx index c0ab08ad99..2374261bd2 100644 --- a/web/app/components/workflow/update-dsl-modal.tsx +++ b/web/app/components/workflow/update-dsl-modal.tsx @@ -29,6 +29,7 @@ import { importDSLConfirm, } from '@/service/apps' import { fetchWorkflowDraft } from '@/service/workflow' +import { collaborationManager } from './collaboration/core/collaboration-manager' import { WORKFLOW_DATA_UPDATE } from './constants' import { getImportNotificationPayload, @@ -36,7 +37,6 @@ import { normalizeWorkflowFeatures, validateDSLContent, } from './update-dsl-modal.helpers' -import { collaborationManager } from './collaboration/core/collaboration-manager' import { initialEdges, initialNodes, diff --git a/web/app/components/workflow/variable-inspect/__tests__/panel.spec.tsx b/web/app/components/workflow/variable-inspect/__tests__/panel.spec.tsx index 2bd1fbb00f..b2c2b68216 100644 --- a/web/app/components/workflow/variable-inspect/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/variable-inspect/__tests__/panel.spec.tsx @@ -150,7 +150,7 @@ describe('VariableInspect Panel', () => { showVariableInspectPanel: true, }) - fireEvent.click(screen.getAllByRole('button')[0]) + fireEvent.click(screen.getByRole('button', { name: 'Close' })) expect(screen.getByText('workflow.debug.variableInspect.emptyTip')).toBeInTheDocument() expect(store.getState().showVariableInspectPanel).toBe(false) diff --git a/web/app/components/workflow/variable-inspect/__tests__/value-content.helpers.spec.ts b/web/app/components/workflow/variable-inspect/__tests__/value-content.helpers.spec.ts index dcf22adcc2..c59d23022e 100644 --- a/web/app/components/workflow/variable-inspect/__tests__/value-content.helpers.spec.ts +++ b/web/app/components/workflow/variable-inspect/__tests__/value-content.helpers.spec.ts @@ -1,5 +1,6 @@ import type { VarInInspect } from '@/types/workflow' import { VarType } from '@/app/components/workflow/types' +import { TransferMethod } from '@/types/app' import { VarInInspectType } from '@/types/workflow' import { formatInspectFileValue, @@ -9,6 +10,19 @@ import { } from '../value-content.helpers' describe('value-content helpers', () => { + const createFileValue = (id: string) => ({ + related_id: id, + extension: '.txt', + filename: `${id}.txt`, + size: 1, + mime_type: 'text/plain', + transfer_method: TransferMethod.local_file, + type: 'document', + url: `https://example.com/${id}.txt`, + upload_file_id: `${id}-upload`, + remote_url: '', + }) + const createVar = (overrides: Partial): VarInInspect => ({ id: 'var-1', name: 'query', @@ -56,7 +70,7 @@ describe('value-content helpers', () => { expect(formatInspectFileValue(createVar({ name: 'file', value_type: VarType.file, - value: { id: 'file-1' }, + value: createFileValue('file-1'), }))).toHaveLength(1) expect(isFileValueUploaded([{ upload_file_id: 'file-1' }])).toBe(true) @@ -65,7 +79,7 @@ describe('value-content helpers', () => { type: VarInInspectType.system, name: 'files', value_type: VarType.arrayFile, - value: [{ id: 'file-2' }], + value: [createFileValue('file-2')], }))).toHaveLength(1) }) diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index 70cbabbed4..96a5378119 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -6339,9 +6339,6 @@ "no-restricted-imports": { "count": 1 }, - "perfectionist/sort-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } diff --git a/web/vite.config.ts b/web/vite.config.ts index a55a9a96ec..9c9e824aae 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -56,7 +56,10 @@ export default defineConfig(({ mode }) => { ], resolve: { alias: { - 'loro-crdt': path.resolve(projectRoot, 'node_modules/loro-crdt/web/index.js'), + 'loro-crdt': path.resolve( + projectRoot, + isTest ? 'node_modules/loro-crdt/nodejs/index.js' : 'node_modules/loro-crdt/web/index.js', + ), }, tsconfigPaths: true, },