mirror of
https://github.com/langgenius/dify.git
synced 2026-04-15 18:06:36 +08:00
fix: web unittests
This commit is contained in:
parent
c3c84419e7
commit
5c88acc5f4
@ -15,6 +15,7 @@ const mockOpenAsyncWindow = vi.fn()
|
||||
const mockFetchInstalledAppList = vi.fn()
|
||||
const mockFetchAppDetailDirect = vi.fn()
|
||||
const mockToastError = vi.fn()
|
||||
const mockInvalidateAppWorkflow = vi.fn()
|
||||
|
||||
const sectionProps = vi.hoisted(() => ({
|
||||
summary: null as null | Record<string, any>,
|
||||
@ -88,6 +89,10 @@ vi.mock('@/service/apps', () => ({
|
||||
fetchAppDetailDirect: (...args: unknown[]) => mockFetchAppDetailDirect(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-workflow', () => ({
|
||||
useInvalidateAppWorkflow: () => mockInvalidateAppWorkflow,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
toast: {
|
||||
error: (...args: unknown[]) => mockToastError(...args),
|
||||
|
||||
@ -386,7 +386,7 @@ describe('List', () => {
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle multiple renders without issues', () => {
|
||||
const { rerender } = renderWithNuqs(<List />)
|
||||
const { rerender, unmount } = renderWithNuqs(<List />)
|
||||
expect(screen.getByText('app.types.all')).toBeInTheDocument()
|
||||
|
||||
unmount()
|
||||
|
||||
@ -31,6 +31,9 @@ const mocks = vi.hoisted(() => {
|
||||
registerNodeTransform: vi.fn(() => vi.fn()),
|
||||
dispatchCommand: vi.fn(),
|
||||
getRootElement: vi.fn(() => rootElement),
|
||||
getEditorState: vi.fn(() => ({
|
||||
read: (fn: () => boolean) => fn(),
|
||||
})),
|
||||
parseEditorState: vi.fn(() => ({ state: 'parsed' })),
|
||||
setEditorState: vi.fn(),
|
||||
focus: vi.fn(),
|
||||
|
||||
@ -85,6 +85,10 @@ vi.mock('@/config', () => ({
|
||||
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 },
|
||||
API_PREFIX: 'http://localhost:5001/console/api',
|
||||
APP_VERSION: '1.0.0',
|
||||
IS_MARKETPLACE: false,
|
||||
MARKETPLACE_API_PREFIX: 'http://localhost:5001/marketplace/api',
|
||||
IS_DEV: false,
|
||||
IS_CE_EDITION: false,
|
||||
}))
|
||||
|
||||
@ -59,6 +59,9 @@ vi.mock('@/app/components/base/features/hooks', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useStore: <T,>(selector: (state: { appId: string }) => T) => selector({
|
||||
appId: 'app-1',
|
||||
}),
|
||||
useWorkflowStore: () => ({
|
||||
getState: () => ({
|
||||
setConversationVariables: mockSetConversationVariables,
|
||||
@ -67,6 +70,32 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
useReactFlow: () => ({
|
||||
getNodes: () => [],
|
||||
setNodes: vi.fn(),
|
||||
getEdges: () => [],
|
||||
setEdges: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/collaboration/hooks/use-collaboration', () => ({
|
||||
useCollaboration: () => ({
|
||||
startCursorTracking: vi.fn(),
|
||||
stopCursorTracking: vi.fn(),
|
||||
onlineUsers: [],
|
||||
cursors: {},
|
||||
isConnected: false,
|
||||
isEnabled: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks/use-workflow-interactions', () => ({
|
||||
useWorkflowUpdate: () => ({
|
||||
handleUpdateWorkflowCanvas: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow', () => ({
|
||||
WorkflowWithInnerContext: ({
|
||||
nodes,
|
||||
@ -87,7 +116,7 @@ vi.mock('@/app/components/workflow', () => ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onWorkflowDataUpdate?.({
|
||||
features: { file: { enabled: true } },
|
||||
features: { file_upload: { enabled: true } },
|
||||
conversation_variables: [{ id: 'conversation-1' }],
|
||||
environment_variables: [{ id: 'env-1' }],
|
||||
})}
|
||||
@ -204,7 +233,9 @@ describe('WorkflowMain', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /update-workflow-data/i }))
|
||||
|
||||
expect(mockSetFeatures).toHaveBeenCalledWith({ file: { enabled: true } })
|
||||
expect(mockSetFeatures).toHaveBeenCalledWith(expect.objectContaining({
|
||||
file: expect.objectContaining({ enabled: true }),
|
||||
}))
|
||||
expect(mockSetConversationVariables).toHaveBeenCalledWith([{ id: 'conversation-1' }])
|
||||
expect(mockSetEnvironmentVariables).toHaveBeenCalledWith([{ id: 'env-1' }])
|
||||
})
|
||||
|
||||
@ -8,8 +8,25 @@ import { InputVarType } from '../types'
|
||||
import { createStartNode } from './fixtures'
|
||||
import { renderWorkflowFlowComponent } from './workflow-test-env'
|
||||
|
||||
const mockHandleSyncWorkflowDraft = vi.fn()
|
||||
const mockHandleAddVariable = vi.fn()
|
||||
const mockUpdateFeatures = vi.fn()
|
||||
const mockFeaturesStore = {
|
||||
getState: () => ({
|
||||
features: {
|
||||
opening: {
|
||||
enabled: false,
|
||||
opening_statement: '',
|
||||
suggested_questions: [],
|
||||
},
|
||||
suggested: false,
|
||||
text2speech: false,
|
||||
speech2text: false,
|
||||
citation: false,
|
||||
moderation: false,
|
||||
file: false,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
let mockIsChatMode = true
|
||||
let mockNodesReadOnly = false
|
||||
@ -22,9 +39,6 @@ vi.mock('../hooks', async () => {
|
||||
useNodesReadOnly: () => ({
|
||||
nodesReadOnly: mockNodesReadOnly,
|
||||
}),
|
||||
useNodesSyncDraft: () => ({
|
||||
handleSyncWorkflowDraft: mockHandleSyncWorkflowDraft,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
@ -34,6 +48,14 @@ vi.mock('../nodes/start/use-config', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/workflow', () => ({
|
||||
updateFeatures: (...args: unknown[]) => mockUpdateFeatures(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/features/hooks', () => ({
|
||||
useFeaturesStore: () => mockFeaturesStore,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/features/new-feature-panel', () => ({
|
||||
default: ({
|
||||
show,
|
||||
@ -112,21 +134,29 @@ const DelayedFeatures = () => {
|
||||
return <Features />
|
||||
}
|
||||
|
||||
const renderFeatures = (options?: Omit<Parameters<typeof renderWorkflowFlowComponent>[1], 'nodes' | 'edges'>) =>
|
||||
renderWorkflowFlowComponent(
|
||||
const renderFeatures = (options?: Omit<Parameters<typeof renderWorkflowFlowComponent>[1], 'nodes' | 'edges'>) => {
|
||||
const mergedInitialStoreState = {
|
||||
appId: 'app-1',
|
||||
...(options?.initialStoreState || {}),
|
||||
}
|
||||
|
||||
return renderWorkflowFlowComponent(
|
||||
<DelayedFeatures />,
|
||||
{
|
||||
nodes: [startNode],
|
||||
edges: [],
|
||||
...options,
|
||||
initialStoreState: mergedInitialStoreState,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
describe('Features', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsChatMode = true
|
||||
mockNodesReadOnly = false
|
||||
mockUpdateFeatures.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
@ -146,8 +176,10 @@ describe('Features', () => {
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'open features' }))
|
||||
|
||||
expect(mockHandleSyncWorkflowDraft).toHaveBeenCalledTimes(1)
|
||||
expect(store.getState().showFeaturesPanel).toBe(true)
|
||||
await vi.waitFor(() => {
|
||||
expect(mockUpdateFeatures).toHaveBeenCalledTimes(1)
|
||||
expect(store.getState().showFeaturesPanel).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('should close the workflow feature panel and transform required prompt variables', async () => {
|
||||
|
||||
@ -110,6 +110,12 @@ vi.mock('@/next/dynamic', () => ({
|
||||
default: () => () => null,
|
||||
}))
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useParams: () => ({
|
||||
appId: 'app-1',
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/event-emitter', () => ({
|
||||
useEventEmitterContextContext: () => ({
|
||||
eventEmitter: {
|
||||
@ -254,6 +260,7 @@ vi.mock('../hooks', () => ({
|
||||
useWorkflowRefreshDraft: () => ({
|
||||
handleRefreshWorkflowDraft: vi.fn(),
|
||||
}),
|
||||
useLeaderRestoreListener: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../hooks/use-workflow-search', () => ({
|
||||
|
||||
@ -9,6 +9,7 @@ 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: () => ({
|
||||
@ -42,6 +43,9 @@ vi.mock('../../hooks', () => ({
|
||||
useWorkflowRefreshDraft: () => ({
|
||||
handleRefreshWorkflowDraft: mockHandleRefreshWorkflowDraft,
|
||||
}),
|
||||
useLeaderRestore: () => ({
|
||||
requestRestore: mockRequestRestore,
|
||||
}),
|
||||
}))
|
||||
|
||||
const createVersion = (overrides: Partial<VersionHistory> = {}): VersionHistory => ({
|
||||
|
||||
@ -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()
|
||||
@ -33,6 +33,9 @@ vi.mock('../../hooks', () => ({
|
||||
handleBackupDraft: mockHandleBackupDraft,
|
||||
handleLoadBackupDraft: mockHandleLoadBackupDraft,
|
||||
}),
|
||||
useLeaderRestore: () => ({
|
||||
requestRestore: mockRequestRestore,
|
||||
}),
|
||||
useNodesSyncDraft: () => ({
|
||||
handleSyncWorkflowDraft: vi.fn(),
|
||||
}),
|
||||
@ -55,9 +58,6 @@ vi.mock('@/hooks/use-theme', () => ({
|
||||
|
||||
vi.mock('@/service/use-workflow', () => ({
|
||||
useInvalidAllLastRun: () => mockInvalidAllLastRun,
|
||||
useRestoreWorkflow: () => ({
|
||||
mutateAsync: mockRestoreWorkflow,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
@ -77,6 +77,10 @@ vi.mock('../scroll-to-selected-node-button', () => ({
|
||||
default: () => <div>scroll-button</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../online-users', () => ({
|
||||
default: () => <div data-testid="online-users" />,
|
||||
}))
|
||||
|
||||
vi.mock('../env-button', () => ({
|
||||
default: ({ disabled }: { disabled: boolean }) => <div data-testid="env-button">{`${disabled}`}</div>,
|
||||
}))
|
||||
@ -162,7 +166,13 @@ describe('Header layout components', () => {
|
||||
mockNodesReadOnly = false
|
||||
mockTheme = 'light'
|
||||
mockUseNodes.mockReturnValue([])
|
||||
mockRestoreWorkflow.mockResolvedValue(undefined)
|
||||
mockRequestRestore.mockImplementation((_payload: unknown, callbacks?: {
|
||||
onSuccess?: () => void
|
||||
onSettled?: () => void
|
||||
}) => {
|
||||
callbacks?.onSuccess?.()
|
||||
callbacks?.onSettled?.()
|
||||
})
|
||||
})
|
||||
|
||||
describe('HeaderInNormal', () => {
|
||||
@ -267,7 +277,7 @@ 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).toHaveBeenCalledTimes(1)
|
||||
expect(store.getState().showWorkflowVersionHistoryPanel).toBe(false)
|
||||
expect(store.getState().isRestoring).toBe(false)
|
||||
expect(store.getState().backupDraft).toBeUndefined()
|
||||
|
||||
@ -1,17 +1,14 @@
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import UndoRedo from '../undo-redo'
|
||||
|
||||
type TemporalSnapshot = {
|
||||
pastStates: unknown[]
|
||||
futureStates: unknown[]
|
||||
}
|
||||
|
||||
const mockUnsubscribe = vi.fn()
|
||||
const mockTemporalSubscribe = vi.fn()
|
||||
const mockCanUndo = vi.fn()
|
||||
const mockCanRedo = vi.fn()
|
||||
const mockOnUndoRedoStateChange = vi.fn()
|
||||
const mockHandleUndo = vi.fn()
|
||||
const mockHandleRedo = vi.fn()
|
||||
|
||||
let latestTemporalListener: ((state: TemporalSnapshot) => void) | undefined
|
||||
let latestUndoRedoListener: ((state: { canUndo: boolean, canRedo: boolean }) => void) | undefined
|
||||
let mockNodesReadOnly = false
|
||||
|
||||
vi.mock('@/app/components/workflow/header/view-workflow-history', () => ({
|
||||
@ -26,16 +23,22 @@ vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
|
||||
vi.mock('@/app/components/workflow/workflow-history-store', () => ({
|
||||
useWorkflowHistoryStore: () => ({
|
||||
store: {
|
||||
temporal: {
|
||||
subscribe: mockTemporalSubscribe,
|
||||
},
|
||||
},
|
||||
shortcutsEnabled: true,
|
||||
setShortcutsEnabled: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/collaboration/core/collaboration-manager', () => ({
|
||||
collaborationManager: {
|
||||
canUndo: () => mockCanUndo(),
|
||||
canRedo: () => mockCanRedo(),
|
||||
onUndoRedoStateChange: (listener: (state: { canUndo: boolean, canRedo: boolean }) => void) => {
|
||||
latestUndoRedoListener = listener
|
||||
return mockOnUndoRedoStateChange(listener)
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/divider', () => ({
|
||||
default: () => <div data-testid="divider" />,
|
||||
}))
|
||||
@ -48,20 +51,19 @@ describe('UndoRedo', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockNodesReadOnly = false
|
||||
latestTemporalListener = undefined
|
||||
mockTemporalSubscribe.mockImplementation((listener: (state: TemporalSnapshot) => void) => {
|
||||
latestTemporalListener = listener
|
||||
return mockUnsubscribe
|
||||
})
|
||||
latestUndoRedoListener = undefined
|
||||
mockCanUndo.mockReturnValue(false)
|
||||
mockCanRedo.mockReturnValue(false)
|
||||
mockOnUndoRedoStateChange.mockReturnValue(mockUnsubscribe)
|
||||
})
|
||||
|
||||
it('enables undo and redo when history exists and triggers the callbacks', () => {
|
||||
render(<UndoRedo handleRedo={mockHandleRedo} handleUndo={mockHandleUndo} />)
|
||||
|
||||
act(() => {
|
||||
latestTemporalListener?.({
|
||||
pastStates: [{}],
|
||||
futureStates: [{}],
|
||||
latestUndoRedoListener?.({
|
||||
canUndo: true,
|
||||
canRedo: true,
|
||||
})
|
||||
})
|
||||
|
||||
@ -93,9 +95,9 @@ describe('UndoRedo', () => {
|
||||
const redoButton = screen.getByRole('button', { name: 'workflow.common.redo' })
|
||||
|
||||
act(() => {
|
||||
latestTemporalListener?.({
|
||||
pastStates: [{}],
|
||||
futureStates: [{}],
|
||||
latestUndoRedoListener?.({
|
||||
canUndo: true,
|
||||
canRedo: true,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import { ChatVarType } from '../type'
|
||||
|
||||
type MockWorkflowStoreState = {
|
||||
setShowChatVariablePanel: (value: boolean) => void
|
||||
appId: string
|
||||
conversationVariables: ConversationVariable[]
|
||||
setConversationVariables: (value: ConversationVariable[]) => void
|
||||
}
|
||||
@ -17,10 +18,8 @@ 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 mockUpdateConversationVariables = vi.fn().mockResolvedValue(undefined)
|
||||
const mockFindUsedVarNodes = vi.fn<(selector: string[], nodes: Node[]) => Node[]>()
|
||||
const mockUpdateNodeVars = vi.fn<(node: Node, current: string[], next: string[]) => Node>()
|
||||
|
||||
@ -62,15 +61,14 @@ vi.mock('reactflow', () => ({
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useStore: <T,>(selector: (state: MockWorkflowStoreState) => T) => selector({
|
||||
setShowChatVariablePanel: mockSetShowChatVariablePanel,
|
||||
appId: 'app-1',
|
||||
conversationVariables: mockConversationVariables,
|
||||
setConversationVariables: mockSetConversationVariables,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks/use-nodes-sync-draft', () => ({
|
||||
useNodesSyncDraft: () => ({
|
||||
doSyncWorkflowDraft: mockDoSyncWorkflowDraft,
|
||||
}),
|
||||
vi.mock('@/service/workflow', () => ({
|
||||
updateConversationVariables: (...args: unknown[]) => mockUpdateConversationVariables(...args),
|
||||
}))
|
||||
|
||||
vi.mock('../../../hooks/use-inspect-vars-crud', () => ({
|
||||
@ -175,6 +173,7 @@ describe('ChatVariablePanel', () => {
|
||||
vi.clearAllMocks()
|
||||
mockConversationVariables = [createConversationVariable()]
|
||||
mockFlowNodes = [createNode('node-1'), createNode('node-2')]
|
||||
mockUpdateConversationVariables.mockResolvedValue(undefined)
|
||||
mockFindUsedVarNodes.mockReturnValue([])
|
||||
mockUpdateNodeVars.mockImplementation((node: Node) => node)
|
||||
})
|
||||
@ -207,9 +206,15 @@ describe('ChatVariablePanel', () => {
|
||||
expect.objectContaining({ id: 'var-added', name: 'fresh_var' }),
|
||||
createConversationVariable(),
|
||||
])
|
||||
expect(mockUpdateConversationVariables).toHaveBeenCalledWith({
|
||||
appId: 'app-1',
|
||||
conversationVariables: [
|
||||
expect.objectContaining({ id: 'var-added', name: 'fresh_var' }),
|
||||
createConversationVariable(),
|
||||
],
|
||||
})
|
||||
expect(mockInvalidateConversationVarValues).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
expect(mockDoSyncWorkflowDraft).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateConversationVarValues).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should rename existing variables and update affected node references', async () => {
|
||||
|
||||
@ -324,7 +324,7 @@ const SelectionContextmenu = () => {
|
||||
if (alignType === AlignType.DistributeHorizontal || alignType === AlignType.DistributeVertical) {
|
||||
const distributedNodes = distributeNodes(nodesToAlign, nodes, alignType)
|
||||
if (distributedNodes) {
|
||||
setNodes(distributeNodes)
|
||||
setNodes(distributedNodes)
|
||||
handleSelectionContextmenuCancel()
|
||||
|
||||
const { setHelpLineHorizontal, setHelpLineVertical } = workflowStore.getState()
|
||||
|
||||
@ -173,9 +173,9 @@ describe('createWorkflowStore', () => {
|
||||
expect(store.getState().controlMode).toBe('pointer')
|
||||
})
|
||||
|
||||
it('should default controlMode to hand when localStorage has no value', () => {
|
||||
it('should default controlMode to pointer when localStorage has no value', () => {
|
||||
const store = createStore()
|
||||
expect(store.getState().controlMode).toBe('hand')
|
||||
expect(store.getState().controlMode).toBe('pointer')
|
||||
})
|
||||
|
||||
it('should read panelWidth from localStorage', () => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user