diff --git a/lib/playwright_ex/channels/browser_context.ex b/lib/playwright_ex/channels/browser_context.ex index 62bd4fc..88ff214 100644 --- a/lib/playwright_ex/channels/browser_context.ex +++ b/lib/playwright_ex/channels/browser_context.ex @@ -261,4 +261,88 @@ 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, {:struct, DateTime}]}, + required: false, + doc: + "Optional base time to install, as milliseconds since epoch, an ISO8601 datetime, or a string accepted by Playwright." + ] + ) + + @doc """ + Install fake implementations for the other time-related functions (e.g. `clock_fast_forward/2`). + + Reference: https://playwright.dev/docs/api/class-clock#clock-install + + ## 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} + %DateTime{} = time -> %{time_string: DateTime.to_iso8601(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 `ss` / `mm:ss` / `hh:mm:ss` string format." + ] + ) + + @doc """ + 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://playwright.dev/docs/api/class-clock#clock-fast-forward + + ## 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_test.exs b/test/playwright_ex/browser_context_test.exs index bc5071e..641763d 100644 --- a/test/playwright_ex/browser_context_test.exs +++ b/test/playwright_ex/browser_context_test.exs @@ -18,4 +18,56 @@ defmodule PlaywrightEx.BrowserContextTest do assert {:ok, "ok"} = eval(page.main_frame.guid, "() => window.__browser_context_add_init_script") 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) + 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 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 + assert {:ok, _} = Frame.goto(frame.guid, url: "about:blank", timeout: @timeout) + assert {:ok, before_now} = eval(frame.guid, "() => Date.now()") + + assert {:ok, _} = BrowserContext.clock_fast_forward(browser_context.guid, ticks: 60_001, timeout: @timeout) + + assert {:ok, after_now} = eval(frame.guid, "() => Date.now()") + assert before_now > 1_000_000 + assert after_now in 60_001..60_101 + end + + test "accepts string ticks", %{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_fast_forward(browser_context.guid, ticks: "01:01", timeout: @timeout) + + assert {:ok, after_now} = eval(frame.guid, "() => Date.now()") + assert before_now > 1_000_000 + assert after_now in 61_000..61_100 + end + end end