From b02358032125e90f61702aff7f2a6923d349baea Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 19 Jun 2026 11:09:32 -0700 Subject: [PATCH] feat(clock): add java.time.Instant overloads to the Clock API Add Instant overloads to Clock.pauseAt/setFixedTime/setSystemTime and InstallOptions.setTime alongside the existing java.util.Date ones. The API generator synthesizes an Instant overload for Date-typed unions (Clock only); the Date/Instant impls delegate to the canonical millis-based overload. References https://github.com/microsoft/playwright-java/issues/1686 --- .../java/com/microsoft/playwright/Clock.java | 74 +++++++++++++++++++ .../microsoft/playwright/impl/ClockImpl.java | 30 +++++--- .../microsoft/playwright/TestPageClock.java | 22 ++++++ .../playwright/tools/ApiGenerator.java | 17 ++++- 4 files changed, 131 insertions(+), 12 deletions(-) diff --git a/playwright/src/main/java/com/microsoft/playwright/Clock.java b/playwright/src/main/java/com/microsoft/playwright/Clock.java index bab0a7f65..9641c055b 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Clock.java +++ b/playwright/src/main/java/com/microsoft/playwright/Clock.java @@ -17,6 +17,7 @@ package com.microsoft.playwright; import java.util.Date; +import java.time.Instant; /** * Accurately simulating time-dependent behavior is essential for verifying the correctness of applications. Learn more @@ -53,6 +54,13 @@ public InstallOptions setTime(Date time) { this.time = time; return this; } + /** + * Time to initialize with, current system time by default. + */ + public InstallOptions setTime(Instant time) { + this.time = time; + return this; + } } /** * Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user closing the @@ -257,6 +265,39 @@ default void install() { * @since v1.45 */ void pauseAt(Date time); + /** + * Advance the clock by jumping forward in time and pause the time. Once this method is called, no timers are fired unless + * {@link com.microsoft.playwright.Clock#runFor Clock.runFor()}, {@link com.microsoft.playwright.Clock#fastForward + * Clock.fastForward()}, {@link com.microsoft.playwright.Clock#pauseAt Clock.pauseAt()} or {@link + * com.microsoft.playwright.Clock#resume Clock.resume()} is called. + * + *

Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and reopening it at + * the specified time and pausing. + * + *

Usage + *

{@code
+   * SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd");
+   * page.clock().pauseAt(format.parse("2020-02-02"));
+   * page.clock().pauseAt("2020-02-02");
+   * }
+ * + *

