Testing (live)views with translations and accents

Hello there :waving_hand:

I am facing an issue with a small phoenix app I am currently hacking.
As a frenchman, I am developing web apps for a French audience, and so I have to deal with all the diacritics (accents etc.) of the language.

Doing so, I faced some issues then testing my pages: there is probably a better way to handle them, but I could not figure it out :person_shrugging:

Issue 1: accents get escaped

For example, I cannot simply use the =~ operator to check the presence of some text in the HTML:

assert html =~ "S'inscrire" # 🛑 Not working
assert html =~ dgettext("auth", "Register") # 🛑 Neither

This is because the “apostrophe” is escaped as “'” in the final HTML.
I ended up adding a new function to my ConnCase

  import Phoenix.HTML

  # ...

  @doc """
  Call GetText *and escape* the result.
  """
  def translation(domain \\ nil, key, bindings \\ %{}) do
    if domain do
      Gettext.dgettext(MaGardeWeb.Gettext, domain, key, bindings)
    else
      Gettext.gettext(MaGardeWeb.Gettext, key, bindings)
    end
    |> html_escape()
    |> safe_to_string()
  end

This way I can use a call to translation(...) in my tests in place of the Gettext functions whenever I need it : not ideal but better that looking up the escaped strings every time.

Is there an easier / better way to do this ?

Issue 2 : even when I use HTML escaping, I cannot select elements in LiveView tests

Once again, when I try to select the “S’inscrire” button (means register in French), nothing seems to work:

lv |> element("main a", "S'inscrire") # 🛑 Not working
lv |> element("main a", "S'inscrire") # 🛑 Nope
lv |> element("main a", translation("auth", "Register")) # 🛑 Still not good...

And when I look into the error that raised, it literally prints the escaped text content while complaining it cannot be found :cry: :

** (ArgumentError) selector "main a" did not match text filter "S'inscrire", got: 

    <a href="/users/register" data-phx-link="redirect" data-phx-link-state="push" 
      class="font-semibold text-brand hover:underline">S&#39;inscrire</a>

What did I do wrong ? and what’s the right way to do this ?

Thanks a lot for any insight on these topics, since I’m quite stuck right now.
Regards,
Aurélien

Only tangential, but I very rarely (or never?) test against actual text on the webpage because that is just so brittle.

Instead, I would assert that a button exists or that some element has been added or removed after some action.

I understand your point, and kinda agree when testing for page content, but for CTAs I like selecting elements using the actual label: “Click Sign In”, “Click Submit” etc.

And it works nicely in English, but for some obscure reason it does not in French (the app is “french-only”)…

Even for things like this I would avoid using the text because 2 days later some designer comes along and feels that “Please click Sign In now” is much better and then you have to change your test.

You do you, but my experience has been to avoid using text to assert or as selectors.

Another workaround could be to use a different selector and then use your own function to verify the text contents:

lv |> element("[test-role=register-button]") |> assert_text("S'inscrire")

where assert_text handles translation or escaping and either raises or returns the original input.

For anyone finding this later:

I have a similar case, where I want to assert that a customer’s name is in the html somewhere, but customer names often have accent marks, etc.

# conn_case.ex or test utils
  def escape(input) do
    input
    |> Phoenix.HTML.html_escape()
    |> Phoenix.HTML.safe_to_string()
  end

  row = view |> element("##{customer.id}") |> render()
  assert row =~ escape(customer.name)

Alternatively, you could use PhoenixTest which doesn’t seem to have this issue

Seems like something common enough that it would be nice to have that escape function built in (maybe in LiveViewTest), but the html_escape/safe_to_string approach is indeed actually documented as the way to escape strings.

Personally I think I would prefer the assert_text suggestion as I suspect I’d frequently forget the escape() call in practice.

Digging into PhoenixTest, I found that the assertions act on the same conn that you would get using a stock LiveViewTest, escaped HTML and everything. But using the PhoenixTest assertions doesn’t have this issue.

[(phoenix_test 0.9.1) lib/phoenix_test/html.ex:22: PhoenixTest.Html.element_text/1]
element #=> #LazyHTML<
  1 node (from selector)

  #1
  <h1>
        
      Customer Angela D&#39;Augistino
  
      
      </h1>
>
|> LazyHTML.to_tree(skip_whitespace_nodes: true) #=> [
  {"h1", [{"class", "text-lg font-semibold leading-8"}],
   ["\n      \n    Customer Angela D'Augistino\n\n    \n    "]}
]
|> text_from_text_nodes() #=> " \n      \n    Customer Angela D'Augistino\n\n    \n    "
|> String.trim() #=> "Customer Angela D'Augistino"
|> normalize_whitespace() #=> "Customer Angela D'Augistino"

When it LazyHTML.to_tree/2 is called, we get a tree with normal text, and the result of this pipeline is what ends up being operated on with == or =~ .

What’s interesting / unfortunate is that LiveViewTest calls the exact same LazyHTML.to_tree/2, but then before it’s returned to the test, it’s run through LazyHTML.to_html/2, giving us escaped strings strings again.