mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 12:59:18 +08:00
tix test
This commit is contained in:
parent
f0041ec619
commit
a7178b4d5c
@ -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: <T extends (...args: unknown[]) => unknown>(fn: T) => {
|
||||
const run = (...args: Parameters<T>) => 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)
|
||||
|
||||
@ -264,7 +264,7 @@ describe('UrlInput', () => {
|
||||
|
||||
render(<UrlInput {...props} />)
|
||||
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(<UrlInput {...props} />)
|
||||
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(<UrlInput {...props} />)
|
||||
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')
|
||||
})
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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<typeof import('@/config')>()
|
||||
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 = {
|
||||
|
||||
@ -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',
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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<string, string>
|
||||
icon_small: Record<string, string>
|
||||
icon_small_dark?: Record<string, string>
|
||||
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<typeof import('../hooks')>('../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 }) => <div>{model.provider}</div>,
|
||||
}))
|
||||
|
||||
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: () => <div data-testid="credits-exhausted-alert" />,
|
||||
}))
|
||||
|
||||
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<typeof import('../utils')>('../utils')
|
||||
return {
|
||||
...actual,
|
||||
MODEL_PROVIDER_QUOTA_GET_PAID: ['test-openai', 'test-anthropic'],
|
||||
providerIconMap: {
|
||||
'test-openai': ({ className }: { className?: string }) => <span className={className}>OAI</span>,
|
||||
'test-anthropic': ({ className }: { className?: string }) => <span className={className}>ANT</span>,
|
||||
},
|
||||
modelNameMap: {
|
||||
'test-openai': 'TestOpenAI',
|
||||
'test-anthropic': 'TestAnthropic',
|
||||
},
|
||||
providerKeyToPluginId: {
|
||||
'test-openai': 'langgenius/openai',
|
||||
'test-anthropic': 'langgenius/anthropic',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const makeModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
|
||||
model: 'gpt-4',
|
||||
label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' },
|
||||
@ -54,22 +141,30 @@ const makeModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
|
||||
})
|
||||
|
||||
const makeModel = (overrides: Partial<Model> = {}): 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<typeof vi.spyOn>
|
||||
|
||||
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(
|
||||
<Popup
|
||||
modelList={[makeModel()]}
|
||||
onSelect={vi.fn()}
|
||||
@ -192,8 +287,8 @@ describe('Popup', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
@ -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: <T,>(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<typeof import('nuqs')>()
|
||||
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<typeof import('@/app/components/workflow/utils')>()
|
||||
return {
|
||||
@ -118,19 +169,25 @@ vi.mock('@/app/components/base/loading', () => ({
|
||||
default: () => <div data-testid="loading">loading</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/features', () => ({
|
||||
FeaturesProvider: ({
|
||||
features,
|
||||
children,
|
||||
}: {
|
||||
features: Record<string, unknown>
|
||||
children: ReactNode
|
||||
}) => (
|
||||
<div data-testid="features-provider" data-features={JSON.stringify(features)}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
vi.mock('@/app/components/base/features', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/features')>()
|
||||
return {
|
||||
...actual,
|
||||
FeaturesProvider: ({
|
||||
features,
|
||||
children,
|
||||
}: {
|
||||
features: Record<string, unknown>
|
||||
children: ReactNode
|
||||
}) => (
|
||||
<div data-testid="features-provider" data-features={JSON.stringify(features)}>
|
||||
<actual.FeaturesProvider features={features as never}>
|
||||
{children}
|
||||
</actual.FeaturesProvider>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
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 }) => <div data-testid="header-shell">{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/header/online-users', () => ({
|
||||
default: () => <div data-testid="online-users" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/view-picker', () => ({
|
||||
default: () => <div data-testid="view-picker" />,
|
||||
}))
|
||||
|
||||
vi.mock('../components/sandbox-migration-modal', () => ({
|
||||
default: () => <div data-testid="sandbox-migration-modal" />,
|
||||
}))
|
||||
|
||||
vi.mock('../components/upgraded-from-banner', () => ({
|
||||
default: () => <div data-testid="upgraded-from-banner" />,
|
||||
}))
|
||||
|
||||
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', () => {
|
||||
|
||||
@ -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<WorkflowProps, 'nodes' | 'edges' |
|
||||
}
|
||||
|
||||
vi.mock('@/app/components/base/features/hooks', () => ({
|
||||
useFeatures: <T,>(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: <T,>(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()
|
||||
})
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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: <T>(selector: (state: typeof appStoreState) => T) => selector(appStoreState),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/features/hooks', () => ({
|
||||
useFeatures: <T>(selector: (state: { features: { sandbox?: { enabled: boolean } } }) => T) => selector({ features: {} }),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path: string) => `/docs${path}`,
|
||||
}))
|
||||
|
||||
@ -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<typeof import('@/service/fetch')>()
|
||||
return {
|
||||
...actual,
|
||||
postWithKeepalive: (...args: unknown[]) => mockPostWithKeepalive(...args),
|
||||
}
|
||||
})
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/config')>()
|
||||
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()
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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([])
|
||||
})
|
||||
})
|
||||
|
||||
@ -76,7 +76,7 @@ describe('useWorkflowTemplate', () => {
|
||||
expect(generateNewNodeCalls[2].data).toMatchObject({
|
||||
type: 'answer',
|
||||
title: 'workflow.blocks.answer',
|
||||
answer: '{{#llm.text#}}',
|
||||
answer: '{{#llm.generation#}}',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
@ -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 <Features />
|
||||
}
|
||||
|
||||
const renderFeatures = (options?: Omit<Parameters<typeof renderWorkflowFlowComponent>[1], 'nodes' | 'edges'>) =>
|
||||
renderWorkflowFlowComponent(
|
||||
const renderFeatures = (options?: Omit<NonNullable<Parameters<typeof renderWorkflowFlowComponent>[1]>, 'nodes' | 'edges'>) => {
|
||||
const { initialStoreState, ...rest } = options ?? {}
|
||||
|
||||
return renderWorkflowFlowComponent(
|
||||
<DelayedFeatures />,
|
||||
{
|
||||
nodes: [startNode],
|
||||
edges: [],
|
||||
...options,
|
||||
initialStoreState: {
|
||||
appId: 'app-1',
|
||||
...initialStoreState,
|
||||
},
|
||||
...rest,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
describe('Features', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsChatMode = true
|
||||
mockNodesReadOnly = false
|
||||
mockUpdateFeatures.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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)', () => {
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ -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> = {}): VersionHistory => ({
|
||||
@ -73,6 +82,10 @@ const createVersion = (overrides: Partial<VersionHistory> = {}): 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', () => {
|
||||
|
||||
@ -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: () => <div>scroll-button</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../online-users', () => ({
|
||||
default: () => <div>online-users</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../env-button', () => ({
|
||||
default: ({ disabled }: { disabled: boolean }) => <div data-testid="env-button">{`${disabled}`}</div>,
|
||||
}))
|
||||
@ -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({
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -41,7 +41,7 @@ const renderFormInputItem = (props: Partial<ComponentProps<typeof FormInputItem>
|
||||
renderWorkflowFlowComponent(
|
||||
<FormInputItem
|
||||
readOnly={false}
|
||||
nodeId="node-1"
|
||||
nodeId=""
|
||||
schema={createSchema()}
|
||||
value={{
|
||||
field: {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import type { InputVar, Node } from '../../../types'
|
||||
import type { IterationNodeType } from '../types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { act } from '@testing-library/react'
|
||||
import { renderWorkflowHook } from '@/app/components/workflow/__tests__/workflow-test-env'
|
||||
import { BlockEnum, ErrorHandleMode, InputVarType, VarType } from '@/app/components/workflow/types'
|
||||
import useSingleRunFormParams from '../use-single-run-form-params'
|
||||
|
||||
@ -94,7 +95,7 @@ describe('iteration/use-single-run-form-params', () => {
|
||||
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'],
|
||||
|
||||
@ -82,6 +82,8 @@ vi.mock('@/app/components/workflow/block-selector', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('../../loop-start', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div>loop-start-node</div>,
|
||||
LoopStartNodeDumb: () => <div>loop-start-node</div>,
|
||||
}))
|
||||
|
||||
|
||||
@ -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: () => <button type="button">Install Plugin</button>,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useGetLanguage: () => 'en-US',
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-strategy', () => ({
|
||||
useStrategyProviders: () => ({ data: undefined }),
|
||||
}))
|
||||
|
||||
const createNodeData = (overrides: Partial<ToolNodeType> = {}): ToolNodeType => ({
|
||||
title: 'Google Search',
|
||||
desc: '',
|
||||
@ -37,6 +55,8 @@ const createNodeData = (overrides: Partial<ToolNodeType> = {}): ToolNodeType =>
|
||||
describe('ToolNode', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseNodes.mockReturnValue([])
|
||||
mockUseNodesMetaData.mockReturnValue({ nodesMap: {} })
|
||||
mockUseNodePluginInstallation.mockReturnValue({
|
||||
isChecking: false,
|
||||
isMissing: false,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(
|
||||
<WorkflowPreview />,
|
||||
{
|
||||
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<Shape['workflowRunningData']>,
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
@ -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: <T,>(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)
|
||||
})
|
||||
|
||||
|
||||
@ -77,7 +77,7 @@ describe('variable-modal helpers', () => {
|
||||
objectValue: [],
|
||||
type: ChatVarType.Boolean,
|
||||
value: undefined,
|
||||
})).toBe(true)
|
||||
})).toBe(false)
|
||||
|
||||
expect(formatChatVariableValue({
|
||||
editInJSON: false,
|
||||
|
||||
@ -109,7 +109,7 @@ vi.mock('@/app/components/base/chat/chat', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../hooks', () => ({
|
||||
vi.mock('../hooks/use-chat', () => ({
|
||||
useChat: (...args: unknown[]) => mockUseChat(...args),
|
||||
}))
|
||||
|
||||
|
||||
@ -220,6 +220,7 @@ describe('useFileOperations', () => {
|
||||
nodeId: 'node-from-node',
|
||||
node,
|
||||
treeRef,
|
||||
nodeType: 'file',
|
||||
appId: 'app-1',
|
||||
storeApi: mocks.workflowStore,
|
||||
treeData: mocks.treeData,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>): 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)
|
||||
})
|
||||
|
||||
|
||||
@ -6339,9 +6339,6 @@
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"perfectionist/sort-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user