├── .editorconfig ├── .github ├── CONTRIBUTING.md ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── code-coverage.yml │ ├── coding-standards.yml │ ├── static-code-analysis.yml │ └── unit-tests.yml ├── .gitignore ├── .phpcs.xml.dist ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── _config.yml ├── composer.json ├── phpstan.neon.dist ├── phpunit.xml.dist ├── src └── MarkupAssertionsTrait.php └── tests └── MarkupAssertionsTraitTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # PHP PSR-2 Coding Standards 5 | # http://www.php-fig.org/psr/psr-2/ 6 | 7 | root = true 8 | 9 | [*] 10 | charset = utf-8 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.php] 15 | end_of_line = lf 16 | indent_style = space 17 | indent_size = 4 18 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to PHPUnit Markup Assertions 2 | 3 | Thank you for your interest in contributing the ongoing development of the PHPUnit Markup Assertion library! 4 | 5 | 6 | ## Contributing code 7 | 8 | Begin by cloning the GitHub repo locally and installing the dependencies with [Composer](https://getcomposer.org): 9 | 10 | ```sh 11 | # Clone the repository + change into the directory 12 | $ git clone https://github.com/stevegrunwell/phpunit-markup-assertions.git \ 13 | && cd phpunit-markup-assertions 14 | 15 | # Install local dependencies 16 | $ composer install 17 | ``` 18 | 19 | 20 | ### Branching 21 | 22 | Pull requests should be based off the `develop` branch, which represents the current development state of the library. The only thing ever merged into `master` should be new release branches, at the time a release is tagged. 23 | 24 | To create a new feature branch: 25 | 26 | ```bash 27 | # Start on develop, making sure it's up-to-date 28 | $ git checkout develop && git pull 29 | 30 | # Create a new branch for your feature 31 | $ git checkout -b feature/my-cool-new-feature 32 | ``` 33 | 34 | When submitting a new pull request, your `feature/my-cool-new-feature` should be compared against `develop`. 35 | 36 | 37 | ### Coding standards 38 | 39 | This project uses [the PSR-2 coding standards](http://www.php-fig.org/psr/psr-2/). 40 | 41 | 42 | ### Running unit tests 43 | 44 | [PHPUnit](https://phpunit.de/) is included as a development dependency, and should be run regularly. When submitting changes, please be sure to add or update unit tests accordingly. You may run unit tests at any time by running: 45 | 46 | ```bash 47 | $ composer test 48 | ``` 49 | 50 | #### Code coverage 51 | 52 | [![Coverage Status](https://coveralls.io/repos/github/stevegrunwell/phpunit-markup-assertions/badge.svg?branch=develop)](https://coveralls.io/github/stevegrunwell/phpunit-markup-assertions?branch=develop) 53 | 54 | To generate a report of code coverage for the current branch, you may run the following Composer script, which will generate an HTML report in `tests/coverage/`: 55 | 56 | ```bash 57 | $ composer test-coverage 58 | ``` 59 | 60 | Note that [both the Xdebug and tokenizer PHP extensions must be installed and active](https://phpunit.de/manual/current/en/textui.html) on the machine running the tests. 61 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [stevegrunwell] 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: phpunit/phpunit 11 | versions: 12 | - "> 6.0" 13 | -------------------------------------------------------------------------------- /.github/workflows/code-coverage.yml: -------------------------------------------------------------------------------- 1 | name: Code Coverage 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - develop 8 | - main 9 | 10 | jobs: 11 | coverage: 12 | name: Report code coverage 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup PHP 19 | uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: '8.2' 22 | coverage: xdebug 23 | 24 | - name: Install Composer dependencies 25 | uses: ramsey/composer-install@v2 26 | 27 | - name: Run test suite 28 | run: vendor/bin/simple-phpunit --coverage-text --coverage-clover=tests/coverage 29 | 30 | - name: Publish to Coveralls 31 | uses: coverallsapp/github-action@v2 32 | with: 33 | files: tests/coverage 34 | format: clover 35 | fail-on-error: false 36 | -------------------------------------------------------------------------------- /.github/workflows/coding-standards.yml: -------------------------------------------------------------------------------- 1 | name: Coding Standards 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | phpcs: 7 | name: PHP_CodeSniffer 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Setup PHP 14 | uses: shivammathur/setup-php@v2 15 | with: 16 | php-version: '8.2' 17 | coverage: none 18 | 19 | - name: Install Composer dependencies 20 | uses: ramsey/composer-install@v2 21 | 22 | - name: Run test suite 23 | run: composer coding-standards 24 | -------------------------------------------------------------------------------- /.github/workflows/static-code-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static Code Analysis 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | phpcs: 7 | name: PHPStan 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Setup PHP 14 | uses: shivammathur/setup-php@v2 15 | with: 16 | php-version: '8.2' 17 | coverage: none 18 | 19 | - name: Install Composer dependencies 20 | uses: ramsey/composer-install@v2 21 | 22 | - name: Run PHPStan 23 | run: composer static-analysis 24 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | phpunit: 7 | name: PHP ${{ matrix.php-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | php-version: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: ${{ matrix.php-version }} 20 | coverage: none 21 | 22 | - name: Remove PHPStan as a dependency 23 | run: composer remove --dev phpstan/phpstan 24 | 25 | - name: Install Composer dependencies 26 | uses: ramsey/composer-install@v2 27 | 28 | - name: Run test suite 29 | run: composer test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | .phpunit.result.cache 3 | .vscode 4 | phpcs.xml 5 | phpstan.neon 6 | phpunit.xml 7 | tests/coverage 8 | vendor 9 | 10 | # The composer.lock file is not needed, as this is a library whose dependencies 11 | # will depend on the version of PHP being used. 12 | composer.lock 13 | -------------------------------------------------------------------------------- /.phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Coding standards for PHPUnit Markup Assertions 4 | 5 | 6 | 7 | 8 | 9 | 10 | . 11 | */vendor/* 12 | 13 | 14 | 15 | 16 | 17 | 18 | tests/* 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.4.0] – 2022-12-28 9 | 10 | * Force UTF-8 encoding for better support for non-Latin character sets ([#35]) 11 | * Move away from deprecated classes in laminas/laminas-dom ([#32]) 12 | 13 | ## [1.3.1] — 2020-01-14 14 | 15 | * Fix PHPUnit warnings regarding `assertContains()` and `assertRegExp()`. Props [@jakobbuis](https://github.com/jakobbuis) ([#20], [#27], [#28]) 16 | * Refactor the internal test scaffolding, including a move from Travis CI to GitHub Actions. Props [@peter279k](https://github.com/peter279k) for the assist with GitHub Actions ([#24]) 17 | * Added PHP 8.0 support ([#26]) 18 | 19 | ## [1.3.0] — 2020-01-27 20 | 21 | * Replace `zendframework/zend-dom` with `laminas/laminas-dom` ([#16]) 22 | * Update Composer dependencies, add a `composer test` script ([#15]) 23 | 24 | 25 | ## [1.2.0] - 2018-03-27 26 | 27 | * Bumped the minimum version of zendframework/zend-dom to 2.7, which includes a fix for attribute values that include spaces ([#13]). 28 | 29 | 30 | ## [1.1.0] - 2018-01-14 31 | 32 | * Added the `assertElementContains()`, `assertElementNotContains()`, `assertElementRegExp()`, and `assertElementNotRegExp()` assertions, for verifying the contents of elements that match the given DOM query ([#6]) 33 | * Moved the `Tests` namespace into a development-only autoloader, to prevent them from potentially being included in projects using this library ([#7]) 34 | * [Based on this article by Martin Hujer](https://blog.martinhujer.cz/17-tips-for-using-composer-efficiently/#tip-%236%3A-put-%60composer.lock%60-into-%60.gitignore%60-in-libraries), remove the `composer.lock` file from the library ([#8]) 35 | * _Lower_ the minimum version of [zendframework/zend-dom](https://packagist.org/packages/zendframework/zend-dom) to 2.2.5 for maximum portability ([#9]) 36 | 37 | 38 | ## [1.0.0] - 2017-10-24 39 | 40 | * Initial release of the PHPUnit Markup Assertions Composer package. 41 | 42 | 43 | [Unreleased]: https://github.com/stevegrunwell/phpunit-markup-assertions/compare/main...develop 44 | [1.4.0]: https://github.com/stevegrunwell/phpunit-markup-assertions/releases/tag/v1.4.0 45 | [1.3.1]: https://github.com/stevegrunwell/phpunit-markup-assertions/releases/tag/v1.3.1 46 | [1.3.0]: https://github.com/stevegrunwell/phpunit-markup-assertions/releases/tag/v1.3.0 47 | [1.2.0]: https://github.com/stevegrunwell/phpunit-markup-assertions/releases/tag/v1.2.0 48 | [1.1.0]: https://github.com/stevegrunwell/phpunit-markup-assertions/releases/tag/v1.1.0 49 | [1.0.0]: https://github.com/stevegrunwell/phpunit-markup-assertions/releases/tag/v1.0.0 50 | [#6]: https://github.com/stevegrunwell/phpunit-markup-assertions/issues/6 51 | [#7]: https://github.com/stevegrunwell/phpunit-markup-assertions/issues/7 52 | [#8]: https://github.com/stevegrunwell/phpunit-markup-assertions/issues/8 53 | [#9]: https://github.com/stevegrunwell/phpunit-markup-assertions/issues/9 54 | [#13]: https://github.com/stevegrunwell/phpunit-markup-assertions/issues/13 55 | [#15]: https://github.com/stevegrunwell/phpunit-markup-assertions/pull/15 56 | [#16]: https://github.com/stevegrunwell/phpunit-markup-assertions/issues/16 57 | [#20]: https://github.com/stevegrunwell/phpunit-markup-assertions/pull/20 58 | [#24]: https://github.com/stevegrunwell/phpunit-markup-assertions/pull/24 59 | [#26]: https://github.com/stevegrunwell/phpunit-markup-assertions/pull/26 60 | [#27]: https://github.com/stevegrunwell/phpunit-markup-assertions/pull/27 61 | [#28]: https://github.com/stevegrunwell/phpunit-markup-assertions/pull/28 62 | [#32]: https://github.com/stevegrunwell/phpunit-markup-assertions/pull/32 63 | [#35]: https://github.com/stevegrunwell/phpunit-markup-assertions/pull/35 64 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2017 Steve Grunwell 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHPUnit Markup Assertions 2 | 3 | ![Build Status](https://github.com/stevegrunwell/phpunit-markup-assertions/workflows/Unit%20Tests/badge.svg) 4 | [![Code Coverage](https://coveralls.io/repos/github/stevegrunwell/phpunit-markup-assertions/badge.svg?branch=develop)](https://coveralls.io/github/stevegrunwell/phpunit-markup-assertions?branch=develop) 5 | [![GitHub Release](https://img.shields.io/github/release/stevegrunwell/phpunit-markup-assertions.svg)](https://github.com/stevegrunwell/phpunit-markup-assertions/releases) 6 | 7 | This library introduces the `MarkupAssertionsTrait` trait for use in [PHPUnit](https://phpunit.de) tests. 8 | 9 | These assertions enable you to inspect generated markup without having to muddy tests with [`DOMDocument`](http://php.net/manual/en/class.domdocument.php) or nasty regular expressions. If you're generating markup at all with PHP, the PHPUnit Markup Assertions trait aims to make the output testable without making your tests fragile. 10 | 11 | ## Example 12 | 13 | ```php 14 | use PHPUnit\Framework\TestCase; 15 | use SteveGrunwell\PHPUnit_Markup_Assertions\MarkupAssertionsTrait; 16 | 17 | class MyUnitTest extends TestCase 18 | { 19 | use MarkupAssertionsTrait; 20 | 21 | /** 22 | * Ensure the #first-name and #last-name selectors are present in the form. 23 | */ 24 | public function testRenderFormContainsInputs() 25 | { 26 | $markup = render_form(); 27 | 28 | $this->assertContainsSelector('#first-name', $markup); 29 | $this->assertContainsSelector('#last-name', $markup); 30 | } 31 | } 32 | ``` 33 | 34 | ## Installation 35 | 36 | To add PHPUnit Markup Assertions to your project, first install the library via Composer: 37 | 38 | ```sh 39 | $ composer require --dev stevegrunwell/phpunit-markup-assertions 40 | ``` 41 | 42 | Next, import the `SteveGrunwell\PHPUnit_Markup_Assertions\MarkupAssertionsTrait` trait into each test case that will leverage the assertions: 43 | 44 | ```php 45 | use PHPUnit\Framework\TestCase; 46 | use SteveGrunwell\PHPUnit_Markup_Assertions\MarkupAssertionsTrait; 47 | 48 | class MyTestCase extends TestCase 49 | { 50 | use MarkupAssertionsTrait; 51 | } 52 | ``` 53 | 54 | ### Making PHPUnit Markup Assertions available globally 55 | 56 | If you'd like the methods to be available across your entire test suite, you might consider [sub-classing the PHPUnit test case and applying the trait there](https://phpunit.de/manual/current/en/extending-phpunit.html#extending-phpunit.PHPUnit_Framework_TestCase): 57 | 58 | ```php 59 | # tests/TestCase.php 60 | 61 | namespace Tests; 62 | 63 | use PHPUnit\Framework\TestCase as BaseTestCase; 64 | use SteveGrunwell\PHPUnit_Markup_Assertions\MarkupAssertionsTrait; 65 | 66 | class TestCase extends BaseTestCase 67 | { 68 | use MarkupAssertionsTrait; 69 | } 70 | ``` 71 | 72 | Then update your other test cases to use your new base: 73 | 74 | ```php 75 | # tests/Unit/ExampleTest.php 76 | 77 | namespace Tests/Unit; 78 | 79 | use Tests\TestCase; 80 | 81 | class MyUnitTest extends TestCase 82 | { 83 | // This class now automatically has markup assertions. 84 | } 85 | ``` 86 | 87 | ## Available methods 88 | 89 | These are the assertions made available to PHPUnit via the `MarkupAssertionsTrait`. 90 | 91 | * [`assertContainsSelector()`](#assertcontainsselector) 92 | * [`assertNotContainsSelector()`](#assertnotcontainsselector) 93 | * [`assertSelectorCount()`](#assertselectorcount) 94 | * [`assertHasElementWithAttributes()`](#asserthaselementwithattributes) 95 | * [`assertNotHasElementWithAttributes()`](#assertnothaselementwithattributes) 96 | * [`assertElementContains()`](#assertelementcontains) 97 | * [`assertElementNotContains()`](#assertelementnotcontains) 98 | * [`assertElementRegExp()`](#assertelementregexp) 99 | * [`assertElementNotRegExp()`](#assertelementnotregexp) 100 | 101 | ### assertContainsSelector() 102 | 103 | Assert that the given string contains an element matching the given selector. 104 | 105 |
106 |
(string) $selector
107 |
A query selector for the element to find.
108 |
(string) $markup
109 |
The markup that should contain the $selector.
110 |
(string) $message
111 |
A message to display if the assertion fails.
112 |
113 | 114 | #### Example 115 | 116 | ```php 117 | public function testBodyContainsImage() 118 | { 119 | $body = getPageBody(); 120 | 121 | $this->assertContainsSelector('img', $body, 'Did not find an image in the page body.'); 122 | } 123 | ``` 124 | 125 | ### assertNotContainsSelector() 126 | 127 | Assert that the given string does not contain an element matching the given selector. 128 | 129 | This method is the inverse of [`assertContainsSelector()`](#assertcontainsselector). 130 | 131 |
132 |
(string) $selector
133 |
A query selector for the element to find.
134 |
(string) $markup
135 |
The markup that should not contain the $selector.
136 |
(string) $message
137 |
A message to display if the assertion fails.
138 |
139 | 140 | ### assertSelectorCount() 141 | 142 | Assert the number of times an element matching the given selector is found. 143 | 144 |
145 |
(int) $count
146 |
The number of matching elements expected.
147 |
(string) $selector
148 |
A query selector for the element to find.
149 |
(string) $markup
150 |
The markup to run the assertion against.
151 |
(string) $message
152 |
A message to display if the assertion fails.
153 |
154 | 155 | #### Example 156 | 157 | ```php 158 | public function testPostList() 159 | { 160 | factory(Post::class, 10)->create(); 161 | 162 | $response = $this->get('/posts'); 163 | 164 | $this->assertSelectorCount(10, 'li.post-item', $response->getBody()); 165 | } 166 | ``` 167 | 168 | ### assertHasElementWithAttributes() 169 | 170 | Assert that an element with the given attributes exists in the given markup. 171 | 172 |
173 |
(array) $attributes
174 |
An array of HTML attributes that should be found on the element.
175 |
(string) $markup
176 |
The markup that should contain an element with the provided $attributes.
177 |
(string) $message
178 |
A message to display if the assertion fails.
179 |
180 | 181 | #### Example 182 | 183 | ```php 184 | public function testExpectedInputsArePresent() 185 | { 186 | $user = getUser(); 187 | $form = getFormMarkup(); 188 | 189 | $this->assertHasElementWithAttributes( 190 | [ 191 | 'name' => 'first-name', 192 | 'value' => $user->first_name, 193 | ], 194 | $form, 195 | 'Did not find the expected input for the user first name.' 196 | ); 197 | } 198 | ``` 199 | 200 | ### assertNotHasElementWithAttributes() 201 | 202 | Assert that an element with the given attributes does not exist in the given markup. 203 | 204 |
205 |
(array) $attributes
206 |
An array of HTML attributes that should not be found on the element.
207 |
(string) $markup
208 |
The markup that should not contain an element with the provided $attributes.
209 |
(string) $message
210 |
A message to display if the assertion fails.
211 |
212 | 213 | ### assertElementContains() 214 | 215 | Assert that the element with the given selector contains a string. 216 | 217 |
218 |
(string) $contents
219 |
The string to look for within the DOM node's contents.
220 |
(string) $selector
221 |
A query selector for the element to find.
222 |
(string) $markup
223 |
The markup that should contain the $selector.
224 |
(string) $message
225 |
A message to display if the assertion fails.
226 |
227 | 228 | #### Example 229 | 230 | ```php 231 | public function testColumnShowsUserEmail() 232 | { 233 | $user = getUser(); 234 | $table = getTableMarkup(); 235 | 236 | $this->assertElementContains( 237 | $user->email, 238 | 'td.email', 239 | $table, 240 | 'The should contain the user\'s email address.' 241 | ); 242 | } 243 | ``` 244 | 245 | ### assertElementNotContains() 246 | 247 | Assert that the element with the given selector does not contain a string. 248 | 249 | This method is the inverse of [`assertElementContains()`](#assertelementcontains). 250 | 251 |
252 |
(string) $contents
253 |
The string to look for within the DOM node's contents.
254 |
(string) $selector
255 |
A query selector for the element to find.
256 |
(string) $markup
257 |
The markup that should contain the $selector.
258 |
(string) $message
259 |
A message to display if the assertion fails.
260 |
261 | 262 | ### assertElementRegExp() 263 | 264 | Assert that the element with the given selector contains a string. 265 | 266 | This method works just like [`assertElementContains()`](#assertelementcontains), but uses regular expressions instead of simple string matching. 267 | 268 |
269 |
(string) $regexp
270 |
The regular expression pattern to look for within the DOM node.
271 |
(string) $selector
272 |
A query selector for the element to find.
273 |
(string) $markup
274 |
The markup that should contain the $selector.
275 |
(string) $message
276 |
A message to display if the assertion fails.
277 |
278 | 279 | ### assertElementNotRegExp() 280 | 281 | Assert that the element with the given selector does not contain a string. 282 | 283 | This method is the inverse of [`assertElementRegExp()`](#assertelementregexp) and behaves like [`assertElementNotContains()`](#assertelementnotcontains) except with regular expressions instead of simple string matching. 284 | 285 |
286 |
(string) $regexp
287 |
The regular expression pattern to look for within the DOM node.
288 |
(string) $selector
289 |
A query selector for the element to find.
290 |
(string) $markup
291 |
The markup that should contain the $selector.
292 |
(string) $message
293 |
A message to display if the assertion fails.
294 |
295 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stevegrunwell/phpunit-markup-assertions", 3 | "description": "Assertions for PHPUnit to verify the presence or state of elements within markup", 4 | "keywords": ["phpunit", "testing", "markup", "dom"], 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Steve Grunwell", 10 | "homepage": "https://stevegrunwell.com" 11 | } 12 | ], 13 | "support": { 14 | "issues": "https://github.com/stevegrunwell/phpunit-markup-assertions/issues", 15 | "source": "https://github.com/stevegrunwell/phpunit-markup-assertions/" 16 | }, 17 | "require": { 18 | "php": "^5.6 || ^7.0 || ^8.0", 19 | "symfony/css-selector": "^3.4|^4.4|^5.4|^6.0", 20 | "symfony/dom-crawler": "^3.4|^4.4|^5.4|^6.0" 21 | }, 22 | "require-dev": { 23 | "dealerdirect/phpcodesniffer-composer-installer": "^1.0", 24 | "phpcompatibility/php-compatibility": "^9.3", 25 | "phpstan/phpstan": "^1.10", 26 | "squizlabs/php_codesniffer": "^3.7", 27 | "symfony/phpunit-bridge": "^5.2 || ^6.2 || ^7.0" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "SteveGrunwell\\PHPUnit_Markup_Assertions\\": "src/" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Tests\\": "tests/" 37 | } 38 | }, 39 | "scripts": { 40 | "coding-standards": [ 41 | "phpcs" 42 | ], 43 | "static-analysis": [ 44 | "phpstan analyse" 45 | ], 46 | "test": [ 47 | "simple-phpunit --testdox" 48 | ], 49 | "test-coverage": [ 50 | "XDEBUG_MODE=coverage ./vendor/bin/simple-phpunit --coverage-html=tests/coverage --colors=always" 51 | ] 52 | }, 53 | "scripts-descriptions": { 54 | "coding-standards": "Check coding standards.", 55 | "static-analysis": "Run static code analysis", 56 | "test": "Run all test suites.", 57 | "test-coverage": "Generate code coverage reports in tests/coverage." 58 | }, 59 | "config": { 60 | "preferred-install": "dist", 61 | "sort-packages": true, 62 | "allow-plugins": { 63 | "dealerdirect/phpcodesniffer-composer-installer": true 64 | } 65 | }, 66 | "archive": { 67 | "exclude": [ 68 | "_config.yml", 69 | ".*", 70 | "phpunit.*", 71 | "tests" 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 6 3 | paths: 4 | - src 5 | - tests 6 | excludePaths: 7 | - tests/coverage 8 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests 14 | 15 | 16 | 17 | 18 | ./src 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/MarkupAssertionsTrait.php: -------------------------------------------------------------------------------- 1 | executeDomQuery($markup, $selector); 31 | 32 | $this->assertGreaterThan(0, count($results), $message); 33 | } 34 | 35 | /** 36 | * Assert that the given string does not contain an element matching the given selector. 37 | * 38 | * @since 1.0.0 39 | * 40 | * @param string $selector A query selector for the element to find. 41 | * @param string $markup The output that should not contain the $selector. 42 | * @param string $message A message to display if the assertion fails. 43 | * 44 | * @return void 45 | */ 46 | public function assertNotContainsSelector($selector, $markup = '', $message = '') 47 | { 48 | $results = $this->executeDomQuery($markup, $selector); 49 | 50 | $this->assertEquals(0, count($results), $message); 51 | } 52 | 53 | /** 54 | * Assert the number of times an element matching the given selector is found. 55 | * 56 | * @since 1.0.0 57 | * 58 | * @param int $count The number of matching elements expected. 59 | * @param string $selector A query selector for the element to find. 60 | * @param string $markup The markup to run the assertion against. 61 | * @param string $message A message to display if the assertion fails. 62 | * 63 | * @return void 64 | */ 65 | public function assertSelectorCount($count, $selector, $markup = '', $message = '') 66 | { 67 | $results = $this->executeDomQuery($markup, $selector); 68 | 69 | $this->assertCount($count, $results, $message); 70 | } 71 | 72 | /** 73 | * Assert that an element with the given attributes exists in the given markup. 74 | * 75 | * @since 1.0.0 76 | * 77 | * @param array $attributes An array of HTML attributes that should be found 78 | * on the element. 79 | * @param string $markup The output that should contain an element with the 80 | * provided $attributes. 81 | * @param string $message A message to display if the assertion fails. 82 | * 83 | * @return void 84 | */ 85 | public function assertHasElementWithAttributes($attributes = [], $markup = '', $message = '') 86 | { 87 | $this->assertContainsSelector( 88 | '*' . $this->flattenAttributeArray($attributes), 89 | $markup, 90 | $message 91 | ); 92 | } 93 | 94 | /** 95 | * Assert that an element with the given attributes does not exist in the given markup. 96 | * 97 | * @since 1.0.0 98 | * 99 | * @param array $attributes An array of HTML attributes that should be found 100 | * on the element. 101 | * @param string $markup The output that should not contain an element with 102 | * the provided $attributes. 103 | * @param string $message A message to display if the assertion fails. 104 | * 105 | * @return void 106 | */ 107 | public function assertNotHasElementWithAttributes($attributes = [], $markup = '', $message = '') 108 | { 109 | $this->assertNotContainsSelector( 110 | '*' . $this->flattenAttributeArray($attributes), 111 | $markup, 112 | $message 113 | ); 114 | } 115 | 116 | /** 117 | * Assert an element's contents contain the given string. 118 | * 119 | * @since 1.1.0 120 | * 121 | * @param string $contents The string to look for within the DOM node's contents. 122 | * @param string $selector A query selector for the element to find. 123 | * @param string $markup The output that should contain the $selector. 124 | * @param string $message A message to display if the assertion fails. 125 | * 126 | * @return void 127 | */ 128 | public function assertElementContains($contents, $selector = '', $markup = '', $message = '') 129 | { 130 | $method = method_exists($this, 'assertStringContainsString') 131 | ? 'assertStringContainsString' 132 | : 'assertContains'; // @codeCoverageIgnore 133 | 134 | $this->$method( 135 | $contents, 136 | $this->getInnerHtmlOfMatchedElements($markup, $selector), 137 | $message 138 | ); 139 | } 140 | 141 | /** 142 | * Assert an element's contents do not contain the given string. 143 | * 144 | * @since 1.1.0 145 | * 146 | * @param string $contents The string to look for within the DOM node's contents. 147 | * @param string $selector A query selector for the element to find. 148 | * @param string $markup The output that should not contain the $selector. 149 | * @param string $message A message to display if the assertion fails. 150 | * 151 | * @return void 152 | */ 153 | public function assertElementNotContains($contents, $selector = '', $markup = '', $message = '') 154 | { 155 | $method = method_exists($this, 'assertStringNotContainsString') 156 | ? 'assertStringNotContainsString' 157 | : 'assertNotContains'; // @codeCoverageIgnore 158 | 159 | $this->$method( 160 | $contents, 161 | $this->getInnerHtmlOfMatchedElements($markup, $selector), 162 | $message 163 | ); 164 | } 165 | 166 | /** 167 | * Assert an element's contents contain the given regular expression pattern. 168 | * 169 | * @since 1.1.0 170 | * 171 | * @param string $regexp The regular expression pattern to look for within the DOM node. 172 | * @param string $selector A query selector for the element to find. 173 | * @param string $markup The output that should contain the $selector. 174 | * @param string $message A message to display if the assertion fails. 175 | * 176 | * @return void 177 | */ 178 | public function assertElementRegExp($regexp, $selector = '', $markup = '', $message = '') 179 | { 180 | $method = method_exists($this, 'assertMatchesRegularExpression') 181 | ? 'assertMatchesRegularExpression' 182 | : 'assertRegExp'; // @codeCoverageIgnore 183 | 184 | $this->$method( 185 | $regexp, 186 | $this->getInnerHtmlOfMatchedElements($markup, $selector), 187 | $message 188 | ); 189 | } 190 | 191 | /** 192 | * Assert an element's contents do not contain the given regular expression pattern. 193 | * 194 | * @since 1.1.0 195 | * 196 | * @param string $regexp The regular expression pattern to look for within the DOM node. 197 | * @param string $selector A query selector for the element to find. 198 | * @param string $markup The output that should not contain the $selector. 199 | * @param string $message A message to display if the assertion fails. 200 | * 201 | * @return void 202 | */ 203 | public function assertElementNotRegExp($regexp, $selector = '', $markup = '', $message = '') 204 | { 205 | $method = method_exists($this, 'assertDoesNotMatchRegularExpression') 206 | ? 'assertDoesNotMatchRegularExpression' 207 | : 'assertNotRegExp'; // @codeCoverageIgnore 208 | 209 | $this->$method( 210 | $regexp, 211 | $this->getInnerHtmlOfMatchedElements($markup, $selector), 212 | $message 213 | ); 214 | } 215 | 216 | /** 217 | * Build a new DOMDocument from the given markup, then execute a query against it. 218 | * 219 | * @since 1.0.0 220 | * 221 | * @param string $markup The HTML for the DOMDocument. 222 | * @param string $query The DOM selector query. 223 | * 224 | * @return Crawler 225 | */ 226 | private function executeDomQuery($markup, $query) 227 | { 228 | $dom = new Crawler($markup); 229 | 230 | return $dom->filter($query); 231 | } 232 | 233 | /** 234 | * Given an array of HTML attributes, flatten them into a XPath attribute selector. 235 | * 236 | * @since 1.0.0 237 | * 238 | * @throws RiskyTestError When the $attributes array is empty. 239 | * 240 | * @param array $attributes HTML attributes and their values. 241 | * 242 | * @return string A XPath attribute query selector. 243 | */ 244 | private function flattenAttributeArray(array $attributes) 245 | { 246 | if (empty($attributes)) { 247 | throw new RiskyTestError('Attributes array is empty.'); 248 | } 249 | 250 | array_walk($attributes, function (&$value, $key) { 251 | // Boolean attributes. 252 | if (null === $value) { 253 | $value = sprintf('[%s]', $key); 254 | } else { 255 | $value = sprintf('[%s="%s"]', $key, htmlspecialchars($value)); 256 | } 257 | }); 258 | 259 | return implode('', $attributes); 260 | } 261 | 262 | /** 263 | * Given HTML markup and a DOM selector query, collect the innerHTML of the matched selectors. 264 | * 265 | * @since 1.1.0 266 | * 267 | * @param string $markup The HTML for the DOMDocument. 268 | * @param string $query The DOM selector query. 269 | * 270 | * @return string The concatenated innerHTML of any matched selectors. 271 | */ 272 | private function getInnerHtmlOfMatchedElements($markup, $query) 273 | { 274 | $results = $this->executeDomQuery($markup, $query); 275 | $contents = []; 276 | 277 | // Loop through results and collect their innerHTML values. 278 | foreach ($results as $result) { 279 | $document = new \DOMDocument(); 280 | $document->appendChild($document->importNode($result->firstChild, true)); 281 | 282 | $contents[] = trim(html_entity_decode($document->saveHTML())); 283 | } 284 | 285 | return implode(PHP_EOL, $contents); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /tests/MarkupAssertionsTraitTest.php: -------------------------------------------------------------------------------- 1 | assertContainsSelector( 25 | $selector, 26 | 'Example' 27 | ); 28 | } 29 | 30 | /** 31 | * @test 32 | * @testdox assertContainsSelector() should pick up multiple instances of a selector 33 | */ 34 | public function assertContainsSelector_should_pick_up_multiple_instances() 35 | { 36 | $this->assertContainsSelector( 37 | 'a', 38 | 'Home | About | Contact' 39 | ); 40 | } 41 | 42 | /** 43 | * @test 44 | * @testdox assertNotContainsSelector() should verify that the given selector does not exist 45 | * @dataProvider provideSelectorVariants 46 | */ 47 | public function assertNotContainsSelector_should_verify_that_the_given_selector_does_not_exist($selector) 48 | { 49 | $this->assertNotContainsSelector( 50 | $selector, 51 | '

This element has little to do with the link.

' 52 | ); 53 | } 54 | 55 | /** 56 | * @test 57 | * @testdox assertSelectorCount() should count the instances of a selector 58 | */ 59 | public function assertSelectorCount_should_count_the_number_of_instances() 60 | { 61 | $this->assertSelectorCount( 62 | 3, 63 | 'li', 64 | '
  • 1
  • 2
  • 3
' 65 | ); 66 | } 67 | 68 | /** 69 | * @test 70 | * @testdox assertHasElementWithAttributes() should find an element with the given attributes 71 | */ 72 | public function assertHasElementWithAttributes_should_find_elements_with_matching_attributes() 73 | { 74 | $this->assertHasElementWithAttributes( 75 | [ 76 | 'type' => 'email', 77 | 'value' => 'test@example.com', 78 | ], 79 | '
' 80 | ); 81 | } 82 | 83 | /** 84 | * @test 85 | * @testdox assertHasElementWithAttributes() should be able to parse spaces in attribute values 86 | * @ticket https://github.com/stevegrunwell/phpunit-markup-assertions/issues/13 87 | */ 88 | public function assertHasElementWithAttributes_should_be_able_to_handle_spaces() 89 | { 90 | $this->assertHasElementWithAttributes( 91 | [ 92 | 'data-attr' => 'foo bar baz', 93 | ], 94 | '
Contents
' 95 | ); 96 | } 97 | 98 | /** 99 | * @test 100 | * @testdox assertNotHasElementWithAttributes() should ensure no element has the provided attributes 101 | */ 102 | public function assertNotHasElementWithAttributes_should_find_no_elements_with_matching_attributes() 103 | { 104 | $this->assertNotHasElementWithAttributes( 105 | [ 106 | 'type' => 'email', 107 | 'value' => 'test@example.com', 108 | ], 109 | '
' 110 | ); 111 | } 112 | 113 | /** 114 | * @test 115 | * @testdox assertElementContains() should be able to search for a selector 116 | */ 117 | public function assertElementContains_can_match_a_selector() 118 | { 119 | $this->assertElementContains( 120 | 'ipsum', 121 | '#main', 122 | '
Lorem ipsum
Lorem ipsum
' 123 | ); 124 | } 125 | 126 | /** 127 | * @test 128 | * @testdox assertElementContains() should be able to chain multiple selectors 129 | */ 130 | public function assertElementContains_can_chain_multiple_selectors() 131 | { 132 | $this->assertElementContains( 133 | 'ipsum', 134 | '#main .foo', 135 | '
Lorem ipsum
' 136 | ); 137 | } 138 | 139 | /** 140 | * @test 141 | * @testdox assertElementContains() should scope text to the selected element 142 | */ 143 | public function assertElementContains_should_scope_matches_to_selector() 144 | { 145 | $this->expectException(AssertionFailedError::class); 146 | $this->expectExceptionMessage('The #main div does not contain the string "ipsum".'); 147 | 148 | $this->assertElementContains( 149 | 'ipsum', 150 | '#main', 151 | '
Lorem ipsum
Foo bar baz
', 152 | 'The #main div does not contain the string "ipsum".' 153 | ); 154 | } 155 | 156 | /** 157 | * @test 158 | * @testdox assertElementContains() should handle various character sets 159 | * @dataProvider provideGreetingsInDifferentLanguages 160 | * @ticket https://github.com/stevegrunwell/phpunit-markup-assertions/issues/31 161 | */ 162 | public function assertElementContains_should_handle_various_character_sets($greeting) 163 | { 164 | $this->assertElementContains( 165 | $greeting, 166 | 'h1', 167 | sprintf('

%s

', $greeting) 168 | ); 169 | } 170 | 171 | /** 172 | * @test 173 | * @testdox assertElementNotContains() should be able to search for a selector 174 | */ 175 | public function assertElementNotContains_can_match_a_selector() 176 | { 177 | $this->assertElementNotContains( 178 | 'ipsum', 179 | '#main', 180 | '
Foo bar baz
Some string
' 181 | ); 182 | } 183 | 184 | /** 185 | * @test 186 | * @testdox assertElementNotContains() should handle various character sets 187 | * @dataProvider provideGreetingsInDifferentLanguages 188 | * @ticket https://github.com/stevegrunwell/phpunit-markup-assertions/issues/31 189 | */ 190 | public function assertElementNotContains_should_handle_various_character_sets($greeting) 191 | { 192 | $this->assertElementNotContains( 193 | $greeting, 194 | 'h1', 195 | sprintf('

Translation

%s

', $greeting) 196 | ); 197 | } 198 | 199 | /** 200 | * @test 201 | * @testdox assertElementRegExp() should use regular expression matching 202 | */ 203 | public function assertElementRegExp_should_use_regular_expression_matching() 204 | { 205 | $this->assertElementRegExp( 206 | '/[A-Z0-9-]+/', 207 | '#main', 208 | '
Lorem ipsum
ABC123
' 209 | ); 210 | } 211 | 212 | /** 213 | * @test 214 | * @testdox assertElementRegExp() should be able to search for nested contents 215 | */ 216 | public function assertElementRegExp_should_be_able_to_match_nested_contents() 217 | { 218 | $this->assertElementRegExp( 219 | '/[A-Z]+/', 220 | '#main', 221 | '
Lorem ipsum
ABC
' 222 | ); 223 | } 224 | 225 | /** 226 | * @test 227 | * @testdox assertElementNotRegExp() should use regular expression matching 228 | */ 229 | public function testAssertElementNotRegExp() 230 | { 231 | $this->assertElementNotRegExp( 232 | '/[0-9-]+/', 233 | '#main', 234 | '
Foo bar baz
ABC
' 235 | ); 236 | } 237 | 238 | 239 | /** 240 | * @test 241 | * @testdox flattenAttributeArray() should flatten an array of attributes 242 | * @dataProvider provideAttributes 243 | */ 244 | public function flattenArrayAttribute_should_flatten_arrays_of_attributes($attributes, $expected) 245 | { 246 | $method = new \ReflectionMethod($this, 'flattenAttributeArray'); 247 | $method->setAccessible(true); 248 | 249 | $this->assertSame($expected, $method->invoke($this, $attributes)); 250 | } 251 | 252 | /** 253 | * @test 254 | * @testdox flattenAttributeArray() should throw a RiskyTestError if the array is empty 255 | * @dataProvider provideAttributes 256 | */ 257 | public function flattenAttributeArray_should_throw_a_RiskyTestError_if_given_an_empty_array() 258 | { 259 | $this->expectException(RiskyTestError::class); 260 | 261 | $method = new \ReflectionMethod($this, 'flattenAttributeArray'); 262 | $method->setAccessible(true); 263 | $method->invoke($this, []); 264 | } 265 | 266 | /** 267 | * @test 268 | * @testdox getInnerHtmlOfMatchedElements() should retrieve the inner HTML 269 | * @dataProvider provideInnerHtml 270 | */ 271 | public function getInnerHtmlOfMatchedElements_should_retrieve_the_inner_HTML($markup, $selector, $expected) 272 | { 273 | $method = new \ReflectionMethod($this, 'getInnerHtmlOfMatchedElements'); 274 | $method->setAccessible(true); 275 | 276 | $this->assertEquals($expected, $method->invoke($this, $markup, $selector)); 277 | } 278 | 279 | /** 280 | * Data provider for testFlattenAttributeArray(). 281 | */ 282 | public function provideAttributes() 283 | { 284 | return [ 285 | 'Single attribute' => [ 286 | [ 287 | 'id' => 'first-name', 288 | ], 289 | '[id="first-name"]', 290 | ], 291 | 'Multiple attributes' => [ 292 | [ 293 | 'id' => 'first-name', 294 | 'value' => 'Ringo', 295 | ], 296 | '[id="first-name"][value="Ringo"]', 297 | ], 298 | 'Boolean attribute' => [ 299 | [ 300 | 'checked' => null, 301 | ], 302 | '[checked]', 303 | ], 304 | 'Data attribute' => [ 305 | [ 306 | 'data-foo' => 'bar', 307 | ], 308 | '[data-foo="bar"]', 309 | ], 310 | 'Value contains quotes' => [ 311 | [ 312 | 'name' => 'Austin "Danger" Powers', 313 | ], 314 | '[name="Austin "Danger" Powers"]', 315 | ], 316 | ]; 317 | } 318 | 319 | /** 320 | * Data provider for testGetInnerHtmlOfMatchedElements(). 321 | * 322 | * @return array> 323 | */ 324 | public function provideInnerHtml() 325 | { 326 | return [ 327 | 'A single match' => [ 328 | 'Foo bar baz', 329 | 'body', 330 | 'Foo bar baz', 331 | ], 332 | 'Multiple matching elements' => [ 333 | '
  • Foo
  • Bar
  • Baz
  • ', 334 | 'li', 335 | 'Foo' . PHP_EOL . 'Bar' . PHP_EOL . 'Baz', 336 | ], 337 | 'Nested elements' => [ 338 | '

    Example site

    ', 339 | 'h1', 340 | 'Example site', 341 | ], 342 | ]; 343 | } 344 | 345 | /** 346 | * Data provider for testAssertContainsSelector(). 347 | * 348 | * @return array> 349 | */ 350 | public function provideSelectorVariants() 351 | { 352 | return [ 353 | 'Simple tag name' => ['a'], 354 | 'Class name' => ['.link'], 355 | 'Multiple class names' => ['.link.another-class'], 356 | 'Element ID' => ['#my-link'], 357 | 'Tag name with class' => ['a.link'], 358 | 'Tag name with ID' => ['a#my-link'], 359 | 'Tag with href attribute' => ['a[href="https://example.com"]'], 360 | ]; 361 | } 362 | 363 | /** 364 | * Provide a list of strings in various language. 365 | * 366 | * @return array> 367 | */ 368 | public function provideGreetingsInDifferentLanguages() 369 | { 370 | return [ 371 | 'Arabic' => ['مرحبا!'], 372 | 'Chinese' => ['你好'], 373 | 'English' => ['Hello'], 374 | 'Hebrew' => ['שלום'], 375 | 'Japanese' => ['こんにちは'], 376 | 'Korean' => ['안녕하십니까'], 377 | 'Punjabi' => ['ਸਤ ਸ੍ਰੀ ਅਕਾਲ'], 378 | 'Ukrainian' => ['Привіт'], 379 | ]; 380 | } 381 | } 382 | --------------------------------------------------------------------------------