├── LICENSE ├── README.md ├── composer.json └── src └── DOMAssert.php /LICENSE: -------------------------------------------------------------------------------- 1 | PHPUnit 2 | 3 | Copyright (c) 2014-2015, Sebastian Bergmann . 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in 15 | the documentation and/or other materials provided with the 16 | distribution. 17 | 18 | * Neither the name of Sebastian Bergmann nor the names of his 19 | contributors may be used to endorse or promote products derived 20 | from this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHPUnit DOM Assertions 2 | 3 | [![Latest Stable Version](https://img.shields.io/packagist/v/phpunit/phpunit-dom-assertions.svg)](https://packagist.org/packages/phpunit/phpunit-dom-assertions) 4 | [![Downloads](https://img.shields.io/packagist/dt/phpunit/phpunit-dom-assertions.svg)](https://packagist.org/packages/phpunit/phpunit-dom-assertions) 5 | [![Integrate](https://github.com/lstrojny/phpunit-dom-assertions/workflows/CI/badge.svg?branch=master)](https://github.com/lstrojny/phpunit-dom-assertions/actions) 6 | 7 | A work in progress, drop-in replacement for the following deprecated PHPUnit assertions: 8 | 9 | * `assertSelectCount()` 10 | * `assertSelectRegExp()` 11 | * `assertXPathCount()` 12 | * `assertXPathEquals()` 13 | * `assertXPathSelectRegExp()` 14 | * `assertSelectEquals()` 15 | 16 | ## Installation 17 | 18 | ```console 19 | $ composer require --dev phpunit/phpunit-dom-assertions 20 | ``` 21 | 22 | ## Usage 23 | 24 | Extend `PHPUnit\Framework\DOMTestCase` to use the DOM assertions: 25 | 26 | ```php 27 | namespace My\Tests; 28 | 29 | use PHPUnit\Framework\DOMAssert; 30 | use PHPUnit\Framework\TestCase; 31 | 32 | final class DOMTest extends TestCase 33 | { 34 | public function testSelectEquals(): void 35 | { 36 | $html = file_get_contents('test.html'); 37 | $selector = 'span.test_class'; 38 | $content = 'Test Class Text'; 39 | 40 | DOMAssert::assertSelectEquals($selector, $content, true, $html); 41 | } 42 | } 43 | ``` 44 | 45 | ## License 46 | 47 | The PHPUnit DOM assertions library is licensed under the [BSD 3-Clause license](LICENSE). 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpunit/phpunit-dom-assertions", 3 | "description": "DOM assertions for PHPUnit", 4 | "license": "MIT", 5 | "keywords": [ 6 | "dom", 7 | "xpath", 8 | "css", 9 | "assertions", 10 | "phpunit", 11 | "tests" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "Sebastian Bergmann", 16 | "email": "sebastian@phpunit.de" 17 | }, 18 | { 19 | "name": "Jeff Welch", 20 | "email": "whatthejeff@gmail.com" 21 | }, 22 | { 23 | "name": "Lars Strojny", 24 | "email": "lars@strojny.net" 25 | } 26 | ], 27 | "homepage": "https://github.com/lstrojny/phpunit-dom-assertions", 28 | "require": { 29 | "php": "~8.3.0 || ~8.4.0", 30 | "ext-dom": "*", 31 | "phpunit/phpunit": "^10.5.35 || ^11.3.6 || ^12.0.0", 32 | "symfony/css-selector": "^6.4.8 || ^7.1.1", 33 | "symfony/dom-crawler": "^6.4.12 || ^7.1.5" 34 | }, 35 | "require-dev": { 36 | "friendsofphp/php-cs-fixer": "^3.64.0", 37 | "phpstan/phpstan": "^1.12.4", 38 | "phpstan/phpstan-deprecation-rules": "^1.2.1", 39 | "phpstan/phpstan-phpunit": "^1.4.0" 40 | }, 41 | "autoload": { 42 | "classmap": [ 43 | "src/" 44 | ] 45 | }, 46 | "autoload-dev": { 47 | "classmap": [ 48 | "tests/" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DOMAssert.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace PHPUnit\Framework; 14 | 15 | use Symfony\Component\DomCrawler\Crawler; 16 | 17 | /** 18 | * @author Sebastian Bergmann 19 | * @author Jeff Welch 20 | * @copyright Sebastian Bergmann 21 | * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License 22 | */ 23 | final class DOMAssert 24 | { 25 | /** 26 | * Assert the presence, absence, or count of elements in a document matching 27 | * the CSS $selector, regardless of the contents of those elements. 28 | * 29 | * The first argument, $selector, is the CSS selector used to match 30 | * the elements in the $actual document. 31 | * 32 | * The second argument, $count, can be either boolean or numeric. 33 | * When boolean, it asserts for presence of elements matching the selector 34 | * (true) or absence of elements (false). 35 | * When numeric, it asserts the count of elements. 36 | * 37 | * assertSelectCount("#binder", true, $xml); // any? 38 | * assertSelectCount(".binder", 3, $xml); // exactly 3? 39 | * 40 | * @param array{"<"?: int, ">"?: int, "<="?: int, ">="?: int}|bool|int $count 41 | * 42 | * @throws Exception 43 | */ 44 | public static function assertSelectCount( 45 | string $selector, 46 | array|bool|int $count, 47 | \DOMDocument|string $actual, 48 | string $message = '', 49 | bool $isHtml = true 50 | ): void { 51 | self::assertSelectEquals( 52 | $selector, 53 | null, 54 | $count, 55 | $actual, 56 | $message, 57 | $isHtml 58 | ); 59 | } 60 | 61 | /** 62 | * assertSelectRegExp("#binder .name", "/Mike|Derek/", true, $xml); // any? 63 | * assertSelectRegExp("#binder .name", "/Mike|Derek/", 3, $xml); // 3? 64 | * 65 | * @param array{"<"?: int, ">"?: int, "<="?: int, ">="?: int}|bool|int $count 66 | * 67 | * @throws Exception 68 | */ 69 | public static function assertSelectRegExp( 70 | string $selector, 71 | string $pattern, 72 | array|bool|int $count, 73 | \DOMDocument|string $actual, 74 | string $message = '', 75 | bool $isHtml = true 76 | ): void { 77 | self::assertSelectEquals( 78 | $selector, 79 | "regexp:{$pattern}", 80 | $count, 81 | $actual, 82 | $message, 83 | $isHtml 84 | ); 85 | } 86 | 87 | /** 88 | * @param array{"<"?: int, ">"?: int, "<="?: int, ">="?: int}|bool|int $count 89 | * 90 | * @throws Exception 91 | */ 92 | public static function assertXPathCount( 93 | string $selector, 94 | array|bool|int $count, 95 | \DOMDocument|string $actual, 96 | string $message = '', 97 | bool $isHtml = true 98 | ): void { 99 | self::assertSelectEquals( 100 | $selector, 101 | null, 102 | $count, 103 | $actual, 104 | $message, 105 | $isHtml, 106 | true 107 | ); 108 | } 109 | 110 | /** 111 | * @param array{"<"?: int, ">"?: int, "<="?: int, ">="?: int}|bool|int $count 112 | * 113 | * @throws Exception 114 | */ 115 | public static function assertXPathEquals( 116 | string $selector, 117 | string $content, 118 | array|bool|int $count, 119 | \DOMDocument|string $actual, 120 | string $message = '', 121 | bool $isHtml = true 122 | ): void { 123 | self::assertSelectEquals( 124 | $selector, 125 | $content, 126 | $count, 127 | $actual, 128 | $message, 129 | $isHtml, 130 | true 131 | ); 132 | } 133 | 134 | /** 135 | * @param array{"<"?: int, ">"?: int, "<="?: int, ">="?: int}|bool|int $count 136 | * 137 | * @throws Exception 138 | */ 139 | public static function assertXPathSelectRegExp( 140 | string $xpath, 141 | string $pattern, 142 | array|bool|int $count, 143 | \DOMDocument|string $actual, 144 | string $message = '', 145 | bool $isHtml = true 146 | ): void { 147 | self::assertSelectEquals( 148 | $xpath, 149 | "regexp:{$pattern}", 150 | $count, 151 | $actual, 152 | $message, 153 | $isHtml, 154 | true 155 | ); 156 | } 157 | 158 | /** 159 | * assertSelectEquals("#binder .name", "Chuck", true, $xml); // any? 160 | * assertSelectEquals("#binder .name", "Chuck", false, $xml); // none? 161 | * 162 | * @param array{"<"?: int, ">"?: int, "<="?: int, ">="?: int}|bool|int $count 163 | * 164 | * @throws Exception 165 | */ 166 | public static function assertSelectEquals( 167 | string $selector, 168 | ?string $content, 169 | array|bool|int $count, 170 | \DOMDocument|string $actual, 171 | string $message = '', 172 | bool $isHtml = true, 173 | bool $isXPath = false 174 | ): void { 175 | $crawler = new Crawler(); 176 | 177 | if ($actual instanceof \DOMDocument) { 178 | $crawler->addDocument($actual); 179 | } elseif ($isHtml) { 180 | $crawler->addHtmlContent($actual); 181 | } else { 182 | $crawler->addXmlContent($actual); 183 | } 184 | 185 | if (true === $isXPath) { 186 | $crawler = $crawler->filterXPath($selector); 187 | } else { 188 | $crawler = $crawler->filter($selector); 189 | } 190 | 191 | if (\is_string($content)) { 192 | $crawler = $crawler->reduce(static function (Crawler $node) use ($content) { 193 | $text = $node->text(null, true); 194 | 195 | if ('' === $content) { 196 | return '' === $text; 197 | } 198 | 199 | if (preg_match('/^regexp\s*:\s*(.*)/i', $content, $matches)) { 200 | return (bool) preg_match($matches[1], $text); 201 | } 202 | 203 | return str_contains($text, $content); 204 | }); 205 | } 206 | 207 | $found = \count($crawler); 208 | 209 | if (is_numeric($count)) { 210 | Assert::assertSame($count, $found, $message); 211 | } elseif (\is_bool($count)) { 212 | if ($count) { 213 | Assert::assertGreaterThan(0, $found, $message); 214 | } else { 215 | Assert::assertSame(0, $found, $message); 216 | } 217 | } elseif (\is_array($count) 218 | && (isset($count['>']) || isset($count['<']) 219 | || isset($count['>=']) || isset($count['<=']))) { 220 | if (isset($count['>'])) { 221 | Assert::assertGreaterThan($count['>'], $found, $message); 222 | } 223 | 224 | if (isset($count['>='])) { 225 | Assert::assertGreaterThanOrEqual($count['>='], $found, $message); 226 | } 227 | 228 | if (isset($count['<'])) { 229 | Assert::assertLessThan($count['<'], $found, $message); 230 | } 231 | 232 | if (isset($count['<='])) { 233 | Assert::assertLessThanOrEqual($count['<='], $found, $message); 234 | } 235 | } else { 236 | throw new Exception('Invalid count format'); 237 | } 238 | } 239 | } 240 | --------------------------------------------------------------------------------