lib/playwright/browser_type.ex

defmodule Playwright.BrowserType do
  @moduledoc """
  `Playwright.BrowserType` provides functions to launch a specific browser
  instance or connect to an existing one.

  The following is a typical example of using Playwright to drive automation:

      alias Playwright.{Browser, BrowserType, Page}

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

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

      Browser.close(browser)

  ## Example

  Open a new chromium via the CLI driver:

      {connection, browser} = Playwright.BrowserType.launch()

  Connect to a running playwright instances:

      {connection, browser} =
        Playwright.BrowserType.connect("ws://localhost:3000/playwright")
  """

  use Playwright.ChannelOwner
  alias Playwright.BrowserType
  alias Playwright.Runner.{Config,Connection,Transport}

  @typedoc "The web client type used for `launch/1` and `connect/2` functions."
  @type client :: :chromium | :firefox | :webkit

  @typedoc "Options for `connect/2`"
  @type connect_options :: map()

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

  @typedoc "A string URL"
  @type url :: String.t()

  @typedoc "A websocket endpoint (URL)"
  @type ws_endpoint :: url()

  @doc """
  Attaches Playwright to an existing browser instance over a websocket.

  ## Returns

    - `{connection, %Playwright.Browser{}}`

  ## Arguments

  | key / name    | type   |                             | description |
  | ------------- | ------ | --------------------------- | ----------- |
  | `ws_endpoint` | param  | `BrowserType.ws_endpoint()` | A browser websocket endpoint to connect to. |
  | `:headers`    | option | `map()`                     | Additional HTTP headers to be sent with websocket connect request |
  | `:slow_mow`   | option | `integer()`                 | Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. `(default: 0)` |
  | `:logger`     | option |                             | Logger sink for Playwright logging |
  | `:timeout`    | option | `integer()`                 | Maximum time in milliseconds to wait for the connection to be established. Pass `0` to disable timeout. `(default: 30_000 (30 seconds))` |
  """
  @spec connect(ws_endpoint(), connect_options()) :: {pid(), Playwright.Browser.t()}
  def connect(ws_endpoint, options \\ %{})

  def connect(ws_endpoint, _options) do
    with {:ok, connection} <- new_session(Transport.WebSocket, [ws_endpoint]),
         launched <- launched_browser(connection),
         browser <- Channel.get(connection, {:guid, launched}) do
      {connection, browser}
    else
      {:error, error} -> {:error, {"Error connecting to #{inspect(ws_endpoint)}", error}}
      error -> {:error, {"Error connecting to #{inspect(ws_endpoint)}", error}}
    end
  end

  # ---

  # @spec connect_over_cdp(BrowserType.t(), url(), options()) :: {:ok, Playwright.Browser.t()}
  # def connect_over_cdp(owner, endpoint_url, options \\ %{})

  # @spec executable_path(BrowserType.t()) :: String.t()
  # def executable_path(owner)

  # ---

  @doc """
  Launches a new browser instance via the Playwright driver CLI.

  ## Example

      # Use `:ignore_default_args` option to filter out `--mute-audio` from
      # default arguments:
      browser =
        Playwright.launch(:chromium, %{ignore_default_args = ["--mute-audio"]})

  ## Returns

    - `{connection, %Playwright.Browser{}}`

  ## Arguments

  ... (many)

  ## NOTE

  > **Chromium-only** Playwright can also be used to control the Google Chrome
  > or Microsoft Edge browsers, but it works best with the version of Chromium
  > it is bundled with. There is no guarantee it will work with any other
  > version. Use `:executable_path` option with extreme caution.
  >
  > If Google Chrome (rather than Chromium) is preferred, a
  > [Chrome Canary](https://www.google.com/chrome/browser/canary.html) or
  > [Dev Channel](https://www.chromium.org/getting-involved/dev-channel) build
  > is suggested.
  >
  > Stock browsers like Google Chrome and Microsoft Edge are suitable for tests
  > that require proprietary media codecs for video playback.
  > See [this article](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/)
  > for other differences between Chromium and Chrome.
  > [This article](https://chromium.googlesource.com/chromium/src/+/lkgr/docs/chromium_browser_vs_google_chrome.md)
  > describes some differences for Linux users.
  """
  @spec launch(client() | nil) :: {pid(), Playwright.Browser.t()}
  def launch(client \\ nil)

  def launch(nil) do
    launch(:chromium)
  end

  def launch(client) when client in [:chromium] do
    {:ok, connection} = new_session(Transport.Driver, ["assets/node_modules/playwright/cli.js"])
    {connection, chromium(connection)}

  end

  def launch(client) when client in [:firefox, :webkit] do
    raise RuntimeError, message: "not yet implemented"
  end

  # ---

  # @spec launch_persistent_context(BrowserType.t(), String.t(), options()) :: {:ok, Playwright.BrowserContext.t()}
  # def launch_persistent_context(owner, user_data_dir, options \\ %{})

  # @spec launch_server(BrowserType.t(), options()) :: {:ok, Playwright.BrowserServer.t()}
  # def launch_server(owner, options \\ %{})

  # @spec name(BrowserType.t()) :: client()
  # def name(owner)

  # ---

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

  defp browser(%BrowserType{} = owner) do
    case Channel.post(owner, :launch, Config.launch_options(true)) do
      {:ok, %Playwright.Browser{} = result} ->
        result

      other ->
        raise("expected launch to return a  Playwright.Browser, received: #{inspect(other)}")
    end
  end

  defp chromium(connection) do
    playwright = Channel.get(connection, {:guid, "Playwright"})

    case playwright do
      %Playwright{} ->
        %{guid: guid} = playwright.chromium

        Channel.get(connection, {:guid, guid}) |> browser()

      _other ->
        raise("expected chromium to return a  `Playwright`, received: #{inspect(playwright)}")
    end
  end

  defp new_session(transport, args) do
    DynamicSupervisor.start_child(
      BrowserType.Supervisor,
      {Connection, {transport, args}}
    )
  end

  defp launched_browser(connection) do
    playwright = Channel.get(connection, {:guid, "Playwright"})
    %{guid: guid} = playwright.initializer.preLaunchedBrowser
    guid
  end
end