test(workflow): add unit tests for workflow components (#33910)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
Coding On Star
2026-03-23 16:37:03 +08:00
committed by GitHub
parent abda859075
commit fdc880bc67
54 changed files with 12469 additions and 189 deletions

View File

@ -0,0 +1,162 @@
import type { HumanInputFormData } from '@/types/workflow'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { CUSTOM_NODE } from '@/app/components/workflow/constants'
import { DeliveryMethodType, UserActionButtonType } from '@/app/components/workflow/nodes/human-input/types'
import { InputVarType } from '@/app/components/workflow/types'
import HumanInputFormList from '../human-input-form-list'
const mockNodes: Array<{
id: string
type: string
data: {
delivery_methods: Array<Record<string, unknown>>
}
}> = []
vi.mock('reactflow', () => ({
useStoreApi: () => ({
getState: () => ({
getNodes: () => mockNodes,
}),
}),
}))
vi.mock('@/context/app-context', () => ({
useSelector: <T,>(selector: (state: { userProfile: { email: string } }) => T) => selector({
userProfile: { email: 'debug@example.com' },
}),
}))
vi.mock('@/context/i18n', () => ({
useLocale: () => 'en-US',
}))
const createFormData = (overrides: Partial<HumanInputFormData> = {}): HumanInputFormData => ({
form_id: 'form-1',
node_id: 'human-node-1',
node_title: 'Need Approval',
form_content: 'Before {{#$output.reason#}} after',
inputs: [{
type: InputVarType.paragraph,
output_variable_name: 'reason',
default: {
selector: [],
type: 'constant',
value: 'prefill',
},
}],
actions: [{
id: 'approve',
title: 'Approve',
button_style: UserActionButtonType.Primary,
}],
form_token: 'token-1',
resolved_default_values: {},
display_in_ui: true,
expiration_time: 2_000_000_000,
...overrides,
})
describe('HumanInputFormList', () => {
beforeEach(() => {
vi.clearAllMocks()
mockNodes.splice(0, mockNodes.length)
})
it('should render only visible forms, derive delivery method tips, and submit updated inputs', async () => {
const user = userEvent.setup()
const onHumanInputFormSubmit = vi.fn().mockResolvedValue(undefined)
mockNodes.push(
{
id: 'human-node-1',
type: CUSTOM_NODE,
data: {
delivery_methods: [{
id: 'email-1',
type: DeliveryMethodType.Email,
enabled: true,
config: {
recipients: {
whole_workspace: false,
items: [],
},
subject: 'Need approval',
body: 'Please review',
debug_mode: true,
},
}],
},
},
{
id: 'human-node-2',
type: CUSTOM_NODE,
data: {
delivery_methods: [],
},
},
)
render(
<HumanInputFormList
humanInputFormDataList={[
createFormData(),
createFormData({
form_id: 'form-2',
node_id: 'human-node-2',
node_title: 'Hidden Form',
display_in_ui: false,
}),
]}
onHumanInputFormSubmit={onHumanInputFormSubmit}
/>,
)
expect(screen.getByText('Need Approval')).toBeInTheDocument()
expect(screen.queryByText('Hidden Form')).not.toBeInTheDocument()
expect(screen.getByDisplayValue('prefill')).toBeInTheDocument()
expect(screen.getByTestId('expiration-time')).toBeInTheDocument()
expect(screen.getByTestId('tips')).toBeInTheDocument()
await user.clear(screen.getByDisplayValue('prefill'))
await user.type(screen.getByTestId('content-item-textarea'), 'updated reason')
await user.click(screen.getByRole('button', { name: 'Approve' }))
expect(onHumanInputFormSubmit).toHaveBeenCalledWith('token-1', {
inputs: {
reason: 'updated reason',
},
action: 'approve',
})
})
it('should omit delivery tips when the node has no enabled delivery methods', () => {
mockNodes.push({
id: 'human-node-1',
type: CUSTOM_NODE,
data: {
delivery_methods: [],
},
})
render(
<HumanInputFormList
humanInputFormDataList={[
createFormData(),
]}
/>,
)
expect(screen.queryByTestId('tips')).not.toBeInTheDocument()
})
it('should render an empty container when there are no visible forms', () => {
render(
<HumanInputFormList
humanInputFormDataList={[]}
/>,
)
expect(screen.queryByTestId('content-wrapper')).not.toBeInTheDocument()
})
})

View File

@ -1,115 +1,252 @@
import type { PanelProps } from '../index'
import { screen } from '@testing-library/react'
import { createNode } from '../../__tests__/fixtures'
import { resetReactFlowMockState, rfState } from '../../__tests__/reactflow-mock-state'
import { renderWorkflowComponent } from '../../__tests__/workflow-test-env'
import { render, screen } from '@testing-library/react'
import * as React from 'react'
import Panel from '../index'
const mockVersionHistoryPanel = vi.hoisted(() => vi.fn())
class MockResizeObserver implements ResizeObserver {
observe = vi.fn()
unobserve = vi.fn()
disconnect = vi.fn()
constructor(_callback: ResizeObserverCallback) {}
type MockNodeData = {
selected?: boolean
title?: string
}
vi.mock('@/next/dynamic', () => ({
default: () => (props: { latestVersionId?: string }) => {
mockVersionHistoryPanel(props)
return <div data-testid="version-history-panel">{props.latestVersionId}</div>
},
type MockNode = {
id: string
type: string
data: MockNodeData
}
type MockPanelStoreState = {
showEnvPanel: boolean
isRestoring: boolean
showWorkflowVersionHistoryPanel: boolean
workflowCanvasWidth: number
previewPanelWidth: number
setPreviewPanelWidth: (value: number) => void
setRightPanelWidth: (value: number) => void
setOtherPanelWidth: (value: number) => void
}
type MockResizeMode = 'borderBox' | 'contentRect' | 'fallback'
let mockResizeModes: MockResizeMode[] = []
let mockResizeObservers: MockResizeObserver[] = []
const createResizeEntry = (mode: MockResizeMode): ResizeObserverEntry => ({
borderBoxSize: mode === 'borderBox'
? [{ inlineSize: 720, blockSize: 0 }] as ResizeObserverSize[]
: [],
contentBoxSize: [],
devicePixelContentBoxSize: [],
contentRect: {
width: mode === 'contentRect' ? 530 : 0,
height: 0,
x: 0,
y: 0,
top: 0,
right: 0,
bottom: 0,
left: 0,
toJSON: () => ({}),
} as DOMRectReadOnly,
target: document.createElement('div'),
} as unknown as ResizeObserverEntry)
class MockResizeObserver {
callback: ResizeObserverCallback
observe = vi.fn(() => {
if (!mockResizeModes.length)
return
this.callback(
mockResizeModes.map(createResizeEntry),
this as unknown as ResizeObserver,
)
})
disconnect = vi.fn()
unobserve = vi.fn()
constructor(callback: ResizeObserverCallback) {
this.callback = callback
mockResizeObservers.push(this)
}
}
let mockNodes: MockNode[] = []
let mockPanelStoreState: MockPanelStoreState
vi.mock('reactflow', () => ({
useStore: (selector: (state: { getNodes: () => MockNode[] }) => unknown) => selector({
getNodes: () => mockNodes,
}),
useStoreApi: () => ({
getState: () => ({
getNodes: () => mockNodes,
setNodes: vi.fn(),
}),
}),
}))
vi.mock('reactflow', async () => {
const mod = await import('../../__tests__/reactflow-mock-state')
const base = mod.createReactFlowModuleMock()
return {
...base,
useStore: vi.fn(selector => selector({
getNodes: () => mod.rfState.nodes,
})),
}
})
vi.mock('../env-panel', () => ({
default: () => <div data-testid="env-panel" />,
vi.mock('../../store', () => ({
useStore: <T,>(selector: (state: MockPanelStoreState) => T) => selector(mockPanelStoreState),
}))
vi.mock('../../nodes', () => ({
Panel: ({ id }: { id: string }) => <div data-testid="node-panel">{id}</div>,
Panel: ({ id, data }: { id: string, data: MockNodeData }) => (
<div data-testid="node-panel">{`${id}:${data.title || 'untitled'}`}</div>
),
}))
const versionHistoryPanelProps = {
latestVersionId: 'version-1',
restoreVersionUrl: (versionId: string) => `/workflows/${versionId}/restore`,
} satisfies NonNullable<PanelProps['versionHistoryPanelProps']>
vi.mock('@/app/components/workflow/panel/env-panel', () => ({
default: () => <div data-testid="env-panel">env-panel</div>,
}))
vi.mock('@/app/components/workflow/panel/version-history-panel', () => ({
default: ({ latestVersionId }: { latestVersionId?: string }) => (
<div data-testid="version-history-panel">{latestVersionId || 'none'}</div>
),
}))
vi.mock('@/next/dynamic', async () => {
const ReactModule = await import('react')
return {
default: (
loader: () => Promise<{ default: React.ComponentType<Record<string, unknown>> }>,
) => {
const DynamicComponent = (props: Record<string, unknown>) => {
const [Loaded, setLoaded] = ReactModule.useState<React.ComponentType<Record<string, unknown>> | null>(null)
ReactModule.useEffect(() => {
let mounted = true
loader().then((mod) => {
if (mounted)
setLoaded(() => mod.default)
})
return () => {
mounted = false
}
}, [])
return Loaded ? <Loaded {...props} /> : null
}
return DynamicComponent
},
}
})
describe('Panel', () => {
beforeEach(() => {
vi.clearAllMocks()
resetReactFlowMockState()
beforeAll(() => {
vi.stubGlobal('ResizeObserver', MockResizeObserver)
})
beforeEach(() => {
vi.clearAllMocks()
mockNodes = []
mockResizeModes = []
mockResizeObservers = []
mockPanelStoreState = {
showEnvPanel: false,
isRestoring: false,
showWorkflowVersionHistoryPanel: false,
workflowCanvasWidth: 0,
previewPanelWidth: 420,
setPreviewPanelWidth: vi.fn(),
setRightPanelWidth: vi.fn(),
setOtherPanelWidth: vi.fn(),
}
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(() => ({
width: 640,
height: 320,
top: 0,
right: 640,
bottom: 320,
left: 0,
x: 0,
y: 0,
toJSON: () => ({}),
}))
})
afterEach(() => {
vi.restoreAllMocks()
})
afterAll(() => {
vi.unstubAllGlobals()
})
describe('Version History Panel', () => {
it('should render the version history panel when the panel is open and props are provided', () => {
renderWorkflowComponent(
<Panel versionHistoryPanelProps={versionHistoryPanelProps} />,
{
initialStoreState: {
showWorkflowVersionHistoryPanel: true,
},
},
)
it('should render slots, selected node details, and secondary panels while constraining oversized preview widths', async () => {
mockNodes = [{
id: 'node-1',
type: 'custom',
data: {
selected: true,
title: 'Selected Node',
},
}]
mockPanelStoreState = {
...mockPanelStoreState,
showEnvPanel: true,
showWorkflowVersionHistoryPanel: true,
workflowCanvasWidth: 1000,
previewPanelWidth: 520,
}
expect(screen.getByTestId('version-history-panel')).toHaveTextContent('version-1')
expect(mockVersionHistoryPanel).toHaveBeenCalledWith(expect.objectContaining({
latestVersionId: 'version-1',
}))
})
render(
<Panel
components={{
left: <div>left-slot</div>,
right: <div>right-slot</div>,
}}
versionHistoryPanelProps={{
latestVersionId: 'version-1',
restoreVersionUrl: versionId => `/apps/app-1/workflows/${versionId}/restore`,
}}
/>,
)
it('should not render the version history panel when the panel is open but props are missing', () => {
renderWorkflowComponent(
<Panel />,
{
initialStoreState: {
showWorkflowVersionHistoryPanel: true,
},
},
)
expect(screen.getByText('left-slot')).toBeInTheDocument()
expect(screen.getByText('right-slot')).toBeInTheDocument()
expect(screen.getByTestId('node-panel')).toHaveTextContent('node-1:Selected Node')
expect(screen.getByTestId('env-panel')).toBeInTheDocument()
expect(await screen.findByTestId('version-history-panel')).toHaveTextContent('version-1')
expect(mockPanelStoreState.setPreviewPanelWidth).toHaveBeenCalledWith(400)
expect(mockPanelStoreState.setRightPanelWidth).toHaveBeenCalledWith(640)
expect(mockPanelStoreState.setOtherPanelWidth).toHaveBeenCalledWith(640)
})
expect(screen.queryByTestId('version-history-panel')).not.toBeInTheDocument()
expect(mockVersionHistoryPanel).not.toHaveBeenCalled()
})
it('should skip node and auxiliary panels when there is no selected node or open side panel state', () => {
render(
<Panel
components={{
left: <div>left-only</div>,
}}
/>,
)
it('should not render the version history panel when the panel is closed', () => {
rfState.nodes = [
createNode({
id: 'selected-node',
data: {
selected: true,
},
}),
] as typeof rfState.nodes
expect(screen.getByText('left-only')).toBeInTheDocument()
expect(screen.queryByTestId('node-panel')).not.toBeInTheDocument()
expect(screen.queryByTestId('env-panel')).not.toBeInTheDocument()
expect(screen.queryByTestId('version-history-panel')).not.toBeInTheDocument()
expect(mockPanelStoreState.setPreviewPanelWidth).not.toHaveBeenCalled()
})
renderWorkflowComponent(
<Panel versionHistoryPanelProps={versionHistoryPanelProps} />,
{
initialStoreState: {
showWorkflowVersionHistoryPanel: false,
},
},
)
it('should derive observer widths from border-box, content-rect, and fallback values and disconnect on unmount', () => {
mockResizeModes = ['borderBox', 'contentRect', 'fallback']
expect(screen.queryByTestId('version-history-panel')).not.toBeInTheDocument()
expect(screen.getByTestId('node-panel')).toHaveTextContent('selected-node')
})
const { unmount } = render(<Panel />)
expect(mockPanelStoreState.setRightPanelWidth).toHaveBeenCalledWith(720)
expect(mockPanelStoreState.setRightPanelWidth).toHaveBeenCalledWith(530)
expect(mockPanelStoreState.setRightPanelWidth).toHaveBeenCalledWith(640)
expect(mockPanelStoreState.setOtherPanelWidth).toHaveBeenCalledWith(720)
expect(mockPanelStoreState.setOtherPanelWidth).toHaveBeenCalledWith(530)
expect(mockPanelStoreState.setOtherPanelWidth).toHaveBeenCalledWith(640)
unmount()
expect(mockResizeObservers).toHaveLength(2)
mockResizeObservers.forEach(observer => expect(observer.disconnect).toHaveBeenCalledTimes(1))
})
})

View File

@ -0,0 +1,354 @@
import type { Shape } from '../../store/workflow'
import type { HumanInputFilledFormData, HumanInputFormData } from '@/types/workflow'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import copy from 'copy-to-clipboard'
import { toast } from '@/app/components/base/ui/toast'
import { createNodeTracing, createWorkflowRunningData } from '@/app/components/workflow/__tests__/fixtures'
import { renderWorkflowComponent } from '@/app/components/workflow/__tests__/workflow-test-env'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
import { submitHumanInputForm } from '@/service/workflow'
import WorkflowPreview from '../workflow-preview'
const mockHandleCancelDebugAndPreviewPanel = vi.fn()
vi.mock('copy-to-clipboard', () => ({
default: vi.fn(),
}))
vi.mock('@/app/components/base/ui/toast', () => ({
toast: {
success: vi.fn(),
},
}))
vi.mock('@/service/workflow', () => ({
submitHumanInputForm: vi.fn(),
}))
vi.mock('@/app/components/workflow/hooks', () => ({
useWorkflowInteractions: () => ({
handleCancelDebugAndPreviewPanel: mockHandleCancelDebugAndPreviewPanel,
}),
}))
vi.mock('@/app/components/workflow/run/result-panel', () => ({
default: ({ status }: { status?: string }) => <div data-testid="result-panel">{status}</div>,
}))
vi.mock('@/app/components/workflow/run/result-text', () => ({
default: ({
outputs,
isPaused,
isRunning,
onClick,
}: {
outputs?: string
isPaused?: boolean
isRunning?: boolean
onClick?: () => void
}) => (
<div>
<div data-testid="result-text">{JSON.stringify({ outputs, isPaused, isRunning })}</div>
<button type="button" onClick={onClick}>open-detail</button>
</div>
),
}))
vi.mock('@/app/components/workflow/run/tracing-panel', () => ({
default: ({ list }: { list: unknown[] }) => <div data-testid="tracing-panel">{list.length}</div>,
}))
vi.mock('@/app/components/workflow/panel/inputs-panel', () => ({
default: ({ onRun }: { onRun: () => void }) => (
<button type="button" onClick={onRun}>
run-inputs
</button>
),
}))
vi.mock('@/app/components/workflow/panel/human-input-form-list', () => ({
default: ({
humanInputFormDataList,
onHumanInputFormSubmit,
}: {
humanInputFormDataList: unknown[]
onHumanInputFormSubmit?: (token: string, formData: Record<string, string>) => Promise<void>
}) => (
<div>
<div data-testid="human-form-list">{humanInputFormDataList.length}</div>
<button type="button" onClick={() => onHumanInputFormSubmit?.('form-token', { answer: 'ok' })}>
submit-human-form
</button>
</div>
),
}))
vi.mock('@/app/components/workflow/panel/human-input-filled-form-list', () => ({
default: ({ humanInputFilledFormDataList }: { humanInputFilledFormDataList: unknown[] }) => (
<div data-testid="filled-form-list">{humanInputFilledFormDataList.length}</div>
),
}))
const mockCopy = vi.mocked(copy)
const mockToastSuccess = vi.mocked(toast.success)
const mockSubmitHumanInputForm = vi.mocked(submitHumanInputForm)
type WorkflowResult = NonNullable<ReturnType<typeof createWorkflowRunningData>['result']>
const createWorkflowResult = (overrides: Partial<WorkflowResult> = {}): WorkflowResult => ({
status: WorkflowRunningStatus.Running,
inputs_truncated: false,
process_data_truncated: false,
outputs_truncated: false,
...overrides,
})
const createHumanInputFormData = (
overrides: Partial<HumanInputFormData> = {},
): HumanInputFormData => ({
form_id: 'form-1',
node_id: 'human-node-1',
node_title: 'Need Approval',
form_content: 'Before {{#$output.reason#}} after',
inputs: [],
actions: [],
form_token: 'token-1',
resolved_default_values: {},
display_in_ui: true,
expiration_time: 2_000_000_000,
...overrides,
})
const createHumanInputFilledFormData = (
overrides: Partial<HumanInputFilledFormData> = {},
): HumanInputFilledFormData => ({
node_id: 'node-1',
node_title: 'Need Approval',
rendered_content: 'rendered',
action_id: 'approve',
action_text: 'Approve',
...overrides,
})
describe('WorkflowPreview', () => {
beforeEach(() => {
vi.clearAllMocks()
Object.defineProperty(window, 'innerWidth', {
configurable: true,
value: 1200,
})
})
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(
<WorkflowPreview />,
{
initialStoreState: {
showInputsPanel: true,
showDebugAndPreviewPanel: true,
previewPanelWidth: 420,
},
},
)
expect(screen.getByRole('button', { name: 'run-inputs' })).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'run-inputs' }))
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)
})
it('should switch to detail when the workflow is listening', () => {
renderWorkflowComponent(
<WorkflowPreview />,
{
initialStoreState: {
isListening: true,
workflowRunningData: createWorkflowRunningData({
result: createWorkflowResult({
status: WorkflowRunningStatus.Running,
}),
}),
},
},
)
expect(screen.getByTestId('result-panel')).toHaveTextContent(WorkflowRunningStatus.Running)
})
it('should switch to detail when a finished run has no outputs or files', () => {
renderWorkflowComponent(
<WorkflowPreview />,
{
initialStoreState: {
workflowRunningData: {
...createWorkflowRunningData({
result: createWorkflowResult({
status: WorkflowRunningStatus.Succeeded,
files: [],
}),
}),
resultText: '',
} as NonNullable<Shape['workflowRunningData']>,
},
},
)
expect(screen.getByTestId('result-panel')).toHaveTextContent(WorkflowRunningStatus.Succeeded)
})
it('should render paused human input results and submit pending forms', async () => {
const user = userEvent.setup()
const pausedData = createWorkflowRunningData({
result: createWorkflowResult({
status: WorkflowRunningStatus.Paused,
files: [],
}),
humanInputFormDataList: [createHumanInputFormData()],
humanInputFilledFormDataList: [createHumanInputFilledFormData()],
})
renderWorkflowComponent(
<WorkflowPreview />,
{
initialStoreState: {
workflowRunningData: pausedData,
},
},
)
expect(screen.getByTestId('human-form-list')).toHaveTextContent('1')
expect(screen.getByTestId('filled-form-list')).toHaveTextContent('1')
await user.click(screen.getByRole('button', { name: 'submit-human-form' }))
expect(mockSubmitHumanInputForm).toHaveBeenCalledWith('form-token', { answer: 'ok' })
})
it('should copy successful string output and show a success toast', async () => {
const user = userEvent.setup()
renderWorkflowComponent(
<WorkflowPreview />,
{
initialStoreState: {
workflowRunningData: {
...createWorkflowRunningData({
result: createWorkflowResult({
status: WorkflowRunningStatus.Succeeded,
files: [],
}),
}),
resultText: 'final answer',
} as NonNullable<Shape['workflowRunningData']>,
},
},
)
await user.click(screen.getByText('runLog.result'))
await user.click(screen.getByRole('button', { name: 'common.operation.copy' }))
expect(mockCopy).toHaveBeenCalledWith('final answer')
expect(mockToastSuccess).toHaveBeenCalledWith('common.actionMsg.copySuccessfully')
})
it('should show a loading state for an empty detail panel', () => {
renderWorkflowComponent(
<WorkflowPreview />,
{
initialStoreState: {
isListening: true,
workflowRunningData: undefined,
},
},
)
expect(screen.getByRole('status', { name: 'appApi.loading' })).toBeInTheDocument()
})
it('should show a loading state for an empty tracing panel', () => {
renderWorkflowComponent(
<WorkflowPreview />,
{
initialStoreState: {
workflowRunningData: createWorkflowRunningData({
tracing: [],
}),
},
},
)
expect(screen.getByTestId('tracing-panel')).toHaveTextContent('0')
expect(screen.getByRole('status', { name: 'appApi.loading' })).toBeInTheDocument()
})
it('should keep inert tabs disabled without run data and switch among result, detail, and tracing when data exists', async () => {
const user = userEvent.setup()
const { store } = renderWorkflowComponent(
<WorkflowPreview />,
{
initialStoreState: {
showInputsPanel: true,
workflowRunningData: undefined,
},
},
)
await user.click(screen.getByText('runLog.result'))
await user.click(screen.getByText('runLog.detail'))
await user.click(screen.getByText('runLog.tracing'))
expect(screen.getByRole('button', { name: 'run-inputs' })).toBeInTheDocument()
store.setState({
workflowRunningData: {
...createWorkflowRunningData({
result: createWorkflowResult({
status: WorkflowRunningStatus.Succeeded,
files: [],
}),
tracing: [createNodeTracing()],
}),
resultText: 'ready',
} as NonNullable<Shape['workflowRunningData']>,
})
await user.click(screen.getByText('runLog.result'))
expect(screen.getByTestId('result-text')).toBeInTheDocument()
await user.click(screen.getByText('runLog.detail'))
expect(screen.getByTestId('result-panel')).toBeInTheDocument()
await user.click(screen.getByText('runLog.tracing'))
expect(screen.getByTestId('tracing-panel')).toHaveTextContent('1')
await user.click(screen.getByText('runLog.result'))
await user.click(screen.getByRole('button', { name: 'open-detail' }))
expect(screen.getByTestId('result-panel')).toBeInTheDocument()
})
it('should resize the preview panel within the allowed workflow canvas bounds', async () => {
const { container, store } = renderWorkflowComponent(
<WorkflowPreview />,
{
initialStoreState: {
previewPanelWidth: 450,
workflowCanvasWidth: 1000,
},
},
)
const resizeHandle = container.querySelector('.cursor-col-resize') as HTMLElement
fireEvent.mouseDown(resizeHandle)
fireEvent.mouseMove(window, { clientX: 700 })
fireEvent.mouseMove(window, { clientX: 100 })
fireEvent.mouseUp(window)
await waitFor(() => {
expect(store.getState().previewPanelWidth).toBe(500)
})
})
})