From 12d1dba36c603879b51790fb4b54706cc97e7c54 Mon Sep 17 00:00:00 2001 From: Dustin Wilson Date: Wed, 29 Dec 2021 09:34:58 -0600 Subject: [PATCH] More tests for HTMLElement --- composer.json | 7 +- composer.lock | 12 +-- lib/Document.php | 30 +++++++ lib/HTMLElement.php | 96 ++++++++++++++-------- lib/Inner/Document.php | 6 +- tests/cases/TestHTMLElement.php | 133 ++++++++++++++++++++++++++++++- vendor-bin/phpunit/composer.lock | 5 +- vendor-bin/robo/composer.lock | 2 +- 8 files changed, 239 insertions(+), 52 deletions(-) diff --git a/composer.json b/composer.json index f8d03cf..dbd39d9 100644 --- a/composer.json +++ b/composer.json @@ -59,5 +59,10 @@ "type": "git", "url": "mensbeam-gitea:MensBeam/HTML-Parser.git" } - ] + ], + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } + } } diff --git a/composer.lock b/composer.lock index 979162e..2c32d6f 100644 --- a/composer.lock +++ b/composer.lock @@ -1367,16 +1367,16 @@ }, { "name": "scrivo/highlight.php", - "version": "v9.18.1.8", + "version": "v9.18.1.9", "source": { "type": "git", "url": "https://github.com/scrivo/highlight.php.git", - "reference": "6d5049cd2578e19a06adbb6ac77879089be1e3f9" + "reference": "d45585504777e6194a91dffc7270ca39833787f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/6d5049cd2578e19a06adbb6ac77879089be1e3f9", - "reference": "6d5049cd2578e19a06adbb6ac77879089be1e3f9", + "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/d45585504777e6194a91dffc7270ca39833787f8", + "reference": "d45585504777e6194a91dffc7270ca39833787f8", "shasum": "" }, "require": { @@ -1441,7 +1441,7 @@ "type": "github" } ], - "time": "2021-10-24T00:28:14+00:00" + "time": "2021-12-03T06:45:28+00:00" }, { "name": "symfony/console", @@ -2808,5 +2808,5 @@ "ext-dom": "*" }, "platform-dev": [], - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.2.0" } diff --git a/lib/Document.php b/lib/Document.php index 62ca9fa..2be5338 100644 --- a/lib/Document.php +++ b/lib/Document.php @@ -28,6 +28,7 @@ class Document extends Node implements \ArrayAccess { protected string $_characterSet = 'UTF-8'; protected string $_compatMode = 'CSS1Compat'; protected string $_contentType = 'text/html'; + protected bool $designModeEnabled = false; protected DOMImplementation $_implementation; protected string $_URL = 'about:blank'; @@ -68,6 +69,35 @@ class Document extends Node implements \ArrayAccess { return $this->_contentType; } + protected function __get_designMode(): string { + # The designMode getter steps are to return "on" if this's design mode enabled + # is true; otherwise "off". + return ($this->designModeEnabled) ? 'on' : 'off'; + } + + protected function __set_designMode(string $value): void { + # The designMode setter steps are: + + # 1. Let value be the given value, converted to ASCII lowercase. + $value = strtolower($value); + + # 2. If value is "on" and this's design mode enabled is false, then: + if ($value === 'on' && !$this->designModeEnabled) { + # 1. Set this's design mode enabled to true. + $this->designModeEnabled = true; + + # 2. Reset this's active range's start and end boundary points to be at the start of this. + // Ranges aren't implemented. + + # 3. Run the focusing steps for this's document element, if non-null. + // There's nothing to do here; there's no chance for a focusing element. + } + # 3. If value is "off", then set this's design mode enabled to false. + elseif ($value === 'off') { + $this->designModeEnabled = false; + } + } + protected function __get_dir(): string { # The dir IDL attribute on Document objects must reflect the dir content attribute # of the html element, if any, limited to only known values. If there is no such diff --git a/lib/HTMLElement.php b/lib/HTMLElement.php index 40998be..354ffb1 100644 --- a/lib/HTMLElement.php +++ b/lib/HTMLElement.php @@ -25,19 +25,39 @@ class HTMLElement extends Element { # The autocapitalize getter steps are to: # # 1. Let state be the own autocapitalization hint of this. - $state = $this->autoCapitalizationHint($this); + $state = $this->autoCapitalizationHint($this->innerNode); # 2. If state is default, then return the empty string. - if ($state === 'off') { - return ''; - } - # 3. If state is none, then return "none". - if ($state === 'none') { - # 4. If state is sentences, then return "sentences". - # 5. Return the keyword value corresponding to state. + // Switch below will handle all of these steps. + + # The autocapitalize attribute is an enumerated attribute whose states are the + # possible autocapitalization hints. The autocapitalization hint specified by + # the attribute's state combines with other considerations to form the used + # autocapitalization hint, which informs the behavior of the user agent. The + # keywords for this attribute and their state mappings are as follows: + switch ($state) { + case 'default': + return ''; + case 'none': + case 'sentences': + case 'words': + case 'characters': + return $state; + case 'off': + return 'none'; + case 'on': + # The invalid value default is the sentences state. + default: return 'sentences'; + } + } + + protected function __set_autocapitalize(string $value): void { + # The autocapitalize setter steps are to set the autocapitalize content + # attribute to the given value. + $this->setAttribute('autocapitalize', $value); } protected function __get_contentEditable(): string { @@ -63,7 +83,7 @@ class HTMLElement extends Element { # be set to the string "false", and otherwise the attribute setter must throw a # "SyntaxError" DOMException. - $value = strtolower($this->getAttribute('contenteditable')); + $value = strtolower($this->getAttribute('contenteditable') ?? ''); return ($value === 'true' || $value === 'false') ? $value : 'inherit'; } @@ -153,7 +173,7 @@ class HTMLElement extends Element { } $data = $this->getAttribute('data'); - if ($data !== null && in_array(strtolower(substr(strrchr($fileName, '.'), 1)), [ 'apng', 'avif', 'gif', 'jpeg', 'jpg', 'png', 'svg', 'webp' ])) { + if ($data !== null && str_contains(needle: '.', haystack: $data) && in_array(strtolower(substr(strrchr($data, '.'), 1)), [ 'apng', 'avif', 'gif', 'jpeg', 'jpg', 'png', 'svg', 'webp' ])) { return true; } } @@ -196,15 +216,18 @@ class HTMLElement extends Element { protected function __get_hidden(): bool { # The hidden getter steps are to return true if this's visibility state is # "hidden", otherwise false. + // There's no visibility state in this implementation because there's nothing to + // render. The only way for the visibility state to be off is if the element is + // hidden via the hidden attribute. - $value = ($this->getAttribute('hidden') !== null); - if ($value) { + if ($this->hasAttribute('hidden')) { return true; } - $n = $this; + $n = $this->innerNode; + $doc = $n->ownerDocument; while ($n = $n->parentNode) { - if (property_exists($n, 'hidden') && ($n->getAttribute('hidden') !== null)) { + if ($doc->getWrapperNode($n) instanceof HTMLElement && $n->hasAttribute('hidden')) { return true; } } @@ -280,7 +303,7 @@ class HTMLElement extends Element { $this->setAttribute('inputmode', $value); } - protected function __get_isContentEditable(): string { + protected function __get_isContentEditable(): bool { # The isContentEditable IDL attribute, on getting, must return true if the # element is either an editing host or editable, and false otherwise. @@ -294,21 +317,24 @@ class HTMLElement extends Element { # math element, or it is not an Element and its parent is an HTML element. $doc = ($this instanceof Document) ? $this : $this->ownerDocument; - if ($doc->designMode) { + if ($doc->designMode === 'on') { return true; } - $value = strtolower($this->getAttribute('contenteditable')); - if ($value === 'true') { - return true; - } elseif ($value === 'false') { - return false; + $value = $this->getAttribute('contenteditable'); + if ($value !== null) { + $value = strtolower($value); + if ($value === 'true') { + return true; + } elseif ($value === 'false') { + return false; + } } - if ($doc !== this) { - $n = $this; - while ($n = $n->parentNode) { - if (property_exists($n, 'contentEditable') && $n->contentEditable === 'true') { + $n = $this->innerNode; + while ($n = $n->parentNode) { + if ($n instanceof \DOMElement) { + if ($n->getAttribute('contenteditable') === 'true' && $n->ownerDocument->getWrapperNode($n) instanceof HTMLElement) { return true; } } @@ -449,9 +475,10 @@ class HTMLElement extends Element { return false; } - $n = $this; + $n = $this->innerNode; + $doc = $n->ownerDocument; while ($n = $n->parentNode) { - if (property_exists($n, 'translate') && $n->getAttribute('translate') === 'yes') { + if ($n->getAttribute('translate') === 'yes' && $doc->getWrapperNode($n) instanceof HTMLElement) { return true; } } @@ -466,19 +493,19 @@ class HTMLElement extends Element { } - protected function autoCapitalizationHint(HTMLElement $element): string { + protected function autoCapitalizationHint(\DOMElement $element): string { # To compute the own autocapitalization hint of an element element, run the # following steps: # 1. If the autocapitalize content attribute is present on element, and its # value is not the empty string, return the state of the attribute. - $value = $this->getAttribute('autocapitalize'); + $value = $element->getAttribute('autocapitalize'); if ($value !== null && $value !== '') { return $value; } # 2. If element is an autocapitalize-inheriting element and has a non-null form # owner, return the own autocapitalization hint of element's form owner. - elseif (in_array($this->tagName, [ 'button', 'fieldset', 'input', 'output', 'select', 'textarea' ])) { + elseif (in_array($element->tagName, [ 'button', 'fieldset', 'input', 'output', 'select', 'textarea' ])) { # A form-associated element can have a relationship with a form element, which # is called the element's form owner. If a form-associated element is not # associated with a form element, its form owner is said to be null. @@ -487,16 +514,15 @@ class HTMLElement extends Element { # form element (as described below), but, if it is listed, may have a form # attribute specified to override this. - $n = $this; - while ($n = $this->parentNode) { - if ($n instanceof HTMLElement && $n->tagName === 'form') { + $n = $element; + while ($n = $n->parentNode) { + if ($n->tagName === 'form' && $n->ownerDocument->getWrapperNode($n) instanceof HTMLElement) { return $this->autoCapitalizationHint($n); } } } ## 3. Return default. - // The default of this user agent is 'off' - return 'off'; + return 'default'; } } \ No newline at end of file diff --git a/lib/Inner/Document.php b/lib/Inner/Document.php index 5547f9e..3489aa5 100644 --- a/lib/Inner/Document.php +++ b/lib/Inner/Document.php @@ -61,9 +61,9 @@ class Document extends \DOMDocument { public function getWrapperNode(\DOMNode $node): ?WrapperNode { - // If the node is a Document then the wrapperNode is this's wrapperNode - // property. - if ($node instanceof Document) { + // If the node is this document then return the wrapper node; it's already + // known. + if ($node === $this) { return $this->wrapperNode; } diff --git a/tests/cases/TestHTMLElement.php b/tests/cases/TestHTMLElement.php index cf2187e..348fc54 100644 --- a/tests/cases/TestHTMLElement.php +++ b/tests/cases/TestHTMLElement.php @@ -16,6 +16,124 @@ use MensBeam\HTML\DOM\{ /** @covers \MensBeam\HTML\DOM\HTMLElement */ class TestHTMLElement extends \PHPUnit\Framework\TestCase { + public function testProperty_accessKey(): void { + $d = new Document(''); + $ook = $d->getElementById('ook'); + $this->assertSame('o', $ook->accessKey); + $ook->accessKey = 'e'; + $this->assertSame('e', $ook->accessKey); + } + + public function testProperty_autocapitalize(): void { + $d = new Document('
'); + $ook = $d->getElementsByTagName('input')[0]; + $this->assertSame('sentences', $ook->autocapitalize); + $ook->removeAttribute('autocapitalize'); + $this->assertSame('', $ook->autocapitalize); + $ook->autocapitalize = 'words'; + $this->assertSame('words', $ook->autocapitalize); + $ook->autocapitalize = 'off'; + $this->assertSame('none', $ook->autocapitalize); + + $form = $d->getElementsByTagName('form')[0]; + $form->autocapitalize = 'bullshit'; + $ook->removeAttribute('autocapitalize'); + $this->assertSame('sentences', $ook->autocapitalize); + } + + public function testProperty_contentEditable_isContentEditable(): void { + $d = new Document('
'); + $div = $d->getElementsByTagName('div')[0]; + $this->assertSame('inherit', $div->contentEditable); + $this->assertFalse($div->isContentEditable); + $div->contentEditable = 'true'; + $this->assertSame('true', $div->contentEditable); + $this->assertTrue($div->isContentEditable); + $div->contentEditable = 'false'; + $this->assertSame('false', $div->contentEditable); + $this->assertFalse($div->isContentEditable); + $div->contentEditable = 'inherit'; + $this->assertFalse($div->hasAttribute('contenteditable')); + $this->assertSame('inherit', $div->contentEditable); + $div->removeAttribute('contenteditable'); + $d->body->contentEditable = 'true'; + $this->assertSame('inherit', $div->contentEditable); + $this->assertTrue($div->isContentEditable); + + $d->designMode = 'on'; + $this->assertTrue($div->isContentEditable); + } + + public function testProperty_contentEditable__errors(): void { + $this->expectException(DOMException::class); + $this->expectExceptionCode(DOMException::SYNTAX_ERROR); + $d = new Document(''); + $d->documentElement->contentEditable = 'fail'; + } + + public function testProperty_dir(): void { + $d = new Document(''); + $html = $d->documentElement; + $this->assertSame('ltr', $html->dir); + $html->dir = 'bullshit'; + $this->assertSame('', $html->dir); + } + + public function testProperty_draggable(): void { + $d = new Document('
'); + $img = $d->getElementsByTagName('img')[0]; + $div = $d->getElementsByTagName('div')[0]; + $object = $d->getElementsByTagName('object')[0]; + $this->assertTrue($img->draggable); + $this->assertFalse($div->draggable); + + $div->draggable = true; + $this->assertTrue($div->draggable); + $div->draggable = false; + $this->assertFalse($div->draggable); + $img->draggable = false; + $this->assertFalse($img->draggable); + + $this->assertFalse($object->draggable); + $object->setAttribute('type', 'image/jpeg'); + $this->assertTrue($object->draggable); + $object->removeAttribute('type'); + $object->setAttribute('data', 'ook.png'); + $this->assertTrue($object->draggable); + } + + public function testProperty_enterKeyHint(): void { + $d = new Document(''); + $html = $d->documentElement; + $this->assertSame('', $html->enterKeyHint); + $html->enterKeyHint = 'ook'; + $this->assertSame('', $html->enterKeyHint); + $html->enterKeyHint = 'done'; + $this->assertSame('done', $html->enterKeyHint); + } + + public function testProperty_hidden(): void { + $d = new Document('
'); + $div = $d->getElementsByTagName('div')[0]; + $this->assertFalse($div->hidden); + $div->hidden = true; + $this->assertTrue($div->hidden); + $div->hidden = false; + $d->documentElement->hidden = true; + $this->assertTrue($div->hidden); + } + + public function testProperty_lang(): void { + $d = new Document(''); + $html = $d->documentElement; + + $this->assertSame('', $html->lang); + $html->lang = 'en'; + $this->assertSame('en', $html->lang); + $html->lang = 'ook'; + $this->assertSame('ook', $html->lang); + } + /** * @covers \MensBeam\HTML\DOM\HTMLElement::__get_innerText * @covers \MensBeam\HTML\DOM\HTMLElement::__set_innerText @@ -56,7 +174,7 @@ class TestHTMLElement extends \PHPUnit\Framework\TestCase { * @covers \MensBeam\HTML\DOM\Inner\Reflection::createFromProtectedConstructor * @covers \MensBeam\HTML\DOM\Inner\Reflection::getProtectedProperty */ - public function testProperty_innerText_outerText() { + public function testProperty_innerText_outerText(): void { $d = new Document(); $d->appendChild($d->createElement('html')); $d->documentElement->appendChild($d->createElement('body')); @@ -84,6 +202,17 @@ class TestHTMLElement extends \PHPUnit\Framework\TestCase { } + public function testProperty_inputMode(): void { + $d = new Document(''); + $html = $d->documentElement; + $this->assertSame('', $html->inputMode); + $html->inputMode = 'ook'; + $this->assertSame('', $html->inputMode); + $html->inputMode = 'tel'; + $this->assertSame('tel', $html->inputMode); + } + + /** * @covers \MensBeam\HTML\DOM\HTMLElement::__set_outerText * @@ -102,7 +231,7 @@ class TestHTMLElement extends \PHPUnit\Framework\TestCase { * @covers \MensBeam\HTML\DOM\Inner\NodeCache::set * @covers \MensBeam\HTML\DOM\Inner\Reflection::createFromProtectedConstructor */ - public function testProperty_outerText__errors() { + public function testProperty_outerText__errors(): void { $this->expectException(DOMException::class); $this->expectExceptionCode(DOMException::NO_MODIFICATION_ALLOWED); $d = new Document(); diff --git a/vendor-bin/phpunit/composer.lock b/vendor-bin/phpunit/composer.lock index f2b576b..6c1b242 100644 --- a/vendor-bin/phpunit/composer.lock +++ b/vendor-bin/phpunit/composer.lock @@ -92,9 +92,6 @@ "require": { "php": "^7.1 || ^8.0" }, - "replace": { - "myclabs/deep-copy": "self.version" - }, "require-dev": { "doctrine/collections": "^1.0", "doctrine/common": "^2.6", @@ -2108,5 +2105,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.2.0" } diff --git a/vendor-bin/robo/composer.lock b/vendor-bin/robo/composer.lock index cddf265..54e1257 100644 --- a/vendor-bin/robo/composer.lock +++ b/vendor-bin/robo/composer.lock @@ -2012,5 +2012,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.2.0" }