From 3efa4c04a7ba5f1e6e70c54ac1460b44d9e4409a Mon Sep 17 00:00:00 2001 From: German Velasco Date: Mon, 19 Feb 2024 09:21:42 -0500 Subject: [PATCH] Handle PUT/DELETE through hidden input What changed? ============= An HTML form's `method` can only be `get`, `post` or `dialog` (which we ignore). In order to send `PUT` or `DELETE` methods, form helpers (such as Phoenix's) embed a hidden input with `name="_method"` whose `value` is the operative or effective method. This commit updates our code to parse the "operative method" -- that is, the method that should take effect. By default it'll use the `form`'s method unless there's a hidden input, which will override the HTTP method we use when dispatching the request. For more, see [MDN's docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#method) --- lib/phoenix_test/html/form.ex | 18 ++++++++++ lib/phoenix_test/static.ex | 49 ++++++++++++++-------------- test/phoenix_test/html/form_test.exs | 39 +++++++++++++++++++++- test/phoenix_test/static_test.exs | 20 ++++++++++-- test/support/page_controller.ex | 3 +- test/support/page_view.ex | 20 ++++++++++-- 6 files changed, 117 insertions(+), 32 deletions(-) diff --git a/lib/phoenix_test/html/form.ex b/lib/phoenix_test/html/form.ex index f9025bd1..b52c50fa 100644 --- a/lib/phoenix_test/html/form.ex +++ b/lib/phoenix_test/html/form.ex @@ -7,6 +7,7 @@ defmodule PhoenixTest.Html.Form do %{} |> Map.put("attributes", build_attributes(attrs)) |> Map.put("fields", build_fields(fields)) + |> put_operative_method() end defp build_attributes(attrs) do @@ -41,6 +42,23 @@ defmodule PhoenixTest.Html.Form do Enum.map(options, &build_field/1) end + defp put_operative_method(form) do + method = hidden_input_method_value(form["fields"]) || form["attributes"]["method"] || "get" + + Map.put(form, "operative_method", method) + end + + defp hidden_input_method_value(fields) do + fields + |> Enum.find(:no_method_input, fn field -> + field["tag"] == "input" && field["attributes"]["name"] == "_method" + end) + |> case do + :no_method_input -> nil + field -> field["attributes"]["value"] + end + end + def validate_form_data!(form, form_data) do action = get_in(form, ["attributes", "action"]) unless action, do: raise(ArgumentError, "Expected form to have an action but found none") diff --git a/lib/phoenix_test/static.ex b/lib/phoenix_test/static.ex index f4ca9654..fb475736 100644 --- a/lib/phoenix_test/static.ex +++ b/lib/phoenix_test/static.ex @@ -73,6 +73,27 @@ defimpl PhoenixTest.Driver, for: PhoenixTest.Static do end end + def fill_form(session, selector, form_data) do + form = + session + |> render_html() + |> Query.find!(selector) + |> Html.Form.build() + + :ok = Html.Form.validate_form_data!(form, form_data) + + active_form = %{selector: selector, form_data: form_data, parsed: form} + + session + |> PhoenixTest.Static.put_private(:active_form, active_form) + end + + def submit_form(session, selector, form_data) do + session + |> fill_form(selector, form_data) + |> submit_active_form() + end + defp has_active_form?(session) do case PhoenixTest.Static.get_private(session, :active_form) do :not_found -> false @@ -93,7 +114,7 @@ defimpl PhoenixTest.Driver, for: PhoenixTest.Static do defp submit_active_form(session) do {form, session} = PhoenixTest.Static.pop_private(session, :active_form) action = form.parsed["attributes"]["action"] - method = form.parsed["attributes"]["method"] || "get" + method = form.parsed["operative_method"] session.conn |> dispatch(@endpoint, method, action, form.form_data) @@ -105,36 +126,16 @@ defimpl PhoenixTest.Driver, for: PhoenixTest.Static do session |> render_html() |> Query.find!("form", text) + |> Html.Form.build() - action = Html.attribute(form, "action") - method = Html.attribute(form, "method") || "get" + action = form["attributes"]["action"] + method = form["operative_method"] session.conn |> dispatch(@endpoint, method, action) |> maybe_redirect(session) end - def fill_form(session, selector, form_data) do - form = - session - |> render_html() - |> Query.find!(selector) - |> Html.Form.build() - - :ok = Html.Form.validate_form_data!(form, form_data) - - active_form = %{selector: selector, form_data: form_data, parsed: form} - - session - |> PhoenixTest.Static.put_private(:active_form, active_form) - end - - def submit_form(session, selector, form_data) do - session - |> fill_form(selector, form_data) - |> submit_active_form() - end - defp maybe_redirect(conn, session) do case conn do %{status: 302} -> diff --git a/test/phoenix_test/html/form_test.exs b/test/phoenix_test/html/form_test.exs index d911a7a1..4b3b96f9 100644 --- a/test/phoenix_test/html/form_test.exs +++ b/test/phoenix_test/html/form_test.exs @@ -4,7 +4,44 @@ defmodule PhoenixTest.Html.FormTest do alias PhoenixTest.Html alias PhoenixTest.Query - describe "parse/1" do + describe "build/1" do + test "sets get as the form's operative_method by default" do + data = + form_data(""" +
+
+ """) + + %{"operative_method" => method} = Html.Form.build(data) + + assert method == "get" + end + + test "sets form method as operative_method if present" do + data = + form_data(""" +
+
+ """) + + %{"operative_method" => method} = Html.Form.build(data) + + assert method == "post" + end + + test "sets operative_method based on hidden input if available" do + data = + form_data(""" +
+ +
+ """) + + %{"operative_method" => method} = Html.Form.build(data) + + assert method == "put" + end + test "includes attributes" do data = form_data(""" diff --git a/test/phoenix_test/static_test.exs b/test/phoenix_test/static_test.exs index 76e5c5c7..e0987b17 100644 --- a/test/phoenix_test/static_test.exs +++ b/test/phoenix_test/static_test.exs @@ -104,14 +104,14 @@ defmodule PhoenixTest.StaticTest do |> assert_has("h1", "Record received") end - test "handles a button clicks when button PUTs data", %{conn: conn} do + test "handles a button clicks when button PUTs data (hidden input)", %{conn: conn} do conn |> visit("/page/index") |> click_button("Mark as active") - |> assert_has("h1", "Marked active!") + |> assert_has("h1", "Record updated") end - test "handles a button clicks when button DELETEs data", %{conn: conn} do + test "handles a button clicks when button DELETEs data (hidden input)", %{conn: conn} do conn |> visit("/page/index") |> click_button("Delete record") @@ -228,6 +228,20 @@ defmodule PhoenixTest.StaticTest do |> assert_has("h1", "LiveView main page") end + test "handles when form PUTs data through hidden input", %{conn: conn} do + conn + |> visit("/page/index") + |> submit_form("#update-form", name: "Aragorn") + |> assert_has("#form-data", "name: Aragorn") + end + + test "handles a button clicks when button DELETEs data (hidden input)", %{conn: conn} do + conn + |> visit("/page/index") + |> click_button("Delete record") + |> assert_has("h1", "Record deleted") + end + test "raises an error if the form can't be found", %{conn: conn} do assert_raise ArgumentError, ~r/Could not find element with selector/, fn -> conn diff --git a/test/support/page_controller.ex b/test/support/page_controller.ex index dc47959c..5687ad73 100644 --- a/test/support/page_controller.ex +++ b/test/support/page_controller.ex @@ -20,8 +20,9 @@ defmodule PhoenixTest.PageController do |> render("record_created.html") end - def update(conn, _) do + def update(conn, params) do conn + |> assign(:params, params) |> render("record_updated.html") end diff --git a/test/support/page_view.ex b/test/support/page_view.ex index 8abf5c53..ddfac295 100644 --- a/test/support/page_view.ex +++ b/test/support/page_view.ex @@ -45,11 +45,13 @@ defmodule PhoenixTest.PageView do -
+ +
-
+ +
@@ -59,6 +61,12 @@ defmodule PhoenixTest.PageView do +
+ + + +
+
@@ -144,7 +152,13 @@ defmodule PhoenixTest.PageView do def render("record_updated.html", assigns) do ~H""" -

Marked active!

+

Record updated

+ +
+ <%= for {key, value} <- @params do %> + <%= render_input_data(key, value) %> + <% end %> +
""" end