From 38e5816c21139536b4c3cef136b68ce678920567 Mon Sep 17 00:00:00 2001 From: Dustin Wilson Date: Tue, 23 Nov 2021 16:55:23 -0600 Subject: [PATCH] Added removal of attributes back in, querySelector --- composer.json | 2 +- lib/DOMTokenList.php | 35 +++++-- lib/Element.php | 62 +++++++++++- lib/Inner/Document.php | 9 ++ lib/ParentNode.php | 53 ++++++++++- tests/cases/TestDOMTokenList.php | 156 ++++++++++++++++++++++++++++++- tests/cases/TestElement.php | 134 ++++++++++++++++++++++++++ tests/cases/TestParentNode.php | 86 +++++++++++++++++ tests/phpunit.dist.xml | 1 + 9 files changed, 522 insertions(+), 16 deletions(-) create mode 100644 tests/cases/TestParentNode.php diff --git a/composer.json b/composer.json index 0654519..c99eac1 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Modern DOM library written in PHP for HTML documents", "type": "library", "require": { - "php": ">=8.0", + "php": ">=8.0.2", "ext-dom": "*", "mensbeam/html-parser": "dev-master", "mensbeam/framework": "dev-main", diff --git a/lib/DOMTokenList.php b/lib/DOMTokenList.php index 8904ab3..b2476e7 100644 --- a/lib/DOMTokenList.php +++ b/lib/DOMTokenList.php @@ -15,11 +15,11 @@ class DOMTokenList implements \ArrayAccess, \Countable, \Iterator { use MagicProperties; - protected string $localName; protected \WeakReference $element; - protected int $_length = 0; + protected string $localName; protected int $position = 0; + protected array $supportedTokens; # A DOMTokenList object has an associated token set (a set), which is initially # empty. protected array $tokenSet = []; @@ -47,9 +47,12 @@ class DOMTokenList implements \ArrayAccess, \Countable, \Iterator { } - protected function __construct(Element $element, string $attributeLocalName) { + protected function __construct(Element $element, string $attributeLocalName, array $supportedTokens = []) { # A DOMTokenList object also has an associated element and an attribute’s local # name. + // Apparently the "attribute's local name" has an associated set of supported + // tokens, but the specification is extremely vague on how this is supposed to + // be done. Going to have a list of supported tokens as a parameter. # When a DOMTokenList object is created, then: # @@ -60,6 +63,7 @@ class DOMTokenList implements \ArrayAccess, \Countable, \Iterator { $this->element = \WeakReference::create($element); # 2. Let localName be associated attribute’s local name. $this->localName = $attributeLocalName; + $this->supportedTokens = $supportedTokens; # 3. Let value be the result of getting an attribute value given element and # localName. $element = Reflection::getProtectedProperty($element, 'innerNode'); @@ -106,6 +110,8 @@ class DOMTokenList implements \ArrayAccess, \Countable, \Iterator { # 2. For each token in tokens, append token to this’s token set. foreach ($tokens as $token) { + # To append to an ordered set: if the set contains the given item, then do + # nothing; otherwise, perform the normal list append operation. if (!in_array($token, $this->tokenSet)) { $this->tokenSet[] = $token; $this->_length++; @@ -239,14 +245,27 @@ class DOMTokenList implements \ArrayAccess, \Countable, \Iterator { # # 1. If the associated attribute’s local name does not define supported tokens, # throw a TypeError. + if (count($this->supportedTokens) === 0) { + trigger_error('Type error; there are no defined supported tokens', \E_USER_ERROR); + } + + // This part cannot be covered until there's something in the standard which + // defines supported tokens. HTMLMediaElement::controlsList is a non-standard + // method which does define supported tokens, but until it is standardized it + // won't be added in this implementation. + + // @codeCoverageIgnoreStart # 2. Let lowercase token be a copy of token, in ASCII lowercase. + $lowercaseToken = strtolower($token); + # 3. If lowercase token is present in supported tokens, return true. - # 4. Return false. + if (in_array($lowercaseToken, $this->supportedTokens)) { + return true; + } - // This class is presently only used for Element::classList, and it supports any - // valid class name as a token. So, there's nothing to do here at the moment. - // Just return true. - return true; + # 4. Return false. + return false; + // @codeCoverageIgnoreEnd } public function toggle(string $token, ?bool $force = null): bool { diff --git a/lib/Element.php b/lib/Element.php index 3a0348b..c15e68f 100644 --- a/lib/Element.php +++ b/lib/Element.php @@ -11,7 +11,9 @@ use MensBeam\HTML\DOM\Inner\{ Document as InnerDocument, Reflection }; -use MensBeam\HTML\Parser; +use MensBeam\HTML\Parser, + Symfony\Component\CssSelector\CssSelectorConverter, + Symfony\Component\CssSelector\Exception\SyntaxErrorException as SymfonySyntaxErrorException; class Element extends Node { @@ -84,6 +86,16 @@ class Element extends Node { } + public function closest(string $selectors): ?Element { + # The closest(selectors) method steps are: + + # 1. Let s be the result of parse a selector from selectors. [SELECTORS4] + # 2. If s is failure, throw a "SyntaxError" DOMException. + # 3. Let elements be this’s inclusive ancestors that are elements, in reverse tree order. + # 4. For each element in elements, if match a selector against an element, using s, element, and :scope element this, returns success, return element. [SELECTORS4] + # 5. Return null. + } + public function getAttribute(string $qualifiedName): ?string { # The getAttribute(qualifiedName) method steps are: # @@ -248,6 +260,54 @@ class Element extends Node { return $this->innerNode->hasAttributes(); } + public function removeAttribute(string $qualifiedName): void { + # The removeAttribute(qualifiedName) method steps are to remove an attribute + # given qualifiedName and this, and then return undefined. + # + # To remove an attribute by name given a qualifiedName and element element, run + # these steps: + + # 1. Let attr be the result of getting an attribute given qualifiedName and + # element. + # 2. If attr is non-null, then remove attr. + # 3. Return attr. + // Going to let PHP's DOM do the heavy lifting here instead + $this->innerNode->removeAttribute($this->coerceName($qualifiedName)); + } + + public function removeAttributeNode(Attr $attr): Attr { + # The removeAttributeNode(attr) method steps are: + # 1. If this’s attribute list does not contain attr, then throw a + # "NotFoundError" DOMException. + // PHP's DOM does this already. Will catch its exception and rethrow as HTML-DOM + // DOMException. + + # 2. Remove attr. + try { + $this->innerNode->removeAttributeNode(Reflection::getProtectedProperty($attr, 'innerNode')); + } catch (\DOMException $e) { + throw new DOMException($e->code); + } + + # 3. Return attr. + return $attr; + } + + public function removeAttributeNS(?string $namespace, string $localName): void { + # The removeAttributeNS(namespace, localName) method steps are to remove an + # attribute given namespace, localName, and this, and then return undefined. + # + # To remove an attribute by namespace and local name given a namespace, + # localName, and element element, run these steps: + + # 1. Let attr be the result of getting an attribute given namespace, localName, + # and element. + # 2. If attr is non-null, then remove attr. + # 3. Return attr. + // Going to let PHP's DOM do the heavy lifting here instead + $this->innerNode->removeAttributeNS($namespace, $this->coerceName($localName)); + } + public function setAttribute(string $qualifiedName, string $value): void { # 1. If qualifiedName does not match the Name production in XML, then throw an # "InvalidCharacterError" DOMException. diff --git a/lib/Inner/Document.php b/lib/Inner/Document.php index 8c2074f..c722249 100644 --- a/lib/Inner/Document.php +++ b/lib/Inner/Document.php @@ -26,11 +26,20 @@ class Document extends \DOMDocument { protected NodeCache $nodeCache; protected \WeakReference $_wrapperNode; + protected ?\DOMXPath $_xpath = null; protected function __get_wrapperNode(): WrapperNode { return $this->_wrapperNode->get(); } + protected function __get_xpath(): \DOMXPath { + if ($this->_xpath === null) { + $this->_xpath = new \DOMXPath($this); + } + + return $this->_xpath; + } + private static ?string $parentNamespace = null; diff --git a/lib/ParentNode.php b/lib/ParentNode.php index 2f92265..9542c2b 100644 --- a/lib/ParentNode.php +++ b/lib/ParentNode.php @@ -11,9 +11,26 @@ use MensBeam\HTML\DOM\Inner\{ Document as InnerDocument, Reflection }; +use Symfony\Component\CssSelector\CssSelectorConverter, + Symfony\Component\CssSelector\Exception\SyntaxErrorException as SymfonySyntaxErrorException; trait ParentNode { + public function querySelector(string $selectors): ?Element { + # The querySelector(selectors) method steps are to return the first result of + # running scope-match a selectors string selectors against this, if the result + # is not an empty list; otherwise null. + $nodeList = $this->scopeMatchSelector($selectors); + return ($nodeList->length > 0) ? $this->getInnerDocument()->getWrapperNode($nodeList[0]) : null; + } + + public function querySelectorAll(string $selectors): NodeList { + # The querySelectorAll(selectors) method steps are to return the static result + # of running scope-match a selectors string selectors against this. + $nodeList = $this->scopeMatchSelector($selectors); + return Reflection::createFromProtectedConstructor(__NAMESPACE__ . '\\NodeList', $this->getInnerDocument(), $nodeList); + } + /** * Generator which walks down the DOM from the node the method is being run on. * Non-standard. @@ -34,17 +51,17 @@ trait ParentNode { do { $next = $node->nextSibling; $wrapperNode = $doc->getWrapperNode($node); - $result = ($filter === null) ? Node::WALK_FILTER_ACCEPT : $filter($wrapperNode); + $result = ($filter === null) ? Node::WALK_ACCEPT : $filter($wrapperNode); switch ($result) { - case Node::WALK_FILTER_ACCEPT: + case Node::WALK_ACCEPT: yield $wrapperNode; break; - case Node::WALK_FILTER_ACCEPT | Node::WALK_FILTER_SKIP_CHILDREN: + case Node::WALK_ACCEPT | Node::WALK_SKIP_CHILDREN: yield $wrapperNode; - case Node::WALK_FILTER_REJECT | Node::WALK_FILTER_SKIP_CHILDREN: + case Node::WALK_REJECT | Node::WALK_SKIP_CHILDREN: continue 2; - case Node::WALK_FILTER_REJECT: + case Node::WALK_REJECT: break; default: return; } @@ -57,6 +74,32 @@ trait ParentNode { } + protected function scopeMatchSelector(string $selectors): \DOMNodeList { + # To scope-match a selectors string selectors against a node, run these steps: + # 1. Let s be the result of parse a selector selectors. [SELECTORS4] + // This implementation will instead convert the CSS selector to an XPath query + // using Symfony's CSS selector converter library. + try { + $converter = new CssSelectorConverter(); + $s = $converter->toXPath($selectors); + } catch (\Exception $e) { + # 2. If s is failure, then throw a "SyntaxError" DOMException. + // Symfony's library will throw an exception if something is unsupported, too, + // so only throw exception when an actual syntax error, otherwise return an + // empty nodelist. + if ($e instanceof SymfonySyntaxErrorException) { + throw new DOMException(DOMException::SYNTAX_ERROR); + } + + return new \DOMNodeList; + } + + # 3. Return the result of match a selector against a tree with s and node’s root + # using scoping root node. [SELECTORS4]. + $nodeList = $this->getInnerDocument()->xpath->query($s, $this->innerNode); + return $nodeList; + } + protected function walkInner(\DOMNode $node, ?\Closure $filter = null, bool $includeReferenceNode = false): \Generator { if (!$node instanceof DocumentFragment && !$includeReferenceNode) { $node = $node->firstChild; diff --git a/tests/cases/TestDOMTokenList.php b/tests/cases/TestDOMTokenList.php index a181903..3e1929a 100644 --- a/tests/cases/TestDOMTokenList.php +++ b/tests/cases/TestDOMTokenList.php @@ -361,14 +361,93 @@ class TestDOMTokenList extends \PHPUnit\Framework\TestCase { } + /** + * @covers \MensBeam\HTML\DOM\DOMTokenList::supports + * + * @covers \MensBeam\HTML\DOM\Document::__construct + * @covers \MensBeam\HTML\DOM\Document::createElement + * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct + * @covers \MensBeam\HTML\DOM\DOMTokenList::__construct + * @covers \MensBeam\HTML\DOM\DOMTokenList::__toString + * @covers \MensBeam\HTML\DOM\DOMTokenList::add + * @covers \MensBeam\HTML\DOM\DOMTokenList::parseOrderedSet + * @covers \MensBeam\HTML\DOM\DOMTokenList::update + * @covers \MensBeam\HTML\DOM\Element::__construct + * @covers \MensBeam\HTML\DOM\Element::__get_classList + * @covers \MensBeam\HTML\DOM\Node::__construct + * @covers \MensBeam\HTML\DOM\Node::appendChild + * @covers \MensBeam\HTML\DOM\Node::getInnerDocument + * @covers \MensBeam\HTML\DOM\Node::getInnerNode + * @covers \MensBeam\HTML\DOM\Node::getRootNode + * @covers \MensBeam\HTML\DOM\Node::postInsertionBugFixes + * @covers \MensBeam\HTML\DOM\Node::preInsertionBugFixes + * @covers \MensBeam\HTML\DOM\Node::preInsertionValidity + * @covers \MensBeam\HTML\DOM\Inner\Document::__construct + * @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 + */ public function testMethod_supports(): void { + // PHPUnit is supposed to support expecting of errors, but it doesn't. So let's + // write a bunch of bullshit so we can catch and assert errors instead. + set_error_handler(function($errno) { + if ($errno === \E_USER_ERROR) { + $this->assertEquals(\E_USER_ERROR, $errno); + } + }); + $d = new Document(); $e = $d->appendChild($d->createElement('html')); $e->classList->add('ook', 'eek', 'ack', 'ookeek'); - $this->assertTrue($e->classList->supports('ack')); + $e->classList->supports('ack'); + + restore_error_handler(); } + /** + * @covers \MensBeam\HTML\DOM\DOMTokenList::toggle + * + * @covers \MensBeam\HTML\DOM\Attr::__get_value + * @covers \MensBeam\HTML\DOM\Document::__construct + * @covers \MensBeam\HTML\DOM\Document::createElement + * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct + * @covers \MensBeam\HTML\DOM\DOMTokenList::__construct + * @covers \MensBeam\HTML\DOM\DOMTokenList::__get_value + * @covers \MensBeam\HTML\DOM\DOMTokenList::__toString + * @covers \MensBeam\HTML\DOM\DOMTokenList::add + * @covers \MensBeam\HTML\DOM\DOMTokenList::parseOrderedSet + * @covers \MensBeam\HTML\DOM\DOMTokenList::remove + * @covers \MensBeam\HTML\DOM\DOMTokenList::update + * @covers \MensBeam\HTML\DOM\Element::__construct + * @covers \MensBeam\HTML\DOM\Element::__get_classList + * @covers \MensBeam\HTML\DOM\Element::__get_namespaceURI + * @covers \MensBeam\HTML\DOM\Element::getAttribute + * @covers \MensBeam\HTML\DOM\Element::getAttributeNode + * @covers \MensBeam\HTML\DOM\Element::setAttribute + * @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::getRootNode + * @covers \MensBeam\HTML\DOM\Node::postInsertionBugFixes + * @covers \MensBeam\HTML\DOM\Node::preInsertionBugFixes + * @covers \MensBeam\HTML\DOM\Node::preInsertionValidity + * @covers \MensBeam\HTML\DOM\Inner\Document::__construct + * @covers \MensBeam\HTML\DOM\Inner\Document::__get_wrapperNode + * @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 + */ public function testMethod_toggle(): void { $d = new Document(); $e = $d->appendChild($d->createElement('html')); @@ -391,6 +470,42 @@ class TestDOMTokenList extends \PHPUnit\Framework\TestCase { } + /** + * @covers \MensBeam\HTML\DOM\Document::__construct + * @covers \MensBeam\HTML\DOM\Document::createElement + * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct + * @covers \MensBeam\HTML\DOM\DOMTokenList::__construct + * @covers \MensBeam\HTML\DOM\DOMTokenList::__toString + * @covers \MensBeam\HTML\DOM\DOMTokenList::add + * @covers \MensBeam\HTML\DOM\DOMTokenList::current + * @covers \MensBeam\HTML\DOM\DOMTokenList::item + * @covers \MensBeam\HTML\DOM\DOMTokenList::key + * @covers \MensBeam\HTML\DOM\DOMTokenList::next + * @covers \MensBeam\HTML\DOM\DOMTokenList::offsetExists + * @covers \MensBeam\HTML\DOM\DOMTokenList::offsetGet + * @covers \MensBeam\HTML\DOM\DOMTokenList::parseOrderedSet + * @covers \MensBeam\HTML\DOM\DOMTokenList::rewind + * @covers \MensBeam\HTML\DOM\DOMTokenList::update + * @covers \MensBeam\HTML\DOM\DOMTokenList::valid + * @covers \MensBeam\HTML\DOM\Element::__construct + * @covers \MensBeam\HTML\DOM\Element::__get_classList + * @covers \MensBeam\HTML\DOM\Node::__construct + * @covers \MensBeam\HTML\DOM\Node::appendChild + * @covers \MensBeam\HTML\DOM\Node::getInnerDocument + * @covers \MensBeam\HTML\DOM\Node::getInnerNode + * @covers \MensBeam\HTML\DOM\Node::getRootNode + * @covers \MensBeam\HTML\DOM\Node::postInsertionBugFixes + * @covers \MensBeam\HTML\DOM\Node::preInsertionBugFixes + * @covers \MensBeam\HTML\DOM\Node::preInsertionValidity + * @covers \MensBeam\HTML\DOM\Inner\Document::__construct + * @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 + */ public function testProcess_iteration(): void { $d = new Document(); $e = $d->appendChild($d->createElement('html')); @@ -404,6 +519,45 @@ class TestDOMTokenList extends \PHPUnit\Framework\TestCase { } + /** + * @covers \MensBeam\HTML\DOM\DOMTokenList::__get_value + * @covers \MensBeam\HTML\DOM\DOMTokenList::__set_value + * + * @covers \MensBeam\HTML\DOM\Attr::__get_value + * @covers \MensBeam\HTML\DOM\Document::__construct + * @covers \MensBeam\HTML\DOM\Document::createElement + * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct + * @covers \MensBeam\HTML\DOM\DOMTokenList::__construct + * @covers \MensBeam\HTML\DOM\DOMTokenList::__toString + * @covers \MensBeam\HTML\DOM\DOMTokenList::add + * @covers \MensBeam\HTML\DOM\DOMTokenList::item + * @covers \MensBeam\HTML\DOM\DOMTokenList::offsetGet + * @covers \MensBeam\HTML\DOM\DOMTokenList::parseOrderedSet + * @covers \MensBeam\HTML\DOM\DOMTokenList::update + * @covers \MensBeam\HTML\DOM\Element::__construct + * @covers \MensBeam\HTML\DOM\Element::__get_classList + * @covers \MensBeam\HTML\DOM\Element::__get_namespaceURI + * @covers \MensBeam\HTML\DOM\Element::getAttribute + * @covers \MensBeam\HTML\DOM\Element::getAttributeNode + * @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::getRootNode + * @covers \MensBeam\HTML\DOM\Node::postInsertionBugFixes + * @covers \MensBeam\HTML\DOM\Node::preInsertionBugFixes + * @covers \MensBeam\HTML\DOM\Node::preInsertionValidity + * @covers \MensBeam\HTML\DOM\Inner\Document::__construct + * @covers \MensBeam\HTML\DOM\Inner\Document::__get_wrapperNode + * @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 + */ public function testProperty_value(): void { // Test it with and without an attached document element $d = new Document(); diff --git a/tests/cases/TestElement.php b/tests/cases/TestElement.php index eded228..d4c8a1e 100644 --- a/tests/cases/TestElement.php +++ b/tests/cases/TestElement.php @@ -261,6 +261,140 @@ class TestElement extends \PHPUnit\Framework\TestCase { } + /** + * @covers \MensBeam\HTML\DOM\Element::removeAttribute + * + * @covers \MensBeam\HTML\DOM\Collection::__construct + * @covers \MensBeam\HTML\DOM\Collection::__get_length + * @covers \MensBeam\HTML\DOM\Collection::count + * @covers \MensBeam\HTML\DOM\Collection::item + * @covers \MensBeam\HTML\DOM\Document::__construct + * @covers \MensBeam\HTML\DOM\Document::load + * @covers \MensBeam\HTML\DOM\DocumentOrElement::getElementsByTagNameNS + * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct + * @covers \MensBeam\HTML\DOM\Element::__construct + * @covers \MensBeam\HTML\DOM\Element::__get_attributes + * @covers \MensBeam\HTML\DOM\HTMLCollection::item + * @covers \MensBeam\HTML\DOM\HTMLCollection::offsetGet + * @covers \MensBeam\HTML\DOM\NamedNodeMap::__construct + * @covers \MensBeam\HTML\DOM\Node::__construct + * @covers \MensBeam\HTML\DOM\Node::getInnerDocument + * @covers \MensBeam\HTML\DOM\Node::hasChildNodes + * @covers \MensBeam\HTML\DOM\Inner\Document::__construct + * @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 + */ + public function testMethod_removeAttribute() { + $d = new Document('', 'UTF-8'); + $svg = $d->getElementsByTagNameNS(Node::SVG_NAMESPACE, 'svg')[0]; + + // Trying to remove namespaced attribute + $svg->removeAttribute('xmlns'); + $this->assertEquals(4, $svg->attributes->length); + // Removing attribute + $svg->removeAttribute('viewBox'); + $this->assertEquals(3, $svg->attributes->length); + // Removing coerced attribute + $svg->removeAttribute('poop💩'); + $this->assertEquals(2, $svg->attributes->length); + } + + + public function testMethod_removeAttributeNode() { + $d = new Document('', 'UTF-8'); + $svg = $d->getElementsByTagNameNS(Node::SVG_NAMESPACE, 'svg')[0]; + // Parser per the spec doesn't parse xmlns prefixed attributes except xlink, so let's add one manually instead to test coercion. + $svg->setAttributeNS(Node::XMLNS_NAMESPACE, 'xmlns:poop💩', 'https://poop💩.poop'); + + $xmlns = $svg->getAttributeNodeNS(Node::XMLNS_NAMESPACE, 'xmlns'); + $xlink = $svg->getAttributeNodeNS(Node::XMLNS_NAMESPACE, 'xlink'); + $poop = $svg->getAttributeNodeNS(Node::XMLNS_NAMESPACE, 'poop💩'); + $viewBox = $svg->getAttributeNode('viewBox'); + + $svg->removeAttributeNode($xmlns); + $this->assertEquals(3, $svg->attributes->length); + $svg->removeAttributeNode($xlink); + $this->assertEquals(2, $svg->attributes->length); + $svg->removeAttributeNode($poop); + $this->assertEquals(1, $svg->attributes->length); + $svg->removeAttributeNode($viewBox); + $this->assertEquals(0, $svg->attributes->length); + } + + + public function testMethod_removeAttributeNode__errors() { + $this->expectException(DOMException::class); + $this->expectExceptionCode(DOMException::NOT_FOUND); + $d = new Document(); + $documentElement = $d->appendChild($d->createElement('html')); + $documentElement->removeAttributeNode($d->createAttribute('shit')); + } + + + /** + * @covers \MensBeam\HTML\DOM\Element::removeAttributeNS + * + * @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\Collection::__construct + * @covers \MensBeam\HTML\DOM\Collection::__get_length + * @covers \MensBeam\HTML\DOM\Collection::count + * @covers \MensBeam\HTML\DOM\Collection::item + * @covers \MensBeam\HTML\DOM\Document::__construct + * @covers \MensBeam\HTML\DOM\Document::__get_documentElement + * @covers \MensBeam\HTML\DOM\Document::createAttributeNS + * @covers \MensBeam\HTML\DOM\Document::load + * @covers \MensBeam\HTML\DOM\DocumentOrElement::getElementsByTagNameNS + * @covers \MensBeam\HTML\DOM\DocumentOrElement::validateAndExtract + * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct + * @covers \MensBeam\HTML\DOM\Element::__construct + * @covers \MensBeam\HTML\DOM\Element::__get_attributes + * @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\HTMLCollection::item + * @covers \MensBeam\HTML\DOM\HTMLCollection::offsetGet + * @covers \MensBeam\HTML\DOM\NamedNodeMap::__construct + * @covers \MensBeam\HTML\DOM\Node::__construct + * @covers \MensBeam\HTML\DOM\Node::__get_ownerDocument + * @covers \MensBeam\HTML\DOM\Node::getInnerDocument + * @covers \MensBeam\HTML\DOM\Node::getInnerNode + * @covers \MensBeam\HTML\DOM\Node::hasChildNodes + * @covers \MensBeam\HTML\DOM\Inner\Document::__construct + * @covers \MensBeam\HTML\DOM\Inner\Document::__get_wrapperNode + * @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 + */ + public function testMethod_removeAttributeNS() { + $d = new Document('', 'UTF-8'); + $svg = $d->getElementsByTagNameNS(Node::SVG_NAMESPACE, 'svg')[0]; + // Parser per the spec doesn't parse xmlns prefixed attributes except xlink, so let's add one manually instead to test coercion. + $svg->setAttributeNS(Node::XMLNS_NAMESPACE, 'xmlns:poop💩', 'https://poop💩.poop'); + + // Remove null namespaced attribute + $svg->removeAttributeNS(null, 'viewBox'); + $this->assertEquals(3, $svg->attributes->length); + // Remove namespaced attribute + $svg->removeAttributeNS(Node::XMLNS_NAMESPACE, 'xmlns'); + $this->assertEquals(2, $svg->attributes->length); + // Remove coerced namespaced attribute + $svg->removeAttributeNS(Node::XMLNS_NAMESPACE, 'poop💩'); + $this->assertEquals(1, $svg->attributes->length); + } + + /** * @covers \MensBeam\HTML\DOM\Element::setAttribute * diff --git a/tests/cases/TestParentNode.php b/tests/cases/TestParentNode.php new file mode 100644 index 0000000..d817f20 --- /dev/null +++ b/tests/cases/TestParentNode.php @@ -0,0 +1,86 @@ +
ook
eek
'); + $div = $d->body->querySelector('div'); + $this->assertSame('div', $div->tagName); + $this->assertNull($d->querySelector('body::before')); + + $divs = $d->body->querySelectorAll('div'); + $this->assertEquals(2, $divs->length); + $this->assertSame('eek', $divs[1]->getAttribute('id')); + $this->assertNull($d->querySelector('.ook')); + $this->assertEquals(0, $d->querySelectorAll('body::before')->length); + } + + + /** + * @covers \MensBeam\HTML\DOM\ParentNode::querySelector + * + * @covers \MensBeam\HTML\DOM\Document::__construct + * @covers \MensBeam\HTML\DOM\DOMException::__construct + * @covers \MensBeam\HTML\DOM\DOMImplementation::__construct + * @covers \MensBeam\HTML\DOM\Node::__construct + * @covers \MensBeam\HTML\DOM\ParentNode::scopeMatchSelector + * @covers \MensBeam\HTML\DOM\Inner\Document::__construct + */ + public function testMethod_querySelector__errors(): void { + $this->expectException(DOMException::class); + $this->expectExceptionCode(DOMException::SYNTAX_ERROR); + $d = new Document(); + $d->querySelector('fail?'); + } +} \ No newline at end of file diff --git a/tests/phpunit.dist.xml b/tests/phpunit.dist.xml index d84e8d0..a491bff 100644 --- a/tests/phpunit.dist.xml +++ b/tests/phpunit.dist.xml @@ -21,6 +21,7 @@ cases/TestDOMTokenList.php cases/TestElement.php cases/TestNode.php + cases/TestParentNode.php