From 7581dcdfabbf6eebf24e8df849409679293a0bcd Mon Sep 17 00:00:00 2001 From: Michael Crumm Date: Tue, 16 May 2023 12:03:44 -0700 Subject: [PATCH] Add LiveViewTest.put_submitter/2 (#2598) --- lib/phoenix_live_view/test/client_proxy.ex | 45 +++++++- lib/phoenix_live_view/test/live_view_test.ex | 42 +++++++- .../integrations/elements_test.exs | 100 ++++++++++++++++++ test/support/live_views/elements.ex | 13 +++ 4 files changed, 196 insertions(+), 4 deletions(-) diff --git a/lib/phoenix_live_view/test/client_proxy.ex b/lib/phoenix_live_view/test/client_proxy.ex index 85f009898..6b0261950 100644 --- a/lib/phoenix_live_view/test/client_proxy.ex +++ b/lib/phoenix_live_view/test/client_proxy.ex @@ -1056,8 +1056,10 @@ defmodule Phoenix.LiveViewTest.ClientProxy do end) |> Enum.reduce(%{}, &form_defaults/2) - case fill_in_map(Enum.to_list(element.form_data || %{}), "", node, []) do - {:ok, value} -> {:ok, DOM.deep_merge(defaults, value)} + with {:ok, defaults} <- maybe_submitter(defaults, type, node, element), + {:ok, value} <- fill_in_map(Enum.to_list(element.form_data || %{}), "", node, []) do + {:ok, DOM.deep_merge(defaults, value)} + else {:error, _, _} = error -> error end @@ -1073,6 +1075,45 @@ defmodule Phoenix.LiveViewTest.ClientProxy do {:ok, DOM.all_values(node)} end + defp maybe_submitter(defaults, :submit, form, %Element{meta: %{submitter: element}}) do + collect_submitter(form, element, defaults) + end + + defp maybe_submitter(defaults, _, _, _), do: {:ok, defaults} + + defp collect_submitter(form, element, defaults) do + case select_node(form, element) do + {:ok, node} -> collect_submitter(node, form, element, defaults) + {:error, _, msg} -> {:error, :invalid, "invalid form submitter, " <> msg} + end + end + + defp collect_submitter(node, form, element, defaults) do + name = DOM.attribute(node, "name") + + cond do + is_nil(name) -> + {:error, :invalid, + "form submitter selected by #{inspect(element.selector)} must have a name"} + + submitter?(node) and is_nil(DOM.attribute(node, "disabled")) -> + {:ok, Plug.Conn.Query.decode_pair({name, DOM.attribute(node, "value")}, defaults)} + + true -> + {:error, :invalid, + "could not find non-disabled submit input or button with name #{inspect(name)} within:\n\n" <> + DOM.inspect_html(DOM.all(form, "[name]"))} + end + end + + defp submitter?({"input", _, _} = node) do + DOM.attribute(node, "type") == "submit" + end + + defp submitter?({"button", _, _} = node) do + DOM.attribute(node, "type") in ["submit", nil] + end + defp maybe_push_events(diff, state) do case diff do %{@events => events} -> diff --git a/lib/phoenix_live_view/test/live_view_test.ex b/lib/phoenix_live_view/test/live_view_test.ex index 5868d8cb7..a0c529afc 100644 --- a/lib/phoenix_live_view/test/live_view_test.ex +++ b/lib/phoenix_live_view/test/live_view_test.ex @@ -570,6 +570,37 @@ defmodule Phoenix.LiveViewTest 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. @@ -594,8 +625,15 @@ defmodule Phoenix.LiveViewTest do 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" + |> 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 \\ %{}) diff --git a/test/phoenix_live_view/integrations/elements_test.exs b/test/phoenix_live_view/integrations/elements_test.exs index 4abec9328..44acf7d44 100644 --- a/test/phoenix_live_view/integrations/elements_test.exs +++ b/test/phoenix_live_view/integrations/elements_test.exs @@ -409,6 +409,15 @@ defmodule Phoenix.LiveView.ElementsTest do end end + test "put_submitter/2 puts submitter meta on element", %{live: view} do + selector = "button[name=submitter]" + + from_element = view |> element("form") |> put_submitter(element(view, selector)) + from_selector = view |> element("form") |> put_submitter(selector) + + assert from_element.meta.submitter == from_selector.meta.submitter + end + describe "render_submit" do test "raises if element is not a form", %{live: view} do assert_raise ArgumentError, "phx-submit is only allowed in forms, got \"a\"", fn -> @@ -426,6 +435,97 @@ defmodule Phoenix.LiveView.ElementsTest do assert view |> element("#empty-form") |> render_submit(%{"foo" => "bar"}) assert last_event(view) =~ ~s|form-submit: %{"foo" => "bar"}| end + + test "raises on invalid submitter", %{live: view} do + assert_raise ArgumentError, ~r"invalid form submitter", fn -> + assert view + |> element("#submitter-form") + |> put_submitter("#element-does-not-exist") + |> render_submit() + end + + assert_raise ArgumentError, ~r"invalid form submitter", fn -> + assert view + |> element("#submitter-form") + |> put_submitter("button") + |> render_submit() + end + + assert_raise ArgumentError, + ~r"form submitter selected by \"#input_no_name\" must have a name", + fn -> + assert view + |> element("#submitter-form") + |> put_submitter("#input_no_name") + |> render_submit() + end + + assert_raise ArgumentError, + ~r"could not find non-disabled submit input or button with name \"input_disabled\"", + fn -> + assert view + |> element("#submitter-form") + |> put_submitter("[name=input_disabled]") + |> render_submit() + end + + assert_raise ArgumentError, + ~r"could not find non-disabled submit input or button with name \"button_disabled\"", + fn -> + assert view + |> element("#submitter-form") + |> put_submitter("[name=button_disabled]") + |> render_submit() + end + + assert_raise ArgumentError, + ~r"could not find non-disabled submit input or button with name \"button_no_submit\"", + fn -> + assert view + |> element("#submitter-form") + |> put_submitter("[name=button_no_submit]") + |> render_submit() + end + end + + test "includes the submitter key/value pair in the payload", %{live: view} do + assert view + |> element("#submitter-form") + |> put_submitter("[name=input]") + |> render_submit() + + assert last_event(view) =~ ~s|form-submit: %{"data" => %{"a" => "b"}, "input" => "yes"}| + + assert view + |> element("#submitter-form") + |> put_submitter("input#data-nested") + |> render_submit() + + assert last_event(view) =~ ~s|form-submit: %{"data" => %{"a" => "b", "nested" => "yes"}}| + + assert view + |> element("#submitter-form") + |> put_submitter("[name=button]") + |> render_submit() + + assert last_event(view) =~ ~s|form-submit: %{"button" => "yes", "data" => %{"a" => "b"}}| + + assert view + |> element("#submitter-form") + |> put_submitter("[name=button_no_type]") + |> render_submit() + + assert last_event(view) =~ + ~s|form-submit: %{"button_no_type" => "yes", "data" => %{"a" => "b"}}| + + assert view + |> element("#submitter-form") + |> put_submitter("[name=button_no_value]") + |> render_submit() + + assert last_event(view) =~ + ~s|form-submit: %{"button_no_value" => "", "data" => %{"a" => "b"}}| + end end describe "follow_trigger_action" do diff --git a/test/support/live_views/elements.ex b/test/support/live_views/elements.ex index 310d0b4b8..4020a3a7e 100644 --- a/test/support/live_views/elements.ex +++ b/test/support/live_views/elements.ex @@ -131,6 +131,19 @@ defmodule Phoenix.LiveViewTest.ElementsLive do +
+ + + + + + + + + +
+