Browse lessons

Testing Form Changes

Newsletter form

Open up our router.ex. You will see we have a NewsletterLive mounted on /newsletter.

live "/newsletter", NewsletterLive

When we open the NewsletterLive we see a simple sign up form so people can sign up for our newsletter:

def render(assigns) do
  ~H"""
  <.simple_form :let={f} id="subscribe" phx-change="validate" for={@changeset}>

    <.input phx-debounce="500" type="email" field={{f, :email}} placeholder="email address" />
    <span class="text-sm text-gray-500">No spam. Unsubscribe anytime</span>

    <:actions>
      <.button>Subscribe</.button>
    </:actions>
  </.simple_form>
  """
end

def mount(_, _, socket) do
  changeset = Newsletter.new_subscription()

  {:ok, assign(socket, :changeset, changeset)}
end

def handle_event("validate", %{"subscription" => params}, socket) do
  changeset = params |> Newsletter.new_subscription() |> Map.put(:action, :insert)

  {:noreply, assign(socket, :changeset, changeset)}
end
  • Our render/1 function renders a simple form with an email input so that people can subscribe.
  • When we mount our LiveView, we set a new changeset for a subscription through the Newsletter module.
  • Finally, our form’s phx-change="validate" event comes into our handle_event/3 callback, and we create a new changeset with validations.

If you open up Newsletter.Subscription you’ll see that we’re validating the email just slightly:

# lib/ranger/newsletter/subscription.ex
def changeset(subscription, attrs) do
  subscription
  |> cast(attrs, [:email])
  |> validate_required([:email])
  |> validate_format(:email, ~r/.*@.*/)
end

Email validation is fraught with problems. Here, we only concern ourselves with whether or not the email has an @ sign.

Testing form changes

Let’s jump into writing a test. Our test is going to be very similar to the ones we wrote when submitting a form.

We’ll just use a different action function:

test "warns user of invalid email as user types", %{conn: conn} do
  {:ok, view, _} = live(conn, ~p"/newsletter")
  invalid_email = "something"

  html =
    view
    |> form("#subscribe", %{subscription: %{email: invalid_email}})
    |> render_change()

  assert html =~ "has invalid format"
end
  • We mount our NewsletterLive regularly and take the view.
  • We then target our form by id and pass the form data.
  • Finally, we perform the action with render_change/2.

By now, you should see that the primary way to interact with the LiveView page comes in that three-step process:

  • Use the view,
  • Target an element with element/3 or form/3, and
  • Perform a render_* action.

In this case, we’re grabbing the html returned and asserting that the error is present on the page.

Why use html instead of has_element?

You might wonder why I’m using the html returned instead of the has_element?/3 helper.

Well, I wanted to show that there’s nothing wrong with using the html returned by one of our action functions so long as we know that the text would not be present in the page in any other case.

In this test, the validation only shows up if it shows up due to our phx-change="validate" event rendering errors.

Of course, you can write the test with the has_element?/3 helper.

Perhaps you have several validation errors and want to make sure the correct one is saying "has invalid format".

Alternate render_change/3 function

Let’s quickly see the alternate render_change/3 function.

As we’ve seen with render_click/3 and render_submit/3 in previous lessons, sending the event directly bypasses our HTML, and thus the test doesn’t validate our HTML and can result in a false positive.

Let’s write it out anyway and confirm our suspicion:

test "warns user of invalid email as user types (target event directly)", %{conn: conn} do
  {:ok, view, _} = live(conn, ~p"/newsletter")
  invalid_email = "something"

  html = view |> render_change("validate", %{subscription: %{email: invalid_email}})

  assert html =~ "has invalid format"
end

In this case, we cannot delete the entire HTML because the error is rendered as part of the input component. So if we delete the HTML, the test will fail.

But the fact remains that the form/3 helper helps validate our HTML. To see that, let’s remove the phx-change attribute from our form:

- <.simple_form :let={f} id="subscribe" phx-change="validate" for={@changeset}>
+ <.simple_form :let={f} id="subscribe" for={@changeset}>

And now, let’s rerun our test:

mix test test/ranger_web/live/newsletter_live_done_test.exs:20
Compiling 1 file (.ex)
Excluding tags: [:test]
Including tags: [line: "20"]

.

Finished in 0.2 seconds (0.00s async, 0.2s sync)
2 tests, 0 failures, 1 excluded

As you can see, the test still passes, but production is broken because we don’t have the phx-change="validate" attribute defined in the form.

By comparison, let’s run our first test to see what kind of feedback the form/3 helper gives us:

mix test test/ranger_web/live/newsletter_live_done_test.exs:10
Excluding tags: [:test]
Including tags: [line: "10"]

  1) test warns user of invalid email as user types (RangerWeb.NewsletterLiveDoneTest)

     ** (ArgumentError) element selected by "#subscribe" does not have phx-change attribute

     code: |> render_change()

Finished in 0.2 seconds (0.00s async, 0.2s sync)
2 tests, 1 failure, 1 excluded

The test tells us that we don’t have a phx-change attribute on our form.

Much better.

That’s why we always use the form/3 along with the render action when testing forms.

Resources