diff --git a/docs/fields/date.mdx b/docs/fields/date.mdx index d04d1aba29d..32f26d9c4b6 100644 --- a/docs/fields/date.mdx +++ b/docs/fields/date.mdx @@ -236,7 +236,28 @@ To enable timezone selection on a Date field, set the `timezone` property to `tr This will add a dropdown to the date picker that allows users to select a timezone. The selected timezone will be saved in the database along with the date in a new column named `date_tz`. -You can customise the available list of timezones in the [global admin config](../admin/overview#timezones). +You can customise the available list of timezones in the [global admin config](../admin/overview#timezones) or on the field config itself which accepts the following config as well: + +| Property | Description | +| -------------------- | ------------------------------------------------------------------------- | +| `defaultTimezone` | A value for the default timezone to be set. | +| `supportedTimezones` | An array of supported timezones with label and value object. | +| `required` | If true, the timezone selection will be required even if the date is not. | + +```ts +{ + name: 'date', + type: 'date', + timezone: { + defaultTimezone: 'America/New_York', + supportedTimezones: [ + { label: 'New York', value: 'America/New_York' }, + { label: 'Los Angeles', value: 'America/Los_Angeles' }, + { label: 'London', value: 'Europe/London' }, + ], + }, +} +``` **Good to know:** diff --git a/packages/payload/src/config/types.ts b/packages/payload/src/config/types.ts index 839d2e9e89b..885af8e83c2 100644 --- a/packages/payload/src/config/types.ts +++ b/packages/payload/src/config/types.ts @@ -439,7 +439,7 @@ export type Timezone = { type SupportedTimezonesFn = (args: { defaultTimezones: Timezone[] }) => Timezone[] -type TimezonesConfig = { +export type TimezonesConfig = { /** * The default timezone to use for the admin panel. */ diff --git a/packages/payload/src/exports/shared.ts b/packages/payload/src/exports/shared.ts index 0edea43735a..50f9106b6c5 100644 --- a/packages/payload/src/exports/shared.ts +++ b/packages/payload/src/exports/shared.ts @@ -62,30 +62,31 @@ export { isImage } from '../uploads/isImage.js' export { appendUploadSelectFields } from '../utilities/appendUploadSelectFields.js' export { applyLocaleFiltering } from '../utilities/applyLocaleFiltering.js' export { combineWhereConstraints } from '../utilities/combineWhereConstraints.js' - export { deepCopyObject, deepCopyObjectComplex, deepCopyObjectSimple, deepCopyObjectSimpleWithoutReactComponents, } from '../utilities/deepCopyObject.js' + export { deepMerge, deepMergeWithCombinedArrays, deepMergeWithReactComponents, deepMergeWithSourceArrays, } from '../utilities/deepMerge.js' - export { extractID } from '../utilities/extractID.js' export { flattenAllFields } from '../utilities/flattenAllFields.js' + export { flattenTopLevelFields } from '../utilities/flattenTopLevelFields.js' export { formatAdminURL } from '../utilities/formatAdminURL.js' export { formatLabels, toWords } from '../utilities/formatLabels.js' - export { getBestFitFromSizes } from '../utilities/getBestFitFromSizes.js' + export { getDataByPath } from '../utilities/getDataByPath.js' export { getFieldPermissions } from '../utilities/getFieldPermissions.js' +export { getObjectDotNotation } from '../utilities/getObjectDotNotation.js' export { getSafeRedirect } from '../utilities/getSafeRedirect.js' export { getSelectMode } from '../utilities/getSelectMode.js' diff --git a/packages/payload/src/fields/config/sanitize.ts b/packages/payload/src/fields/config/sanitize.ts index 00ae8b74873..4c35b46f599 100644 --- a/packages/payload/src/fields/config/sanitize.ts +++ b/packages/payload/src/fields/config/sanitize.ts @@ -407,9 +407,20 @@ export const sanitizeFields = async ({ // Insert our field after assignment if (field.type === 'date' && field.timezone) { const name = field.name + '_tz' - const defaultTimezone = config.admin?.timezones?.defaultTimezone - const supportedTimezones = config.admin?.timezones?.supportedTimezones + const defaultTimezone = + field.timezone && typeof field.timezone === 'object' + ? field.timezone.defaultTimezone + : config.admin?.timezones?.defaultTimezone + + const required = + field.required || + (field.timezone && typeof field.timezone === 'object' && field.timezone.required) + + const supportedTimezones = + field.timezone && typeof field.timezone === 'object' && field.timezone.supportedTimezones + ? field.timezone.supportedTimezones + : config.admin?.timezones?.supportedTimezones const options = typeof supportedTimezones === 'function' @@ -422,7 +433,7 @@ export const sanitizeFields = async ({ name, defaultValue: defaultTimezone, options, - required: field.required, + required, }) fields.splice(++i, 0, timezoneField) diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index 425f8a99726..537740a4722 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -116,6 +116,8 @@ import type { LabelFunction, PayloadComponent, StaticLabel, + Timezone, + TimezonesConfig, } from '../../config/types.js' import type { DBIdentifierName } from '../../database/types.js' import type { SanitizedGlobalConfig } from '../../globals/config/types.js' @@ -723,6 +725,14 @@ export type CheckboxFieldClient = { } & FieldBaseClient & Pick +type DateFieldTimezoneConfig = { + /** + * Make only the timezone required in the admin interface. This means a timezone is always required to be selected. + */ + required?: boolean + supportedTimezones?: Timezone[] +} & Pick + export type DateField = { admin?: { components?: { @@ -737,7 +747,7 @@ export type DateField = { /** * Enable timezone selection in the admin interface. */ - timezone?: true + timezone?: DateFieldTimezoneConfig | true type: 'date' validate?: DateFieldValidation } & Omit diff --git a/packages/payload/src/utilities/getObjectDotNotation.ts b/packages/payload/src/utilities/getObjectDotNotation.ts index eaa5d205dec..443924b81d0 100644 --- a/packages/payload/src/utilities/getObjectDotNotation.ts +++ b/packages/payload/src/utilities/getObjectDotNotation.ts @@ -1,3 +1,17 @@ +/** + * + * @deprecated use getObjectDotNotation from `'payload/shared'` instead of `'payload'` + * + * @example + * + * ```ts + * import { getObjectDotNotation } from 'payload/shared' + * + * const obj = { a: { b: { c: 42 } } } + * const value = getObjectDotNotation(obj, 'a.b.c', 0) // value is 42 + * const defaultValue = getObjectDotNotation(obj, 'a.b.x', 0) // defaultValue is 0 + * ``` + */ export const getObjectDotNotation = ( obj: Record, path: string, diff --git a/packages/ui/src/elements/Table/DefaultCell/fields/Date/index.tsx b/packages/ui/src/elements/Table/DefaultCell/fields/Date/index.tsx index 5c13b4457b5..b28afd6b477 100644 --- a/packages/ui/src/elements/Table/DefaultCell/fields/Date/index.tsx +++ b/packages/ui/src/elements/Table/DefaultCell/fields/Date/index.tsx @@ -1,25 +1,42 @@ 'use client' import type { DateFieldClient, DefaultCellComponentProps } from 'payload' +import { getObjectDotNotation } from 'payload/shared' import React from 'react' import { useConfig } from '../../../../../providers/Config/index.js' import { useTranslation } from '../../../../../providers/Translation/index.js' import { formatDate } from '../../../../../utilities/formatDocTitle/formatDateTitle.js' -export const DateCell: React.FC> = ({ - cellData, - field: { admin: { date } = {} }, -}) => { +export const DateCell: React.FC< + DefaultCellComponentProps<{ accessor?: string } & DateFieldClient> +> = (props) => { + const { + cellData, + field: { name, accessor, admin: { date } = {}, timezone: timezoneFromField }, + rowData, + } = props + const { config: { admin: { dateFormat: dateFormatFromRoot }, }, } = useConfig() + const { i18n } = useTranslation() - const dateFormat = date?.displayFormat || dateFormatFromRoot + const fieldPath = accessor || name - const { i18n } = useTranslation() + const timezoneFieldName = `${fieldPath}_tz` + const timezone = + Boolean(timezoneFromField) && rowData + ? getObjectDotNotation(rowData, timezoneFieldName, undefined) + : undefined + + const dateFormat = date?.displayFormat || dateFormatFromRoot - return {cellData && formatDate({ date: cellData, i18n, pattern: dateFormat })} + return ( + + {Boolean(cellData) && formatDate({ date: cellData, i18n, pattern: dateFormat, timezone })} + + ) } diff --git a/packages/ui/src/elements/TimezonePicker/index.scss b/packages/ui/src/elements/TimezonePicker/index.scss index 365bf814fa3..477c2af47cb 100644 --- a/packages/ui/src/elements/TimezonePicker/index.scss +++ b/packages/ui/src/elements/TimezonePicker/index.scss @@ -28,6 +28,7 @@ background: none; border: none; padding: 0; + padding-left: calc(var(--base) * 0.25); min-height: auto !important; position: relative; box-shadow: unset; diff --git a/packages/ui/src/elements/TimezonePicker/index.tsx b/packages/ui/src/elements/TimezonePicker/index.tsx index c41d84b6ade..af27729f1e5 100644 --- a/packages/ui/src/elements/TimezonePicker/index.tsx +++ b/packages/ui/src/elements/TimezonePicker/index.tsx @@ -18,6 +18,7 @@ export const TimezonePicker: React.FC = (props) => { id, onChange: onChangeFromProps, options: optionsFromProps, + readOnly: readOnlyFromProps, required, selectedTimezone: selectedTimezoneFromProps, } = props @@ -33,6 +34,8 @@ export const TimezonePicker: React.FC = (props) => { }) }, [options, selectedTimezoneFromProps]) + const readOnly = Boolean(readOnlyFromProps) || options.length === 1 + return (
= (props) => { /> { if (onChangeFromProps) { diff --git a/packages/ui/src/elements/TimezonePicker/types.ts b/packages/ui/src/elements/TimezonePicker/types.ts index fdbb0100b55..ded93b5e72b 100644 --- a/packages/ui/src/elements/TimezonePicker/types.ts +++ b/packages/ui/src/elements/TimezonePicker/types.ts @@ -3,6 +3,7 @@ import type { SelectFieldClient } from 'payload' export type Props = { id: string onChange?: (val: string) => void + readOnly?: boolean required?: boolean selectedTimezone?: string } & Pick diff --git a/packages/ui/src/fields/DateTime/index.tsx b/packages/ui/src/fields/DateTime/index.tsx index dc1e335b2f2..a8704772d95 100644 --- a/packages/ui/src/fields/DateTime/index.tsx +++ b/packages/ui/src/fields/DateTime/index.tsx @@ -70,13 +70,23 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => { const timezonePath = path + '_tz' const timezoneField = useFormFields(([fields, _]) => fields?.[timezonePath]) - const supportedTimezones = config.admin.timezones.supportedTimezones + + const supportedTimezones = useMemo(() => { + if (timezone && typeof timezone === 'object' && timezone.supportedTimezones) { + return timezone.supportedTimezones + } + + return config.admin.timezones.supportedTimezones + }, [config.admin.timezones.supportedTimezones, timezone]) + /** * Date appearance doesn't include timestamps, * which means we need to pin the time to always 12:00 for the selected date */ const isDateOnly = ['dayOnly', 'default', 'monthOnly'].includes(pickerAppearance) const selectedTimezone = timezoneField?.value as string + const timezoneRequired = + required || (timezone && typeof timezone === 'object' && timezone.required) // The displayed value should be the original value, adjusted to the user's timezone const displayedValue = useMemo(() => { @@ -192,7 +202,8 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => { id={`${path}-timezone-picker`} onChange={onChangeTimezone} options={supportedTimezones} - required={required} + readOnly={readOnly || disabled} + required={timezoneRequired} selectedTimezone={selectedTimezone} /> )} diff --git a/test/fields/collections/Date/e2e.spec.ts b/test/fields/collections/Date/e2e.spec.ts index 9a4acce04d6..ab67320e582 100644 --- a/test/fields/collections/Date/e2e.spec.ts +++ b/test/fields/collections/Date/e2e.spec.ts @@ -487,16 +487,8 @@ describe('Date', () => { const timezoneClearButton = page.locator( `#field-dayAndTimeWithTimezone .rs__control .clear-indicator`, ) - await timezoneClearButton.click() - - // Expect an error here - await saveDocAndAssert(page, undefined, 'error') - - const errorMessage = page.locator( - '#field-dayAndTimeWithTimezone .field-error .tooltip-content:has-text("A timezone is required.")', - ) - await expect(errorMessage).toBeVisible() + await expect(timezoneClearButton).toBeHidden() }) test('can clear a selected timezone', async () => { @@ -508,14 +500,12 @@ describe('Date', () => { await page.goto(url.edit(existingDoc!.id)) - const dateField = page.locator( - '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', - ) + const dateField = page.locator('#field-defaultWithTimezone .react-datepicker-wrapper input') const initialDate = await dateField.inputValue() const timezoneClearButton = page.locator( - `#field-dayAndTimeWithTimezone .rs__control .clear-indicator`, + `#field-defaultWithTimezone .rs__control .clear-indicator`, ) await timezoneClearButton.click() @@ -526,6 +516,42 @@ describe('Date', () => { }).toPass({ timeout: 10000, intervals: [100] }) }) + // This test should pass but it does not currently due to a11y issues with date fields - will fix in follow up PR + test.skip('can not clear a timezone that is required', async () => { + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + }) + + await page.goto(url.edit(existingDoc!.id)) + + const dateField = page.locator( + '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', + ) + + await expect(dateField).toBeVisible() + await expect(dateField).toHaveAttribute('required') + + const timezoneClearButton = page.locator( + `#field-dayAndTimeWithTimezone .rs__control .clear-indicator`, + ) + await expect(timezoneClearButton).toBeHidden() + + const dateFieldRequiredOnlyTz = page.locator( + '#field-dayAndTimeWithTimezoneRequired .react-datepicker-wrapper input', + ) + + await expect(dateFieldRequiredOnlyTz).toBeVisible() + // eslint-disable-next-line jest-dom/prefer-required + await expect(dateFieldRequiredOnlyTz).not.toHaveAttribute('required') + + const timezoneClearButtonOnlyTz = page.locator( + `#field-dayAndTimeWithTimezoneRequired .rs__control .clear-indicator`, + ) + await expect(timezoneClearButtonOnlyTz).toBeHidden() + }) + test('creates the expected UTC value when the timezone is Tokyo', async () => { // We send this value through the input const expectedDateInput = 'Jan 1, 2025 6:00 PM' @@ -572,848 +598,361 @@ describe('Date', () => { }) }) -describe('Date with TZ - Context: America/Detroit', () => { - beforeAll(async ({ browser }, testInfo) => { - testInfo.setTimeout(TEST_TIMEOUT_LONG) - process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit - ;({ payload, serverURL } = await initPayloadE2ENoConfig({ - dirname, - // prebuild, - })) - url = new AdminUrlUtil(serverURL, dateFieldsSlug) - - const context = await browser.newContext({ timezoneId: detroitTimezone }) - page = await context.newPage() - initPageConsoleErrorCatch(page) - - await ensureCompilationIsDone({ page, serverURL }) - }) - beforeEach(async () => { - await reInitializeDB({ - serverURL, - snapshotKey: 'fieldsTest', - uploadsDir: path.resolve(dirname, './collections/Upload/uploads'), - }) - - if (client) { - await client.logout() - } - client = new RESTClient({ defaultSlug: 'users', serverURL }) - await client.login() - - await ensureCompilationIsDone({ page, serverURL }) - }) - - test('displayed value should remain unchanged', async () => { - const { - docs: [existingDoc], - } = await payload.find({ - collection: dateFieldsSlug, - }) +/** + * Helper function to create timezone context test suites + * Reduces repetition across different timezone test scenarios + */ +const createTimezoneContextTests = (contextName: string, timezoneId: string) => { + describe(`Date with TZ - Context: ${contextName}`, () => { + beforeAll(async ({ browser }, testInfo) => { + testInfo.setTimeout(TEST_TIMEOUT_LONG) + process.env.SEED_IN_CONFIG_ONINIT = 'false' + ;({ payload, serverURL } = await initPayloadE2ENoConfig({ + dirname, + })) + url = new AdminUrlUtil(serverURL, dateFieldsSlug) + + const context = await browser.newContext({ timezoneId }) + page = await context.newPage() + initPageConsoleErrorCatch(page) + + await ensureCompilationIsDone({ page, serverURL }) + }) + + beforeEach(async () => { + await reInitializeDB({ + serverURL, + snapshotKey: 'fieldsTest', + uploadsDir: path.resolve(dirname, './collections/Upload/uploads'), + }) - await page.goto(url.edit(existingDoc!.id)) + if (client) { + await client.logout() + } + client = new RESTClient({ defaultSlug: 'users', serverURL }) + await client.login() - const result = await page.evaluate(() => { - return Intl.DateTimeFormat().resolvedOptions().timeZone + await ensureCompilationIsDone({ page, serverURL }) }) - await expect(() => { - // Confirm that the emulated timezone is set to London - expect(result).toEqual(detroitTimezone) - }).toPass({ timeout: 10000, intervals: [100] }) - - const dateOnlyLocator = page.locator( - '#field-defaultWithTimezone .react-datepicker-wrapper input', - ) - const dateTimeLocator = page.locator( - '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', - ) - - const expectedDateOnlyValue = '08/12/2027' - const expectedDateTimeValue = 'Aug 12, 2027 10:00 AM' // This is the seeded value for 10AM at Asia/Tokyo time - - await expect(dateOnlyLocator).toHaveValue(expectedDateOnlyValue) - await expect(dateTimeLocator).toHaveValue(expectedDateTimeValue) - }) - - test('creates the expected UTC value when the selected timezone is Paris - no daylight savings', async () => { - // We send this value through the input - const expectedDateInput = 'Jan 1, 2025 6:00 PM' - // We're testing this specific date because Paris has no daylight savings time in January - // but the UTC date will be different from 6PM local time in the summer versus the winter - const expectedUTCValue = '2025-01-01T17:00:00.000Z' - - await page.goto(url.create) - - const dateField = page.locator('#field-default input') - await dateField.fill('01/01/2025') - - const dateTimeLocator = page.locator( - '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', - ) + test('displayed value should remain unchanged', async () => { + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + }) - const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` - const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")` + await page.goto(url.edit(existingDoc!.id)) - await page.click(dropdownControlSelector) - await page.click(timezoneOptionSelector) - await dateTimeLocator.fill(expectedDateInput) + const result = await page.evaluate(() => { + return Intl.DateTimeFormat().resolvedOptions().timeZone + }) - await saveDocAndAssert(page) + await expect(() => { + expect(result).toEqual(timezoneId) + }).toPass({ timeout: 10000, intervals: [100] }) - const docID = page.url().split('/').pop() + const dateOnlyLocator = page.locator( + '#field-defaultWithTimezone .react-datepicker-wrapper input', + ) + const dateTimeLocator = page.locator( + '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', + ) - // eslint-disable-next-line payload/no-flaky-assertions - expect(docID).toBeTruthy() + const expectedDateOnlyValue = '08/12/2027' + const expectedDateTimeValue = 'Aug 12, 2027 10:00 AM' - const { - docs: [existingDoc], - } = await payload.find({ - collection: dateFieldsSlug, - where: { - id: { - equals: docID, - }, - }, + await expect(dateOnlyLocator).toHaveValue(expectedDateOnlyValue) + await expect(dateTimeLocator).toHaveValue(expectedDateTimeValue) }) - // eslint-disable-next-line payload/no-flaky-assertions - expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue) - }) - - test('creates the expected UTC value when the selected timezone is Paris - with daylight savings', async () => { - // We send this value through the input - const expectedDateInput = 'Jul 1, 2025 6:00 PM' - - // We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone) - // but the UTC date will be different from 6PM local time in the summer versus the winter - const expectedUTCValue = '2025-07-01T16:00:00.000Z' + test('displayed value in list view should remain unchanged', async () => { + await page.goto(url.list) - await page.goto(url.create) - - const dateField = page.locator('#field-default input') - await dateField.fill('01/01/2025') - - const dateTimeLocator = page.locator( - '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', - ) + const dateTimeLocator = page.locator('.cell-timezoneGroup__dayAndTime').first() - const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` - const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")` - - await page.click(dropdownControlSelector) - await page.click(timezoneOptionSelector) - await dateTimeLocator.fill(expectedDateInput) - - await saveDocAndAssert(page) - - const docID = page.url().split('/').pop() + await expect(async () => { + await expect(dateTimeLocator).toHaveText('January 31st 2025, 10:00 AM') + }).toPass({ timeout: 10000, intervals: [100] }) - // eslint-disable-next-line payload/no-flaky-assertions - expect(docID).toBeTruthy() + const dateTimeLocatorFixed = page.locator('.cell-dayAndTimeWithTimezoneFixed').first() - const { - docs: [existingDoc], - } = await payload.find({ - collection: dateFieldsSlug, - where: { - id: { - equals: docID, - }, - }, + await expect(async () => { + await expect(dateTimeLocatorFixed).toHaveText('October 29th 2025, 8:00 PM') + }).toPass({ timeout: 10000, intervals: [100] }) }) - // eslint-disable-next-line payload/no-flaky-assertions - expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue) - }) - - test('creates the expected UTC value when the selected timezone is Auckland - no daylight savings', async () => { - // We send this value through the input - const expectedDateTimeInput = 'Jan 1, 2025 6:00 PM' - // The timestamp for this date should be normalised to 12PM local time - const expectedDateOnlyInput = '01/02/2025' // 2nd July 2025 - - // We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone) - // but the UTC date will be different from 6PM local time in the summer versus the winter - const expectedDateTimeUTCValue = '2025-01-01T05:00:00.000Z' - // The timestamp for this date should be normalised to 12PM local time - const expectedDateOnlyUTCValue = '2025-01-01T23:00:00.000Z' // 2nd July 2025 at 12PM in Auckland - - await page.goto(url.create) - - // Default date field - filling it because it's required for the form to be valid - const dateField = page.locator('#field-default input') - await dateField.fill('01/01/2025') - - // Date input fields - const dateTimeLocator = page.locator( - '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', - ) - const dateOnlyLocator = page.locator( - '#field-defaultWithTimezone .react-datepicker-wrapper input', - ) - - // Fill in date only - const dateOnlyDropdownSelector = `#field-defaultWithTimezone .rs__control` - const dateOnlytimezoneSelector = `#field-defaultWithTimezone .rs__menu .rs__option:has-text("Auckland")` - await page.click(dateOnlyDropdownSelector) - await page.click(dateOnlytimezoneSelector) - await dateOnlyLocator.fill(expectedDateOnlyInput) - - // Fill in date and time - const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` - const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Auckland")` - await page.click(dropdownControlSelector) - await page.click(timezoneOptionSelector) - await dateTimeLocator.fill(expectedDateTimeInput) - - await saveDocAndAssert(page) - - const docID = page.url().split('/').pop() - - // eslint-disable-next-line payload/no-flaky-assertions - expect(docID).toBeTruthy() - - const { - docs: [existingDoc], - } = await payload.find({ - collection: dateFieldsSlug, - where: { - id: { - equals: docID, - }, - }, - }) + test('creates the expected UTC value when the selected timezone is Paris - no daylight savings', async () => { + const expectedDateInput = 'Jan 1, 2025 6:00 PM' + const expectedUTCValue = '2025-01-01T17:00:00.000Z' - // eslint-disable-next-line payload/no-flaky-assertions - expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedDateTimeUTCValue) - expect(existingDoc?.defaultWithTimezone).toEqual(expectedDateOnlyUTCValue) - }) + await page.goto(url.create) - test('creates the expected UTC value when the selected timezone is Auckland - with daylight savings', async () => { - // We send this value through the input - const expectedDateTimeInput = 'Jul 1, 2025 6:00 PM' - // The timestamp for this date should be normalised to 12PM local time - const expectedDateOnlyInput = '07/02/2025' // 2nd July 2025 + const dateField = page.locator('#field-default input') + await dateField.fill('01/01/2025') - // We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone) - // but the UTC date will be different from 6PM local time in the summer versus the winter - const expectedDateTimeUTCValue = '2025-07-01T06:00:00.000Z' - // The timestamp for this date should be normalised to 12PM local time - const expectedDateOnlyUTCValue = '2025-07-02T00:00:00.000Z' // 2nd July 2025 at 12PM in Auckland + const dateTimeLocator = page.locator( + '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', + ) - await page.goto(url.create) + const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` + const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")` - // Default date field - filling it because it's required for the form to be valid - const dateField = page.locator('#field-default input') - await dateField.fill('01/01/2025') - - // Date input fields - const dateTimeLocator = page.locator( - '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', - ) - const dateOnlyLocator = page.locator( - '#field-defaultWithTimezone .react-datepicker-wrapper input', - ) - - // Fill in date only - const dateOnlyDropdownSelector = `#field-defaultWithTimezone .rs__control` - const dateOnlytimezoneSelector = `#field-defaultWithTimezone .rs__menu .rs__option:has-text("Auckland")` - await page.click(dateOnlyDropdownSelector) - await page.click(dateOnlytimezoneSelector) - await dateOnlyLocator.fill(expectedDateOnlyInput) - - // Fill in date and time - const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` - const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Auckland")` - await page.click(dropdownControlSelector) - await page.click(timezoneOptionSelector) - await dateTimeLocator.fill(expectedDateTimeInput) + await page.click(dropdownControlSelector) + await page.click(timezoneOptionSelector) + await dateTimeLocator.fill(expectedDateInput) - await saveDocAndAssert(page) + await saveDocAndAssert(page) - const docID = page.url().split('/').pop() + const docID = page.url().split('/').pop() - // eslint-disable-next-line payload/no-flaky-assertions - expect(docID).toBeTruthy() + // eslint-disable-next-line payload/no-flaky-assertions + expect(docID).toBeTruthy() - const { - docs: [existingDoc], - } = await payload.find({ - collection: dateFieldsSlug, - where: { - id: { - equals: docID, + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + where: { + id: { + equals: docID, + }, }, - }, - }) - - // eslint-disable-next-line payload/no-flaky-assertions - expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedDateTimeUTCValue) - expect(existingDoc?.defaultWithTimezone).toEqual(expectedDateOnlyUTCValue) - }) -}) - -describe('Date with TZ - Context: Europe/London', () => { - beforeAll(async ({ browser }, testInfo) => { - testInfo.setTimeout(TEST_TIMEOUT_LONG) - process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit - ;({ payload, serverURL } = await initPayloadE2ENoConfig({ - dirname, - // prebuild, - })) - url = new AdminUrlUtil(serverURL, dateFieldsSlug) - - const context = await browser.newContext({ timezoneId: londonTimezone }) - page = await context.newPage() - initPageConsoleErrorCatch(page) - - await ensureCompilationIsDone({ page, serverURL }) - }) - beforeEach(async () => { - await reInitializeDB({ - serverURL, - snapshotKey: 'fieldsTest', - uploadsDir: path.resolve(dirname, './collections/Upload/uploads'), - }) - - if (client) { - await client.logout() - } - client = new RESTClient({ defaultSlug: 'users', serverURL }) - await client.login() - - await ensureCompilationIsDone({ page, serverURL }) - }) - - test('displayed value should remain unchanged', async () => { - const { - docs: [existingDoc], - } = await payload.find({ - collection: dateFieldsSlug, - }) - - await page.goto(url.edit(existingDoc!.id)) + }) - const result = await page.evaluate(() => { - return Intl.DateTimeFormat().resolvedOptions().timeZone + // eslint-disable-next-line payload/no-flaky-assertions + expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue) }) - await expect(() => { - // Confirm that the emulated timezone is set to London - expect(result).toEqual(londonTimezone) - }).toPass({ timeout: 10000, intervals: [100] }) - - const dateOnlyLocator = page.locator( - '#field-defaultWithTimezone .react-datepicker-wrapper input', - ) - const dateTimeLocator = page.locator( - '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', - ) - - const expectedDateOnlyValue = '08/12/2027' - const expectedDateTimeValue = 'Aug 12, 2027 10:00 AM' // This is the seeded value for 10AM at Asia/Tokyo time - - await expect(dateOnlyLocator).toHaveValue(expectedDateOnlyValue) - await expect(dateTimeLocator).toHaveValue(expectedDateTimeValue) - }) - - test('creates the expected UTC value when the selected timezone is Paris - no daylight savings', async () => { - // We send this value through the input - const expectedDateInput = 'Jan 1, 2025 6:00 PM' - // We're testing this specific date because Paris has no daylight savings time in January - // but the UTC date will be different from 6PM local time in the summer versus the winter - const expectedUTCValue = '2025-01-01T17:00:00.000Z' + test('creates the expected UTC value when the selected timezone is Paris - with daylight savings', async () => { + const expectedDateInput = 'Jul 1, 2025 6:00 PM' + const expectedUTCValue = '2025-07-01T16:00:00.000Z' - await page.goto(url.create) + await page.goto(url.create) - const dateField = page.locator('#field-default input') - await dateField.fill('01/01/2025') + const dateField = page.locator('#field-default input') + await dateField.fill('01/01/2025') - const dateTimeLocator = page.locator( - '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', - ) + const dateTimeLocator = page.locator( + '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', + ) - const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` - const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")` + const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` + const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")` - await page.click(dropdownControlSelector) - await page.click(timezoneOptionSelector) - await dateTimeLocator.fill(expectedDateInput) + await page.click(dropdownControlSelector) + await page.click(timezoneOptionSelector) + await dateTimeLocator.fill(expectedDateInput) - await saveDocAndAssert(page) + await saveDocAndAssert(page) - const docID = page.url().split('/').pop() + const docID = page.url().split('/').pop() - // eslint-disable-next-line payload/no-flaky-assertions - expect(docID).toBeTruthy() + // eslint-disable-next-line payload/no-flaky-assertions + expect(docID).toBeTruthy() - const { - docs: [existingDoc], - } = await payload.find({ - collection: dateFieldsSlug, - where: { - id: { - equals: docID, + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + where: { + id: { + equals: docID, + }, }, - }, - }) - - // eslint-disable-next-line payload/no-flaky-assertions - expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue) - }) - - test('creates the expected UTC value when the selected timezone is Paris - with daylight savings', async () => { - // We send this value through the input - const expectedDateInput = 'Jul 1, 2025 6:00 PM' - - // We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone) - // but the UTC date will be different from 6PM local time in the summer versus the winter - const expectedUTCValue = '2025-07-01T16:00:00.000Z' - - await page.goto(url.create) - - const dateField = page.locator('#field-default input') - await dateField.fill('01/01/2025') - - const dateTimeLocator = page.locator( - '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', - ) - - const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` - const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")` - - await page.click(dropdownControlSelector) - await page.click(timezoneOptionSelector) - await dateTimeLocator.fill(expectedDateInput) - - await saveDocAndAssert(page) - - const docID = page.url().split('/').pop() - - // eslint-disable-next-line payload/no-flaky-assertions - expect(docID).toBeTruthy() + }) - const { - docs: [existingDoc], - } = await payload.find({ - collection: dateFieldsSlug, - where: { - id: { - equals: docID, - }, - }, + // eslint-disable-next-line payload/no-flaky-assertions + expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue) }) - // eslint-disable-next-line payload/no-flaky-assertions - expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue) - }) - - test('creates the expected UTC value when the selected timezone is Auckland - no daylight savings', async () => { - // We send this value through the input - const expectedDateTimeInput = 'Jan 1, 2025 6:00 PM' - // The timestamp for this date should be normalised to 12PM local time - const expectedDateOnlyInput = '01/02/2025' // 2nd July 2025 - - // We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone) - // but the UTC date will be different from 6PM local time in the summer versus the winter - const expectedDateTimeUTCValue = '2025-01-01T05:00:00.000Z' - // The timestamp for this date should be normalised to 12PM local time - const expectedDateOnlyUTCValue = '2025-01-01T23:00:00.000Z' // 2nd July 2025 at 12PM in Auckland - - await page.goto(url.create) + test('creates the expected UTC value when the selected timezone is Auckland - no daylight savings', async () => { + const expectedDateTimeInput = 'Jan 1, 2025 6:00 PM' + const expectedDateOnlyInput = '01/02/2025' - // Default date field - filling it because it's required for the form to be valid - const dateField = page.locator('#field-default input') - await dateField.fill('01/01/2025') - - // Date input fields - const dateTimeLocator = page.locator( - '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', - ) - const dateOnlyLocator = page.locator( - '#field-defaultWithTimezone .react-datepicker-wrapper input', - ) - - // Fill in date only - const dateOnlyDropdownSelector = `#field-defaultWithTimezone .rs__control` - const dateOnlytimezoneSelector = `#field-defaultWithTimezone .rs__menu .rs__option:has-text("Auckland")` - await page.click(dateOnlyDropdownSelector) - await page.click(dateOnlytimezoneSelector) - await dateOnlyLocator.fill(expectedDateOnlyInput) - - // Fill in date and time - const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` - const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Auckland")` - await page.click(dropdownControlSelector) - await page.click(timezoneOptionSelector) - await dateTimeLocator.fill(expectedDateTimeInput) - - await saveDocAndAssert(page) + const expectedDateTimeUTCValue = '2025-01-01T05:00:00.000Z' + const expectedDateOnlyUTCValue = '2025-01-01T23:00:00.000Z' - const docID = page.url().split('/').pop() - - // eslint-disable-next-line payload/no-flaky-assertions - expect(docID).toBeTruthy() - - const { - docs: [existingDoc], - } = await payload.find({ - collection: dateFieldsSlug, - where: { - id: { - equals: docID, - }, - }, - }) - - // eslint-disable-next-line payload/no-flaky-assertions - expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedDateTimeUTCValue) - expect(existingDoc?.defaultWithTimezone).toEqual(expectedDateOnlyUTCValue) - }) + await page.goto(url.create) - test('creates the expected UTC value when the selected timezone is Auckland - with daylight savings', async () => { - // We send this value through the input - const expectedDateTimeInput = 'Jul 1, 2025 6:00 PM' - // The timestamp for this date should be normalised to 12PM local time - const expectedDateOnlyInput = '07/02/2025' // 2nd July 2025 + const dateField = page.locator('#field-default input') + await dateField.fill('01/01/2025') - // We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone) - // but the UTC date will be different from 6PM local time in the summer versus the winter - const expectedDateTimeUTCValue = '2025-07-01T06:00:00.000Z' - // The timestamp for this date should be normalised to 12PM local time - const expectedDateOnlyUTCValue = '2025-07-02T00:00:00.000Z' // 2nd July 2025 at 12PM in Auckland + const dateTimeLocator = page.locator( + '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', + ) + const dateOnlyLocator = page.locator( + '#field-defaultWithTimezone .react-datepicker-wrapper input', + ) - await page.goto(url.create) + const dateOnlyDropdownSelector = `#field-defaultWithTimezone .rs__control` + const dateOnlytimezoneSelector = `#field-defaultWithTimezone .rs__menu .rs__option:has-text("Auckland")` + await page.click(dateOnlyDropdownSelector) + await page.click(dateOnlytimezoneSelector) + await dateOnlyLocator.fill(expectedDateOnlyInput) - // Default date field - filling it because it's required for the form to be valid - const dateField = page.locator('#field-default input') - await dateField.fill('01/01/2025') - - // Date input fields - const dateTimeLocator = page.locator( - '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', - ) - const dateOnlyLocator = page.locator( - '#field-defaultWithTimezone .react-datepicker-wrapper input', - ) - - // Fill in date only - const dateOnlyDropdownSelector = `#field-defaultWithTimezone .rs__control` - const dateOnlytimezoneSelector = `#field-defaultWithTimezone .rs__menu .rs__option:has-text("Auckland")` - await page.click(dateOnlyDropdownSelector) - await page.click(dateOnlytimezoneSelector) - await dateOnlyLocator.fill(expectedDateOnlyInput) - - // Fill in date and time - const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` - const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Auckland")` - await page.click(dropdownControlSelector) - await page.click(timezoneOptionSelector) - await dateTimeLocator.fill(expectedDateTimeInput) + const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` + const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Auckland")` + await page.click(dropdownControlSelector) + await page.click(timezoneOptionSelector) + await dateTimeLocator.fill(expectedDateTimeInput) - await saveDocAndAssert(page) + await saveDocAndAssert(page) - const docID = page.url().split('/').pop() + const docID = page.url().split('/').pop() - // eslint-disable-next-line payload/no-flaky-assertions - expect(docID).toBeTruthy() + // eslint-disable-next-line payload/no-flaky-assertions + expect(docID).toBeTruthy() - const { - docs: [existingDoc], - } = await payload.find({ - collection: dateFieldsSlug, - where: { - id: { - equals: docID, + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + where: { + id: { + equals: docID, + }, }, - }, - }) - - // eslint-disable-next-line payload/no-flaky-assertions - expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedDateTimeUTCValue) - expect(existingDoc?.defaultWithTimezone).toEqual(expectedDateOnlyUTCValue) - }) -}) - -describe('Date with TZ - Context: Pacific/Auckland', () => { - beforeAll(async ({ browser }, testInfo) => { - testInfo.setTimeout(TEST_TIMEOUT_LONG) - process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit - ;({ payload, serverURL } = await initPayloadE2ENoConfig({ - dirname, - // prebuild, - })) - url = new AdminUrlUtil(serverURL, dateFieldsSlug) - - const context = await browser.newContext({ timezoneId: aucklandTimezone }) - page = await context.newPage() - initPageConsoleErrorCatch(page) - - await ensureCompilationIsDone({ page, serverURL }) - }) - beforeEach(async () => { - await reInitializeDB({ - serverURL, - snapshotKey: 'fieldsTest', - uploadsDir: path.resolve(dirname, './collections/Upload/uploads'), - }) - - if (client) { - await client.logout() - } - client = new RESTClient({ defaultSlug: 'users', serverURL }) - await client.login() - - await ensureCompilationIsDone({ page, serverURL }) - }) + }) - test('displayed value should remain unchanged', async () => { - const { - docs: [existingDoc], - } = await payload.find({ - collection: dateFieldsSlug, + // eslint-disable-next-line payload/no-flaky-assertions + expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedDateTimeUTCValue) + expect(existingDoc?.defaultWithTimezone).toEqual(expectedDateOnlyUTCValue) }) - await page.goto(url.edit(existingDoc!.id)) + test('creates the expected UTC value when the selected timezone is Auckland - with daylight savings', async () => { + const expectedDateTimeInput = 'Jul 1, 2025 6:00 PM' + const expectedDateOnlyInput = '07/02/2025' - const result = await page.evaluate(() => { - return Intl.DateTimeFormat().resolvedOptions().timeZone - }) + const expectedDateTimeUTCValue = '2025-07-01T06:00:00.000Z' + const expectedDateOnlyUTCValue = '2025-07-02T00:00:00.000Z' - await expect(() => { - // Confirm that the emulated timezone is set to London - expect(result).toEqual(aucklandTimezone) - }).toPass({ timeout: 10000, intervals: [100] }) - - const dateOnlyLocator = page.locator( - '#field-defaultWithTimezone .react-datepicker-wrapper input', - ) - const dateTimeLocator = page.locator( - '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', - ) - - const expectedDateOnlyValue = '08/12/2027' - const expectedDateTimeValue = 'Aug 12, 2027 10:00 AM' // This is the seeded value for 10AM at Asia/Tokyo time - - await expect(dateOnlyLocator).toHaveValue(expectedDateOnlyValue) - await expect(dateTimeLocator).toHaveValue(expectedDateTimeValue) - }) - - test('creates the expected UTC value when the selected timezone is Paris - no daylight savings', async () => { - // We send this value through the input - const expectedDateInput = 'Jan 1, 2025 6:00 PM' - // We're testing this specific date because Paris has no daylight savings time in January - // but the UTC date will be different from 6PM local time in the summer versus the winter - const expectedUTCValue = '2025-01-01T17:00:00.000Z' - - await page.goto(url.create) + await page.goto(url.create) - const dateField = page.locator('#field-default input') - await dateField.fill('01/01/2025') + const dateField = page.locator('#field-default input') + await dateField.fill('01/01/2025') - const dateTimeLocator = page.locator( - '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', - ) + const dateTimeLocator = page.locator( + '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', + ) + const dateOnlyLocator = page.locator( + '#field-defaultWithTimezone .react-datepicker-wrapper input', + ) - const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` - const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")` + const dateOnlyDropdownSelector = `#field-defaultWithTimezone .rs__control` + const dateOnlytimezoneSelector = `#field-defaultWithTimezone .rs__menu .rs__option:has-text("Auckland")` + await page.click(dateOnlyDropdownSelector) + await page.click(dateOnlytimezoneSelector) + await dateOnlyLocator.fill(expectedDateOnlyInput) - await page.click(dropdownControlSelector) - await page.click(timezoneOptionSelector) - await dateTimeLocator.fill(expectedDateInput) + const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` + const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Auckland")` + await page.click(dropdownControlSelector) + await page.click(timezoneOptionSelector) + await dateTimeLocator.fill(expectedDateTimeInput) - await saveDocAndAssert(page) + await saveDocAndAssert(page) - const docID = page.url().split('/').pop() + const docID = page.url().split('/').pop() - // eslint-disable-next-line payload/no-flaky-assertions - expect(docID).toBeTruthy() + // eslint-disable-next-line payload/no-flaky-assertions + expect(docID).toBeTruthy() - const { - docs: [existingDoc], - } = await payload.find({ - collection: dateFieldsSlug, - where: { - id: { - equals: docID, + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + where: { + id: { + equals: docID, + }, }, - }, - }) - - // eslint-disable-next-line payload/no-flaky-assertions - expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue) - }) - - test('creates the expected UTC value when the selected timezone is Paris - with daylight savings', async () => { - // We send this value through the input - const expectedDateInput = 'Jul 1, 2025 6:00 PM' - - // We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone) - // but the UTC date will be different from 6PM local time in the summer versus the winter - const expectedUTCValue = '2025-07-01T16:00:00.000Z' - - await page.goto(url.create) - - const dateField = page.locator('#field-default input') - await dateField.fill('01/01/2025') - - const dateTimeLocator = page.locator( - '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', - ) - - const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` - const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")` + }) - await page.click(dropdownControlSelector) - await page.click(timezoneOptionSelector) - await dateTimeLocator.fill(expectedDateInput) + // eslint-disable-next-line payload/no-flaky-assertions + expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedDateTimeUTCValue) + expect(existingDoc?.defaultWithTimezone).toEqual(expectedDateOnlyUTCValue) + }) - await saveDocAndAssert(page) + test('when only one timezone is supported the timezone should be disabled and enforced', async () => { + const expectedFixedUTCValue = '2025-10-29T20:00:00.000Z' // This is 8PM UTC on Oct 29, 2025 + const expectedFixedTimezoneValue = 'Europe/London' - const docID = page.url().split('/').pop() + const expectedUpdatedInput = 'Oct 29, 2025 4:00 PM' + const expectedUpdatedUTCValue = '2025-10-29T16:00:00.000Z' // This is 4PM UTC on Oct 29, 2025 - // eslint-disable-next-line payload/no-flaky-assertions - expect(docID).toBeTruthy() + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + }) - const { - docs: [existingDoc], - } = await payload.find({ - collection: dateFieldsSlug, - where: { - id: { - equals: docID, - }, - }, - }) + await page.goto(url.edit(existingDoc!.id)) - // eslint-disable-next-line payload/no-flaky-assertions - expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue) - }) + const dateTimeLocator = page.locator( + '#field-dayAndTimeWithTimezoneFixed .date-time-picker input', + ) - test('creates the expected UTC value when the selected timezone is Auckland - no daylight savings', async () => { - // We send this value through the input - const expectedDateTimeInput = 'Jan 1, 2025 6:00 PM' - // The timestamp for this date should be normalised to 12PM local time - const expectedDateOnlyInput = '01/02/2025' // 2nd July 2025 + await expect(dateTimeLocator).toBeEnabled() - // We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone) - // but the UTC date will be different from 6PM local time in the summer versus the winter - const expectedDateTimeUTCValue = '2025-01-01T05:00:00.000Z' - // The timestamp for this date should be normalised to 12PM local time - const expectedDateOnlyUTCValue = '2025-01-01T23:00:00.000Z' // 2nd July 2025 at 12PM in Auckland + const dateFieldWrapper = page.locator('.date-time-field').filter({ has: dateTimeLocator }) + const dropdownInput = dateFieldWrapper.locator('.timezone-picker .rs__input') + const dropdownValue = dateFieldWrapper.locator('.timezone-picker .rs__value-container') - await page.goto(url.create) + await expect(dropdownInput).toBeDisabled() + await expect(dropdownValue).toContainText('London') - // Default date field - filling it because it's required for the form to be valid - const dateField = page.locator('#field-default input') - await dateField.fill('01/01/2025') - - // Date input fields - const dateTimeLocator = page.locator( - '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', - ) - const dateOnlyLocator = page.locator( - '#field-defaultWithTimezone .react-datepicker-wrapper input', - ) - - // Fill in date only - const dateOnlyDropdownSelector = `#field-defaultWithTimezone .rs__control` - const dateOnlytimezoneSelector = `#field-defaultWithTimezone .rs__menu .rs__option:has-text("Auckland")` - await page.click(dateOnlyDropdownSelector) - await page.click(dateOnlytimezoneSelector) - await dateOnlyLocator.fill(expectedDateOnlyInput) - - // Fill in date and time - const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` - const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Auckland")` - await page.click(dropdownControlSelector) - await page.click(timezoneOptionSelector) - await dateTimeLocator.fill(expectedDateTimeInput) + // Verify the stored values are as expected + expect(existingDoc?.dayAndTimeWithTimezoneFixed).toEqual(expectedFixedUTCValue) + expect(existingDoc?.dayAndTimeWithTimezoneFixed_tz).toEqual(expectedFixedTimezoneValue) - await saveDocAndAssert(page) + await dateTimeLocator.fill(expectedUpdatedInput) - const docID = page.url().split('/').pop() + await saveDocAndAssert(page) - // eslint-disable-next-line payload/no-flaky-assertions - expect(docID).toBeTruthy() + const { + docs: [updatedDoc], + } = await payload.find({ + collection: dateFieldsSlug, + }) - const { - docs: [existingDoc], - } = await payload.find({ - collection: dateFieldsSlug, - where: { - id: { - equals: docID, - }, - }, + expect(updatedDoc?.dayAndTimeWithTimezoneFixed).toEqual(expectedUpdatedUTCValue) + expect(updatedDoc?.dayAndTimeWithTimezoneFixed_tz).toEqual(expectedFixedTimezoneValue) }) - // eslint-disable-next-line payload/no-flaky-assertions - expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedDateTimeUTCValue) - expect(existingDoc?.defaultWithTimezone).toEqual(expectedDateOnlyUTCValue) - }) + test('readonly field should be disabled and timezone should not be selectable', async () => { + const expectedReadOnlyUTCValue = '2027-08-12T01:00:00.000Z' + const expectedReadOnlyTimezoneValue = 'Asia/Tokyo' - test('creates the expected UTC value when the selected timezone is Auckland - with daylight savings', async () => { - // We send this value through the input - const expectedDateTimeInput = 'Jul 1, 2025 6:00 PM' - // The timestamp for this date should be normalised to 12PM local time - const expectedDateOnlyInput = '07/02/2025' // 2nd July 2025 - - // We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone) - // but the UTC date will be different from 6PM local time in the summer versus the winter - const expectedDateTimeUTCValue = '2025-07-01T06:00:00.000Z' - // The timestamp for this date should be normalised to 12PM local time - const expectedDateOnlyUTCValue = '2025-07-02T00:00:00.000Z' // 2nd July 2025 at 12PM in Auckland + const { + docs: [existingDoc], + } = await payload.find({ + collection: dateFieldsSlug, + }) - await page.goto(url.create) + await page.goto(url.edit(existingDoc!.id)) - // Default date field - filling it because it's required for the form to be valid - const dateField = page.locator('#field-default input') - await dateField.fill('01/01/2025') - - // Date input fields - const dateTimeLocator = page.locator( - '#field-dayAndTimeWithTimezone .react-datepicker-wrapper input', - ) - const dateOnlyLocator = page.locator( - '#field-defaultWithTimezone .react-datepicker-wrapper input', - ) - - // Fill in date only - const dateOnlyDropdownSelector = `#field-defaultWithTimezone .rs__control` - const dateOnlytimezoneSelector = `#field-defaultWithTimezone .rs__menu .rs__option:has-text("Auckland")` - await page.click(dateOnlyDropdownSelector) - await page.click(dateOnlytimezoneSelector) - await dateOnlyLocator.fill(expectedDateOnlyInput) - - // Fill in date and time - const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control` - const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Auckland")` - await page.click(dropdownControlSelector) - await page.click(timezoneOptionSelector) - await dateTimeLocator.fill(expectedDateTimeInput) + const dateTimeLocator = page.locator( + '#field-dayAndTimeWithTimezoneReadOnly .date-time-picker input', + ) - await saveDocAndAssert(page) + await expect(dateTimeLocator).toBeDisabled() - const docID = page.url().split('/').pop() + const dateFieldWrapper = page.locator('.date-time-field').filter({ has: dateTimeLocator }) + const dropdownInput = dateFieldWrapper.locator('.timezone-picker .rs__input') + const dropdownValue = dateFieldWrapper.locator('.timezone-picker .rs__value-container') - // eslint-disable-next-line payload/no-flaky-assertions - expect(docID).toBeTruthy() + await expect(dropdownInput).toBeDisabled() + await expect(dropdownValue).toContainText('Tokyo') - const { - docs: [existingDoc], - } = await payload.find({ - collection: dateFieldsSlug, - where: { - id: { - equals: docID, - }, - }, + // Verify the stored values are as expected + expect(existingDoc?.dayAndTimeWithTimezoneReadOnly).toEqual(expectedReadOnlyUTCValue) + expect(existingDoc?.dayAndTimeWithTimezoneReadOnly_tz).toEqual(expectedReadOnlyTimezoneValue) }) - - // eslint-disable-next-line payload/no-flaky-assertions - expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedDateTimeUTCValue) - expect(existingDoc?.defaultWithTimezone).toEqual(expectedDateOnlyUTCValue) }) -}) +} + +// Create timezone context test suites for different timezones +createTimezoneContextTests('America/Detroit', detroitTimezone) +createTimezoneContextTests('Europe/London', londonTimezone) +createTimezoneContextTests('Pacific/Auckland', aucklandTimezone) diff --git a/test/fields/collections/Date/index.ts b/test/fields/collections/Date/index.ts index a5f94152e28..c00b9c34ea2 100644 --- a/test/fields/collections/Date/index.ts +++ b/test/fields/collections/Date/index.ts @@ -8,6 +8,13 @@ const DateFields: CollectionConfig = { slug: dateFieldsSlug, admin: { useAsTitle: 'default', + defaultColumns: [ + 'default', + 'timeOnly', + 'dayAndTimeWithTimezone', + 'timezoneGroup.dayAndTime', + 'dayAndTimeWithTimezoneFixed', + ], }, fields: [ { @@ -88,6 +95,43 @@ const DateFields: CollectionConfig = { }, timezone: true, }, + { + name: 'dayAndTimeWithTimezoneFixed', + type: 'date', + admin: { + date: { + pickerAppearance: 'dayAndTime', + }, + }, + timezone: { + defaultTimezone: 'Europe/London', + supportedTimezones: [{ label: 'London', value: 'Europe/London' }], + }, + }, + { + name: 'dayAndTimeWithTimezoneRequired', + type: 'date', + admin: { + date: { + pickerAppearance: 'dayAndTime', + }, + }, + timezone: { + defaultTimezone: 'America/New_York', + required: true, + }, + }, + { + name: 'dayAndTimeWithTimezoneReadOnly', + type: 'date', + admin: { + date: { + pickerAppearance: 'dayAndTime', + }, + readOnly: true, + }, + timezone: true, + }, { type: 'blocks', name: 'timezoneBlocks', @@ -125,6 +169,22 @@ const DateFields: CollectionConfig = { }, ], }, + { + type: 'group', + name: 'timezoneGroup', + fields: [ + { + name: 'dayAndTime', + type: 'date', + admin: { + date: { + pickerAppearance: 'dayAndTime', + }, + }, + timezone: true, + }, + ], + }, { type: 'array', name: 'array', diff --git a/test/fields/collections/Date/shared.ts b/test/fields/collections/Date/shared.ts index 9e196c253c1..a703baf0cb8 100644 --- a/test/fields/collections/Date/shared.ts +++ b/test/fields/collections/Date/shared.ts @@ -24,4 +24,11 @@ export const dateDoc: Partial = { dayAndTime_tz: 'Europe/Berlin', }, ], + timezoneGroup: { + dayAndTime: '2025-01-31T09:00:00.000Z', + dayAndTime_tz: 'Europe/Berlin', + }, + dayAndTimeWithTimezoneReadOnly: '2027-08-12T01:00:00.000+00:00', + dayAndTimeWithTimezoneReadOnly_tz: 'Asia/Tokyo', + dayAndTimeWithTimezoneFixed: '2025-10-29T20:00:00.000+00:00', } diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index 5c51032358a..bf65e089c26 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -994,6 +994,12 @@ export interface DateField { */ dayAndTimeWithTimezone: string; dayAndTimeWithTimezone_tz: SupportedTimezones; + dayAndTimeWithTimezoneFixed?: string | null; + dayAndTimeWithTimezoneFixed_tz?: SupportedTimezones; + dayAndTimeWithTimezoneRequired?: string | null; + dayAndTimeWithTimezoneRequired_tz: SupportedTimezones; + dayAndTimeWithTimezoneReadOnly?: string | null; + dayAndTimeWithTimezoneReadOnly_tz?: SupportedTimezones; timezoneBlocks?: | { dayAndTime?: string | null; @@ -1010,6 +1016,10 @@ export interface DateField { id?: string | null; }[] | null; + timezoneGroup?: { + dayAndTime?: string | null; + dayAndTime_tz?: SupportedTimezones; + }; array?: | { date?: string | null; @@ -2715,6 +2725,12 @@ export interface DateFieldsSelect { defaultWithTimezone_tz?: T; dayAndTimeWithTimezone?: T; dayAndTimeWithTimezone_tz?: T; + dayAndTimeWithTimezoneFixed?: T; + dayAndTimeWithTimezoneFixed_tz?: T; + dayAndTimeWithTimezoneRequired?: T; + dayAndTimeWithTimezoneRequired_tz?: T; + dayAndTimeWithTimezoneReadOnly?: T; + dayAndTimeWithTimezoneReadOnly_tz?: T; timezoneBlocks?: | T | { @@ -2734,6 +2750,12 @@ export interface DateFieldsSelect { dayAndTime_tz?: T; id?: T; }; + timezoneGroup?: + | T + | { + dayAndTime?: T; + dayAndTime_tz?: T; + }; array?: | T | {