For best results, install the clock before navigating the page and set it to a time slightly before the intended test + * time. This ensures that all timers run normally during page loading, preventing the page from getting stuck. Once the + * page has fully loaded, you can safely use {@link com.microsoft.playwright.Clock#pauseAt Clock.pauseAt()} to pause the + * clock. + *

{@code
+   * // Initialize clock with some time before the test time and let the page load
+   * // naturally. `Date.now` will progress as the timers fire.
+   * SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
+   * page.clock().install(new Clock.InstallOptions().setTime(format.parse("2024-12-10T08:00:00")));
+   * page.navigate("http://localhost:3333");
+   * page.clock().pauseAt(format.parse("2024-12-10T10:00:00"));
+   * }
+ * + * @param time Time to pause at. + * @since v1.45 + */ + void pauseAt(Instant time); /** * Resumes timers. Once this method is called, time resumes flowing, timers are fired as usual. * @@ -317,6 +358,24 @@ default void install() { * @since v1.45 */ void setFixedTime(Date time); + /** + * Makes {@code Date.now} and {@code new Date()} return fixed fake time at all times, keeps all the timers running. + * + *

Use this method for simple scenarios where you only need to test with a predefined time. For more advanced scenarios, + * use {@link com.microsoft.playwright.Clock#install Clock.install()} instead. Read docs on clock emulation to learn more. + * + *

Usage + *

{@code
+   * page.clock().setFixedTime(new Date());
+   * page.clock().setFixedTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
+   * page.clock().setFixedTime("2020-02-02");
+   * }
+ * + * @param time Time to be set in milliseconds. + * @since v1.45 + */ + void setFixedTime(Instant time); /** * Sets system time, but does not trigger any timers. Use this to test how the web page reacts to a time shift, for example * switching from summer to winter time, or changing time zones. @@ -362,5 +421,20 @@ default void install() { * @since v1.45 */ void setSystemTime(Date time); + /** + * Sets system time, but does not trigger any timers. Use this to test how the web page reacts to a time shift, for example + * switching from summer to winter time, or changing time zones. + * + *

Usage + *

{@code
+   * page.clock().setSystemTime(new Date());
+   * page.clock().setSystemTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
+   * page.clock().setSystemTime("2020-02-02");
+   * }
+ * + * @param time Time to be set in milliseconds. + * @since v1.45 + */ + void setSystemTime(Instant time); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/ClockImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/ClockImpl.java index 67d506cd3..4bc8687db 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/ClockImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ClockImpl.java @@ -3,6 +3,7 @@ import com.google.gson.JsonObject; import com.microsoft.playwright.Clock; +import java.time.Instant; import java.util.Date; import static com.microsoft.playwright.impl.ChannelOwner.NO_TIMEOUT; @@ -72,9 +73,12 @@ public void pauseAt(String time) { @Override public void pauseAt(Date time) { - JsonObject params = new JsonObject(); - params.addProperty("timeNumber", time.getTime()); - sendMessageWithLogging("pauseAt", params); + pauseAt(time.getTime()); + } + + @Override + public void pauseAt(Instant time) { + pauseAt(time.toEpochMilli()); } @Override @@ -98,9 +102,12 @@ public void setFixedTime(String time) { @Override public void setFixedTime(Date time) { - JsonObject params = new JsonObject(); - params.addProperty("timeNumber", time.getTime()); - sendMessageWithLogging("setFixedTime", params); + setFixedTime(time.getTime()); + } + + @Override + public void setFixedTime(Instant time) { + setFixedTime(time.toEpochMilli()); } @Override @@ -119,9 +126,12 @@ public void setSystemTime(String time) { @Override public void setSystemTime(Date time) { - JsonObject params = new JsonObject(); - params.addProperty("timeNumber", time.getTime()); - sendMessageWithLogging("setSystemTime", params); + setSystemTime(time.getTime()); + } + + @Override + public void setSystemTime(Instant time) { + setSystemTime(time.toEpochMilli()); } private static void parseTime(Object time, JsonObject params) { @@ -129,6 +139,8 @@ private static void parseTime(Object time, JsonObject params) { params.addProperty("timeNumber", (Long) time); } else if (time instanceof Date) { params.addProperty("timeNumber", ((Date) time).getTime()); + } else if (time instanceof Instant) { + params.addProperty("timeNumber", ((Instant) time).toEpochMilli()); } else if (time instanceof String) { params.addProperty("timeString", (String) time); } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageClock.java b/playwright/src/test/java/com/microsoft/playwright/TestPageClock.java index d00d365d6..719b35f28 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageClock.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageClock.java @@ -9,6 +9,9 @@ import java.io.Writer; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Date; @@ -350,6 +353,25 @@ void setFixedTimeAllowsInstallingFakeTimersAfterSettingTime(Page page) { assertEquals(200, ((Object[]) calls.get(0))[0]); } + @Test + void acceptsInstant(Page page) { + // A LocalDateTime has no time zone, so convert it to an Instant at an explicit offset. + page.clock().install(new Clock.InstallOptions().setTime( + LocalDateTime.of(2024, 12, 10, 8, 0, 0).toInstant(ZoneOffset.UTC))); + + Instant paused = LocalDateTime.of(2024, 12, 10, 10, 0, 0).toInstant(ZoneOffset.UTC); + page.clock().pauseAt(paused); + assertEquals(paused.toEpochMilli(), ((Number) page.evaluate("() => Date.now()")).longValue()); + + Instant system = LocalDateTime.of(2024, 12, 10, 12, 0, 0).toInstant(ZoneOffset.UTC); + page.clock().setSystemTime(system); + assertEquals(system.toEpochMilli(), ((Number) page.evaluate("() => Date.now()")).longValue()); + + Instant fixed = LocalDateTime.of(2024, 12, 10, 14, 0, 0).toInstant(ZoneOffset.UTC); + page.clock().setFixedTime(fixed); + assertEquals(fixed.toEpochMilli(), ((Number) page.evaluate("() => Date.now()")).longValue()); + } + @Test void whileRunningShouldProgressTime(Page page) { page.clock().install(new Clock.InstallOptions().setTime(0)); diff --git a/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java b/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java index a41df3f87..48f86f81e 100644 --- a/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java +++ b/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java @@ -402,13 +402,23 @@ private List supportedUnionTypes() { return result; } + // The Clock time parameters are a union of number|string|Date. java.util.Date is a legacy type, + // so for Date-typed unions we additionally generate a java.time.Instant overload. + private static boolean hasDate(List unionTypes) { + return unionTypes.stream().anyMatch(o -> "Date".equals(o.get("name").getAsString())); + } + int unionSize() { - return supportedUnionTypes().size(); + List types = supportedUnionTypes(); + return types.size() + (hasDate(types) ? 1 : 0); } String formatTypeFromUnion(int i) { - JsonElement overloadedType = supportedUnionTypes().get(i); - return convertBuiltinType(overloadedType.getAsJsonObject()); + List types = supportedUnionTypes(); + if (i == types.size() && hasDate(types)) { + return "Instant"; + } + return convertBuiltinType(types.get(i)); } boolean isNullable() { @@ -1012,6 +1022,7 @@ void writeTo(List output, String offset) { } if ("Clock".equals(jsonName)) { output.add("import java.util.Date;"); + output.add("import java.time.Instant;"); } if (asList("Page", "Frame", "ElementHandle", "Locator", "LocatorAssertions", "APIRequest", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright", "Debugger", "Screencast", "WebSocketRoute", "Credentials", "WebStorage").contains(jsonName)) { output.add("import java.util.*;");