phoenixframework / phoenix_live_view

Rich, real-time user experiences with server-rendered HTML
https://hex.pm/packages/phoenix_live_view
MIT License
6k stars 906 forks source link

phx-value-* attributes on <form> are not sent in tests #3261

Closed teamon closed 1 month ago

teamon commented 1 month ago

Environment

Actual behavior

phx-value-* attributes on <form> are not sent when using render_submit in tests.

Form:

  def render(assigns) do
    ~H"""
    <form phx-submit="submit" phx-value-one="1">
      <input type="hidden" name="two" value="2"/>
      <button type="submit">Submit</button>
    </form>

    """
  end

  def handle_event("submit", params, socket) do
    # params is missing "one"
  end

Test:

  test "test" do
    conn = Phoenix.ConnTest.build_conn()
    conn = get(conn, "/")

    {:ok, view, _html} = live(conn)

    view
    |> element("form")
    |> render_submit()
  end

Expected behavior

phx-value-* attributes on <form> are sent in test the same way they are sent when using browser

Reproduction

Single-file example ```elixir Application.put_env(:sample, Example.Endpoint, http: [ip: {127, 0, 0, 1}, port: 5001], server: true, live_view: [signing_salt: "aaaaaaaa"], secret_key_base: String.duplicate("a", 64) ) Mix.install([ {:plug_cowboy, "~> 2.5"}, {:jason, "~> 1.0"}, {:phoenix, "~> 1.7"}, # please test your issue using the latest version of LV from GitHub! {:phoenix_live_view, github: "phoenixframework/phoenix_live_view", branch: "main", override: true}, {:floki, ">= 0.30.0"} ]) # build the LiveView JavaScript assets (this needs mix and npm available in your path!) path = Phoenix.LiveView.__info__(:compile)[:source] |> Path.dirname() |> Path.join("../") System.cmd("mix", ["deps.get"], cd: path, into: IO.binstream()) System.cmd("npm", ["install"], cd: Path.join(path, "./assets"), into: IO.binstream()) System.cmd("mix", ["assets.build"], cd: path, into: IO.binstream()) defmodule Example.Application do use Application def start(_type, _args) do children = [ Example.Endpoint ] opts = [strategy: :one_for_one, name: Example.Supervisor] Supervisor.start_link(children, opts) end end defmodule Example.ErrorView do def render(template, _), do: Phoenix.Controller.status_message_from_template(template) end defmodule Example.HomeLive do use Phoenix.LiveView, layout: {__MODULE__, :live} def mount(_params, _session, socket) do {:ok, assign(socket, :last_params, %{})} end def render("live.html", assigns) do ~H""" <%!-- uncomment to use enable tailwind --%> <%!-- --%> <%= @inner_content %> """ end def render(assigns) do ~H"""
<%= inspect @last_params %>
""" end def handle_event("submit", params, socket) do {:noreply, assign(socket, :last_params, params)} end end defmodule Example.Router do use Phoenix.Router import Phoenix.LiveView.Router pipeline :browser do plug(:accepts, ["html"]) end scope "/", Example do pipe_through(:browser) live("/", HomeLive, :index) end end defmodule Example.Endpoint do use Phoenix.Endpoint, otp_app: :sample socket("/live", Phoenix.LiveView.Socket) plug(Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix") plug(Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view") plug(Example.Router) end ## Create and load sample app (necessary to allow Application.app_dir calls from Phoenix.LiveViewTest) ebin = String.replace_trailing(to_string(:code.lib_dir(:phoenix)), "/phoenix", "/sample/ebin") File.mkdir_p!(ebin) File.write!( Path.join(ebin, "sample.app"), """ {application, sample, [{description, "sample"}, {vsn, "1.0.0"}, {modules, ['Elixir.Example.Application']}, {registered, []}, {applications, [kernel, stdlib]}, {mod, {'Elixir.Example.Application', []}}, {env, []}]}. """ ) :code.add_path(to_charlist(ebin)) :application.load(:sample) :application.ensure_all_started(:sample) ExUnit.start(autorun: false) defmodule ExampleTest do use ExUnit.Case import Phoenix.ConnTest import Phoenix.LiveViewTest @endpoint Example.Endpoint test "test" do conn = Phoenix.ConnTest.build_conn() conn = get(conn, "/") {:ok, view, _html} = live(conn) view |> element("form") |> render_submit() end end ExUnit.run([ExampleTest]) Process.sleep(:infinity) ```

Workaround

Just in case someone needs this - you can use a hidden input instead:

<input type="hidden" name="one" value="1" />

This causes <form> to get removed - see https://github.com/phoenixframework/phoenix_live_view/issues/3262

josevalim commented 1 month ago

If they are submitted in the browser, we should fix this in tests. PRs welcome!

teamon commented 1 month ago

I can try to give it a shot. Any hints on where to start? 😅

josevalim commented 1 month ago

This is the code that deals with submitting forms: https://github.com/phoenixframework/phoenix_live_view/blob/main/lib/phoenix_live_view/test/client_proxy.ex#L1092

It seems we do not consider the attributes in the form itself and we probably should. There is already an existing function that converts phx-value-* into a map or similar (see DOM.all_values).