Skip to content

Commit 345204a

Browse files
committed
refactor(frontend): extract date helpers and add unit tests
Move formatDate out of PayrollScheduler into a shared utils/dateHelpers module. Add getRemainingDays helper that computes the day-level difference between now and a target date. Include 12 vitest cases covering empty input, ISO strings, date-only strings, invalid dates, past/future dates, and day-level precision.
1 parent 4e8d293 commit 345204a

3 files changed

Lines changed: 102 additions & 19 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { formatDate, getRemainingDays } from '../../utils/dateHelpers';
3+
4+
describe('formatDate', () => {
5+
it('returns N/A for empty string', () => {
6+
expect(formatDate('')).toBe('N/A');
7+
});
8+
9+
it('formats a date-only string (YYYY-MM-DD) correctly', () => {
10+
const result = formatDate('2024-01-15');
11+
expect(result).toBe('Jan 15, 2024');
12+
});
13+
14+
it('formats an ISO datetime string correctly', () => {
15+
const result = formatDate('2024-06-01T10:00:00Z');
16+
expect(result).toContain('2024');
17+
expect(result).toContain('Jun');
18+
});
19+
20+
it('returns the original string for an invalid date', () => {
21+
expect(formatDate('not-a-date')).toBe('not-a-date');
22+
});
23+
24+
it('handles single-digit month and day', () => {
25+
const result = formatDate('2024-03-05');
26+
expect(result).toBe('Mar 5, 2024');
27+
});
28+
29+
it('handles Dec 31 edge case', () => {
30+
const result = formatDate('2024-12-31');
31+
expect(result).toBe('Dec 31, 2024');
32+
});
33+
});
34+
35+
describe('getRemainingDays', () => {
36+
it('returns 0 for an invalid date string', () => {
37+
expect(getRemainingDays('garbage')).toBe(0);
38+
});
39+
40+
it('returns 0 for today', () => {
41+
const today = new Date();
42+
expect(getRemainingDays(today)).toBe(0);
43+
});
44+
45+
it('returns a positive number for a future date', () => {
46+
const future = new Date();
47+
future.setDate(future.getDate() + 10);
48+
expect(getRemainingDays(future)).toBe(10);
49+
});
50+
51+
it('returns a negative number for a past date', () => {
52+
const past = new Date();
53+
past.setDate(past.getDate() - 5);
54+
expect(getRemainingDays(past)).toBe(-5);
55+
});
56+
57+
it('accepts a date string', () => {
58+
const future = new Date();
59+
future.setDate(future.getDate() + 3);
60+
const iso = future.toISOString();
61+
expect(getRemainingDays(iso)).toBe(3);
62+
});
63+
64+
it('uses day-level precision, not time-level', () => {
65+
const tomorrow = new Date();
66+
tomorrow.setDate(tomorrow.getDate() + 1);
67+
tomorrow.setHours(0, 0, 0, 0);
68+
expect(getRemainingDays(tomorrow)).toBe(1);
69+
});
70+
});

frontend/src/pages/PayrollScheduler.tsx

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { BulkPaymentStatusTracker } from '../components/BulkPaymentStatusTracker
1515
import { ContractErrorPanel } from '../components/ContractErrorPanel';
1616
import { parseContractError, type ContractErrorDetail } from '../utils/contractErrorParser';
1717
import { HelpLink } from '../components/HelpLink';
18+
import { formatDate } from '../utils/dateHelpers';
1819

1920
interface PayrollFormState {
2021
employeeName: string;
@@ -106,25 +107,6 @@ function computeNextRunDate(config: SchedulingConfig, from: Date = new Date()):
106107
return first;
107108
}
108109

109-
const formatDate = (dateString: string) => {
110-
if (!dateString) return 'N/A';
111-
112-
const dateOnlyMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateString);
113-
const date = dateOnlyMatch
114-
? new Date(
115-
Number.parseInt(dateOnlyMatch[1], 10),
116-
Number.parseInt(dateOnlyMatch[2], 10) - 1,
117-
Number.parseInt(dateOnlyMatch[3], 10)
118-
)
119-
: new Date(dateString);
120-
121-
if (isNaN(date.getTime())) return dateString;
122-
return date.toLocaleDateString('en-US', {
123-
month: 'short',
124-
day: 'numeric',
125-
year: 'numeric',
126-
});
127-
};
128110

129111
interface PendingClaim {
130112
id: string;

frontend/src/utils/dateHelpers.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export function formatDate(dateString: string): string {
2+
if (!dateString) return 'N/A';
3+
4+
const dateOnlyMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateString);
5+
const date = dateOnlyMatch
6+
? new Date(
7+
Number.parseInt(dateOnlyMatch[1], 10),
8+
Number.parseInt(dateOnlyMatch[2], 10) - 1,
9+
Number.parseInt(dateOnlyMatch[3], 10)
10+
)
11+
: new Date(dateString);
12+
13+
if (isNaN(date.getTime())) return dateString;
14+
return date.toLocaleDateString('en-US', {
15+
month: 'short',
16+
day: 'numeric',
17+
year: 'numeric',
18+
});
19+
}
20+
21+
export function getRemainingDays(targetDate: string | Date): number {
22+
const target = typeof targetDate === 'string' ? new Date(targetDate) : targetDate;
23+
if (isNaN(target.getTime())) return 0;
24+
25+
const now = new Date();
26+
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
27+
const startOfTarget = new Date(target.getFullYear(), target.getMonth(), target.getDate());
28+
29+
const diffMs = startOfTarget.getTime() - startOfToday.getTime();
30+
return Math.ceil(diffMs / (1000 * 60 * 60 * 24));
31+
}

0 commit comments

Comments
 (0)