123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395 |
- <?php
- declare(strict_types=1);
- namespace DiDom\Tests;
- use DiDom\Exceptions\InvalidSelectorException;
- use DiDom\Query;
- use InvalidArgumentException;
- use RuntimeException;
- class QueryTest extends TestCase
- {
- public function testCompileWithUnknownExpressionType()
- {
- $this->expectException(RuntimeException::class);
- $this->expectExceptionMessage('Unknown expression type "foo"');
- Query::compile('h1', 'foo');
- }
- /**
- * @dataProvider compileCssTests
- */
- public function testCompileCssSelector($selector, $xpath)
- {
- $this->assertEquals($xpath, Query::compile($selector));
- }
- /**
- * @dataProvider getSegmentsTests
- *
- * @param string $selector
- * @param array $segments
- */
- public function testGetSegments($selector, $segments)
- {
- $this->assertEquals($segments, Query::getSegments($selector));
- }
- /**
- * @dataProvider buildXpathTests
- *
- * @param array $segments
- * @param string $xpath
- */
- public function testBuildXpath($segments, $xpath)
- {
- $this->assertEquals($xpath, Query::buildXpath($segments));
- }
- public function testBuildXpathWithEmptyArray()
- {
- $this->expectException(InvalidArgumentException::class);
- Query::buildXpath([]);
- }
- public function testCompileWithEmptyXpathExpression()
- {
- $this->expectException(InvalidSelectorException::class);
- $this->expectExceptionMessage('The expression must not be empty.');
- Query::compile('', Query::TYPE_XPATH);
- }
- public function testCompileWithEmptyCssExpression()
- {
- $this->expectException(InvalidSelectorException::class);
- $this->expectExceptionMessage('The expression must not be empty.');
- Query::compile('', Query::TYPE_CSS);
- }
- public function testGetSegmentsWithEmptySelector()
- {
- $this->expectException(InvalidSelectorException::class);
- $this->expectExceptionMessage('The selector must not be empty.');
- Query::getSegments('');
- }
- public function testEmptyAttributeName()
- {
- $this->expectException(InvalidSelectorException::class);
- $this->expectExceptionMessage('Invalid selector "input[=foo]": attribute name must not be empty.');
- Query::compile('input[=foo]');
- }
- public function testUnknownPseudoClass()
- {
- $this->expectException(InvalidSelectorException::class);
- $this->expectExceptionMessage('Unknown pseudo-class "unknown-pseudo-class".');
- Query::compile('li:unknown-pseudo-class');
- }
- /**
- * @dataProvider containsInvalidCaseSensitiveParameterDataProvider
- */
- public function testContainsInvalidCaseSensitiveParameter($caseSensitive)
- {
- $message = sprintf('Parameter 2 of "contains" pseudo-class must be equal true or false, "%s" given', $caseSensitive);
- $this->expectException(InvalidSelectorException::class, $message);
- Query::compile("a:contains('Log in', {$caseSensitive})");
- }
- public function containsInvalidCaseSensitiveParameterDataProvider(): array
- {
- return [
- ['foo'],
- ['TRUE'],
- ['FALSE'],
- ];
- }
- public function testEmptyNthExpression()
- {
- $this->expectException(InvalidSelectorException::class);
- $this->expectExceptionMessage('nth-child (or nth-last-child) expression must not be empty.');
- Query::compile('li:nth-child()');
- }
- public function testEmptyProperty()
- {
- $this->expectException(InvalidSelectorException::class);
- $this->expectExceptionMessage('Invalid property "::".');
- Query::compile('li::');
- }
- public function testInvalidProperty()
- {
- $this->expectException(InvalidSelectorException::class);
- $this->expectExceptionMessage('Unknown property "foo".');
- Query::compile('li::foo');
- }
- public function testUnknownNthExpression()
- {
- $this->expectException(InvalidSelectorException::class);
- $this->expectExceptionMessage('Invalid nth-child expression "foo".');
- Query::compile('li:nth-child(foo)');
- }
- public function testGetSegmentsWithEmptyClassName()
- {
- $this->expectException(InvalidSelectorException::class);
- $this->expectExceptionMessage('Invalid selector ".".');
- Query::getSegments('.');
- }
- public function testCompileWithEmptyClassName()
- {
- $this->expectException(InvalidSelectorException::class);
- $this->expectExceptionMessage('Invalid selector ".".');
- Query::compile('span.');
- }
- public function testCompileXpath()
- {
- $this->assertEquals('//div', Query::compile('//div', Query::TYPE_XPATH));
- }
- public function testSetCompiled()
- {
- $xpath = "//*[@id='foo']//*[contains(concat(' ', normalize-space(@class), ' '), ' bar ')]//baz";
- $compiled = ['#foo .bar baz' => $xpath];
- Query::setCompiled($compiled);
- $this->assertEquals($compiled, Query::getCompiled());
- }
- public function testGetCompiled()
- {
- Query::setCompiled([]);
- $selector = '#foo .bar baz';
- $xpath = '//*[@id="foo"]//*[contains(concat(" ", normalize-space(@class), " "), " bar ")]//baz';
- $compiled = [$selector => $xpath];
- Query::compile($selector);
- $this->assertEquals($compiled, Query::getCompiled());
- }
- public function compileCssTests()
- {
- $compiled = [
- ['a', '//a'],
- ['foo bar baz', '//foo//bar//baz'],
- ['foo > bar > baz', '//foo/bar/baz'],
- ['#foo', '//*[@id="foo"]'],
- ['.foo', '//*[contains(concat(" ", normalize-space(@class), " "), " foo ")]'],
- ['.foo.bar', '//*[(contains(concat(" ", normalize-space(@class), " "), " foo ")) and (contains(concat(" ", normalize-space(@class), " "), " bar "))]'],
- ['*[foo=bar]', '//*[@foo="bar"]'],
- ['*[foo="bar"]', '//*[@foo="bar"]'],
- ['*[foo=\'bar\']', '//*[@foo="bar"]'],
- ['select[name=category] option[selected=selected]', '//select[@name="category"]//option[@selected="selected"]'],
- ['*[^data-]', '//*[@*[starts-with(name(), "data-")]]'],
- ['*[^data-=foo]', '//*[@*[starts-with(name(), "data-")]="foo"]'],
- ['a[href^=https]', '//a[starts-with(@href, "https")]'],
- ['img[src$=png]', '//img[substring(@src, string-length(@src) - string-length("png") + 1) = "png"]'],
- ['a[href*=example.com]', '//a[contains(@href, "example.com")]'],
- ['script[!src]', '//script[not(@src)]'],
- ['a[href!="http://foo.com/"]', '//a[not(@href="http://foo.com/")]'],
- ['a[foo~="bar"]', '//a[contains(concat(" ", normalize-space(@foo), " "), " bar ")]'],
- ['input, textarea, select', '//input|//textarea|//select'],
- ['input[name="name"], textarea[name="description"], select[name="type"]', '//input[@name="name"]|//textarea[@name="description"]|//select[@name="type"]'],
- ['li:first-child', '//li[position() = 1]'],
- ['li:last-child', '//li[position() = last()]'],
- ['*:not(a[href*="example.com"])', '//*[not(self::a[contains(@href, "example.com")])]'],
- ['*:not(a[href*="example.com"]):not(.foo)', '//*[(not(self::a[contains(@href, "example.com")])) and (not(self::*[contains(concat(" ", normalize-space(@class), " "), " foo ")]))]'],
- ['ul:empty', '//ul[count(descendant::*) = 0]'],
- ['ul:not-empty', '//ul[count(descendant::*) > 0]'],
- ['li:nth-child(odd)', '//*[(name()="li") and (position() mod 2 = 1 and position() >= 1)]'],
- ['li:nth-child(even)', '//*[(name()="li") and (position() mod 2 = 0 and position() >= 0)]'],
- ['li:nth-child(3)', '//*[(name()="li") and (position() = 3)]'],
- ['li:nth-child(-3)', '//*[(name()="li") and (position() = -3)]'],
- ['li:nth-child(3n)', '//*[(name()="li") and ((position() + 0) mod 3 = 0 and position() >= 0)]'],
- ['li:nth-child(3n+1)', '//*[(name()="li") and ((position() - 1) mod 3 = 0 and position() >= 1)]'],
- ['li:nth-child(3n-1)', '//*[(name()="li") and ((position() + 1) mod 3 = 0 and position() >= 1)]'],
- ['li:nth-child(n+3)', '//*[(name()="li") and ((position() - 3) mod 1 = 0 and position() >= 3)]'],
- ['li:nth-child(n-3)', '//*[(name()="li") and ((position() + 3) mod 1 = 0 and position() >= 3)]'],
- ['li:nth-of-type(odd)', '//li[position() mod 2 = 1 and position() >= 1]'],
- ['li:nth-of-type(even)', '//li[position() mod 2 = 0 and position() >= 0]'],
- ['li:nth-of-type(3)', '//li[position() = 3]'],
- ['li:nth-of-type(-3)', '//li[position() = -3]'],
- ['li:nth-of-type(3n)', '//li[(position() + 0) mod 3 = 0 and position() >= 0]'],
- ['li:nth-of-type(3n+1)', '//li[(position() - 1) mod 3 = 0 and position() >= 1]'],
- ['li:nth-of-type(3n-1)', '//li[(position() + 1) mod 3 = 0 and position() >= 1]'],
- ['li:nth-of-type(n+3)', '//li[(position() - 3) mod 1 = 0 and position() >= 3]'],
- ['li:nth-of-type(n-3)', '//li[(position() + 3) mod 1 = 0 and position() >= 3]'],
- ['ul:has(li.item)', '//ul[.//li[contains(concat(" ", normalize-space(@class), " "), " item ")]]'],
- ['form[name=register]:has(input[name=foo])', '//form[(@name="register") and (.//input[@name="foo"])]'],
- ['ul li a::text', '//ul//li//a/text()'],
- ['ul li a::text()', '//ul//li//a/text()'],
- ['ul li a::attr(href)', '//ul//li//a/@*[name() = "href"]'],
- ['ul li a::attr(href, title)', '//ul//li//a/@*[name() = "href" or name() = "title"]'],
- ['> ul li a', '/ul//li//a'],
- ];
- $compiled = array_merge($compiled, $this->getContainsPseudoClassTests());
- $compiled = array_merge($compiled, $this->getPropertiesTests());
- $compiled = array_merge($compiled, [
- ['a[title="foo, bar::baz"]', '//a[@title="foo, bar::baz"]'],
- ]);
- return $compiled;
- }
- private function getContainsPseudoClassTests(): array
- {
- $strToLowerFunction = function_exists('mb_strtolower') ? 'mb_strtolower' : 'strtolower';
- $containsXpath = [
- // caseSensitive = true, fullMatch = false
- ['li:contains(foo)', '//li[contains(text(), "foo")]'],
- ['li:contains("foo")', '//li[contains(text(), "foo")]'],
- ['li:contains(\'foo\')', '//li[contains(text(), "foo")]'],
- // caseSensitive = true, fullMatch = false
- ['li:contains(foo, true)', '//li[contains(text(), "foo")]'],
- ['li:contains("foo", true)', '//li[contains(text(), "foo")]'],
- ['li:contains(\'foo\', true)', '//li[contains(text(), "foo")]'],
- // caseSensitive = true, fullMatch = false
- ['li:contains(foo, true, false)', '//li[contains(text(), "foo")]'],
- ['li:contains("foo", true, false)', '//li[contains(text(), "foo")]'],
- ['li:contains(\'foo\', true, false)', '//li[contains(text(), "foo")]'],
- // caseSensitive = true, fullMatch = true
- ['li:contains(foo, true, true)', '//li[text() = "foo"]'],
- ['li:contains("foo", true, true)', '//li[text() = "foo"]'],
- ['li:contains(\'foo\', true, true)', '//li[text() = "foo"]'],
- // caseSensitive = false, fullMatch = false
- ['li:contains(foo, false)', "//li[contains(php:functionString(\"{$strToLowerFunction}\", .), php:functionString(\"{$strToLowerFunction}\", \"foo\"))]"],
- ['li:contains("foo", false)', "//li[contains(php:functionString(\"{$strToLowerFunction}\", .), php:functionString(\"{$strToLowerFunction}\", \"foo\"))]"],
- ['li:contains(\'foo\', false)', "//li[contains(php:functionString(\"{$strToLowerFunction}\", .), php:functionString(\"{$strToLowerFunction}\", \"foo\"))]"],
- // caseSensitive = false, fullMatch = false
- ['li:contains(foo, false, false)', "//li[contains(php:functionString(\"{$strToLowerFunction}\", .), php:functionString(\"{$strToLowerFunction}\", \"foo\"))]"],
- ['li:contains("foo", false, false)', "//li[contains(php:functionString(\"{$strToLowerFunction}\", .), php:functionString(\"{$strToLowerFunction}\", \"foo\"))]"],
- ['li:contains(\'foo\', false, false)', "//li[contains(php:functionString(\"{$strToLowerFunction}\", .), php:functionString(\"{$strToLowerFunction}\", \"foo\"))]"],
- // caseSensitive = false, fullMatch = true
- ['li:contains(foo, false, true)', "//li[php:functionString(\"{$strToLowerFunction}\", .) = php:functionString(\"{$strToLowerFunction}\", \"foo\")]"],
- ['li:contains("foo", false, true)', "//li[php:functionString(\"{$strToLowerFunction}\", .) = php:functionString(\"{$strToLowerFunction}\", \"foo\")]"],
- ['li:contains(\'foo\', false, true)', "//li[php:functionString(\"{$strToLowerFunction}\", .) = php:functionString(\"{$strToLowerFunction}\", \"foo\")]"],
- ];
- return $containsXpath;
- }
- private function getPropertiesTests(): array
- {
- return [
- ['a::text', '//a/text()'],
- ['a::text()', '//a/text()'],
- ['a::attr', '//a/@*'],
- ['a::attr()', '//a/@*'],
- ['a::attr(href)', '//a/@*[name() = "href"]'],
- ['a::attr(href,title)', '//a/@*[name() = "href" or name() = "title"]'],
- ['a::attr(href, title)', '//a/@*[name() = "href" or name() = "title"]'],
- ];
- }
- public function buildXpathTests(): array
- {
- $xpath = [
- '//a',
- '//*[@id="foo"]',
- '//a[@id="foo"]',
- '//a[contains(concat(" ", normalize-space(@class), " "), " foo ")]',
- '//a[(contains(concat(" ", normalize-space(@class), " "), " foo ")) and (contains(concat(" ", normalize-space(@class), " "), " bar "))]',
- '//a[@href]',
- '//a[@href="http://example.com/"]',
- '//a[(@href="http://example.com/") and (@title="Example Domain")]',
- '//a[(@target="_blank") and (starts-with(@href, "https"))]',
- '//a[substring(@href, string-length(@href) - string-length(".com") + 1) = ".com"]',
- '//a[contains(@href, "example")]',
- '//a[not(@href="http://foo.com/")]',
- '//script[not(@src)]',
- '//li[position() = 1]',
- '//*[(@id="id") and (contains(concat(" ", normalize-space(@class), " "), " foo ")) and (@name="value") and (position() = 1)]',
- ];
- $segments = [
- ['tag' => 'a'],
- ['id' => 'foo'],
- ['tag' => 'a', 'id' => 'foo'],
- ['tag' => 'a', 'classes' => ['foo']],
- ['tag' => 'a', 'classes' => ['foo', 'bar']],
- ['tag' => 'a', 'attributes' => ['href' => null]],
- ['tag' => 'a', 'attributes' => ['href' => 'http://example.com/']],
- ['tag' => 'a', 'attributes' => ['href' => 'http://example.com/', 'title' => 'Example Domain']],
- ['tag' => 'a', 'attributes' => ['target' => '_blank', 'href^' => 'https']],
- ['tag' => 'a', 'attributes' => ['href$' => '.com']],
- ['tag' => 'a', 'attributes' => ['href*' => 'example']],
- ['tag' => 'a', 'attributes' => ['href!' => 'http://foo.com/']],
- ['tag' => 'script', 'attributes' => ['!src' => null]],
- ['tag' => 'li', 'pseudo' => [['type' => 'first-child', 'expression' => null]]],
- ['tag' => '*', 'id' => 'id', 'classes' => ['foo'], 'attributes' => ['name' => 'value'], 'pseudo' => [['type' => 'first-child', 'expression' => null]], 'rel' => '>'],
- ];
- $parameters = [];
- foreach ($segments as $index => $segment) {
- $parameters[] = [$segment, $xpath[$index]];
- }
- return $parameters;
- }
- public function getSegmentsTests(): array
- {
- $segments = [
- ['selector' => 'a', 'tag' => 'a'],
- ['selector' => '#foo', 'id' => 'foo'],
- ['selector' => 'a#foo', 'tag' => 'a', 'id' => 'foo'],
- ['selector' => 'a.foo', 'tag' => 'a', 'classes' => ['foo']],
- ['selector' => 'a.foo.bar', 'tag' => 'a', 'classes' => ['foo', 'bar']],
- ['selector' => 'a[href]', 'tag' => 'a', 'attributes' => ['href' => null]],
- ['selector' => 'a[href=http://example.com/]', 'tag' => 'a', 'attributes' => ['href' => 'http://example.com/']],
- ['selector' => 'a[href="http://example.com/"]', 'tag' => 'a', 'attributes' => ['href' => 'http://example.com/']],
- ['selector' => 'a[href=\'http://example.com/\']', 'tag' => 'a', 'attributes' => ['href' => 'http://example.com/']],
- ['selector' => 'a[href=http://example.com/][title=Example Domain]', 'tag' => 'a', 'attributes' => ['href' => 'http://example.com/', 'title' => 'Example Domain']],
- ['selector' => 'a[href=http://example.com/][href=http://example.com/404]', 'tag' => 'a', 'attributes' => ['href' => 'http://example.com/404']],
- ['selector' => 'a[href^=https]', 'tag' => 'a', 'attributes' => ['href^' => 'https']],
- ['selector' => 'li:first-child', 'tag' => 'li', 'pseudo' => [['type' => 'first-child', 'expression' => null]]],
- ['selector' => 'ul >', 'tag' => 'ul', 'rel' => '>'],
- ['selector' => '#id.foo[name=value]:first-child >', 'id' => 'id', 'classes' => ['foo'], 'attributes' => ['name' => 'value'], 'pseudo' => [['type' => 'first-child', 'expression' => null]], 'rel' => '>'],
- ['selector' => 'li.bar:nth-child(2n)', 'tag' => 'li', 'classes' => ['bar'], 'pseudo' => [['type' => 'nth-child', 'expression' => '2n']]],
- ];
- $parameters = [];
- foreach ($segments as $segment) {
- $parameters[] = [$segment['selector'], $segment];
- }
- return $parameters;
- }
- }
|