diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6e2ce48..fb6f3225 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ defaults: env: TZ: 'America/Phoenix' + LOCALE_ID: 'en-US' jobs: install-and-build: diff --git a/packages/xpath/test/helpers.ts b/packages/xpath/test/helpers.ts index 9ce0427f..7c9bd13b 100644 --- a/packages/xpath/test/helpers.ts +++ b/packages/xpath/test/helpers.ts @@ -10,14 +10,33 @@ type AnyParentNode = | XMLDocument; declare global { + /** + * The timezone identifier used for all date and time operations in tests. + * This string follows the IANA Time Zone Database format (e.g., 'America/Phoenix'). + * It determines the offset and DST behavior for `Date` objects and + * related functions. + * + * @example 'America/Phoenix' // Fixed UTC-7, no DST + * @example 'Europe/London' // UTC+0 (GMT) or UTC+1 (BST) with DST + */ // eslint-disable-next-line no-var var TZ: string | undefined; // eslint-disable-next-line no-var + var LOCALE_ID: string | undefined; + /** + * The locale string defining the language and regional formatting for tests. + * This follows the BCP 47 language tag format (e.g., 'en-US'). It ensures consistent formatting + * across tests. + * + * @example 'en-US' // American English + */ + // eslint-disable-next-line no-var var IMPLEMENTATION: string | undefined; } globalThis.IMPLEMENTATION = typeof IMPLEMENTATION === 'string' ? IMPLEMENTATION : undefined; globalThis.TZ = typeof TZ === 'string' ? TZ : undefined; +globalThis.LOCALE_ID = typeof LOCALE_ID === 'string' ? LOCALE_ID : undefined; const namespaces: Record = { xhtml: 'http://www.w3.org/1999/xhtml', @@ -338,3 +357,7 @@ export const getNonNamespaceAttributes = (element: Element): readonly Attr[] => return attrs.filter(({ name }) => name !== 'xmlns' && !name.startsWith('xmlns:')); }; + +export const getDefaultDateTimeLocale = (): string => { + return new Date().toLocaleString(LOCALE_ID, { timeZone: TZ }); +}; diff --git a/packages/xpath/test/setup.ts b/packages/xpath/test/setup.ts new file mode 100644 index 00000000..b4ce741b --- /dev/null +++ b/packages/xpath/test/setup.ts @@ -0,0 +1,14 @@ +import { afterEach, beforeEach } from 'vitest'; +import { vi } from 'vitest'; +import { getDefaultDateTimeLocale } from './helpers.ts'; + +beforeEach(() => { + const dateOnTimezone = getDefaultDateTimeLocale(); + vi.useFakeTimers({ + now: new Date(dateOnTimezone).getTime(), + }); +}); + +afterEach(() => { + vi.useRealTimers(); +}); diff --git a/packages/xpath/test/xforms/now.test.ts b/packages/xpath/test/xforms/now.test.ts index ccc251f1..3454c064 100644 --- a/packages/xpath/test/xforms/now.test.ts +++ b/packages/xpath/test/xforms/now.test.ts @@ -14,8 +14,6 @@ describe('#now()', () => { // > including timezone offset (i.e. not normalized to UTC) as described // > under the dateTime datatype. it('should return a timestamp for this instant', () => { - // this check might fail if run at precisely midnight ;-) - // given const now = new Date(); const today = `${now.getFullYear()}-${(1 + now.getMonth()).toString().padStart(2, '0')}-${now diff --git a/packages/xpath/vite.config.ts b/packages/xpath/vite.config.ts index bc0aa8f2..bbd636b8 100644 --- a/packages/xpath/vite.config.ts +++ b/packages/xpath/vite.config.ts @@ -44,13 +44,26 @@ const TEST_ENVIRONMENT = BROWSER_ENABLED ? 'node' : 'jsdom'; */ const TEST_TIME_ZONE = 'America/Phoenix'; +/** + * The locale used for formatting dates and times in all test cases. + * This ensures consistent language and regional settings across tests. + * Currently set to 'en-US' (American English), which affects date formats + * (e.g., MM/DD/YYYY) and time separators. + * + * @constant + * @default 'en-US' + */ +const TEST_LOCALE = 'en-US'; + export default defineConfig(({ mode }) => { const isTest = mode === 'test'; let timeZoneId: string | null = process.env.TZ ?? null; + let localeId: string | null = process.env.LOCALE_ID ?? null; if (isTest) { timeZoneId = timeZoneId ?? TEST_TIME_ZONE; + localeId = localeId ?? TEST_LOCALE; } // `expressionParser.ts` is built as a separate entry so it can be consumed @@ -83,6 +96,7 @@ export default defineConfig(({ mode }) => { }, define: { TZ: JSON.stringify(timeZoneId), + LOCALE_ID: JSON.stringify(localeId), }, esbuild: { target: 'esnext', @@ -122,7 +136,7 @@ export default defineConfig(({ mode }) => { headless: true, screenshotFailures: false, }, - + setupFiles: ['test/setup.ts'], environment: TEST_ENVIRONMENT, globals: false, include: ['test/**/*.test.ts'],