Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
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.
8
//
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.
13
//
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/>.
16
 
17
/**
18
 * Base test case class.
19
 *
20
 * @package    core
21
 * @category   test
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
25
 */
26
 
27
 
28
/**
29
 * Base class for PHPUnit test cases customised for Moodle
30
 *
31
 * It is intended for functionality common to both basic and advanced_testcase.
32
 *
33
 * @package    core
34
 * @category   test
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
38
 */
39
abstract class base_testcase extends PHPUnit\Framework\TestCase {
40
    // phpcs:disable
41
    // Following code is legacy code from phpunit to support assertTag
42
    // and assertNotTag.
43
 
44
    /**
45
     * Note: we are overriding this method to remove the deprecated error
46
     * @see https://tracker.moodle.org/browse/MDL-47129
47
     *
48
     * @param  array   $matcher
49
     * @param  string  $actual
50
     * @param  string  $message
51
     * @param  boolean $ishtml
52
     *
53
     * @deprecated 3.0
54
     */
55
    public static function assertTag($matcher, $actual, $message = '', $ishtml = true) {
56
        $dom = (new PHPUnit\Util\Xml\Loader)->load($actual, $ishtml);
57
        $tags = self::findNodes($dom, $matcher, $ishtml);
58
        $matched = (is_array($tags) && count($tags) > 0) && $tags[0] instanceof DOMNode;
59
        self::assertTrue($matched, $message);
60
    }
61
 
62
    /**
63
     * Note: we are overriding this method to remove the deprecated error
64
     * @see https://tracker.moodle.org/browse/MDL-47129
65
     *
66
     * @param  array   $matcher
67
     * @param  string  $actual
68
     * @param  string  $message
69
     * @param  boolean $ishtml
70
     *
71
     * @deprecated 3.0
72
     */
73
    public static function assertNotTag($matcher, $actual, $message = '', $ishtml = true) {
74
        $dom = (new PHPUnit\Util\Xml\Loader)->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);
78
    }
79
 
80
    /**
81
     * Validate list of keys in the associative array.
82
     *
83
     * @param array $hash
84
     * @param array $validKeys
85
     *
86
     * @return array
87
     *
88
     * @throws PHPUnit\Framework\Exception
89
     */
90
    public static function assertValidKeys(array $hash, array $validKeys) {
91
        $valids = array();
92
 
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;
97
        }
98
 
99
        $validKeys = array_keys($valids);
100
 
101
        // Check for invalid keys.
102
        foreach ($hash as $key => $value) {
103
            if (!in_array($key, $validKeys)) {
104
                $unknown[] = $key;
105
            }
106
        }
107
 
108
        if (!empty($unknown)) {
109
            throw new PHPUnit\Framework\Exception(
110
                'Unknown key(s): ' . implode(', ', $unknown)
111
            );
112
        }
113
 
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;
118
            }
119
        }
120
 
121
        return $hash;
122
    }
123
 
124
    /**
125
     * Assert that two Date/Time strings are equal.
126
     *
127
     * The strings generated by \DateTime, \strtotime, \date, \time, etc. are generated outside of our control.
128
     * From time-to-time string changes are made.
129
     * One such example is from ICU 72.1 which changed the time format to include a narrow-non-breaking-space (U+202F)
130
     * between the time and AM/PM.
131
     *
132
     * We should not update our tests to match these changes, as it is not our code that is
133
     * generating the strings and they may change again.
134
     * In addition, the changes are not equal amongst all systems as they depend on the version of ICU installed.
135
     *
136
     * @param string $expected
137
     * @param string $actual
138
     * @param string $message
139
     */
140
    public function assertEqualsIgnoringWhitespace($expected, $actual, string $message = ''): void {
141
        // ICU 72.1 introduced the use of a narrow-non-breaking-space (U+202F) between the time and the AM/PM.
142
        // Normalise all whitespace when performing the comparison.
143
        $expected = preg_replace('/\s+/u', ' ', $expected);
144
        $actual = preg_replace('/\s+/u', ' ', $actual);
145
 
146
        $this->assertEquals($expected, $actual, $message);
147
    }
148
 
149
    /**
150
     * Parse out the options from the tag using DOM object tree.
151
     *
152
     * @param DOMDocument $dom
153
     * @param array       $options
154
     * @param bool        $isHtml
155
     *
156
     * @return array
157
     */
