Problem
- Global HTML scope is sometimes too overwhelming to write CSS selectors for in
element/3
function. - Reading an element attribute to use in another CSS selector isn’t convenient
Example: targeting inputs inside inputs_for
Here are some options to target the name input of the third assoc row:
# test-specific ids, not ideal
has_element?(view, "#todo_list_todo_list_item_2_name", "Foobar")
# Rely on phoenix form attr encodings in the input name
has_element?("input[name='todo_list[item][2][name]", "Foobar")
# Checks HTML structure is sound, reads the input like an user (by label name)
[label_id] = view |> element("label", "Name") |> render() |> Floki.parse_fragment!() |> Floki.attribute("id")
has_element?(view, "li:nth-child(2) input[aria-labelledby='##{label_id}'][value='foobar']")
The approaches above have some inconveniences like:
- When the final element isn’t found, there’s no feedback where the CSS selector chain is breaking
- Test helper functions can’t compose really well, they end up being glorified css selector builders
- The mechanics of how to target an element in the tests obfuscates the testing intent
What am I asking for?
Proposal: allow nested element
calls
view
|> element("form", "Today's task list")
|> element("li:nth-child(2)")
|> has_element?("input[aria-labelledby='##{label_id}'][value='foobar']")
An equivalent solution without nesting isn’t convenient to achieve with the current API:
view
|> has_element("form:fl-contains('Today's task list') li-nth-child(2) input[aria-labelledby='##{label_id}'][value='foobar']")
Nesting element
calls:
- Enhance feedback why a single element couldn’t be targeted, broken down by each nesting level
- Enable composable domain specific selector-utility functions
Proposal: get_attribute/2
and has_attribute?/3
test functions
These two functions will make it easier to read html attributes without parsing html again:
# Before
[label_id] = view |> element("label", "Name") |> render() |> Floki.parse_fragment!() |> Floki.attribute("id")
# After
label_id = view |> element("label", "Name") |> get_attribute("id")
# Or
label_id = view |> get_attribute("label", "Name", "id")
And improve clarity on attributes validation:
# Before: failed because there's not button with text 'Save' or because it's not disabled?
assert has_element?(view, "button[disabled]", "Save")
# After: feedback if the problem is the button, the 'Save' text or the attribute
assert view |> element("button", "Save") |> has_attribute?("disabled")
# OR
assert view |> has_attribute?("button", "Save", "disabled")
Additional context
I come from a LiveView heavy product with complex forms. We observed that for our context, testing from the interface (LiveView) provides the greatest amount of confidence with least amount of effort, since the same test that verifies core business rules also tests if every layer in the application integrates correctly with each other.
We are also trying to write LiveView tests in a style to mimic user’s behaviour as much as possible. Like targeting HTML elements by the accessible name. We observed this to also maximize confidence, lower chance of regressions and increase test clarity.
Also, our Quality Engineers write E2E tests with https://playwright.dev/. We minimize the amount of breaking E2E tests by writing this style of tests which rely on HTML semantics and accessibility structure.
With these proposals implemented, it will enable us to implement a element_by_name
function more easily, which targets the elements by accessible name. I’d love to see a function like this being included in the framework
Would my suggestion benefit Phoenix or the community as a whole?
I’ve come to know a lot of Elixir developers with the majority of background being Backend development only, which are now writing a lot of frontend with LiveView. I believe by better supporting html-semantics-accessibility-centric web frontend testing, the framework would help improve the journey from backend-only to learning HTML/web practices guided by the LiveView testing functions.
Alternatives
Implement this proposal as a standalone live_view_test_extras
library ?
Please also consider volunteering to work on any implementations yourself
Yes, I would love to work on this.
Is my suggestion easily implemented?
On first look, both proposals seem feasible.
The has_attribute?
and get_attribute
ones seem fairly straight forward to implement on Phoenix.LiveViewTest.ClientProxy
.
Nesting element
calls looks like we could add an :ancestor
key to %Element{}
, or use a list of %Element{}
to describe the hierarchy and traverse it in the Phoenix.LiveViewTest.ClientProxy.select_node/2
function