defmodule AssertHTML do @moduledoc ~s""" AssertHTML adds ExUnit assert helpers for testing rendered HTML using CSS selectors. ## Usage in Phoenix Controller and Integration Test Assuming the `html_response(conn, 200)` returns: ```html
Hello
}) assert_html(html, match: ~r{Hello
}) assert_html(html, match: "Hello
") \# Asserts a text element in HTML ### Examples iex> html = ~S{Hello
}) """ @spec assert_html(html, Regex.t()) :: html | no_return() def assert_html(html, %Regex{} = value) do html(:assert, html, nil, match: value) end @spec assert_html(html, block_fn) :: html | no_return() def assert_html(html, block_fn) when is_binary(html) and is_function(block_fn) do html(:assert, html, nil, nil, block_fn) end @spec assert_html(html, css_selector) :: html | no_return() def assert_html(html, css_selector) when is_binary(html) and is_binary(css_selector) do html(:assert, html, css_selector) end @spec assert_html(html, attributes) :: html | no_return() def assert_html(html, attributes) when is_binary(html) and is_list(attributes) do html(:assert, html, nil, attributes) end @spec assert_html(html, Regex.t(), block_fn) :: html | no_return() def assert_html(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_html(html, attributes, block_fn) :: html | no_return() def assert_html(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_html(html, css_selector, block_fn) :: html | no_return() def assert_html(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_html(html, css_selector, attributes, block_fn \\ nil) @spec assert_html(html, css_selector, value, block_fn | nil) :: html | no_return() def assert_html(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_html(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, [match: value], block_fn) end @spec assert_html(html, css_selector, attributes, block_fn | nil) :: html | no_return() def assert_html(html, css_selector, attributes, block_fn) do html(:assert, html, css_selector, attributes, block_fn) end ################################### ### Refute @doc ~S""" Opposite method for assert_html See more (t:refute_html/2) """ @spec refute_html(html, Regex.t()) :: html | no_return() def refute_html(html, %Regex{} = value) do html(:refute, html, nil, match: value) end @spec refute_html(html, css_selector) :: html | no_return() def refute_html(html, css_selector) when is_binary(html) and is_binary(css_selector) do html(:refute, html, css_selector) end @spec refute_html(html, attributes) :: html | no_return() def refute_html(html, attributes) when is_binary(html) and is_list(attributes) do html(:refute, html, nil, attributes) end @spec refute_html(html, Regex.t(), block_fn) :: html | no_return() def refute_html(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_html(html, attributes, block_fn) :: html | no_return() def refute_html(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_html(html, css_selector, block_fn) :: html | no_return() def refute_html(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_html(html, css_selector, attributes, block_fn \\ nil) @spec refute_html(html, css_selector, value, block_fn | nil) :: html | no_return() def refute_html(html, css_selector, %Regex{} = value, block_fn) do html(:refute, html, css_selector, [match: value], block_fn) end def refute_html(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_html(html, css_selector, attributes, block_fn | nil) :: html | no_return() def refute_html(html, css_selector, attributes, block_fn) do html(:refute, html, css_selector, attributes, block_fn) end defp html(matcher, html_content, css_selector, attributes \\ nil, block_fn \\ nil) defp html(matcher, html_content, css_selector, nil = _attributes, block_fn) do html(matcher, html_content, css_selector, [], block_fn) end defp html(matcher, html_content, css_selector, attributes, block_fn) when is_map(attributes) do attributes = Enum.into(attributes, []) html(matcher, html_content, css_selector, attributes, block_fn) end defp html(matcher, html_content, css_selector, attributes, block_fn) when matcher in [:assert, :refute] and is_binary(html_content) 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())}") params = {collection_params, attributes_params} = Keyword.split(attributes, @collection_checks) context = {matcher, html_content} # check selector check_selector(params, context, css_selector) # collection checks (:count, :min, :max and :match collection) check_collection(collection_params, context, css_selector) # check element attributes check_element(attributes_params, context, css_selector) # call inside block if block_fn do sub_html_content = get_sub_html!(context, css_selector, once: true) block_fn.(sub_html_content) end html_content end defp check_element(attributes, context, css_selector) defp check_element([], _context, _css_selector) do :skip end defp check_element(attributes, {matcher, html}, css_selector) do sub_html_content = get_sub_html!({matcher, html}, css_selector, once: true, skip_refute: true) Matcher.attributes({matcher, sub_html_content}, attributes) end defp check_collection([], _context, _css_selector) do :skip end # assert check selection exists defp check_collection(attributes, {matcher, _html} = context, css_selector) do # check :match meta-attribute {contain_value, attributes} = Keyword.pop(attributes, :match) if contain_value do sub_html_content = get_sub_html!(context, css_selector, once: true, skip_refute: true) Matcher.contain({matcher, sub_html_content}, contain_value) end # check :count meta-attribute {count_value, attributes} = Keyword.pop(attributes, :count) count_value && Matcher.count(context, css_selector, count_value) # check :min meta-attribute {min_value, attributes} = Keyword.pop(attributes, :min) min_value && Matcher.min(context, css_selector, min_value) # check :max meta-attribute {max_value, _attributes} = Keyword.pop(attributes, :max) max_value && Matcher.max(context, css_selector, max_value) end defp get_sub_html!({_matcher, html_content}, nil, _options) do html_content end defp get_sub_html!(context, css_selector, options) do Matcher.selector(context, css_selector, options) end defp check_selector(params, context, css_selector) defp check_selector({[], []}, context, css_selector) do get_sub_html!(context, css_selector, once: true) end defp check_selector({[], _}, context, css_selector) do get_sub_html!(context, css_selector, once: true, skip_refute: true) end defp check_selector({_, []}, context, css_selector) do get_sub_html!(context, css_selector, skip_refute: true) end defp check_selector(_params, _mc, _css_selector) do :ok end end