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

View File

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