Browse Source

Added removal of attributes back in, querySelector

wrapper-classes
Dustin Wilson 2 years ago
parent
commit
38e5816c21
  1. 2
      composer.json
  2. 35
      lib/DOMTokenList.php
  3. 62
      lib/Element.php
  4. 9
      lib/Inner/Document.php
  5. 53
      lib/ParentNode.php
  6. 156
      tests/cases/TestDOMTokenList.php
  7. 134
      tests/cases/TestElement.php
  8. 86
      tests/cases/TestParentNode.php
  9. 1
      tests/phpunit.dist.xml

2
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",

35
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 {

62
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.

9
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;

53
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;

156
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();

134
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('<!DOCTYPE html><html><head></head><body><svg xmlns="' . Node::SVG_NAMESPACE . '" xmlns:xlink="' . Node::XLINK_NAMESPACE . '" viewBox="0 0 42 42" poop💩="jeff"></svg></body></html>', '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('<!DOCTYPE html><html><head></head><body><svg xmlns="' . Node::SVG_NAMESPACE . '" xmlns:xlink="' . Node::XLINK_NAMESPACE . '" viewBox="0 0 42 42"></svg></body></html>', '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('<!DOCTYPE html><html><head></head><body><svg xmlns="' . Node::SVG_NAMESPACE . '" xmlns:xlink="' . Node::XLINK_NAMESPACE . '" viewBox="0 0 42 42"></svg></body></html>', '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
*

86
tests/cases/TestParentNode.php

@ -0,0 +1,86 @@
<?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,
DOMException,
Node
};
/** @covers \MensBeam\HTML\DOM\ParentNode */
class TestParentNode extends \PHPUnit\Framework\TestCase {
/**
* @covers \MensBeam\HTML\DOM\ParentNode::querySelector
* @covers \MensBeam\HTML\DOM\ParentNode::querySelectorAll
*
* @covers \MensBeam\HTML\DOM\Attr::__get_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\Collection::offsetGet
* @covers \MensBeam\HTML\DOM\Document::__construct
* @covers \MensBeam\HTML\DOM\Document::__get_body
* @covers \MensBeam\HTML\DOM\Document::load
* @covers \MensBeam\HTML\DOM\DOMImplementation::__construct
* @covers \MensBeam\HTML\DOM\Element::__construct
* @covers \MensBeam\HTML\DOM\Element::__get_localName
* @covers \MensBeam\HTML\DOM\Element::__get_namespaceURI
* @covers \MensBeam\HTML\DOM\Element::__get_prefix
* @covers \MensBeam\HTML\DOM\Element::__get_tagName
* @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::getInnerDocument
* @covers \MensBeam\HTML\DOM\Node::hasChildNodes
* @covers \MensBeam\HTML\DOM\ParentNode::scopeMatchSelector
* @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
*/
public function testMethod_querySelector_querySelectorAll(): void {
$d = new Document('<!DOCTYPE html><html><body><div>ook</div><div id="eek">eek</div></body></html>');
$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?');
}
}

1
tests/phpunit.dist.xml

@ -21,6 +21,7 @@
<file>cases/TestDOMTokenList.php</file>
<file>cases/TestElement.php</file>
<file>cases/TestNode.php</file>
<file>cases/TestParentNode.php</file>
</testsuite>
</testsuites>
</phpunit>

Loading…
Cancel
Save