lib/adh.ex

defmodule Adh do
  @moduledoc """
  Adh is a tiny library of helpers to make assertions on DOM.

  Powered by Floki.
  """

  use ExUnit.Case

  @typedoc """
  An HTML document as a string
  """
  @type html :: String.t()

  @typedoc """
  An assertion
  """
  @type assertion :: {atom(), String.t() | tuple()}

  @typedoc """
  A list of assertions
  """
  @type assertions :: [assertions()]

  @typedoc """
  The assertions result
  """
  @type assert_result :: [:ok | {:fail, String.t()}]

  @doc """
  Run the assertions against the given html string.

  ## Examples

      iex> Adh.dom_assert("<p class='red'>Red Text</p>", [{:dom_present, ".red"}])
      [:ok]
      iex> Adh.dom_assert("<p class='red'>Red Text</p>", [{:dom_present, ".yellow"}])
      [{:fail, "dom present: `.yellow` is not present."}]

  """
  @spec dom_assert(html(), assertions()) :: assert_result() | no_return()
  def dom_assert(html, assertions) when is_list(assertions) do
    document = Floki.parse_document!(html)

    assertions
    |> Enum.map(&check(document, &1))
  end

  def dom_assert(_, assertions) do
    raise(Adh.InvalidAssertions, assertions)
  end

  defp check(document, {:dom_count, {selector, count}}) do
    got = document |> Floki.find(selector) |> length()
    ok_fail(got == count, "dom count: expected #{count}, got #{got} instead.")
  end

  defp check(document, {:dom_present, selector}) do
    got = document |> Floki.find(selector) |> length()
    ok_fail(got > 0, "dom present: `#{selector}` is not present.")
  end

  defp check(document, {:dom_absent, selector}) do
    got = document |> Floki.find(selector)
    ok_fail(got == [], "dom absent: `#{selector}` is not absent.")
  end

  defp check(document, {:dom_single, selector}) do
    got = document |> Floki.find(selector) |> length()
    ok_fail(got == 1, "dom single: `#{selector}` is not single.")
  end

  defp check(document, {:dom_multi, selector}) do
    got = document |> Floki.find(selector) |> length()
    ok_fail(got > 1, "dom multi: `#{selector}` is not multi.")
  end

  defp check(document, {:dom_text, {selector, expected}}) when is_binary(expected) do
    [got] = document |> Floki.find(selector) |> Enum.map(&Floki.text(&1))
    ok_fail(got == expected, "dom text: got `#{got}` instead of `#{expected}`.")
  end

  defp check(document, {:dom_text, {selector, expected}}) when is_list(expected) do
    got = document |> Floki.find(selector) |> Enum.map(&Floki.text(&1))

    ok_fail(
      got == expected,
      "dom text: got `#{Enum.join(got, ",")}` instead of `#{Enum.join(expected, ", ")}`."
    )
  end

  defp check(_, x), do: raise(Adh.InvalidAssertion, x)

  defp ok_fail(true, _), do: :ok
  defp ok_fail(false, message), do: {:fail, message}
end