Browse Source

XPath done and fully tested

master
Dustin Wilson 2 years ago
parent
commit
17d6b8a2ed
  1. 6
      README.md
  2. 4
      lib/Document.php
  3. 37
      lib/XPathEvaluate.php
  4. 4
      lib/XPathEvaluator.php
  5. 9
      lib/XPathEvaluatorBase.php
  6. 4
      lib/XPathException.php
  7. 56
      lib/XPathExpression.php
  8. 14
      lib/XPathNSResolver.php
  9. 8
      lib/XPathResult.php
  10. 57
      tests/cases/TestXPathEvaluate.php
  11. 9
      tests/cases/TestXPathEvaluator.php
  12. 159
      tests/cases/TestXPathExpression.php
  13. 149
      tests/cases/TestXPathResult.php
  14. 2
      tests/phpunit.dist.xml

6
README.md

@ -57,6 +57,8 @@ The primary aim of this library is accuracy. However, due either to limitations
11. All of the `Range` APIs will also not be implemented due to the sheer complexity of creating them in userland and how it adds undue difficulty to node manipulation in the "core" DOM. Numerous operations reference in excrutiating detail what to do with Ranges when manipulating nodes and would have to be added here to be compliant or mostly so -- slowing everything else down in the process on an already extremely front-heavy library.
12. The `DOMParser` and `XMLSerializer` APIs will not be implemented because they are ridiculous and limited in their scope. For instance, `DOMParser::parseFromString` won't set a document's character set to anything but UTF-8. This library needs to be able to print to other encodings due to the nature of how it is used. `Document::__construct` will accept optional `$source` and `$charset` arguments, and there are both `Document::load` and `Document::loadFile` methods for loading DOM from a string or a file respectively.
13. Aside from `HTMLElement`, `HTMLPreElement`, `HTMLTemplateElement`, `HTMLUnknownElement`, `MathMLElement`, and `SVGElement` none of the specific derived element classes (such as `HTMLAnchorElement` or `SVGSVGElement`) are implemented. The ones listed before are required for the element interface algorithm. The focus on this library will be on the core DOM before moving onto those -- if ever.
14. This class is meant to be used with HTML, but it will -MOSTLY- as needed work with XML. Loading of XML uses PHP DOM's XML parser which does not conform to the XML specification. Writing an actual conforming XML parser is outside of the scope of this library.
14. This class is meant to be used with HTML, but it will -MOSTLY- as needed work with XML. Loading of XML uses PHP DOM's XML parser which does not completely conform to the XML specification. Writing an actual conforming XML parser is outside of the scope of this library.
15. While there is implementation of much of the XPath extensions, there will only be support for XPath 1.0 because that is all PHP DOM's XPath supports.
16. The XPath DOM specification allows for the use of the `XPathNSResolver` to automatically resolve namespaces for prefixes. To polyfill this behavior for use with PHP's XPath implementation would require writing at least partially a XPath 1.0 parser to grab any prefixes and then use `DOMXPath::registerNamespace` to associate namespaces. This might be something done at a later date. In the meantime this implementation instead exposes this ability to assocate namespaces with prefixes through the `Document::registerXPathNamespace` and `XPathEvaluator::registerXPathNamespace` methods. However, to eliminate common uses of namespace association the `xmlns` namespace is automatically associated.
16. This library's XPath API is -- like the rest of the library itself -- a wrapper that wraps PHP's implementation but instead works like the specification, so there is no need to manually register namespaces. Namespaces that are associated with prefixes will be looked up when evaluating the expression if a `XPathNSResolver` is specified. However, access to registering PHP functions for use within XPath isn't in the specification but is available through `Document::registerXPathFunctions` and `XPathEvaluator::registerXPathFunctions`.
17. `XPathEvaluatorBase::evaluate` has a `result` argument where one provides it with an existing result object to use. I can't find any usable documentation on what this is supposed to do, and the specifications on it are vague. So, at present it does nothing until what it needs to do can be deduced.
18. At present XPath expressions cannot select elements or attributes which use any valid non-ascii character. This is because those nodes are coerced internally to work within PHP's DOM which doesn't support those characters. This can be worked around by coercing names in XPath queries, but that can only be reliably accomplished through an XPath parser. Writing an entire XPath parser for what amounts to an edge case isn't desirable.

4
lib/Document.php

