diff --git a/lib/phpunit/classes/base_testcase.php b/lib/phpunit/classes/base_testcase.php index dd1dc8f6aa4..231ce1f4198 100644 --- a/lib/phpunit/classes/base_testcase.php +++ b/lib/phpunit/classes/base_testcase.php @@ -37,6 +37,10 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class base_testcase extends PHPUnit_Framework_TestCase { + // @codingStandardsIgnoreStart + // Following code is legacy code from phpunit to support assertTag + // and assertNotTag. + /** * Note: we are overriding this method to remove the deprecated error * @see https://tracker.moodle.org/browse/MDL-47129 @@ -50,7 +54,7 @@ abstract class base_testcase extends PHPUnit_Framework_TestCase { */ public static function assertTag($matcher, $actual, $message = '', $ishtml = true) { $dom = PHPUnit_Util_XML::load($actual, $ishtml); - $tags = PHPUnit_Util_XML::findNodes($dom, $matcher, $ishtml); + $tags = self::findNodes($dom, $matcher, $ishtml); $matched = count($tags) > 0 && $tags[0] instanceof DOMNode; self::assertTrue($matched, $message); } @@ -68,8 +72,503 @@ abstract class base_testcase extends PHPUnit_Framework_TestCase { */ public static function assertNotTag($matcher, $actual, $message = '', $ishtml = true) { $dom = PHPUnit_Util_XML::load($actual, $ishtml); - $tags = PHPUnit_Util_XML::findNodes($dom, $matcher, $ishtml); + $tags = self::findNodes($dom, $matcher, $ishtml); $matched = count($tags) > 0 && $tags[0] instanceof DOMNode; self::assertFalse($matched, $message); } + + /** + * Validate list of keys in the associative array. + * + * @param array $hash + * @param array $validKeys + * + * @return array + * + * @throws PHPUnit_Framework_Exception + */ + public static function assertValidKeys(array $hash, array $validKeys) { + $valids = array(); + + // Normalize validation keys so that we can use both indexed and + // associative arrays. + foreach ($validKeys as $key => $val) { + is_int($key) ? $valids[$val] = null : $valids[$key] = $val; + } + + $validKeys = array_keys($valids); + + // Check for invalid keys. + foreach ($hash as $key => $value) { + if (!in_array($key, $validKeys)) { + $unknown[] = $key; + } + } + + if (!empty($unknown)) { + throw new PHPUnit_Framework_Exception( + 'Unknown key(s): ' . implode(', ', $unknown) + ); + } + + // Add default values for any valid keys that are empty. + foreach ($valids as $key => $value) { + if (!isset($hash[$key])) { + $hash[$key] = $value; + } + } + + return $hash; + } + + /** + * Parse out the options from the tag using DOM object tree. + * + * @param DOMDocument $dom + * @param array $options + * @param bool $isHtml + * + * @return array + */ + public static function findNodes(DOMDocument $dom, array $options, $isHtml = true) { + $valid = array( + 'id', 'class', 'tag', 'content', 'attributes', 'parent', + 'child', 'ancestor', 'descendant', 'children', 'adjacent-sibling' + ); + + $filtered = array(); + $options = self::assertValidKeys($options, $valid); + + // find the element by id + if ($options['id']) { + $options['attributes']['id'] = $options['id']; + } + + if ($options['class']) { + $options['attributes']['class'] = $options['class']; + } + + $nodes = array(); + + // find the element by a tag type + if ($options['tag']) { + if ($isHtml) { + $elements = self::getElementsByCaseInsensitiveTagName( + $dom, + $options['tag'] + ); + } else { + $elements = $dom->getElementsByTagName($options['tag']); + } + + foreach ($elements as $element) { + $nodes[] = $element; + } + + if (empty($nodes)) { + return false; + } + } // no tag selected, get them all + else { + $tags = array( + 'a', 'abbr', 'acronym', 'address', 'area', 'b', 'base', 'bdo', + 'big', 'blockquote', 'body', 'br', 'button', 'caption', 'cite', + 'code', 'col', 'colgroup', 'dd', 'del', 'div', 'dfn', 'dl', + 'dt', 'em', 'fieldset', 'form', 'frame', 'frameset', 'h1', 'h2', + 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'iframe', + 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', + 'map', 'meta', 'noframes', 'noscript', 'object', 'ol', 'optgroup', + 'option', 'p', 'param', 'pre', 'q', 'samp', 'script', 'select', + 'small', 'span', 'strong', 'style', 'sub', 'sup', 'table', + 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title', + 'tr', 'tt', 'ul', 'var', + // HTML5 + 'article', 'aside', 'audio', 'bdi', 'canvas', 'command', + 'datalist', 'details', 'dialog', 'embed', 'figure', 'figcaption', + 'footer', 'header', 'hgroup', 'keygen', 'mark', 'meter', 'nav', + 'output', 'progress', 'ruby', 'rt', 'rp', 'track', 'section', + 'source', 'summary', 'time', 'video', 'wbr' + ); + + foreach ($tags as $tag) { + if ($isHtml) { + $elements = self::getElementsByCaseInsensitiveTagName( + $dom, + $tag + ); + } else { + $elements = $dom->getElementsByTagName($tag); + } + + foreach ($elements as $element) { + $nodes[] = $element; + } + } + + if (empty($nodes)) { + return false; + } + } + + // filter by attributes + if ($options['attributes']) { + foreach ($nodes as $node) { + $invalid = false; + + foreach ($options['attributes'] as $name => $value) { + // match by regexp if like "regexp:/foo/i" + if (preg_match('/^regexp\s*:\s*(.*)/i', $value, $matches)) { + if (!preg_match($matches[1], $node->getAttribute($name))) { + $invalid = true; + } + } // class can match only a part + elseif ($name == 'class') { + // split to individual classes + $findClasses = explode( + ' ', + preg_replace("/\s+/", ' ', $value) + ); + + $allClasses = explode( + ' ', + preg_replace("/\s+/", ' ', $node->getAttribute($name)) + ); + + // make sure each class given is in the actual node + foreach ($findClasses as $findClass) { + if (!in_array($findClass, $allClasses)) { + $invalid = true; + } + } + } // match by exact string + else { + if ($node->getAttribute($name) != $value) { + $invalid = true; + } + } + } + + // if every attribute given matched + if (!$invalid) { + $filtered[] = $node; + } + } + + $nodes = $filtered; + $filtered = array(); + + if (empty($nodes)) { + return false; + } + } + + // filter by content + if ($options['content'] !== null) { + foreach ($nodes as $node) { + $invalid = false; + + // match by regexp if like "regexp:/foo/i" + if (preg_match('/^regexp\s*:\s*(.*)/i', $options['content'], $matches)) { + if (!preg_match($matches[1], self::getNodeText($node))) { + $invalid = true; + } + } // match empty string + elseif ($options['content'] === '') { + if (self::getNodeText($node) !== '') { + $invalid = true; + } + } // match by exact string + elseif (strstr(self::getNodeText($node), $options['content']) === false) { + $invalid = true; + } + + if (!$invalid) { + $filtered[] = $node; + } + } + + $nodes = $filtered; + $filtered = array(); + + if (empty($nodes)) { + return false; + } + } + + // filter by parent node + if ($options['parent']) { + $parentNodes = self::findNodes($dom, $options['parent'], $isHtml); + $parentNode = isset($parentNodes[0]) ? $parentNodes[0] : null; + + foreach ($nodes as $node) { + if ($parentNode !== $node->parentNode) { + continue; + } + + $filtered[] = $node; + } + + $nodes = $filtered; + $filtered = array(); + + if (empty($nodes)) { + return false; + } + } + + // filter by child node + if ($options['child']) { + $childNodes = self::findNodes($dom, $options['child'], $isHtml); + $childNodes = !empty($childNodes) ? $childNodes : array(); + + foreach ($nodes as $node) { + foreach ($node->childNodes as $child) { + foreach ($childNodes as $childNode) { + if ($childNode === $child) { + $filtered[] = $node; + } + } + } + } + + $nodes = $filtered; + $filtered = array(); + + if (empty($nodes)) { + return false; + } + } + + // filter by adjacent-sibling + if ($options['adjacent-sibling']) { + $adjacentSiblingNodes = self::findNodes($dom, $options['adjacent-sibling'], $isHtml); + $adjacentSiblingNodes = !empty($adjacentSiblingNodes) ? $adjacentSiblingNodes : array(); + + foreach ($nodes as $node) { + $sibling = $node; + + while ($sibling = $sibling->nextSibling) { + if ($sibling->nodeType !== XML_ELEMENT_NODE) { + continue; + } + + foreach ($adjacentSiblingNodes as $adjacentSiblingNode) { + if ($sibling === $adjacentSiblingNode) { + $filtered[] = $node; + break; + } + } + + break; + } + } + + $nodes = $filtered; + $filtered = array(); + + if (empty($nodes)) { + return false; + } + } + + // filter by ancestor + if ($options['ancestor']) { + $ancestorNodes = self::findNodes($dom, $options['ancestor'], $isHtml); + $ancestorNode = isset($ancestorNodes[0]) ? $ancestorNodes[0] : null; + + foreach ($nodes as $node) { + $parent = $node->parentNode; + + while ($parent && $parent->nodeType != XML_HTML_DOCUMENT_NODE) { + if ($parent === $ancestorNode) { + $filtered[] = $node; + } + + $parent = $parent->parentNode; + } + } + + $nodes = $filtered; + $filtered = array(); + + if (empty($nodes)) { + return false; + } + } + + // filter by descendant + if ($options['descendant']) { + $descendantNodes = self::findNodes($dom, $options['descendant'], $isHtml); + $descendantNodes = !empty($descendantNodes) ? $descendantNodes : array(); + + foreach ($nodes as $node) { + foreach (self::getDescendants($node) as $descendant) { + foreach ($descendantNodes as $descendantNode) { + if ($descendantNode === $descendant) { + $filtered[] = $node; + } + } + } + } + + $nodes = $filtered; + $filtered = array(); + + if (empty($nodes)) { + return false; + } + } + + // filter by children + if ($options['children']) { + $validChild = array('count', 'greater_than', 'less_than', 'only'); + $childOptions = self::assertValidKeys( + $options['children'], + $validChild + ); + + foreach ($nodes as $node) { + $childNodes = $node->childNodes; + + foreach ($childNodes as $childNode) { + if ($childNode->nodeType !== XML_CDATA_SECTION_NODE && + $childNode->nodeType !== XML_TEXT_NODE) { + $children[] = $childNode; + } + } + + // we must have children to pass this filter + if (!empty($children)) { + // exact count of children + if ($childOptions['count'] !== null) { + if (count($children) !== $childOptions['count']) { + break; + } + } // range count of children + elseif ($childOptions['less_than'] !== null && + $childOptions['greater_than'] !== null) { + if (count($children) >= $childOptions['less_than'] || + count($children) <= $childOptions['greater_than']) { + break; + } + } // less than a given count + elseif ($childOptions['less_than'] !== null) { + if (count($children) >= $childOptions['less_than']) { + break; + } + } // more than a given count + elseif ($childOptions['greater_than'] !== null) { + if (count($children) <= $childOptions['greater_than']) { + break; + } + } + + // match each child against a specific tag + if ($childOptions['only']) { + $onlyNodes = self::findNodes( + $dom, + $childOptions['only'], + $isHtml + ); + + // try to match each child to one of the 'only' nodes + foreach ($children as $child) { + $matched = false; + + foreach ($onlyNodes as $onlyNode) { + if ($onlyNode === $child) { + $matched = true; + } + } + + if (!$matched) { + break 2; + } + } + } + + $filtered[] = $node; + } + } + + $nodes = $filtered; + + if (empty($nodes)) { + return; + } + } + + // return the first node that matches all criteria + return !empty($nodes) ? $nodes : array(); + } + + /** + * Recursively get flat array of all descendants of this node. + * + * @param DOMNode $node + * + * @return array + */ + protected static function getDescendants(DOMNode $node) { + $allChildren = array(); + $childNodes = $node->childNodes ? $node->childNodes : array(); + + foreach ($childNodes as $child) { + if ($child->nodeType === XML_CDATA_SECTION_NODE || + $child->nodeType === XML_TEXT_NODE) { + continue; + } + + $children = self::getDescendants($child); + $allChildren = array_merge($allChildren, $children, array($child)); + } + + return isset($allChildren) ? $allChildren : array(); + } + + /** + * Gets elements by case insensitive tagname. + * + * @param DOMDocument $dom + * @param string $tag + * + * @return DOMNodeList + */ + protected static function getElementsByCaseInsensitiveTagName(DOMDocument $dom, $tag) { + $elements = $dom->getElementsByTagName(strtolower($tag)); + + if ($elements->length == 0) { + $elements = $dom->getElementsByTagName(strtoupper($tag)); + } + + return $elements; + } + + /** + * Get the text value of this node's child text node. + * + * @param DOMNode $node + * + * @return string + */ + protected static function getNodeText(DOMNode $node) { + if (!$node->childNodes instanceof DOMNodeList) { + return ''; + } + + $result = ''; + + foreach ($node->childNodes as $childNode) { + if ($childNode->nodeType === XML_TEXT_NODE || + $childNode->nodeType === XML_CDATA_SECTION_NODE) { + $result .= trim($childNode->data) . ' '; + } else { + $result .= self::getNodeText($childNode); + } + } + + return str_replace(' ', ' ', $result); + } + + // @codingStandardsIgnoreEnd }