lib/playwright/page/accessibility.ex

defmodule Playwright.Page.Accessibility do
  @moduledoc """
  `Playwright.Page.Accessibility` provides functions for inspecting Chromium's accessibility tree.

  The accessibility tree is used by assistive technology such as [screen readers][1] or [switches][2].

  Accessibility is a very platform-specific thing. On different platforms, there are different screen readers that
  might have wildly different output.

  Rendering engines of Chromium, Firefox and WebKit have a concept of "accessibility tree", which is then translated
  into different platform-specific APIs. Accessibility namespace gives access to this Accessibility Tree.

  Most of the accessibility tree gets filtered out when converting from internal browser AX Tree to Platform-specific
  AX-Tree or by assistive technologies themselves. By default, Playwright tries to approximate this filtering,
  exposing only the "interesting" nodes of the tree.

  [1]: https://en.wikipedia.org/wiki/Screen_reader
  [2]: https://en.wikipedia.org/wiki/Switch_access
  """

  alias Playwright.Runner.Channel

  @type page :: %Playwright.Page{}
  @type opts :: map

  @doc """
  Captures the current state of the accessibility tree.

  The returned object represents the root accessible node of the page.

  ## Options

  - `:interestingOnly` - Prune uninteresting nodes from the tree (default: true)
  - `:root` - The root DOM element for the snapshot (default: page)

  ## Examples

  Dumping an entire accessibility tree:

      iex> page = PlaywrightTest.Page.setup()
      ...> page
      ...>   |> Playwright.Page.set_content("<p>Hello!</p>")
      ...>   |> Playwright.Page.Accessibility.snapshot()
      %{children: [%{name: "Hello!", role: "text"}], name: "", role: "WebArea"}

  Retrieving the name of a focused node:

      iex> page = PlaywrightTest.Page.setup()
      ...> body = "<input placeholder='pick me' autofocus /><input placeholder='not me' />"
      ...> page
      ...>   |> Playwright.Page.set_content(body)
      ...>   |> Playwright.Page.Accessibility.snapshot()
      ...>   |> (&(Enum.find(&1.children, fn e -> e.focused end))).()
      ...>   |> Map.get(:name)
      "pick me"
  """
  @spec snapshot(page, opts) :: map
  def snapshot(page, opts \\ %{}) do
    page
    |> Channel.send("accessibilitySnapshot", opts)
    # |> IO.inspect()
    |> ax_node_from_protocol()
  end

  # private
  # ---------------------------------------------------------------------------

  defp ax_node_from_protocol(%{role: role} = input)
       when role in ["text"] do
    ax_node_from_protocol(input, fn e -> e.role != "text" end)
  end

  defp ax_node_from_protocol(input) do
    ax_node_from_protocol(input, fn _ -> true end)
  end

  defp ax_node_from_protocol(input, filter) do
    Enum.reduce(input, %{}, fn {k, v}, acc ->
      cond do
        is_list(v) ->
          normal =
            v
            |> Enum.map(&ax_node_from_protocol/1)
            |> Enum.filter(filter)

          Map.put(acc, k, normal)

        k == :checked ->
          normal =
            case v do
              "checked" -> true
              "unchecked" -> false
              other -> other
            end

          Map.put(acc, k, normal)

        k == :valueString ->
          Map.put(acc, :value, v)

        true ->
          Map.put(acc, k, v)
      end
    end)
  end
end