2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * Base test case class.
22 * @author Tony Levi <tony.levi@blackboard.com>
23 * @copyright 2015 Blackboard (http://www.blackboard.com)
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29 * Base class for PHPUnit test cases customised for Moodle
31 * It is intended for functionality common to both basic and advanced_testcase.
35 * @author Tony Levi <tony.levi@blackboard.com>
36 * @copyright 2015 Blackboard (http://www.blackboard.com)
37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39 abstract class base_testcase
extends PHPUnit_Framework_TestCase
{
40 // @codingStandardsIgnoreStart
41 // Following code is legacy code from phpunit to support assertTag
45 * Note: we are overriding this method to remove the deprecated error
46 * @see https://tracker.moodle.org/browse/MDL-47129
48 * @param array $matcher
49 * @param string $actual
50 * @param string $message
51 * @param boolean $ishtml
55 public static function assertTag($matcher, $actual, $message = '', $ishtml = true) {
56 $dom = PHPUnit_Util_XML
::load($actual, $ishtml);
57 $tags = self
::findNodes($dom, $matcher, $ishtml);
58 $matched = count($tags) > 0 && $tags[0] instanceof DOMNode
;
59 self
::assertTrue($matched, $message);
63 * Note: we are overriding this method to remove the deprecated error
64 * @see https://tracker.moodle.org/browse/MDL-47129
66 * @param array $matcher
67 * @param string $actual
68 * @param string $message
69 * @param boolean $ishtml
73 public static function assertNotTag($matcher, $actual, $message = '', $ishtml = true) {
74 $dom = PHPUnit_Util_XML
::load($actual, $ishtml);
75 $tags = self
::findNodes($dom, $matcher, $ishtml);
76 $matched = (is_array($tags) && count($tags) > 0) && $tags[0] instanceof DOMNode
;
77 self
::assertFalse($matched, $message);
81 * Validate list of keys in the associative array.
84 * @param array $validKeys
88 * @throws PHPUnit_Framework_Exception
90 public static function assertValidKeys(array $hash, array $validKeys) {
93 // Normalize validation keys so that we can use both indexed and
94 // associative arrays.
95 foreach ($validKeys as $key => $val) {
96 is_int($key) ?
$valids[$val] = null : $valids[$key] = $val;
99 $validKeys = array_keys($valids);
101 // Check for invalid keys.
102 foreach ($hash as $key => $value) {
103 if (!in_array($key, $validKeys)) {
108 if (!empty($unknown)) {
109 throw new PHPUnit_Framework_Exception(
110 'Unknown key(s): ' . implode(', ', $unknown)
114 // Add default values for any valid keys that are empty.
115 foreach ($valids as $key => $value) {
116 if (!isset($hash[$key])) {
117 $hash[$key] = $value;
125 * Parse out the options from the tag using DOM object tree.
127 * @param DOMDocument $dom
128 * @param array $options
129 * @param bool $isHtml
133 public static function findNodes(DOMDocument
$dom, array $options, $isHtml = true) {
135 'id', 'class', 'tag', 'content', 'attributes', 'parent',
136 'child', 'ancestor', 'descendant', 'children', 'adjacent-sibling'
140 $options = self
::assertValidKeys($options, $valid);
142 // find the element by id
143 if ($options['id']) {
144 $options['attributes']['id'] = $options['id'];
147 if ($options['class']) {
148 $options['attributes']['class'] = $options['class'];
153 // find the element by a tag type
154 if ($options['tag']) {
156 $elements = self
::getElementsByCaseInsensitiveTagName(
161 $elements = $dom->getElementsByTagName($options['tag']);
164 foreach ($elements as $element) {
171 } // no tag selected, get them all
174 'a', 'abbr', 'acronym', 'address', 'area', 'b', 'base', 'bdo',
175 'big', 'blockquote', 'body', 'br', 'button', 'caption', 'cite',
176 'code', 'col', 'colgroup', 'dd', 'del', 'div', 'dfn', 'dl',
177 'dt', 'em', 'fieldset', 'form', 'frame', 'frameset', 'h1', 'h2',
178 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'iframe',
179 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link',
180 'map', 'meta', 'noframes', 'noscript', 'object', 'ol', 'optgroup',
181 'option', 'p', 'param', 'pre', 'q', 'samp', 'script', 'select',
182 'small', 'span', 'strong', 'style', 'sub', 'sup', 'table',
183 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title',
184 'tr', 'tt', 'ul', 'var',
186 'article', 'aside', 'audio', 'bdi', 'canvas', 'command',
187 'datalist', 'details', 'dialog', 'embed', 'figure', 'figcaption',
188 'footer', 'header', 'hgroup', 'keygen', 'mark', 'meter', 'nav',
189 'output', 'progress', 'ruby', 'rt', 'rp', 'track', 'section',
190 'source', 'summary', 'time', 'video', 'wbr'
193 foreach ($tags as $tag) {
195 $elements = self
::getElementsByCaseInsensitiveTagName(
200 $elements = $dom->getElementsByTagName($tag);
203 foreach ($elements as $element) {
213 // filter by attributes
214 if ($options['attributes']) {
215 foreach ($nodes as $node) {
218 foreach ($options['attributes'] as $name => $value) {
219 // match by regexp if like "regexp:/foo/i"
220 if (preg_match('/^regexp\s*:\s*(.*)/i', $value, $matches)) {
221 if (!preg_match($matches[1], $node->getAttribute($name))) {
224 } // class can match only a part
225 elseif ($name == 'class') {
226 // split to individual classes
227 $findClasses = explode(
229 preg_replace("/\s+/", ' ', $value)
232 $allClasses = explode(
234 preg_replace("/\s+/", ' ', $node->getAttribute($name))
237 // make sure each class given is in the actual node
238 foreach ($findClasses as $findClass) {
239 if (!in_array($findClass, $allClasses)) {
243 } // match by exact string
245 if ($node->getAttribute($name) != $value) {
251 // if every attribute given matched
266 if ($options['content'] !== null) {
267 foreach ($nodes as $node) {
270 // match by regexp if like "regexp:/foo/i"
271 if (preg_match('/^regexp\s*:\s*(.*)/i', $options['content'], $matches)) {
272 if (!preg_match($matches[1], self
::getNodeText($node))) {
275 } // match empty string
276 elseif ($options['content'] === '') {
277 if (self
::getNodeText($node) !== '') {
280 } // match by exact string
281 elseif (strstr(self
::getNodeText($node), $options['content']) === false) {
298 // filter by parent node
299 if ($options['parent']) {
300 $parentNodes = self
::findNodes($dom, $options['parent'], $isHtml);
301 $parentNode = isset($parentNodes[0]) ?
$parentNodes[0] : null;
303 foreach ($nodes as $node) {
304 if ($parentNode !== $node->parentNode
) {
319 // filter by child node
320 if ($options['child']) {
321 $childNodes = self
::findNodes($dom, $options['child'], $isHtml);
322 $childNodes = !empty($childNodes) ?
$childNodes : array();
324 foreach ($nodes as $node) {
325 foreach ($node->childNodes
as $child) {
326 foreach ($childNodes as $childNode) {
327 if ($childNode === $child) {
342 // filter by adjacent-sibling
343 if ($options['adjacent-sibling']) {
344 $adjacentSiblingNodes = self
::findNodes($dom, $options['adjacent-sibling'], $isHtml);
345 $adjacentSiblingNodes = !empty($adjacentSiblingNodes) ?
$adjacentSiblingNodes : array();
347 foreach ($nodes as $node) {
350 while ($sibling = $sibling->nextSibling
) {
351 if ($sibling->nodeType
!== XML_ELEMENT_NODE
) {
355 foreach ($adjacentSiblingNodes as $adjacentSiblingNode) {
356 if ($sibling === $adjacentSiblingNode) {
374 // filter by ancestor
375 if ($options['ancestor']) {
376 $ancestorNodes = self
::findNodes($dom, $options['ancestor'], $isHtml);
377 $ancestorNode = isset($ancestorNodes[0]) ?
$ancestorNodes[0] : null;
379 foreach ($nodes as $node) {
380 $parent = $node->parentNode
;
382 while ($parent && $parent->nodeType
!= XML_HTML_DOCUMENT_NODE
) {
383 if ($parent === $ancestorNode) {
387 $parent = $parent->parentNode
;
399 // filter by descendant
400 if ($options['descendant']) {
401 $descendantNodes = self
::findNodes($dom, $options['descendant'], $isHtml);
402 $descendantNodes = !empty($descendantNodes) ?
$descendantNodes : array();
404 foreach ($nodes as $node) {
405 foreach (self
::getDescendants($node) as $descendant) {
406 foreach ($descendantNodes as $descendantNode) {
407 if ($descendantNode === $descendant) {
422 // filter by children
423 if ($options['children']) {
424 $validChild = array('count', 'greater_than', 'less_than', 'only');
425 $childOptions = self
::assertValidKeys(
426 $options['children'],
430 foreach ($nodes as $node) {
431 $childNodes = $node->childNodes
;
433 foreach ($childNodes as $childNode) {
434 if ($childNode->nodeType
!== XML_CDATA_SECTION_NODE
&&
435 $childNode->nodeType
!== XML_TEXT_NODE
) {
436 $children[] = $childNode;
440 // we must have children to pass this filter
441 if (!empty($children)) {
442 // exact count of children
443 if ($childOptions['count'] !== null) {
444 if (count($children) !== $childOptions['count']) {
447 } // range count of children
448 elseif ($childOptions['less_than'] !== null &&
449 $childOptions['greater_than'] !== null) {
450 if (count($children) >= $childOptions['less_than'] ||
451 count($children) <= $childOptions['greater_than']) {
454 } // less than a given count
455 elseif ($childOptions['less_than'] !== null) {
456 if (count($children) >= $childOptions['less_than']) {
459 } // more than a given count
460 elseif ($childOptions['greater_than'] !== null) {
461 if (count($children) <= $childOptions['greater_than']) {
466 // match each child against a specific tag
467 if ($childOptions['only']) {
468 $onlyNodes = self
::findNodes(
470 $childOptions['only'],
474 // try to match each child to one of the 'only' nodes
475 foreach ($children as $child) {
478 foreach ($onlyNodes as $onlyNode) {
479 if ($onlyNode === $child) {
501 // return the first node that matches all criteria
502 return !empty($nodes) ?
$nodes : array();
506 * Recursively get flat array of all descendants of this node.
508 * @param DOMNode $node
512 protected static function getDescendants(DOMNode
$node) {
513 $allChildren = array();
514 $childNodes = $node->childNodes ?
$node->childNodes
: array();
516 foreach ($childNodes as $child) {
517 if ($child->nodeType
=== XML_CDATA_SECTION_NODE ||
518 $child->nodeType
=== XML_TEXT_NODE
) {
522 $children = self
::getDescendants($child);
523 $allChildren = array_merge($allChildren, $children, array($child));
526 return isset($allChildren) ?
$allChildren : array();
530 * Gets elements by case insensitive tagname.
532 * @param DOMDocument $dom
535 * @return DOMNodeList
537 protected static function getElementsByCaseInsensitiveTagName(DOMDocument
$dom, $tag) {
538 $elements = $dom->getElementsByTagName(strtolower($tag));
540 if ($elements->length
== 0) {
541 $elements = $dom->getElementsByTagName(strtoupper($tag));
548 * Get the text value of this node's child text node.
550 * @param DOMNode $node
554 protected static function getNodeText(DOMNode
$node) {
555 if (!$node->childNodes
instanceof DOMNodeList
) {
561 foreach ($node->childNodes
as $childNode) {
562 if ($childNode->nodeType
=== XML_TEXT_NODE ||
563 $childNode->nodeType
=== XML_CDATA_SECTION_NODE
) {
564 $result .= trim($childNode->data
) . ' ';
566 $result .= self
::getNodeText($childNode);
570 return str_replace(' ', ' ', $result);
573 // @codingStandardsIgnoreEnd