defmodule AssertHTML do @moduledoc ~s""" AssertHTML is an Elixir library for parsing and extracting data from HTML and XML with CSS. """ alias AssertHTML.{Debug, Matcher, Selector} @typedoc ~S""" CSS selector ## Supported selectors | Pattern | Description | |-----------------|------------------------------| | * | any element | | E | an element of type `E` | | E[foo] | an `E` element with a "foo" attribute | | E[foo="bar"] | an E element whose "foo" attribute value is exactly equal to "bar" | | E[foo~="bar"] | an E element whose "foo" attribute value is a list of whitespace-separated values, one of which is exactly equal to "bar" | | E[foo^="bar"] | an E element whose "foo" attribute value begins exactly with the string "bar" | | E[foo$="bar"] | an E element whose "foo" attribute value ends exactly with the string "bar" | | E[foo*="bar"] | an E element whose "foo" attribute value contains the substring "bar" | | E[foo\|="en"] | an E element whose "foo" attribute has a hyphen-separated list of values beginning (from the left) with "en" | | E:nth-child(n) | an E element, the n-th child of its parent | | E:first-child | an E element, first child of its parent | | E:last-child | an E element, last child of its parent | | E:nth-of-type(n) | an E element, the n-th child of its type among its siblings | | E:first-of-type | an E element, first child of its type among its siblings | | E:last-of-type | an E element, last child of its type among its siblings | | E.warning | an E element whose class is "warning" | | E#myid | an E element with ID equal to "myid" | | E:not(s) | an E element that does not match simple selector s | | E F | an F element descendant of an E element | | E > F | an F element child of an E element | | E + F | an F element immediately preceded by an E element | | E ~ F | an F element preceded by an E element | """ @type css_selector :: String.t() @typedoc """ HTML response """ @type html :: String.t() @typedoc """ HTML element attributes """ @type attributes :: [{attribute_name, value}] @typedoc """ Checking value - if nil should not exists """ @type value :: nil | String.t() | Regex.t() @typedoc """ HTML element attribute name """ @type attribute_name :: atom() | binary() @typep block_fn :: (html -> any()) # @typep value :: String.t() # use macro definition defmacro __using__(_opts) do quote location: :keep do import AssertHTML.DSL import AssertHTML, except: [ assert_html: 2, assert_html: 3, assert_html: 4, refute_html: 2, refute_html: 3, refute_html: 4 ] end end @doc ~S""" Asserts an attributes in HTML element ## assert attributes - `text` – asserts an text element in HTML - `:match` - asserts containing value in html ``` iex> html = ~S{
} ...> assert_html(html, ".zoo", class: "bar zoo") ~S{} # check if `id` not exsists iex> assert_html(~S{Hello
}) assert_html(html, match: ~r{Hello
}) assert_html(html, match: "Hello
") \# Asserts an 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""" """ @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, block_fn) :: html | no_return() def refute_html(html, block_fn) when is_binary(html) and is_function(block_fn) do block_fn.(html) html 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, 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 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_attributes(matcher, sub_context, attributes) # call inside block block_fn && block_fn.(sub_context) context end defp check_attributes(matcher, sub_context, attributes) do {contain_value, attributes} = Keyword.pop(attributes, :match) # check metattribute :match contain_value && Matcher.contain(matcher, sub_context, contain_value) if attributes != [] do Matcher.attributes(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