@@ -11,9 +11,12 @@ import {
1111} from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/inline-editors'
1212import { cleanCellValue } from '@/app/workspace/[workspaceId]/tables/[tableId]/utils'
1313
14- const { mockUseTimezone } = vi.hoisted(() => ({ mockUseTimezone: vi.fn() }))
14+ const { mockToastError, mockUseTimezoneState } = vi.hoisted(() => ({
15+ mockToastError: vi.fn(),
16+ mockUseTimezoneState: vi.fn(),
17+ }))
1518
16- vi.mock('@/hooks/queries/general-settings', () => ({ useTimezone: mockUseTimezone }))
19+ vi.mock('@/hooks/queries/general-settings', () => ({ useTimezoneState: mockUseTimezoneState }))
1720vi.mock('@sim/emcn', () => {
1821 const passthrough = ({ children }: { children?: ReactNode }) => children ?? null
1922 return {
@@ -26,15 +29,24 @@ vi.mock('@sim/emcn', () => {
2629 Popover: passthrough,
2730 PopoverAnchor: () => null,
2831 PopoverContent: passthrough,
29- toast: { error: vi.fn() },
32+ toast: { error: mockToastError },
3033 }
3134})
3235const column = (type: ColumnDefinition['type']): ColumnDefinition => ({ name: 'expires_at', type })
3336
37+ function changeInput(input: HTMLInputElement, value: string) {
38+ const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
39+ setter?.call(input, value)
40+ input.dispatchEvent(new Event('input', { bubbles: true }))
41+ }
42+
3443describe('dateEditorRawValue', () => {
3544 beforeEach(() => {
3645 vi.clearAllMocks()
37- mockUseTimezone.mockReturnValue('America/Los_Angeles')
46+ mockUseTimezoneState.mockReturnValue({
47+ timezone: 'America/Los_Angeles',
48+ status: 'ready',
49+ })
3850 })
3951
4052 it('leaves TTL drafts for TTL coercion to resolve safely', () => {
@@ -73,7 +85,10 @@ describe('dateEditorRawValue', () => {
7385
7486 ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
7587 act(() => root.render(createElement(InlineEditor, props)))
76- mockUseTimezone.mockReturnValue('America/New_York')
88+ mockUseTimezoneState.mockReturnValue({
89+ timezone: 'America/New_York',
90+ status: 'ready',
91+ })
7792 act(() => root.render(createElement(InlineEditor, props)))
7893
7994 const input = container.querySelector('input')
@@ -86,4 +101,130 @@ describe('dateEditorRawValue', () => {
86101 act(() => root.unmount())
87102 container.remove()
88103 })
104+
105+ it('waits for the saved timezone before creating a TTL draft', () => {
106+ mockUseTimezoneState.mockReturnValue({
107+ timezone: 'Asia/Tokyo',
108+ status: 'loading',
109+ })
110+ const container = document.createElement('div')
111+ document.body.appendChild(container)
112+ const root = createRoot(container)
113+ const onSave = vi.fn()
114+ const props = {
115+ value: Date.parse('2026-06-15T13:00:30Z') / 1000,
116+ column: column('ttl'),
117+ onSave,
118+ onCancel: vi.fn(),
119+ }
120+
121+ act(() => root.render(createElement(InlineEditor, props)))
122+
123+ expect(container.querySelector('input')).toMatchObject({
124+ disabled: true,
125+ placeholder: 'Loading timezone...',
126+ })
127+
128+ mockUseTimezoneState.mockReturnValue({
129+ timezone: 'America/Los_Angeles',
130+ status: 'ready',
131+ })
132+ act(() => root.render(createElement(InlineEditor, props)))
133+
134+ const input = container.querySelector('input') as HTMLInputElement
135+ expect(input.disabled).toBe(false)
136+ act(() => changeInput(input, '09/01/2026 9:00 AM'))
137+ act(() => input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })))
138+
139+ expect(onSave).toHaveBeenCalledWith(Date.parse('2026-09-01T16:00:00Z') / 1000, 'enter')
140+ act(() => root.unmount())
141+ container.remove()
142+ })
143+
144+ it('rejects an impossible TTL draft without clearing the cell', () => {
145+ const container = document.createElement('div')
146+ document.body.appendChild(container)
147+ const root = createRoot(container)
148+ const onSave = vi.fn()
149+
150+ act(() =>
151+ root.render(
152+ createElement(InlineEditor, {
153+ value: Date.parse('2026-06-15T13:00:30Z') / 1000,
154+ column: column('ttl'),
155+ onSave,
156+ onCancel: vi.fn(),
157+ })
158+ )
159+ )
160+
161+ const input = container.querySelector('input') as HTMLInputElement
162+ act(() => changeInput(input, '02/30/2026 1:30 AM'))
163+ act(() => input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })))
164+
165+ expect(onSave).not.toHaveBeenCalled()
166+ expect(mockToastError).toHaveBeenCalledWith('Invalid expiration date')
167+ act(() => root.unmount())
168+ container.remove()
169+ })
170+
171+ it.each([
172+ { caseName: 'a historical sub-minute offset', timezone: 'Africa/Monrovia', value: 2670 },
173+ {
174+ caseName: 'the far-future representable boundary',
175+ timezone: 'Asia/Tokyo',
176+ value: 253_402_300_799,
177+ },
178+ ])('preserves the exact epoch for $caseName when untouched', ({ timezone, value }) => {
179+ mockUseTimezoneState.mockReturnValue({ timezone, status: 'ready' })
180+ const container = document.createElement('div')
181+ document.body.appendChild(container)
182+ const root = createRoot(container)
183+ const onSave = vi.fn()
184+
185+ act(() =>
186+ root.render(
187+ createElement(InlineEditor, {
188+ value,
189+ column: column('ttl'),
190+ onSave,
191+ onCancel: vi.fn(),
192+ })
193+ )
194+ )
195+
196+ const input = container.querySelector('input') as HTMLInputElement
197+ act(() => input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })))
198+
199+ expect(onSave).toHaveBeenCalledWith(value, 'enter')
200+ act(() => root.unmount())
201+ container.remove()
202+ })
203+
204+ it('cancels TTL editing when the saved timezone cannot be loaded', () => {
205+ mockUseTimezoneState.mockReturnValue({
206+ timezone: 'America/Los_Angeles',
207+ status: 'error',
208+ })
209+ const container = document.createElement('div')
210+ document.body.appendChild(container)
211+ const root = createRoot(container)
212+ const onCancel = vi.fn()
213+
214+ act(() =>
215+ root.render(
216+ createElement(InlineEditor, {
217+ value: 2670,
218+ column: column('ttl'),
219+ onSave: vi.fn(),
220+ onCancel,
221+ })
222+ )
223+ )
224+
225+ expect(onCancel).toHaveBeenCalledOnce()
226+ expect(mockToastError).toHaveBeenCalledWith('Could not load timezone')
227+ act(() => root.unmount())
228+ container.remove()
229+ })
89230})
0 commit comments