This commit is contained in:
lyzno1 2025-10-30 17:05:27 +08:00
parent 8750796f9f
commit d137d0eed0
No known key found for this signature in database
2 changed files with 0 additions and 895 deletions

View File

@ -1,520 +0,0 @@
import { isValidCronExpression, parseCronExpression } from './cron-parser'
describe('cron-parser', () => {
describe('isValidCronExpression', () => {
test('validates correct cron expressions', () => {
expect(isValidCronExpression('15 10 1 * *')).toBe(true)
expect(isValidCronExpression('0 0 * * 0')).toBe(true)
expect(isValidCronExpression('*/5 * * * *')).toBe(true)
expect(isValidCronExpression('0 9-17 * * 1-5')).toBe(true)
expect(isValidCronExpression('30 14 * * 1')).toBe(true)
expect(isValidCronExpression('0 0 1,15 * *')).toBe(true)
})
test('validates enhanced dayOfWeek syntax', () => {
expect(isValidCronExpression('0 9 * * 7')).toBe(true) // Sunday as 7
expect(isValidCronExpression('0 9 * * SUN')).toBe(true) // Sunday abbreviation
expect(isValidCronExpression('0 9 * * MON')).toBe(true) // Monday abbreviation
expect(isValidCronExpression('0 9 * * MON-FRI')).toBe(true) // Range with abbreviations
expect(isValidCronExpression('0 9 * * SUN,WED,FRI')).toBe(true) // List with abbreviations
})
test('validates enhanced month syntax', () => {
expect(isValidCronExpression('0 9 1 JAN *')).toBe(true) // January abbreviation
expect(isValidCronExpression('0 9 1 DEC *')).toBe(true) // December abbreviation
expect(isValidCronExpression('0 9 1 JAN-MAR *')).toBe(true) // Range with abbreviations
expect(isValidCronExpression('0 9 1 JAN,JUN,DEC *')).toBe(true) // List with abbreviations
})
test('validates special characters', () => {
expect(isValidCronExpression('0 9 ? * 1')).toBe(true) // ? wildcard
expect(isValidCronExpression('0 9 L * *')).toBe(true) // Last day of month
expect(isValidCronExpression('0 9 * * 1#1')).toBe(true) // First Monday of month
expect(isValidCronExpression('0 9 * * 1L')).toBe(true) // Last Monday of month
})
test('validates predefined expressions', () => {
expect(isValidCronExpression('@yearly')).toBe(true)
expect(isValidCronExpression('@monthly')).toBe(true)
expect(isValidCronExpression('@weekly')).toBe(true)
expect(isValidCronExpression('@daily')).toBe(true)
expect(isValidCronExpression('@hourly')).toBe(true)
})
test('rejects invalid cron expressions', () => {
expect(isValidCronExpression('')).toBe(false)
expect(isValidCronExpression('15 10 1')).toBe(false) // Not enough fields
expect(isValidCronExpression('15 10 1 * * *')).toBe(false) // Too many fields
expect(isValidCronExpression('60 10 1 * *')).toBe(false) // Invalid minute
expect(isValidCronExpression('15 25 1 * *')).toBe(false) // Invalid hour
expect(isValidCronExpression('15 10 32 * *')).toBe(false) // Invalid day
expect(isValidCronExpression('15 10 1 13 *')).toBe(false) // Invalid month
expect(isValidCronExpression('15 10 1 * 8')).toBe(false) // Invalid day of week
expect(isValidCronExpression('15 10 1 INVALID *')).toBe(false) // Invalid month abbreviation
expect(isValidCronExpression('15 10 1 * INVALID')).toBe(false) // Invalid day abbreviation
expect(isValidCronExpression('@invalid')).toBe(false) // Invalid predefined expression
})
test('handles edge cases', () => {
expect(isValidCronExpression(' 15 10 1 * * ')).toBe(true) // Whitespace
expect(isValidCronExpression('0 0 29 2 *')).toBe(true) // Feb 29 (valid in leap years)
expect(isValidCronExpression('59 23 31 12 6')).toBe(true) // Max values
expect(isValidCronExpression('0 0 29 FEB *')).toBe(true) // Feb 29 with month abbreviation
expect(isValidCronExpression('59 23 31 DEC SAT')).toBe(true) // Max values with abbreviations
})
})
describe('parseCronExpression', () => {
beforeEach(() => {
// Mock current time to make tests deterministic
jest.useFakeTimers()
jest.setSystemTime(new Date('2024-01-15T10:00:00Z'))
})
afterEach(() => {
jest.useRealTimers()
})
test('parses monthly expressions correctly', () => {
const result = parseCronExpression('15 10 1 * *') // 1st day of every month at 10:15
expect(result).toHaveLength(5)
expect(result[0].getDate()).toBe(1) // February 1st
expect(result[0].getHours()).toBe(10)
expect(result[0].getMinutes()).toBe(15)
expect(result[1].getDate()).toBe(1) // March 1st
expect(result[2].getDate()).toBe(1) // April 1st
})
test('parses weekly expressions correctly', () => {
const result = parseCronExpression('30 14 * * 1') // Every Monday at 14:30
expect(result).toHaveLength(5)
// Should find next 5 Mondays
result.forEach((date) => {
expect(date.getDay()).toBe(1) // Monday
expect(date.getHours()).toBe(14)
expect(date.getMinutes()).toBe(30)
})
})
test('parses daily expressions correctly', () => {
const result = parseCronExpression('0 9 * * *') // Every day at 9:00
expect(result).toHaveLength(5)
result.forEach((date) => {
expect(date.getHours()).toBe(9)
expect(date.getMinutes()).toBe(0)
})
// Should be consecutive days (starting from tomorrow since current time is 10:00)
for (let i = 1; i < result.length; i++) {
const prevDate = new Date(result[i - 1])
const currDate = new Date(result[i])
const dayDiff = (currDate.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24)
expect(dayDiff).toBe(1)
}
})
test('handles complex cron expressions with ranges', () => {
const result = parseCronExpression('0 9-17 * * 1-5') // Weekdays, 9-17 hours
expect(result).toHaveLength(5)
result.forEach((date) => {
expect(date.getDay()).toBeGreaterThanOrEqual(1) // Monday
expect(date.getDay()).toBeLessThanOrEqual(5) // Friday
expect(date.getHours()).toBeGreaterThanOrEqual(9)
expect(date.getHours()).toBeLessThanOrEqual(17)
expect(date.getMinutes()).toBe(0)
})
})
test('handles step expressions', () => {
const result = parseCronExpression('*/15 * * * *') // Every 15 minutes
expect(result).toHaveLength(5)
result.forEach((date) => {
expect(date.getMinutes() % 15).toBe(0)
})
})
test('handles list expressions', () => {
const result = parseCronExpression('0 0 1,15 * *') // 1st and 15th of each month
expect(result).toHaveLength(5)
result.forEach((date) => {
expect([1, 15]).toContain(date.getDate())
expect(date.getHours()).toBe(0)
expect(date.getMinutes()).toBe(0)
})
})
test('handles expressions that span multiple months', () => {
// Test with an expression that might not have many matches in current month
const result = parseCronExpression('0 12 29 * *') // 29th of each month at noon
expect(result.length).toBeGreaterThan(0)
expect(result.length).toBeLessThanOrEqual(5)
result.forEach((date) => {
expect(date.getDate()).toBe(29)
expect(date.getHours()).toBe(12)
expect(date.getMinutes()).toBe(0)
})
})
test('returns empty array for invalid expressions', () => {
expect(parseCronExpression('')).toEqual([])
expect(parseCronExpression('invalid')).toEqual([])
expect(parseCronExpression('60 10 1 * *')).toEqual([])
expect(parseCronExpression('15 25 1 * *')).toEqual([])
})
test('handles edge case: February 29th in non-leap years', () => {
// Set to a non-leap year
jest.setSystemTime(new Date('2023-01-15T10:00:00Z'))
const result = parseCronExpression('0 12 29 2 *') // Feb 29th at noon
// Should return empty or skip 2023 and find 2024
if (result.length > 0) {
result.forEach((date) => {
expect(date.getMonth()).toBe(1) // February
expect(date.getDate()).toBe(29)
// Should be in a leap year
const year = date.getFullYear()
expect(year % 4).toBe(0)
})
}
})
test('sorts results chronologically', () => {
const result = parseCronExpression('0 */6 * * *') // Every 6 hours
expect(result).toHaveLength(5)
for (let i = 1; i < result.length; i++)
expect(result[i].getTime()).toBeGreaterThan(result[i - 1].getTime())
})
test('excludes past times', () => {
// Set current time to 15:30
jest.setSystemTime(new Date('2024-01-15T15:30:00Z'))
const result = parseCronExpression('0 10 * * *') // Daily at 10:00
expect(result).toHaveLength(5)
result.forEach((date) => {
// Since we're using UTC timezone in this test, the returned dates should
// be in the future relative to the current time
// Note: our implementation returns dates in "user timezone representation"
// but for UTC, this should match the expected behavior
expect(date.getTime()).toBeGreaterThan(Date.now())
})
// First result should be tomorrow since today's 10:00 has passed
expect(result[0].getDate()).toBe(16)
})
test('handles midnight expressions correctly', () => {
const result = parseCronExpression('0 0 * * *') // Daily at midnight
expect(result).toHaveLength(5)
result.forEach((date) => {
expect(date.getHours()).toBe(0)
expect(date.getMinutes()).toBe(0)
})
})
test('handles year boundary correctly', () => {
// Set to end of December
jest.setSystemTime(new Date('2024-12-30T10:00:00Z'))
const result = parseCronExpression('0 12 1 * *') // 1st of every month at noon
expect(result).toHaveLength(5)
// Should include January 1st of next year
const nextYear = result.find(date => date.getFullYear() === 2025)
expect(nextYear).toBeDefined()
if (nextYear) {
expect(nextYear.getMonth()).toBe(0) // January
expect(nextYear.getDate()).toBe(1)
}
})
})
describe('enhanced syntax tests', () => {
test('handles month abbreviations correctly', () => {
const result = parseCronExpression('0 12 1 JAN *') // First day of January at noon
expect(result).toHaveLength(5)
result.forEach((date) => {
expect(date.getMonth()).toBe(0) // January
expect(date.getDate()).toBe(1)
expect(date.getHours()).toBe(12)
expect(date.getMinutes()).toBe(0)
})
})
test('handles day abbreviations correctly', () => {
const result = parseCronExpression('0 14 * * MON') // Every Monday at 14:00
expect(result).toHaveLength(5)
result.forEach((date) => {
expect(date.getDay()).toBe(1) // Monday
expect(date.getHours()).toBe(14)
expect(date.getMinutes()).toBe(0)
})
})
test('handles Sunday as both 0 and 7', () => {
const result0 = parseCronExpression('0 10 * * 0') // Sunday as 0
const result7 = parseCronExpression('0 10 * * 7') // Sunday as 7
const resultSUN = parseCronExpression('0 10 * * SUN') // Sunday as SUN
expect(result0).toHaveLength(5)
expect(result7).toHaveLength(5)
expect(resultSUN).toHaveLength(5)
// All should return Sundays
result0.forEach(date => expect(date.getDay()).toBe(0))
result7.forEach(date => expect(date.getDay()).toBe(0))
resultSUN.forEach(date => expect(date.getDay()).toBe(0))
})
test('handles question mark wildcard', () => {
const resultStar = parseCronExpression('0 9 * * 1') // Using *
const resultQuestion = parseCronExpression('0 9 ? * 1') // Using ?
expect(resultStar).toHaveLength(5)
expect(resultQuestion).toHaveLength(5)
// Both should return Mondays at 9:00
resultStar.forEach((date) => {
expect(date.getDay()).toBe(1)
expect(date.getHours()).toBe(9)
})
resultQuestion.forEach((date) => {
expect(date.getDay()).toBe(1)
expect(date.getHours()).toBe(9)
})
})
test('handles predefined expressions', () => {
const daily = parseCronExpression('@daily')
const weekly = parseCronExpression('@weekly')
const monthly = parseCronExpression('@monthly')
expect(daily).toHaveLength(5)
expect(weekly).toHaveLength(5)
expect(monthly).toHaveLength(5)
// @daily should be at midnight
daily.forEach((date) => {
expect(date.getHours()).toBe(0)
expect(date.getMinutes()).toBe(0)
})
// @weekly should be on Sundays at midnight
weekly.forEach((date) => {
expect(date.getDay()).toBe(0) // Sunday
expect(date.getHours()).toBe(0)
expect(date.getMinutes()).toBe(0)
})
// @monthly should be on the 1st of each month at midnight
monthly.forEach((date) => {
expect(date.getDate()).toBe(1)
expect(date.getHours()).toBe(0)
expect(date.getMinutes()).toBe(0)
})
})
})
describe('edge cases and error handling', () => {
test('handles complex month/day combinations', () => {
// Test Feb 29 with month abbreviation
const result = parseCronExpression('0 12 29 FEB *')
if (result.length > 0) {
result.forEach((date) => {
expect(date.getMonth()).toBe(1) // February
expect(date.getDate()).toBe(29)
// Should only occur in leap years
const year = date.getFullYear()
expect(year % 4).toBe(0)
})
}
})
test('handles mixed syntax correctly', () => {
// Mix of numbers and abbreviations (using only dayOfMonth OR dayOfWeek, not both)
// Test 1: Month abbreviations with specific day
const result1 = parseCronExpression('30 14 15 JAN,JUN,DEC *')
expect(result1.length).toBeGreaterThan(0)
result1.forEach((date) => {
expect(date.getDate()).toBe(15) // Should be 15th day
expect([0, 5, 11]).toContain(date.getMonth()) // Jan, Jun, Dec
expect(date.getHours()).toBe(14)
expect(date.getMinutes()).toBe(30)
})
// Test 2: Month abbreviations with weekdays
const result2 = parseCronExpression('0 9 * JAN-MAR MON-FRI')
expect(result2.length).toBeGreaterThan(0)
result2.forEach((date) => {
// Should be weekday OR in Q1 months
const isWeekday = date.getDay() >= 1 && date.getDay() <= 5
const isQ1 = [0, 1, 2].includes(date.getMonth())
expect(isWeekday || isQ1).toBe(true)
expect(date.getHours()).toBe(9)
expect(date.getMinutes()).toBe(0)
})
})
test('handles timezone edge cases', () => {
// Test with different timezones
const utcResult = parseCronExpression('0 12 * * *', 'UTC')
const nyResult = parseCronExpression('0 12 * * *', 'America/New_York')
const tokyoResult = parseCronExpression('0 12 * * *', 'Asia/Tokyo')
expect(utcResult).toHaveLength(5)
expect(nyResult).toHaveLength(5)
expect(tokyoResult).toHaveLength(5)
// All should be at noon in their respective timezones
utcResult.forEach(date => expect(date.getHours()).toBe(12))
nyResult.forEach(date => expect(date.getHours()).toBe(12))
tokyoResult.forEach(date => expect(date.getHours()).toBe(12))
})
test('timezone compatibility and DST handling', () => {
// Test DST boundary scenarios
jest.useFakeTimers()
try {
// Test 1: DST spring forward (March 2024) - America/New_York
jest.setSystemTime(new Date('2024-03-08T10:00:00Z'))
const springDST = parseCronExpression('0 2 * * *', 'America/New_York')
expect(springDST).toHaveLength(5)
springDST.forEach(date => expect([2, 3]).toContain(date.getHours()))
// Test 2: DST fall back (November 2024) - America/New_York
jest.setSystemTime(new Date('2024-11-01T10:00:00Z'))
const fallDST = parseCronExpression('0 1 * * *', 'America/New_York')
expect(fallDST).toHaveLength(5)
fallDST.forEach(date => expect(date.getHours()).toBe(1))
// Test 3: Cross-timezone consistency on same UTC moment
jest.setSystemTime(new Date('2024-06-15T12:00:00Z'))
const utcNoon = parseCronExpression('0 12 * * *', 'UTC')
const nycMorning = parseCronExpression('0 8 * * *', 'America/New_York') // 8 AM NYC = 12 PM UTC in summer
const tokyoEvening = parseCronExpression('0 21 * * *', 'Asia/Tokyo') // 9 PM Tokyo = 12 PM UTC
expect(utcNoon).toHaveLength(5)
expect(nycMorning).toHaveLength(5)
expect(tokyoEvening).toHaveLength(5)
// Verify timezone consistency - all should represent the same UTC moments
const utcTime = utcNoon[0]
const nycTime = nycMorning[0]
const tokyoTime = tokyoEvening[0]
expect(utcTime.getHours()).toBe(12)
expect(nycTime.getHours()).toBe(8)
expect(tokyoTime.getHours()).toBe(21)
}
finally {
jest.useRealTimers()
}
})
test('backward compatibility with execution-time-calculator timezone logic', () => {
// Simulate the exact usage pattern from execution-time-calculator.ts:47
const mockData = {
cron_expression: '30 14 * * 1-5', // 2:30 PM weekdays
timezone: 'America/New_York',
}
// This is the exact call from execution-time-calculator.ts
const results = parseCronExpression(mockData.cron_expression, mockData.timezone)
expect(results).toHaveLength(5)
results.forEach((date) => {
// Should be weekdays (1-5)
expect(date.getDay()).toBeGreaterThanOrEqual(1)
expect(date.getDay()).toBeLessThanOrEqual(5)
// Should be 2:30 PM in the user's timezone representation
expect(date.getHours()).toBe(14)
expect(date.getMinutes()).toBe(30)
expect(date.getSeconds()).toBe(0)
// Should be Date objects (not CronDate or other types)
expect(date).toBeInstanceOf(Date)
// Should be in the future (relative to test time)
expect(date.getTime()).toBeGreaterThan(Date.now())
})
})
test('edge case timezone handling', () => {
// Test uncommon but valid timezones
const australiaResult = parseCronExpression('0 15 * * *', 'Australia/Sydney')
const indiaResult = parseCronExpression('0 9 * * *', 'Asia/Kolkata') // UTC+5:30
const alaskaResult = parseCronExpression('0 6 * * *', 'America/Anchorage')
expect(australiaResult).toHaveLength(5)
expect(indiaResult).toHaveLength(5)
expect(alaskaResult).toHaveLength(5)
australiaResult.forEach(date => expect(date.getHours()).toBe(15))
indiaResult.forEach(date => expect(date.getHours()).toBe(9))
alaskaResult.forEach(date => expect(date.getHours()).toBe(6))
// Test invalid timezone graceful handling
const invalidTzResult = parseCronExpression('0 12 * * *', 'Invalid/Timezone')
// Should either return empty array or handle gracefully
expect(Array.isArray(invalidTzResult)).toBe(true)
})
test('gracefully handles invalid enhanced syntax', () => {
// Invalid but close to valid expressions
expect(parseCronExpression('0 12 * JANUARY *')).toEqual([]) // Full month name
expect(parseCronExpression('0 12 * * MONDAY')).toEqual([]) // Full day name
expect(parseCronExpression('0 12 32 JAN *')).toEqual([]) // Invalid day with valid month
expect(parseCronExpression('@invalid')).toEqual([]) // Invalid predefined
})
})
describe('performance tests', () => {
test('performs well for complex expressions', () => {
const start = performance.now()
// Test multiple complex expressions including new syntax
const expressions = [
'*/5 9-17 * * 1-5', // Every 5 minutes, weekdays, business hours
'0 */2 1,15 * *', // Every 2 hours on 1st and 15th
'30 14 * * 1,3,5', // Mon, Wed, Fri at 14:30
'15,45 8-18 * * 1-5', // 15 and 45 minutes past the hour, weekdays
'0 9 * JAN-MAR MON-FRI', // Weekdays in Q1 at 9:00
'0 12 ? * SUN', // Sundays at noon using ?
'@daily', // Predefined expression
'@weekly', // Predefined expression
]
expressions.forEach((expr) => {
const result = parseCronExpression(expr)
expect(result.length).toBeGreaterThan(0)
expect(result.length).toBeLessThanOrEqual(5)
})
// Test quarterly expression separately (may return fewer than 5 results)
const quarterlyResult = parseCronExpression('0 0 1 */3 *') // First day of every 3rd month
expect(quarterlyResult.length).toBeGreaterThan(0)
expect(quarterlyResult.length).toBeLessThanOrEqual(5)
const end = performance.now()
// Should complete within reasonable time (less than 150ms for all expressions)
expect(end - start).toBeLessThan(150)
})
})
})

