DomHelpers
is a collection of helpers for creating selectors and ways of accessing different parts of the dom.
DomHelpers
also accept as documents anything like Phoenix.LiveViewTest.View
s, Phoenix.LiveViewTest.Element
s or
Plug.Conn
s.
DomHelpers
is built on top of Floki
to provide some sweet helpers for your tests.
Add the following package to mix.exs
:
def deps do
[
{:dom_helpers, "~> 0.3.0", only: [:dev, :test]}
]
end
ExUnit does not have an API for implementing assertions with support for custom messages. That makes that when an assertion
like has_element?/2
or has_element?/3
fails, we get a huge error printing out either the view struct or the html passed
in. Furthermore, the API for testing dead controllers and dead views are different. find/2
can be used with views and elements
in LiveView testing, with controllers when in a classic Phoenix app, and with any kind of html anywhere.
Furthermore, dom_helpers
encourage better testing practices (=~
I'm looking at you) by providing easy ways to access
different part of the DOM. For example, text/3
provides several pros over has_element?/3
or render(view) =~
:
- Easier debugging when a test fails. True story, the first problem I had while maintaining an app was that a formatted date
was not matching the expected output. In that same page there were over 8 different dates, and the matcher was
render(view) =~ formatted_date
, so most of the time was figuring out which of the dates was supposed to match. While it might be obvious for someone used to the codebase, new developers will struggle with this. - Better error when failing. When
has_element?/3
fails, you get a dump of the whole struct or html and the other arguments. Whentext/3 == some_text
fails, you get a nice diff with the differences. - Tests are easier to reason about. I've seen and experiences problems with tests that were testing things like dates, percentages,
numbers, etc. In these cases, having some assertion like
assert has_element?(view, date_selector, "2nd Feb")
can easily give false positives, as"22nd Feb"
will match without problem.
Selectors can get really complicated and hard to parse when reading. For example, reading:
with_attrs("input", %{
checked: true,
type: "checkbox",
value: "t",
class: {:contains_word, "primary"},
name: {:ends_with, "[remember_me]"}
})
is much easier than reading:
~s/input["name"$="[remember_me]"]["type"="checkbox"]["value"="t"]["checked"]["class"~="primary"]
or even:
~s/input[name$="[remember_me]"][type=checkbox][value=t][checked][class~=primary]
It is more verbose, but the intent is clearer.
They are also chainable: you can easily create more complex selectors for your app based on these.
input_being_tested = "input" |> with_attrs(type: "checkbox", name: {:ends_with, "[remember_me]"}) |> without_class("secondary")
assert has_element?(view, with_attr(input_being_tested, :checked))
# Some actions
assert has_element?(view, without_attr(input_being_tested, :checked))
Just add use DomHelpers
in your tests. It will import all helpers from the different modules.
If you are interested in more granular control, just manually alias
or import
each module.
There are several helpers for constructing complex selectors in DomHelpers.Selectors
.
There are many interesting helpers, but please, take special attention to:
with_attr/3
and how you can provide values to use different matchers.with_attrs/2
that lets you provide complex attribute selectors easily and in a readable way. It also handles some current caveats in Floki.
You can find some other helpers for dealing with classes and ids too.
Please, request extra helpers if you find anything you need.
Currently, the following accessors are available in DomHelpers.Accessors
:
attribute/2
andattribute/3
lets you access attributes values by the attribute name.classes/1
andclasses/2
let you access theclass
attribute as a list of classes. If you want the full, unparsed string, useattribute/2
orattribute/3
instead.find/2
let's you find all the elements that satisfy a given selector.find_count/2
is pretty much syntactic sugar forfind(html, selector) |> length()
find_first/2
is pretty much syntactic sugar forfind(html, selector) |> List.first()
. Frontend developers beware that while in JS we havequerySelector
andquerySelectorAll
, indom_helpers
by default all matching elements are returned.text/3
returns the text. Please, read the documentation for all the possibilities.
For more details on usage and some sweet examples, please refer to the linked documentation.
Please, request extra helpers if you find anything you need.