Swing the A11y Axe as an Elixir Playwright

Accessibility testing without Wallaby.

While working on the PhoenixTest Playwright driver, I was worried I might lose a11y_audit along the way.

This wonderful library wraps axe-core, a Javascript accessibility testing engine, for ExUnit. Usage with Wallaby and Hound is documented. And then there is the ominous sounding section "For other environments":

get_audit_result =
  fn ->
    execute_script(A11yAudit.JS.axe_core())
    axe_result_map = execute_script(A11yAudit.JS.await_audit_results())
    A11yAudit.Results.from_json(axe_result_map)
  end

A11yAudit.Assertions.assert_no_violations(get_audit_result.())

And now for Playwright

So mainly, we need the ability to execute some Javascript in the browser. Since Playwright exposes this functionality via the SDKs, it should be possible to with a custom Elixir wrapper also.

And it is! Here are two short snippets showing how:

defmodule MyTest do
  use PhoenixTest.Case, playwright: true

  test "is accessible", %{session: session} do
    session
    |> visit(~p"/")
    |> assert_accessible()
  end

  defp assert_accessible(session) do
    run_audit =
      fn ->
        Playwright.Frame.evaluate(session.frame_id, A11yAudit.JS.axe_core())
        Playwright.Frame.evaluate(session.frame_id, "axe.run()")
      end

    A11yAudit.Assertions.assert_no_violations(run_audit.())
    session
  end
end

Notice that we use axe.run() instead of A11yAudit.JS.await_audit_results(). Playwright automatically awaits the Promise returned by axe.run(), but doesn't support top-level awaits as used by await_audit_results().

defmodule Playwright.Frame do
  def evaluate(frame_id, js, opts \\ []) do
    params =
      opts
      |> Enum.into(%{expression: js, isFunction: false, arg: nil})
      |> Map.update!(:arg, &Serialization.serialize/1)

    [guid: frame_id, method: :evaluateExpression, params: params]
    |> Connection.post()
    |> unwrap_response(& &1.result.value)
    |> Serialization.deserialize()
  end
end

The Connection module is explained in my previous post on using Plawright in Elixir. The serialization is somewhat cryptic at first, but a straight-forward implementation can be taken from playwright-elixir.

And that's it!