diff --git a/.changeset/short-tools-sleep.md b/.changeset/short-tools-sleep.md new file mode 100644 index 00000000..fd2fd4c0 --- /dev/null +++ b/.changeset/short-tools-sleep.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/app": patch +--- + +fix: time selector always resets to 00:00 diff --git a/packages/app/src/components/TimePicker/TimePicker.tsx b/packages/app/src/components/TimePicker/TimePicker.tsx index 3f72ba98..d6186208 100644 --- a/packages/app/src/components/TimePicker/TimePicker.tsx +++ b/packages/app/src/components/TimePicker/TimePicker.tsx @@ -43,7 +43,7 @@ const modeAtom = atomWithStorage( TimePickerMode.Range, ); -const DATE_INPUT_PLACEHOLDER = 'YYY-MM-DD HH:mm:ss'; +const DATE_INPUT_PLACEHOLDER = 'YYYY-MM-DD HH:mm:ss'; const DATE_INPUT_FORMAT = 'YYYY-MM-DD HH:mm:ss'; /** Ensure a value is a Date object (Mantine v9 DateInput returns strings). */ @@ -55,6 +55,11 @@ const toDate = (v: Date | string | null): Date | null => * Mantine v9 DateInput expects/emits string values, but the TimePickerForm * stores Date objects (used by date-fns). This wrapper converts in both * directions: value (Date → string) and onChange (string → Date). + * + * `withTime` is required: by default DateInput strips the time part and + * normalizes values to midnight, even when `valueFormat` includes time + * tokens. Setting `withTime` preserves HH:mm:ss so manually-typed times + * survive blur/commit. */ type DateInputCmpProps = Omit & { value?: Date | null; @@ -69,6 +74,7 @@ const DateInputCmp = ({ { + let searchPage: SearchPage; + + test.beforeEach(async ({ page }) => { + searchPage = new SearchPage(page); + await searchPage.goto(); + await expect(searchPage.form).toBeVisible(); + await searchPage.timePicker.open(); + // Search page defaults to live/relative mode; switch to absolute so the + // Range inputs become editable. + await searchPage.timePicker.disableRelativeTime(); + // Guard against prior tests leaving the mode atom on "Around a time". + await searchPage.timePicker.selectRangeMode(); + }); + + test('should preserve manually typed Start and End times', async () => { + const start = '2026-04-15 14:37:42'; + const end = '2026-04-15 15:09:11'; + + await test.step('Type the datetimes into the inputs', async () => { + await searchPage.timePicker.fillStartDate(start); + await searchPage.timePicker.fillEndDate(end); + }); + + await test.step('Inputs retain the typed values before applying', async () => { + await expect(searchPage.timePicker.startDateInput).toHaveValue(start); + await expect(searchPage.timePicker.endDateInput).toHaveValue(end); + }); + + await test.step('Apply and confirm times propagate to the picker input', async () => { + await searchPage.timePicker.apply(); + + // The main picker input re-formats the range via date-fns, so the + // literal YYYY-MM-DD HH:mm:ss string won't appear. Instead, assert + // the typed HH:mm:ss components are present and the range is NOT + // collapsed to two midnights (the old broken behavior). + const inputValue = await searchPage.timePicker.input.inputValue(); + expect(inputValue).toContain('14:37:42'); + expect(inputValue).toContain('15:09:11'); + }); + + await test.step('URL reflects an absolute range', async () => { + await searchPage.page.waitForURL('**/search**from=**to=**'); + const url = searchPage.page.url(); + expect(url).toContain('from='); + expect(url).toContain('to='); + expect(url).toContain('isLive=false'); + }); + }); + + test('should preserve typed times after closing and reopening the picker', async () => { + const start = '2026-04-10 09:15:30'; + const end = '2026-04-10 11:45:20'; + + await searchPage.timePicker.fillStartDate(start); + await searchPage.timePicker.fillEndDate(end); + await searchPage.timePicker.apply(); + + await test.step('Reopen picker and verify inputs still show typed times', async () => { + await searchPage.timePicker.open(); + await expect(searchPage.timePicker.startDateInput).toHaveValue(start); + await expect(searchPage.timePicker.endDateInput).toHaveValue(end); + }); + }); + + test('should accept chrono natural-language times with non-midnight hours', async () => { + // "dateParser" in utils.ts uses chrono-node; this spec guards that natural + // language input still resolves to a non-midnight time after the fix. + await searchPage.timePicker.fillStartDate('yesterday at 3:22 pm'); + // Format produced by the DateInput re-serialization is YYYY-MM-DD HH:mm:ss + // (24h). 3:22 pm → 15:22. + await expect(searchPage.timePicker.startDateInput).toHaveValue(/15:22/); + }); +});