158
    public static function findNodes(DOMDocument $dom, array $options, $isHtml = true) {
159
        $valid = array(
160
            'id', 'class', 'tag', 'content', 'attributes', 'parent',
161
            'child', 'ancestor', 'descendant', 'children', 'adjacent-sibling'
162
        );
163
 
164
        $filtered = array();
165
        $options  = self::assertValidKeys($options, $valid);
166
 
167
        // find the element by id
168
        if ($options['id']) {
169
            $options['attributes']['id'] = $options['id'];
170
        }
171
 
172
        if ($options['class']) {
173
            $options['attributes']['class'] = $options['class'];
174
        }
175
 
176
        $nodes = array();
177
 
178
        // find the element by a tag type
179
        if ($options['tag']) {
180
            if ($isHtml) {
181
                $elements = self::getElementsByCaseInsensitiveTagName(
182
                    $dom,
183
                    $options['tag']
184
                );
185
            } else {
186
                $elements = $dom->getElementsByTagName($options['tag']);
187
            }
188
 
189
            foreach ($elements as $element) {
190
                $nodes[] = $element;
191
            }
192
 
193
            if (empty($nodes)) {
194
                return false;
195
            }
196
        } // no tag selected, get them all
197
        else {
198
            $tags = array(
199
                'a', 'abbr', 'acronym', 'address', 'area', 'b', 'base', 'bdo',
200
                'big', 'blockquote', 'body', 'br', 'button', 'caption', 'cite',
201
                'code', 'col', 'colgroup', 'dd', 'del', 'div', 'dfn', 'dl',
202
                'dt', 'em', 'fieldset', 'form', 'frame', 'frameset', 'h1', 'h2',
203
                'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'iframe',
204
                'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link',
205
                'map', 'meta', 'noframes', 'noscript', 'object', 'ol', 'optgroup',
206
                'option', 'p', 'param', 'pre', 'q', 'samp', 'script', 'select',
207
                'small', 'span', 'strong', 'style', 'sub', 'sup', 'table',
208
                'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title',
209
                'tr', 'tt', 'ul', 'var',
210
                // HTML5
211
                'article', 'aside', 'audio', 'bdi', 'canvas', 'command',
212
                'datalist', 'details', 'dialog', 'embed', 'figure', 'figcaption',
213
                'footer', 'header', 'hgroup', 'keygen', 'mark', 'meter', 'nav',
214
                'output', 'progress', 'ruby', 'rt', 'rp', 'track', 'section',
215
                'source', 'summary', 'time', 'video', 'wbr'
216
            );
217
 
218
            foreach ($tags as $tag) {
219
                if ($isHtml) {
220
                    $elements = self::getElementsByCaseInsensitiveTagName(
221
                        $dom,
222
                        $tag
223
                    );
224
                } else {
225
                    $elements = $dom->getElementsByTagName($tag);
226
                }
227
 
228
                foreach ($elements as $element) {
229
                    $nodes[] = $element;
230
                }
231
            }
232
 
233
            if (empty($nodes)) {
234
                return false;
235
            }
236
        }
237
 
238
        // filter by attributes
239
        if ($options['attributes']) {
240
            foreach ($nodes as $node) {
241
                $invalid = false;
242
 
243
                foreach ($options['attributes'] as $name => $value) {
244
                    // match by regexp if like "regexp:/foo/i"
245
                    if (preg_match('/^regexp\s*:\s*(.*)/i', $value, $matches)) {
246
                        if (!preg_match($matches[1], $node->getAttribute($name))) {
247
                            $invalid = true;
248
                        }
249
                    } // class can match only a part
250
                    elseif ($name == 'class') {
251
                        // split to individual classes
252
                        $findClasses = explode(
253
                            ' ',
254
                            preg_replace("/\s+/", ' ', $value)
255
                        );
256
 
257
                        $allClasses = explode(
258
                            ' ',
259
                            preg_replace("/\s+/", ' ', $node->getAttribute($name))
260
                        );
261
 
262
                        // make sure each class given is in the actual node
263
                        foreach ($findClasses as $findClass) {
264
                            if (!in_array($findClass, $allClasses)) {
265
                                $invalid = true;
266
                            }
267
                        }
268
                    } // match by exact string
269
                    else {
270
                        if ($node->getAttribute($name) !== (string) $value) {
271
                            $invalid = true;
272
                        }
273
                    }
274
                }
275
 
276
                // if every attribute given matched
277
                if (!$invalid) {
278
                    $filtered[] = $node;
279
                }
280
            }
281
 
282
            $nodes    = $filtered;
283
            $filtered = array();
284
 
285
            if (empty($nodes)) {
286
                return false;
287
            }
288
        }
289
 
290
        // filter by content
291
        if ($options['content'] !== null) {
292
            foreach ($nodes as $node) {
293
                $invalid = false;
294
 
295
                // match by regexp if like "regexp:/foo/i"
296
                if (preg_match('/^regexp\s*:\s*(.*)/i', $options['content'], $matches)) {
297
                    if (!preg_match($matches[1], self::getNodeText($node))) {
298
                        $invalid = true;
299
                    }
300
                } // match empty string
301
                elseif ($options['content'] === '') {
302
                    if (self::getNodeText($node) !== '') {
303
                        $invalid = true;
304
                    }
305
                } // match by exact string
306
                elseif (strstr(self::getNodeText($node), $options['content']) === false) {
307
                    $invalid = true;
308
                }
309
 
310
                if (!$invalid) {
311
                    $filtered[] = $node;
312
                }
313
            }
314
 
315
            $nodes    = $filtered;
316
            $filtered = array();
317
 
318
            if (empty($nodes)) {
319
                return false;
320
            }
321
        }
322
 
323
        // filter by parent node
324
        if ($options['parent']) {
325
            $parentNodes = self::findNodes($dom, $options['parent'], $isHtml);
326
            $parentNode  = isset($parentNodes[0]) ? $parentNodes[0] : null;
327
 
328
            foreach ($nodes as $node) {
329
                if ($parentNode !== $node->parentNode) {
330
                    continue;
331
                }
332
 
333
                $filtered[] = $node;
334
            }
335
 
336
            $nodes    = $filtered;
337
            $filtered = array();
338
 
339
            if (empty($nodes)) {
340
                return false;
341
            }
342
        }
343
 
344
        // filter by child node
345
        if ($options['child']) {
346
            $childNodes = self::findNodes($dom, $options['child'], $isHtml);
347
            $childNodes = !empty($childNodes) ? $childNodes : array();
348
 
349
            foreach ($nodes as $node) {
350
                foreach ($node->childNodes as $child) {
351
                    foreach ($childNodes as $childNode) {
352
                        if ($childNode === $child) {
353
                            $filtered[] = $node;
354
                        }
355
                    }
356
                }
357
            }
358
 
359
            $nodes    = $filtered;
360
            $filtered = array();
361
 
362
            if (empty($nodes)) {
363
                return false;
364
            }
365
        }
366
 
367
        // filter by adjacent-sibling
368
        if ($options['adjacent-sibling']) {
369
            $adjacentSiblingNodes = self::findNodes($dom, $options['adjacent-sibling'], $isHtml);
370
            $adjacentSiblingNodes = !empty($adjacentSiblingNodes) ? $adjacentSiblingNodes : array();
371
 
372
            foreach ($nodes as $node) {
373
                $sibling = $node;
374
 
375
                while ($sibling = $sibling->nextSibling) {
376
                    if ($sibling->nodeType !== XML_ELEMENT_NODE) {
377
                        continue;
378
                    }
379
 
380
                    foreach ($adjacentSiblingNodes as $adjacentSiblingNode) {
381
                        if ($sibling === $adjacentSiblingNode) {
382
                            $filtered[] = $node;
383
                            break;
384
                        }
385
                    }
386
 
387
                    break;
388
                }
389
            }
390
 
391
            $nodes    = $filtered;
392
            $filtered = array();
393
 
394
            if (empty($nodes)) {
395
                return false;
396
            }
397
        }
398
 
399
        // filter by ancestor
400
        if ($options['ancestor']) {
401
            $ancestorNodes = self::findNodes($dom, $options['ancestor'], $isHtml);
402
            $ancestorNode  = isset($ancestorNodes[0]) ? $ancestorNodes[0] : null;
403
 
404
            foreach ($nodes as $node) {
405
                $parent = $node->parentNode;
406
 
407
                while ($parent && $parent->nodeType != XML_HTML_DOCUMENT_NODE) {
408
                    if ($parent === $ancestorNode) {
409
                        $filtered[] = $node;
410
                    }
411
 
412
                    $parent = $parent->parentNode;
413
                }
414
            }
415
 
416
            $nodes    = $filtered;
417
            $filtered = array();
418
 
419
            if (empty($nodes)) {
420
                return false;
421
            }
422
        }
423
 
424
        // filter by descendant
425
        if ($options['descendant']) {
426
            $descendantNodes = self::findNodes($dom, $options['descendant'], $isHtml);
427
            $descendantNodes = !empty($descendantNodes) ? $descendantNodes : array();
428
 
429
            foreach ($nodes as $node) {
430
                foreach (self::getDescendants($node) as $descendant) {
431
                    foreach ($descendantNodes as $descendantNode) {
432
                        if ($descendantNode === $descendant) {
433
                            $filtered[] = $node;
434
                        }
435
                    }
436
                }
437
            }
438
 
439
            $nodes    = $filtered;
440
            $filtered = array();
441
 
442
            if (empty($nodes)) {
443
                return false;
444
            }
445
        }
446
 
447
        // filter by children
448
        if ($options['children']) {
449
            $validChild   = array('count', 'greater_than', 'less_than', 'only');
450
            $childOptions = self::assertValidKeys(
451
                $options['children'],
452
                $validChild
453
            );
454
 
455
            foreach ($nodes as $node) {
456
                $childNodes = $node->childNodes;
457
 
458
                foreach ($childNodes as $childNode) {
459
                    if ($childNode->nodeType !== XML_CDATA_SECTION_NODE &&
460
                        $childNode->nodeType !== XML_TEXT_NODE) {
461
                        $children[] = $childNode;
462
                    }
463
                }
464
 
465
                // we must have children to pass this filter
466
                if (!empty($children)) {
467
                    // exact count of children
468
                    if ($childOptions['count'] !== null) {
469
                        if (count($children) !== $childOptions['count']) {
470
                            break;
471
                        }
472
                    } // range count of children
473
                    elseif ($childOptions['less_than']    !== null &&
474
                        $childOptions['greater_than'] !== null) {
475
                        if (count($children) >= $childOptions['less_than'] ||
476
                            count($children) <= $childOptions['greater_than']) {
477
                            break;
478
                        }
479
                    } // less than a given count
480
                    elseif ($childOptions['less_than'] !== null) {
481
                        if (count($children) >= $childOptions['less_than']) {
482
                            break;
483
                        }
484
                    } // more than a given count
485
                    elseif ($childOptions['greater_than'] !== null) {
486
                        if (count($children) <= $childOptions['greater_than']) {
487
                            break;
488
                        }
489
                    }
490
 
491
                    // match each child against a specific tag
492
                    if ($childOptions['only']) {
493
                        $onlyNodes = self::findNodes(
494
                            $dom,
495
                            $childOptions['only'],
496
                            $isHtml
497
                        );
498
 
499
                        // try to match each child to one of the 'only' nodes
500
                        foreach ($children as $child) {
501
                            $matched = false;
502
 
503
                            foreach ($onlyNodes as $onlyNode) {
504
                                if ($onlyNode === $child) {
505
                                    $matched = true;
506
                                }
507
                            }
508
 
509
                            if (!$matched) {
510
                                break 2;
511
                            }
512
                        }
513
                    }
514
 
515
                    $filtered[] = $node;
516
                }
517
            }
518
 
519
            $nodes = $filtered;
520
 
521
            if (empty($nodes)) {
522
                return;
523
            }
524
        }
525
 
526
        // return the first node that matches all criteria
527
        return !empty($nodes) ? $nodes : array();
528
    }
