Packages
pages
1.3.0
4.1.0
4.0.0
3.5.0
3.4.2
3.4.1
3.4.0
3.3.0
3.2.0
3.1.0
3.0.1
3.0.0
2.3.0
2.2.1
2.2.0
2.1.1
2.1.0
2.0.0
1.3.1
1.3.0
1.2.0
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
0.14.0
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.1
0.11.0
0.10.1
0.10.0
0.9.0
0.8.0
0.7.1
0.7.0
0.6.2
0.6.1
0.6.0
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.1
0.3.0
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Page pattern for interacting with web pages
Current section
Files
Jump to
Current section
Files
lib/pages/driver/live_view.ex
defmodule Pages.Driver.LiveView do
# @related [drivers](lib/pages/driver.ex)
# @related [tests](test/pages/driver/live_view_test.exs)
@moduledoc """
A page driver for interacting with Phoenix.LiveView pages.
"""
@behaviour Pages.Driver
alias HtmlQuery, as: Hq
alias 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, conn.query_params)
def new(conn, request_path, params \\ %{})
def new(%Plug.Conn{} = conn, request_path, params) when is_binary(request_path) do
new_live(conn, request_path, params)
|> handle_rendered_result(%__MODULE__{conn: conn})
end
# # #
@doc "Called from `Pages.click/4` when the given page is a LiveView."
@spec click(Pages.Driver.t(), Pages.http_method(), Pages.text_filter() | nil, Hq.Css.selector()) ::
Pages.result()
@impl Pages.Driver
def click(%__MODULE__{} = page, :get, maybe_title, selector) do
page.live
|> LiveViewTest.element(Hq.Css.selector(selector), maybe_title)
|> LiveViewTest.render_click()
|> handle_rendered_result(page)
end
def click(%__MODULE__{} = page, :post, maybe_title, selector),
do: Pages.Driver.Conn.click(page, :post, maybe_title, selector)
@doc "Called from `Pages.handle_redirect/1` when the given page is a LiveView."
@spec handle_redirect(Pages.Driver.t()) :: Pages.Driver.t()
@impl Pages.Driver
def handle_redirect(page) do
{path, _flash} = page.live |> Phoenix.LiveViewTest.assert_redirect()
page.conn
|> Phoenix.ConnTest.ensure_recycled()
|> Pages.Shim.__retain_connect_params(page.conn)
|> Pages.visit(path)
end
@doc "Called from `Pages.rerender/1` when the given page is a LiveView."
@spec rerender(Pages.Driver.t()) :: Pages.result()
@impl Pages.Driver
def rerender(page),
do: %{page | rendered: LiveViewTest.render(page.live)}
@doc "Called from `Pages.render_change/3` when the given page is a LiveView."
@spec render_change(Pages.Driver.t(), Hq.Css.selector(), Enum.t()) :: Pages.result()
@impl Pages.Driver
def render_change(%__MODULE__{} = page, selector, value) do
page.live
|> LiveViewTest.element(Hq.Css.selector(selector))
|> LiveViewTest.render_change(value)
|> handle_rendered_result(page)
end
@doc "Called from `Pages.render_hook/3` when the given page is a LiveView."
@spec render_hook(Pages.Driver.t(), binary(), Pages.attrs_t(), keyword()) :: Pages.result()
@impl Pages.Driver
def render_hook(%__MODULE__{} = page, event, value_attrs, options) do
case Keyword.get(options, :target) do
nil -> page.live
target -> page.live |> LiveViewTest.element(target)
end
|> LiveViewTest.render_hook(event, value_attrs)
|> handle_rendered_result(page)
end
@doc "Called from `Pages.render_upload/4` when the given page is a LiveView."
@spec render_upload(Pages.Driver.t(), Pages.live_view_upload(), binary(), integer()) :: Pages.result()
@impl Pages.Driver
def render_upload(%__MODULE__{} = page, %Phoenix.LiveViewTest.Upload{} = upload, entry_name, percent) do
upload
|> LiveViewTest.render_upload(entry_name, percent)
|> handle_rendered_result(page)
end
@doc """
Perform a live redirect to the given path.
When issuing a live_redirect from one live view to another live view where the
routes cross between two `live_session`s, then live view will not just remount
the socket, but will issue a redirect. In this case, if the test pages have been
initialized via `Phoenix.ConnTest.build_conn/0`, the test adapter will have the
URI host set to be `www.example.com`... the live_redirect will be seen as an
external redirect.
When using `live_redirect/2` between two live sessions, ensure that the initial
test setup (for example in ConnCase) instead calls
`Phoenix.ConnTest.build_conn(:get, "http://localhost:4002/")`, so that redirects
are seen as internal redirects.
This is not implemented in `Pages` due to its specificity to LiveView and LiveViewTest.
"""
@spec live_redirect(Pages.Driver.t(), binary()) :: Pages.result()
def live_redirect(page, destination_path),
do: page.live |> Phoenix.LiveViewTest.live_redirect(to: destination_path) |> handle_rendered_result(page)
@doc "Called from `Pages.submit_form/2` when the given page is a LiveView."
@spec submit_form(Pages.Driver.t(), Hq.Css.selector()) :: Pages.result()
@impl Pages.Driver
def submit_form(%__MODULE__{} = page, selector) do
page.live
|> LiveViewTest.form(Hq.Css.selector(selector))
|> LiveViewTest.render_submit()
|> handle_rendered_result(page)
end
@doc "Called from `Pages.submit_form/4` and `Pages.submit_form/5` when the given page is a LiveView."
@spec submit_form(Pages.Driver.t(), Hq.Css.selector(), atom(), Pages.attrs_t()) :: Pages.result()
@spec submit_form(Pages.Driver.t(), Hq.Css.selector(), atom(), Pages.attrs_t(), Pages.attrs_t()) ::
Pages.result()
@impl Pages.Driver
def submit_form(%__MODULE__{} = page, selector, schema, form_attrs, hidden_attrs \\ %{}) do
params = [{schema, Map.new(form_attrs)}]
hidden_params = [{schema, Map.new(hidden_attrs)}]
page.live
|> LiveViewTest.form(Hq.Css.selector(selector), params)
|> LiveViewTest.render_submit(hidden_params)
|> handle_rendered_result(page)
|> maybe_trigger_action(params)
end
@doc "Called from `Pages.update_form/4` when the given page is a LiveView."
@spec update_form(Pages.Driver.t(), Hq.Css.selector(), atom(), Pages.attrs_t(), keyword()) ::
Pages.result()
@impl Pages.Driver
def update_form(%__MODULE__{} = page, selector, schema, attrs, opts \\ []) do
params =
%{schema => Map.new(attrs)}
|> then(fn params ->
case Keyword.get(opts, :target) do
nil -> params
target -> Map.put(params, :_target, target)
end
end)
page.live
|> LiveViewTest.form(Hq.Css.selector(selector))
|> LiveViewTest.render_change(params)
|> handle_rendered_result(page)
|> maybe_trigger_action(params)
end
@doc """
Initialize a `live` with the given path.
This is called from `Pages.visit/2` when the conn indicates that the pages is a LiveView,
and should only be called directly if the parent function does not work for some reason.
"""
@spec visit(Pages.Driver.t(), binary()) :: Pages.result()
@impl Pages.Driver
def visit(%__MODULE__{} = page, path) do
uri = URI.parse(to_string(path))
if uri.host in [nil, "localhost"] do
new_live(page.conn, path, %{})
|> handle_rendered_result(page)
else
{:error, :external, path}
end
end
@doc """
Find a child component, and pass it as a new Page into the given function.
Rerenders the top-level page upon completion. See `Pages.with_child_component/3`.
"""
@impl Pages.Driver
def with_child_component(%__MODULE__{live: view} = page, child_id, fun) when is_function(fun, 1) do
child = Phoenix.LiveViewTest.find_live_child(view, child_id)
if !child,
do: raise(Pages.Error, "Expected to find a child component with id `#{child_id}`, but found nil")
fun.(%{page | live: child})
Pages.rerender(page)
end
# # #
defp new_live(conn, path, params) do
cond do
is_binary(path) ->
conn
|> Phoenix.ConnTest.ensure_recycled()
|> Pages.Shim.__dispatch(:get, path, params)
|> then(fn
%{assigns: %{live_module: _}} = new_conn ->
new_conn
|> Pages.Shim.__retain_connect_params(conn)
|> Phoenix.LiveViewTest.__live__(path)
conn ->
conn
|> Phoenix.ConnTest.ensure_recycled()
|> Pages.new()
end)
is_nil(path) ->
conn
|> Phoenix.ConnTest.ensure_recycled()
|> then(&Pages.Shim.__retain_connect_params(&1, conn))
|> Phoenix.LiveViewTest.__live__()
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, opts}} ->
endpoint = Pages.Shim.__endpoint()
{conn, to} = Phoenix.LiveViewTest.__follow_redirect__(page.conn, endpoint, nil, opts)
conn = conn |> Pages.Shim.__retain_connect_params(page.conn)
new(conn, to)
{:error, {:redirect, %{to: new_path}}} ->
page.conn
|> Phoenix.ConnTest.ensure_recycled()
|> Pages.Shim.__retain_connect_params(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 |> Hq.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
defp maybe_trigger_action(page, _params), do: page
defimpl String.Chars, for: Pages.Driver.LiveView 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