lib/playwright/page.ex

defmodule Playwright.Page do
  @moduledoc """
  `Page` provides methods to interact with a single tab in a
  `Playwright.Browser`, or an [extension background page](https://developer.chrome.com/extensions/background_pages)
  in Chromium.

  One `Playwright.Browser` instance might have multiple `Page` instances.

  ## Example

  Create a page, navigate it to a URL, and save a screenshot:

      {:ok, page} = Browser.new_page(browser)
      {:ok, resp} = Page.goto(page, "https://example.com")

      {:ok _} = Page.screenshot(page, %{path: "screenshot.png"})

      :ok = Page.close(page)

  The Page module is capable of hanlding various emitted events (described below).

  ## Example

  Log a message for a single page load event (WIP: `once` is not yet implemented):

      Page.once(page, :load, fn e ->
        IO.puts("page loaded!")
      end)

  Unsubscribe from events with the `remove_lstener` function (WIP: `remove_listener` is not yet implemented):

      def log_request(request) do
        IO.inspect(label: "A request was made")
      end

      Page.on(page, :request, fn e ->
        log_request(e.pages.request)
      end)

      Page.remove_listener(page, log_request)
  """
  use Playwright.ChannelOwner,
    fields: [:is_closed, :main_frame, :owned_context, :viewport_size]

  alias Playwright.{BrowserContext, Frame, Page}
  alias Playwright.ChannelOwner
  alias Playwright.Runner.Helpers

  @type function_or_options :: fun() | options() | nil
  @type options :: map()

  require Logger

  # callbacks
  # ---------------------------------------------------------------------------

  @impl ChannelOwner
  def init(owner, _intializer) do
    Channel.bind(owner, :close, fn event ->
      {:patch, %{event.target | is_closed: true}}
    end)

    {:ok, owner}
  end

  # delegates
  # ---------------------------------------------------------------------------

  defdelegate click(page, selector, options \\ %{}),
    to: Playwright.Frame

  defdelegate evaluate(page, expression, arg \\ nil),
    to: Playwright.Frame

  defdelegate evaluate_handle(page, expression, arg \\ nil),
    to: Playwright.Frame

  defdelegate expect_event(owner, event, trigger),
    to: Playwright.BrowserContext

  defdelegate fill(page, selector, value),
    to: Playwright.Frame

  defdelegate get_attribute(page, selector, name, options \\ %{}),
    to: Playwright.Frame

  defdelegate goto(page, url, params \\ %{}),
    to: Playwright.Frame

  defdelegate press(page, selector, key, options \\ %{}),
    to: Playwright.Frame

  defdelegate q(page, selector, options \\ %{}),
    to: Playwright.Frame

  defdelegate q!(page, selector, options \\ %{}),
    to: Playwright.Frame

  defdelegate query_selector(page, selector, options \\ %{}),
    to: Playwright.Frame

  defdelegate query_selector!(page, selector, options \\ %{}),
    to: Playwright.Frame

  defdelegate qq(page, selector, options \\ %{}),
    to: Playwright.Frame

  defdelegate query_selector_all(page, selector, options \\ %{}),
    to: Playwright.Frame

  defdelegate set_content(page, html, options \\ %{}),
    to: Playwright.Frame

  defdelegate text_content(page, selector, options \\ %{}),
    to: Playwright.Frame

  defdelegate title(page),
    to: Playwright.Frame

  defdelegate url(page),
    to: Playwright.Frame

  defdelegate wait_for_selector(page, selector, options),
    to: Playwright.Frame

  # API
  # ---------------------------------------------------------------------------

  # ---

  # @spec add_init_script(Page.t(), binary(), options()) :: :ok
  # def add_init_script(owner, script, options \\ %{})

  # @spec add_script_tag(Page.t(), options()) :: {:ok, ElementHandle.t()}
  # def add_script_tag(owner, options \\ %{})

  # @spec add_style_tag(Page.t(), options()) :: {:ok, ElementHandle.t()}
  # def add_style_tag(owner, options \\ %{})

  # @spec bring_to_front(Page.t()) :: :ok
  # def bring_to_front(owner)

  # @spec check(Page.t(), binary(), options()) :: :ok
  # def check(owner, selector, options \\ %{})

  # ---

  @doc """
  Closes the `Page`.

  If the `Page` has an "owned context" (1-to-1 co-dependency with a
  `Playwright.BrowserContext`), that context is closed as well.

  If `option: run_before_unload` is false, does not run any unload handlers and
  waits for the page to be closed. If `option: run_before_unload` is `true`
  the function will run unload handlers, but will not wait for the page to
  close. By default, `Playwright.Page.close/1` does not run `:beforeunload`
  handlers.

  ## Returns

    - `:ok`

  ## Arguments

  | key / name          | type   |             | description |
  | ------------------- | ------ | ----------- | ----------- |
  | `run_before_unload` | option | `boolean()` | Whether to run the before unload page handlers. `(default: false)` |

  ## NOTE

  > if `option: run_before_unload` is passed as `true`, a `:beforeunload`
  > dialog might be summoned and should be handled manually via
  > `Playwright.Page.on/3`.
  """
  @spec close(t() | {:ok, t()}, options()) :: :ok
  def close(owner, options \\ %{})

  def close(%Page{} = owner, options) do
    Channel.post(owner, :close, options)

    # NOTE: this *might* prefer to be done on `__dispose__`
    # ...OR, `.on(_, "close", _)`
    if owner.owned_context do
      context(owner) |> BrowserContext.close()
    end

    :ok
  end

  def close({:ok, owner}, options) do
    close(owner, options)
  end

  @doc """
  Get the full HTML contents of the page, including the doctype.
  """
  @spec content(t() | {:ok, t()}) :: {:ok, binary()}
  def content(owner)

  def content(%Page{} = owner) do
    Channel.post(owner, :content)
  end

  def content({:ok, owner}) do
    content(owner)
  end

  @doc """
  Get the `Playwright.BrowserContext` that the page belongs to.
  """
  @spec context(t() | {:ok, t()}) :: BrowserContext.t()
  def context(owner)

  def context(%Page{} = owner) do
    {:ok, ctx} = Channel.find(owner, owner.parent)
    ctx
  end

  def context({:ok, owner}) do
    context(owner)
  end

  # ---

  # @spec add_init_script(Page.t(), binary(), options()) :: :ok
  # def add_init_script(owner, script, options \\ %{})

  # ---

  @spec eval_on_selector(Page.t(), binary(), binary(), term(), map()) :: term()
  def eval_on_selector(owner, selector, expression, arg \\ nil, options \\ %{})

  def eval_on_selector(%Page{} = owner, selector, expression, arg, options) do
    main_frame(owner)
    |> Frame.eval_on_selector(selector, expression, arg, options)
  end

  def eval_on_selector({:ok, owner}, selector, expression, arg, options) do
    eval_on_selector(owner, selector, expression, arg, options)
  end

  # NOTE: the event/method will be recv'd from Playwright server with
  # the parent BrowserContext as the context/bound :guid. So, we need to
  # add our handlers there, on that (BrowserContext) parent.
  def on(%Page{} = owner, event, callback)
      when event in [:request, :response, :request_finished, "request", "response", "requestFinished"] do
    context(owner) |> Channel.bind(event, callback)
  end

  def on(%Page{} = owner, event, callback) do
    Channel.bind(owner, event, callback)
  end

  def on({:ok, owner}, event, callback) do
    on(owner, event, callback)
  end

  @spec route(Page.t(), binary(), function(), map()) :: {atom(), Page.t()}
  def route(owner, pattern, callback, options \\ %{})

  def route(%Page{} = owner, pattern, callback, _options) do
    matcher = Helpers.URLMatcher.new(pattern)

    handler = fn %{params: %{request: request, route: route}} ->
      if Helpers.URLMatcher.matches(matcher, request.url) do
        Task.start_link(fn ->
          callback.(route, request)
        end)
      end
    end

    if Enum.empty?(owner.listeners["route"] || []) do
      Channel.post(owner, :set_network_interception_enabled, %{enabled: true})
    end

    Channel.bind(owner, :route, handler)
  end

  def route({:ok, owner}, pattern, callback, options) do
    route(owner, pattern, callback, options)
  end

  @spec screenshot(Page.t(), map()) :: {:ok, binary()}
  def screenshot(owner, options \\ %{})

  def screenshot(%Page{} = owner, options) do
    case Map.pop(options, :path) do
      {nil, params} ->
        Channel.post(owner, :screenshot, params)

      {path, params} ->
        [_, filetype] = String.split(path, ".")

        {:ok, data} = Channel.post(owner, :screenshot, Map.put(params, :type, filetype))
        File.write!(path, Base.decode64!(data))
        {:ok, data}
    end
  end

  def screenshot({:ok, owner}, options) do
    screenshot(owner, options)
  end

  @spec wait_for_load_state(Page.t(), binary(), options()) :: {:ok, Page.t()}
  def wait_for_load_state(owner, state \\ "load", options \\ %{})

  def wait_for_load_state(%Page{} = owner, state, _options)
      when is_binary(state)
      when state in ["load", "domcontentloaded", "networkidle", "commit"] do
    Logger.warn("Page.wait_for_load_state")

    {:ok, _} =
      main_frame(owner)
      |> Frame.wait_for_load_state(state)

    {:ok, owner}
  end

  def wait_for_load_state(%Page{} = owner, state, options) when is_binary(state) do
    wait_for_load_state(owner, state, options)
  end

  def wait_for_load_state(%Page{} = owner, options, _) when is_map(options) do
    wait_for_load_state(owner, "load", options)
  end

  # private
  # ---------------------------------------------------------------------------

  defp main_frame(owner) do
    {:ok, frame} = Channel.find(owner, owner.main_frame)
    frame
  end
end