529
 
530
    /**
531
     * Recursively get flat array of all descendants of this node.
532
     *
533
     * @param DOMNode $node
534
     *
535
     * @return array
536
     */
537
    protected static function getDescendants(DOMNode $node) {
538
        $allChildren = array();
539
        $childNodes  = $node->childNodes ? $node->childNodes : array();
540
 
541
        foreach ($childNodes as $child) {
542
            if ($child->nodeType === XML_CDATA_SECTION_NODE ||
543
                $child->nodeType === XML_TEXT_NODE) {
544
                continue;
545
            }
546
 
547
            $children    = self::getDescendants($child);
548
            $allChildren = array_merge($allChildren, $children, array($child));
549
        }
550
 
551
        return isset($allChildren) ? $allChildren : array();
552
    }
553
 
554
    /**
555
     * Gets elements by case insensitive tagname.
556
     *
557
     * @param DOMDocument $dom
558
     * @param string      $tag
559
     *
560
     * @return DOMNodeList
561
     */
562
    protected static function getElementsByCaseInsensitiveTagName(DOMDocument $dom, $tag) {
563
        $elements = $dom->getElementsByTagName(strtolower($tag));
564
 
565
        if ($elements->length == 0) {
566
            $elements = $dom->getElementsByTagName(strtoupper($tag));
567
        }
568
 
569
        return $elements;
570
    }
571
 
572
    /**
573
     * Get the text value of this node's child text node.
574
     *
575
     * @param DOMNode $node
576
     *
577
     * @return string
578
     */
579
    protected static function getNodeText(DOMNode $node) {
580
        if (!$node->childNodes instanceof DOMNodeList) {
581
            return '';
582
        }
583
 
584
        $result = '';
585
 
586
        foreach ($node->childNodes as $childNode) {
587
            if ($childNode->nodeType === XML_TEXT_NODE ||
588
                $childNode->nodeType === XML_CDATA_SECTION_NODE) {
589
                $result .= trim($childNode->data) . ' ';
590
            } else {
591
                $result .= self::getNodeText($childNode);
592
            }
593
        }
594
 
595
        return str_replace('  ', ' ', $result);
596
    }
597
    // phpcs:enable
598
}