11import { describe , it , expect , vi , beforeEach , afterEach } from "vitest" ;
22import type { TFunction } from "i18next" ;
33import {
4- formatDate ,
4+ formatDateShort ,
5+ formatDateLong ,
56 getInitials ,
67 toTitleCase ,
78 formatJournalistName ,
@@ -22,7 +23,7 @@ describe("utils", () => {
2223 vi . useRealTimers ( ) ;
2324 } ) ;
2425
25- describe ( "formatDate " , ( ) => {
26+ describe ( "formatDateShort " , ( ) => {
2627 const mockT = vi . fn ( ( key : string ) => {
2728 switch ( key ) {
2829 case "yesterday" :
@@ -39,19 +40,19 @@ describe("utils", () => {
3940 describe ( "today formatting" , ( ) => {
4041 it ( "should format today's date as time only" , ( ) => {
4142 const todayDate = "2024-01-15T14:30:00Z" ;
42- const result = formatDate ( todayDate , "en-US" , mockT ) ;
43+ const result = formatDateShort ( todayDate , "en-US" , mockT ) ;
4344 expect ( result ) . toMatch ( / \d { 1 , 2 } : \d { 2 } .* [ A P ] M / ) ; // Matches time format like "2:30 PM"
4445 } ) ;
4546
4647 it ( "should format today's date with different locale" , ( ) => {
4748 const todayDate = "2024-01-15T09:15:00Z" ;
48- const result = formatDate ( todayDate , "fr-FR" , mockT ) ;
49+ const result = formatDateShort ( todayDate , "fr-FR" , mockT ) ;
4950 expect ( result ) . toMatch ( / \d { 1 , 2 } : \d { 2 } / ) ; // French format without AM/PM
5051 } ) ;
5152
5253 it ( "should handle today's date at midnight" , ( ) => {
5354 const midnightDate = "2024-01-15T00:00:00Z" ;
54- const result = formatDate ( midnightDate , "en-US" , mockT ) ;
55+ const result = formatDateShort ( midnightDate , "en-US" , mockT ) ;
5556 // Midnight UTC might be displayed as yesterday in local time
5657 expect ( result ) . toMatch ( / 1 2 : 0 0 .* A M | Y e s t e r d a y / ) ;
5758 } ) ;
@@ -60,72 +61,76 @@ describe("utils", () => {
6061 describe ( "yesterday formatting" , ( ) => {
6162 it ( "should format yesterday's date as 'Yesterday'" , ( ) => {
6263 const yesterdayDate = "2024-01-14T15:30:00Z" ;
63- const result = formatDate ( yesterdayDate , "en-US" , mockT ) ;
64+ const result = formatDateShort ( yesterdayDate , "en-US" , mockT ) ;
6465 expect ( result ) . toBe ( "Yesterday" ) ;
6566 expect ( mockT ) . toHaveBeenCalledWith ( "yesterday" ) ;
6667 } ) ;
6768
6869 it ( "should handle yesterday at different times" , ( ) => {
6970 const yesterdayMorning = "2024-01-14T08:00:00Z" ;
70- const result = formatDate ( yesterdayMorning , "en-US" , mockT ) ;
71+ const result = formatDateShort ( yesterdayMorning , "en-US" , mockT ) ;
7172 expect ( result ) . toBe ( "Yesterday" ) ;
7273 } ) ;
7374 } ) ;
7475
7576 describe ( "this year formatting" , ( ) => {
7677 it ( "should format dates in current year as month and day" , ( ) => {
7778 const currentYearDate = "2024-03-10T10:00:00Z" ;
78- const result = formatDate ( currentYearDate , "en-US" , mockT ) ;
79+ const result = formatDateShort ( currentYearDate , "en-US" , mockT ) ;
7980 expect ( result ) . toBe ( "Mar 10" ) ;
8081 } ) ;
8182
8283 it ( "should format dates with different locale" , ( ) => {
8384 const currentYearDate = "2024-12-25T10:00:00Z" ;
84- const result = formatDate ( currentYearDate , "fr-FR" , mockT ) ;
85+ const result = formatDateShort ( currentYearDate , "fr-FR" , mockT ) ;
8586 expect ( result ) . toMatch ( / d é c \. ? \s * 2 5 | 2 5 \s * d é c / i) ; // French format
8687 } ) ;
8788
8889 it ( "should handle single digit days" , ( ) => {
8990 const singleDigitDay = "2024-05-05T10:00:00Z" ;
90- const result = formatDate ( singleDigitDay , "en-US" , mockT ) ;
91+ const result = formatDateShort ( singleDigitDay , "en-US" , mockT ) ;
9192 expect ( result ) . toBe ( "May 5" ) ;
9293 } ) ;
9394 } ) ;
9495
9596 describe ( "previous years formatting" , ( ) => {
9697 it ( "should format dates from previous years with year" , ( ) => {
9798 const previousYearDate = "2023-06-15T10:00:00Z" ;
98- const result = formatDate ( previousYearDate , "en-US" , mockT ) ;
99+ const result = formatDateShort ( previousYearDate , "en-US" , mockT ) ;
99100 expect ( result ) . toBe ( "Jun 15, 2023" ) ;
100101 } ) ;
101102
102103 it ( "should format very old dates" , ( ) => {
103104 const oldDate = "2020-01-01T10:00:00Z" ;
104- const result = formatDate ( oldDate , "en-US" , mockT ) ;
105+ const result = formatDateShort ( oldDate , "en-US" , mockT ) ;
105106 expect ( result ) . toBe ( "Jan 1, 2020" ) ;
106107 } ) ;
107108
108109 it ( "should handle different locale for previous years" , ( ) => {
109110 const previousYearDate = "2022-11-30T10:00:00Z" ;
110- const result = formatDate ( previousYearDate , "de-DE" , mockT ) ;
111+ const result = formatDateShort ( previousYearDate , "de-DE" , mockT ) ;
111112 // German format: "30. Nov. 2022"
112113 expect ( result ) . toMatch ( / 3 0 \. ? \s * N o v \. ? \s * 2 0 2 2 / i) ;
113114 } ) ;
114115 } ) ;
115116
116117 describe ( "locale normalization" , ( ) => {
117118 it ( "should handle POSIX locale format" , ( ) => {
118- const result = formatDate ( "2024-03-10T10:00:00Z" , "en_US.UTF-8" , mockT ) ;
119+ const result = formatDateShort (
120+ "2024-03-10T10:00:00Z" ,
121+ "en_US.UTF-8" ,
122+ mockT ,
123+ ) ;
119124 expect ( result ) . toBe ( "Mar 10" ) ;
120125 } ) ;
121126
122127 it ( "should handle locale with underscore" , ( ) => {
123- const result = formatDate ( "2024-03-10T10:00:00Z" , "fr_FR" , mockT ) ;
128+ const result = formatDateShort ( "2024-03-10T10:00:00Z" , "fr_FR" , mockT ) ;
124129 expect ( result ) . toMatch ( / m a r s ? \s * 1 0 | 1 0 \s * m a r s ? / i) ;
125130 } ) ;
126131
127132 it ( "should fallback to language code for invalid locale" , ( ) => {
128- const result = formatDate (
133+ const result = formatDateShort (
129134 "2024-03-10T10:00:00Z" ,
130135 "invalid_LOCALE" ,
131136 mockT ,
@@ -134,21 +139,21 @@ describe("utils", () => {
134139 } ) ;
135140
136141 it ( "should fallback to 'en' for completely invalid locale" , ( ) => {
137- const result = formatDate ( "2024-03-10T10:00:00Z" , "xxx" , mockT ) ;
142+ const result = formatDateShort ( "2024-03-10T10:00:00Z" , "xxx" , mockT ) ;
138143 expect ( result ) . toBeTruthy ( ) ; // Should fallback to English
139144 } ) ;
140145
141146 it ( "should use browser default when locale is empty" , ( ) => {
142- const result = formatDate ( "2024-03-10T10:00:00Z" , "" , mockT ) ;
147+ const result = formatDateShort ( "2024-03-10T10:00:00Z" , "" , mockT ) ;
143148 expect ( result ) . toBeTruthy ( ) ;
144149 } ) ;
145150
146151 it ( "should handle null/undefined locale" , ( ) => {
147152 // @ts -expect-error - Testing null locale
148- const resultNull = formatDate ( "2024-03-10T10:00:00Z" , null , mockT ) ;
153+ const resultNull = formatDateShort ( "2024-03-10T10:00:00Z" , null , mockT ) ;
149154 expect ( resultNull ) . toBeTruthy ( ) ;
150155
151- const resultUndefined = formatDate (
156+ const resultUndefined = formatDateShort (
152157 "2024-03-10T10:00:00Z" ,
153158 // @ts -expect-error - Testing undefined locale
154159 undefined ,
@@ -160,38 +165,148 @@ describe("utils", () => {
160165
161166 describe ( "edge cases and error handling" , ( ) => {
162167 it ( "should handle invalid date strings gracefully" , ( ) => {
163- const result = formatDate ( "invalid-date" , "en-US" , mockT ) ;
168+ const result = formatDateShort ( "invalid-date" , "en-US" , mockT ) ;
164169 expect ( result ) . toMatch ( / I n v a l i d D a t e | N a N / i) ;
165170 } ) ;
166171
167172 it ( "should handle empty date string" , ( ) => {
168- const result = formatDate ( "" , "en-US" , mockT ) ;
173+ const result = formatDateShort ( "" , "en-US" , mockT ) ;
169174 expect ( result ) . toMatch ( / I n v a l i d D a t e | N a N / i) ;
170175 } ) ;
171176
172177 it ( "should handle dates far in the future" , ( ) => {
173178 const futureDate = "2030-12-31T23:59:59Z" ;
174- const result = formatDate ( futureDate , "en-US" , mockT ) ;
179+ const result = formatDateShort ( futureDate , "en-US" , mockT ) ;
175180 expect ( result ) . toBe ( "Dec 31, 2030" ) ;
176181 } ) ;
177182
178183 it ( "should handle leap year dates" , ( ) => {
179184 const leapYearDate = "2024-02-29T10:00:00Z" ;
180- const result = formatDate ( leapYearDate , "en-US" , mockT ) ;
185+ const result = formatDateShort ( leapYearDate , "en-US" , mockT ) ;
181186 expect ( result ) . toBe ( "Feb 29" ) ;
182187 } ) ;
183188
184189 it ( "should handle different timezone inputs" , ( ) => {
185190 const utcDate = "2024-03-10T10:00:00Z" ;
186191 const offsetDate = "2024-03-10T10:00:00+05:00" ;
187192
188- const resultUTC = formatDate ( utcDate , "en-US" , mockT ) ;
189- const resultOffset = formatDate ( offsetDate , "en-US" , mockT ) ;
193+ const resultUTC = formatDateShort ( utcDate , "en-US" , mockT ) ;
194+ const resultOffset = formatDateShort ( offsetDate , "en-US" , mockT ) ;
190195
191196 expect ( resultUTC ) . toBeTruthy ( ) ;
192197 expect ( resultOffset ) . toBeTruthy ( ) ;
193198 } ) ;
194199 } ) ;
200+
201+ describe ( "UTC assumption" , ( ) => {
202+ it ( "should treat timestamp without timezone as UTC" , ( ) => {
203+ const timestampWithoutTZ = "2024-08-29T21:13:10.760877" ;
204+ const timestampWithTZ = "2024-08-29T21:13:10.760877Z" ;
205+
206+ const resultWithoutTZ = formatDateShort (
207+ timestampWithoutTZ ,
208+ "en-US" ,
209+ mockT ,
210+ ) ;
211+ const resultWithTZ = formatDateShort ( timestampWithTZ , "en-US" , mockT ) ;
212+
213+ // Both should produce the same result since we treat no-TZ as UTC
214+ expect ( resultWithoutTZ ) . toBe ( resultWithTZ ) ;
215+ } ) ;
216+ } ) ;
217+ } ) ;
218+
219+ describe ( "formatDateLong" , ( ) => {
220+ it ( "should format date with full timestamp including time and timezone" , ( ) => {
221+ const dateString = "2024-03-10T14:30:45Z" ;
222+ const result = formatDateLong ( dateString , "en-US" ) ;
223+
224+ // Should include year, month, day, time, and timezone
225+ expect ( result ) . toMatch ( / M a r c h .* 1 0 .* 2 0 2 4 / ) ; // Date parts with full month name
226+ expect ( result ) . toMatch ( / \d { 1 , 2 } : \d { 2 } (? ! \d ) / ) ; // Time without seconds or leading zeros on hours
227+ expect ( result ) . toMatch ( / [ A P ] M / ) ; // AM/PM
228+ expect ( result ) . toMatch ( / [ A - Z ] { 3 , 4 } / ) ; // Timezone abbreviation
229+ } ) ;
230+
231+ it ( "should format date with different locale" , ( ) => {
232+ const dateString = "2024-12-25T09:15:30Z" ;
233+ const result = formatDateLong ( dateString , "fr-FR" ) ;
234+
235+ // French format should have full month name
236+ expect ( result ) . toMatch ( / d é c e m b r e | D e c e m b e r / i) ; // French December (full name)
237+ expect ( result ) . toMatch ( / 2 5 / ) ; // Day
238+ expect ( result ) . toMatch ( / 2 0 2 4 / ) ; // Year
239+ expect ( result ) . toMatch ( / \d { 1 , 2 } : \d { 2 } (? ! \d ) / ) ; // Time without seconds
240+ } ) ;
241+
242+ it ( "should handle POSIX locale format" , ( ) => {
243+ const dateString = "2024-06-15T18:45:12Z" ;
244+ const result = formatDateLong ( dateString , "en_US.UTF-8" ) ;
245+
246+ expect ( result ) . toMatch ( / J u n e .* 1 5 .* 2 0 2 4 / ) ;
247+ expect ( result ) . toMatch ( / \d { 1 , 2 } : \d { 2 } (? ! \d ) / ) ; // Time without seconds
248+ } ) ;
249+
250+ it ( "should fallback gracefully for invalid locale" , ( ) => {
251+ const dateString = "2024-01-01T00:00:00Z" ;
252+ const result = formatDateLong ( dateString , "invalid_locale" ) ;
253+
254+ // Should still produce a valid date string (may show different date due to timezone conversion)
255+ expect ( result ) . toMatch ( / J a n u a r y .* 1 .* 2 0 2 4 | D e c e m b e r .* 3 1 .* 2 0 2 3 / ) ; // Could be either due to timezone (full month names)
256+ expect ( result ) . toMatch ( / \d { 1 , 2 } : \d { 2 } (? ! \d ) / ) ; // Time without seconds
257+ } ) ;
258+
259+ it ( "should handle different timezone inputs" , ( ) => {
260+ const utcDate = "2024-05-20T10:30:15Z" ;
261+ const offsetDate = "2024-05-20T10:30:15+02:00" ;
262+
263+ const resultUTC = formatDateLong ( utcDate , "en-US" ) ;
264+ const resultOffset = formatDateLong ( offsetDate , "en-US" ) ;
265+
266+ // Both should be valid timestamps (times will be converted to local timezone)
267+ expect ( resultUTC ) . toMatch ( / M a y .* 2 0 .* 2 0 2 4 / ) ;
268+ expect ( resultUTC ) . toMatch ( / \d { 1 , 2 } : \d { 2 } (? ! \d ) / ) ;
269+ expect ( resultOffset ) . toMatch ( / M a y .* 2 0 .* 2 0 2 4 / ) ;
270+ expect ( resultOffset ) . toMatch ( / \d { 1 , 2 } : \d { 2 } (? ! \d ) / ) ;
271+ } ) ;
272+
273+ it ( "should handle edge case dates" , ( ) => {
274+ // New Year's Day (may show as Dec 31 in local timezone)
275+ const newYear = "2024-01-01T00:00:00Z" ;
276+ const result1 = formatDateLong ( newYear , "en-US" ) ;
277+ expect ( result1 ) . toMatch ( / J a n u a r y .* 1 .* 2 0 2 4 | D e c e m b e r .* 3 1 .* 2 0 2 3 / ) ; // Could be either due to timezone (full month names)
278+ expect ( result1 ) . toMatch ( / \d { 1 , 2 } : \d { 2 } (? ! \d ) / ) ;
279+
280+ // Year end
281+ const yearEnd = "2024-12-31T23:59:59Z" ;
282+ const result2 = formatDateLong ( yearEnd , "en-US" ) ;
283+ expect ( result2 ) . toMatch ( / D e c e m b e r .* 3 1 .* 2 0 2 4 / ) ;
284+ expect ( result2 ) . toMatch ( / \d { 1 , 2 } : \d { 2 } (? ! \d ) / ) ;
285+ } ) ;
286+
287+ it ( "should handle invalid date strings" , ( ) => {
288+ const invalidDate = "invalid-date-string" ;
289+ const result = formatDateLong ( invalidDate , "en-US" ) ;
290+
291+ // Should contain "Invalid Date" or similar
292+ expect ( result ) . toMatch ( / I n v a l i d D a t e / i) ;
293+ } ) ;
294+
295+ it ( "should handle empty date string" , ( ) => {
296+ const result = formatDateLong ( "" , "en-US" ) ;
297+ expect ( result ) . toMatch ( / I n v a l i d D a t e / i) ;
298+ } ) ;
299+
300+ it ( "should treat timestamp without timezone as UTC" , ( ) => {
301+ const timestampWithoutTZ = "2024-08-29T21:13:10.760877" ;
302+ const timestampWithTZ = "2024-08-29T21:13:10.760877Z" ;
303+
304+ const resultWithoutTZ = formatDateLong ( timestampWithoutTZ , "en-US" ) ;
305+ const resultWithTZ = formatDateLong ( timestampWithTZ , "en-US" ) ;
306+
307+ // Both should produce the same result since we treat no-TZ as UTC
308+ expect ( resultWithoutTZ ) . toBe ( resultWithTZ ) ;
309+ } ) ;
195310 } ) ;
196311
197312 describe ( "getInitials" , ( ) => {
0 commit comments