fix: web unittests

This commit is contained in:
hjlarry 2026-04-09 15:55:37 +08:00
parent c3c84419e7
commit 5c88acc5f4
13 changed files with 155 additions and 52 deletions

View File

@ -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),

View File

@ -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()

View File

@ -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(),

View File

@ -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,
}))

View File

@ -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' }])
})

View File

@ -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 () => {

View File

@ -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', () => ({

View File

@ -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 => ({

View File

@ -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()

View File

@ -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,
})
})

View File

@ -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 () => {

View File

@ -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()

View File

@ -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', () => {