test(workflow): reorganize specs into __tests__ and align with shared test infrastructure (#33625)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Coding On Star
2026-03-18 16:40:28 +08:00
committed by GitHub
parent 387e5a345f
commit db4deb1d6b
39 changed files with 3538 additions and 203 deletions

View File

@ -0,0 +1,323 @@
import type { Shape as HooksStoreShape } from '../../hooks-store/store'
import type { RunFile } from '../../types'
import type { FileUpload } from '@/app/components/base/features/types'
import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import ReactFlow, { ReactFlowProvider } from 'reactflow'
import { TransferMethod } from '@/types/app'
import { FlowType } from '@/types/common'
import { createStartNode } from '../../__tests__/fixtures'
import { renderWorkflowComponent } from '../../__tests__/workflow-test-env'
import { InputVarType, WorkflowRunningStatus } from '../../types'
import InputsPanel from '../inputs-panel'
const mockCheckInputsForm = vi.fn()
const mockNotify = vi.fn()
vi.mock('next/navigation', () => ({
useParams: () => ({}),
}))
vi.mock('@/app/components/base/toast/context', () => ({
useToastContext: () => ({
notify: mockNotify,
close: vi.fn(),
}),
}))
vi.mock('@/app/components/base/chat/chat/check-input-forms-hooks', () => ({
useCheckInputsForms: () => ({
checkInputsForm: mockCheckInputsForm,
}),
}))
const fileSettingsWithImage = {
enabled: true,
image: {
enabled: true,
},
allowed_file_upload_methods: [TransferMethod.remote_url],
number_limits: 3,
image_file_size_limit: 10,
} satisfies FileUpload & { image_file_size_limit: number }
const uploadedRunFile = {
transfer_method: TransferMethod.remote_url,
upload_file_id: 'file-2',
} as unknown as RunFile
const uploadingRunFile = {
transfer_method: TransferMethod.local_file,
} as unknown as RunFile
const createHooksStoreProps = (
overrides: Partial<HooksStoreShape> = {},
): Partial<HooksStoreShape> => ({
handleRun: vi.fn(),
configsMap: {
flowId: 'flow-1',
flowType: FlowType.appFlow,
fileSettings: fileSettingsWithImage,
},
...overrides,
})
const renderInputsPanel = (
startNode: ReturnType<typeof createStartNode>,
options?: Parameters<typeof renderWorkflowComponent>[1],
) => {
return renderWorkflowComponent(
<div style={{ width: 800, height: 600 }}>
<ReactFlowProvider>
<ReactFlow nodes={[startNode]} edges={[]} fitView />
<InputsPanel onRun={vi.fn()} />
</ReactFlowProvider>
</div>,
options,
)
}
describe('InputsPanel', () => {
beforeEach(() => {
vi.clearAllMocks()
mockCheckInputsForm.mockReturnValue(true)
})
describe('Rendering', () => {
it('should render current inputs, defaults, and the image uploader from the start node', () => {
renderInputsPanel(
createStartNode({
data: {
variables: [
{
type: InputVarType.textInput,
variable: 'question',
label: 'Question',
required: true,
default: 'default question',
},
{
type: InputVarType.number,
variable: 'count',
label: 'Count',
required: false,
default: '2',
},
],
},
}),
{
initialStoreState: {
inputs: {
question: 'overridden question',
},
},
hooksStoreProps: createHooksStoreProps(),
},
)
expect(screen.getByDisplayValue('overridden question')).toHaveFocus()
expect(screen.getByRole('spinbutton')).toHaveValue(2)
expect(screen.getByText('common.imageUploader.pasteImageLink')).toBeInTheDocument()
})
})
describe('User Interactions', () => {
it('should update workflow inputs and image files when users edit the form', async () => {
const user = userEvent.setup()
const { store } = renderInputsPanel(
createStartNode({
data: {
variables: [
{
type: InputVarType.textInput,
variable: 'question',
label: 'Question',
required: true,
},
],
},
}),
{
hooksStoreProps: createHooksStoreProps(),
},
)
await user.type(screen.getByPlaceholderText('Question'), 'changed question')
expect(store.getState().inputs).toEqual({ question: 'changed question' })
await user.click(screen.getByText('common.imageUploader.pasteImageLink'))
await user.type(
await screen.findByPlaceholderText('common.imageUploader.pasteImageLinkInputPlaceholder'),
'https://example.com/image.png',
)
await user.click(screen.getByRole('button', { name: 'common.operation.ok' }))
await waitFor(() => {
expect(store.getState().files).toEqual([{
type: 'image',
transfer_method: TransferMethod.remote_url,
url: 'https://example.com/image.png',
upload_file_id: '',
}])
})
})
it('should not start a run when input validation fails', async () => {
const user = userEvent.setup()
mockCheckInputsForm.mockReturnValue(false)
const onRun = vi.fn()
const handleRun = vi.fn()
renderWorkflowComponent(
<div style={{ width: 800, height: 600 }}>
<ReactFlowProvider>
<ReactFlow
nodes={[
createStartNode({
data: {
variables: [
{
type: InputVarType.textInput,
variable: 'question',
label: 'Question',
required: true,
default: 'default question',
},
],
},
}),
]}
edges={[]}
fitView
/>
<InputsPanel onRun={onRun} />
</ReactFlowProvider>
</div>,
{
hooksStoreProps: createHooksStoreProps({ handleRun }),
},
)
await user.click(screen.getByRole('button', { name: 'workflow.singleRun.startRun' }))
expect(mockCheckInputsForm).toHaveBeenCalledWith(
{ question: 'default question' },
expect.arrayContaining([
expect.objectContaining({ variable: 'question' }),
expect.objectContaining({ variable: '__image' }),
]),
)
expect(onRun).not.toHaveBeenCalled()
expect(handleRun).not.toHaveBeenCalled()
})
it('should start a run with processed inputs when validation succeeds', async () => {
const user = userEvent.setup()
const onRun = vi.fn()
const handleRun = vi.fn()
renderWorkflowComponent(
<div style={{ width: 800, height: 600 }}>
<ReactFlowProvider>
<ReactFlow
nodes={[
createStartNode({
data: {
variables: [
{
type: InputVarType.textInput,
variable: 'question',
label: 'Question',
required: true,
},
{
type: InputVarType.checkbox,
variable: 'confirmed',
label: 'Confirmed',
required: false,
},
],
},
}),
]}
edges={[]}
fitView
/>
<InputsPanel onRun={onRun} />
</ReactFlowProvider>
</div>,
{
initialStoreState: {
inputs: {
question: 'run this',
confirmed: 'truthy',
},
files: [uploadedRunFile],
},
hooksStoreProps: createHooksStoreProps({
handleRun,
configsMap: {
flowId: 'flow-1',
flowType: FlowType.appFlow,
fileSettings: {
enabled: false,
},
},
}),
},
)
await user.click(screen.getByRole('button', { name: 'workflow.singleRun.startRun' }))
expect(onRun).toHaveBeenCalledTimes(1)
expect(handleRun).toHaveBeenCalledWith({
inputs: {
question: 'run this',
confirmed: true,
},
files: [uploadedRunFile],
})
})
})
describe('Disabled States', () => {
it('should disable the run button while a local file is still uploading', () => {
renderInputsPanel(createStartNode(), {
initialStoreState: {
files: [uploadingRunFile],
},
hooksStoreProps: createHooksStoreProps({
configsMap: {
flowId: 'flow-1',
flowType: FlowType.appFlow,
fileSettings: {
enabled: false,
},
},
}),
})
expect(screen.getByRole('button', { name: 'workflow.singleRun.startRun' })).toBeDisabled()
})
it('should disable the run button while the workflow is already running', () => {
renderInputsPanel(createStartNode(), {
initialStoreState: {
workflowRunningData: {
result: {
status: WorkflowRunningStatus.Running,
inputs_truncated: false,
process_data_truncated: false,
outputs_truncated: false,
},
tracing: [],
},
},
hooksStoreProps: createHooksStoreProps(),
})
expect(screen.getByRole('button', { name: 'workflow.singleRun.startRun' })).toBeDisabled()
})
})
})

View File

@ -0,0 +1,163 @@
import type { WorkflowRunDetailResponse } from '@/models/log'
import { act, screen } from '@testing-library/react'
import { createEdge, createNode } from '../../__tests__/fixtures'
import { renderWorkflowComponent } from '../../__tests__/workflow-test-env'
import Record from '../record'
const mockHandleUpdateWorkflowCanvas = vi.fn()
const mockFormatWorkflowRunIdentifier = vi.fn((finishedAt?: number) => finishedAt ? ' (Finished)' : ' (Running)')
let latestGetResultCallback: ((res: WorkflowRunDetailResponse) => void) | undefined
vi.mock('@/app/components/workflow/hooks', () => ({
useWorkflowUpdate: () => ({
handleUpdateWorkflowCanvas: mockHandleUpdateWorkflowCanvas,
}),
}))
vi.mock('@/app/components/workflow/run', () => ({
default: ({
runDetailUrl,
tracingListUrl,
getResultCallback,
}: {
runDetailUrl: string
tracingListUrl: string
getResultCallback: (res: WorkflowRunDetailResponse) => void
}) => {
latestGetResultCallback = getResultCallback
return (
<div
data-run-detail-url={runDetailUrl}
data-testid="run"
data-tracing-list-url={tracingListUrl}
/>
)
},
}))
vi.mock('@/app/components/workflow/utils', () => ({
formatWorkflowRunIdentifier: (finishedAt?: number) => mockFormatWorkflowRunIdentifier(finishedAt),
}))
const createRunDetail = (overrides: Partial<WorkflowRunDetailResponse> = {}): WorkflowRunDetailResponse => ({
id: 'run-1',
version: '1',
graph: {
nodes: [],
edges: [],
},
inputs: '{}',
inputs_truncated: false,
status: 'succeeded',
outputs: '{}',
outputs_truncated: false,
total_steps: 1,
created_by_role: 'account',
created_at: 1,
finished_at: 2,
...overrides,
})
describe('Record', () => {
beforeEach(() => {
vi.clearAllMocks()
latestGetResultCallback = undefined
})
it('renders the run title and passes run and trace URLs to the run panel', () => {
const getWorkflowRunAndTraceUrl = vi.fn((runId?: string) => ({
runUrl: `/runs/${runId}`,
traceUrl: `/traces/${runId}`,
}))
renderWorkflowComponent(<Record />, {
initialStoreState: {
historyWorkflowData: {
id: 'run-1',
status: 'succeeded',
finished_at: 1700000000000,
},
},
hooksStoreProps: {
getWorkflowRunAndTraceUrl,
},
})
expect(screen.getByText('Test Run (Finished)')).toBeInTheDocument()
expect(screen.getByTestId('run')).toHaveAttribute('data-run-detail-url', '/runs/run-1')
expect(screen.getByTestId('run')).toHaveAttribute('data-tracing-list-url', '/traces/run-1')
expect(getWorkflowRunAndTraceUrl).toHaveBeenCalledTimes(2)
expect(getWorkflowRunAndTraceUrl).toHaveBeenNthCalledWith(1, 'run-1')
expect(getWorkflowRunAndTraceUrl).toHaveBeenNthCalledWith(2, 'run-1')
expect(mockFormatWorkflowRunIdentifier).toHaveBeenCalledWith(1700000000000)
})
it('updates the workflow canvas with a fallback viewport when the response omits one', () => {
const nodes = [createNode({ id: 'node-1' })]
const edges = [createEdge({ id: 'edge-1' })]
renderWorkflowComponent(<Record />, {
initialStoreState: {
historyWorkflowData: {
id: 'run-1',
status: 'succeeded',
},
},
hooksStoreProps: {
getWorkflowRunAndTraceUrl: () => ({ runUrl: '/runs/run-1', traceUrl: '/traces/run-1' }),
},
})
expect(latestGetResultCallback).toBeDefined()
act(() => {
latestGetResultCallback?.(createRunDetail({
graph: {
nodes,
edges,
},
}))
})
expect(mockHandleUpdateWorkflowCanvas).toHaveBeenCalledWith({
nodes,
edges,
viewport: { x: 0, y: 0, zoom: 1 },
})
})
it('uses the response viewport when one is available', () => {
const nodes = [createNode({ id: 'node-1' })]
const edges = [createEdge({ id: 'edge-1' })]
const viewport = { x: 12, y: 24, zoom: 0.75 }
renderWorkflowComponent(<Record />, {
initialStoreState: {
historyWorkflowData: {
id: 'run-1',
status: 'succeeded',
},
},
hooksStoreProps: {
getWorkflowRunAndTraceUrl: () => ({ runUrl: '/runs/run-1', traceUrl: '/traces/run-1' }),
},
})
act(() => {
latestGetResultCallback?.(createRunDetail({
graph: {
nodes,
edges,
viewport,
},
}))
})
expect(mockHandleUpdateWorkflowCanvas).toHaveBeenCalledWith({
nodes,
edges,
viewport,
})
})
})