├── .credo.exs ├── .dialyzer_ignore.exs ├── .formatter.exs ├── .github ├── dependabot.yml └── workflows │ ├── dependabot-tickets.yml │ └── run_tests.yml ├── .gitignore ├── .tool-versions ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── TODO.md ├── config ├── config.exs ├── dev.exs └── test.exs ├── lib ├── assert_html.ex └── assert_html │ ├── debug.ex │ ├── dsl.ex │ ├── matcher.ex │ ├── parser.ex │ └── selector.ex ├── mix.exs ├── mix.lock └── test ├── assert_html ├── dsl_test.exs ├── matcher_test.exs └── selector_test.exs ├── assert_html_test.exs └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | files: %{ 6 | included: ~w{config lib test} 7 | }, 8 | strict: true, 9 | color: true, 10 | checks: [ 11 | {Credo.Check.Readability.MaxLineLength, max_length: 160} 12 | ] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.dialyzer_ignore.exs: -------------------------------------------------------------------------------- 1 | [ 2 | ] 3 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | locals_without_parens = [ 3 | assert_html: 1, 4 | assert_html: 2, 5 | assert_html: 3, 6 | assert_html: 4, 7 | refute_html: 1, 8 | refute_html: 2, 9 | refute_html: 3, 10 | refute_html: 4 11 | ] 12 | 13 | [ 14 | inputs: [ 15 | "mix.exs", 16 | "{config,lib,test}/**/*.{ex,exs}" 17 | ], 18 | locals_without_parens: locals_without_parens, 19 | line_length: 120, 20 | export: [ 21 | locals_without_parens: locals_without_parens 22 | ] 23 | ] 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "mix" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | open-pull-requests-limit: 5 6 | schedule: 7 | interval: "daily" 8 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-tickets.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Tickets 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | check-dependabot-pull-requests: 10 | runs-on: ubuntu-latest 11 | name: Check for Dependabot Pull Requests 12 | steps: 13 | - name: Step 1 14 | id: step_1 15 | uses: ARPC/dependabot-tickets@v0.2.2 16 | with: 17 | fogbugz_api_url: ${{ secrets.FOGBUGZ_API_URL}} 18 | fogbugz_token: ${{ secrets.FOGBUGZ_API_TOKEN }} 19 | fogbugz_project: ${{ secrets.FOGBUGZ_PROJECT }} 20 | fogbugz_category: ${{ secrets.FOGBUGZ_CATEGORY}} 21 | planview_api_url: ${{ secrets.PLANVIEW_API_URL }} 22 | planview_auth: ${{ secrets.LEANKIT_AUTH }} 23 | planview_board_id: ${{ secrets.PLANVIEW_BOARD_ID }} 24 | planview_lane_id: ${{ secrets.PLANVIEW_LANE_ID }} 25 | planview_type_id: ${{ secrets.PLANVIEW_TYPE_ID }} 26 | users: "dependabot[bot]" 27 | -------------------------------------------------------------------------------- /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | container: 13 | image: elixir:1.17.3 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Install dependencies 20 | run: | 21 | mix local.rebar --force 22 | mix local.hex --force 23 | mix deps.get 24 | 25 | - name: Run Tests 26 | env: 27 | MIX_ENV: test 28 | run: | 29 | mix test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | assert_html-*.tar 24 | 25 | 26 | .elixir_ls 27 | .DS_Store 28 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.18.4-otp-27 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | sudo: false 3 | elixir: 4 | - 1.5 5 | - 1.6 6 | - 1.7 7 | - 1.8 8 | otp_release: 9 | - 19.3 10 | - 20.3 11 | - 21.0 12 | 13 | matrix: 14 | exclude: 15 | - elixir: 1.5 16 | otp_release: 21.0 17 | - elixir: 1.7 18 | otp_release: 19.3 19 | - elixir: 1.7 20 | otp_release: 20.3 21 | - elixir: 1.8 22 | otp_release: 19.3 23 | - elixir: 1.8 24 | otp_release: 20.3 25 | 26 | script: mix coveralls.post -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | 6 | ## v0.0.4 7 | 8 | ## Fixed 9 | - refute attributes 10 | 11 | ## v0.0.3 12 | 13 | ### Fixed 14 | - Checking attributes with non sting values 15 | - Check no existing attributes `attribute_name: nil` 16 | 17 | ### Added 18 | - add `assert_html match: "value"` checker 19 | - Add `assert_html_contains(html, value)` and `refute_html_contains(html, value)` checkers 20 | - Add `assert_html` macro for simplify DSL 21 | ``` 22 | use AssertHTML 23 | 24 | test "shows new page form", %{conn: conn} do 25 | conn_resp = get(conn, Routes.page_path(conn, :new)) 26 | assert response = html_response(conn_resp, 200) 27 | 28 | assert_html(response) do 29 | assert_html("title", "New page") 30 | assert_html("p.description", ~r{You can check text by regular expression}) 31 | refute_html(".check .element .if_doesnt_exist") 32 | assert_html("form.new_page", action: Routes.page_path(conn, :create), method: "post") do 33 | assert_html(".control_group") do 34 | assert_html("label", class: "form-label", text: "Page name") 35 | assert_html("input", type: "text", class: "form-control", value: "", name: "page[name]") 36 | end 37 | assert_html("button", class: "form-button", text: "Submit") 38 | end 39 | end 40 | end 41 | end 42 | ``` 43 | ### Deleted 44 | - Delete `assert_html_contains(html, "text")` -> use `assert_html(html, ~r"text")` instead 45 | - Delete `refute_html_contains(html, "text")` -> use `refute_html(html, ~r"text")` instead 46 | - Delete `refute_html_selector(html, selector)` (use `refute_html(html, selector)` instead) 47 | 48 | ## v0.0.1 49 | 50 | ### Added 51 | - Allow use Regexp for checking attribute value 52 | - Add `assert_attributes(html, selector, [id: "name"], fn(sub_html)-> end)` callback with selected html 53 | - Add `assert_attributes(html, selector, id: "name")` checker 54 | - Add `assert_html_selector(html, css_selector)` and `refute_html_selector((html, css_selector, value)` checkers 55 | - Add `assert_html_text(html, value)` and `assert_html_text(html, css_selector, value)` checkers 56 | - Add `refute_html_text(html, value)` and `refute_html_text((html, css_selector, value)` checkers 57 | - Add `html_selector(html, css_selector)` method 58 | - Add `html_attribute(html, css_selector)` and `html_attribute(html, css_selector, name)` methods 59 | - Add `html_text(html, css_selector)` method 60 | - Basic ExDoc configuration 61 | - Markdown documentation (README, LICENSE, CHANGELOG) -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Anatoliy Kovalchuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AssertHTML: Elixir Library for testing HTML and XML using CSS selectors 2 | 3 | [![Build Status](https://travis-ci.org/Kr00lIX/assert_html.svg?branch=master)](https://travis-ci.org/Kr00lIX/assert_html) 4 | [![Hex pm](https://img.shields.io/hexpm/v/assert_html.svg?style=flat)](https://hex.pm/packages/assert_html) 5 | [![Coverage Status](https://coveralls.io/repos/github/Kr00lIX/assert_html/badge.svg?branch=master)](https://coveralls.io/github/Kr00lIX/assert_html?branch=master) 6 | 7 | AssertHTML is a powerful Elixir library designed for parsing and extracting data from HTML and XML using CSS. It also provides ExUnit assert helpers for testing rendered HTML using CSS selectors, making it an essential tool for Phoenix Controller and Integration tests. 8 | 9 | ## Features 10 | 11 | - **HTML and XML Parsing**: Easily parse and extract data from HTML and XML documents. 12 | - **CSS Selectors**: Use CSS selectors to find and manipulate elements in your HTML or XML. 13 | - **ExUnit Assert Helpers**: Test your rendered HTML with the help of ExUnit assert helpers. 14 | 15 | ## Getting Started 16 | 17 | Follow these steps to get started with AssertHTML: 18 | 19 | 1. **Install the Library**: Add `assert_html` to your list of dependencies in `mix.exs`: 20 | 21 | ```elixir 22 | def deps do 23 | [ 24 | {:assert_html, "~> 0.1"} 25 | ] 26 | end 27 | ``` 28 | 29 | Then run `mix deps.get` to fetch the dependency. 30 | 31 | 2. **Import formating**: Update your .formatter.exs file with the following import: 32 | 33 | ```elixir 34 | [ 35 | import_deps: [ 36 | :assert_html 37 | ] 38 | ] 39 | ``` 40 | 41 | 3. **Add the Library to your Test**: Add `AssertHTML` to your test file: 42 | 43 | ```elixir 44 | use AssertHTML 45 | ``` 46 | 47 | 48 | ## Usage 49 | 50 | ### Usage in Phoenix Controller and Integration Test 51 | 52 | Assuming the `html_response(conn, 200)` returns: 53 | 54 | ```html 55 | 56 | 57 | 58 | PAGE TITLE 59 | 60 | 61 | Sign up 62 | Help 63 | 64 | 65 | ``` 66 | 67 | An example controller test: 68 | 69 | ```elixir 70 | defmodule YourAppWeb.PageControllerTest do 71 | use YourAppWeb.ConnCase, async: true 72 | 73 | test "should get index", %{conn: conn} do 74 | resp_conn = conn 75 | |> get(Routes.page_path(conn, :index)) 76 | 77 | html_response(resp_conn, 200) 78 | # The page title is "PAGE TITLE" 79 | |> assert_html("title", "PAGE TITLE") 80 | # The page title is "PAGE TITLE", and there is only one title element 81 | |> assert_html("title", count: 1, text: "PAGE TITLE") 82 | # The page title matches "PAGE", and there is only one title element 83 | |> assert_html("title", count: 1, match: "PAGE") 84 | # The page has one link with the href value "/signup" 85 | |> assert_html("a[href='/signup']", count: 1) 86 | # The page has at least one link 87 | |> assert_html("a", min: 1) 88 | # The page has at most two links 89 | |> assert_html("a", max: 2) 90 | # The page contains no forms 91 | |> refute_html("form") 92 | end 93 | end 94 | ``` 95 | 96 | ### Contains 97 | 98 | `assert_html(html, ~r{Hello World})` - match string in HTML 99 | `refute_html(html, ~r{Another World})` - should not contain string in HTML 100 | 101 | ```elixir 102 | assert_html(html, ".content") do 103 | assert_html(~r{Hello World}) 104 | end 105 | ``` 106 | 107 | ### CSS selectors 108 | 109 | `assert_html(html, ".css .selector")` - checks if an element exists in the CSS selector path 110 | `refute_html(html, ".errors .error")` - checks if an element does not exist in the path 111 | 112 | ### Check attributes 113 | 114 | ```elixir 115 | assert_html(html, "form", class: "form", method: "post", action: "/session/login") do 116 | assert_html ".-email" do 117 | assert_html("label", text: "Email", for: "staticEmail", class: "col-form-label") 118 | assert_html("div input", type: "text", readonly: true, class: "form-control-plaintext", value: "email@example.com") 119 | end 120 | assert_html(".-password") do 121 | assert_html("label", text: "Password", for: "inputPassword") 122 | assert_html("div input", placeholder: "Password", type: "password", class: "form-control", id: "inputPassword") 123 | end 124 | 125 | assert_html("button", type: "submit", class: "primary") 126 | end 127 | ``` 128 | 129 | ### Example 130 | 131 | ```elixir 132 | defmodule ExampleControllerTest do 133 | use ExUnit.Case, async: true 134 | use AssertHTML 135 | 136 | test "shows search form", %{conn: conn} do 137 | conn_resp = get(conn, Routes.page_path(conn, :new)) 138 | assert response = html_response(conn_resp, 200) 139 | 140 | assert_html response do 141 | # Check if element exists in CSS selector path 142 | assert_html "p.description" 143 | 144 | # Check if element doesn't exist 145 | refute_html ".flash-message" 146 | 147 | # Assert form attributes 148 | assert_html "form.new_page", action: Routes.page_path(conn, :create), method: "post" do 149 | # Assert elements inside the `form.new_page` selector 150 | assert_html "label", class: "form-label", text: "Page name" 151 | assert_html "input", type: "text", class: "form-control", value: "", name: "page_name" 152 | assert_html "button", class: "form-button", text: "Submit" 153 | end 154 | end 155 | end 156 | end 157 | ``` 158 | 159 | Documentation can be found at [https://hexdocs.pm/assert_html](https://hexdocs.pm/assert_html/AssertHTML.html). 160 | 161 | 162 | ## Contribution 163 | Feel free to send your PR with proposals, improvements or corrections 😉. 164 | 165 | ## Author 166 | 167 | Anatolii Kovalchuk (@Kr00liX) 168 | 169 | 170 | ## License 171 | 172 | This software is licensed under [the MIT license](LICENSE.md). 173 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - How check quoted/unquoted values?! 4 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | import Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :assert_html, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:assert_html, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | config :floki, :html_parser, Floki.HTMLParser.Mochiweb 24 | # Floki.HTMLParser.FastHtml 25 | # Floki.HTMLParser.Html5ever 26 | 27 | # It is also possible to import configuration files, relative to this 28 | # directory. For example, you can emulate configuration per environment 29 | # by uncommenting the line below and defining dev.exs, test.exs and such. 30 | # Configuration from the imported file will override the ones defined 31 | # here (which is why it is important to import them last). 32 | # 33 | import_config "#{Mix.env()}.exs" 34 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :assert_html, 4 | log_dsl: true 5 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :assert_html, 4 | log_dsl: true, 5 | log: false 6 | -------------------------------------------------------------------------------- /lib/assert_html.ex: -------------------------------------------------------------------------------- 1 | defmodule AssertHTML do 2 | @moduledoc ~s""" 3 | AssertHTML provides ExUnit assert helpers for testing rendered HTML using CSS selectors. 4 | 5 | ## Usage in Phoenix Controller and Integration Test 6 | 7 | Given the `html_response(conn, 200)` returns: 8 | 9 | ```html 10 | 11 | 12 | 13 | PAGE TITLE 14 | 15 | 16 | Sign up 17 | Help 18 | 19 | 20 | ``` 21 | 22 | An example controller test could be: 23 | 24 | ```elixir 25 | defmodule YourAppWeb.PageControllerTest do 26 | use YourAppWeb.ConnCase, async: true 27 | 28 | test "should get index", %{conn: conn} do 29 | resp_conn = conn 30 | |> get(Routes.page_path(conn, :index)) 31 | 32 | html_response(conn, 200) 33 | # Asserts that the page title is "PAGE TITLE" 34 | |> assert_html("title", "PAGE TITLE") 35 | # Asserts that the page title is "PAGE TITLE" and there is only one title element 36 | |> assert_html("title", count: 1, text: "PAGE TITLE") 37 | # Asserts that the page title matches "PAGE" and there is only one title element 38 | |> assert_html("title", count: 1, match: "PAGE") 39 | # Asserts that the page has one link with href value "/signup" 40 | |> assert_html("a[href='/signup']", count: 1) 41 | # Asserts that the page has at least one link 42 | |> assert_html("a", min: 1) 43 | # Asserts that the page has at most two links 44 | |> assert_html("a", max: 2) 45 | # Asserts that the page contains no forms 46 | |> refute_html("form") 47 | end 48 | end 49 | ``` 50 | 51 | ### Selector Checks 52 | `assert_html(html, ".css .selector .exsits")` - Asserts that an element exists in the selector path. 53 | `refute_html(html, ".css .selector")` - Asserts that an element does not exist in the selector path. 54 | 55 | ### Check Attributes 56 | Supports meta attributes: 57 | 58 | * `:text` – The exact text within the element 59 | * `:match` - A value that the element's text should contain. 60 | 61 | """ 62 | 63 | alias AssertHTML.{Debug, Matcher} 64 | 65 | @collection_checks [:match, :count, :min, :max] 66 | 67 | @typedoc ~S""" 68 | CSS selector 69 | 70 | ## Supported selectors 71 | 72 | | Pattern | Description | 73 | |-----------------|------------------------------| 74 | | * | any element | 75 | | E | an element of type `E` | 76 | | E[foo] | an `E` element with a "foo" attribute | 77 | | E[foo="bar"] | an E element whose "foo" attribute value is exactly equal to "bar" | 78 | | 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" | 79 | | E[foo^="bar"] | an E element whose "foo" attribute value begins exactly with the string "bar" | 80 | | E[foo$="bar"] | an E element whose "foo" attribute value ends exactly with the string "bar" | 81 | | E[foo*="bar"] | an E element whose "foo" attribute value contains the substring "bar" | 82 | | E[foo\|="en"] | an E element whose "foo" attribute has a hyphen-separated list of values beginning (from the left) with "en" | 83 | | E:nth-child(n) | an E element, the n-th child of its parent | 84 | | E:first-child | an E element, first child of its parent | 85 | | E:last-child | an E element, last child of its parent | 86 | | E:nth-of-type(n) | an E element, the n-th child of its type among its siblings | 87 | | E:first-of-type | an E element, first child of its type among its siblings | 88 | | E:last-of-type | an E element, last child of its type among its siblings | 89 | | E.warning | an E element whose class is "warning" | 90 | | E#myid | an `E` element with ID equal to "myid" | 91 | | E:not(s) | an E element that does not match simple selector s | 92 | | E F | an F element descendant of an E element | 93 | | E > F | an F element child of an E element | 94 | | E + F | an F element immediately preceded by an E element | 95 | | E ~ F | an F element preceded by an E element | 96 | 97 | """ 98 | @type css_selector :: String.t() 99 | 100 | @typedoc """ 101 | HTML response 102 | """ 103 | @type html :: String.t() 104 | 105 | @typep matcher :: :assert | :refute 106 | 107 | @type context :: {matcher, html} 108 | 109 | @typedoc """ 110 | HTML element attributes 111 | """ 112 | @type attributes :: [{attribute_name, value}] 113 | 114 | @typedoc """ 115 | Checking value 116 | - if nil should not exist 117 | 118 | """ 119 | @type value :: nil | String.t() | Regex.t() 120 | 121 | @typedoc """ 122 | HTML element attribute name 123 | """ 124 | @type attribute_name :: atom() | binary() 125 | 126 | @typep block_fn :: (html -> any()) 127 | 128 | # @typep value :: String.t() 129 | 130 | # use macro definition 131 | defmacro __using__(_opts) do 132 | quote location: :keep do 133 | import AssertHTML.DSL 134 | 135 | import AssertHTML, 136 | except: [ 137 | assert_html: 2, 138 | assert_html: 3, 139 | assert_html: 4, 140 | refute_html: 2, 141 | refute_html: 3, 142 | refute_html: 4 143 | ] 144 | end 145 | end 146 | 147 | @doc ~S""" 148 | Asserts an attributes in HTML element 149 | 150 | ## Asserting Attributes 151 | - `:text` – Asserts the exact text within an HTML element. 152 | - `:match` - Asserts that the HTML element's text contains a specific value. 153 | 154 | ```elixir 155 | iex> html = ~S{
} 156 | ...> assert_html(html, ".zoo", class: "bar zoo") 157 | ~S{
} 158 | 159 | # Check if `id` attribute does not exist 160 | iex> assert_html(~S{
text
}, id: nil) 161 | "
text
" 162 | ``` 163 | 164 | #### Examples for :text 165 | 166 | Asserts the exact text within an HTML element. 167 | 168 | iex> html = ~S{

