diff --git a/src/CssXpath.php b/src/CssXpath.php index 04c8994..404e7e2 100644 --- a/src/CssXpath.php +++ b/src/CssXpath.php @@ -49,7 +49,7 @@ public static function cssToXpath($selector) } $xpath = self::processRegexs($selector); - $xpath = \preg_match('/^\/\//', $xpath) + $xpath = \preg_match('/^\//', $xpath) ? $xpath : '//' . $xpath; $xpath = \preg_replace('#/{4}#', '', $xpath); @@ -144,6 +144,10 @@ private static function regexs() self::$strings[] = ($matches[1] ? '*' : '') . '[not(' . $xpathNot . ')]'; return '[{' . (\count(self::$strings) - 1) . '}]'; }), + array('/([\s]?):has\((.*?)\)/', static function ($matches) { + self::$strings[] = '[count(' . self::cssToXpath($matches[2]) . ') > 0]'; + return '[{' . (\count(self::$strings) - 1) . '}]'; + }), // All blocks of 2 or more spaces array('/\s{2,}/', static function () { return ' '; @@ -237,23 +241,6 @@ static function ($matches) { array('/:scope/', static function () { return '//'; }), - // The Relational Pseudo-class: :has() - // https://www.w3.org/TR/selectors-4/#has-pseudo - // E! : https://www.w3.org/TR/selectors4/ - array('/^.+!.+$/', static function ($matches) { - $subSelectors = \explode(',', $matches[0]); - foreach ($subSelectors as $i => $subSelector) { - $parts = \explode('!', $subSelector); - $subSelector = \array_shift($parts); - if (\preg_match_all('/((?:[^\/]*\/?\/?)|$)/', $parts[0], $matches)) { - $results = $matches[0]; - $results[] = \str_repeat('/..', \count($results) - 2); - $subSelector .= \implode('', $results); - } - $subSelectors[$i] = $subSelector; - } - return \implode(',', $subSelectors); - }), // Restore strings array('/\[\{(\d+)\}\]/', static function ($matches) { return self::$strings[$matches[1]]; diff --git a/src/DOMAssertionTrait.php b/src/DOMAssertionTrait.php new file mode 100644 index 0000000..3f43529 --- /dev/null +++ b/src/DOMAssertionTrait.php @@ -0,0 +1,146 @@ + + * @license http://opensource.org/licenses/MIT MIT + * @copyright 2018 Brad Kent + * @version 1.0 + * + * @link http://www.github.com/bkdotcom/CssXpath + */ + +namespace bdk\CssXpath; + +/** + * PHPUnit DOM Assertions. + * + * These assertions were provide with PHPUnit 3.3 - < 5 + */ +trait DOMAssertionTrait +{ + /** + * Assert the presence, absence, or count of elements in a document matching + * the CSS $selector, regardless of the contents of those elements. + * + * The first argument, $selector, is the CSS selector used to match + * the elements in the $actual document. + * + * The second argument, $count, can be either boolean or numeric. + * When boolean, it asserts for presence of elements matching the selector + * (true) or absence of elements (false). + * When numeric, it assertsk the count of elements. + * + * examples: + * assertSelectCount("#binder", true, $xml); // any? + * assertSelectCount(".binder", 3, $xml); // exactly 3? + * + * @param array $selector CSS selector + * @param int|bool|array $count bool, count, or array('>'=5, <=10) + * @param mixed $actual HTML + * @param string $message exception message + * @param bool $isHtml not used + * + * @return void + * + * @link(https://phpunit.de/manual/3.7/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.assertions.assertSelectCount + */ + public static function assertSelectCount($selector, $count, $actual, $message = '', $isHtml = true) + { + self::assertSelectEquals($selector, true, $count, $actual, $message, $isHtml); + } + + /** + * Reports an error identified by $message + * if the CSS selector $selector does not match $count elements + * in the DOMNode $actual with the value $content. + * + * $count can be one of the following types: + * bool: Asserts for presence of elements matching the selector (TRUE) or absence of elements (FALSE). + * int: Asserts the count of elements. + * array: Asserts that the count is in a range specified by using <, >, <=, and >= as keys. + * + * Examples + * assertSelectEquals("#binder .name", "Chuck", true, $xml); // any? + * assertSelectEquals("#binder .name", "Chuck", false, $xml); // none? + * + * @param string $selector css selector + * @param string $content content to match against. may specify regex as regexp:/regexp/ + * @param int|bool|array $count bool, integer, or array('>' => 5, '<=' => 10) + * @param mixed $actual html or domdocument + * @param string $message exception message + * @param bool $isHtml not used + * + * @return void + * @throws \PHPUnit\Framework\Exception Invalid count format. + * + * @link https://phpunit.de/manual/3.7/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.assertions.assertSelectEquals + */ + public static function assertSelectEquals($selector, $content, $count, $actual, $message = '', $isHtml = true) + { + $found = CssSelect::select($actual, $selector); + if (\is_string($content)) { + foreach ($found as $k => $node) { + $keep = $content === '' + ? $node['innerHTML'] === '' + : \strstr($node['innerHTML'], $content) !== false; + if (\preg_match('/^regexp\s*:\s*(.*)/i', $content, $matches)) { + $keep = (bool) \preg_match($matches[1], $node['innerHTML']); + } + if (!$keep) { + unset($found[$k]); + } + } + } + $countFound = \count($found); + if (\is_numeric($count)) { + self::assertEquals($count, $countFound, $message); + return; + } + if (\is_bool($count)) { + $isFound = \count($found) > 0; + $count + ? self::assertTrue($isFound, $message) + : self::assertFalse($isFound, $message); + return; + } + if (\is_array($count) && \array_intersect_key($count, \array_flip(array('>', '<', '>=', '<=')))) { + if (isset($count['>'])) { + self::assertTrue($countFound > $count['>'], $message); + } + if (isset($count['>='])) { + self::assertTrue($countFound >= $count['>='], $message); + } + if (isset($count['<'])) { + self::assertTrue($countFound < $count['<'], $message); + } + if (isset($count['<='])) { + self::assertTrue($countFound <= $count['<='], $message); + } + return; + } + throw new \PHPUnit\Framework\Exception('Invalid count format'); + } + + /** + * examples: + * assertSelectRegExp("#binder .name", "/Mike|Derek/", true, $xml); // any? + * assertSelectRegExp("#binder .name", "/Mike|Derek/", 3, $xml); // 3? + * + * @param array $selector CSS selector + * @param string $pattern regex + * @param int|bool|array $count bool, count, or array('>'=5, <=10) + * @param mixed $actual HTML or domdocument + * @param string $message exception message + * @param bool $isHtml not used + * + * @return void + * + * @link( https://phpunit.de/manual/3.7/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.assertions.assertSelectRegExp, link) + */ + public static function assertSelectRegExp($selector, $pattern, $count, $actual, $message = '', $isHtml = true) + { + self::assertSelectEquals($selector, 'regexp:' . $pattern, $count, $actual, $message, $isHtml); + } +} diff --git a/src/DOMTestCase.php b/src/DOMTestCase.php index 3e3cdd6..8d84a2e 100644 --- a/src/DOMTestCase.php +++ b/src/DOMTestCase.php @@ -13,134 +13,12 @@ namespace bdk\CssXpath; +use PHPUnit\Framework\TestCase; + /** - * PHPUnit DOM Assertions. - * - * These assertions were provide with PHPUnit 3.3 - < 5 + * TestCase with DOM Assertions. */ -abstract class DOMTestCase extends \PHPUnit\Framework\TestCase +abstract class DOMTestCase extends TestCase { - /** - * Assert the presence, absence, or count of elements in a document matching - * the CSS $selector, regardless of the contents of those elements. - * - * The first argument, $selector, is the CSS selector used to match - * the elements in the $actual document. - * - * The second argument, $count, can be either boolean or numeric. - * When boolean, it asserts for presence of elements matching the selector - * (true) or absence of elements (false). - * When numeric, it assertsk the count of elements. - * - * examples: - * assertSelectCount("#binder", true, $xml); // any? - * assertSelectCount(".binder", 3, $xml); // exactly 3? - * - * @param array $selector CSS selector - * @param integer|boolean|array $count bool, count, or array('>'=5, <=10) - * @param mixed $actual HTML - * @param string $message exception message - * @param boolean $isHtml not used - * - * @return void - * - * @link(https://phpunit.de/manual/3.7/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.assertions.assertSelectCount - */ - public static function assertSelectCount($selector, $count, $actual, $message = '', $isHtml = true) - { - self::assertSelectEquals($selector, true, $count, $actual, $message, $isHtml); - } - - /** - * Reports an error identified by $message - * if the CSS selector $selector does not match $count elements - * in the DOMNode $actual with the value $content. - * - * $count can be one of the following types: - * boolean: Asserts for presence of elements matching the selector (TRUE) or absence of elements (FALSE). - * integer: Asserts the count of elements. - * array: Asserts that the count is in a range specified by using <, >, <=, and >= as keys. - * - * Examples - * assertSelectEquals("#binder .name", "Chuck", true, $xml); // any? - * assertSelectEquals("#binder .name", "Chuck", false, $xml); // none? - * - * @param string $selector css selector - * @param string $content content to match against. may specify regex as regexp:/regexp/ - * @param integer|boolean|array $count bool, integer, or array('>' => 5, '<=' => 10) - * @param mixed $actual html or domdocument - * @param string $message exception message - * @param boolean $isHtml not used - * - * @return void - * @throws \PHPUnit\Framework\Exception Invalid count format. - * - * @link https://phpunit.de/manual/3.7/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.assertions.assertSelectEquals - */ - public static function assertSelectEquals($selector, $content, $count, $actual, $message = '', $isHtml = true) - { - $found = CssSelect::select($actual, $selector); - if (is_string($content)) { - foreach ($found as $k => $node) { - $keep = true; - if ($content === '') { - $keep = $node['innerHTML'] === ''; - } elseif (preg_match('/^regexp\s*:\s*(.*)/i', $content, $matches)) { - $keep = (bool) preg_match($matches[1], $node['innerHTML']); - } else { - $keep = strstr($node['innerHTML'], $content) !== false; - } - if (!$keep) { - unset($found[$k]); - } - } - } - $countFound = count($found); - if (is_numeric($count)) { - self::assertEquals($count, $countFound, $message); - } elseif (is_bool($count)) { - $isFound = $found > 0; - if ($count) { - self::assertTrue($isFound, $message); - } else { - self::assertFalse($isFound, $message); - } - } elseif (is_array($count) && array_intersect_key($count, array_flip(array('>','<','>=','<=')))) { - if (isset($count['>'])) { - self::assertTrue($countFound > $count['>'], $message); - } - if (isset($count['>='])) { - self::assertTrue($countFound >= $count['>='], $message); - } - if (isset($count['<'])) { - self::assertTrue($countFound < $count['<'], $message); - } - if (isset($count['<='])) { - self::assertTrue($countFound <= $count['<='], $message); - } - } else { - throw new \PHPUnit\Framework\Exception('Invalid count format'); - } - } - - /** - * examples: - * assertSelectRegExp("#binder .name", "/Mike|Derek/", true, $xml); // any? - * assertSelectRegExp("#binder .name", "/Mike|Derek/", 3, $xml); // 3? - * - * @param array $selector CSS selector - * @param string $pattern regex - * @param integer|boolean|array $count bool, count, or array('>'=5, <=10) - * @param mixed $actual HTML or domdocument - * @param string $message exception message - * @param boolean $isHtml not used - * - * @return void - * - * @link( https://phpunit.de/manual/3.7/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.assertions.assertSelectRegExp, link) - */ - public static function assertSelectRegExp($selector, $pattern, $count, $actual, $message = '', $isHtml = true) - { - self::assertSelectEquals($selector, "regexp:$pattern", $count, $actual, $message, $isHtml); - } + use DOMAssertionTrait; } diff --git a/tests/CssXpath/CssSelectTest.php b/tests/CssXpath/CssSelectTest.php index dc88b6d..1919a54 100644 --- a/tests/CssXpath/CssSelectTest.php +++ b/tests/CssXpath/CssSelectTest.php @@ -57,7 +57,7 @@ public function selectProvider() array('li:nth-last-child(3)', 1), array('li:nth-last-child(4)', 1), array('li:nth-last-child(6)', 0), - array('ul li! > a', 1), + // array('ul li! > a', 1), array(':scope ul li > a', 1), array('.a', 2), array('#article', 1), @@ -66,7 +66,7 @@ public function selectProvider() array('ul > li:last-child [href]', 1), array('[class~=large] li[class~=a]', 2), array('li[class]:not(.bar)', 1), - array(':header', 1), + array('div > :header', 1), array('.bar.a', 1), array('bo $ us', 0), ); @@ -99,7 +99,7 @@ public function testSelectStatic($selector, $count, $inner = null) hi HTML; $found = CssSelect::select($html, $selector); - self::assertSame($count, \count($found)); + self::assertSame($count, \count($found), $selector); if ($inner !== null) { self::assertSame($inner, $found[0]['innerHTML']); diff --git a/tests/CssXpath/CssXpathTest.php b/tests/CssXpath/CssXpathTest.php index b1c0639..e195050 100644 --- a/tests/CssXpath/CssXpathTest.php +++ b/tests/CssXpath/CssXpathTest.php @@ -45,9 +45,6 @@ public function cssToXpathProvider() array('foo[id=bar]', '//foo[@id="bar"]'), array('[style=color: red; border: 1px solid black;]', '//*[@style="color: red; border: 1px solid black;"]'), array('foo[style=color: red; border: 1px solid black;]', '//foo[@style="color: red; border: 1px solid black;"]'), - // array(':button', '//input[@type="button"]'), - // array(':submit', '//input[@type="submit"]'), - array('textarea', '//textarea'), array(':first-child', '//*[1]'), array('div:first-child', '//div[1]'), array(':last-child', '//*[last()]'), @@ -67,6 +64,17 @@ public function cssToXpathProvider() # https://github.com/tj/php-selector/issues/14 array('.classa > .classb', '//*[contains(concat(" ", normalize-space(@class), " "), " classa ")]/*[contains(concat(" ", normalize-space(@class), " "), " classb ")]'), array('ul > li:first-child', '//ul/li[1]'), + array(':button', '//*[self::button or @type="button"]'), + array(':submit', '//*[@type="submit" or (self::button and not(@type))]'), + array(':input', '//*[self::input or self::select or self::textarea or self::button]'), + array('textarea', '//textarea'), + array('input:password', '//input[@type="password"]'), + array(':header', '//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 or self::h6]'), + array(':autocomplete', '//[@autocomplete="on"]'), + array(':autofocus', '//[@autofocus]'), + array(':scope', ''), + array('div:not(".thing")', '//div[not(//"*[contains(concat(" ", normalize-space(@class), " "), " thing ")]")]'), + array('a:has(> img)', '//a[count(/img) > 0]'), ); } diff --git a/tests/CssXpath/DOMAssertionTest.php b/tests/CssXpath/DOMAssertionTest.php new file mode 100644 index 0000000..bb693fc --- /dev/null +++ b/tests/CssXpath/DOMAssertionTest.php @@ -0,0 +1,51 @@ +

howdy

'); + self::assertSelectCount('li', false, '

howdy

'); + } + + public function testAssertSelectEquals() + { + self::assertSelectEquals('.name', '', array( + '<' => 2, + '<=' => 2, + '>' => 0, + '>=' => 0, + ), '
Jimmy
'); + } + + public function testAssertSelectRegExp() + { + self::assertSelectRegExp('.name', "/Sam/", 1, '
Sam
'); + } + + public function testInvalidCountArg() + { + $caughtException = false; + $message = null; + try { + self::assertSelectEquals('.name', '', array(), ''); + } catch (\PHPUnit\Framework\Exception $e) { + $caughtException = true; + $message = $e->getMessage(); + } + self::assertTrue($caughtException); + self::assertSame('Invalid count format', $message); + } +}