From 145e21ed871010d335a8458369ec5e1c49f9e9cf Mon Sep 17 00:00:00 2001 From: Fredrik Teschke Date: Mon, 23 Mar 2026 17:29:23 +0100 Subject: [PATCH 1/5] Add BrowserContext clock wrappers --- lib/playwright_ex/channels/browser_context.ex | 82 +++++++++++++++++++ .../browser_context_channel_test.exs | 82 +++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 test/playwright_ex/browser_context_channel_test.exs diff --git a/lib/playwright_ex/channels/browser_context.ex b/lib/playwright_ex/channels/browser_context.ex index 62bd4fc..3ddf45a 100644 --- a/lib/playwright_ex/channels/browser_context.ex +++ b/lib/playwright_ex/channels/browser_context.ex @@ -261,4 +261,86 @@ defmodule PlaywrightEx.BrowserContext do |> Connection.send(%{guid: context_id, method: :addInitScript, params: Map.new(opts)}, timeout) |> ChannelResponse.unwrap(& &1) end + + schema = + NimbleOptions.new!( + connection: PlaywrightEx.Channel.connection_opt(), + timeout: PlaywrightEx.Channel.timeout_opt(), + time: [ + type: {:or, [:non_neg_integer, :string]}, + required: false, + doc: "Optional base time to install, as milliseconds since epoch or a string accepted by Playwright." + ] + ) + + @doc """ + Installs Playwright's browser-context clock support. + + Reference: https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/client/clock.ts + + ## Options + #{NimbleOptions.docs(schema)} + """ + @schema schema + @type clock_install_opt :: unquote(NimbleOptions.option_typespec(schema)) + @spec clock_install(PlaywrightEx.guid(), [clock_install_opt() | PlaywrightEx.unknown_opt()]) :: + {:ok, any()} | {:error, any()} + def clock_install(context_id, opts \\ []) do + {connection, opts} = opts |> PlaywrightEx.Channel.validate_known!(@schema) |> Keyword.pop!(:connection) + {timeout, opts} = Keyword.pop!(opts, :timeout) + {time, opts} = Keyword.pop(opts, :time) + + params = + case time do + nil -> %{} + time when is_integer(time) -> %{time_number: time} + time when is_binary(time) -> %{time_string: time} + end + + connection + |> Connection.send(%{guid: context_id, method: :clock_install, params: Map.merge(params, Map.new(opts))}, timeout) + |> ChannelResponse.unwrap(& &1) + end + + schema = + NimbleOptions.new!( + connection: PlaywrightEx.Channel.connection_opt(), + timeout: PlaywrightEx.Channel.timeout_opt(), + ticks: [ + type: {:or, [:non_neg_integer, :string]}, + required: true, + doc: "Time to advance, in milliseconds or in Playwright's `mm:ss` / `hh:mm:ss` string format." + ] + ) + + @doc """ + Fast forwards the browser context clock. + + Reference: https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/client/clock.ts + + ## Options + #{NimbleOptions.docs(schema)} + """ + @schema schema + @type clock_fast_forward_opt :: unquote(NimbleOptions.option_typespec(schema)) + @spec clock_fast_forward(PlaywrightEx.guid(), [clock_fast_forward_opt() | PlaywrightEx.unknown_opt()]) :: + {:ok, any()} | {:error, any()} + def clock_fast_forward(context_id, opts \\ []) do + {connection, opts} = opts |> PlaywrightEx.Channel.validate_known!(@schema) |> Keyword.pop!(:connection) + {timeout, opts} = Keyword.pop!(opts, :timeout) + {ticks, opts} = Keyword.pop!(opts, :ticks) + + params = + case ticks do + ticks when is_integer(ticks) -> %{ticks_number: ticks} + ticks when is_binary(ticks) -> %{ticks_string: ticks} + end + + connection + |> Connection.send( + %{guid: context_id, method: :clock_fast_forward, params: Map.merge(params, Map.new(opts))}, + timeout + ) + |> ChannelResponse.unwrap(& &1) + end end diff --git a/test/playwright_ex/browser_context_channel_test.exs b/test/playwright_ex/browser_context_channel_test.exs new file mode 100644 index 0000000..40f86ac --- /dev/null +++ b/test/playwright_ex/browser_context_channel_test.exs @@ -0,0 +1,82 @@ +defmodule PlaywrightEx.BrowserContextChannelTest do + use ExUnit.Case, async: true + + alias PlaywrightEx.BrowserContext + + setup do + {:ok, connection} = start_supervised({__MODULE__.FakeConnection, self()}) + [connection: connection] + end + + describe "clock_install/2" do + test "sends clock_install without a time by default", %{connection: connection} do + assert {:ok, %{}} = BrowserContext.clock_install("context-guid", connection: connection, timeout: 123) + + assert_receive {:send, + %{ + guid: "context-guid", + method: :clock_install, + metadata: %{}, + params: %{timeout: 123} + }} + end + + test "sends clock_install with an explicit integer time", %{connection: connection} do + assert {:ok, %{}} = BrowserContext.clock_install("context-guid", time: 456, connection: connection, timeout: 123) + + assert_receive {:send, + %{ + guid: "context-guid", + method: :clock_install, + metadata: %{}, + params: %{time_number: 456, timeout: 123} + }} + end + end + + describe "clock_fast_forward/2" do + test "sends clock_fast_forward with integer ticks", %{connection: connection} do + assert {:ok, %{}} = + BrowserContext.clock_fast_forward("context-guid", ticks: 60_001, connection: connection, timeout: 123) + + assert_receive {:send, + %{ + guid: "context-guid", + method: :clock_fast_forward, + metadata: %{}, + params: %{ticks_number: 60_001, timeout: 123} + }} + end + + test "sends clock_fast_forward with string ticks", %{connection: connection} do + assert {:ok, %{}} = + BrowserContext.clock_fast_forward("context-guid", ticks: "01:00", connection: connection, timeout: 123) + + assert_receive {:send, + %{ + guid: "context-guid", + method: :clock_fast_forward, + metadata: %{}, + params: %{ticks_string: "01:00", timeout: 123} + }} + end + end + + defmodule FakeConnection do + @moduledoc false + use GenServer + + def start_link(test_pid) do + GenServer.start_link(__MODULE__, test_pid) + end + + @impl GenServer + def init(test_pid), do: {:ok, test_pid} + + @impl GenServer + def handle_call({:send, msg}, _from, test_pid) do + send(test_pid, {:send, msg}) + {:reply, %{result: %{}}, test_pid} + end + end +end From fcc9dd30a2c5a35edc497d4458882b6f36257741 Mon Sep 17 00:00:00 2001 From: Fredrik Teschke Date: Mon, 23 Mar 2026 17:38:54 +0100 Subject: [PATCH 2/5] Add browser context clock integration test --- test/playwright_ex/browser_context_test.exs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/playwright_ex/browser_context_test.exs b/test/playwright_ex/browser_context_test.exs index bc5071e..1d147f4 100644 --- a/test/playwright_ex/browser_context_test.exs +++ b/test/playwright_ex/browser_context_test.exs @@ -18,4 +18,17 @@ defmodule PlaywrightEx.BrowserContextTest do assert {:ok, "ok"} = eval(page.main_frame.guid, "() => window.__browser_context_add_init_script") end end + + describe "clock_fast_forward/2" do + test "advances Date.now after installing the clock", %{browser_context: browser_context, frame: frame} do + assert {:ok, _} = Frame.goto(frame.guid, url: "about:blank", timeout: @timeout) + assert {:ok, before_now} = eval(frame.guid, "() => Date.now()") + + assert {:ok, _} = BrowserContext.clock_install(browser_context.guid, timeout: @timeout) + assert {:ok, _} = BrowserContext.clock_fast_forward(browser_context.guid, ticks: 60_001, timeout: @timeout) + + assert {:ok, after_now} = eval(frame.guid, "() => Date.now()") + assert after_now >= before_now + 60_001 + end + end end From d1ac0c1547b581908d44bceeeeecb6da8c575804 Mon Sep 17 00:00:00 2001 From: Fredrik Teschke Date: Mon, 23 Mar 2026 18:16:08 +0100 Subject: [PATCH 3/5] Expand browser context clock integration coverage --- .../browser_context_channel_test.exs | 82 ------------------- test/playwright_ex/browser_context_test.exs | 27 +++++- 2 files changed, 25 insertions(+), 84 deletions(-) delete mode 100644 test/playwright_ex/browser_context_channel_test.exs diff --git a/test/playwright_ex/browser_context_channel_test.exs b/test/playwright_ex/browser_context_channel_test.exs deleted file mode 100644 index 40f86ac..0000000 --- a/test/playwright_ex/browser_context_channel_test.exs +++ /dev/null @@ -1,82 +0,0 @@ -defmodule PlaywrightEx.BrowserContextChannelTest do - use ExUnit.Case, async: true - - alias PlaywrightEx.BrowserContext - - setup do - {:ok, connection} = start_supervised({__MODULE__.FakeConnection, self()}) - [connection: connection] - end - - describe "clock_install/2" do - test "sends clock_install without a time by default", %{connection: connection} do - assert {:ok, %{}} = BrowserContext.clock_install("context-guid", connection: connection, timeout: 123) - - assert_receive {:send, - %{ - guid: "context-guid", - method: :clock_install, - metadata: %{}, - params: %{timeout: 123} - }} - end - - test "sends clock_install with an explicit integer time", %{connection: connection} do - assert {:ok, %{}} = BrowserContext.clock_install("context-guid", time: 456, connection: connection, timeout: 123) - - assert_receive {:send, - %{ - guid: "context-guid", - method: :clock_install, - metadata: %{}, - params: %{time_number: 456, timeout: 123} - }} - end - end - - describe "clock_fast_forward/2" do - test "sends clock_fast_forward with integer ticks", %{connection: connection} do - assert {:ok, %{}} = - BrowserContext.clock_fast_forward("context-guid", ticks: 60_001, connection: connection, timeout: 123) - - assert_receive {:send, - %{ - guid: "context-guid", - method: :clock_fast_forward, - metadata: %{}, - params: %{ticks_number: 60_001, timeout: 123} - }} - end - - test "sends clock_fast_forward with string ticks", %{connection: connection} do - assert {:ok, %{}} = - BrowserContext.clock_fast_forward("context-guid", ticks: "01:00", connection: connection, timeout: 123) - - assert_receive {:send, - %{ - guid: "context-guid", - method: :clock_fast_forward, - metadata: %{}, - params: %{ticks_string: "01:00", timeout: 123} - }} - end - end - - defmodule FakeConnection do - @moduledoc false - use GenServer - - def start_link(test_pid) do - GenServer.start_link(__MODULE__, test_pid) - end - - @impl GenServer - def init(test_pid), do: {:ok, test_pid} - - @impl GenServer - def handle_call({:send, msg}, _from, test_pid) do - send(test_pid, {:send, msg}) - {:reply, %{result: %{}}, test_pid} - end - end -end diff --git a/test/playwright_ex/browser_context_test.exs b/test/playwright_ex/browser_context_test.exs index 1d147f4..9f689a0 100644 --- a/test/playwright_ex/browser_context_test.exs +++ b/test/playwright_ex/browser_context_test.exs @@ -21,14 +21,37 @@ defmodule PlaywrightEx.BrowserContextTest do describe "clock_fast_forward/2" do test "advances Date.now after installing the clock", %{browser_context: browser_context, frame: frame} do + assert_clock_advanced_from_current_time(browser_context.guid, frame.guid, ticks: 60_001) + end + + test "starts the clock near zero without installing first", %{browser_context: browser_context, frame: frame} do assert {:ok, _} = Frame.goto(frame.guid, url: "about:blank", timeout: @timeout) assert {:ok, before_now} = eval(frame.guid, "() => Date.now()") - assert {:ok, _} = BrowserContext.clock_install(browser_context.guid, timeout: @timeout) assert {:ok, _} = BrowserContext.clock_fast_forward(browser_context.guid, ticks: 60_001, timeout: @timeout) assert {:ok, after_now} = eval(frame.guid, "() => Date.now()") - assert after_now >= before_now + 60_001 + assert before_now > 1_000_000 + assert after_now >= 60_001 + assert after_now < before_now end + + test "accepts string ticks", %{browser_context: browser_context, frame: frame} do + assert_clock_advanced_from_current_time(browser_context.guid, frame.guid, ticks: "01:01", expected_delta: 61_000) + end + end + + defp assert_clock_advanced_from_current_time(context_id, frame_id, opts) do + opts = Keyword.validate!(opts, [:ticks, expected_delta: nil]) + expected_delta = opts[:expected_delta] || opts[:ticks] + + assert {:ok, _} = Frame.goto(frame_id, url: "about:blank", timeout: @timeout) + assert {:ok, before_now} = eval(frame_id, "() => Date.now()") + + assert {:ok, _} = BrowserContext.clock_install(context_id, timeout: @timeout) + assert {:ok, _} = BrowserContext.clock_fast_forward(context_id, ticks: opts[:ticks], timeout: @timeout) + + assert {:ok, after_now} = eval(frame_id, "() => Date.now()") + assert after_now >= before_now + expected_delta end end From b927144133dbba131000778e55b7c7a634e29741 Mon Sep 17 00:00:00 2001 From: Fredrik Teschke Date: Mon, 23 Mar 2026 18:29:10 +0100 Subject: [PATCH 4/5] Simplify browser context clock integration tests --- test/playwright_ex/browser_context_test.exs | 31 +++++++++++---------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/test/playwright_ex/browser_context_test.exs b/test/playwright_ex/browser_context_test.exs index 9f689a0..cbe4370 100644 --- a/test/playwright_ex/browser_context_test.exs +++ b/test/playwright_ex/browser_context_test.exs @@ -21,7 +21,14 @@ defmodule PlaywrightEx.BrowserContextTest do describe "clock_fast_forward/2" do test "advances Date.now after installing the clock", %{browser_context: browser_context, frame: frame} do - assert_clock_advanced_from_current_time(browser_context.guid, frame.guid, ticks: 60_001) + assert {:ok, _} = Frame.goto(frame.guid, url: "about:blank", timeout: @timeout) + assert {:ok, before_now} = eval(frame.guid, "() => Date.now()") + + assert {:ok, _} = BrowserContext.clock_install(browser_context.guid, timeout: @timeout) + assert {:ok, _} = BrowserContext.clock_fast_forward(browser_context.guid, ticks: 60_001, timeout: @timeout) + + assert {:ok, after_now} = eval(frame.guid, "() => Date.now()") + assert after_now >= before_now + 60_001 end test "starts the clock near zero without installing first", %{browser_context: browser_context, frame: frame} do @@ -37,21 +44,15 @@ defmodule PlaywrightEx.BrowserContextTest do end test "accepts string ticks", %{browser_context: browser_context, frame: frame} do - assert_clock_advanced_from_current_time(browser_context.guid, frame.guid, ticks: "01:01", expected_delta: 61_000) - end - end - - defp assert_clock_advanced_from_current_time(context_id, frame_id, opts) do - opts = Keyword.validate!(opts, [:ticks, expected_delta: nil]) - expected_delta = opts[:expected_delta] || opts[:ticks] - - assert {:ok, _} = Frame.goto(frame_id, url: "about:blank", timeout: @timeout) - assert {:ok, before_now} = eval(frame_id, "() => Date.now()") + assert {:ok, _} = Frame.goto(frame.guid, url: "about:blank", timeout: @timeout) + assert {:ok, before_now} = eval(frame.guid, "() => Date.now()") - assert {:ok, _} = BrowserContext.clock_install(context_id, timeout: @timeout) - assert {:ok, _} = BrowserContext.clock_fast_forward(context_id, ticks: opts[:ticks], timeout: @timeout) + assert {:ok, _} = BrowserContext.clock_fast_forward(browser_context.guid, ticks: "01:01", timeout: @timeout) - assert {:ok, after_now} = eval(frame_id, "() => Date.now()") - assert after_now >= before_now + expected_delta + assert {:ok, after_now} = eval(frame.guid, "() => Date.now()") + assert before_now > 1_000_000 + assert after_now >= 61_000 + assert after_now < before_now + end end end From 3d8b38d8c2ddd9f3e840b258228cbfb93dd1c6a9 Mon Sep 17 00:00:00 2001 From: Fredrik Teschke Date: Mon, 23 Mar 2026 20:35:24 +0100 Subject: [PATCH 5/5] Support Date values in clock install --- lib/playwright_ex/channels/browser_context.ex | 16 ++++++------ test/playwright_ex/browser_context_test.exs | 25 +++++++++++++++---- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/lib/playwright_ex/channels/browser_context.ex b/lib/playwright_ex/channels/browser_context.ex index 3ddf45a..88ff214 100644 --- a/lib/playwright_ex/channels/browser_context.ex +++ b/lib/playwright_ex/channels/browser_context.ex @@ -267,16 +267,17 @@ defmodule PlaywrightEx.BrowserContext do connection: PlaywrightEx.Channel.connection_opt(), timeout: PlaywrightEx.Channel.timeout_opt(), time: [ - type: {:or, [:non_neg_integer, :string]}, + type: {:or, [:non_neg_integer, :string, {:struct, DateTime}]}, required: false, - doc: "Optional base time to install, as milliseconds since epoch or a string accepted by Playwright." + doc: + "Optional base time to install, as milliseconds since epoch, an ISO8601 datetime, or a string accepted by Playwright." ] ) @doc """ - Installs Playwright's browser-context clock support. + Install fake implementations for the other time-related functions (e.g. `clock_fast_forward/2`). - Reference: https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/client/clock.ts + Reference: https://playwright.dev/docs/api/class-clock#clock-install ## Options #{NimbleOptions.docs(schema)} @@ -295,6 +296,7 @@ defmodule PlaywrightEx.BrowserContext do nil -> %{} time when is_integer(time) -> %{time_number: time} time when is_binary(time) -> %{time_string: time} + %DateTime{} = time -> %{time_string: DateTime.to_iso8601(time)} end connection @@ -309,14 +311,14 @@ defmodule PlaywrightEx.BrowserContext do ticks: [ type: {:or, [:non_neg_integer, :string]}, required: true, - doc: "Time to advance, in milliseconds or in Playwright's `mm:ss` / `hh:mm:ss` string format." + doc: "Time to advance, in milliseconds or in `ss` / `mm:ss` / `hh:mm:ss` string format." ] ) @doc """ - Fast forwards the browser context clock. + Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and reopening it later, after given time.. - Reference: https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/client/clock.ts + Reference: https://playwright.dev/docs/api/class-clock#clock-fast-forward ## Options #{NimbleOptions.docs(schema)} diff --git a/test/playwright_ex/browser_context_test.exs b/test/playwright_ex/browser_context_test.exs index cbe4370..641763d 100644 --- a/test/playwright_ex/browser_context_test.exs +++ b/test/playwright_ex/browser_context_test.exs @@ -19,6 +19,23 @@ defmodule PlaywrightEx.BrowserContextTest do end end + describe "clock_install/2" do + test "installs the clock from a DateTime", %{browser_context: browser_context, frame: frame} do + datetime = ~U[2024-01-02 03:04:05Z] + expected_now = DateTime.to_unix(datetime, :millisecond) + + assert {:ok, _} = Frame.goto(frame.guid, url: "about:blank", timeout: @timeout) + assert {:ok, _} = BrowserContext.clock_install(browser_context.guid, time: datetime, timeout: @timeout) + assert {:ok, installed_now} = eval(frame.guid, "() => Date.now()") + assert installed_now in (expected_now - 100)..(expected_now + 100) + + assert {:ok, _} = BrowserContext.clock_fast_forward(browser_context.guid, ticks: 60_001, timeout: @timeout) + + assert {:ok, advanced_now} = eval(frame.guid, "() => Date.now()") + assert advanced_now in (expected_now + 60_001)..(expected_now + 60_101) + end + end + describe "clock_fast_forward/2" do test "advances Date.now after installing the clock", %{browser_context: browser_context, frame: frame} do assert {:ok, _} = Frame.goto(frame.guid, url: "about:blank", timeout: @timeout) @@ -28,7 +45,7 @@ defmodule PlaywrightEx.BrowserContextTest do assert {:ok, _} = BrowserContext.clock_fast_forward(browser_context.guid, ticks: 60_001, timeout: @timeout) assert {:ok, after_now} = eval(frame.guid, "() => Date.now()") - assert after_now >= before_now + 60_001 + assert after_now in (before_now + 60_001)..(before_now + 60_101) end test "starts the clock near zero without installing first", %{browser_context: browser_context, frame: frame} do @@ -39,8 +56,7 @@ defmodule PlaywrightEx.BrowserContextTest do assert {:ok, after_now} = eval(frame.guid, "() => Date.now()") assert before_now > 1_000_000 - assert after_now >= 60_001 - assert after_now < before_now + assert after_now in 60_001..60_101 end test "accepts string ticks", %{browser_context: browser_context, frame: frame} do @@ -51,8 +67,7 @@ defmodule PlaywrightEx.BrowserContextTest do assert {:ok, after_now} = eval(frame.guid, "() => Date.now()") assert before_now > 1_000_000 - assert after_now >= 61_000 - assert after_now < before_now + assert after_now in 61_000..61_100 end end end