Header

} 169 | ...> assert_html(html, text: "Header") 170 | ~S{

Header

} 171 | 172 | iex> html = ~S{

Header

} 173 | ...> assert_html(html, ".title", text: "Header") 174 | ~S{

Header

} 175 | 176 | iex> html = ~S{

Header

} 177 | ...> try do 178 | ...> assert_html(html, text: "HEADER") 179 | ...> rescue 180 | ...> e in ExUnit.AssertionError -> e 181 | ...> end 182 | %ExUnit.AssertionError{ 183 | left: "HEADER", 184 | right: "Header", 185 | message: "Comparison `text` attribute failed.\n\n\t

Header

.\n" 186 | } 187 | 188 | iex> html = ~S{
Some & text
} 189 | ...> assert_html(html, text: "Some & text") 190 | ~S{
Some & text
} 191 | 192 | ## Selector 193 | 194 | `assert_html(html, "css selector")` 195 | 196 | ```elixir 197 | iex> html = ~S{

Header

} 198 | ...> assert_html(html, "p .foo h1") 199 | ~S{

Header

} 200 | 201 | iex> html = ~S{

Header

} 202 | ...> assert_html(html, "h1") 203 | ~S{

Header

} 204 | ``` 205 | 206 | ## Match elements in HTML 207 | 208 | Asserts that the HTML contains a specific element. 209 | 210 | ```elixir 211 | assert_html(html, ~r{

Hello

}) 212 | assert_html(html, match: ~r{

Hello

}) 213 | assert_html(html, match: "

Hello

") 214 | ``` 215 | 216 | ### Asserts a text element in HTML 217 | 218 | iex> html = ~S{

Header

} 219 | ...> assert_html(html, "p .foo h1", text: "Header") 220 | ~S{

Header

} 221 | 222 | 223 | ### Examples 224 | 225 | iex> html = ~S{

