defmodule HTMLAssertion do @moduledoc ~s""" HTMLAssertion adds assertions for testing rendered HTML using CSS selectors. ## Usage in Phoenix Controller and Integration Test Import the HTML assertion functions in `YourAppWeb.ConnCase`. Remember to replace all instances of `YourApp` with your app module name. ```elixir defmodule YourAppWeb.ConnCase do . . . using do quote do # Import conveniences for testing with connections use Phoenix.ConnTest alias YourAppWeb.Router.Helpers, as: Routes use HTMLAssertion # Imports assert_select, refute_select # The default endpoint for testing @endpoint YourAppWeb.Endpoint end end . . . end ``` Assuming the `html_response(conn, 200)` returns: ```html
Hello
}) assert_select(html, match: ~r{Hello
}) assert_select(html, match: "Hello
") \# Asserts a text element in HTML ### Examples iex> html = ~S{Hello
}) """ @spec assert_select(html, Regex.t()) :: html | no_return() def assert_select(html, %Regex{} = value) do html(:assert, html, nil, match: value) end @spec assert_select(html, block_fn) :: html | no_return() def assert_select(html, block_fn) when is_binary(html) and is_function(block_fn) do html(:assert, html, nil, nil, block_fn) end @spec assert_select(html, css_selector) :: html | no_return() def assert_select(html, css_selector) when is_binary(html) and is_binary(css_selector) do html(:assert, html, css_selector) end @spec assert_select(html, attributes) :: html | no_return() def assert_select(html, attributes) when is_binary(html) and is_list(attributes) do html(:assert, html, nil, attributes) end @spec assert_select(html, Regex.t(), block_fn) :: html | no_return() def assert_select(html, %Regex{} = value, block_fn) when is_binary(html) and is_function(block_fn) do html(:assert, html, nil, [match: value], block_fn) end @spec assert_select(html, attributes, block_fn) :: html | no_return() def assert_select(html, attributes, block_fn) when is_binary(html) and is_list(attributes) and is_function(block_fn) do html(:assert, html, nil, attributes, block_fn) end @spec assert_select(html, css_selector, block_fn) :: html | no_return() def assert_select(html, css_selector, block_fn) when is_binary(html) and is_binary(css_selector) and is_function(block_fn) do html(:assert, html, css_selector, nil, block_fn) end def assert_select(html, css_selector, attributes, block_fn \\ nil) @spec assert_select(html, css_selector, value, block_fn | nil) :: html | no_return() def assert_select(html, css_selector, %Regex{} = value, block_fn) when is_binary(html) and is_binary(css_selector) do html(:assert, html, css_selector, [match: value], block_fn) end def assert_select(html, css_selector, value, block_fn) when is_binary(html) and is_binary(css_selector) and is_binary(value) do html(:assert, html, css_selector, [text: value], block_fn) end @spec assert_select(html, css_selector, attributes, block_fn | nil) :: html | no_return() def assert_select(html, css_selector, attributes, block_fn) do html(:assert, html, css_selector, attributes, block_fn) end ################################### ### refute @doc ~S""" """ @spec refute_select(html, Regex.t()) :: html | no_return() def refute_select(html, %Regex{} = value) do html(:refute, html, nil, match: value) end @spec refute_select(html, css_selector) :: html | no_return() def refute_select(html, css_selector) when is_binary(html) and is_binary(css_selector) do html(:refute, html, css_selector) end @spec refute_select(html, attributes) :: html | no_return() def refute_select(html, attributes) when is_binary(html) and is_list(attributes) do html(:refute, html, nil, attributes) end @spec refute_select(html, Regex.t(), block_fn) :: html | no_return() def refute_select(html, %Regex{} = value, block_fn) when is_binary(html) and is_function(block_fn) do html(:refute, html, nil, [match: value], block_fn) end @spec refute_select(html, attributes, block_fn) :: html | no_return() def refute_select(html, attributes, block_fn) when is_binary(html) and is_list(attributes) and is_function(block_fn) do html(:refute, html, nil, attributes, block_fn) end @spec refute_select(html, css_selector, block_fn) :: html | no_return() def refute_select(html, css_selector, block_fn) when is_binary(html) and is_binary(css_selector) and is_function(block_fn) do html(:refute, html, css_selector, nil, block_fn) end def refute_select(html, css_selector, attributes, block_fn \\ nil) @spec refute_select(html, css_selector, value, block_fn | nil) :: html | no_return() def refute_select(html, css_selector, %Regex{} = value, block_fn) do html(:refute, html, css_selector, [match: value], block_fn) end def refute_select(html, css_selector, value, block_fn) when is_binary(html) and is_binary(css_selector) and is_binary(value) do html(:refute, html, css_selector, [match: value], block_fn) end @spec refute_select(html, css_selector, attributes, block_fn | nil) :: html | no_return() def refute_select(html, css_selector, attributes, block_fn) do html(:refute, html, css_selector, attributes, block_fn) end defp html(matcher, context, css_selector, attributes \\ nil, block_fn \\ nil) defp html(matcher, context, css_selector, nil = _attributes, block_fn) do html(matcher, context, css_selector, [], block_fn) end defp html(matcher, context, css_selector, attributes, block_fn) when is_map(attributes) do attributes = Enum.into(attributes, []) html(matcher, context, css_selector, attributes, block_fn) end defp html(matcher, context, css_selector, attributes, block_fn) when matcher in [:assert, :refute] and is_binary(context) and (is_binary(css_selector) or is_nil(css_selector)) and is_list(attributes) and (is_function(block_fn) or is_nil(block_fn)) do Debug.log("call .html with arguments: #{inspect(binding())}") sub_context = get_context(%{matcher: matcher, context: context, css_selector: css_selector, attributes: attributes}) # check :count meta-attribute {count_value, attributes} = Keyword.pop(attributes, :count) count_value && check_count(%{count_value: count_value, context: context, css_selector: css_selector}) # check :min meta-attribute {min_value, attributes} = Keyword.pop(attributes, :min) min_value && check_min(%{min_value: min_value, context: context, css_selector: css_selector}) # check :max meta-attribute {max_value, attributes} = Keyword.pop(attributes, :max) max_value && check_max(%{max_value: max_value, context: context, css_selector: css_selector}) check_attributes(matcher, sub_context, attributes) # call inside block block_fn && block_fn.(sub_context) context end defp check_count(%{context: context, count_value: count_value, css_selector: css_selector}) when is_integer(count_value) do count_elements = Parser.find(context, css_selector) |> Enum.count() unless count_value == count_elements do raise ExUnit.AssertionError, "Expected #{count_value} element(s). Got #{count_elements} element(s)." end end defp check_min(%{context: context, min_value: min_value, css_selector: css_selector}) when is_integer(min_value) do count_elements = Parser.find(context, css_selector) |> Enum.count() unless count_elements >= min_value do raise ExUnit.AssertionError, "Expected at least #{min_value} element(s). Got #{count_elements} element(s)." end end defp check_max(%{context: context, max_value: max_value, css_selector: css_selector}) when is_integer(max_value) do count_elements = Parser.find(context, css_selector) |> Enum.count() unless count_elements <= max_value do raise ExUnit.AssertionError, "Expected at most #{max_value} element(s). Got #{count_elements} element(s)." end end defp check_attributes(matcher, sub_context, attributes) do {contain_value, attributes} = Keyword.pop(attributes, :match) # check :match meta-attribute contain_value && Matcher.contain(matcher, sub_context, contain_value) if attributes != [] do Matcher.attributes(matcher, sub_context, attributes) end end defp get_context(%{context: context, css_selector: nil}) do context end defp get_context(%{matcher: :refute, attributes: attributes, context: context, css_selector: css_selector}) when attributes != [] do Selector.find(context, css_selector) end defp get_context(%{matcher: matcher, context: context, css_selector: css_selector}) do Matcher.selector(matcher, context, css_selector) end end