This commit is contained in:
Stephen Zhou
2026-03-25 19:25:22 +08:00
parent f0041ec619
commit a7178b4d5c
40 changed files with 652 additions and 156 deletions

View File

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

View File

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

View File

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

View File

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

View File

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