From 6cb7f866977625690d4a5550511cd5927e8ac8f2 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 21 Sep 2026 12:19:38 -0700 Subject: [PATCH 1/2] fix(clock): make Temporal.Now follow page.clock Fixes: https://github.com/microsoft/playwright/issues/42830 --- docs/src/api/class-clock.md | 1 + docs/src/clock.md | 1 + eslint.config.mjs | 1 + packages/injected/src/clock.ts | 27 ++++++++ packages/injected/src/utilityScript.ts | 2 + packages/playwright-client/types/types.d.ts | 1 + packages/playwright-core/types/types.d.ts | 1 + tests/library/page-clock.spec.ts | 74 +++++++++++++++++++++ 8 files changed, 108 insertions(+) diff --git a/docs/src/api/class-clock.md b/docs/src/api/class-clock.md index 233531f2db17d..1c82f32e80900 100644 --- a/docs/src/api/class-clock.md +++ b/docs/src/api/class-clock.md @@ -51,6 +51,7 @@ Time may be the number of milliseconds to advance the clock by or a human-readab Install fake implementations for the following time-related functions: * `Date` +* `Temporal.Now` * `setTimeout` * `clearTimeout` * `setInterval` diff --git a/docs/src/clock.md b/docs/src/clock.md index c6f753fcf0665..6c4114577ea9b 100644 --- a/docs/src/clock.md +++ b/docs/src/clock.md @@ -22,6 +22,7 @@ The recommended approach is to use `setFixedTime` to set the time to a specific :::note [`property: Page.clock`] overrides native global classes and functions related to time allowing them to be manually controlled: - `Date` + - `Temporal.Now` - `setTimeout` - `clearTimeout` - `setInterval` diff --git a/eslint.config.mjs b/eslint.config.mjs index d679710ebe26a..0f1dfe15522ee 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -238,6 +238,7 @@ const noWebGlobalsRuleList = [ { name: "cancelIdleCallback", message: "Use InjectedScript.utils.builtins.cancelIdleCallback instead" }, { name: "Date", message: "Use InjectedScript.utils.builtins.Date instead" }, { name: "Intl", message: "Use InjectedScript.utils.builtins.Intl instead" }, + { name: "Temporal", message: "Use InjectedScript.utils.builtins.Temporal instead" }, { name: "performance", message: "Use InjectedScript.utils.builtins.performance instead" }, ]; diff --git a/packages/injected/src/clock.ts b/packages/injected/src/clock.ts index 69fe461163b8f..3422555da6e10 100644 --- a/packages/injected/src/clock.ts +++ b/packages/injected/src/clock.ts @@ -578,6 +578,27 @@ function createIntl(clock: ClockController, NativeIntl: Builtins['Intl']): Built return ClockIntl; } +function createTemporal(clock: ClockController, NativeTemporal: NonNullable): Builtins['Temporal'] { + const NativeNow = NativeTemporal.Now; + const instant = () => NativeTemporal.Instant.fromEpochMilliseconds(Math.trunc(clock.now())); + const zonedDateTimeISO = (timeZone?: Temporal.TimeZoneLike) => instant().toZonedDateTimeISO(timeZone ?? NativeNow.timeZoneId()); + const overrides: Partial = { + instant, + zonedDateTimeISO, + plainDateTimeISO: timeZone => zonedDateTimeISO(timeZone).toPlainDateTime(), + plainDateISO: timeZone => zonedDateTimeISO(timeZone).toPlainDate(), + plainTimeISO: timeZone => zonedDateTimeISO(timeZone).toPlainTime(), + }; + + // All properties of Temporal are non-enumerable, mirror them with their descriptors. + const ClockNow = Object.create(Object.getPrototypeOf(NativeNow), Object.getOwnPropertyDescriptors(NativeNow)); + for (const [key, value] of Object.entries(overrides)) + Object.defineProperty(ClockNow, key, { value, writable: true, enumerable: false, configurable: true }); + const ClockTemporal = Object.create(Object.getPrototypeOf(NativeTemporal), Object.getOwnPropertyDescriptors(NativeTemporal)); + Object.defineProperty(ClockTemporal, 'Now', { value: ClockNow, writable: true, enumerable: false, configurable: true }); + return ClockTemporal; +} + function compareTimers(a: Timer, b: Timer) { // Sort first by absolute timing if (a.callAt < b.callAt) @@ -623,6 +644,7 @@ function platformOriginals(globalObject: WindowOrWorkerGlobalScope): { raw: Buil performance: globalObject.performance, Intl: (globalObject as any).Intl, AbortSignal: (globalObject as any).AbortSignal, + Temporal: (globalObject as any).Temporal, }; const bound = { ...raw }; for (const key of Object.keys(bound) as (keyof Builtins)[]) { @@ -700,6 +722,7 @@ function createApi(clock: ClockController, originals: Builtins, browserName?: st Date: createDate(clock, originals.Date), performance: originals.performance ? fakePerformance(clock, originals.performance) : (undefined as unknown as Builtins['performance']), AbortSignal: originals.AbortSignal ? fakeAbortSignal(clock, originals.AbortSignal, browserName) : (undefined as unknown as Builtins['AbortSignal']), + Temporal: originals.Temporal ? createTemporal(clock, originals.Temporal) : undefined, }; } @@ -800,12 +823,16 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install const toFake = config.toFake?.length ? config.toFake : Object.keys(originals) as (keyof Builtins)[]; for (const method of toFake) { + if (method === 'Temporal' && !api.Temporal) + continue; if (method === 'Date') { (globalObject as any).Date = mirrorDateProperties(api.Date, (globalObject as any).Date); } else if (method === 'Intl') { (globalObject as any).Intl = api[method]!; } else if (method === 'AbortSignal') { (globalObject as any).AbortSignal = api[method]!; + } else if (method === 'Temporal') { + (globalObject as any).Temporal = api[method]!; } else if (method === 'performance') { (globalObject as any).performance = api[method]!; const kEventTimeStamp = Symbol('playwrightEventTimeStamp'); diff --git a/packages/injected/src/utilityScript.ts b/packages/injected/src/utilityScript.ts index e4cfe21b7ba73..d89782880cd05 100644 --- a/packages/injected/src/utilityScript.ts +++ b/packages/injected/src/utilityScript.ts @@ -30,6 +30,7 @@ export type Builtins = { Intl: typeof window['Intl'], Date: typeof window['Date'], AbortSignal: typeof window['AbortSignal'], + Temporal?: typeof Temporal, }; export class UtilityScript { @@ -57,6 +58,7 @@ export class UtilityScript { Intl: global.Intl, Date: global.Date, AbortSignal: global.AbortSignal, + Temporal: (global as any).Temporal, } satisfies Builtins; } if (this.isUnderTest) diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index cd297c4c18d49..6569067779bee 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -20604,6 +20604,7 @@ export interface Clock { /** * Install fake implementations for the following time-related functions: * - `Date` + * - `Temporal.Now` * - `setTimeout` * - `clearTimeout` * - `setInterval` diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index cd297c4c18d49..6569067779bee 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -20604,6 +20604,7 @@ export interface Clock { /** * Install fake implementations for the following time-related functions: * - `Date` + * - `Temporal.Now` * - `setTimeout` * - `clearTimeout` * - `setInterval` diff --git a/tests/library/page-clock.spec.ts b/tests/library/page-clock.spec.ts index 16f703e965d43..9550de2568e48 100644 --- a/tests/library/page-clock.spec.ts +++ b/tests/library/page-clock.spec.ts @@ -542,6 +542,80 @@ it.describe('Date.now', () => { }); }); +it.describe('Temporal.Now', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42830' } }, () => { + it.beforeEach(async ({ page }) => { + it.skip(!await page.evaluate(() => 'Temporal' in globalThis), 'Temporal is not supported'); + }); + + it('should follow fixed time', async ({ page }) => { + await page.clock.setFixedTime(new Date('2020-01-01T00:00:00Z')); + await page.goto('data:text/html,'); + expect(await page.evaluate(() => { + const Temporal = (globalThis as any).Temporal; + return { + instant: Temporal.Now.instant().toString(), + zonedDateTime: Temporal.Now.zonedDateTimeISO('UTC').toString(), + plainDateTime: Temporal.Now.plainDateTimeISO('UTC').toString(), + plainDate: Temporal.Now.plainDateISO('UTC').toString(), + plainTime: Temporal.Now.plainTimeISO('UTC').toString(), + }; + })).toEqual({ + instant: '2020-01-01T00:00:00Z', + zonedDateTime: '2020-01-01T00:00:00+00:00[UTC]', + plainDateTime: '2020-01-01T00:00:00', + plainDate: '2020-01-01', + plainTime: '00:00:00', + }); + }); + + it('should advance with the clock', async ({ page }) => { + await page.clock.install({ time: 0 }); + await page.goto('data:text/html,'); + await page.clock.pauseAt(1000); + await page.clock.runFor(2000); + expect(await page.evaluate(() => (globalThis as any).Temporal.Now.instant().epochMilliseconds)).toBe(3000); + }); + + it('should use the system time zone by default', async ({ browser }) => { + const context = await browser.newContext({ timezoneId: 'America/Los_Angeles' }); + const page = await context.newPage(); + await page.clock.setFixedTime(new Date('2020-01-01T00:00:00Z')); + await page.goto('data:text/html,'); + expect(await page.evaluate(() => { + const Temporal = (globalThis as any).Temporal; + return { + timeZoneId: Temporal.Now.timeZoneId(), + zonedDateTime: Temporal.Now.zonedDateTimeISO().toString(), + plainDateTime: Temporal.Now.plainDateTimeISO().toString(), + }; + })).toEqual({ + timeZoneId: 'America/Los_Angeles', + zonedDateTime: '2019-12-31T16:00:00-08:00[America/Los_Angeles]', + plainDateTime: '2019-12-31T16:00:00', + }); + await context.close(); + }); + + it('should keep the rest of Temporal intact', async ({ page }) => { + await page.clock.install({ time: 0 }); + await page.goto('data:text/html,'); + expect(await page.evaluate(() => { + const Temporal = (globalThis as any).Temporal; + return { + tag: Object.prototype.toString.call(Temporal.Now), + enumerable: Object.keys(Temporal).concat(Object.keys(Temporal.Now)), + isInstant: Temporal.Now.instant() instanceof Temporal.Instant, + duration: Temporal.Duration.from({ hours: 1 }).toString(), + }; + })).toEqual({ + tag: '[object Temporal.Now]', + enumerable: [], + isInstant: true, + duration: 'PT1H', + }); + }); +}); + it('AbortSignal.timeout', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/39293' } }, async ({ page, browserName }) => { await page.clock.install({ time: 0 }); const controller = await page.evaluateHandle(() => { From 2afc3ab6fd95ff66ba278fd8e85dbae3e128cee6 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 21 Sep 2026 14:11:34 -0700 Subject: [PATCH 2/2] chore(clock): declare Temporal builtin like the others --- packages/injected/src/clock.ts | 4 ++-- packages/injected/src/utilityScript.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/injected/src/clock.ts b/packages/injected/src/clock.ts index 3422555da6e10..b4cda8de57678 100644 --- a/packages/injected/src/clock.ts +++ b/packages/injected/src/clock.ts @@ -578,7 +578,7 @@ function createIntl(clock: ClockController, NativeIntl: Builtins['Intl']): Built return ClockIntl; } -function createTemporal(clock: ClockController, NativeTemporal: NonNullable): Builtins['Temporal'] { +function createTemporal(clock: ClockController, NativeTemporal: Builtins['Temporal']): Builtins['Temporal'] { const NativeNow = NativeTemporal.Now; const instant = () => NativeTemporal.Instant.fromEpochMilliseconds(Math.trunc(clock.now())); const zonedDateTimeISO = (timeZone?: Temporal.TimeZoneLike) => instant().toZonedDateTimeISO(timeZone ?? NativeNow.timeZoneId()); @@ -722,7 +722,7 @@ function createApi(clock: ClockController, originals: Builtins, browserName?: st Date: createDate(clock, originals.Date), performance: originals.performance ? fakePerformance(clock, originals.performance) : (undefined as unknown as Builtins['performance']), AbortSignal: originals.AbortSignal ? fakeAbortSignal(clock, originals.AbortSignal, browserName) : (undefined as unknown as Builtins['AbortSignal']), - Temporal: originals.Temporal ? createTemporal(clock, originals.Temporal) : undefined, + Temporal: originals.Temporal ? createTemporal(clock, originals.Temporal) : (undefined as unknown as Builtins['Temporal']), }; } diff --git a/packages/injected/src/utilityScript.ts b/packages/injected/src/utilityScript.ts index d89782880cd05..af98c76550a61 100644 --- a/packages/injected/src/utilityScript.ts +++ b/packages/injected/src/utilityScript.ts @@ -30,7 +30,7 @@ export type Builtins = { Intl: typeof window['Intl'], Date: typeof window['Date'], AbortSignal: typeof window['AbortSignal'], - Temporal?: typeof Temporal, + Temporal: typeof window['Temporal'], }; export class UtilityScript {