@ -738,10 +738,6 @@ class Document extends Node implements \ArrayAccess {
$this->xpathRegisterPhpFunctions($this, $restrict);
}
public function registerXPathNamespace(string $prefix, string $namespace): bool {
return $this->xpathRegisterNamespace($this, $prefix, $namespace);
}
public function serialize(?Node $node = null, array $config = []): string {
$node = $node ?? $this;
if ($node !== $this) {

37
lib/XPathEvaluate.php

@ -11,21 +11,34 @@ use MensBeam\HTML\DOM\Inner\Reflection;
trait XPathEvaluate {
protected function xpathEvaluate(string $expression, Node $contextNode, int $type = XPathResult::ANY_TYPE, ?XPathResult $result = null): XPathResult {
protected function xpathErrorHandler(int $errno, string $errstr, string $errfile, int $errline) {
$lowerErrstr = strtolower($errstr);
if (str_contains(needle: 'invalid expression', haystack: $lowerErrstr)) {
throw new XPathException(XPathException::INVALID_EXPRESSION);
}
if (str_contains(needle: 'undefined namespace prefix', haystack: $lowerErrstr)) {
throw new XPathException(XPathException::UNRESOLVABLE_NAMESPACE_PREFIX);
}
} // @codeCoverageIgnore
protected function xpathEvaluate(string $expression, Node $contextNode, ?XPathNSResolver $resolver = null, int $type = XPathResult::ANY_TYPE, ?XPathResult $result = null): XPathResult {
$innerContextNode = Reflection::getProtectedProperty($contextNode, 'innerNode');
$doc = ($innerContextNode instanceof \DOMDocument) ? $innerContextNode : $innerContextNode->ownerDocument;
set_error_handler(function(int $errno, string $errstr, string $errfile, int $errline) {
$lowerErrstr = strtolower($errstr);
if (str_contains(needle: 'invalid expression', haystack: $lowerErrstr)) {
throw new XPathException(XPathException::INVALID_EXPRESSION);
if ($resolver !== null && preg_match_all('/([A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}][A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}-\.0-9\x{B7}\x{0300}-\x{036F}\x{203F}-\x{2040}]+):([A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}][A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}-\.0-9\x{B7}\x{0300}-\x{036F}\x{203F}-\x{2040}]+)/u', $expression, $m, \PREG_SET_ORDER)) {
foreach ($m as $prefix) {
$prefix = $prefix[1];
if ($namespace = $contextNode->lookupNamespaceURI($prefix)) {
$doc->xpath->registerNamespace($prefix, $namespace);
}
}
}
if (str_contains(needle: 'undefined namespace prefix', haystack: $lowerErrstr)) {
throw new XPathException(XPathException::UNDEFINED_NAMESPACE_PREFIX);
}
});
// PHP's DOM XPath incorrectly issues a warnings rather than exceptions when
// expressions are incorrect, so we must use a custom error handler here to
// "catch" it and throw an exception in its place.
set_error_handler([ $this, 'xpathErrorHandler' ]);
$result = $doc->xpath->evaluate($expression, $innerContextNode);
restore_error_handler();
@ -129,8 +142,4 @@ trait XPathEvaluate {
protected function xpathRegisterPhpFunctions(Document $document, string|array|null $restrict = null): void {
Reflection::getProtectedProperty($document, 'innerNode')->xpath->registerPhpFunctions($restrict);
}
protected function xpathRegisterNamespace(Document $document, string $prefix, string $namespace): bool {
return Reflection::getProtectedProperty($document, 'innerNode')->xpath->registerNamespace($prefix, $namespace);
}
}

4
lib/XPathEvaluator.php

@ -16,8 +16,4 @@ class XPathEvaluator {
public function registerXPathFunctions(Document $document, string|array|null $restrict = null): void {
$this->xpathRegisterPhpFunctions($document, $restrict);
}
public function registerXPathNamespace(Document $document, string $prefix, string $namespace): bool {
return $this->xpathRegisterNamespace($document, $prefix, $namespace);
}
}

9
lib/XPathEvaluatorBase.php

@ -16,10 +16,15 @@ trait XPathEvaluatorBase {
public function createExpression(string $expression, ?XPathNSResolver $resolver = null): XPathExpression {
// XPathExpression cannot be created from their constructors normally.
return Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\XPathExpression', $expression);
return Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\XPathExpression', $expression, $resolver);
}
public function createNSResolver(Node $nodeResolver): XPathNSResolver {
// XPathNSResolver cannot be created from their constructors normally.
return Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\XPathNSResolver', $nodeResolver);
}
public function evaluate(string $expression, Node $contextNode, ?XPathNSResolver $resolver = null, int $type = XPathResult::ANY_TYPE, ?XPathResult $result = null): XPathResult {
return $this->xpathEvaluate($expression, $contextNode, $type, $result);
return $this->xpathEvaluate($expression, $contextNode, $resolver, $type, $result);
}
}

4
lib/XPathException.php

@ -13,13 +13,13 @@ use MensBeam\Framework\Exception;
class XPathException extends Exception {
public const INVALID_EXPRESSION = 51;
public const TYPE_ERROR = 52;
public const UNDEFINED_NAMESPACE_PREFIX = 53;
public const UNRESOLVABLE_NAMESPACE_PREFIX = 53;
public function __construct(int $code, ...$args) {
self::$messages = array_replace(parent::$messages, [
51 => 'Invalid expression error',
52 => 'Expression cannot be converted to the specified type',
53 => 'Undefined namespace prefix'
53 => 'Unresolvable namespace prefix'
]);
parent::__construct($code, ...$args);

56
lib/XPathExpression.php

@ -7,40 +7,52 @@
declare(strict_types=1);
namespace MensBeam\HTML\DOM;
use MensBeam\HTML\DOM\Inner\Reflection;
class XPathExpression {
use XPathEvaluate;
protected string $expression;
protected function __construct(string $expression) {
// Test the expression by attempting to run it on an empty document. PHP's DOM
// XPath incorrectly issues a warning on an invalid expression rather than an
// exception, so we must use a custom error handler here to "catch" it and throw
// an exception in its place.
set_error_handler(function(int $errno, string $errstr, string $errfile, int $errline) {
$lowerErrstr = strtolower($errstr);
if (str_contains(needle: 'invalid expression', haystack: $lowerErrstr)) {
throw new XPathException(XPathException::INVALID_EXPRESSION);
protected ?XPathNSResolver $resolver;
protected function __construct(string $expression, ?XPathNSResolver $resolver) {
if ($resolver !== null && preg_match_all('/([A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}][A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}-\.0-9\x{B7}\x{0300}-\x{036F}\x{203F}-\x{2040}]+):([A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}][A-Z_a-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}-\.0-9\x{B7}\x{0300}-\x{036F}\x{203F}-\x{2040}]+)/u', $expression, $m, \PREG_SET_ORDER)) {
// This part is especially nasty because of egregious use of reflection to get
// protected properties, but neither should be exposed publicly; this is a crazy
// polyfill hack that wouldn't normally be necessary otherwise.
$nodeResolver = Reflection::getProtectedProperty($resolver, 'nodeResolver');
$innerNodeResolver = Reflection::getProtectedProperty($nodeResolver, 'innerNode');
$doc = ($innerNodeResolver instanceof \DOMDocument) ? $innerNodeResolver : $innerNodeResolver->ownerDocument;
foreach ($m as $prefix) {
$prefix = $prefix[1];
if ($namespace = $resolver->lookupNamespaceURI($prefix)) {
$doc->xpath->registerNamespace($prefix, $namespace);
}
}
// Ignore undefined namespace prefix warnings here because there's no way to
// register namespace prefixes before the expression is created.
});
$xpath = new \DOMXPath(new \DOMDocument());
$xpath->evaluate($expression);
restore_error_handler();
set_error_handler([ $this, 'xpathErrorHandler' ]);
$doc->xpath->evaluate($expression);
restore_error_handler();
} else {
// Test the expression by attempting to run it on an empty document. PHP's DOM
// XPath incorrectly issues a warnings rather than exceptions when expressions
// are incorrect, so we must use a custom error handler here to "catch" it and
// throw an exception in its place.
set_error_handler([ $this, 'xpathErrorHandler' ]);
$xpath = new \DOMXPath(new \DOMDocument());
$xpath->evaluate($expression);
restore_error_handler();
}
$this->expression = $expression;
$this->resolver = $resolver;
}
protected function evaluate(Node $contextNode, int $type = XPathResult::ANY_TYPE, ?XPathResult $result = null): XPathResult {
return $this->xpathEvaluate($this->expression, $contextNode, $type, $result);
public function evaluate(Node $contextNode, int $type = XPathResult::ANY_TYPE, ?XPathResult $result = null): XPathResult {
return $this->xpathEvaluate($this->expression, $contextNode, $this->resolver, $type, $result);
}
}

14
lib/XPathNSResolver.php

@ -9,4 +9,16 @@ declare(strict_types=1);
namespace MensBeam\HTML\DOM;
interface XPathNSResolver {}
class XPathNSResolver {
protected Node $nodeResolver;
protected function __construct(Node $nodeResolver) {
$this->nodeResolver = $nodeResolver;
}
public function lookupNamespaceURI(?string $prefix): ?string {
return $this->nodeResolver->lookupNamespaceURI($prefix);
}
}

8
lib/XPathResult.php

@ -63,12 +63,12 @@ class XPathResult implements \ArrayAccess, \Countable, \Iterator {
return $node->ownerDocument->getWrapperNode($node);
}
protected function __get_snapshotLength(): bool {
protected function __get_snapshotLength(): int {
if (!in_array($this->_resultType, [ self::ORDERED_NODE_SNAPSHOT_TYPE, self::UNORDERED_NODE_SNAPSHOT_TYPE ])) {
throw new XPathException(XPathException::TYPE_ERROR);
}
return $this->count;
return $this->count();
}
protected function __get_stringValue(): string {
@ -102,6 +102,10 @@ class XPathResult implements \ArrayAccess, \Countable, \Iterator {
throw new XPathException(XPathException::TYPE_ERROR);
}
if ($this->position + 1 > $this->count()) {
return null;
}
$node = $this->storage[$this->position++];
return $node->ownerDocument->getWrapperNode($node);
}

57
tests/cases/TestXPathEvaluate.php

@ -22,18 +22,40 @@ class TestXPathEvaluate extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\HTML\DOM\XPathEvaluate::xpathEvaluate
*
* @covers \MensBeam\HTML\DOM\Attr::__get_localName
* @covers \MensBeam\HTML\DOM\Attr::__get_namespaceURI
* @covers \MensBeam\HTML\DOM\Attr::__get_ownerElement
* @covers \MensBeam\HTML\DOM\Attr::__set_value
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::__get_body
* @covers \MensBeam\HTML\DOM\Document::__get_documentElement
* @covers \MensBeam\HTML\DOM\Document::createAttributeNS
* @covers \MensBeam\HTML\DOM\Document::createElementNS
* @covers \MensBeam\HTML\DOM\Document::load
* @covers \MensBeam\HTML\DOM\Document::registerXPathFunctions
* @covers \MensBeam\HTML\DOM\DocumentOrElement::validateAndExtract
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\Element::__construct
* @covers \MensBeam\HTML\DOM\Element::getAttributeNodeNS
* @covers \MensBeam\HTML\DOM\Element::setAttributeNode
* @covers \MensBeam\HTML\DOM\Element::setAttributeNodeNS
* @covers \MensBeam\HTML\DOM\Element::setAttributeNS
* @covers \MensBeam\HTML\DOM\Node::__construct
* @covers \MensBeam\HTML\DOM\Node::__get_ownerDocument
* @covers \MensBeam\HTML\DOM\Node::appendChild
* @covers \MensBeam\HTML\DOM\Node::getInnerDocument
* @covers \MensBeam\HTML\DOM\Node::getInnerNode
* @covers \MensBeam\HTML\DOM\Node::hasChildNodes
* @covers \MensBeam\HTML\DOM\Node::locateNamespace
* @covers \MensBeam\HTML\DOM\Node::lookupNamespaceURI
* @covers \MensBeam\HTML\DOM\Node::postInsertionBugFixes
* @covers \MensBeam\HTML\DOM\Node::postParsingTemplatesFix
* @covers \MensBeam\HTML\DOM\Node::preInsertionBugFixes
* @covers \MensBeam\HTML\DOM\Node::preInsertionValidity
* @covers \MensBeam\HTML\DOM\XPathEvaluate::xpathRegisterPhpFunctions
* @covers \MensBeam\HTML\DOM\XPathEvaluatorBase::createNSResolver
* @covers \MensBeam\HTML\DOM\XPathEvaluatorBase::evaluate
* @covers \MensBeam\HTML\DOM\XPathNSResolver::__construct
* @covers \MensBeam\HTML\DOM\XPathResult::__construct
* @covers \MensBeam\HTML\DOM\XPathResult::__get_booleanValue
* @covers \MensBeam\HTML\DOM\XPathResult::__get_numberValue
@ -80,6 +102,10 @@ class TestXPathEvaluate extends \PHPUnit\Framework\TestCase {
$this->assertEquals(3, count($result));
$result = $d->evaluate('.//span', $d->body, null, XPathResult::FIRST_ORDERED_NODE_TYPE);
$this->assertSame($d->body->firstChild, $result->singleNodeValue);
$d->documentElement->setAttributeNS(Node::XMLNS_NAMESPACE, 'xmlns:poop', 'https://poop.poop');
$poop = $d->body->appendChild($d->createElementNS('https://poop.poop', 'poop:poop'));
$this->assertSame($poop, $d->evaluate('//poop:poop', $d->body, $d->createNSResolver($d->body), XPathResult::FIRST_ORDERED_NODE_TYPE)->singleNodeValue);
}
@ -132,7 +158,7 @@ class TestXPathEvaluate extends \PHPUnit\Framework\TestCase {
$d->evaluate('//svg:svg', $d, null);
},
XPathException::class,
XPathException::UNDEFINED_NAMESPACE_PREFIX ],
XPathException::UNRESOLVABLE_NAMESPACE_PREFIX ],
[ function() {
$d = new Document();
@ -165,33 +191,4 @@ class TestXPathEvaluate extends \PHPUnit\Framework\TestCase {
$this->expectExceptionCode($errorCode);
$closure();
}
/**
* @covers \MensBeam\HTML\DOM\XPathEvaluate::xpathRegisterNamespace
*
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::load
* @covers \MensBeam\HTML\DOM\Document::registerXPathNamespace
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\Node::__construct
* @covers \MensBeam\HTML\DOM\Node::getInnerDocument
* @covers \MensBeam\HTML\DOM\Node::hasChildNodes
* @covers \MensBeam\HTML\DOM\Node::postParsingTemplatesFix
* @covers \MensBeam\HTML\DOM\XPathEvaluate::xpathEvaluate
* @covers \MensBeam\HTML\DOM\XPathEvaluatorBase::evaluate
* @covers \MensBeam\HTML\DOM\XPathResult::__construct
* @covers \MensBeam\HTML\DOM\Inner\Document::__construct
* @covers \MensBeam\HTML\DOM\Inner\Document::__get_xpath
* @covers \MensBeam\HTML\DOM\Inner\NodeCache::has
* @covers \MensBeam\HTML\DOM\Inner\NodeCache::key
* @covers \MensBeam\HTML\DOM\Inner\NodeCache::set
* @covers \MensBeam\HTML\DOM\Inner\Reflection::createFromProtectedConstructor
* @covers \MensBeam\HTML\DOM\Inner\Reflection::getProtectedProperty
*/
function testMethod_xpathRegisterNamespace(): void {
$d = new Document('<!DOCTYPE html><html><body><svg></svg></body></html>');
$d->registerXPathNamespace('svg', Node::SVG_NAMESPACE);
$this->assertEquals(1, count($d->evaluate('//svg:svg', $d)));
}
}

9
tests/cases/TestXPathEvaluator.php

@ -10,7 +10,6 @@ namespace MensBeam\HTML\DOM\TestCase;
use MensBeam\HTML\DOM\{
Document,
Node,
XPathEvaluator
};
@ -22,12 +21,4 @@ class TestXPathEvaluator extends \PHPUnit\Framework\TestCase {
$e = new XPathEvaluator();
$this->assertNull($e->registerXPathFunctions($d));
}
function testMethod_xpathRegisterNamespace(): void {
$d = new Document('<!DOCTYPE html><html><body><svg></svg></body></html>');
$e = new XPathEvaluator();
$e->registerXPathNamespace($d, 'svg', Node::SVG_NAMESPACE);
$this->assertEquals(1, count($e->evaluate('//svg:svg', $d)));
}
}

159
tests/cases/TestXPathExpression.php

@ -0,0 +1,159 @@
<?php
/**
* @license MIT
* Copyright 2017 Dustin Wilson, J. King, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM\TestCase;
use MensBeam\HTML\DOM\{
Document,
Node,
XPathException,
XPathExpression,
XPathResult
};
/** @covers \MensBeam\HTML\DOM\XPathExpression */
class TestXPathExpression extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\HTML\DOM\XPathExpression::__construct
*
* @covers \MensBeam\HTML\DOM\Attr::__get_localName
* @covers \MensBeam\HTML\DOM\Attr::__get_namespaceURI
* @covers \MensBeam\HTML\DOM\Attr::__get_ownerElement
* @covers \MensBeam\HTML\DOM\Attr::__set_value
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::__get_body
* @covers \MensBeam\HTML\DOM\Document::__get_documentElement
* @covers \MensBeam\HTML\DOM\Document::createAttributeNS
* @covers \MensBeam\HTML\DOM\Document::createElementNS
* @covers \MensBeam\HTML\DOM\Document::load
* @covers \MensBeam\HTML\DOM\DocumentOrElement::validateAndExtract
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\Element::__construct
* @covers \MensBeam\HTML\DOM\Element::getAttributeNodeNS
* @covers \MensBeam\HTML\DOM\Element::setAttributeNode
* @covers \MensBeam\HTML\DOM\Element::setAttributeNodeNS
* @covers \MensBeam\HTML\DOM\Element::setAttributeNS
* @covers \MensBeam\HTML\DOM\Node::__construct
* @covers \MensBeam\HTML\DOM\Node::__get_ownerDocument
* @covers \MensBeam\HTML\DOM\Node::appendChild
* @covers \MensBeam\HTML\DOM\Node::getInnerDocument
* @covers \MensBeam\HTML\DOM\Node::getInnerNode
* @covers \MensBeam\HTML\DOM\Node::hasChildNodes
* @covers \MensBeam\HTML\DOM\Node::locateNamespace
* @covers \MensBeam\HTML\DOM\Node::lookupNamespaceURI
* @covers \MensBeam\HTML\DOM\Node::postInsertionBugFixes
* @covers \MensBeam\HTML\DOM\Node::postParsingTemplatesFix
* @covers \MensBeam\HTML\DOM\Node::preInsertionBugFixes
* @covers \MensBeam\HTML\DOM\Node::preInsertionValidity
* @covers \MensBeam\HTML\DOM\XPathEvaluatorBase::createExpression
* @covers \MensBeam\HTML\DOM\XPathEvaluatorBase::createNSResolver
* @covers \MensBeam\HTML\DOM\XPathNSResolver::__construct
* @covers \MensBeam\HTML\DOM\XPathNSResolver::lookupNamespaceURI
* @covers \MensBeam\HTML\DOM\Inner\Document::__construct
* @covers \MensBeam\HTML\DOM\Inner\Document::__get_wrapperNode
* @covers \MensBeam\HTML\DOM\Inner\Document::__get_xpath
* @covers \MensBeam\HTML\DOM\Inner\Document::getWrapperNode
* @covers \MensBeam\HTML\DOM\Inner\NodeCache::get
* @covers \MensBeam\HTML\DOM\Inner\NodeCache::has
* @covers \MensBeam\HTML\DOM\Inner\NodeCache::key
* @covers \MensBeam\HTML\DOM\Inner\NodeCache::set
* @covers \MensBeam\HTML\DOM\Inner\Reflection::createFromProtectedConstructor
* @covers \MensBeam\HTML\DOM\Inner\Reflection::getProtectedProperty
*/
function testMethod_constructor(): void {
$d = new Document('<!DOCTYPE><html></html>');
$d->documentElement->setAttributeNS(Node::XMLNS_NAMESPACE, 'xmlns:poop', 'https://poop.poop');
$poop = $d->body->appendChild($d->createElementNS('https://poop.poop', 'poop:poop'));
$this->assertSame(XPathExpression::class, $d->createExpression('//poop:poop', $d->createNSResolver($d))::class);
$this->assertSame(XPathExpression::class, $d->createExpression('//span')::class);
}
function provideMethod_constructor__errors(): iterable {
return [
[ function() {
$d = new Document();
$d->createExpression('//fail:fail');
},
XPathException::UNRESOLVABLE_NAMESPACE_PREFIX ],
[ function() {
$d = new Document();
$d->createExpression('//fail:fail', $d->createNSResolver($d));
},
XPathException::UNRESOLVABLE_NAMESPACE_PREFIX ],
[ function() {
$d = new Document();
$d->createExpression('fail?');
},
XPathException::INVALID_EXPRESSION ],
[ function() {
$d = new Document();
$d->createExpression('fail?', $d->createNSResolver($d));
},
XPathException::INVALID_EXPRESSION ],
];
}
/**
* @dataProvider provideMethod_constructor__errors
* @covers \MensBeam\HTML\DOM\XPathExpression::__construct
*
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\Node::__construct
* @covers \MensBeam\HTML\DOM\XPathEvaluate::xpathErrorHandler
* @covers \MensBeam\HTML\DOM\XPathEvaluatorBase::createExpression
* @covers \MensBeam\HTML\DOM\XPathException::__construct
* @covers \MensBeam\HTML\DOM\Inner\Document::__construct
* @covers \MensBeam\HTML\DOM\Inner\NodeCache::has
* @covers \MensBeam\HTML\DOM\Inner\NodeCache::key
* @covers \MensBeam\HTML\DOM\Inner\NodeCache::set
* @covers \MensBeam\HTML\DOM\Inner\Reflection::createFromProtectedConstructor
*/
function testMethod_constructor__errors(\Closure $closure, int $errorCode): void {
$this->expectException(XPathException::class);
$this->expectExceptionCode($errorCode);
$closure();
}
/**
* @covers \MensBeam\HTML\DOM\XPathExpression::evaluate
*
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::load
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\Node::__construct
* @covers \MensBeam\HTML\DOM\Node::getInnerDocument
* @covers \MensBeam\HTML\DOM\Node::hasChildNodes
* @covers \MensBeam\HTML\DOM\Node::postParsingTemplatesFix
* @covers \MensBeam\HTML\DOM\XPathEvaluate::xpathEvaluate
* @covers \MensBeam\HTML\DOM\XPathEvaluatorBase::createExpression
* @covers \MensBeam\HTML\DOM\XPathExpression::__construct
* @covers \MensBeam\HTML\DOM\XPathResult::__construct
* @covers \MensBeam\HTML\DOM\XPathResult::__get_booleanValue
* @covers \MensBeam\HTML\DOM\Inner\Document::__construct
* @covers \MensBeam\HTML\DOM\Inner\Document::__get_xpath
* @covers \MensBeam\HTML\DOM\Inner\NodeCache::has
* @covers \MensBeam\HTML\DOM\Inner\NodeCache::key
* @covers \MensBeam\HTML\DOM\Inner\NodeCache::set
* @covers \MensBeam\HTML\DOM\Inner\Reflection::createFromProtectedConstructor
* @covers \MensBeam\HTML\DOM\Inner\Reflection::getProtectedProperty
*/
function testMethod_evaluate(): void {
$d = new Document('<!DOCTYPE html><html><body><span><span>Ook</span></span><span></span></body></html>');
$e = $d->createExpression('//span');
$this->assertTrue($e->evaluate($d, XPathResult::BOOLEAN_TYPE)->booleanValue);
}
}

149
tests/cases/TestXPathResult.php

@ -0,0 +1,149 @@
<?php
/**
* @license MIT
* Copyright 2017 Dustin Wilson, J. King, et al.
* See LICENSE and AUTHORS files for details
*/
declare(strict_types=1);
namespace MensBeam\HTML\DOM\TestCase;
use MensBeam\HTML\DOM\{
Document,
HTMLElement,
Node,
XPathException,
XPathResult
};
/** @covers \MensBeam\HTML\DOM\XPathResult */
class TestXPathResult extends \PHPUnit\Framework\TestCase {
function testIteration(): void {
$d = new Document('<!DOCTYPE html><html><body><span><span>Ook</span></span><span></span></body></html>');
$result = $d->evaluate('.//span', $d);
foreach ($result as $k => $r) {
$this->assertSame(HTMLElement::class, $r::class);
$this->assertSame(HTMLElement::class, $result[$k]::class);
}
}
function testMethod_iterateNext(): void {
$d = new Document('<!DOCTYPE html><html><body><span></span></body></html>');
$result = $d->evaluate('.//span', $d);
$this->assertSame($d->getElementsByTagName('span')[0], $result->iterateNext());
$this->assertNull($result->iterateNext());
}
function testMethod_iterateNext__errors(): void {
$this->expectException(XPathException::class);
$this->expectExceptionCode(XPathException::TYPE_ERROR);
$d = new Document();
$d->evaluate('.//fail', $d, null, XPathResult::ORDERED_NODE_SNAPSHOT_TYPE)->iterateNext();
}
function testMethod_snapshotItem(): void {
$d = new Document('<!DOCTYPE html><html><body><span></span></body></html>');
$this->assertSame($d->getElementsByTagName('span')[0], $d->evaluate('.//span', $d, null, XPathResult::ORDERED_NODE_SNAPSHOT_TYPE)->snapshotItem(0));
$this->assertNull($d->evaluate('.//span', $d, null, XPathResult::ORDERED_NODE_SNAPSHOT_TYPE)->snapshotItem(42));
}
function testMethod_snapshotItem__errors(): void {
$this->expectException(XPathException::class);
$this->expectExceptionCode(XPathException::TYPE_ERROR);
$d = new Document();
$d->evaluate('.//fail', $d)->snapshotItem(0);
}
function testMethod_validateStorage__errors(): void {
$this->expectException(XPathException::class);
$this->expectExceptionCode(XPathException::TYPE_ERROR);
$d = new Document();
$result = $d->evaluate('count(.//fail)', $d, null, XPathResult::NUMBER_TYPE);
$result[0];
}
function provideProperty__errors(): iterable {
return [
[ function() {
$d = new Document();
$result = $d->evaluate('name(//fail)', $d, null, XPathResult::NUMBER_TYPE);
$result->booleanValue;
} ],
[ function() {
$d = new Document();
$result = $d->evaluate('//fail', $d, null, XPathResult::BOOLEAN_TYPE);
$result->numberValue;
} ],
[ function() {
$d = new Document();
$result = $d->evaluate('//fail', $d, null, XPathResult::BOOLEAN_TYPE);
$result->singleNodeValue;
} ],
[ function() {
$d = new Document();
$result = $d->evaluate('//fail', $d, null, XPathResult::BOOLEAN_TYPE);
$result->stringValue;
} ]
];
}
/** @dataProvider provideProperty__errors */
function testProperty__errors(\Closure $closure): void {
$this->expectException(XPathException::class);
$this->expectExceptionCode(XPathException::TYPE_ERROR);
$closure();
}
function testProperty_invalidIteratorState(): void {
$d = new Document();
$this->assertFalse($d->evaluate('//span', $d)->invalidIteratorState);
}
function testProperty_offsetSet_offsetUnset(): void {
$d = new Document('<!DOCTYPE html><html><body><span><span>Ook</span></span><span></span></body></html>');
$result = $d->evaluate('.//span', $d);
$result[1] = 'ook';
$this->assertNotSame('ook', $result[1]);
unset($result[1]);
$this->assertSame(HTMLElement::class, $result[1]::class);
}
function testProperty_resultType(): void {
$d = new Document('<!DOCTYPE html><html><body><span><span>Ook</span></span><span></span></body></html>');
$this->assertEquals(XPathResult::ORDERED_NODE_ITERATOR_TYPE, $d->evaluate('.//span', $d->body, null, XPathResult::ORDERED_NODE_ITERATOR_TYPE)->resultType);
$this->assertEquals(XPathResult::ORDERED_NODE_ITERATOR_TYPE, $d->evaluate('.//span', $d->body)->resultType);
$this->assertEquals(XPathResult::NUMBER_TYPE, $d->evaluate('count(.//span)', $d->body, null, XPathResult::NUMBER_TYPE)->resultType);
$this->assertEquals(XPathResult::NUMBER_TYPE, $d->evaluate('count(.//span)', $d->body)->resultType);
$this->assertEquals(XPathResult::STRING_TYPE, $d->evaluate('name(.//span)', $d->body, null, XPathResult::STRING_TYPE)->resultType);
$this->assertEquals(XPathResult::STRING_TYPE, $d->evaluate('name(.//span)', $d->body)->resultType);
$this->assertEquals(XPathResult::BOOLEAN_TYPE, $d->evaluate('.//span', $d->body, null, XPathResult::BOOLEAN_TYPE)->resultType);
$this->assertEquals(XPathResult::BOOLEAN_TYPE, $d->evaluate('not(.//span)', $d->body, null, XPathResult::BOOLEAN_TYPE)->resultType);
$this->assertEquals(XPathResult::BOOLEAN_TYPE, $d->evaluate('not(.//span)', $d->body)->resultType);
$this->assertEquals(XPathResult::UNORDERED_NODE_SNAPSHOT_TYPE, $d->evaluate('.//span', $d->body, null, XPathResult::UNORDERED_NODE_SNAPSHOT_TYPE)->resultType);
$this->assertEquals(XPathResult::FIRST_ORDERED_NODE_TYPE, $d->evaluate('.//span', $d->body, null, XPathResult::FIRST_ORDERED_NODE_TYPE)->resultType);
}
function testProperty_snapshotLength(): void {
$d = new Document('<!DOCTYPE html><html><body><span><span>Ook</span></span><span></span></body></html>');
$this->assertEquals(3, $d->evaluate('.//span', $d, null, XPathResult::ORDERED_NODE_SNAPSHOT_TYPE)->snapshotLength);
}
function testProperty_snapshotLength__errors(): void {
$this->expectException(XPathException::class);
$this->expectExceptionCode(XPathException::TYPE_ERROR);
$d = new Document();
$d->evaluate('.//fail', $d)->snapshotLength;
}
}

2
tests/phpunit.dist.xml

@ -36,6 +36,8 @@
<file>cases/TestXMLDocument.php</file>
<file>cases/TestXPathEvaluate.php</file>
<file>cases/TestXPathEvaluator.php</file>
<file>cases/TestXPathExpression.php</file>
<file>cases/TestXPathResult.php</file>
</testsuite>
<testsuite name="Serializer">
<file>cases/TestSerializer.php</file>

Loading…
Cancel
Save