lib/pages/driver/live_view.ex

defmodule Pages.Driver.LiveView do
  # @related [drivers](lib/pages/driver.ex)

  @moduledoc """
  A page driver for interacting with Phoenix.LiveView pages.
  """

  @behaviour Pages.Driver

  import Phoenix.LiveViewTest

  defstruct ~w[conn live rendered]a

  @type t() :: %__MODULE__{
          conn: Plug.Conn.t(),
          live: any(),
          rendered: binary() | nil
        }

  def new(%Plug.Conn{} = conn),
    do: new(conn, conn.request_path)

  def new(%Plug.Conn{} = conn, request_path) when is_binary(request_path),
    do: new(conn, new_live(conn, request_path))

  def new(%Plug.Conn{} = conn, {:ok, live, rendered}),
    do: __struct__(conn: conn, live: live, rendered: rendered)

  # # #

  @spec click(Pages.Driver.t(), binary(), Pages.Css.selector()) :: Pages.Driver.t()
  def click(%__MODULE__{} = page, title, selector) do
    page.live
    |> element(Pages.Css.selector(selector), title)
    |> render_click()
    |> handle_rendered_result(page)
  end

  @spec rerender(Pages.Driver.t()) :: Pages.Driver.t()
  def rerender(page),
    do: %{page | rendered: render(page.live)}

  @doc "Perform a live redirect. Not implemented in `Pages` because it's specific to LiveView."
  @spec live_redirect(Pages.Driver.t(), binary()) :: Pages.Driver.t()
  def live_redirect(page, destination_path),
    do: page.live |> Phoenix.LiveViewTest.live_redirect(to: destination_path) |> handle_rendered_result(page)

  @spec submit_form(Pages.Driver.t(), Pages.Css.selector()) :: Pages.Driver.t()
  def submit_form(%__MODULE__{} = page, selector) do
    page.live
    |> form(Pages.Css.selector(selector))
    |> render_submit()
    |> handle_rendered_result(page)
  end

  @spec submit_form(Pages.Driver.t(), Pages.Css.selector(), atom(), Pages.attrs_t()) ::
          Pages.Driver.t()
  def submit_form(%__MODULE__{} = page, selector, schema, attrs) do
    params = [{schema, Map.new(attrs)}]

    page.live
    |> form(Pages.Css.selector(selector), params)
    |> render_submit()
    |> handle_rendered_result(page)
    |> maybe_trigger_action(params)
  end

  @spec update_form(Pages.Driver.t(), Pages.Css.selector(), atom(), Pages.attrs_t()) ::
          Pages.Driver.t()
  def update_form(%__MODULE__{} = page, selector, schema, attrs) do
    params = [{schema, Map.new(attrs)}]

    page.live
    |> form(Pages.Css.selector(selector), params)
    |> render_change()
    |> handle_rendered_result(page)
    |> maybe_trigger_action(params)
  end

  @doc "Go to the given URL, assuming that it will be a new LiveView"
  def visit(%__MODULE__{} = page, path) do
    case new_live(page.conn, path) do
      {:error, {:live_redirect, %{to: new_path}}} -> new(page.conn, new_path)
      {:error, {:redirect, %{to: new_path}}} -> Pages.new(page.conn) |> Pages.visit(new_path)
      {:ok, view, html} -> %__MODULE__{conn: page.conn, live: view, rendered: html}
    end
  end

  # # #

  defp new_live(conn, path) do
    cond do
      is_binary(path) ->
        conn
        |> Pages.Shim.__dispatch(:get, path)
        |> Phoenix.LiveViewTest.__live__(path)

      is_nil(path) ->
        Phoenix.LiveViewTest.__live__(conn)

      true ->
        raise RuntimeError, "path must be nil or a binary, got: #{inspect(path)}"
    end
  end

  defp handle_rendered_result(rendered_result, %__MODULE__{} = page) do
    case rendered_result do
      rendered when is_binary(rendered) -> %{page | rendered: rendered}
      {:error, {:live_redirect, %{to: new_path}}} -> new(page.conn, new_path)
      {:error, {:redirect, %{to: new_path}}} -> Pages.new(page.conn) |> Pages.visit(new_path)
      {:ok, live, html} -> %{page | live: live, rendered: html}
    end
  end

  defp maybe_trigger_action(%__MODULE__{} = page, params) do
    case page |> Pages.Html.find("[phx-trigger-action]") do
      element when not is_nil(element) ->
        page.live
        |> Phoenix.LiveViewTest.form("form[phx-trigger-action]", params)
        |> Pages.Shim.__follow_trigger_action(page.conn)
        |> Pages.new()

      _ ->
        page
    end
  end

  defimpl String.Chars do
    def to_string(%Pages.Driver.LiveView{rendered: rendered}) when not is_nil(rendered),
      do: rendered

    def to_string(%Pages.Driver.LiveView{live: live}) when not is_nil(live),
      do: live |> Phoenix.LiveViewTest.render()
  end
end