PageObjects pattern for e2e testing - And packages yet?

Is there a page objects library for e2e testing phoenix apps out there?

This concept is what I’m looking for:

Back in my ruby days, I loved this project:

I’m having trouble finding something equivalent. Anyone have any pointers?

I haven’t used a library for this in Elixir, but it’s possible to get similar expressiveness with just functions using tools like Phoenix.LiveViewTest.element

You’d write a helper function like:

def shiny_magic_button(view, name_filter \\ nil) do
  element(view, "button.shiny.magic", name_filter)

Then you could write assertions like:

assert view
       |> shiny_magic_button(~r{[wat]+})
       |> render_focus() =~ "Some message"

Alternatively, I’ve also written helpers that took arguments and build a large “selector”, since functions like has_element? expect that.

That’s true, and is likely where I’ll start.

FWIW for anyone used to just wallaby how this could help.

Page objects don’t just encapsulate css in a new way, they provide an abstraction level that lets you define tests in terms of a html/css/network-ignorant manner. For example, here’s part of a test we have:

    # visit and login as a belayalpaca investor
    |> visit("/login")
    |> assert_text("Login")
    |> fill_in(@email_field, with: email)
    |> click(@submit_button)
    # assert we got to the home page and then click a holding to buy an offering for
    |> wait_for_phx_connect()
    |> assert_text("Holdings")
    |> click(@first_holding)
    # assert we got to the policy offering page and select an offering
    |> wait_for_phx_connect()
    |> assert_text("Policy Offers")
    |> click(@first_offer)
    # assert we got to the checkout page and checkout
    |> assert_text("Purchase Insurance")
    |> click(@first_checkbox)
    |> click(@second_checkbox)
    |> click(@checkout_button)
    # assert we get redirected back to the home page after a successful purchase
    |> wait_for_page_redirect("/dashboard")
    |> wait_for_phx_connect()
    |> assert_text("Holdings")
    |> click(@active_policies_button)
    # assert we can see the newly purchased active policy
    |> wait_for_phx_connect()
    |> assert_text("Active Policies")
    |> assert_policy()

Not too bad, but it’s testing how to navigate the site rather than what to do on the site. So compare this to what I would do with site_prism:

|> LoginPage.visit() # visit's and asserts the Login text
|> LoginPage.login(email, password) # fill_in email and submit
|> HoldingsPage.get_holding(0) # click first holding
|> OfferingsPage.buy_offer(0) # all the clicking, and await a success message
|> HoldingsPage.click_active_policies()
|> PoliciesPage.assert_policy()

Where these page objects can look something like:

defmodule LoginPage do
  import Components.Menu
  # creates the visit func, assert_visible()
  use Page, url: "/login" 

  def login(session, email, password) do
    fill_in(session, @email_field, with: email)
    fill_in(session, @password_field, with: password)
     |> click(@submit_button)
     |> wait_for_phx_connect()

html, css, multi-part steps, and (very important) network challenges for slow operations are now completely abstracted away from the test.

Yes, there’s Pages: pages | Hex

I’m the coauthor and have used it on multiple projects. It works with LiveView and controllers.

defmodule Web.HomeLiveTest do
  use Test.ConnCase, async: true
  test "has login button", %{conn: conn} do
    |> Test.Pages.HomePage.assert_here()
    |> Test.Pages.HomePage.click_login_link()
    |> Test.Pages.LoginPage.assert_here()

Here’s an example page, using HtmlQuery for finding things in the HTML, and Moar.Assertions for a pipe-able assertion function with some handy options.

defmodule Test.Pages.HomePage do
  import Moar.Assertions
  alias HtmlQuery, as: Hq

  @spec assert_here(Pages.Driver.t()) :: Pages.Driver.t()
  def assert_here(%Pages.Driver.LiveView{} = page) do
    |> Hq.find("[data-page]")
    |> Hq.attr("data-page")
    |> assert_eq("home", returning: page)

  @spec click_login_link(Pages.Driver.t()) :: Pages.Driver.t()
  def click_login_link(page),
    do: page |>"Log In", test_role: "login-link")

  @spec visit(Pages.Driver.t()) :: Pages.Driver.t()
  def visit(page),
    do: page |> Pages.visit("/")