mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 00:48:04 +08:00
test(workflow): add unit tests for workflow components (#33910)
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
@ -0,0 +1,266 @@
|
||||
/* eslint-disable ts/no-explicit-any, style/jsx-one-expression-per-line */
|
||||
import type { ScheduleTriggerNodeType } from '../types'
|
||||
import type { PanelProps } from '@/types/workflow'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import Panel from '../panel'
|
||||
import useConfig from '../use-config'
|
||||
|
||||
vi.mock('../use-config', () => ({
|
||||
default: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/field', () => ({
|
||||
default: ({ title, operations, children }: any) => (
|
||||
<div>
|
||||
<div>{title}</div>
|
||||
<div>{operations}</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../components/frequency-selector', () => ({
|
||||
default: ({ frequency, onChange }: any) => (
|
||||
<button type="button" onClick={() => onChange('weekly')}>
|
||||
{frequency}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../components/mode-toggle', () => ({
|
||||
default: ({ mode, onChange }: any) => (
|
||||
<button type="button" onClick={() => onChange(mode === 'visual' ? 'cron' : 'visual')}>
|
||||
{mode}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../components/monthly-days-selector', () => ({
|
||||
default: ({ onChange }: any) => (
|
||||
<button type="button" onClick={() => onChange([1, 'last'])}>
|
||||
monthly-days
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../components/next-execution-times', () => ({
|
||||
default: ({ data }: any) => <div>next-times-{data.mode}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../components/on-minute-selector', () => ({
|
||||
default: ({ onChange }: any) => (
|
||||
<button type="button" onClick={() => onChange(25)}>
|
||||
minute-selector
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../components/weekday-selector', () => ({
|
||||
default: ({ onChange }: any) => (
|
||||
<button type="button" onClick={() => onChange(['mon', 'wed'])}>
|
||||
weekday-selector
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
const mockUseConfig = vi.mocked(useConfig)
|
||||
|
||||
const createData = (overrides: Partial<ScheduleTriggerNodeType> = {}): ScheduleTriggerNodeType => ({
|
||||
title: 'Schedule Trigger',
|
||||
desc: '',
|
||||
type: 'trigger-schedule' as ScheduleTriggerNodeType['type'],
|
||||
mode: 'visual',
|
||||
frequency: 'daily',
|
||||
timezone: 'UTC',
|
||||
visual_config: {
|
||||
time: '11:30 AM',
|
||||
weekdays: ['mon'],
|
||||
on_minute: 15,
|
||||
monthly_days: [1],
|
||||
},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const panelProps: PanelProps = {
|
||||
getInputVars: vi.fn(() => []),
|
||||
toVarInputs: vi.fn(() => []),
|
||||
runInputData: {},
|
||||
runInputDataRef: { current: {} },
|
||||
setRunInputData: vi.fn(),
|
||||
runResult: null,
|
||||
}
|
||||
|
||||
const renderPanel = (id: string, data: ScheduleTriggerNodeType) => (
|
||||
render(<Panel id={id} data={data} panelProps={panelProps} />)
|
||||
)
|
||||
|
||||
describe('TriggerSchedulePanel', () => {
|
||||
const setInputs = vi.fn()
|
||||
const handleModeChange = vi.fn()
|
||||
const handleFrequencyChange = vi.fn()
|
||||
const handleCronExpressionChange = vi.fn()
|
||||
const handleWeekdaysChange = vi.fn()
|
||||
const handleTimeChange = vi.fn()
|
||||
const handleOnMinuteChange = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseConfig.mockReturnValue({
|
||||
readOnly: false,
|
||||
inputs: createData(),
|
||||
setInputs,
|
||||
handleModeChange,
|
||||
handleFrequencyChange,
|
||||
handleCronExpressionChange,
|
||||
handleWeekdaysChange,
|
||||
handleTimeChange,
|
||||
handleOnMinuteChange,
|
||||
})
|
||||
})
|
||||
|
||||
// The panel should wire the visual and cron controls back to the schedule config handlers.
|
||||
describe('Panel Wiring', () => {
|
||||
it('should render the visual controls and forward their callbacks', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderPanel('node-1', createData())
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'visual' }))
|
||||
await user.click(screen.getByRole('button', { name: 'daily' }))
|
||||
await user.click(screen.getByDisplayValue('11:30 AM'))
|
||||
await user.click(screen.getAllByText('02')[0]!)
|
||||
await user.click(screen.getByText('45'))
|
||||
await user.click(screen.getByText('PM'))
|
||||
await user.click(screen.getByRole('button', { name: /operation\.ok/i }))
|
||||
|
||||
expect(handleModeChange).toHaveBeenCalledWith('cron')
|
||||
expect(handleFrequencyChange).toHaveBeenCalledWith('weekly')
|
||||
expect(handleTimeChange).toHaveBeenCalledWith('2:45 PM')
|
||||
expect(screen.getByText('next-times-visual')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render weekday and monthly helpers for the matching frequencies', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockUseConfig.mockReturnValueOnce({
|
||||
readOnly: false,
|
||||
inputs: createData({ frequency: 'weekly' }),
|
||||
setInputs,
|
||||
handleModeChange,
|
||||
handleFrequencyChange,
|
||||
handleCronExpressionChange,
|
||||
handleWeekdaysChange,
|
||||
handleTimeChange,
|
||||
handleOnMinuteChange,
|
||||
})
|
||||
|
||||
renderPanel('node-1', createData({ frequency: 'weekly' }))
|
||||
await user.click(screen.getByRole('button', { name: 'weekday-selector' }))
|
||||
expect(handleWeekdaysChange).toHaveBeenCalledWith(['mon', 'wed'])
|
||||
|
||||
mockUseConfig.mockReturnValueOnce({
|
||||
readOnly: false,
|
||||
inputs: createData({ frequency: 'weekly', visual_config: undefined as any }),
|
||||
setInputs,
|
||||
handleModeChange,
|
||||
handleFrequencyChange,
|
||||
handleCronExpressionChange,
|
||||
handleWeekdaysChange,
|
||||
handleTimeChange,
|
||||
handleOnMinuteChange,
|
||||
})
|
||||
|
||||
renderPanel('node-5', createData({ frequency: 'weekly', visual_config: undefined as any }))
|
||||
await user.click(screen.getAllByRole('button', { name: 'weekday-selector' })[1]!)
|
||||
expect(handleWeekdaysChange).toHaveBeenCalledTimes(2)
|
||||
|
||||
mockUseConfig.mockReturnValueOnce({
|
||||
readOnly: false,
|
||||
inputs: createData({ frequency: 'monthly', visual_config: undefined as any }),
|
||||
setInputs,
|
||||
handleModeChange,
|
||||
handleFrequencyChange,
|
||||
handleCronExpressionChange,
|
||||
handleWeekdaysChange,
|
||||
handleTimeChange,
|
||||
handleOnMinuteChange,
|
||||
})
|
||||
|
||||
renderPanel('node-2', createData({ frequency: 'monthly', visual_config: undefined as any }))
|
||||
await user.click(screen.getByRole('button', { name: 'monthly-days' }))
|
||||
expect(setInputs).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render cron mode and forward expression changes', () => {
|
||||
mockUseConfig.mockReturnValueOnce({
|
||||
readOnly: false,
|
||||
inputs: createData({ mode: 'cron', frequency: undefined, cron_expression: '0 0 * * *' }),
|
||||
setInputs,
|
||||
handleModeChange,
|
||||
handleFrequencyChange,
|
||||
handleCronExpressionChange,
|
||||
handleWeekdaysChange,
|
||||
handleTimeChange,
|
||||
handleOnMinuteChange,
|
||||
})
|
||||
|
||||
renderPanel('node-3', createData({ mode: 'cron' }))
|
||||
|
||||
fireEvent.change(screen.getByDisplayValue('0 0 * * *'), { target: { value: '*/5 * * * *' } })
|
||||
|
||||
expect(handleCronExpressionChange).toHaveBeenCalledWith('*/5 * * * *')
|
||||
})
|
||||
|
||||
it('should use daily and empty cron defaults when the schedule values are missing', () => {
|
||||
mockUseConfig.mockReturnValueOnce({
|
||||
readOnly: false,
|
||||
inputs: createData({ frequency: undefined }),
|
||||
setInputs,
|
||||
handleModeChange,
|
||||
handleFrequencyChange,
|
||||
handleCronExpressionChange,
|
||||
handleWeekdaysChange,
|
||||
handleTimeChange,
|
||||
handleOnMinuteChange,
|
||||
})
|
||||
|
||||
const { rerender } = renderPanel('node-6', createData({ frequency: undefined }) as any)
|
||||
expect(screen.getByRole('button', { name: 'daily' })).toBeInTheDocument()
|
||||
expect(screen.getByDisplayValue('11:30 AM')).toBeInTheDocument()
|
||||
|
||||
mockUseConfig.mockReturnValueOnce({
|
||||
readOnly: false,
|
||||
inputs: createData({ mode: 'cron', frequency: undefined, cron_expression: undefined as any }),
|
||||
setInputs,
|
||||
handleModeChange,
|
||||
handleFrequencyChange,
|
||||
handleCronExpressionChange,
|
||||
handleWeekdaysChange,
|
||||
handleTimeChange,
|
||||
handleOnMinuteChange,
|
||||
})
|
||||
|
||||
rerender(<Panel id="node-7" data={createData({ mode: 'cron', frequency: undefined, cron_expression: undefined as any }) as any} panelProps={panelProps} />)
|
||||
expect(screen.getByRole('textbox')).toHaveValue('')
|
||||
})
|
||||
|
||||
it('should render the hourly minute selector when the frequency is hourly', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockUseConfig.mockReturnValueOnce({
|
||||
readOnly: false,
|
||||
inputs: createData({ frequency: 'hourly' }),
|
||||
setInputs,
|
||||
handleModeChange,
|
||||
handleFrequencyChange,
|
||||
handleCronExpressionChange,
|
||||
handleWeekdaysChange,
|
||||
handleTimeChange,
|
||||
handleOnMinuteChange,
|
||||
})
|
||||
|
||||
renderPanel('node-4', createData({ frequency: 'hourly' }))
|
||||
await user.click(screen.getByRole('button', { name: 'minute-selector' }))
|
||||
|
||||
expect(handleOnMinuteChange).toHaveBeenCalledWith(25)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,151 @@
|
||||
/* eslint-disable ts/no-explicit-any */
|
||||
import type { ScheduleTriggerNodeType } from '../../types'
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import FrequencySelector from '../frequency-selector'
|
||||
import ModeSwitcher from '../mode-switcher'
|
||||
import ModeToggle from '../mode-toggle'
|
||||
import MonthlyDaysSelector from '../monthly-days-selector'
|
||||
import NextExecutionTimes from '../next-execution-times'
|
||||
import OnMinuteSelector from '../on-minute-selector'
|
||||
import WeekdaySelector from '../weekday-selector'
|
||||
|
||||
const createData = (overrides: Partial<ScheduleTriggerNodeType> = {}): ScheduleTriggerNodeType => ({
|
||||
title: 'Schedule Trigger',
|
||||
desc: '',
|
||||
type: 'trigger-schedule' as ScheduleTriggerNodeType['type'],
|
||||
mode: 'visual',
|
||||
frequency: 'daily',
|
||||
timezone: 'UTC',
|
||||
visual_config: {
|
||||
time: '11:30 AM',
|
||||
weekdays: ['mon'],
|
||||
on_minute: 15,
|
||||
monthly_days: [1],
|
||||
},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('trigger-schedule components', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// The leaf controls should expose schedule actions and derived previews for the visual scheduler.
|
||||
describe('Leaf Rendering', () => {
|
||||
it('should select a new frequency from the dropdown options', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(
|
||||
<FrequencySelector
|
||||
frequency="daily"
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'workflow.nodes.triggerSchedule.frequency.daily' })
|
||||
fireEvent.click(trigger)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(trigger).toHaveAttribute('aria-expanded', 'true')
|
||||
})
|
||||
|
||||
const listbox = await screen.findByRole('listbox')
|
||||
await user.click(within(listbox).getByText('workflow.nodes.triggerSchedule.frequency.weekly'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledWith('weekly')
|
||||
})
|
||||
})
|
||||
|
||||
it('should switch between visual and cron modes', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(<ModeSwitcher mode="visual" onChange={onChange} />)
|
||||
|
||||
await user.click(screen.getByText('workflow.nodes.triggerSchedule.modeCron'))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('cron')
|
||||
})
|
||||
|
||||
it('should toggle the mode from visual to cron', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(<ModeToggle mode="visual" onChange={onChange} />)
|
||||
|
||||
await user.click(screen.getByRole('button'))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('cron')
|
||||
})
|
||||
|
||||
it('should toggle the mode from cron back to visual', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(<ModeToggle mode="cron" onChange={onChange} />)
|
||||
|
||||
await user.click(screen.getByRole('button'))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('visual')
|
||||
})
|
||||
|
||||
it('should change the hourly minute through the slider', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(<OnMinuteSelector value={15} onChange={onChange} />)
|
||||
|
||||
const slider = screen.getByRole('slider')
|
||||
slider.focus()
|
||||
await user.keyboard('{ArrowRight}')
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(16, 0)
|
||||
})
|
||||
|
||||
it('should keep at least one weekday selected', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(<WeekdaySelector selectedDays={['mon']} onChange={onChange} />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Mon' }))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['mon'])
|
||||
})
|
||||
|
||||
it('should add a new weekday when the day is not selected yet', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(<WeekdaySelector selectedDays={[]} onChange={onChange} />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Tue' }))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['tue'])
|
||||
})
|
||||
|
||||
it('should toggle monthly days and show the day-31 warning', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
render(<MonthlyDaysSelector selectedDays={[31]} onChange={onChange} />)
|
||||
|
||||
expect(screen.getByText('workflow.nodes.triggerSchedule.lastDayTooltip')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByText('workflow.nodes.triggerSchedule.lastDay'))
|
||||
|
||||
expect(onChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render the upcoming execution times when the schedule is valid', () => {
|
||||
render(<NextExecutionTimes data={createData()} />)
|
||||
|
||||
expect(screen.getByText('workflow.nodes.triggerSchedule.nextExecutionTimes')).toBeInTheDocument()
|
||||
expect(screen.getAllByText(/^\d{2}$/).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should hide upcoming execution times when frequency is missing or cron is invalid', () => {
|
||||
const { rerender, container } = render(<NextExecutionTimes data={createData({ frequency: undefined }) as any} />)
|
||||
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
|
||||
rerender(<NextExecutionTimes data={createData({ mode: 'cron', cron_expression: 'bad cron' }) as any} />)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user