germsvel / phoenix_test

PhoenixTest provides a unified way of writing feature tests -- regardless of whether you're testing LiveView pages or static (non-LiveView) pages.
https://hex.pm/packages/phoenix_test
MIT License
181 stars 23 forks source link

Test using hidden inputs #37

Closed revoltaxz closed 8 months ago

revoltaxz commented 9 months ago

Hello! I'm trying to test a Live page with a form with a LiveSelect component. The LiveSelect provides a component with a hidden input with value, but when I try using the fill_form or submit_form to pass the arguments, I get the following error:

######### Test ############# 
 form_attrs = %{
        symbol: "PETR",
        category: "stock",
        symbol_text_input: "PETR4",
        price: "12.50",
        quantity: 100,
        open_date: "2024-04-01",
        direction: "short"
      }

      conn
      |> visit("/strategy-builder")
      |> submit_form("#position_form", position: form_attrs)
      |> open_browser()

####### Error ##########
 test/myield_web/live/strategy_builder_live/index_test.exs:5
     ** (ArgumentError) value for hidden "position[symbol]" must be one of [""], got: "PETR"
     code: |> submit_form("#position_form", position: form_attrs)
     stacktrace:
       (phoenix_live_view 0.20.9) lib/phoenix_live_view/test/live_view_test.ex:1102: Phoenix.LiveViewTest.call/2
       (phoenix_test 0.2.7) lib/phoenix_test/live.ex:127: PhoenixTest.Driver.PhoenixTest.Live.submit_form/3
       test/myield_web/live/strategy_builder_live/index_test.exs:19: (test)

Is there a way to test with hidden inputs in the form?

germsvel commented 9 months ago

Hi @revoltaxz, thanks for opening this issue!

I think I see why your form is failing, and I think it's something we should fix.

The problem

PhoenixTest uses LiveViewTest's render_submit/2 helper for LiveView form submissions.

From the docs we can see:

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"

But right now, we're passing all the form data to form/3 -- so we use render_submit/2 without passing hidden values there.

We need to update the code to pass hidden inputs through render_submit/2's second argument.

I'll try to get to that when I can, but if someone wants to take a stab at it first, feel free!

I don't think it'll be too straightforward since we have to parse the form and pull the hidden fields. But we already do some parsing, so it might not be too complicated either.

germsvel commented 9 months ago

@revoltaxz could you share the HTML form you're trying to test that's giving you that error? I'm trying to reproduce this issue, but not hitting it for some reason.

I'm now thinking that the current implementation works (regardless of what the docs say).

And I'm guessing that you're getting that error because your hidden field's HTML doesn't have a value attribute set. That's why you're seeing the must be one of [""] error.

revoltaxz commented 9 months ago

@germsvel Sorry my late! Yes, of course, I could:

 def render(assigns) do
    ~H"""
    <div>
      <.simple_form
        for={@form}
        id={@id}
        phx-submit="submit"
        phx-target={@myself}
        phx-change="validate"
      >
        <div class="grid xl:grid-cols-2 lg:grid-cols-2 md:grid-cols-2 sm:grid-cols-1 gap-3">
          <.live_select
            field={@form[:symbol]}
            label="Buscar por"
            placeholder="Insira ao menos 4 letras"
            phx-target={@myself}
            phx-focus="clear"
            debounce="500"
            options={[]}
            update_min_len={4}
            dropdown_extra_class="max-h-60 overflow-y-scroll"
          />
          <.field
            field={@form[:category]}
            label="Tipo de posição"
            type="select"
            options={Helpers.asset_types()}
            phx-change="change_category"
          />
          <.field
            field={@form[:quantity]}
            label="Quantidade"
            type="number"
            step="100"
            min="100"
            value="100"
          />
          <.field field={@form[:price]} label="Valor unitário (R$)" type="text" phx-hook="MoneyMask" />
          <.field field={@form[:open_date]} label="Data de compra" type="date" />

          <%= if @category == "option" do %>
            <.field
              field={@form[:contract_type]}
              label="Tipo de opção"
              type="select"
              options={Helpers.options_types()}
            />
          <% end %>
        </div>

        <.field
          field={@form[:direction]}
          label="Tipo de operação"
          type="radio-group"
          group_layout="col"
          options={Helpers.operation_types()}
        />
        <.button type="submit" phx-disable-with="Salvando..." label="Adicionar" />
      </.simple_form>
    </div>
    """
  end

OBS: I'm using the LiveSelect.

germsvel commented 9 months ago

@revoltaxz thanks for posting the form. I've never used LiveSelect. I don't see a hidden input there. I imagine the LiveSelect library is doing some hidden input stuff behind the scenes?

Do you know how you would test that with LiveView? Or how were you testing that previously?

revoltaxz commented 9 months ago

@germsvel No, I was trying to write tests using PhoenixTest, but I found a PR on the Live Select repo with some instructions, so I think that it is possible to pass the hidden input as you mentioned here. Do you think this helps you?

germsvel commented 9 months ago

@revoltaxz I don't think it's a matter of whether or not we can test the hidden inputs.

If you look at this branch, I added a test that passes when testing hidden inputs.

I think the issue is that whatever hidden input LiveSelect is using isn't setting a value properly (or maybe it sets it with JS or something). But that's why you're seeing the error you're seeing:

** (ArgumentError) value for hidden "position[symbol]" must be one of [""], got: "PETR"

LiveViewTest is saying that position[symbol] must be one of <potential value in html element>, but it got "PETR" instead.

So, to give you another example, if you had this HTML

<input type="hidden" name="admin" value="true"/>

And you tried testing it like this:

     |> submit_form("#position_form", admin: "Roger")

You would get the error:

** (ArgumentError) value for hidden "admin" must be one of ["true"], got: "Roger"

So, I don't think it's an issue with PhoenixTest. It's an issue of LiveSelect doing something with hidden inputs that is not matching what you're passing to your form.

Does that make sense?

revoltaxz commented 8 months ago

Oh yes, it makes sense! So, I will close this issue and try a workaround with LiveSelect! Thank you for helping me!

maxmarcon commented 5 months ago

Late to the party 😃 I just stumbled upon this thread yesterday by chance...

@revoltaxz @germsvel I have no experience with PhoenixTest (seems like a cool library btw, I will take a look!), but in order to test a form containing a LiveSelect input with the standard Phoenix.LiveViewTest functions the following trick should work:

form_attrs = %{
        symbol: "PETR",
        category: "stock",
        symbol_text_input: "PETR4",
        price: "12.50",
        quantity: 100,
        open_date: "2024-04-01",
        direction: "short"
}

Phoenix.LiveView.send_update(
      live.pid, # live = your test's live view
      LiveSelect.Component,
      %{id: live_select_id, value: form_attrs.symbol}
     # `live_select_id` is the id of your live select input component
     # (you can pass it explicitly, otherwise, one is generated automatically from form + field name)
)

conn
|> form("#position_form", form_attrs)
|> render_submit()

I tested it and it worked for me.

Yes,LiveSelect does use a hidden input to send the value to the form, and the hidden input's value is set dynamically according to what the user has selected. The value is not set with JS, it's an ordinary LV binding. However, the interaction with the user (i.e. opening the dropdown and selecting an element) does use JS, and that doesn't run in LV tests of course.

So the send_update/3 call above is used to bypasss the user interaction and programmatically force a selection, which will set the value for the hidden input field.

I think I will add a section to the docs documenting this, since quite a few folks asked similar questions already

Thanks!