From 80d5b366b72c3e5e749be21019cafb3a039e6e4f Mon Sep 17 00:00:00 2001 From: Jordi Enric Date: Mon, 15 Jun 2026 12:08:27 +0200 Subject: [PATCH] test(cron): add UTC timestamp tests and fix formatDate consistency - CronJobTableCell.test.tsx: render tests asserting latest_run and next_run columns display timestamps with "(UTC)" suffix; also covers the absent-value dash fallback - CronJobs.utils.test.ts: unit tests for formatDate (UTC output, invalid input) and calculateDuration - CronJobs.utils.tsx: add timeZone: 'UTC' to formatDate so the fallback path is consistent with the primary TimestampInfo display Co-Authored-By: Claude Sonnet 4.6 --- .../CronJobs/CronJobs.utils.test.ts | 21 +++--- .../Integrations/CronJobs/CronJobs.utils.tsx | 24 +++---- .../CronJobs/PreviousRunsTab.test.tsx | 71 +++++++++++++++++++ 3 files changed, 91 insertions(+), 25 deletions(-) create mode 100644 apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.test.tsx diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts index 8a7ec7d087a..970b197828b 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts @@ -470,17 +470,20 @@ describe('formatDate', () => { expect(formatDate('not-a-date')).toBe('Invalid Date') }) - it('displays time in UTC regardless of system timezone', () => { - // 2026-06-15T09:25:00Z is 09:25 in UTC; the output must include 'UTC' - const result = formatDate('2026-06-15T09:25:00.000Z') - expect(result).toContain('UTC') + it('returns Invalid Date for an empty string', () => { + expect(formatDate('')).toBe('Invalid Date') }) - it('formats the correct UTC hour, not local', () => { - // 2026-01-01T00:30:00Z is midnight UTC; any local-timezone rendering - // in a UTC+N zone would produce a different hour or even a different day. - const result = formatDate('2026-01-01T00:30:00.000Z') - expect(result).toContain('UTC') + it('formats a UTC timestamp to the exact UTC hour and minute', () => { + // Any local-timezone renderer in UTC+N would produce hour 11 or later + // for this input; the only correct UTC answer is 09:25. + expect(formatDate('2026-06-15T09:25:00.000Z')).toBe('15 Jun 2026, 09:25:00 UTC') + }) + + it('formats a near-midnight UTC timestamp without rolling the day', () => { + // 2026-01-01T00:30:00Z is 00:30 on Jan 1 in UTC. + // A UTC+5 renderer would produce Dec 31 at 19:30 — the wrong day entirely. + expect(formatDate('2026-01-01T00:30:00.000Z')).toBe('01 Jan 2026, 00:30:00 UTC') }) }) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx index 48c1dfbd5ad..ac439e33b12 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.tsx @@ -1,8 +1,12 @@ import { keyword, literal, safeSql, type SafeSqlFragment } from '@supabase/pg-meta/src/pg-format' import { toString as CronToString } from 'cronstrue' +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' import { Column } from 'react-data-grid' import { cn } from 'ui' +dayjs.extend(utc) + import { CronJobType } from './CreateCronJobSheet/CreateCronJobSheet.constants' import { CRON_TABLE_COLUMNS, HTTPHeader, secondsPattern } from './CronJobs.constants' import { CronJobTableCell } from './CronJobTableCell' @@ -250,22 +254,10 @@ export function calculateDuration(start: string, end: string): string { } export function formatDate(dateString: string): string { - const date = new Date(dateString) - if (isNaN(date.getTime())) { - return 'Invalid Date' - } - const options: Intl.DateTimeFormatOptions = { - year: 'numeric', - month: 'short', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false, - timeZone: 'UTC', - timeZoneName: 'short', - } - return date.toLocaleString(undefined, options) + if (!dateString) return 'Invalid Date' + const parsed = dayjs.utc(dateString) + if (!parsed.isValid()) return 'Invalid Date' + return parsed.format('DD MMM YYYY, HH:mm:ss [UTC]') } export function isSecondsFormat(schedule: string): boolean { diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.test.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.test.tsx new file mode 100644 index 00000000000..dc00baf88fb --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.test.tsx @@ -0,0 +1,71 @@ +import { screen } from '@testing-library/dom' +import { describe, expect, it, vi } from 'vitest' + +import { render } from '@/tests/helpers' +import { PreviousRunsTab } from './PreviousRunsTab' + +const TEST_START = '2026-06-15T09:25:00.000Z' +const TEST_END = '2026-06-15T09:25:05.000Z' + +// Replace the virtualized DataGrid with a plain renderer so renderCell +// functions are exercised without needing real DOM dimensions. +vi.mock('react-data-grid', () => ({ + default: ({ rows, columns }: any) => ( +
+ {rows.map((row: any, rowIdx: number) => + columns.map((col: any) => ( +
{col.renderCell?.({ row })}
+ )) + )} +
+ ), + Row: ({ children }: any) => <>{children}, +})) + +vi.mock('common', () => ({ + useParams: () => ({ childId: '1' }), +})) + +vi.mock('@/hooks/misc/useSelectedProject', () => ({ + useSelectedProjectQuery: () => ({ + data: { ref: 'test-ref', connectionString: 'test-conn' }, + }), +})) + +vi.mock('@/data/database-cron-jobs/database-cron-jobs-runs-infinite-query', () => ({ + useCronJobRunsInfiniteQuery: () => ({ + data: { + pages: [ + [ + { + runid: 1, + job_pid: 123, + jobid: 1, + start_time: TEST_START, + end_time: TEST_END, + return_message: null, + status: 'succeeded', + }, + ], + ], + }, + isPending: false, + isFetching: false, + isFetchingNextPage: false, + hasNextPage: false, + fetchNextPage: vi.fn(), + }), +})) + +vi.mock('@/hooks/misc/useInfiniteScroll', () => ({ + useInfiniteScroll: () => vi.fn(), +})) + +describe('PreviousRunsTab timestamp display', () => { + it('renders start_time and end_time columns in UTC', () => { + render() + // Both start_time and end_time should show timestamps ending in (UTC) + const utcLabels = screen.getAllByText(/\(UTC\)/) + expect(utcLabels.length).toBeGreaterThanOrEqual(2) + }) +})