defmodule LiveViewNativeTest do @moduledoc ~S''' Conveniences for testing function components as well as LiveViews and LiveComponents. ## Testing function components There are two mechanisms for testing function components. Imagine the following component: def greet(assigns) do ~LVN""" Hello, <%= @name %>! """ end You can test it by using `render_component/3`, passing the function reference to the component as first argument: import LiveViewNativeTest test "greets" do assert render_component(&MyComponents.greet/1, name: "Mary") == "Hello, Mary!" end However, for complex components, often the simplest way to test them is by using the `~LVN` sigil itself: import LiveViewNative.Component import LiveViewNativeTest test "greets" do assigns = %{} assert rendered_to_string(~H""" """) == "Hello, Mary!" end The difference is that we use `rendered_to_string/1` to convert the rendered template to a string for testing. ## LiveView Native paths and params When calling LiveView Native paths for either dead renders or live renders, the path that you call in a regular `Phoenix.LiveViewTest` is identical to the path you call in a `LiveViewNativeTest`. The `_format` and `_interface` params are used to delegate to the proper render components and render functions. The examples throughout this documentation will regularly show the use of those two params when calling either `Phoenix.ConnTest.get/3` or `live/3` ## Testing LiveViewNative.LiveViews and LiveViewNative.LiveComponents In LiveViewNative.LiveComponents and LiveViewNative.LiveView tests, we interact with views via process communication in substitution of a client. Like a client, our test process receives messages about the rendered updates from the view which can be asserted against to test the life-cycle and behavior of LiveViewNative.LiveViews and their children. ### Testing LiveViewNative.LiveViews The life-cycle of a LiveViewNative.LiveView as outlined in the `Phoenix.LiveView` docs details how a view starts as a stateless markup render in a disconnected socket state. Once the browser receives the markup, it connects to the server and a new LiveView process is started, remounted in a connected socket state, and the view continues statefully. The LiveView test functions support testing both disconnected and connected mounts separately, for example: import Plug.Conn import Phoenix.ConnTest import LiveViewNativeTest @endpoint MyEndpoint test "disconnected and connected mount", %{conn: conn} do conn = get(conn, "/my-path", _format: "gameboy") assert lvn_response(conn, 200, _format: :gameboy) =~ "My Disconnected View" {:ok, _view, markup} = live(conn, _format: :gameboy) assert markup =~ "My Connected View" end test "redirected mount", %{conn: conn} do assert {:error, {:redirect, %{to: "/somewhere"}}} = live(conn, "my-path") end Here, we start by using the familiar `Phoenix.ConnTest` function, `get/3` to test the regular HTTP GET request which invokes mount with a disconnected socket. Next, `live/2` is called with our sent connection to mount the view in a connected state, which starts our stateful LiveView process. In general, it's often more convenient to test the mounting of a view in a single step, provided you don't need the result of the stateless HTTP render. This is done with a single call to `live/3`, which performs the `get` step for us: test "connected mount", %{conn: conn} do {:ok, _view, markup} = live(conn, "/my-path", _format: :gameboy) assert markup =~ "My Connected View" end ### Testing Events The client can send a variety of events to a LiveView via `phx-` bindings, which are sent to the `handle_event/3` callback. To test events sent by the browser and assert on the rendered side effect of the event, use the `render_*` functions: * `render_click/1` - sends a phx-click event and value, returning the rendered result of the `handle_event/3` callback. * `render_focus/2` - sends a phx-focus event and value, returning the rendered result of the `handle_event/3` callback. * `render_blur/1` - sends a phx-blur event and value, returning the rendered result of the `handle_event/3` callback. * `render_submit/1` - sends a form phx-submit event and value, returning the rendered result of the `handle_event/3` callback. * `render_change/1` - sends a form phx-change event and value, returning the rendered result of the `handle_event/3` callback. * `render_keydown/1` - sends a form phx-keydown event and value, returning the rendered result of the `handle_event/3` callback. * `render_keyup/1` - sends a form phx-keyup event and value, returning the rendered result of the `handle_event/3` callback. * `render_hook/3` - sends a hook event and value, returning the rendered result of the `handle_event/3` callback. For example: {:ok, view, _markup} = live(conn, "/thermo", _format: :gameboy) assert view |> element("button#inc") |> render_click() =~ "The temperature is: 31℉" In the example above, we are looking for a particular element on the page and triggering its phx-click event. LiveView takes care of making sure the element has a phx-click and automatically sends its values to the server. You can also bypass the element lookup and directly trigger the LiveView event in most functions: assert render_click(view, :inc, %{}) =~ "The temperature is: 31℉" The `element` style is preferred as much as possible, as it helps LiveView perform validations and ensure the events in the markup actually matches the event names on the server. ### Testing regular messages LiveViews are `GenServer`'s under the hood, and can send and receive messages just like any other server. To test the side effects of sending or receiving messages, simply message the view and use the `render` function to test the result: send(view.pid, {:set_temp, 50}) assert render(view) =~ "The temperature is: 50℉" ### Testing LiveViewNative.LiveComponents LiveViewNative.LiveComponents can be tested in two ways. One way is to use the same `render_component/2` function as function components. This will mount the LiveComponent and render it once, without testing any of its events: assert render_component(MyComponent, id: 123, user: %User{}) =~ "some markup in component" However, if you want to test how components are mounted by a LiveView and interact with ViewTree events, you must use the regular `live/3` macro to build the LiveView with the component and then scope events by passing the view and a **ViewTree selector** in a list: {:ok, view, _markup} = live(conn, "/users", _format: :gameboy) markup = view |> element("#user-13 a", "Delete") |> render_click() refute markup =~ "user-13" refute view |> element("#user-13") |> has_element?() In the example above, LiveView will lookup for an element with ID=user-13 and retrieve its `phx-target`. If `phx-target` points to a component, that will be the component used, otherwise it will fallback to the view. ''' @flash_cookie "__phoenix_flash__" require Phoenix.ConnTest require Phoenix.ChannelTest alias Phoenix.LiveView.{Diff, Socket} alias LiveViewNativeTest.{ClientProxy, ViewTree, Element, View, Upload, UploadClient} @doc """ Puts connect params to be used on LiveView connections. See `Phoenix.LiveView.get_connect_params/1`. """ def put_connect_params(conn, params) when is_map(params) do Plug.Conn.put_private(conn, :live_view_connect_params, params) end @doc """ Spawns a connected LiveView process. If a `path` is given, then a regular `get(conn, path, params)` is done and the page is upgraded to a LiveView. If no path is given, it assumes a previously rendered `%Plug.Conn{}` is given, which will be converted to a LiveView immediately. ## Examples {:ok, view, markup} = live(conn, "/path", _fromat: :gameboy) assert view.module == MyLive assert markup =~ "the count is 3" assert {:error, {:redirect, %{to: "/somewhere"}}} = live(conn, "/path", _format: :gameboy) """ defmacro live(conn, path \\ nil, params \\ []) do quote bind_quoted: binding(), generated: true do {path, params} = if (is_list(path) && params == []) do {nil, path} else {path, params} end params = params |> Keyword.put_new(:_format, :html) |> Keyword.take([:_format, :_interface]) |> Enum.into(%{}, fn {:_format, value} -> {"_format", LiveViewNative.Utils.stringify(value)} {:_interface, value} -> {"_interface", value} end) conn = Phoenix.LiveViewTest.put_connect_params(conn, params) cond do is_binary(path) -> uri = URI.parse(path) params = Map.merge( (Map.get(uri, :query) || "") |> URI.decode_query(), params ) path = %URI{ path: path, query: Plug.Conn.Query.encode(params) } |> URI.to_string() LiveViewNativeTest.__live__(get(conn, path), path) is_nil(path) -> LiveViewNativeTest.__live__(conn) true -> raise RuntimeError, "path must be nil or a binary, got: #{inspect(path)}" end end end @doc """ Spawns a connected LiveView process mounted in isolation as the sole rendered element. Useful for testing LiveViews that are not directly routable, such as those built as small components to be re-used in multiple parents. Testing routable LiveViews is still recommended whenever possible since features such as live navigation require routable LiveViews. ## Options * `:session` - the session to be given to the LiveView * `:_format` - the format of the client connecting * `:_interface` - a map of interface state values from the client All other options are forwarded to the LiveView for rendering. Refer to `Phoenix.Component.live_render/3` for a list of supported render options. ## Examples {:ok, view, markup} = live_isolated(conn, MyAppWeb.ClockLive, session: %{"tz" => "EST"}) Use `put_connect_params/2` to put connect params for a call to `Phoenix.LiveView.get_connect_params/1` in `c:Phoenix.LiveView.mount/3`: {:ok, view, html} = conn |> put_connect_params(%{"param" => "value"}) |> live_isolated(AppWeb.ClockLive, session: %{"tz" => "EST"}, _format: :gameboy) """ defmacro live_isolated(conn, live_view, opts \\ []) do endpoint = Module.get_attribute(__CALLER__.module, :endpoint) params = opts |> Keyword.take([:_format, :_interface]) |> Enum.into(%{}, fn {:_format, value} -> {"_format", LiveViewNative.Utils.stringify(value)} {:_interface, value} -> {"_interface", value} end) |> Macro.escape() quote bind_quoted: binding(), unquote: true do unquote(__MODULE__).__isolated__(put_connect_params(conn, params), endpoint, live_view, opts) end end @doc false def __isolated__(conn, endpoint, live_view, opts) do put_in(conn.private[:phoenix_endpoint], endpoint || raise("no @endpoint set in test module")) |> Plug.Test.init_test_session(%{}) |> Phoenix.LiveView.Router.fetch_live_flash([]) |> Phoenix.LiveView.Controller.live_render(live_view, opts) |> connect_from_static_token(nil) end @doc false def __live__(%Plug.Conn{state: state, status: status} = conn) do path = rebuild_path(conn) case {state, status} do {:sent, 200} -> connect_from_static_token(conn, path) {:sent, 302} -> error_redirect_conn(conn) {:sent, _} -> raise ArgumentError, "request to #{conn.request_path} received unexpected #{status} response" {_, _} -> raise ArgumentError, """ a request has not yet been sent. live/1 must use a connection with a sent response. Either call get/3 prior to live/2, or use live/3 while providing a path to have a get request issued for you. For example issuing a get yourself: {:ok, view, _body} = conn |> get("#{path}") |> live(_format: :gameboy) or performing the GET and live connect in a single step: {:ok, view, _body} = live(conn, "#{path}", _format: :gameboy) """ end end @doc false def __live__(conn, path) do connect_from_static_token(conn, path) end defp connect_from_static_token( %Plug.Conn{status: 200, assigns: %{live_module: live_module}} = conn, path ) do ViewTree.ensure_loaded!() router = try do Phoenix.Controller.router_module(conn) rescue KeyError -> nil end start_proxy(path, %{ response: Phoenix.ConnTest.response(conn, 200), connect_params: conn.private[:live_view_connect_params] || %{}, connect_info: conn.private[:live_view_connect_info] || prune_conn(conn) || %{}, live_module: live_module, router: router, endpoint: Phoenix.Controller.endpoint_module(conn), session: maybe_get_session(conn), url: Plug.Conn.request_url(conn) }) end defp connect_from_static_token(%Plug.Conn{status: 200}, _path) do {:error, :nosession} end defp connect_from_static_token(%Plug.Conn{status: redir} = conn, _path) when redir in [301, 302] do error_redirect_conn(conn) end defp prune_conn(conn) do %{conn | resp_body: nil, resp_headers: []} end defp error_redirect_conn(conn) do to = hd(Plug.Conn.get_resp_header(conn, "location")) opts = if flash = conn.assigns[:flash] || conn.private[:phoenix_flash] do %{to: to, flash: flash} else %{to: to} end {:error, {error_redirect_key(conn), opts}} end defp error_redirect_key(%{private: %{phoenix_live_redirect: true}}), do: :live_redirect defp error_redirect_key(_), do: :redirect defp start_proxy(path, %{} = opts) do ref = make_ref() %{"_format" => format} = opts.connect_params %{test_client: client} = LiveViewNative.fetch_plugin!(format) opts = Map.merge(opts, %{ caller: {self(), ref}, body: opts.response, client: client, connect_params: opts.connect_params, connect_info: opts.connect_info, live_module: opts.live_module, endpoint: opts.endpoint, session: opts.session, url: opts.url, test_supervisor: fetch_test_supervisor!() }) case ClientProxy.start_link(opts) do {:ok, _} -> receive do {^ref, {:ok, view, body}} -> {:ok, view, body} end {:error, reason} -> exit({reason, {__MODULE__, :live, [path]}}) :ignore -> receive do {^ref, {:error, reason}} -> {:error, reason} end end end defp fetch_test_supervisor!() do case ExUnit.fetch_test_supervisor() do {:ok, sup} -> sup :error -> raise ArgumentError, "LiveView helpers can only be invoked from the test process" end end defp maybe_get_session(%Plug.Conn{} = conn) do try do Plug.Conn.get_session(conn) rescue _ -> %{} end end defp rebuild_path(%Plug.Conn{request_path: request_path, query_string: ""}), do: request_path defp rebuild_path(%Plug.Conn{request_path: request_path, query_string: query_string}), do: request_path <> "?" <> query_string @doc """ Renders a component. The first argument may either be a function component, as an anonymous function: assert render_component(&Weather.city/1, name: "Kraków") =~ "some markup in component" Or a stateful component as a module. In this case, this function will mount, update, and render the component. The `:id` option is a required argument: assert render_component(MyComponent, id: 123, user: %User{}) =~ "some markup in component" If your component is using the router, you can pass it as argument: assert render_component(MyComponent, %{id: 123, user: %User{}}, router: SomeRouter) =~ "some markup in component" """ defmacro render_component(component, assigns \\ Macro.escape(%{}), opts \\ []) do endpoint = Module.get_attribute(__CALLER__.module, :endpoint) component = if is_atom(component) do quote do unquote(component).__live__() unquote(component) end else component end quote do Phoenix.LiveViewTest.__render_component__( unquote(endpoint), unquote(component), unquote(assigns), unquote(opts) ) end end @doc false def __render_component__(endpoint, component, assigns, opts) when is_atom(component) do socket = %Socket{endpoint: endpoint, router: opts[:router]} assigns = Map.new(assigns) # TODO: Make the ID required once we support only stateful module components as live_component mount_assigns = if assigns[:id], do: %{myself: %Phoenix.LiveComponent.CID{cid: -1}}, else: %{} socket |> Diff.component_to_rendered(component, assigns, mount_assigns) |> rendered_to_diff_string(socket) end def __render_component__(endpoint, function, assigns, opts) when is_function(function, 1) do socket = %Socket{endpoint: endpoint, router: opts[:router]} assigns |> Map.new() |> Map.put_new(:__changed__, %{}) |> function.() |> rendered_to_diff_string(socket) end defp rendered_to_diff_string(rendered, socket) do {_, diff, _} = Diff.render(socket, rendered, Diff.new_components()) diff |> Diff.to_iodata() |> IO.iodata_to_binary() end @doc ~S''' Converts a rendered template to a string. ## Examples import LiveViewNative.Component import LiveViewNativeTest test "greets" do assigns = %{} assert rendered_to_string(~LVN""" """) == "Hello, Mary!" end ''' def rendered_to_string(rendered) do rendered |> Phoenix.HTML.html_escape() |> Phoenix.HTML.safe_to_string() end @doc """ Sends a click event given by `element` and returns the rendered result. The `element` is created with `element/3` and must point to a single element on the page with a `phx-click` attribute in it. The event name given set on `phx-click` is then sent to the appropriate LiveView (or component if `phx-target` is set accordingly). All `phx-value-*` entries in the element are sent as values. Extra values can be given with the `value` argument. If the element does not have a `phx-click` attribute but it is a link (the `` tag), the link will be followed accordingly: * if the link is a `patch`, the current view will be patched * if the link is a `navigate`, this function will return `{:error, {:live_redirect, %{to: url}}}`, which can be followed with `follow_redirect/2` * if the link is a regular link, this function will return `{:error, {:redirect, %{to: url}}}`, which can be followed with `follow_redirect/2` It returns the contents of the whole LiveView or an `{:error, redirect}` tuple. ## Examples {:ok, view, body} = live(conn, "/thermo", _format: :gameboy) assert view |> element("button", "Increment") |> render_click() =~ "The temperature is: 30℉" """ def render_click(element, value \\ %{}) def render_click(%Element{} = element, value), do: render_event(element, :click, value) def render_click(view, event), do: render_click(view, event, %{}) @doc """ Sends a click `event` to the `view` with `value` and returns the rendered result. It returns the contents of the whole LiveView or an `{:error, redirect}` tuple. ## Examples {:ok, view, body} = live(conn, "/thermo", _format: :gameboy) assert body =~ "The temperature is: 30℉" assert render_click(view, :inc) =~ "The temperature is: 31℉" """ def render_click(view, event, value) do render_event(view, :click, event, value) end @doc """ Puts the submitter `element_or_selector` on the given `form` element. A submitter is an element that initiates the form's submit event on the client. When a submitter is put on an element created with `form/3` and then the form is submitted via `render_submit/2`, the name/value pair of the submitter will be included in the submit event payload. The given element or selector must exist within the form and match one of the following: - A `button` or `input` element with `type="submit"`. - A `button` element without a `type` attribute. ## Examples form = view |> form("#my-form") assert form |> put_submitter("button[name=example]") |> render_submit() =~ "Submitted example" """ def put_submitter(form, element_or_selector) def put_submitter(%Element{proxy: proxy} = form, submitter) when is_binary(submitter) do put_submitter(form, %Element{proxy: proxy, selector: submitter}) end def put_submitter(%Element{} = form, %Element{} = submitter) do %{form | meta: Map.put(form.meta, :submitter, submitter)} end @doc """ Sends a form submit event given by `element` and returns the rendered result. The `element` is created with `element/3` and must point to a single element on the page with a `phx-submit` attribute in it. The event name given set on `phx-submit` is then sent to the appropriate LiveView (or component if `phx-target` is set accordingly). All `phx-value-*` entries in the element are sent as values. Extra values, including hidden input fields, can be given with the `value` argument. It returns the contents of the whole LiveView or an `{:error, redirect}` tuple. ## Examples {:ok, view, body} = live(conn, "/thermo", _format: :gameboy) assert view |> element("form") |> render_submit(%{deg: 123, avatar: upload}) =~ "123 exceeds limits" To submit a form along with some with hidden input values: assert view |> form("#term", user: %{name: "hello"}) |> render_submit(%{user: %{"hidden_field" => "example"}}) =~ "Name updated" To submit a form by a specific submit element via `put_submitter/2`: assert view |> form("#term", user: %{name: "hello"}) |> put_submitter("button[name=example_action]") |> render_submit() =~ "Action taken" """ def render_submit(element, value \\ %{}) def render_submit(%Element{} = element, value), do: render_event(element, :submit, value) def render_submit(view, event), do: render_submit(view, event, %{}) @doc """ Sends a form submit event to the view and returns the rendered result. It returns the contents of the whole LiveView or an `{:error, redirect}` tuple. ## Examples {:ok, view, body} = live(conn, "/thermo", _format: :gameboy) assert body =~ "The temp is: 30℉" assert render_submit(view, :refresh, %{deg: 32}) =~ "The temp is: 32℉" """ def render_submit(view, event, value) do render_event(view, :submit, event, value) end @doc """ Sends a form change event given by `element` and returns the rendered result. The `element` is created with `element/3` and must point to a single element on the page with a `phx-change` attribute in it. The event name given set on `phx-change` is then sent to the appropriate LiveView (or component if `phx-target` is set accordingly). All `phx-value-*` entries in the element are sent as values. If you need to pass any extra values or metadata, such as the "_target" parameter, you can do so by giving a map under the `value` argument. It returns the contents of the whole LiveView or an `{:error, redirect}` tuple. ## Examples {:ok, view, body} = live(conn, "/thermo", _format: :gameboy) assert view |> element("form") |> render_change(%{deg: 123}) =~ "123 exceeds limits" # Passing metadata {:ok, view, body} = live(conn, "/thermo", _format: :gameboy) assert view |> element("form") |> render_change(%{_target: ["deg"], deg: 123}) =~ "123 exceeds limits" As with `render_submit/2`, hidden input field values can be provided like so: refute view |> form("#term", user: %{name: "hello"}) |> render_change(%{user: %{"hidden_field" => "example"}}) =~ "can't be blank" """ def render_change(element, value \\ %{}) def render_change(%Element{} = element, value), do: render_event(element, :change, value) def render_change(view, event), do: render_change(view, event, %{}) @doc """ Sends a form change event to the view and returns the rendered result. It returns the contents of the whole LiveView or an `{:error, redirect}` tuple. ## Examples {:ok, view, body} = live(conn, "/thermo", _format: :gameboy) assert body =~ "The temp is: 30℉" assert render_change(view, :validate, %{deg: 123}) =~ "123 exceeds limits" """ def render_change(view, event, value) do render_event(view, :change, event, value) end @doc """ Sends a keydown event given by `element` and returns the rendered result. The `element` is created with `element/3` and must point to a single element on the page with a `phx-keydown` or `phx-window-keydown` attribute in it. The event name given set on `phx-keydown` is then sent to the appropriate LiveView (or component if `phx-target` is set accordingly). All `phx-value-*` entries in the element are sent as values. Extra values can be given with the `value` argument. It returns the contents of the whole LiveView or an `{:error, redirect}` tuple. ## Examples {:ok, view, body} = live(conn, "/thermo", _format: :gameboy) assert body =~ "The temp is: 30℉" assert view |> element("#inc") |> render_keydown() =~ "The temp is: 31℉" """ def render_keydown(element, value \\ %{}) def render_keydown(%Element{} = element, value), do: render_event(element, :keydown, value) def render_keydown(view, event), do: render_keydown(view, event, %{}) @doc """ Sends a keydown event to the view and returns the rendered result. It returns the contents of the whole LiveView or an `{:error, redirect}` tuple. ## Examples {:ok, view, body} = live(conn, "/thermo", _format: :gameboy) assert body =~ "The temp is: 30℉" assert render_keydown(view, :inc) =~ "The temp is: 31℉" """ def render_keydown(view, event, value) do render_event(view, :keydown, event, value) end @doc """ Sends a keyup event given by `element` and returns the rendered result. The `element` is created with `element/3` and must point to a single element on the page with a `phx-keyup` or `phx-window-keyup` attribute in it. The event name given set on `phx-keyup` is then sent to the appropriate LiveView (or component if `phx-target` is set accordingly). All `phx-value-*` entries in the element are sent as values. Extra values can be given with the `value` argument. It returns the contents of the whole LiveView or an `{:error, redirect}` tuple. ## Examples {:ok, view, body} = live(conn, "/thermo", _format: :gameboy) assert body =~ "The temp is: 30℉" assert view |> element("#inc") |> render_keyup() =~ "The temp is: 31℉" """ def render_keyup(element, value \\ %{}) def render_keyup(%Element{} = element, value), do: render_event(element, :keyup, value) def render_keyup(view, event), do: render_keyup(view, event, %{}) @doc """ Sends a keyup event to the view and returns the rendered result. It returns the contents of the whole LiveView or an `{:error, redirect}` tuple. ## Examples {:ok, view, body} = live(conn, "/thermo", _format: :gameboy) assert body =~ "The temp is: 30℉" assert render_keyup(view, :inc) =~ "The temp is: 31℉" """ def render_keyup(view, event, value) do render_event(view, :keyup, event, value) end @doc """ Sends a blur event given by `element` and returns the rendered result. The `element` is created with `element/3` and must point to a single element on the page with a `phx-blur` attribute in it. The event name given set on `phx-blur` is then sent to the appropriate LiveView (or component if `phx-target` is set accordingly). All `phx-value-*` entries in the element are sent as values. Extra values can be given with the `value` argument. It returns the contents of the whole LiveView or an `{:error, redirect}` tuple. ## Examples {:ok, view, body} = live(conn, "/thermo", _format: :gameboy) assert view |> element("#inactive") |> render_blur() =~ "Tap to wake" """ def render_blur(element, value \\ %{}) def render_blur(%Element{} = element, value), do: render_event(element, :blur, value) def render_blur(view, event), do: render_blur(view, event, %{}) @doc """ Sends a blur event to the view and returns the rendered result. It returns the contents of the whole LiveView or an `{:error, redirect}` tuple. ## Examples {:ok, view, body} = live(conn, "/thermo", _format: :gameboy) assert body =~ "The temp is: 30℉" assert render_blur(view, :inactive) =~ "Tap to wake" """ def render_blur(view, event, value) do render_event(view, :blur, event, value) end @doc """ Sends a focus event given by `element` and returns the rendered result. The `element` is created with `element/3` and must point to a single element on the page with a `phx-focus` attribute in it. The event name given set on `phx-focus` is then sent to the appropriate LiveView (or component if `phx-target` is set accordingly). All `phx-value-*` entries in the element are sent as values. Extra values can be given with the `value` argument. It returns the contents of the whole LiveView or an `{:error, redirect}` tuple. ## Examples {:ok, view, body} = live(conn, "/thermo", _format: :gameboy) assert view |> element("#inactive") |> render_focus() =~ "Tap to wake" """ def render_focus(element, value \\ %{}) def render_focus(%Element{} = element, value), do: render_event(element, :focus, value) def render_focus(view, event), do: render_focus(view, event, %{}) @doc """ Sends a focus event to the view and returns the rendered result. It returns the contents of the whole LiveView or an `{:error, redirect}` tuple. ## Examples {:ok, view, body} = live(conn, "/thermo", _format: :gameboy) assert body =~ "The temp is: 30℉" assert render_focus(view, :inactive) =~ "Tap to wake" """ def render_focus(view, event, value) do render_event(view, :focus, event, value) end @doc """ Sends a hook event to the view or an element and returns the rendered result. It returns the contents of the whole LiveView or an `{:error, redirect}` tuple. ## Examples {:ok, view, body} = live(conn, "/thermo", _format: :gameboy) assert body =~ "The temp is: 30℉" assert render_hook(view, :refresh, %{deg: 32}) =~ "The temp is: 32℉" If you are pushing events from a hook to a component, then you must pass an `element`, created with `element/3`, as first argument and it must point to a single element on the page with a `phx-target` attribute in it: {:ok, view, _body} = live(conn, "/thermo", _format, :gameboy) assert view |> element("#thermo-component") |> render_hook(:refresh, %{deg: 32}) =~ "The temp is: 32℉" """ def render_hook(view_or_element, event, value \\ %{}) def render_hook(%Element{} = element, event, value) do render_event(%{element | event: to_string(event)}, :hook, value) end def render_hook(view, event, value) do render_event(view, :hook, event, value) end defp render_event(%Element{} = element, type, value) when is_map(value) or is_list(value) do call(element, {:render_event, element, type, value}) end defp render_event(%View{} = view, type, event, value) when is_map(value) or is_list(value) do call(view, {:render_event, {proxy_topic(view), to_string(event), view.target}, type, value}) end @doc """ Awaits all current `assign_async` and `start_async` for a given LiveView or element. It renders the LiveView or Element once complete and returns the result. The default `timeout` is [ExUnit](https://hexdocs.pm/ex_unit/ExUnit.html#configure/1)'s `assert_receive_timeout` (100 ms). ## Examples {:ok, lv, body} = live(conn, "/path", _format: :gameboy) assert body =~ "loading data..." assert render_async(lv) =~ "data loaded!" """ def render_async( view_or_element, timeout \\ Application.fetch_env!(:ex_unit, :assert_receive_timeout) ) do pids = case view_or_element do %View{} = view -> call(view, {:async_pids, {proxy_topic(view), nil, nil}}) %Element{} = element -> call(element, {:async_pids, element}) end timeout_ref = make_ref() Process.send_after(self(), {timeout_ref, :timeout}, timeout) pids |> Enum.map(&Process.monitor(&1)) |> Enum.each(fn ref -> receive do {^timeout_ref, :timeout} -> raise RuntimeError, "expected async processes to finish within #{timeout}ms" {:DOWN, ^ref, :process, _pid, _reason} -> :ok end end) if !Process.cancel_timer(timeout_ref) do receive do {^timeout_ref, :timeout} -> :noop after 0 -> :noop end end render(view_or_element) end @doc """ Simulates a `push_patch` to the given `path` and returns the rendered result. """ def render_patch(%View{} = view, path) when is_binary(path) do call(view, {:render_patch, proxy_topic(view), path}) end @doc """ Returns the current list of LiveView children for the `parent` LiveView. Children are returned in the order they appear in the rendered template body. ## Examples {:ok, view, _body} = live(conn, "/thermo", _format: :gameboy) assert [clock_view] = live_children(view) assert render_click(clock_view, :snooze) =~ "snoozing" """ def live_children(%View{} = parent) do call(parent, {:live_children, proxy_topic(parent)}) end @doc """ Gets the nested LiveView child by `child_id` from the `parent` LiveView. ## Examples {:ok, view, _body} = live(conn, "/thermo", _format: :gameboy) assert clock_view = find_live_child(view, "clock") assert render_click(clock_view, :snooze) =~ "snoozing" """ def find_live_child(%View{} = parent, child_id) do parent |> live_children() |> Enum.find(fn %View{id: id} -> id == child_id end) end @doc """ Checks if the given element exists on the page. ## Examples assert view |> element("#some-element") |> has_element?() """ def has_element?(%Element{} = element) do call(element, {:render_element, :has_element?, element}) end defguardp is_text_filter(text_filter) when is_binary(text_filter) or is_struct(text_filter, Regex) or is_nil(text_filter) @doc """ Checks if the given `selector` with `text_filter` is on `view`. See `element/3` for more information. ## Examples assert has_element?(view, "#some-element") """ def has_element?(%View{} = view, selector, text_filter \\ nil) when is_binary(selector) and is_text_filter(text_filter) do has_element?(element(view, selector, text_filter)) end @doc """ Returns the string template body of the rendered view or element. If a view is provided, the entire LiveView is rendered. If a view after calling `with_target/2` or an element are given, only that particular context is returned. ## Examples {:ok, view, _body} = live(conn, "/thermo", _format: :gameboy) assert render(view) =~ ~s|