Hello World

} 226 | ...> assert_html(html, "h1", "Hello World") == html 227 | true 228 | 229 | iex> html = ~S{

Hello World

} 230 | ...> assert_html(html, ".title", ~r{World}) 231 | ~S{

Hello World

} 232 | 233 | ## assert elements in selector 234 | assert_html(html, ".container table", ~r{

Hello

}) 235 | """ 236 | @spec assert_html(html, Regex.t()) :: html | no_return() 237 | def assert_html(html, %Regex{} = value) do 238 | html(:assert, html, nil, match: value) 239 | end 240 | 241 | @spec assert_html(html, block_fn) :: html | no_return() 242 | def assert_html(html, block_fn) when is_binary(html) and is_function(block_fn) do 243 | html(:assert, html, nil, nil, block_fn) 244 | end 245 | 246 | @spec assert_html(html, css_selector) :: html | no_return() 247 | def assert_html(html, css_selector) when is_binary(html) and is_binary(css_selector) do 248 | html(:assert, html, css_selector) 249 | end 250 | 251 | @spec assert_html(html, attributes) :: html | no_return() 252 | def assert_html(html, attributes) when is_binary(html) and is_list(attributes) do 253 | html(:assert, html, nil, attributes) 254 | end 255 | 256 | @spec assert_html(html, Regex.t(), block_fn) :: html | no_return() 257 | def assert_html(html, %Regex{} = value, block_fn) 258 | when is_binary(html) and is_function(block_fn) do 259 | html(:assert, html, nil, [match: value], block_fn) 260 | end 261 | 262 | @spec assert_html(html, attributes, block_fn) :: html | no_return() 263 | def assert_html(html, attributes, block_fn) 264 | when is_binary(html) and is_list(attributes) and is_function(block_fn) do 265 | html(:assert, html, nil, attributes, block_fn) 266 | end 267 | 268 | @spec assert_html(html, css_selector, block_fn) :: html | no_return() 269 | def assert_html(html, css_selector, block_fn) 270 | when is_binary(html) and is_binary(css_selector) and is_function(block_fn) do 271 | html(:assert, html, css_selector, nil, block_fn) 272 | end 273 | 274 | def assert_html(html, css_selector, attributes, block_fn \\ nil) 275 | 276 | @spec assert_html(html, css_selector, value, block_fn | nil) :: html | no_return() 277 | def assert_html(html, css_selector, %Regex{} = value, block_fn) 278 | when is_binary(html) and is_binary(css_selector) do 279 | html(:assert, html, css_selector, [match: value], block_fn) 280 | end 281 | 282 | def assert_html(html, css_selector, value, block_fn) 283 | when is_binary(html) and is_binary(css_selector) and is_binary(value) do 284 | html(:assert, html, css_selector, [match: value], block_fn) 285 | end 286 | 287 | @spec assert_html(html, css_selector, attributes, block_fn | nil) :: html | no_return() 288 | def assert_html(html, css_selector, attributes, block_fn) do 289 | html(:assert, html, css_selector, attributes, block_fn) 290 | end 291 | 292 | ################################### 293 | ### Refute 294 | 295 | @doc ~S""" 296 | Opposite method for assert_html 297 | 298 | See more (t:refute_html/2) 299 | """ 300 | @spec refute_html(html, Regex.t()) :: html | no_return() 301 | def refute_html(html, %Regex{} = value) do 302 | html(:refute, html, nil, match: value) 303 | end 304 | 305 | @spec refute_html(html, css_selector) :: html | no_return() 306 | def refute_html(html, css_selector) when is_binary(html) and is_binary(css_selector) do 307 | html(:refute, html, css_selector) 308 | end 309 | 310 | @spec refute_html(html, attributes) :: html | no_return() 311 | def refute_html(html, attributes) when is_binary(html) and is_list(attributes) do 312 | html(:refute, html, nil, attributes) 313 | end 314 | 315 | @spec refute_html(html, Regex.t(), block_fn) :: html | no_return() 316 | def refute_html(html, %Regex{} = value, block_fn) 317 | when is_binary(html) and is_function(block_fn) do 318 | html(:refute, html, nil, [match: value], block_fn) 319 | end 320 | 321 | @spec refute_html(html, attributes, block_fn) :: html | no_return() 322 | def refute_html(html, attributes, block_fn) 323 | when is_binary(html) and is_list(attributes) and is_function(block_fn) do 324 | html(:refute, html, nil, attributes, block_fn) 325 | end 326 | 327 | @spec refute_html(html, css_selector, block_fn) :: html | no_return() 328 | def refute_html(html, css_selector, block_fn) 329 | when is_binary(html) and is_binary(css_selector) and is_function(block_fn) do 330 | html(:refute, html, css_selector, nil, block_fn) 331 | end 332 | 333 | def refute_html(html, css_selector, attributes, block_fn \\ nil) 334 | 335 | @spec refute_html(html, css_selector, value, block_fn | nil) :: html | no_return() 336 | def refute_html(html, css_selector, %Regex{} = value, block_fn) do 337 | html(:refute, html, css_selector, [match: value], block_fn) 338 | end 339 | 340 | def refute_html(html, css_selector, value, block_fn) 341 | when is_binary(html) and is_binary(css_selector) and is_binary(value) do 342 | html(:refute, html, css_selector, [match: value], block_fn) 343 | end 344 | 345 | @spec refute_html(html, css_selector, attributes, block_fn | nil) :: html | no_return() 346 | def refute_html(html, css_selector, attributes, block_fn) do 347 | html(:refute, html, css_selector, attributes, block_fn) 348 | end 349 | 350 | defp html(matcher, html_content, css_selector, attributes \\ nil, block_fn \\ nil) 351 | 352 | defp html(matcher, html_content, css_selector, nil = _attributes, block_fn) do 353 | html(matcher, html_content, css_selector, [], block_fn) 354 | end 355 | 356 | defp html(matcher, html_content, css_selector, attributes, block_fn) when is_map(attributes) do 357 | attributes = Enum.into(attributes, []) 358 | html(matcher, html_content, css_selector, attributes, block_fn) 359 | end 360 | 361 | defp html(matcher, html_content, css_selector, attributes, block_fn) 362 | when matcher in [:assert, :refute] and 363 | is_binary(html_content) and 364 | (is_binary(css_selector) or is_nil(css_selector)) and 365 | is_list(attributes) and 366 | (is_function(block_fn) or is_nil(block_fn)) do 367 | Debug.log("call .html with arguments: #{inspect(binding())}") 368 | 369 | params = {collection_params, attributes_params} = Keyword.split(attributes, @collection_checks) 370 | 371 | context = {matcher, html_content} 372 | 373 | # check selector 374 | check_selector(params, context, css_selector) 375 | 376 | # collection checks (:count, :min, :max and :match collection) 377 | check_collection(collection_params, context, css_selector) 378 | 379 | # check element attributes 380 | check_element(attributes_params, context, css_selector) 381 | 382 | # call inside block 383 | if block_fn do 384 | sub_html_content = get_sub_html!(context, css_selector, once: true) 385 | block_fn.(sub_html_content) 386 | end 387 | 388 | html_content 389 | end 390 | 391 | defp check_element(attributes, context, css_selector) 392 | 393 | defp check_element([], _context, _css_selector) do 394 | :skip 395 | end 396 | 397 | defp check_element(attributes, {matcher, html}, css_selector) do 398 | sub_html_content = get_sub_html!({matcher, html}, css_selector, once: true, skip_refute: true) 399 | 400 | Matcher.attributes({matcher, sub_html_content}, attributes) 401 | end 402 | 403 | defp check_collection([], _context, _css_selector) do 404 | :skip 405 | end 406 | 407 | # assert check selection exists 408 | defp check_collection(attributes, {matcher, _html} = context, css_selector) do 409 | # check :match meta-attribute 410 | {contain_value, attributes} = Keyword.pop(attributes, :match) 411 | 412 | if contain_value do 413 | sub_html_content = get_sub_html!(context, css_selector, once: true, skip_refute: true) 414 | Matcher.contain({matcher, sub_html_content}, contain_value) 415 | end 416 | 417 | # check :count meta-attribute 418 | {count_value, attributes} = Keyword.pop(attributes, :count) 419 | count_value && Matcher.count(context, css_selector, count_value) 420 | 421 | # check :min meta-attribute 422 | {min_value, attributes} = Keyword.pop(attributes, :min) 423 | min_value && Matcher.min(context, css_selector, min_value) 424 | 425 | # check :max meta-attribute 426 | {max_value, _attributes} = Keyword.pop(attributes, :max) 427 | max_value && Matcher.max(context, css_selector, max_value) 428 | end 429 | 430 | defp get_sub_html!({_matcher, html_content}, nil, _options) do 431 | html_content 432 | end 433 | 434 | defp get_sub_html!(context, css_selector, options) do 435 | Matcher.selector(context, css_selector, options) 436 | end 437 | 438 | defp check_selector(params, context, css_selector) 439 | 440 | defp check_selector({[], []}, context, css_selector) do 441 | get_sub_html!(context, css_selector, once: true) 442 | end 443 | 444 | defp check_selector({[], _}, context, css_selector) do 445 | get_sub_html!(context, css_selector, once: true, skip_refute: true) 446 | end 447 | 448 | defp check_selector({_, []}, context, css_selector) do 449 | get_sub_html!(context, css_selector, skip_refute: true) 450 | end 451 | 452 | defp check_selector(_params, _mc, _css_selector) do 453 | :ok 454 | end 455 | end 456 | -------------------------------------------------------------------------------- /lib/assert_html/debug.ex: -------------------------------------------------------------------------------- 1 | defmodule AssertHTML.Debug do 2 | @moduledoc false 3 | require Logger 4 | 5 | def log_dsl(entry, level \\ :debug, metadata \\ []) do 6 | if Application.get_env(:assert_html, :log_dsl) do 7 | Logger.log(level, fn -> "\n~~ DSL~~>>>>> \n#{Macro.to_string(entry)} \n<<<<< ~~\n" end, metadata) 8 | end 9 | 10 | entry 11 | end 12 | 13 | def log(entry, level \\ :debug, metadata \\ []) do 14 | if Application.get_env(:assert_html, :log) do 15 | Logger.log(level, fn -> to_iodata(entry) end, metadata) 16 | end 17 | end 18 | 19 | defp to_iodata(entry) when is_binary(entry), do: entry 20 | defp to_iodata(entry), do: inspect(entry) 21 | end 22 | -------------------------------------------------------------------------------- /lib/assert_html/dsl.ex: -------------------------------------------------------------------------------- 1 | defmodule AssertHTML.DSL do 2 | @moduledoc ~S""" 3 | Add aditional syntax to passing current context inside block 4 | 5 | ### Example: pass context 6 | ``` 7 | assert_html html, ".container" do 8 | assert_html "form", action: "/users" do 9 | refute_html ".flash_message" 10 | assert_html ".control_group" do 11 | assert_html "label", class: "title", text: ~r{Full name} 12 | assert_html "input", class: "control", type: "text" 13 | end 14 | assert_html("a", text: "Submit", class: "button") 15 | end 16 | assert_html ".user_list" do 17 | assert_html "li" 18 | end 19 | end 20 | ``` 21 | 22 | ## Example 2: print current context for debug 23 | 24 | ``` 25 | assert_html(html, ".selector") do 26 | IO.inspect(assert_html, label: "current context html") 27 | end 28 | ``` 29 | """ 30 | alias AssertHTML, as: HTML 31 | alias AssertHTML.Debug 32 | 33 | defmacro assert_html(context, selector \\ nil, attributes \\ nil, maybe_do_block \\ nil) do 34 | Debug.log(context: context, selector: selector, attributes: attributes, maybe_do_block: maybe_do_block) 35 | {args, block} = extract_block([context, selector, attributes], maybe_do_block) 36 | 37 | call_html_method(:assert, args, block) 38 | |> Debug.log_dsl() 39 | end 40 | 41 | defmacro refute_html(context, selector \\ nil, attributes \\ nil, maybe_do_block \\ nil) do 42 | Debug.log(context: context, selector: selector, attributes: attributes, maybe_do_block: maybe_do_block) 43 | {args, block} = extract_block([context, selector, attributes], maybe_do_block) 44 | 45 | call_html_method(:refute, args, block) 46 | |> Debug.log_dsl() 47 | end 48 | 49 | defp call_html_method(matcher, args, block \\ nil) 50 | 51 | defp call_html_method(:assert, args, nil) do 52 | quote do 53 | HTML.assert_html(unquote_splicing(args)) 54 | end 55 | end 56 | 57 | defp call_html_method(:refute, args, nil) do 58 | quote do 59 | HTML.refute_html(unquote_splicing(args)) 60 | end 61 | end 62 | 63 | defp call_html_method(matcher, args, block) do 64 | block_arg = 65 | quote do 66 | fn unquote(context_var()) -> 67 | unquote(Macro.prewalk(block, &postwalk/1)) 68 | end 69 | end 70 | 71 | call_html_method(matcher, args ++ [block_arg]) 72 | end 73 | 74 | # found do: block if exists 75 | defp extract_block(args, do: do_block) do 76 | {args, do_block} 77 | end 78 | 79 | defp extract_block(args, _maybe_block) do 80 | args 81 | |> Enum.reverse() 82 | |> Enum.reduce({[], nil}, fn 83 | arg, {args, block} when is_list(arg) -> 84 | {maybe_block, updated_arg} = Keyword.pop(arg, :do) 85 | 86 | { 87 | (updated_arg == [] && args) || [updated_arg | args], 88 | block || maybe_block 89 | } 90 | 91 | nil, {args, block} -> 92 | {args, block} 93 | 94 | arg, {args, block} -> 95 | {[arg | args], block} 96 | end) 97 | end 98 | 99 | # replace assert_html without arguments to context 100 | defp postwalk({:assert_html, env, nil}) do 101 | context_var(env) 102 | end 103 | 104 | defp postwalk({:assert_html, env, arguments}) do 105 | context = context_var(env) 106 | {args, block} = extract_block([context | arguments], nil) 107 | 108 | call_html_method(:assert, args, block) 109 | end 110 | 111 | # replace refute_html without arguments to context 112 | defp postwalk({:refute_html, env, nil}) do 113 | context_var(env) 114 | end 115 | 116 | defp postwalk({:refute_html, env, arguments}) do 117 | context = context_var(env) 118 | {args, block} = extract_block([context | arguments], nil) 119 | 120 | call_html_method(:refute, args, block) 121 | end 122 | 123 | defp postwalk(segment) do 124 | segment 125 | end 126 | 127 | defp context_var(env \\ []) do 128 | {:assert_html_context, env, nil} 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/assert_html/matcher.ex: -------------------------------------------------------------------------------- 1 | defmodule AssertHTML.Matcher do 2 | @moduledoc false 3 | 4 | alias AssertHTML 5 | alias AssertHTML.{Parser, Selector} 6 | 7 | @compile {:inline, raise_match: 3} 8 | 9 | @typep assert_or_refute :: :assert | :refute 10 | 11 | ## ---------------------------------------------------- 12 | ## Collection 13 | 14 | @doc """ 15 | Gets html by selector and raise error if it doesn't exists 16 | 17 | # Options 18 | * `once` - only one element 19 | * `skip_refute` - do not raise error if element exists for refute 20 | """ 21 | @spec selector(AssertHTML.context(), binary(), list()) :: AssertHTML.html() 22 | def selector({matcher, html}, selector, options \\ []) when is_binary(html) and is_binary(selector) do 23 | docs = Parser.find(html, selector) 24 | 25 | # found more than one element 26 | if options[:once] && length(docs) > 1 do 27 | raise_match(matcher, matcher == :assert, fn 28 | :assert -> 29 | "Found more than one element by `#{selector}` selector.\nPlease use `#{selector}:first-child`, `#{selector}:nth-child(n)` for limiting search area.\n\n\t#{html}\n" 30 | 31 | :refute -> 32 | "Selector `#{selector}` succeeded, but should have failed.\n\n\t#{html}\n" 33 | end) 34 | end 35 | 36 | raise_match(matcher, docs == [], fn 37 | :assert -> 38 | "Element `#{selector}` not found.\n\n\t#{html}\n" 39 | 40 | :refute -> 41 | if options[:skip_refute], 42 | do: nil, 43 | else: "Selector `#{selector}` succeeded, but should have failed.\n\n\t#{html}\n" 44 | end) 45 | 46 | Parser.to_html(docs) 47 | end 48 | 49 | @doc """ 50 | Check count of elements on selector 51 | """ 52 | @spec count(AssertHTML.context(), binary(), integer()) :: any() 53 | def count({matcher, html}, selector, check_value) do 54 | count_elements = Parser.count(html, selector) 55 | 56 | raise_match(matcher, count_elements != check_value, fn 57 | :assert -> 58 | [ 59 | message: "Expected #{check_value} element(s). Got #{count_elements} element(s).", 60 | left: count_elements, 61 | right: check_value 62 | ] 63 | 64 | :refute -> 65 | [ 66 | message: "Expected different number of element(s), but received equal", 67 | left: count_elements, 68 | right: check_value 69 | ] 70 | end) 71 | end 72 | 73 | @doc """ 74 | Check count of elements on selector 75 | """ 76 | @spec min(AssertHTML.context(), binary(), integer()) :: any() 77 | def min({matcher, html}, selector, min_value) do 78 | count_elements = Parser.count(html, selector) 79 | 80 | raise_match(matcher, count_elements < min_value, fn 81 | :assert -> 82 | [ 83 | message: "Expected at least #{min_value} element(s). Got #{count_elements} element(s).", 84 | left: count_elements, 85 | right: min_value 86 | ] 87 | 88 | :refute -> 89 | [ 90 | message: "Expected at most #{min_value} element(s). Got #{count_elements} element(s).", 91 | left: count_elements, 92 | right: min_value 93 | ] 94 | end) 95 | end 96 | 97 | @doc """ 98 | Check count of elements on selector 99 | """ 100 | @spec max(AssertHTML.context(), binary(), integer()) :: any() 101 | def max({matcher, html}, selector, max_value) do 102 | count_elements = Parser.count(html, selector) 103 | 104 | raise_match(matcher, count_elements > max_value, fn 105 | :assert -> 106 | [ 107 | message: "Expected at most #{max_value} element(s). Got #{count_elements} element(s).", 108 | left: count_elements, 109 | right: max_value 110 | ] 111 | 112 | :refute -> 113 | [ 114 | message: "Expected at least #{max_value} element(s). Got #{count_elements} element(s).", 115 | left: count_elements, 116 | right: max_value 117 | ] 118 | end) 119 | end 120 | 121 | ## ---------------------------------------------------- 122 | ## Element 123 | 124 | @spec attributes(AssertHTML.context(), AssertHTML.attributes()) :: any() 125 | def attributes({matcher, html}, attributes) when is_list(attributes) do 126 | attributes 127 | |> Enum.into(%{}, fn {k, v} -> {to_string(k), v} end) 128 | |> Enum.each(fn {attribute, check_value} -> 129 | attr_value = Selector.attribute(html, attribute) 130 | match_attribute(matcher, attribute, check_value, attr_value, html) 131 | end) 132 | end 133 | 134 | @spec contain(AssertHTML.context(), Regex.t()) :: any() 135 | def contain({matcher, html}, %Regex{} = value) when is_binary(html) do 136 | raise_match(matcher, !Regex.match?(value, html), fn 137 | :assert -> 138 | [ 139 | message: "Value not matched.", 140 | left: value, 141 | right: html, 142 | expr: "assert_html(#{inspect(value)})" 143 | ] 144 | 145 | :refute -> 146 | [ 147 | message: "Value `#{inspect(value)}` matched, but shouldn't.", 148 | left: value, 149 | right: html, 150 | expr: "assert_html(#{inspect(value)})" 151 | ] 152 | end) 153 | end 154 | 155 | @spec contain(AssertHTML.context(), AssertHTML.html()) :: any() 156 | def contain({matcher, html}, value) when is_binary(html) and is_binary(value) do 157 | raise_match(matcher, !String.contains?(html, value), fn 158 | :assert -> 159 | [ 160 | message: "Value not found.", 161 | left: value, 162 | right: html, 163 | expr: "assert_html(#{inspect(value)})" 164 | ] 165 | 166 | :refute -> 167 | [ 168 | message: "Value `#{inspect(value)}` found, but shouldn't.", 169 | left: value, 170 | right: html, 171 | expr: "assert_html(#{inspect(value)})" 172 | ] 173 | end) 174 | end 175 | 176 | @spec match_attribute( 177 | assert_or_refute, 178 | AssertHTML.attribute_name(), 179 | AssertHTML.value(), 180 | binary() | nil, 181 | AssertHTML.html() 182 | ) :: no_return 183 | defp match_attribute(matcher, attribute, check_value, attr_value, html) 184 | 185 | # attribute should exists 186 | defp match_attribute(matcher, attribute, check_value, attr_value, html) when check_value in [nil, true, false] do 187 | raise_match(matcher, if(check_value, do: attr_value == nil, else: attr_value != nil), fn 188 | :assert -> 189 | if check_value, 190 | do: "Attribute `#{attribute}` should exists.\n\n\t#{html}\n", 191 | else: "Attribute `#{attribute}` shouldn't exists.\n\n\t#{html}\n" 192 | 193 | :refute -> 194 | if check_value, 195 | do: "Attribute `#{attribute}` shouldn't exists.\n\n\t#{html}\n", 196 | else: "Attribute `#{attribute}` should exists.\n\n\t#{html}\n" 197 | end) 198 | end 199 | 200 | # attribute should not exists 201 | defp match_attribute(matcher, attribute, _check_value, nil = _attr_value, html) do 202 | raise_match(matcher, matcher == :assert, fn 203 | _ -> "Attribute `#{attribute}` not found.\n\n\t#{html}\n" 204 | end) 205 | end 206 | 207 | defp match_attribute(matcher, attribute, %Regex{} = check_value, attr_value, html) do 208 | raise_match(matcher, !Regex.match?(check_value, attr_value), fn _ -> 209 | [ 210 | message: "Matching `#{attribute}` attribute failed.\n\n\t#{html}.\n", 211 | left: check_value, 212 | right: attr_value 213 | ] 214 | end) 215 | end 216 | 217 | defp match_attribute(matcher, "class", check_value, attr_value, html) do 218 | for check_class <- String.split(to_string(check_value), " ") do 219 | raise_match(matcher, !String.contains?(attr_value, check_class), fn 220 | :assert -> "Class `#{check_class}` not found in `#{attr_value}` class attribute\n\n\t#{html}\n" 221 | :refute -> "Class `#{check_class}` found in `#{attr_value}` class attribute\n\n\t#{html}\n" 222 | end) 223 | end 224 | end 225 | 226 | defp match_attribute(matcher, attribute, check_value, attr_value, html) do 227 | str_check_value = to_string(check_value) 228 | 229 | raise_match(matcher, str_check_value != attr_value, fn _ -> 230 | [ 231 | message: "Comparison `#{attribute}` attribute failed.\n\n\t#{html}.\n", 232 | left: str_check_value, 233 | right: attr_value 234 | ] 235 | end) 236 | end 237 | 238 | defp raise_match(check, condition, message_fn) when check in [:assert, :refute] do 239 | cond do 240 | check == :assert -> condition 241 | check == :refute -> !condition 242 | true -> false 243 | end 244 | |> if do 245 | message_or_args = message_fn.(check) 246 | 247 | if message_or_args do 248 | args = (is_list(message_or_args) && message_or_args) || [message: message_or_args] 249 | raise ExUnit.AssertionError, args 250 | end 251 | end 252 | end 253 | end 254 | -------------------------------------------------------------------------------- /lib/assert_html/parser.ex: -------------------------------------------------------------------------------- 1 | defmodule AssertHTML.Parser do 2 | @moduledoc false 3 | 4 | @typep html_element_tuple :: binary() | [any()] | tuple() 5 | @typep html_tree :: html_element_tuple 6 | 7 | @doc """ 8 | Find node 9 | 10 | ## Example 11 | 12 | Assuming that you have the following HTML: 13 | 14 | ```html 15 | 16 | 17 | 18 |
19 |

