lib/playwright/browser.ex

defmodule Playwright.Browser do
  @moduledoc """
  A `Playwright.Browser` instance is createed via:

    - `Playwright.BrowserType.launch/0`, when using the "driver" transport.
    - `Playwright.BrowserType.connect/1`, when using the "websocket" transport.

  An example of using a `Playwright.Browser` to create a `Playwright.Page`:

      alias Playwright.{Browser, Page}

      {:ok, browser} = Playwright.launch(:chromium)
      {:ok, page} = Browser.new_page(browser)

      Page.goto(page, "https://example.com")
      Browser.close(browser)

  ## Properties

    - `:name`
    - `:version`
  """
  use Playwright.ChannelOwner,
    fields: [:name, :version]
  alias Playwright.{Browser, BrowserContext, ChannelOwner, Extra, Page}
  alias Playwright.Runner.Channel

  @typedoc "Supported events"
  @type event :: :disconnected

  @typedoc "A map/struct providing call options"
  @type options :: map()

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

  @impl ChannelOwner
  def init(owner, _initializer) do
    # Channel.bind(owner, :close, fn event ->
    #   {:patch, }
    # end)

    {:ok, %{owner | version: cut_version(owner.version)}}
  end

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

  # @spec close(Browser.t()) :: :ok
  # def close(owner)

  @doc """
  Returns an array of all open browser contexts. In a newly created browser,
  this will return zero browser contexts.

  ## Example

      {:ok, contexts} = Browser.contexts(browser)
      asset Enum.empty?(contexts)

      Browser.new_context(browser)

      {:ok, contexts} = Browser.contexts(browser)
      assert length(contexts) == 1
  """
  @spec contexts(Browser.t()) :: {:ok, [BrowserContext.t()]}
  def contexts(%Browser{} = owner) do
    result = Channel.all(owner.connection, %{
      parent: owner,
      type: "BrowserContext"
    })
    {:ok, result}
  end

  # ---

  # @spec is_connected(BrowserContext.t()) :: boolean()
  # def is_connected(owner)

  # @spec new_browser_cdp_session(BrowserContext.t()) :: {:ok, Playwright.CDPSession.t()}
  # def new_browser_cdp_session(owner)

  # ---

  @doc """
  Create a new `Playwright.BrowserContext` for this `Playwright.Browser`.

  A `BrowserContext` does not share cookies/cache with other `BrowserContexts`
  and is somewhat equivalent to an "incognito" browser "window".

  ## Example

      # create a new "incognito" browser context.
      {:ok, context} = Browser.new_context(browser)

      # create a new page in a pristine context.
      {:ok, page} = BrowserContext.new_page(context)

      Page.goto(page, "https://example.com")

  ## Returns

    - `{:ok, Playwright.BrowserContext.t()}`

  ## Arguments

  | key / name         | type   |             | description |
  | ------------------ | ------ | ----------- | ----------- |
  | `accept_downloads` | option | `boolean()` | Whether to automatically download all the attachments. If false, all the downloads are canceled. `(default: false)` |
  | `...`              | option | `...`       | ... |
  """
  @spec new_context(Browser.t(), options()) :: {:ok, BrowserContext.t()}
  def new_context(%Browser{} = owner, options \\ %{}) do
    Channel.post(owner, :new_context, prepare(options))
  end

  @doc """
  Create a new `Playwright.Page` for this Browser, within a new "owned"
  `Playwright.BrowserContext`.

  Closing this page will close the context as well.

  This is a convenience API function that should only be used for single-page
  scenarios and short snippets. Production code and testing frameworks should
  explicitly create via `Playwright.Browser.new_context/2` followed by
  `Playwright.BrowserContext.new_page/2`, given the new context, to manage
  resource lifecycles.
  """
  @spec new_page(Browser.t()) :: {:ok, Page.t()}
  def new_page(%Browser{connection: connection} = owner) do
    {:ok, context} = new_context(owner)
    {:ok, page} = BrowserContext.new_page(context)

    # establish co-dependency
    {:ok, _} = Channel.patch(connection, context.guid, %{owner_page: page})
    {:ok, _} = Channel.patch(connection, page.guid, %{owned_context: context})
  end

  # ---

  # @spec on(Browser.t(), event(), function()) :: {:ok, Browser.t()}
  # def on(owner, event, callback)

  # @spec start_tracing(Browser.t(), Page.t(), options()) :: :ok
  # def start_tracing(owner, page \\ nil, options \\ %{})

  # @spec stop_tracing(Browser.t()) :: {:ok, binary()}
  # def stop_tracing(owner)

  # ---

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

  # Chromium version is \d+.\d+.\d+.\d+, but that doesn't parse well with
  # `Version`. So, until it causes issue we're cutting it down to
  # <major.minor.patch>.
  defp cut_version(version) do
    version |> String.split(".") |> Enum.take(3) |> Enum.join(".")
  end

  defp prepare(%{extra_http_headers: headers}) do
    %{
      extraHTTPHeaders:
        Enum.reduce(headers, [], fn {k, v}, acc ->
          [%{name: k, value: v} | acc]
        end)
    }
  end

  defp prepare(opts) when is_map(opts) do
    Enum.reduce(opts, %{}, fn {k, v}, acc -> Map.put(acc, prepare(k), v) end)
  end

  defp prepare(atom) when is_atom(atom) do
    Extra.Atom.to_string(atom)
    |> Recase.to_camel()
    |> Extra.Atom.from_string()
  end
end