From 9e03ba41a6d8918ee6f7043ee08d08d855e99134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Kondratiuk?= Date: Mon, 5 Oct 2026 21:35:22 -0300 Subject: [PATCH] Make Temporal.Now follow page.clock Port upstream microsoft/playwright@0facd9234 (#42838) into the bundled clock script and add the four Temporal.Now tests from page-clock.spec.ts. Co-Authored-By: Claude Opus 5.5 --- .../LibraryPageClockParityTests.cs | 102 ++++++++++++++++++ src/PlaywrightNative/Helpers/clockSource.js | 28 ++++- 2 files changed, 128 insertions(+), 2 deletions(-) diff --git a/src/PlaywrightNative.Tests/LibraryPageClockParityTests.cs b/src/PlaywrightNative.Tests/LibraryPageClockParityTests.cs index 208b9404..d199b552 100644 --- a/src/PlaywrightNative.Tests/LibraryPageClockParityTests.cs +++ b/src/PlaywrightNative.Tests/LibraryPageClockParityTests.cs @@ -727,6 +727,100 @@ public async Task CheckDateNowIsAnInteger2() Assert.That(await _page.EvaluateAsync("Date.now()").ConfigureAwait(false), Is.EqualTo(1001)); } + [PlaywrightTest("page-clock.spec.ts", "should follow fixed time")] + [Test] + [Timeout(TestConstants.DefaultTestTimeout)] + public async Task ShouldFollowFixedTime() + { + await SkipUnlessTemporalAsync().ConfigureAwait(false); + await _page.Clock.SetFixedTimeAsync(new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc)).ConfigureAwait(false); + await _page.GoToAsync("data:text/html,").ConfigureAwait(false); + Dictionary result = await _page.EvaluateAsync>( + @"() => { + const Temporal = globalThis.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(), + }; + }").ConfigureAwait(false); + Assert.That(result, Is.EqualTo(new Dictionary + { + ["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", + })); + } + + [PlaywrightTest("page-clock.spec.ts", "should advance with the clock")] + [Test] + [Timeout(TestConstants.DefaultTestTimeout)] + public async Task ShouldAdvanceWithTheClock() + { + await SkipUnlessTemporalAsync().ConfigureAwait(false); + await _page.Clock.InstallAsync(0).ConfigureAwait(false); + await _page.GoToAsync("data:text/html,").ConfigureAwait(false); + await _page.Clock.PauseAtAsync(1000).ConfigureAwait(false); + await _page.Clock.RunForAsync(2000).ConfigureAwait(false); + Assert.That(await _page.EvaluateAsync("() => globalThis.Temporal.Now.instant().epochMilliseconds").ConfigureAwait(false), Is.EqualTo(3000)); + } + + [PlaywrightTest("page-clock.spec.ts", "should use the system time zone by default")] + [Test] + [Timeout(TestConstants.DefaultTestTimeout)] + public async Task ShouldUseTheSystemTimeZoneByDefault() + { + await SkipUnlessTemporalAsync().ConfigureAwait(false); + IBrowserContext context = await _browser.NewContextAsync(new BrowserNewContextOptions { TimezoneId = "America/Los_Angeles" }).ConfigureAwait(false); + IPage page = await context.NewPageAsync().ConfigureAwait(false); + await page.Clock.SetFixedTimeAsync(new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc)).ConfigureAwait(false); + await page.GoToAsync("data:text/html,").ConfigureAwait(false); + Dictionary result = await page.EvaluateAsync>( + @"() => { + const Temporal = globalThis.Temporal; + return { + timeZoneId: Temporal.Now.timeZoneId(), + zonedDateTime: Temporal.Now.zonedDateTimeISO().toString(), + plainDateTime: Temporal.Now.plainDateTimeISO().toString(), + }; + }").ConfigureAwait(false); + Assert.That(result, Is.EqualTo(new Dictionary + { + ["timeZoneId"] = "America/Los_Angeles", + ["zonedDateTime"] = "2019-12-31T16:00:00-08:00[America/Los_Angeles]", + ["plainDateTime"] = "2019-12-31T16:00:00", + })); + await context.CloseAsync().ConfigureAwait(false); + } + + [PlaywrightTest("page-clock.spec.ts", "should keep the rest of Temporal intact")] + [Test] + [Timeout(TestConstants.DefaultTestTimeout)] + public async Task ShouldKeepTheRestOfTemporalIntact() + { + await SkipUnlessTemporalAsync().ConfigureAwait(false); + await _page.Clock.InstallAsync(0).ConfigureAwait(false); + await _page.GoToAsync("data:text/html,").ConfigureAwait(false); + JsonElement result = await _page.EvaluateAsync( + @"() => { + const Temporal = globalThis.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(), + }; + }").ConfigureAwait(false); + Assert.That(result.GetProperty("tag").GetString(), Is.EqualTo("[object Temporal.Now]")); + Assert.That(result.GetProperty("enumerable").GetArrayLength(), Is.Zero); + Assert.That(result.GetProperty("isInstant").GetBoolean(), Is.True); + Assert.That(result.GetProperty("duration").GetString(), Is.EqualTo("PT1H")); + } + [PlaywrightTest("page-clock.spec.ts", "AbortSignal.timeout")] [Test] [Timeout(TestConstants.DefaultTestTimeout)] @@ -801,6 +895,14 @@ private async Task InstallPausedAsync() await _page.Clock.PauseAtAsync(1000).ConfigureAwait(false); } + private async Task SkipUnlessTemporalAsync() + { + if (!await _page.EvaluateAsync("() => 'Temporal' in globalThis").ConfigureAwait(false)) + { + Assert.Ignore("Temporal is not supported"); + } + } + private long[] CallNumbers() { long[] values = new long[_calls.Count]; diff --git a/src/PlaywrightNative/Helpers/clockSource.js b/src/PlaywrightNative/Helpers/clockSource.js index 9ded7f69..f2357bc0 100644 --- a/src/PlaywrightNative/Helpers/clockSource.js +++ b/src/PlaywrightNative/Helpers/clockSource.js @@ -448,6 +448,24 @@ function createIntl(clock, NativeIntl) { ClockIntl.DateTimeFormat.supportedLocalesOf = NativeIntl.DateTimeFormat.supportedLocalesOf; return ClockIntl; } +function createTemporal(clock, NativeTemporal) { + const NativeNow = NativeTemporal.Now; + const instant = () => NativeTemporal.Instant.fromEpochMilliseconds(Math.trunc(clock.now())); + const zonedDateTimeISO = (timeZone) => instant().toZonedDateTimeISO(timeZone != null ? timeZone : NativeNow.timeZoneId()); + const overrides = { + instant, + zonedDateTimeISO, + plainDateTimeISO: (timeZone) => zonedDateTimeISO(timeZone).toPlainDateTime(), + plainDateISO: (timeZone) => zonedDateTimeISO(timeZone).toPlainDate(), + plainTimeISO: (timeZone) => zonedDateTimeISO(timeZone).toPlainTime() + }; + 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, b) { if (a.callAt < b.callAt) return -1; @@ -481,7 +499,8 @@ function platformOriginals(globalObject) { Date: globalObject.Date, performance: globalObject.performance, Intl: globalObject.Intl, - AbortSignal: globalObject.AbortSignal + AbortSignal: globalObject.AbortSignal, + Temporal: globalObject.Temporal }; const bound = { ...raw }; for (const key of Object.keys(bound)) { @@ -551,7 +570,8 @@ function createApi(clock, originals, browserName) { Intl: originals.Intl ? createIntl(clock, originals.Intl) : void 0, Date: createDate(clock, originals.Date), performance: originals.performance ? fakePerformance(clock, originals.performance) : void 0, - AbortSignal: originals.AbortSignal ? fakeAbortSignal(clock, originals.AbortSignal, browserName) : void 0 + AbortSignal: originals.AbortSignal ? fakeAbortSignal(clock, originals.AbortSignal, browserName) : void 0, + Temporal: originals.Temporal ? createTemporal(clock, originals.Temporal) : void 0 }; } function getClearHandler(type) { @@ -653,12 +673,16 @@ function install(globalObject, config = {}) { const { clock, api, originals } = createClock(globalObject, config); const toFake = ((_b = config.toFake) == null ? void 0 : _b.length) ? config.toFake : Object.keys(originals); for (const method of toFake) { + if (method === "Temporal" && !api.Temporal) + continue; if (method === "Date") { globalObject.Date = mirrorDateProperties(api.Date, globalObject.Date); } else if (method === "Intl") { globalObject.Intl = api[method]; } else if (method === "AbortSignal") { globalObject.AbortSignal = api[method]; + } else if (method === "Temporal") { + globalObject.Temporal = api[method]; } else if (method === "performance") { globalObject.performance = api[method]; const kEventTimeStamp = Symbol("playwrightEventTimeStamp");