Floki

20 | Github page 21 | philss 22 |
23 | 24 | 25 | ``` 26 | 27 | Examples of queries that you can perform: 28 | 29 | find(html, "#content") 30 | find(html, ".headline") 31 | find(html, "a") 32 | find(html, "[data-model=user]") 33 | find(html, "#content a") 34 | find(html, ".headline, a") 35 | Each HTML node is represented by a tuple like: 36 | 37 | {tag_name, attributes, children_nodes} 38 | """ 39 | @spec find(AssertHTML.html(), AssertHTML.css_selector()) :: html_tree 40 | def find(html, selector) do 41 | html 42 | |> Floki.parse_document!() 43 | |> Floki.find(selector) 44 | end 45 | 46 | @spec count(AssertHTML.html(), AssertHTML.css_selector()) :: integer() 47 | def count(html, selector) do 48 | html |> find(selector) |> Enum.count() 49 | end 50 | 51 | @doc """ 52 | Returns attribute value for a given selector. 53 | """ 54 | @spec attribute(AssertHTML.html(), String.t()) :: String.t() | nil 55 | def attribute(html, name) do 56 | html 57 | |> Floki.parse_document!() 58 | |> Floki.attribute(name) 59 | |> case do 60 | [value] -> value 61 | _ -> nil 62 | end 63 | end 64 | 65 | @spec text(AssertHTML.html()) :: String.t() 66 | def text(html_element_tuple) do 67 | html_element_tuple 68 | |> Floki.parse_document!() 69 | |> Floki.text(deep: false) 70 | end 71 | 72 | @spec to_html(html_tree) :: String.t() 73 | def to_html(html_element_tuple) do 74 | Floki.raw_html(html_element_tuple) 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/assert_html/selector.ex: -------------------------------------------------------------------------------- 1 | defmodule AssertHTML.Selector do 2 | @moduledoc false 3 | 4 | alias AssertHTML.Parser 5 | 6 | @spec find(AssertHTML.html(), AssertHTML.css_selector()) :: AssertHTML.html() | nil 7 | def find(_html, nil) do 8 | nil 9 | end 10 | 11 | def find(html, css_selector) do 12 | html 13 | |> Parser.find(css_selector) 14 | |> Parser.to_html() 15 | |> case do 16 | "" -> nil 17 | other when is_binary(other) -> other 18 | end 19 | end 20 | 21 | @spec attribute(AssertHTML.html(), atom() | binary()) :: nil | binary() 22 | def attribute(html, attribute_name) when is_binary(html) and is_atom(attribute_name) do 23 | attribute(html, to_string(attribute_name)) 24 | end 25 | 26 | def attribute(html, "text") when is_binary(html) do 27 | text(html) 28 | end 29 | 30 | def attribute(html, attribute_name) when is_binary(html) and is_binary(attribute_name) do 31 | Parser.attribute(html, attribute_name) 32 | end 33 | 34 | @doc ~S""" 35 | Gets text from HTML element 36 | """ 37 | @spec text(AssertHTML.html()) :: binary() 38 | def text(html) when is_binary(html) do 39 | html 40 | |> Parser.text() 41 | |> String.trim() 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule AssertHTML.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.2.1" 5 | @github_url "https://github.com/Kr00lIX/assert_html" 6 | 7 | def project do 8 | [ 9 | app: :assert_html, 10 | version: @version, 11 | elixir: "~> 1.17", 12 | build_embedded: Mix.env() == :prod, 13 | start_permanent: Mix.env() == :prod, 14 | deps: deps(), 15 | 16 | # Hex 17 | description: "ExUnit assert helpers for testing rendered HTML.", 18 | source_url: @github_url, 19 | package: package(), 20 | 21 | # Docs 22 | name: "AssertHTML", 23 | source_url: @github_url, 24 | docs: docs(), 25 | 26 | # Test 27 | test_coverage: [tool: ExCoveralls], 28 | preferred_cli_env: [ 29 | coveralls: :test, 30 | "coveralls.detail": :test, 31 | "coveralls.post": :test, 32 | "coveralls.html": :test, 33 | "coveralls.travis": :test 34 | ], 35 | 36 | # dev 37 | dialyzer: [ 38 | ignore_warnings: ".dialyzer_ignore.exs", 39 | list_unused_filters: true, 40 | remove_defaults: [:unknown] 41 | ] 42 | ] 43 | end 44 | 45 | # Run "mix help compile.app" to learn about applications. 46 | def application do 47 | [ 48 | extra_applications: [] 49 | ] 50 | end 51 | 52 | # Run "mix help deps" to learn about dependencies. 53 | defp deps do 54 | [ 55 | {:floki, ">= 0.30.0"}, 56 | # {:html5ever, "~> 0.15.0"}, 57 | # {:fast_html, ">= 0.0.1"}, 58 | 59 | # Test 60 | {:excoveralls, "~> 0.18", only: :test}, 61 | {:junit_formatter, "~> 3.4", only: :test}, 62 | {:credo, "~> 1.7", only: [:dev, :test]}, 63 | 64 | # Dev 65 | {:dialyxir, "~> 1.4", only: :dev, runtime: false}, 66 | {:ex_doc, "~> 0.35", only: :dev, runtime: false} 67 | ] 68 | end 69 | 70 | defp docs do 71 | [ 72 | main: "AssertHTML", 73 | source_ref: "v#{@version}", 74 | extras: ["README.md", "CHANGELOG.md"], 75 | source_url: @github_url, 76 | deps: [Floki: "https://hexdocs.pm/floki/Floki.html"] 77 | ] 78 | end 79 | 80 | # Settings for publishing in Hex package manager: 81 | defp package do 82 | %{ 83 | name: "assert_html", 84 | contributors: ["Kr00lIX"], 85 | maintainers: ["Anatolii Kovalchuk"], 86 | links: %{"GitHub" => @github_url}, 87 | licenses: ["MIT"], 88 | files: ~w(.formatter.exs mix.exs README.md CHANGELOG.md lib) 89 | } 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, 4 | "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 6 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 7 | "ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"}, 8 | "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"}, 9 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 10 | "floki": {:hex, :floki, "0.37.1", "d7aaee758c8a5b4a7495799a4260754fec5530d95b9c383c03b27359dea117cf", [:mix], [], "hexpm", "673d040cb594d31318d514590246b6dd587ed341d3b67e17c1c0eb8ce7ca6f04"}, 11 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 12 | "junit_formatter": {:hex, :junit_formatter, "3.4.0", "d0e8db6c34dab6d3c4154c3b46b21540db1109ae709d6cf99ba7e7a2ce4b1ac2", [:mix], [], "hexpm", "bb36e2ae83f1ced6ab931c4ce51dd3dbef1ef61bb4932412e173b0cfa259dacd"}, 13 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 14 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 15 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 16 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 17 | } 18 | -------------------------------------------------------------------------------- /test/assert_html/dsl_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AssertHTML.DSLTest do 2 | use ExUnit.Case, async: true 3 | import AssertHTML.DSL 4 | 5 | describe "(context definition)" do 6 | setup do 7 | [html: ~S{ 8 |
9 |

Title

10 |

11 | Yes 12 | No 13 |

14 |
15 | }] 16 | end 17 | 18 | test "gets context html for defining context", %{html: html} do 19 | assert_html(html) do 20 | assert assert_html == 21 | "\n
\n

Title

\n

\n Yes\n No\n

\n
\n " 22 | 23 | assert_html("p") do 24 | assert assert_html == 25 | "

YesNo

" 26 | 27 | assert_html("a.link1", class: "active", text: "Yes") 28 | assert_html("a.link2", id: nil, text: "No") 29 | end 30 | end 31 | end 32 | 33 | test "use macro for defining context with selector" do 34 | html = ~S{ 35 |

36 | Click me 37 |

38 | } 39 | 40 | assert_html(html, "p") do 41 | assert_html("a", class: "link", text: "Click me", id: nil) 42 | end 43 | end 44 | 45 | test "use macro for defining context with selector and attributes" do 46 | html = ~S{ 47 |

48 | Click me 49 |

50 | } 51 | 52 | assert_html(html, "p", class: "foo", id: "descr") do 53 | assert_html("a", class: "link", text: "Click me", id: nil) 54 | end 55 | end 56 | end 57 | 58 | describe "(check simple form)" do 59 | setup do 60 | [html: ~S{ 61 |
62 |
63 | 64 |
65 | 66 |
67 |
68 |
69 | 70 |
71 | 72 |
73 |
74 | 75 |
76 | }] 77 | end 78 | 79 | test "check elements", %{html: html} do 80 | html 81 | |> assert_html("form", class: "form", method: "post", action: "/session/login") do 82 | refute_html(".message") 83 | 84 | assert_html ".-email" do 85 | assert_html("label", text: "Email", for: "staticEmail", class: "col-form-label") 86 | 87 | assert_html("div input", 88 | type: "text", 89 | readonly: true, 90 | class: "form-control-plaintext", 91 | value: "email@example.com" 92 | ) 93 | end 94 | 95 | assert_html(".-password") do 96 | assert_html("label", text: "Password", for: "inputPassword") 97 | 98 | assert_html("div input", 99 | placeholder: "Password", 100 | type: "password", 101 | class: "form-control", 102 | id: "inputPassword", 103 | placeholder: "Password" 104 | ) 105 | end 106 | 107 | assert_html("button", type: "submit", class: "primary") 108 | end 109 | end 110 | end 111 | 112 | describe "(check contains)" do 113 | setup do 114 | [html: ~S{ 115 |
116 |

Hello World

117 |
118 | }] 119 | end 120 | 121 | test "expect find contain text", %{html: html} do 122 | assert_html(html, ~r{Hello World}) 123 | refute_html(html, ~r{Another World}) 124 | 125 | assert_html(html, ".content") do 126 | assert_html(~r{Hello World}) 127 | end 128 | end 129 | end 130 | 131 | describe "(pass pipeline)" do 132 | test "pass html to method through pipeline" do 133 | ~S{ 134 |
135 | Click me 136 |
137 | } 138 | |> assert_html("#qwe") do 139 | assert_html("a", class: "link", text: "Click me", id: nil) 140 | end 141 | end 142 | end 143 | 144 | describe "(check attributes)" do 145 | setup do 146 | [html: ~S{ 147 | 148 | 149 | }] 150 | end 151 | 152 | test "expect refute checked without errors", %{html: html} do 153 | refute_html(html, "input[type=radio][value=two]", checked: true) 154 | end 155 | 156 | test "expect check attributes as map", %{html: html} do 157 | assert_html(html, "#field_two", %{"type" => "radio", "value" => "two", "name" => "field[birds]"}) 158 | end 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /test/assert_html/matcher_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AssertHTMLTest.MatcherTest do 2 | use ExUnit.Case, async: true 3 | doctest AssertHTML.Matcher, import: true 4 | import AssertHTML.Matcher 5 | alias ExUnit.AssertionError 6 | 7 | describe ".attributes/3" do 8 | setup do 9 | [ 10 | html: ~S{
quotes: " & '
} 11 | ] 12 | end 13 | 14 | test "raise error for unexpected attribute", %{html: html} do 15 | assert_raise AssertionError, ~r{Attribute `class` shouldn't exists.}, fn -> 16 | attributes({:assert, html}, class: nil) 17 | end 18 | 19 | attributes({:refute, html}, class: nil) 20 | end 21 | 22 | test "expect check `class` attribute splitted by space", %{html: html} do 23 | attributes({:assert, html}, class: "table") 24 | attributes({:assert, html}, class: "table -vertical") 25 | attributes({:assert, html}, class: "-vertical") 26 | 27 | message = ~r{Class `wrong_class` not found in `table -vertical` class attribute} 28 | 29 | assert_raise AssertionError, message, fn -> 30 | attributes({:assert, html}, class: "wrong_class") 31 | end 32 | 33 | attributes({:refute, html}, class: "container") 34 | 35 | assert_raise AssertionError, ~r"Class `-vertical` found in `table -vertical` class attribute", fn -> 36 | attributes({:refute, html}, class: "-vertical") 37 | end 38 | end 39 | 40 | test "expect check escaped text from `text` attribute", %{html: html} do 41 | attributes({:assert, html}, text: "quotes: \" & '") 42 | attributes({:assert, html}, text: ~r"quotes:") 43 | end 44 | 45 | test "expect error if attribute not exsists", %{html: html} do 46 | message = 47 | "\n\nAttribute `id` not found.\n\n \t
quotes: " & '
\n\n" 48 | 49 | assert_raise AssertionError, message, fn -> 50 | attributes({:assert, html}, id: "new_element") 51 | end 52 | 53 | assert_raise AssertionError, ~r"Attribute `id` should exists.", fn -> 54 | attributes({:refute, html}, id: nil) 55 | end 56 | end 57 | 58 | test "expect stringify values for checking attribuites" do 59 | html = ~S{} 60 | attributes({:assert, html}, value: 111, id: "zoo") 61 | attributes({:refute, html}, value: 222) 62 | end 63 | 64 | test "check if attribute not exsists" do 65 | html = ~S{} 66 | attributes({:assert, html}, type: "checkbox", checked: nil) 67 | attributes({:refute, html}, type: nil) 68 | end 69 | 70 | test "check if attribute exsists" do 71 | html = ~S{} 72 | attributes({:assert, html}, type: "text", readonly: true) 73 | attributes({:refute, html}, type: "tel", readonly: false) 74 | 75 | assert_raise AssertionError, ~r"Attribute `readonly` shouldn't exists.", fn -> 76 | attributes({:assert, html}, readonly: false) 77 | end 78 | 79 | assert_raise AssertionError, ~r"Attribute `readonly` shouldn't exists.", fn -> 80 | attributes({:refute, html}, readonly: true) 81 | end 82 | end 83 | end 84 | 85 | describe ".contain" do 86 | setup do 87 | [html: ~S{

Merry Christmas

}] 88 | end 89 | 90 | test "expect check value", %{html: html} do 91 | contain({:assert, html}, ~r"Merry Christmas") 92 | contain({:assert, html}, ~r"

Merry Christmas

") 93 | contain({:refute, html}, ~r"Peper") 94 | contain({:refute, html}, ~r"

Merry Christmas") 95 | end 96 | 97 | test "expect raise error for unmached value", %{html: html} do 98 | assert_raise AssertionError, ~r{Value `~r/Merry Christmas/` matched, but shouldn't.}, fn -> 99 | contain({:refute, html}, ~r"Merry Christmas") 100 | end 101 | 102 | assert_raise AssertionError, ~r"Value not matched.", fn -> 103 | contain({:assert, html}, ~r"

Merry Christmas") 104 | end 105 | end 106 | end 107 | 108 | describe ".selector" do 109 | setup do 110 | [html: ~S{
112 |
  • One
  • 113 |
  • Two
  • 114 | 115 |
    }] 116 | end 117 | 118 | test "assert: raise error if found more than two elements", %{html: html} do 119 | assert_raise AssertionError, ~r{Found more than one element by `.container li` selector}, fn -> 120 | selector({:assert, html}, ".container li", once: true) 121 | end 122 | end 123 | 124 | test "assert: returns HTML if element exsists", %{html: html} do 125 | assert selector({:assert, html}, ".container li:first-child") == "
  • One
  • " 126 | end 127 | 128 | test "refute: raise error if element exsists", %{html: html} do 129 | assert_raise AssertionError, ~r{Selector `.container li` succeeded, but should have failed.}, fn -> 130 | selector({:refute, html}, ".container li") 131 | end 132 | end 133 | 134 | test "assert: returns HTML if selection exsists", %{html: html} do 135 | assert selector({:assert, html}, ".container li") == "
  • One
  • Two
  • " 136 | end 137 | 138 | test "refute: raise error if selection exsists", %{html: html} do 139 | assert_raise AssertionError, ~r{Selector }, fn -> 140 | selector({:refute, html}, ".container li") 141 | end 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /test/assert_html/selector_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AssertHTMLTest.SelectorTest do 2 | use ExUnit.Case, async: true 3 | doctest AssertHTML.Selector, import: true 4 | import AssertHTML.Selector 5 | 6 | describe ".attribute/2" do 7 | setup do 8 | [html: ~S{Click Me}] 9 | end 10 | 11 | test "get inner text for `text` attribute", %{html: html} do 12 | assert attribute(html, "text") == "Click Me" 13 | end 14 | 15 | test "expect returns unparsed class attribute for element", %{html: html} do 16 | assert attribute(html, "class") == "red bold" 17 | end 18 | 19 | test "expect get attribute by atom name", %{html: html} do 20 | assert attribute(html, :id) == "cta" 21 | end 22 | 23 | test "expect returns nil for non exsising attribute", %{html: html} do 24 | assert attribute(html, "data") == nil 25 | end 26 | end 27 | 28 | describe ".text/2" do 29 | test "returns text from attribute" do 30 | assert text(~S{Click Me}) == "Click Me" 31 | end 32 | 33 | test "expect get unescaped text" do 34 | assert text(~S{

    M & F}) == "M & F" 35 | end 36 | 37 | test "expect get emptry string if not exists" do 38 | assert text(~S{}) == "" 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/assert_html_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AssertHTMLTest do 2 | use ExUnit.Case, async: true 3 | doctest AssertHTML, import: true 4 | import AssertHTML 5 | alias ExUnit.AssertionError 6 | 7 | describe "assert_html (check css selector)" do 8 | setup do 9 | [html: ~S{ 10 |

    11 |

    Hello

    12 |

    13 | Paragraph 14 |

    15 |

    World

    16 |
    17 | }] 18 | end 19 | 20 | test "expect match selector", %{html: html} do 21 | assert_html(html, "p") 22 | assert_html(html, ".container .description") 23 | 24 | refute_html(html, "table") 25 | refute_html(html, ".container h5") 26 | end 27 | 28 | test "expect pass to callback selected html", %{html: html} do 29 | result_html = 30 | assert_html(html, ".container", fn sub_html -> 31 | assert sub_html == 32 | "

    Hello

    \n Paragraph\n

    World

    " 33 | 34 | assert_html(sub_html, ".description", fn sub_html -> 35 | assert sub_html == "

    \n Paragraph\n

    " 36 | end) 37 | end) 38 | 39 | assert result_html == 40 | "\n
    \n

    Hello

    \n

    \n Paragraph\n

    \n

    World

    \n
    \n " 41 | end 42 | 43 | test "raise AssertionError exception for unmatched selection", %{html: html} do 44 | assert_raise AssertionError, ~r{Element `.invalid .selector` not found.}, fn -> 45 | assert_html(html, ".invalid .selector") 46 | end 47 | 48 | assert_raise AssertionError, ~r{Selector `.container h1` succeeded, but should have failed}, fn -> 49 | refute_html(html, ".container h1") 50 | end 51 | end 52 | end 53 | 54 | describe ".assert_html (check attributes)" do 55 | setup do 56 | html = ~S{ 57 |
    58 |

    Hello & HTML

    59 |

    60 | Long Read Paragraph 61 |

    62 | World 63 |
    64 | } 65 | [html: html] 66 | end 67 | 68 | test "expect pass equal attributes", %{html: html} do 69 | assert_html(html, "#main", [class: "container", id: "main", text: "World"], fn sub_html -> 70 | assert_html(sub_html, "h1", class: nil, text: "Hello & HTML") 71 | refute_html(sub_html, "h2") 72 | assert_html(sub_html, "p", class: "highlight", text: ~r"Read") 73 | end) 74 | end 75 | end 76 | 77 | describe ".assert_html (check contains)" do 78 | setup do 79 | [html: ~S{ 80 |
    81 |

    Hello World

    82 |
    83 | }] 84 | end 85 | 86 | test "expect find contain text", %{html: html} do 87 | assert_html(html, ~r{Hello World}) 88 | refute_html(html, ~r{Another World}) 89 | 90 | assert_html(html, ".content", fn sub_html -> 91 | assert_html(sub_html, ~r{Hello World}) 92 | end) 93 | end 94 | 95 | test "check contains in selector", %{html: html} do 96 | assert_raise AssertionError, ~r"Value not matched.", fn -> 97 | assert_html(html, "h1", ~r{Hello World!!!!}) 98 | end 99 | 100 | assert_html(html, "h1", ~r{Hello World}) 101 | assert_html(html, "h1", "Hello World") 102 | 103 | assert_raise AssertionError, ~r"Value not found", fn -> 104 | assert_html(html, "h1", "Hello World!!!!") 105 | end 106 | 107 | refute_html(html, "h1", match: "Hello World!!!") 108 | end 109 | 110 | test "check contains as a second argument", %{html: html} do 111 | refute_html(html, "h1", ~r{Hello World!!!!}) 112 | 113 | assert_raise AssertionError, ~r"Value `~r/Hello World/` matched, but shouldn't.", fn -> 114 | refute_html(html, "h1", ~r{Hello World}) 115 | end 116 | 117 | assert_raise AssertionError, ~r{Value `"Hello World"` found, but shouldn't.}, fn -> 118 | refute_html(html, "h1", "Hello World") 119 | end 120 | 121 | refute_html(html, "h1", "Hello World!!!!") 122 | end 123 | 124 | test "check match as attribute argument", %{html: html} do 125 | assert_html(html, match: "Hello World") 126 | 127 | assert_raise AssertionError, ~r"Value not found", fn -> 128 | assert_html(html, "h1", match: "Hello World!!!!") 129 | end 130 | 131 | refute_html(html, match: "Hello World!!!!") 132 | end 133 | end 134 | 135 | describe ".asserh_html (multiply elements)" do 136 | setup do 137 | [html: ~S{ 138 |
    139 |

    Header

    140 | 145 |
    146 | }] 147 | end 148 | 149 | test "check `first-child` or `nth-of-type` css selectors", %{html: html} do 150 | assert_html(html, ".container", fn sub_html -> 151 | assert_html(sub_html, ".item:first-child", "First") 152 | assert_html(sub_html, ".item:nth-child(2)", "Second") 153 | assert_html(sub_html, ".item:nth-of-type(3)", "Third") 154 | refute_html(sub_html, ".item:nth-child(4)") 155 | end) 156 | end 157 | 158 | test "raise error if gets more than on element by selector", %{html: html} do 159 | assert_raise AssertionError, ~r"Found more than one element by `.container li` selector.", fn -> 160 | assert_html(html, ".container li", "First") 161 | end 162 | 163 | assert_raise AssertionError, ~r"Selector `.container li` succeeded, but should have failed.", fn -> 164 | refute_html(html, ".container li") 165 | end 166 | end 167 | 168 | test "expect count meta-attribute to equal number of elements found", %{html: html} do 169 | assert_html(html, ".container", [count: 1], fn sub_html -> 170 | assert_html(sub_html, "h1", count: 1) 171 | assert_html(sub_html, "li", count: 3) 172 | end) 173 | end 174 | 175 | test "expect min meta-attribute that number of elements found is greater than or equal", %{html: html} do 176 | assert_html(html, ".container", [min: 1], fn sub_html -> 177 | assert_html(sub_html, "h1", min: 1) 178 | assert_html(sub_html, "li", min: 3) 179 | end) 180 | end 181 | 182 | test "expect max meta-attribute that number of elements found is less than or equal", %{html: html} do 183 | assert_html(html, ".container", [max: 1], fn sub_html -> 184 | assert_html(sub_html, "h1", max: 1) 185 | assert_html(sub_html, "li", max: 3) 186 | end) 187 | end 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------