Skip to content

Commit

Permalink
Handle PUT/DELETE through hidden input
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
germsvel committed Feb 19, 2024
1 parent a98f48d commit 3efa4c0
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 32 deletions.
18 changes: 18 additions & 0 deletions lib/phoenix_test/html/form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
49 changes: 25 additions & 24 deletions lib/phoenix_test/static.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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} ->
Expand Down
39 changes: 38 additions & 1 deletion test/phoenix_test/html/form_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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("""
<form id="user-form" action="/">
</form>
""")

%{"operative_method" => method} = Html.Form.build(data)

assert method == "get"
end

test "sets form method as operative_method if present" do
data =
form_data("""
<form id="user-form" action="/" method="post">
</form>
""")

%{"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("""
<form id="user-form" action="/" method="post">
<input type="hidden" name="_method" value="put"/>
</form>
""")

%{"operative_method" => method} = Html.Form.build(data)

assert method == "put"
end

test "includes attributes" do
data =
form_data("""
Expand Down
20 changes: 17 additions & 3 deletions test/phoenix_test/static_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion test/support/page_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 17 additions & 3 deletions test/support/page_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ defmodule PhoenixTest.PageView do
<button>Get record</button>
</form>
<form action="/page/update_record" method="put">
<form action="/page/update_record" method="post">
<input name="_method" type="hidden" value="put" />
<button>Mark as active</button>
</form>
<form action="/page/delete_record" method="delete">
<form action="/page/delete_record" method="post">
<input name="_method" type="hidden" value="delete" />
<button>Delete record</button>
</form>
Expand All @@ -59,6 +61,12 @@ defmodule PhoenixTest.PageView do
<button>Save</button>
</form>
<form id="update-form" action="/page/update_record" method="post">
<input name="_method" type="hidden" value="put" />
<label for="name">Name</label>
<input name="name" />
</form>
<form action="/page/create_record" method="post" id="no-submit-button-form">
<label for="name">Name</label>
<input name="name" />
Expand Down Expand Up @@ -144,7 +152,13 @@ defmodule PhoenixTest.PageView do

def render("record_updated.html", assigns) do
~H"""
<h1>Marked active!</h1>
<h1>Record updated</h1>
<div id="form-data">
<%= for {key, value} <- @params do %>
<%= render_input_data(key, value) %>
<% end %>
</div>
"""
end

Expand Down

0 comments on commit 3efa4c0

Please sign in to comment.