@@ -17,49 +17,37 @@ describe('dateValueToLocalParts / localPartsToDateValue', () => {
1717 expect ( localPartsToDateValue ( '2026-07-06' , null ) ) . toBe ( '2026-07-06' )
1818 } )
1919
20- it ( 'splits instants into local day/time and round-trips exactly' , ( ) => {
21- const stored = new Date ( 2026 , 6 , 6 , 16 , 4 , 55 ) . toISOString ( )
22- const parts = dateValueToLocalParts ( stored )
23- expect ( parts ) . toEqual ( { day : '2026-07-06' , time : '16:04:55' } )
24- expect ( localPartsToDateValue ( parts . day as string , parts . time ) ) . toBe ( stored )
25- } )
26-
27- it ( 'keeps the time when only the day changes' , ( ) => {
28- const stored = new Date ( 2026 , 6 , 6 , 16 , 4 , 55 ) . toISOString ( )
29- const parts = dateValueToLocalParts ( stored )
30- expect ( localPartsToDateValue ( '2026-07-09' , parts . time ) ) . toBe (
31- new Date ( 2026 , 6 , 9 , 16 , 4 , 55 ) . toISOString ( )
20+ it ( 'splits instants into their literal wall day/time — no zone conversion' , ( ) => {
21+ expect ( dateValueToLocalParts ( '2026-07-06T16:04:55-07:00' ) ) . toEqual ( {
22+ day : '2026-07-06' ,
23+ time : '16:04:55' ,
24+ } )
25+ expect ( dateValueToLocalParts ( '2026-07-06T23:04:55Z' ) ) . toEqual ( {
26+ day : '2026-07-06' ,
27+ time : '23:04:55' ,
28+ } )
29+ expect ( dateValueToLocalParts ( '2026-07-06T23:04:55.000Z' ) ) . toEqual ( {
30+ day : '2026-07-06' ,
31+ time : '23:04:55' ,
32+ } )
33+ } )
34+
35+ it ( 'recombines parts stamping the given zone offset, keeping the wall time' , ( ) => {
36+ expect ( localPartsToDateValue ( '2026-07-06' , '16:04:55' , 'America/New_York' ) ) . toBe (
37+ '2026-07-06T16:04:55-04:00'
3238 )
33- } )
34-
35- it ( 'accepts HH:mm times, defaulting seconds to zero' , ( ) => {
36- expect ( localPartsToDateValue ( '2026-07-06' , '16:04' ) ) . toBe (
37- new Date ( 2026 , 6 , 6 , 16 , 4 , 0 ) . toISOString ( )
39+ expect ( localPartsToDateValue ( '2026-07-09' , '16:04:55' , 'America/New_York' ) ) . toBe (
40+ '2026-07-09T16:04:55-04:00'
41+ )
42+ expect ( localPartsToDateValue ( '2026-07-06' , '16:04' , 'America/New_York' ) ) . toBe (
43+ ' 2026-07-06T16:04:00-04:00'
3844 )
3945 } )
4046
4147 it ( 'returns null parts for unparseable values' , ( ) => {
4248 expect ( dateValueToLocalParts ( 'garbage' ) ) . toEqual ( { day : null , time : null } )
4349 expect ( dateValueToLocalParts ( '' ) ) . toEqual ( { day : null , time : null } )
4450 } )
45-
46- it ( 'reads and recombines parts in an explicit IANA zone' , ( ) => {
47- const stored = '2026-07-06T20:04:55.000Z'
48- const parts = dateValueToLocalParts ( stored , 'America/New_York' )
49- expect ( parts ) . toEqual ( { day : '2026-07-06' , time : '16:04:55' } )
50- expect ( localPartsToDateValue ( parts . day as string , parts . time , 'America/New_York' ) ) . toBe ( stored )
51- } )
52- } )
53-
54- describe ( 'timezone-aware display round-trip' , ( ) => {
55- it ( 'parses and renders wall times in the effective zone, not the runtime zone' , ( ) => {
56- const zone = 'America/New_York'
57- const stored = displayToStorage ( '07/06/2026 4:04:55 PM' , zone )
58- expect ( stored ) . toBe ( '2026-07-06T20:04:55.000Z' )
59- expect ( storageToDisplay ( stored as string , { seconds : true , timeZone : zone } ) ) . toBe (
60- '07/06/2026 4:04:55 PM'
61- )
62- } )
6351} )
6452
6553describe ( 'displayToStorage' , ( ) => {
@@ -70,22 +58,25 @@ describe('displayToStorage', () => {
7058 expect ( displayToStorage ( '7/6' ) ) . toBe ( `${ new Date ( ) . getFullYear ( ) } -07-06` )
7159 } )
7260
73- it ( 'parses M/D/YYYY with a time to a local-zone UTC instant' , ( ) => {
74- expect ( displayToStorage ( '07/06/2026 4:04 PM' ) ) . toBe (
75- new Date ( 2026 , 6 , 6 , 16 , 4 , 0 ) . toISOString ( )
61+ it ( 'parses M/D/YYYY with a time to a wall time stamped with the given zone' , ( ) => {
62+ expect ( displayToStorage ( '07/06/2026 4:04 PM' , 'America/New_York' ) ) . toBe (
63+ '2026-07-06T16:04:00-04:00'
64+ )
65+ expect ( displayToStorage ( '07/06/2026 4:04:55 PM' , 'America/New_York' ) ) . toBe (
66+ '2026-07-06T16:04:55-04:00'
7667 )
77- expect ( displayToStorage ( '07/06/2026 4 :04:55 PM ' ) ) . toBe (
78- new Date ( 2026 , 6 , 6 , 16 , 4 , 55 ) . toISOString ( )
68+ expect ( displayToStorage ( '07/06/2026 16 :04' , 'America/New_York ') ) . toBe (
69+ ' 2026-07-06T16:04:00-04:00'
7970 )
80- expect ( displayToStorage ( '07/06/2026 16:04' ) ) . toBe ( new Date ( 2026 , 6 , 6 , 16 , 4 , 0 ) . toISOString ( ) )
81- expect ( displayToStorage ( '07/06/2026 12:00 AM' ) ) . toBe (
82- new Date ( 2026 , 6 , 6 , 0 , 0 , 0 ) . toISOString ( )
71+ expect ( displayToStorage ( '07/06/2026 12:00 AM' , 'America/New_York' ) ) . toBe (
72+ '2026-07-06T00:00:00-04:00'
8373 )
8474 } )
8575
86- it ( 'passes canonical instants and offset strings through Date.parse exactly' , ( ) => {
87- expect ( displayToStorage ( '2026-07-06T23:04:55.000Z' ) ) . toBe ( '2026-07-06T23:04:55.000Z' )
88- expect ( displayToStorage ( '2026-07-06 16:04:55 PDT' ) ) . toBe ( '2026-07-06T23:04:55.000Z' )
76+ it ( 'preserves the wall time and offset of canonical and offset strings' , ( ) => {
77+ expect ( displayToStorage ( '2026-07-06T16:04:55-07:00' ) ) . toBe ( '2026-07-06T16:04:55-07:00' )
78+ expect ( displayToStorage ( '2026-07-06T23:04:55.000Z' ) ) . toBe ( '2026-07-06T23:04:55Z' )
79+ expect ( displayToStorage ( '2026-07-06 16:04:55 PDT' ) ) . toBe ( '2026-07-06T16:04:55-07:00' )
8980 } )
9081
9182 it ( 'rejects invalid dates and times' , ( ) => {
@@ -102,18 +93,28 @@ describe('storageToDisplay', () => {
10293 expect ( storageToDisplay ( '2026-07-06' ) ) . toBe ( '07/06/2026' )
10394 } )
10495
105- it ( 'round-trips an instant through the editor draft format' , ( ) => {
106- const stored = new Date ( 2026 , 6 , 6 , 16 , 4 , 55 ) . toISOString ( )
96+ it ( 'renders the literal wall time identically regardless of viewer or offset' , ( ) => {
97+ expect ( storageToDisplay ( '2026-07-06T16:04:55-07:00' , { seconds : true } ) ) . toBe (
98+ '07/06/2026 4:04:55 PM'
99+ )
100+ expect ( storageToDisplay ( '2026-07-06T16:04:55+09:00' , { seconds : true } ) ) . toBe (
101+ '07/06/2026 4:04:55 PM'
102+ )
103+ } )
104+
105+ it ( 'round-trips an instant through the editor draft format without shifting' , ( ) => {
106+ const stored = displayToStorage ( '07/06/2026 4:04:55 PM' , 'America/New_York' ) as string
107107 const draft = storageToDisplay ( stored , { seconds : true } )
108- expect ( displayToStorage ( draft ) ) . toBe ( stored )
108+ expect ( draft ) . toBe ( '07/06/2026 4:04:55 PM' )
109+ expect ( displayToStorage ( draft , 'America/New_York' ) ) . toBe ( stored )
109110 } )
110111} )
111112
112113describe ( 'cleanCellValue' , ( ) => {
113114 it ( 'normalizes date cells to canonical storage' , ( ) => {
114115 const column = { name : 'due' , type : 'date' } as const
115116 expect ( cleanCellValue ( '07/06/2026' , column ) ) . toBe ( '2026-07-06' )
116- expect ( cleanCellValue ( '2026-07-06T23 :04:55.000Z ' , column ) ) . toBe ( '2026-07-06T23 :04:55.000Z ' )
117+ expect ( cleanCellValue ( '2026-07-06T16 :04:55-07:00 ' , column ) ) . toBe ( '2026-07-06T16 :04:55-07:00 ' )
117118 expect ( cleanCellValue ( 'nope' , column ) ) . toBeNull ( )
118119 expect ( cleanCellValue ( '' , column ) ) . toBeNull ( )
119120 } )
@@ -127,7 +128,9 @@ describe('cleanCellValue', () => {
127128describe ( 'formatValueForInput' , ( ) => {
128129 it ( 'gives editors the canonical value, surfacing legacy UTC midnights as calendar days' , ( ) => {
129130 expect ( formatValueForInput ( '2026-07-06T00:00:00.000Z' , 'date' ) ) . toBe ( '2026-07-06' )
130- expect ( formatValueForInput ( '2026-07-06T23:04:55.000Z' , 'date' ) ) . toBe ( '2026-07-06T23:04:55.000Z' )
131+ expect ( formatValueForInput ( '2026-07-06T16:04:55-07:00' , 'date' ) ) . toBe (
132+ '2026-07-06T16:04:55-07:00'
133+ )
131134 expect ( formatValueForInput ( '2026-07-06' , 'date' ) ) . toBe ( '2026-07-06' )
132135 } )
133136} )
0 commit comments