diff --git a/web/app/components/workflow/nodes/trigger-schedule/__tests__/default-compatibility.test.ts b/web/app/components/workflow/nodes/trigger-schedule/__tests__/default-compatibility.test.ts deleted file mode 100644 index 208ce80105..0000000000 --- a/web/app/components/workflow/nodes/trigger-schedule/__tests__/default-compatibility.test.ts +++ /dev/null @@ -1,247 +0,0 @@ -import nodeDefault from '../default' -import type { ScheduleTriggerNodeType } from '../types' - -// Mock translation function -const mockT = (key: string, params?: any) => { - if (key.includes('fieldRequired')) return `${params?.field} is required` - if (key.includes('invalidCronExpression')) return 'Invalid cron expression' - if (key.includes('invalidTimezone')) return 'Invalid timezone' - if (key.includes('noValidExecutionTime')) return 'No valid execution time' - if (key.includes('executionTimeCalculationError')) return 'Execution time calculation error' - return key -} - -describe('Schedule Trigger Default - Backward Compatibility', () => { - describe('Enhanced Cron Expression Support', () => { - it('should accept enhanced month abbreviations', () => { - const payload: ScheduleTriggerNodeType = { - mode: 'cron', - timezone: 'UTC', - cron_expression: '0 9 1 JAN *', // January 1st at 9 AM - } - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(true) - expect(result.errorMessage).toBe('') - }) - - it('should accept enhanced day abbreviations', () => { - const payload: ScheduleTriggerNodeType = { - mode: 'cron', - timezone: 'UTC', - cron_expression: '0 15 * * MON', // Every Monday at 3 PM - } - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(true) - expect(result.errorMessage).toBe('') - }) - - it('should accept predefined expressions', () => { - const predefinedExpressions = ['@daily', '@weekly', '@monthly', '@yearly', '@hourly'] - - predefinedExpressions.forEach((expr) => { - const payload: ScheduleTriggerNodeType = { - mode: 'cron', - timezone: 'UTC', - cron_expression: expr, - } - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(true) - expect(result.errorMessage).toBe('') - }) - }) - - it('should accept special characters', () => { - const specialExpressions = [ - '0 9 ? * 1', // ? wildcard - '0 12 * * 7', // Sunday as 7 - '0 15 L * *', // Last day of month - ] - - specialExpressions.forEach((expr) => { - const payload: ScheduleTriggerNodeType = { - mode: 'cron', - timezone: 'UTC', - cron_expression: expr, - } - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(true) - expect(result.errorMessage).toBe('') - }) - }) - - it('should maintain backward compatibility with legacy expressions', () => { - const legacyExpressions = [ - '15 10 1 * *', // Monthly 1st at 10:15 - '0 0 * * 0', // Weekly Sunday midnight - '*/5 * * * *', // Every 5 minutes - '0 9-17 * * 1-5', // Business hours weekdays - '30 14 * * 1', // Monday 14:30 - '0 0 1,15 * *', // 1st and 15th midnight - ] - - legacyExpressions.forEach((expr) => { - const payload: ScheduleTriggerNodeType = { - mode: 'cron', - timezone: 'UTC', - cron_expression: expr, - } - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(true) - expect(result.errorMessage).toBe('') - }) - }) - }) - - describe('Error Detection and Validation', () => { - it('should detect invalid enhanced syntax', () => { - const invalidExpressions = [ - '0 12 * JANUARY *', // Full month name not supported - '0 12 * * MONDAY', // Full day name not supported - '0 12 32 JAN *', // Invalid day with month abbreviation - '@invalid', // Invalid predefined expression - '0 12 1 INVALID *', // Invalid month abbreviation - '0 12 * * INVALID', // Invalid day abbreviation - ] - - invalidExpressions.forEach((expr) => { - const payload: ScheduleTriggerNodeType = { - mode: 'cron', - timezone: 'UTC', - cron_expression: expr, - } - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(false) - expect(result.errorMessage).toContain('Invalid cron expression') - }) - }) - - it('should handle execution time calculation errors gracefully', () => { - // Test with an expression that contains invalid date (Feb 30th) - const payload: ScheduleTriggerNodeType = { - mode: 'cron', - timezone: 'UTC', - cron_expression: '0 0 30 2 *', // Feb 30th (invalid date) - } - - const result = nodeDefault.checkValid(payload, mockT) - // Should be an invalid expression error since cron-parser detects Feb 30th as invalid - expect(result.isValid).toBe(false) - expect(result.errorMessage).toBe('Invalid cron expression') - }) - }) - - describe('Timezone Integration', () => { - it('should validate with various timezones', () => { - const timezones = ['UTC', 'America/New_York', 'Asia/Tokyo', 'Europe/London'] - - timezones.forEach((timezone) => { - const payload: ScheduleTriggerNodeType = { - mode: 'cron', - timezone, - cron_expression: '0 12 * * *', // Daily noon - } - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(true) - expect(result.errorMessage).toBe('') - }) - }) - - it('should reject invalid timezones', () => { - const payload: ScheduleTriggerNodeType = { - mode: 'cron', - timezone: 'Invalid/Timezone', - cron_expression: '0 12 * * *', - } - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(false) - expect(result.errorMessage).toContain('Invalid timezone') - }) - }) - - describe('Visual Mode Compatibility', () => { - it('should maintain visual mode validation', () => { - const payload: ScheduleTriggerNodeType = { - mode: 'visual', - timezone: 'UTC', - frequency: 'daily', - visual_config: { - time: '9:00 AM', - }, - } - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(true) - expect(result.errorMessage).toBe('') - }) - - it('should validate weekly configuration', () => { - const payload: ScheduleTriggerNodeType = { - mode: 'visual', - timezone: 'UTC', - frequency: 'weekly', - visual_config: { - time: '2:30 PM', - weekdays: ['mon', 'wed', 'fri'], - }, - } - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(true) - expect(result.errorMessage).toBe('') - }) - - it('should validate monthly configuration', () => { - const payload: ScheduleTriggerNodeType = { - mode: 'visual', - timezone: 'UTC', - frequency: 'monthly', - visual_config: { - time: '11:30 AM', - monthly_days: [1, 15, 'last'], - }, - } - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(true) - expect(result.errorMessage).toBe('') - }) - }) - - describe('Edge Cases and Robustness', () => { - it('should handle empty/whitespace cron expressions', () => { - const emptyExpressions = ['', ' ', '\t\n '] - - emptyExpressions.forEach((expr) => { - const payload: ScheduleTriggerNodeType = { - mode: 'cron', - timezone: 'UTC', - cron_expression: expr, - } - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(false) - expect(result.errorMessage).toMatch(/(Invalid cron expression|required)/) - }) - }) - - it('should validate whitespace-padded expressions', () => { - const payload: ScheduleTriggerNodeType = { - mode: 'cron', - timezone: 'UTC', - cron_expression: ' 0 12 * * * ', // Padded with whitespace - } - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(true) - expect(result.errorMessage).toBe('') - }) - }) -}) diff --git a/web/app/components/workflow/nodes/trigger-schedule/__tests__/default.test.ts b/web/app/components/workflow/nodes/trigger-schedule/__tests__/default.test.ts deleted file mode 100644 index edde646711..0000000000 --- a/web/app/components/workflow/nodes/trigger-schedule/__tests__/default.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Schedule Trigger Node Default Tests - * - * Simple test for the Schedule Trigger node default configuration and validation. - * Tests core checkValid functionality following project patterns. - */ - -import nodeDefault from '../default' -import type { ScheduleTriggerNodeType } from '../types' - -// Mock external dependencies -jest.mock('../utils/cron-parser', () => ({ - isValidCronExpression: jest.fn((expr: string) => { - return expr === '0 9 * * 1' // Only this specific expression is valid - }), -})) - -jest.mock('../utils/execution-time-calculator', () => ({ - getNextExecutionTimes: jest.fn(() => [new Date(Date.now() + 86400000)]), -})) - -// Simple mock translation function -const mockT = (key: string, params?: any) => { - if (key.includes('fieldRequired')) return `${params?.field} is required` - if (key.includes('invalidCronExpression')) return 'Invalid cron expression' - if (key.includes('invalidTimezone')) return 'Invalid timezone' - return key -} - -describe('Schedule Trigger Node Default', () => { - describe('Basic Configuration', () => { - it('should have correct default value', () => { - expect(nodeDefault.defaultValue.mode).toBe('visual') - expect(nodeDefault.defaultValue.frequency).toBe('daily') - }) - - it('should have correct metadata for trigger node', () => { - expect(nodeDefault.metaData).toBeDefined() - expect(nodeDefault.metaData.type).toBe('trigger-schedule') - expect(nodeDefault.metaData.sort).toBe(2) - expect(nodeDefault.metaData.isStart).toBe(true) - }) - }) - - describe('Validation - checkValid', () => { - it('should validate successfully with valid visual config', () => { - const payload: ScheduleTriggerNodeType = { - mode: 'visual', - timezone: 'UTC', - frequency: 'daily', - visual_config: { - time: '9:00 AM', - }, - } - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(true) - expect(result.errorMessage).toBe('') - }) - - it('should require mode field', () => { - const payload = { - timezone: 'UTC', - } as ScheduleTriggerNodeType - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(false) - expect(result.errorMessage).toContain('required') - }) - - it('should require timezone field', () => { - const payload = { - mode: 'visual', - } as ScheduleTriggerNodeType - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(false) - expect(result.errorMessage).toContain('required') - }) - - it('should validate cron mode with valid expression', () => { - const payload: ScheduleTriggerNodeType = { - mode: 'cron', - timezone: 'UTC', - cron_expression: '0 9 * * 1', - } - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(true) - }) - - it('should reject invalid cron expression', () => { - const payload: ScheduleTriggerNodeType = { - mode: 'cron', - timezone: 'UTC', - cron_expression: 'invalid', - } - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(false) - expect(result.errorMessage).toContain('Invalid cron expression') - }) - - it('should reject invalid timezone', () => { - const payload = { - mode: 'visual', - timezone: 'Invalid/Timezone', - frequency: 'daily', - visual_config: { time: '9:00 AM' }, - } as ScheduleTriggerNodeType - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(false) - expect(result.errorMessage).toContain('Invalid timezone') - }) - - it('should require frequency in visual mode', () => { - const payload = { - mode: 'visual', - timezone: 'UTC', - } as ScheduleTriggerNodeType - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(false) - expect(result.errorMessage).toContain('required') - }) - }) -}) diff --git a/web/app/components/workflow/nodes/trigger-schedule/__tests__/monthly-days-selector.test.tsx b/web/app/components/workflow/nodes/trigger-schedule/__tests__/monthly-days-selector.test.tsx deleted file mode 100644 index 9a7732ffcf..0000000000 --- a/web/app/components/workflow/nodes/trigger-schedule/__tests__/monthly-days-selector.test.tsx +++ /dev/null @@ -1,265 +0,0 @@ -import React from 'react' -import { fireEvent, render, screen } from '@testing-library/react' -import { useTranslation } from 'react-i18next' -import MonthlyDaysSelector from '../components/monthly-days-selector' - -jest.mock('react-i18next') -const mockUseTranslation = useTranslation as jest.MockedFunction - -const mockTranslation = { - t: (key: string) => { - const translations: Record = { - 'workflow.nodes.triggerSchedule.days': 'Days', - 'workflow.nodes.triggerSchedule.lastDay': 'Last', - 'workflow.nodes.triggerSchedule.lastDayTooltip': 'Last day of month', - } - return translations[key] || key - }, -} - -beforeEach(() => { - mockUseTranslation.mockReturnValue(mockTranslation as any) -}) - -describe('MonthlyDaysSelector', () => { - describe('Single selection', () => { - test('renders with single selected day', () => { - const onChange = jest.fn() - - render( - , - ) - - const button15 = screen.getByRole('button', { name: '15' }) - expect(button15).toHaveClass('border-util-colors-blue-brand-blue-brand-600') - }) - - test('calls onChange when day is clicked', () => { - const onChange = jest.fn() - - render( - , - ) - - fireEvent.click(screen.getByRole('button', { name: '20' })) - expect(onChange).toHaveBeenCalledWith([15, 20]) - }) - - test('handles last day selection', () => { - const onChange = jest.fn() - - render( - , - ) - - const lastButton = screen.getByRole('button', { name: 'Last' }) - expect(lastButton).toHaveClass('border-util-colors-blue-brand-blue-brand-600') - }) - }) - - describe('Multi-select functionality', () => { - test('renders with multiple selected days', () => { - const onChange = jest.fn() - - render( - , - ) - - expect(screen.getByRole('button', { name: '1' })).toHaveClass('border-util-colors-blue-brand-blue-brand-600') - expect(screen.getByRole('button', { name: '15' })).toHaveClass('border-util-colors-blue-brand-blue-brand-600') - expect(screen.getByRole('button', { name: '30' })).toHaveClass('border-util-colors-blue-brand-blue-brand-600') - }) - - test('adds day to selection when clicked', () => { - const onChange = jest.fn() - - render( - , - ) - - fireEvent.click(screen.getByRole('button', { name: '20' })) - expect(onChange).toHaveBeenCalledWith([1, 15, 20]) - }) - - test('removes day from selection when clicked', () => { - const onChange = jest.fn() - - render( - , - ) - - fireEvent.click(screen.getByRole('button', { name: '15' })) - expect(onChange).toHaveBeenCalledWith([1, 20]) - }) - - test('handles last day selection', () => { - const onChange = jest.fn() - - render( - , - ) - - expect(screen.getByRole('button', { name: 'Last' })).toHaveClass('border-util-colors-blue-brand-blue-brand-600') - - fireEvent.click(screen.getByRole('button', { name: 'Last' })) - expect(onChange).toHaveBeenCalledWith([1]) - }) - - test('handles empty selection array', () => { - const onChange = jest.fn() - - render( - , - ) - - fireEvent.click(screen.getByRole('button', { name: '10' })) - expect(onChange).toHaveBeenCalledWith([10]) - }) - - test('supports mixed selection of numbers and last day', () => { - const onChange = jest.fn() - - render( - , - ) - - expect(screen.getByRole('button', { name: '5' })).toHaveClass('border-util-colors-blue-brand-blue-brand-600') - expect(screen.getByRole('button', { name: '15' })).toHaveClass('border-util-colors-blue-brand-blue-brand-600') - expect(screen.getByRole('button', { name: 'Last' })).toHaveClass('border-util-colors-blue-brand-blue-brand-600') - }) - }) - - describe('Component structure', () => { - test('renders all day buttons from 1 to 31', () => { - render( - , - ) - - for (let i = 1; i <= 31; i++) - expect(screen.getByRole('button', { name: i.toString() })).toBeInTheDocument() - }) - - test('renders last day button', () => { - render( - , - ) - - expect(screen.getByRole('button', { name: 'Last' })).toBeInTheDocument() - }) - - test('displays correct label', () => { - render( - , - ) - - expect(screen.getByText('Days')).toBeInTheDocument() - }) - - test('applies correct grid layout', () => { - const { container } = render( - , - ) - - const gridRows = container.querySelectorAll('.grid-cols-7') - expect(gridRows).toHaveLength(5) - }) - }) - - describe('Accessibility', () => { - test('buttons are keyboard accessible', () => { - const onChange = jest.fn() - - render( - , - ) - - const button = screen.getByRole('button', { name: '20' }) - button.focus() - expect(document.activeElement).toBe(button) - }) - - test('last day button has tooltip', () => { - render( - , - ) - - expect(screen.getByRole('button', { name: 'Last' })).toBeInTheDocument() - }) - - test('selected state is visually distinct', () => { - render( - , - ) - - const selectedButton = screen.getByRole('button', { name: '15' }) - const unselectedButton = screen.getByRole('button', { name: '16' }) - - expect(selectedButton).toHaveClass('border-util-colors-blue-brand-blue-brand-600') - expect(unselectedButton).toHaveClass('border-divider-subtle') - }) - }) - - describe('Default behavior', () => { - test('handles interaction correctly', () => { - const onChange = jest.fn() - - render( - , - ) - - fireEvent.click(screen.getByRole('button', { name: '20' })) - expect(onChange).toHaveBeenCalledWith([15, 20]) - }) - }) -}) diff --git a/web/app/components/workflow/nodes/trigger-schedule/__tests__/monthly-edge-cases.test.ts b/web/app/components/workflow/nodes/trigger-schedule/__tests__/monthly-edge-cases.test.ts deleted file mode 100644 index d3ebfcb65f..0000000000 --- a/web/app/components/workflow/nodes/trigger-schedule/__tests__/monthly-edge-cases.test.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { getNextExecutionTimes } from '../utils/execution-time-calculator' -import type { ScheduleTriggerNodeType } from '../types' - -const createMonthlyConfig = (monthly_days: (number | 'last')[], time = '10:30 AM', timezone = 'UTC'): ScheduleTriggerNodeType => ({ - mode: 'visual', - frequency: 'monthly', - visual_config: { - time, - monthly_days, - }, - timezone, -}) - -describe('Monthly Edge Cases', () => { - beforeEach(() => { - jest.useFakeTimers() - jest.setSystemTime(new Date('2024-02-15T08:00:00.000Z')) - }) - - afterEach(() => { - jest.useRealTimers() - }) - - describe('31st day selection logic', () => { - test('31st day skips months without 31 days', () => { - const config = createMonthlyConfig([31]) - const times = getNextExecutionTimes(config, 5) - - const expectedMonths = times.map(date => date.getMonth() + 1) - - expect(expectedMonths).not.toContain(2) - expect(expectedMonths).not.toContain(4) - expect(expectedMonths).not.toContain(6) - expect(expectedMonths).not.toContain(9) - expect(expectedMonths).not.toContain(11) - - times.forEach((date) => { - expect(date.getDate()).toBe(31) - }) - }) - - test('30th day skips February', () => { - const config = createMonthlyConfig([30]) - const times = getNextExecutionTimes(config, 5) - - const expectedMonths = times.map(date => date.getMonth() + 1) - expect(expectedMonths).not.toContain(2) - - times.forEach((date) => { - expect(date.getDate()).toBe(30) - }) - }) - - test('29th day works in all months', () => { - const config = createMonthlyConfig([29]) - const times = getNextExecutionTimes(config, 12) - - const months = times.map(date => date.getMonth() + 1) - expect(months).toContain(1) - expect(months).toContain(3) - expect(months).toContain(4) - expect(months).toContain(5) - expect(months).toContain(6) - expect(months).toContain(7) - expect(months).toContain(8) - expect(months).toContain(9) - expect(months).toContain(10) - expect(months).toContain(11) - expect(months).toContain(12) - }) - - test('29th day skips February in non-leap years', () => { - jest.setSystemTime(new Date('2023-01-15T08:00:00.000Z')) - - const config = createMonthlyConfig([29]) - const times = getNextExecutionTimes(config, 12) - - const februaryExecutions = times.filter(date => date.getMonth() === 1) - expect(februaryExecutions).toHaveLength(0) - }) - - test('29th day includes February in leap years', () => { - jest.setSystemTime(new Date('2024-01-15T08:00:00.000Z')) - - const config = createMonthlyConfig([29]) - const times = getNextExecutionTimes(config, 12) - - const februaryExecutions = times.filter(date => date.getMonth() === 1) - expect(februaryExecutions).toHaveLength(1) - expect(februaryExecutions[0].getDate()).toBe(29) - }) - }) - - describe('last day vs specific day distinction', () => { - test('31st selection is different from last day in short months', () => { - const config31 = createMonthlyConfig([31]) - const configLast = createMonthlyConfig(['last']) - - const times31 = getNextExecutionTimes(config31, 12) - const timesLast = getNextExecutionTimes(configLast, 12) - - const months31 = times31.map(date => date.getMonth() + 1) - const monthsLast = timesLast.map(date => date.getMonth() + 1) - - expect(months31).not.toContain(2) - expect(monthsLast).toContain(2) - - expect(months31).not.toContain(4) - expect(monthsLast).toContain(4) - }) - - test('31st and last day both work correctly in 31-day months', () => { - const config31 = createMonthlyConfig([31]) - const configLast = createMonthlyConfig(['last']) - - const times31 = getNextExecutionTimes(config31, 5) - const timesLast = getNextExecutionTimes(configLast, 5) - - const march31 = times31.find(date => date.getMonth() === 2) - const marchLast = timesLast.find(date => date.getMonth() === 2) - - expect(march31?.getDate()).toBe(31) - expect(marchLast?.getDate()).toBe(31) - }) - - test('mixed selection with 31st and last behaves correctly', () => { - const config = createMonthlyConfig([31, 'last']) - const times = getNextExecutionTimes(config, 12) - - const februaryExecutions = times.filter(date => date.getMonth() === 1) - expect(februaryExecutions).toHaveLength(1) - expect(februaryExecutions[0].getDate()).toBe(29) - - const marchExecutions = times.filter(date => date.getMonth() === 2) - expect(marchExecutions).toHaveLength(1) - expect(marchExecutions[0].getDate()).toBe(31) - }) - - test('deduplicates overlapping selections in 31-day months', () => { - const config = createMonthlyConfig([31, 'last']) - const times = getNextExecutionTimes(config, 12) - - const monthsWith31Days = [0, 2, 4, 6, 7, 9, 11] - - monthsWith31Days.forEach((month) => { - const monthExecutions = times.filter(date => date.getMonth() === month) - expect(monthExecutions.length).toBeLessThanOrEqual(1) - - if (monthExecutions.length === 1) - expect(monthExecutions[0].getDate()).toBe(31) - }) - }) - - test('deduplicates overlapping selections in 30-day months', () => { - const config = createMonthlyConfig([30, 'last']) - const times = getNextExecutionTimes(config, 12) - - const monthsWith30Days = [3, 5, 8, 10] - - monthsWith30Days.forEach((month) => { - const monthExecutions = times.filter(date => date.getMonth() === month) - expect(monthExecutions.length).toBeLessThanOrEqual(1) - - if (monthExecutions.length === 1) - expect(monthExecutions[0].getDate()).toBe(30) - }) - }) - - test('handles complex multi-day with last selection', () => { - const config = createMonthlyConfig([15, 30, 31, 'last']) - const times = getNextExecutionTimes(config, 20) - - const marchExecutions = times.filter(date => date.getMonth() === 2).sort((a, b) => a.getDate() - b.getDate()) - expect(marchExecutions).toHaveLength(3) - expect(marchExecutions.map(d => d.getDate())).toEqual([15, 30, 31]) - - const aprilExecutions = times.filter(date => date.getMonth() === 3).sort((a, b) => a.getDate() - b.getDate()) - expect(aprilExecutions).toHaveLength(2) - expect(aprilExecutions.map(d => d.getDate())).toEqual([15, 30]) - }) - }) - - describe('current month offset calculation', () => { - test('skips current month when no valid days exist', () => { - jest.setSystemTime(new Date('2024-02-15T08:00:00.000Z')) - - const config = createMonthlyConfig([31]) - const times = getNextExecutionTimes(config, 3) - - times.forEach((date) => { - expect(date.getMonth()).toBeGreaterThan(1) - }) - }) - - test('includes current month when valid days exist', () => { - jest.setSystemTime(new Date('2024-03-15T08:00:00.000Z')) - - const config = createMonthlyConfig([31]) - const times = getNextExecutionTimes(config, 3) - - const currentMonthExecution = times.find(date => date.getMonth() === 2) - expect(currentMonthExecution).toBeDefined() - expect(currentMonthExecution?.getDate()).toBe(31) - }) - }) - - describe('sorting and deduplication', () => { - test('handles duplicate selections correctly', () => { - const config = createMonthlyConfig([15, 15, 15]) - const times = getNextExecutionTimes(config, 5) - - const marchExecutions = times.filter(date => date.getMonth() === 2) - expect(marchExecutions).toHaveLength(1) - }) - - test('sorts multiple days within same month', () => { - jest.setSystemTime(new Date('2024-03-01T08:00:00.000Z')) - - const config = createMonthlyConfig([31, 15, 1]) - const times = getNextExecutionTimes(config, 5) - - const marchExecutions = times.filter(date => date.getMonth() === 2).sort((a, b) => a.getDate() - b.getDate()) - expect(marchExecutions.map(d => d.getDate())).toEqual([1, 15, 31]) - }) - }) -}) diff --git a/web/app/components/workflow/nodes/trigger-schedule/__tests__/monthly-multiselect.test.ts b/web/app/components/workflow/nodes/trigger-schedule/__tests__/monthly-multiselect.test.ts deleted file mode 100644 index f2f1bbedf8..0000000000 --- a/web/app/components/workflow/nodes/trigger-schedule/__tests__/monthly-multiselect.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { getNextExecutionTimes } from '../utils/execution-time-calculator' -import type { ScheduleTriggerNodeType } from '../types' - -const createMonthlyConfig = (monthlyDays: (number | 'last')[], time = '10:30 AM'): ScheduleTriggerNodeType => ({ - mode: 'visual', - frequency: 'monthly', - visual_config: { - time, - monthly_days: monthlyDays, - }, - timezone: 'UTC', - id: 'test', - type: 'trigger-schedule', - data: {}, - position: { x: 0, y: 0 }, -}) - -describe('Monthly Multi-Select Execution Time Calculator', () => { - beforeEach(() => { - jest.useFakeTimers() - jest.setSystemTime(new Date('2024-01-15T08:00:00Z')) - }) - - afterEach(() => { - jest.useRealTimers() - }) - - describe('Multi-select functionality', () => { - test('calculates execution times for multiple days in same month', () => { - const config = createMonthlyConfig([1, 15, 30]) - const times = getNextExecutionTimes(config, 5) - - expect(times).toHaveLength(5) - expect(times[0].getDate()).toBe(15) - expect(times[0].getMonth()).toBe(0) - expect(times[1].getDate()).toBe(30) - expect(times[1].getMonth()).toBe(0) - expect(times[2].getDate()).toBe(1) - expect(times[2].getMonth()).toBe(1) - }) - - test('handles last day with multiple selections', () => { - const config = createMonthlyConfig([1, 'last']) - const times = getNextExecutionTimes(config, 4) - - expect(times[0].getDate()).toBe(31) - expect(times[0].getMonth()).toBe(0) - expect(times[1].getDate()).toBe(1) - expect(times[1].getMonth()).toBe(1) - expect(times[2].getDate()).toBe(29) - expect(times[2].getMonth()).toBe(1) - }) - - test('skips invalid days in months with fewer days', () => { - const config = createMonthlyConfig([30, 31]) - jest.setSystemTime(new Date('2024-01-01T08:00:00Z')) - const times = getNextExecutionTimes(config, 6) - - const febTimes = times.filter(t => t.getMonth() === 1) - expect(febTimes.length).toBe(0) - - const marchTimes = times.filter(t => t.getMonth() === 2) - expect(marchTimes.length).toBe(2) - expect(marchTimes[0].getDate()).toBe(30) - expect(marchTimes[1].getDate()).toBe(31) - }) - - test('sorts execution times chronologically', () => { - const config = createMonthlyConfig([25, 5, 15]) - const times = getNextExecutionTimes(config, 6) - - for (let i = 1; i < times.length; i++) - expect(times[i].getTime()).toBeGreaterThan(times[i - 1].getTime()) - }) - - test('handles single day selection', () => { - const config = createMonthlyConfig([15]) - const times = getNextExecutionTimes(config, 3) - - expect(times).toHaveLength(3) - expect(times[0].getDate()).toBe(15) - expect(times[1].getDate()).toBe(15) - expect(times[2].getDate()).toBe(15) - - for (let i = 1; i < times.length; i++) - expect(times[i].getTime()).toBeGreaterThan(times[i - 1].getTime()) - }) - }) - - describe('Single day configuration', () => { - test('supports single day selection', () => { - const config = createMonthlyConfig([15]) - const times = getNextExecutionTimes(config, 3) - - expect(times).toHaveLength(3) - expect(times[0].getDate()).toBe(15) - expect(times[1].getDate()).toBe(15) - expect(times[2].getDate()).toBe(15) - }) - - test('supports last day selection', () => { - const config = createMonthlyConfig(['last']) - const times = getNextExecutionTimes(config, 3) - - expect(times[0].getDate()).toBe(31) - expect(times[0].getMonth()).toBe(0) - expect(times[1].getDate()).toBe(29) - expect(times[1].getMonth()).toBe(1) - }) - - test('falls back to day 1 when no configuration provided', () => { - const config: ScheduleTriggerNodeType = { - mode: 'visual', - frequency: 'monthly', - visual_config: { - time: '10:30 AM', - }, - timezone: 'UTC', - id: 'test', - type: 'trigger-schedule', - data: {}, - position: { x: 0, y: 0 }, - } - - const times = getNextExecutionTimes(config, 2) - - expect(times).toHaveLength(2) - expect(times[0].getDate()).toBe(1) - expect(times[1].getDate()).toBe(1) - }) - }) - - describe('Edge cases', () => { - test('handles empty monthly_days array', () => { - const config = createMonthlyConfig([]) - const times = getNextExecutionTimes(config, 2) - - expect(times).toHaveLength(2) - expect(times[0].getDate()).toBe(1) - expect(times[1].getDate()).toBe(1) - }) - - test('handles execution time that has already passed today', () => { - jest.setSystemTime(new Date('2024-01-15T12:00:00Z')) - const config = createMonthlyConfig([15], '10:30 AM') - const times = getNextExecutionTimes(config, 2) - - expect(times[0].getMonth()).toBe(1) - expect(times[0].getDate()).toBe(15) - }) - - test('limits search to reasonable number of months', () => { - const config = createMonthlyConfig([29, 30, 31]) - jest.setSystemTime(new Date('2024-03-01T08:00:00Z')) - const times = getNextExecutionTimes(config, 50) - - expect(times.length).toBeGreaterThan(0) - expect(times.length).toBeLessThanOrEqual(50) - }) - - test('handles duplicate days in selection', () => { - const config = createMonthlyConfig([15, 15, 15]) - const times = getNextExecutionTimes(config, 4) - - const uniqueDates = new Set(times.map(t => t.getTime())) - expect(uniqueDates.size).toBe(times.length) - }) - - test('correctly handles leap year February', () => { - const config = createMonthlyConfig([29]) - jest.setSystemTime(new Date('2024-01-01T08:00:00Z')) - const times = getNextExecutionTimes(config, 3) - - expect(times[0].getDate()).toBe(29) - expect(times[0].getMonth()).toBe(0) - expect(times[1].getDate()).toBe(29) - expect(times[1].getMonth()).toBe(1) - }) - - test('handles non-leap year February', () => { - const config = createMonthlyConfig([29]) - jest.setSystemTime(new Date('2023-01-01T08:00:00Z')) - const times = getNextExecutionTimes(config, 3) - - expect(times[0].getDate()).toBe(29) - expect(times[0].getMonth()).toBe(0) - expect(times[1].getDate()).toBe(29) - expect(times[1].getMonth()).toBe(2) - }) - }) - - describe('Time handling', () => { - test('respects specified execution time', () => { - const config = createMonthlyConfig([1], '2:45 PM') - const times = getNextExecutionTimes(config, 1) - - expect(times[0].getHours()).toBe(14) - expect(times[0].getMinutes()).toBe(45) - }) - - test('handles AM/PM conversion correctly', () => { - const configAM = createMonthlyConfig([1], '6:30 AM') - const configPM = createMonthlyConfig([1], '6:30 PM') - - const timesAM = getNextExecutionTimes(configAM, 1) - const timesPM = getNextExecutionTimes(configPM, 1) - - expect(timesAM[0].getHours()).toBe(6) - expect(timesPM[0].getHours()).toBe(18) - }) - - test('handles 12 AM and 12 PM correctly', () => { - const config12AM = createMonthlyConfig([1], '12:00 AM') - const config12PM = createMonthlyConfig([1], '12:00 PM') - - const times12AM = getNextExecutionTimes(config12AM, 1) - const times12PM = getNextExecutionTimes(config12PM, 1) - - expect(times12AM[0].getHours()).toBe(0) - expect(times12PM[0].getHours()).toBe(12) - }) - }) -}) diff --git a/web/app/components/workflow/nodes/trigger-schedule/__tests__/monthly-validation.test.ts b/web/app/components/workflow/nodes/trigger-schedule/__tests__/monthly-validation.test.ts deleted file mode 100644 index c23622efad..0000000000 --- a/web/app/components/workflow/nodes/trigger-schedule/__tests__/monthly-validation.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import nodeDefault from '../default' - -const mockT = (key: string, options?: any) => { - const translations: Record = { - 'workflow.errorMsg.fieldRequired': `${options?.field} is required`, - 'workflow.nodes.triggerSchedule.monthlyDay': 'Monthly Day', - 'workflow.nodes.triggerSchedule.invalidMonthlyDay': 'Invalid monthly day', - 'workflow.nodes.triggerSchedule.time': 'Time', - 'workflow.nodes.triggerSchedule.invalidTimeFormat': 'Invalid time format', - } - return translations[key] || key -} - -describe('Monthly Validation', () => { - describe('Single day validation', () => { - test('validates single day selection', () => { - const config = { - mode: 'visual' as const, - frequency: 'monthly' as const, - visual_config: { - time: '10:30 AM', - monthly_days: [15], - }, - timezone: 'UTC', - } - - const result = nodeDefault.checkValid(config, mockT) - expect(result.isValid).toBe(true) - expect(result.errorMessage).toBe('') - }) - - test('validates last day selection', () => { - const config = { - mode: 'visual' as const, - frequency: 'monthly' as const, - visual_config: { - time: '10:30 AM', - monthly_days: ['last' as const], - }, - timezone: 'UTC', - } - - const result = nodeDefault.checkValid(config, mockT) - expect(result.isValid).toBe(true) - expect(result.errorMessage).toBe('') - }) - }) - - describe('Multi-day validation', () => { - test('validates multiple day selection', () => { - const config = { - mode: 'visual' as const, - frequency: 'monthly' as const, - visual_config: { - time: '10:30 AM', - monthly_days: [1, 15, 30], - }, - timezone: 'UTC', - } - - const result = nodeDefault.checkValid(config, mockT) - expect(result.isValid).toBe(true) - expect(result.errorMessage).toBe('') - }) - - test('validates mixed selection with last day', () => { - const config = { - mode: 'visual' as const, - frequency: 'monthly' as const, - visual_config: { - time: '10:30 AM', - monthly_days: [1, 15, 'last' as const], - }, - timezone: 'UTC', - } - - const result = nodeDefault.checkValid(config, mockT) - expect(result.isValid).toBe(true) - expect(result.errorMessage).toBe('') - }) - - test('rejects empty array', () => { - const config = { - mode: 'visual' as const, - frequency: 'monthly' as const, - visual_config: { - time: '10:30 AM', - monthly_days: [], - }, - timezone: 'UTC', - } - - const result = nodeDefault.checkValid(config, mockT) - expect(result.isValid).toBe(false) - expect(result.errorMessage).toBe('Monthly Day is required') - }) - - test('rejects invalid day in array', () => { - const config = { - mode: 'visual' as const, - frequency: 'monthly' as const, - visual_config: { - time: '10:30 AM', - monthly_days: [1, 35, 15], - }, - timezone: 'UTC', - } - - const result = nodeDefault.checkValid(config, mockT) - expect(result.isValid).toBe(false) - expect(result.errorMessage).toBe('Invalid monthly day') - }) - }) - - describe('Edge cases', () => { - test('requires monthly configuration', () => { - const config = { - mode: 'visual' as const, - frequency: 'monthly' as const, - visual_config: { - time: '10:30 AM', - }, - timezone: 'UTC', - } - - const result = nodeDefault.checkValid(config, mockT) - expect(result.isValid).toBe(false) - expect(result.errorMessage).toBe('Monthly Day is required') - }) - - test('validates time format along with monthly_days', () => { - const config = { - mode: 'visual' as const, - frequency: 'monthly' as const, - visual_config: { - time: 'invalid-time', - monthly_days: [1, 15], - }, - timezone: 'UTC', - } - - const result = nodeDefault.checkValid(config, mockT) - expect(result.isValid).toBe(false) - expect(result.errorMessage).toBe('Invalid time format') - }) - - test('handles very large arrays', () => { - const config = { - mode: 'visual' as const, - frequency: 'monthly' as const, - visual_config: { - time: '10:30 AM', - monthly_days: Array.from({ length: 31 }, (_, i) => i + 1), - }, - timezone: 'UTC', - } - - const result = nodeDefault.checkValid(config, mockT) - expect(result.isValid).toBe(true) - expect(result.errorMessage).toBe('') - }) - }) -}) diff --git a/web/app/components/workflow/nodes/trigger-schedule/__tests__/weekly-time-logic.test.ts b/web/app/components/workflow/nodes/trigger-schedule/__tests__/weekly-time-logic.test.ts deleted file mode 100644 index a6c00d069f..0000000000 --- a/web/app/components/workflow/nodes/trigger-schedule/__tests__/weekly-time-logic.test.ts +++ /dev/null @@ -1,443 +0,0 @@ -import { getNextExecutionTimes } from '../utils/execution-time-calculator' -import type { ScheduleTriggerNodeType } from '../types' - -const createWeeklyConfig = ( - weekdays: string[], - time = '2:30 PM', - timezone = 'UTC', -): ScheduleTriggerNodeType => ({ - id: 'test-node', - type: 'schedule-trigger', - mode: 'visual', - frequency: 'weekly', - visual_config: { - time, - weekdays, - }, - timezone, -}) - -describe('Weekly Schedule Time Logic Tests', () => { - beforeEach(() => { - jest.useFakeTimers() - }) - - afterEach(() => { - jest.useRealTimers() - }) - - describe('Same weekday time comparison logic', () => { - test('should execute today when time has not passed yet', () => { - jest.setSystemTime(new Date('2024-08-28T10:00:00.000Z')) - - const config = createWeeklyConfig(['wed'], '2:30 PM', 'UTC') - const times = getNextExecutionTimes(config, 3) - - expect(times[0].getDay()).toBe(3) - expect(times[0].getDate()).toBe(28) - expect(times[0].getHours()).toBe(14) - expect(times[0].getMinutes()).toBe(30) - }) - - test('should skip to next week when time has already passed', () => { - jest.setSystemTime(new Date('2024-08-28T16:00:00.000Z')) - - const config = createWeeklyConfig(['wed'], '2:30 PM', 'UTC') - const times = getNextExecutionTimes(config, 3) - - expect(times[0].getDay()).toBe(3) - expect(times[0].getDate()).toBe(4) - expect(times[0].getMonth()).toBe(8) - expect(times[0].getHours()).toBe(14) - expect(times[0].getMinutes()).toBe(30) - }) - - test('should skip to next week when exact time has passed', () => { - jest.setSystemTime(new Date('2024-08-28T14:30:01.000Z')) - - const config = createWeeklyConfig(['wed'], '2:30 PM', 'UTC') - const times = getNextExecutionTimes(config, 3) - - expect(times[0].getDate()).toBe(4) - expect(times[0].getMonth()).toBe(8) - }) - - test('should execute today when time is exactly now', () => { - jest.setSystemTime(new Date('2024-08-28T14:30:00.000Z')) - - const config = createWeeklyConfig(['wed'], '2:30 PM', 'UTC') - const times = getNextExecutionTimes(config, 3) - - expect(times[0].getDate()).toBe(4) - expect(times[0].getMonth()).toBe(8) - }) - }) - - describe('Cross-day scenarios', () => { - test('should handle early morning execution on same day', () => { - jest.setSystemTime(new Date('2024-08-28T02:00:00.000Z')) - - const config = createWeeklyConfig(['wed'], '6:00 AM', 'UTC') - const times = getNextExecutionTimes(config, 2) - - expect(times[0].getDate()).toBe(28) - expect(times[0].getHours()).toBe(6) - expect(times[1].getDate()).toBe(4) - expect(times[1].getMonth()).toBe(8) - }) - - test('should handle midnight execution correctly', () => { - jest.setSystemTime(new Date('2024-08-27T23:30:00.000Z')) - - const config = createWeeklyConfig(['wed'], '12:00 AM', 'UTC') - const times = getNextExecutionTimes(config, 2) - - expect(times[0].getDate()).toBe(28) - expect(times[0].getHours()).toBe(0) - expect(times[1].getDate()).toBe(4) - expect(times[1].getMonth()).toBe(8) - }) - - test('should handle noon execution correctly', () => { - jest.setSystemTime(new Date('2024-08-28T10:00:00.000Z')) - - const config = createWeeklyConfig(['wed'], '12:00 PM', 'UTC') - const times = getNextExecutionTimes(config, 2) - - expect(times[0].getDate()).toBe(28) - expect(times[0].getHours()).toBe(12) - }) - }) - - describe('Multiple weekdays with time logic', () => { - test('should respect time for multiple weekdays in same week', () => { - jest.setSystemTime(new Date('2024-08-28T10:00:00.000Z')) - - const config = createWeeklyConfig(['wed', 'fri'], '2:00 PM', 'UTC') - const times = getNextExecutionTimes(config, 4) - - expect(times[0].getDay()).toBe(3) - expect(times[0].getDate()).toBe(28) - - expect(times[1].getDay()).toBe(5) - expect(times[1].getDate()).toBe(30) - - expect(times[2].getDay()).toBe(3) - expect(times[2].getDate()).toBe(4) - expect(times[2].getMonth()).toBe(8) - }) - - test('should skip past weekdays in current week', () => { - jest.setSystemTime(new Date('2024-08-28T16:00:00.000Z')) - - const config = createWeeklyConfig(['mon', 'wed'], '2:00 PM', 'UTC') - const times = getNextExecutionTimes(config, 4) - - expect(times[0].getDay()).toBe(1) - expect(times[0].getDate()).toBe(2) - expect(times[0].getMonth()).toBe(8) - - expect(times[1].getDay()).toBe(3) - expect(times[1].getDate()).toBe(4) - expect(times[1].getMonth()).toBe(8) - }) - - test('should handle weekend execution correctly', () => { - jest.setSystemTime(new Date('2024-08-31T10:00:00.000Z')) - - const config = createWeeklyConfig(['sat', 'sun'], '9:00 AM', 'UTC') - const times = getNextExecutionTimes(config, 4) - - expect(times[0].getDay()).toBe(0) - expect(times[0].getDate()).toBe(1) - expect(times[0].getMonth()).toBe(8) - - expect(times[1].getDay()).toBe(6) - expect(times[1].getDate()).toBe(7) - expect(times[1].getMonth()).toBe(8) - }) - }) - - describe('Timezone handling with time logic', () => { - test('should respect timezone when checking if time has passed', () => { - jest.setSystemTime(new Date('2024-08-28T14:30:00.000Z')) - - const config = createWeeklyConfig(['wed'], '6:00 PM', 'America/New_York') - const times = getNextExecutionTimes(config, 2) - - expect(times[0].getDate()).toBe(28) - expect(times[0].getHours()).toBe(18) - }) - - test('should handle timezone difference when time has passed', () => { - jest.setSystemTime(new Date('2024-08-28T23:00:00.000Z')) - - const config = createWeeklyConfig(['wed'], '6:00 PM', 'America/New_York') - const times = getNextExecutionTimes(config, 2) - - expect(times[0].getDate()).toBe(4) - expect(times[0].getMonth()).toBe(8) - }) - }) - - describe('Edge cases and boundary conditions', () => { - test('should handle year boundary correctly with time logic', () => { - jest.setSystemTime(new Date('2024-12-31T10:00:00.000Z')) - - const config = createWeeklyConfig(['tue'], '2:00 PM', 'UTC') - const times = getNextExecutionTimes(config, 3) - - expect(times[0].getDate()).toBe(31) - expect(times[0].getMonth()).toBe(11) - expect(times[0].getFullYear()).toBe(2024) - - expect(times[1].getDate()).toBe(7) - expect(times[1].getMonth()).toBe(0) - expect(times[1].getFullYear()).toBe(2025) - }) - - test('should handle month boundary correctly', () => { - jest.setSystemTime(new Date('2024-08-31T10:00:00.000Z')) - - const config = createWeeklyConfig(['sat'], '2:00 PM', 'UTC') - const times = getNextExecutionTimes(config, 2) - - expect(times[0].getDate()).toBe(31) - expect(times[0].getMonth()).toBe(7) - - expect(times[1].getDate()).toBe(7) - expect(times[1].getMonth()).toBe(8) - }) - - test('should handle leap year February correctly', () => { - jest.setSystemTime(new Date('2024-02-29T10:00:00.000Z')) - - const config = createWeeklyConfig(['thu'], '2:00 PM', 'UTC') - const times = getNextExecutionTimes(config, 2) - - expect(times[0].getDate()).toBe(29) - expect(times[0].getMonth()).toBe(1) - expect(times[0].getFullYear()).toBe(2024) - - expect(times[1].getDate()).toBe(7) - expect(times[1].getMonth()).toBe(2) - }) - - test('should handle daylight saving time transitions', () => { - jest.setSystemTime(new Date('2024-03-10T10:00:00.000Z')) - - const config = createWeeklyConfig(['sun'], '2:00 AM', 'America/New_York') - const times = getNextExecutionTimes(config, 3) - - expect(times.length).toBeGreaterThan(0) - times.forEach((time) => { - expect(time.getDay()).toBe(0) - expect(time.getHours()).toBe(2) - }) - }) - }) - - describe('Validation of PR #24641 fix', () => { - test('should correctly calculate weekday offsets (not use index as day offset)', () => { - jest.setSystemTime(new Date('2024-08-28T10:00:00.000Z')) - - const config = createWeeklyConfig(['sun'], '2:00 PM', 'UTC') - const times = getNextExecutionTimes(config, 2) - - expect(times[0].getDay()).toBe(0) - expect(times[0].getDate()).toBe(1) - expect(times[0].getMonth()).toBe(8) - - expect(times[1].getDate()).toBe(8) - expect(times[1].getMonth()).toBe(8) - }) - - test('should correctly handle multiple weekdays selection', () => { - jest.setSystemTime(new Date('2024-08-26T11:00:00.000Z')) - - const config = createWeeklyConfig(['mon', 'wed', 'fri'], '9:00 AM', 'UTC') - const times = getNextExecutionTimes(config, 6) - - expect(times[0].getDay()).toBe(3) - expect(times[0].getDate()).toBe(28) - - expect(times[1].getDay()).toBe(5) - expect(times[1].getDate()).toBe(30) - - expect(times[2].getDay()).toBe(1) - expect(times[2].getDate()).toBe(2) - expect(times[2].getMonth()).toBe(8) - }) - - test('should prevent infinite loops with invalid weekdays', () => { - jest.setSystemTime(new Date('2024-08-28T10:00:00.000Z')) - - const config = createWeeklyConfig(['invalid'], '2:00 PM', 'UTC') - const times = getNextExecutionTimes(config, 3) - - expect(times).toEqual([]) - }) - }) - - describe('Comprehensive time scenarios for all weekdays', () => { - const weekdays = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] - - test.each(weekdays)('should respect time logic for %s', (weekday) => { - jest.setSystemTime(new Date('2024-08-28T10:00:00.000Z')) - - const config = createWeeklyConfig([weekday], '2:00 PM', 'UTC') - const times = getNextExecutionTimes(config, 3) - - expect(times.length).toBe(3) - times.forEach((time) => { - expect(time.getHours()).toBe(14) - expect(time.getMinutes()).toBe(0) - }) - }) - - test.each(weekdays)('should handle early morning execution for %s', (weekday) => { - jest.setSystemTime(new Date('2024-08-28T23:00:00.000Z')) - - const config = createWeeklyConfig([weekday], '6:00 AM', 'UTC') - const times = getNextExecutionTimes(config, 2) - - expect(times.length).toBeGreaterThan(0) - times.forEach((time) => { - expect(time.getHours()).toBe(6) - expect(time.getMinutes()).toBe(0) - }) - }) - }) - - describe('Performance and edge cases', () => { - test('should complete execution within reasonable time', () => { - jest.setSystemTime(new Date('2024-08-28T10:00:00.000Z')) - - const start = performance.now() - - const config = createWeeklyConfig(['mon', 'tue', 'wed', 'thu', 'fri'], '9:00 AM', 'UTC') - const times = getNextExecutionTimes(config, 10) - - const end = performance.now() - - expect(times.length).toBe(10) - expect(end - start).toBeLessThan(50) - }) - - test('should handle large count requests efficiently', () => { - jest.setSystemTime(new Date('2024-08-28T10:00:00.000Z')) - - const config = createWeeklyConfig(['sun'], '2:00 PM', 'UTC') - const times = getNextExecutionTimes(config, 100) - - expect(times.length).toBe(100) - - for (let i = 1; i < times.length; i++) { - const timeDiff = times[i].getTime() - times[i - 1].getTime() - const daysDiff = timeDiff / (1000 * 60 * 60 * 24) - expect(daysDiff).toBe(7) - } - }) - - test('should handle empty weekdays array', () => { - jest.setSystemTime(new Date('2024-08-28T10:00:00.000Z')) - - const config = createWeeklyConfig([], '2:00 PM', 'UTC') - const times = getNextExecutionTimes(config, 3) - - expect(times).toEqual([]) - }) - }) - - describe('Comparison with other frequency modes consistency', () => { - test('should behave consistently with daily mode time logic', () => { - jest.setSystemTime(new Date('2024-08-28T10:00:00.000Z')) - - const weeklyConfig = createWeeklyConfig(['wed'], '2:00 PM', 'UTC') - const dailyConfig: ScheduleTriggerNodeType = { - id: 'test-node', - type: 'schedule-trigger', - mode: 'visual', - frequency: 'daily', - visual_config: { - time: '2:00 PM', - }, - timezone: 'UTC', - } - - const weeklyTimes = getNextExecutionTimes(weeklyConfig, 1) - const dailyTimes = getNextExecutionTimes(dailyConfig, 1) - - expect(weeklyTimes[0].getDate()).toBe(28) - expect(dailyTimes[0].getDate()).toBe(28) - expect(weeklyTimes[0].getHours()).toBe(14) - expect(dailyTimes[0].getHours()).toBe(14) - }) - - test('should behave consistently when time has passed', () => { - jest.setSystemTime(new Date('2024-08-28T16:00:00.000Z')) - - const weeklyConfig = createWeeklyConfig(['wed'], '2:00 PM', 'UTC') - const dailyConfig: ScheduleTriggerNodeType = { - id: 'test-node', - type: 'schedule-trigger', - mode: 'visual', - frequency: 'daily', - visual_config: { - time: '2:00 PM', - }, - timezone: 'UTC', - } - - const weeklyTimes = getNextExecutionTimes(weeklyConfig, 1) - const dailyTimes = getNextExecutionTimes(dailyConfig, 1) - - expect(weeklyTimes[0].getDate()).toBe(4) - expect(dailyTimes[0].getDate()).toBe(29) - - expect(weeklyTimes[0].getHours()).toBe(14) - expect(dailyTimes[0].getHours()).toBe(14) - }) - }) - - describe('Real-world scenarios', () => { - test('Monday morning meeting scheduled on Monday at 10am should execute today if before 10am', () => { - jest.setSystemTime(new Date('2024-08-26T08:00:00.000Z')) - - const config = createWeeklyConfig(['mon'], '10:00 AM', 'UTC') - const times = getNextExecutionTimes(config, 1) - - expect(times[0].getDay()).toBe(1) - expect(times[0].getDate()).toBe(26) - expect(times[0].getHours()).toBe(10) - }) - - test('Friday afternoon report scheduled on Friday at 5pm should wait until next Friday if after 5pm', () => { - jest.setSystemTime(new Date('2024-08-30T18:00:00.000Z')) - - const config = createWeeklyConfig(['fri'], '5:00 PM', 'UTC') - const times = getNextExecutionTimes(config, 1) - - expect(times[0].getDay()).toBe(5) - expect(times[0].getDate()).toBe(6) - expect(times[0].getMonth()).toBe(8) - expect(times[0].getHours()).toBe(17) - }) - - test('Weekend cleanup scheduled for Saturday and Sunday should work correctly', () => { - jest.setSystemTime(new Date('2024-08-30T14:00:00.000Z')) - - const config = createWeeklyConfig(['sat', 'sun'], '11:00 PM', 'UTC') - const times = getNextExecutionTimes(config, 4) - - expect(times[0].getDay()).toBe(6) - expect(times[0].getDate()).toBe(31) - expect(times[0].getHours()).toBe(23) - - expect(times[1].getDay()).toBe(0) - expect(times[1].getDate()).toBe(1) - expect(times[1].getMonth()).toBe(8) - expect(times[1].getHours()).toBe(23) - }) - }) -}) diff --git a/web/app/components/workflow/nodes/trigger-webhook/__tests__/default.test.ts b/web/app/components/workflow/nodes/trigger-webhook/__tests__/default.test.ts deleted file mode 100644 index 5036e6feac..0000000000 --- a/web/app/components/workflow/nodes/trigger-webhook/__tests__/default.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Webhook Trigger Node Default Tests - * - * Tests for webhook trigger node default configuration and field validation. - * Tests core checkValid functionality following project patterns. - */ - -import nodeDefault from '../default' -import type { WebhookTriggerNodeType } from '../types' - -// Simple mock translation function -const mockT = (key: string, params?: any) => { - const translations: Record = { - 'workflow.nodes.triggerWebhook.validation.webhookUrlRequired': 'Webhook URL is required', - 'workflow.nodes.triggerWebhook.validation.invalidParameterType': `Invalid parameter type ${params?.type} for ${params?.name}`, - } - - if (key.includes('fieldRequired')) return `${params?.field} is required` - return translations[key] || key -} - -describe('Webhook Trigger Node Default', () => { - describe('Basic Configuration', () => { - it('should have correct default values for all backend fields', () => { - const defaultValue = nodeDefault.defaultValue - - // Core webhook configuration - expect(defaultValue.webhook_url).toBe('') - expect(defaultValue.method).toBe('POST') - expect(defaultValue.content_type).toBe('application/json') - - // Response configuration fields - expect(defaultValue.async_mode).toBe(true) - expect(defaultValue.status_code).toBe(200) - expect(defaultValue.response_body).toBe('') - - // Parameter arrays - expect(Array.isArray(defaultValue.headers)).toBe(true) - expect(Array.isArray(defaultValue.params)).toBe(true) - expect(Array.isArray(defaultValue.body)).toBe(true) - expect(Array.isArray(defaultValue.variables)).toBe(true) - - // Initial arrays should be empty - expect(defaultValue.headers).toHaveLength(0) - expect(defaultValue.params).toHaveLength(0) - expect(defaultValue.body).toHaveLength(0) - expect(defaultValue.variables).toHaveLength(1) - - const rawVariable = defaultValue.variables?.[0] - expect(rawVariable?.variable).toBe('_webhook_raw') - expect(rawVariable?.label).toBe('raw') - expect(rawVariable?.value_type).toBe('object') - }) - - it('should have correct metadata for trigger node', () => { - expect(nodeDefault.metaData).toBeDefined() - expect(nodeDefault.metaData.type).toBe('trigger-webhook') - expect(nodeDefault.metaData.sort).toBe(3) - expect(nodeDefault.metaData.isStart).toBe(true) - }) - }) - - describe('Validation - checkValid', () => { - it('should require webhook_url to be configured', () => { - const payload = nodeDefault.defaultValue as WebhookTriggerNodeType - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(false) - expect(result.errorMessage).toContain('required') - }) - - it('should validate successfully when webhook_url is provided', () => { - const payload = { - ...nodeDefault.defaultValue, - webhook_url: 'https://example.com/webhook', - } as WebhookTriggerNodeType - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(true) - expect(result.errorMessage).toBe('') - }) - - it('should handle response configuration fields when webhook_url is provided', () => { - const payload = { - ...nodeDefault.defaultValue, - webhook_url: 'https://example.com/webhook', - status_code: 404, - response_body: '{"error": "Not found"}', - } as WebhookTriggerNodeType - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(true) - }) - - it('should handle async_mode field correctly when webhook_url is provided', () => { - const payload = { - ...nodeDefault.defaultValue, - webhook_url: 'https://example.com/webhook', - async_mode: false, - } as WebhookTriggerNodeType - - const result = nodeDefault.checkValid(payload, mockT) - expect(result.isValid).toBe(true) - }) - }) -}) diff --git a/web/app/components/workflow/nodes/trigger-webhook/utils/parameter-type-utils.test.ts b/web/app/components/workflow/nodes/trigger-webhook/utils/parameter-type-utils.test.ts deleted file mode 100644 index 34b1ec4e6f..0000000000 --- a/web/app/components/workflow/nodes/trigger-webhook/utils/parameter-type-utils.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - createParameterTypeOptions, - getAvailableParameterTypes, - isValidParameterType, - normalizeParameterType, -} from './parameter-type-utils' - -describe('Parameter Type Utils', () => { - describe('isValidParameterType', () => { - it('should validate specific array types', () => { - expect(isValidParameterType('array[string]')).toBe(true) - expect(isValidParameterType('array[number]')).toBe(true) - expect(isValidParameterType('array[boolean]')).toBe(true) - expect(isValidParameterType('array[object]')).toBe(true) - }) - - it('should validate basic types', () => { - expect(isValidParameterType('string')).toBe(true) - expect(isValidParameterType('number')).toBe(true) - expect(isValidParameterType('boolean')).toBe(true) - expect(isValidParameterType('object')).toBe(true) - expect(isValidParameterType('file')).toBe(true) - }) - - it('should reject invalid types', () => { - expect(isValidParameterType('array')).toBe(false) - expect(isValidParameterType('invalid')).toBe(false) - expect(isValidParameterType('array[invalid]')).toBe(false) - }) - }) - - describe('normalizeParameterType', () => { - it('should normalize valid types', () => { - expect(normalizeParameterType('string')).toBe('string') - expect(normalizeParameterType('array[string]')).toBe('array[string]') - }) - - it('should migrate legacy array type', () => { - expect(normalizeParameterType('array')).toBe('array[string]') - }) - - it('should default to string for invalid types', () => { - expect(normalizeParameterType('invalid')).toBe('string') - }) - }) - - describe('getAvailableParameterTypes', () => { - it('should return only string for non-request body', () => { - const types = getAvailableParameterTypes('application/json', false) - expect(types).toEqual(['string']) - }) - - it('should return all types for application/json', () => { - const types = getAvailableParameterTypes('application/json', true) - expect(types).toContain('string') - expect(types).toContain('number') - expect(types).toContain('boolean') - expect(types).toContain('array[string]') - expect(types).toContain('array[number]') - expect(types).toContain('array[boolean]') - expect(types).toContain('array[object]') - expect(types).toContain('object') - }) - - it('should include file type for multipart/form-data', () => { - const types = getAvailableParameterTypes('multipart/form-data', true) - expect(types).toContain('file') - }) - }) - - describe('createParameterTypeOptions', () => { - it('should create options with display names', () => { - const options = createParameterTypeOptions('application/json', true) - const stringOption = options.find(opt => opt.value === 'string') - const arrayStringOption = options.find(opt => opt.value === 'array[string]') - - expect(stringOption?.name).toBe('String') - expect(arrayStringOption?.name).toBe('Array[String]') - }) - }) -})