View File

@ -1,375 +0,0 @@
import {
getDefaultDateTime,
getNextExecutionTime,
getNextExecutionTimes,
} from './execution-time-calculator'
import type { ScheduleTriggerNodeType } from '../types'
const createMockData = (overrides: Partial<ScheduleTriggerNodeType> = {}): ScheduleTriggerNodeType => ({
id: 'test-node',
type: 'schedule-trigger',
mode: 'visual',
frequency: 'daily',
visual_config: {
time: '2:30 PM',
},
timezone: 'UTC',
...overrides,
})
describe('execution-time-calculator', () => {
beforeAll(() => {
jest.useFakeTimers()
jest.setSystemTime(new Date('2024-01-15T10:00:00Z'))
})
afterAll(() => {
jest.useRealTimers()
})
describe('getDefaultDateTime', () => {
it('returns consistent default datetime', () => {
const defaultDate = getDefaultDateTime()
expect(defaultDate.getFullYear()).toBe(2024)
expect(defaultDate.getMonth()).toBe(0)
expect(defaultDate.getDate()).toBe(2)
})
})
describe('daily frequency', () => {
it('generates daily executions at configured time', () => {
const data = createMockData({
frequency: 'daily',
visual_config: { time: '9:15 AM' },
timezone: 'UTC',
})
const result = getNextExecutionTimes(data, 2)
expect(result).toHaveLength(2)
expect(result[0].getHours()).toBe(9)
expect(result[0].getMinutes()).toBe(15)
})
it('skips today if time has passed', () => {
jest.setSystemTime(new Date('2024-01-15T15:00:00Z'))
const data = createMockData({
frequency: 'daily',
visual_config: { time: '2:30 PM' },
timezone: 'UTC',
})
const result = getNextExecutionTimes(data, 2)
expect(result[0].getDate()).toBe(16) // Tomorrow
})
})
describe('hourly frequency', () => {
it('generates hourly executions', () => {
const data = createMockData({
frequency: 'hourly',
visual_config: { on_minute: 30 },
timezone: 'UTC',
})
const result = getNextExecutionTimes(data, 2)
expect(result).toHaveLength(2)
expect(result[0].getMinutes()).toBe(30)
expect(result[1].getMinutes()).toBe(30)
})
})
describe('cron mode', () => {
it('handles valid cron expressions', () => {
const data = createMockData({
mode: 'cron',
cron_expression: '30 14 * * *',
timezone: 'UTC',
})
const result = getNextExecutionTimes(data, 2)
expect(result.length).toBeGreaterThan(0)
if (result.length > 0) {
expect(result[0].getHours()).toBe(14)
expect(result[0].getMinutes()).toBe(30)
}
})
it('returns empty for invalid cron', () => {
const data = createMockData({
mode: 'cron',
cron_expression: 'invalid',
timezone: 'UTC',
})
const result = getNextExecutionTimes(data, 2)
expect(result).toEqual([])
})
})
describe('getNextExecutionTime', () => {
it('returns formatted time string', () => {
const data = createMockData({
frequency: 'daily',
visual_config: { time: '3:15 PM' },
timezone: 'UTC',
})
const result = getNextExecutionTime(data)
expect(result).toContain('3:15 PM')
expect(result).toContain('2024')
})
})
describe('edge cases', () => {
it('handles zero count', () => {
const data = createMockData()
const result = getNextExecutionTimes(data, 0)
expect(result).toEqual([])
})
it('handles missing visual_config', () => {
const data = createMockData({
visual_config: undefined,
})
expect(() => getNextExecutionTimes(data, 1)).not.toThrow()
})
})
describe('timezone handling and cron integration', () => {
beforeEach(() => {
jest.useFakeTimers()
jest.setSystemTime(new Date('2024-01-15T10:00:00Z'))
})
afterEach(() => {
jest.useRealTimers()
})
it('cron mode timezone consistency', () => {
// Test the exact integration path with cron-parser
const data = createMockData({
mode: 'cron',
cron_expression: '0 9 * * 1-5', // 9 AM weekdays
timezone: 'America/New_York',
})
const result = getNextExecutionTimes(data, 3)
expect(result.length).toBeGreaterThan(0)
result.forEach((date) => {
// Should be weekdays
expect(date.getDay()).toBeGreaterThanOrEqual(1)
expect(date.getDay()).toBeLessThanOrEqual(5)
// Should be 9 AM in the target timezone representation
expect(date.getHours()).toBe(9)
expect(date.getMinutes()).toBe(0)
// Should be Date objects
expect(date).toBeInstanceOf(Date)
})
})
it('cron mode with enhanced syntax', () => {
// Test new cron syntax features work through execution-time-calculator
const testCases = [
{
expression: '@daily',
expectedHour: 0,
expectedMinute: 0,
},
{
expression: '0 15 * * MON',
expectedHour: 15,
expectedMinute: 0,
},
{
expression: '30 10 1 JAN *',
expectedHour: 10,
expectedMinute: 30,
},
]
testCases.forEach(({ expression, expectedHour, expectedMinute }) => {
const data = createMockData({
mode: 'cron',
cron_expression: expression,
timezone: 'UTC',
})
const result = getNextExecutionTimes(data, 1)
if (result.length > 0) {
expect(result[0].getHours()).toBe(expectedHour)
expect(result[0].getMinutes()).toBe(expectedMinute)
}
})
})
it('timezone consistency across different modes', () => {
const timezone = 'Europe/London'
// Test visual mode with timezone
const visualData = createMockData({
mode: 'visual',
frequency: 'daily',
visual_config: { time: '2:00 PM' },
timezone,
})
// Test cron mode with same timezone
const cronData = createMockData({
mode: 'cron',
cron_expression: '0 14 * * *', // 2:00 PM
timezone,
})
const visualResult = getNextExecutionTimes(visualData, 1)
const cronResult = getNextExecutionTimes(cronData, 1)
expect(visualResult).toHaveLength(1)
expect(cronResult).toHaveLength(1)
// Both should show 2 PM (14:00) in their timezone representation
expect(visualResult[0].getHours()).toBe(14)
expect(cronResult[0].getHours()).toBe(14)
})
it('DST boundary handling in cron mode', () => {
// Test around DST transition
jest.setSystemTime(new Date('2024-03-08T10:00:00Z')) // Before DST in US
const data = createMockData({
mode: 'cron',
cron_expression: '0 2 * * *', // 2 AM daily
timezone: 'America/New_York',
})
const result = getNextExecutionTimes(data, 5)
expect(result.length).toBeGreaterThan(0)
// During DST spring forward, 2 AM becomes 3 AM
// This is correct behavior - the cron-parser library handles DST properly
result.forEach((date) => {
// Should be either 2 AM (non-DST days) or 3 AM (DST transition day)
expect([2, 3]).toContain(date.getHours())
expect(date.getMinutes()).toBe(0)
})
})
it('invalid cron expression handling', () => {
const data = createMockData({
mode: 'cron',
cron_expression: '',
timezone: 'UTC',
})
const result = getNextExecutionTimes(data, 5)
expect(result).toEqual([])
// Test getNextExecutionTime with invalid cron
const timeString = getNextExecutionTime(data)
expect(timeString).toBe('--')
})
it('cron vs visual mode consistency check', () => {
// Compare equivalent expressions in both modes
const cronDaily = createMockData({
mode: 'cron',
cron_expression: '0 0 * * *', // Daily at midnight
timezone: 'UTC',
})
const visualDaily = createMockData({
mode: 'visual',
frequency: 'daily',
visual_config: { time: '12:00 AM' },
timezone: 'UTC',
})
const cronResult = getNextExecutionTimes(cronDaily, 1)
const visualResult = getNextExecutionTimes(visualDaily, 1)
if (cronResult.length > 0 && visualResult.length > 0) {
expect(cronResult[0].getHours()).toBe(visualResult[0].getHours())
expect(cronResult[0].getMinutes()).toBe(visualResult[0].getMinutes())
}
})
})
describe('weekly and monthly frequency timezone handling', () => {
beforeEach(() => {
jest.useFakeTimers()
jest.setSystemTime(new Date('2024-01-15T10:00:00Z'))
})
afterEach(() => {
jest.useRealTimers()
})
it('weekly frequency with timezone', () => {
const data = createMockData({
frequency: 'weekly',
visual_config: {
time: '9:00 AM',
weekdays: ['mon', 'wed', 'fri'],
},
timezone: 'Asia/Tokyo',
})
const result = getNextExecutionTimes(data, 3)
expect(result.length).toBeGreaterThan(0)
result.forEach((date) => {
expect([1, 3, 5]).toContain(date.getDay()) // Mon, Wed, Fri
expect(date.getHours()).toBe(9)
expect(date.getMinutes()).toBe(0)
})
})
it('monthly frequency with timezone', () => {
const data = createMockData({
frequency: 'monthly',
visual_config: {
time: '11:30 AM',
monthly_days: [1, 15, 'last'],
},
timezone: 'America/Los_Angeles',
})
const result = getNextExecutionTimes(data, 3)
expect(result.length).toBeGreaterThan(0)
result.forEach((date) => {
expect(date.getHours()).toBe(11)
expect(date.getMinutes()).toBe(30)
// Should be on specified days (1st, 15th, or last day of month)
const day = date.getDate()
const lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate()
expect(day === 1 || day === 15 || day === lastDay).toBe(true)
})
})
it('hourly frequency with timezone', () => {
const data = createMockData({
frequency: 'hourly',
visual_config: { on_minute: 45 },
timezone: 'Europe/Berlin',
})
const result = getNextExecutionTimes(data, 3)
expect(result.length).toBeGreaterThan(0)
result.forEach((date) => {
expect(date.getMinutes()).toBe(45)
expect(date.getSeconds()).toBe(0)
})
})
})
})