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 namespace core\navigation\views; 18 19 use booktool_print\output\renderer; 20 use navigation_node; 21 use ReflectionMethod; 22 use moodle_url; 23 24 /** 25 * Class core_secondary_testcase 26 * 27 * Unit test for the secondary nav view. 28 * 29 * @package core 30 * @category navigation 31 * @copyright 2021 onwards Peter Dias 32 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 */ 34 class secondary_test extends \advanced_testcase { 35 /** 36 * Test the get_leaf_nodes function 37 * @param float $siteorder The order for the siteadmin node 38 * @param float $courseorder The order for the course node 39 * @param float $moduleorder The order for the module node 40 * @dataProvider leaf_nodes_order_provider 41 */ 42 public function test_get_leaf_nodes(float $siteorder, float $courseorder, float $moduleorder) { 43 global $PAGE; 44 45 // Create a secondary navigation and populate with some dummy nodes. 46 $secondary = new secondary($PAGE); 47 $secondary->add('Site Admin', '#', secondary::TYPE_SETTING, null, 'siteadmin'); 48 $secondary->add('Course Admin', '#', secondary::TYPE_CUSTOM, null, 'courseadmin'); 49 $secondary->add('Module Admin', '#', secondary::TYPE_SETTING, null, 'moduleadmin'); 50 $nodes = [ 51 navigation_node::TYPE_SETTING => [ 52 'siteadmin' => $siteorder, 53 'moduleadmin' => $courseorder, 54 ], 55 navigation_node::TYPE_CUSTOM => [ 56 'courseadmin' => $moduleorder, 57 ] 58 ]; 59 $expectednodes = [ 60 "$siteorder" => 'siteadmin', 61 "$courseorder" => 'moduleadmin', 62 "$moduleorder" => 'courseadmin', 63 ]; 64 65 $method = new ReflectionMethod('core\navigation\views\secondary', 'get_leaf_nodes'); 66 $method->setAccessible(true); 67 $sortednodes = $method->invoke($secondary, $secondary, $nodes); 68 foreach ($sortednodes as $order => $node) { 69 $this->assertEquals($expectednodes[$order], $node->key); 70 } 71 } 72 73 /** 74 * Data provider for test_get_leaf_nodes 75 * @return array 76 */ 77 public function leaf_nodes_order_provider(): array { 78 return [ 79 'Initialise the order with whole numbers' => [3, 2, 1], 80 'Initialise the order with a mix of whole and float numbers' => [2.1, 2, 1], 81 ]; 82 } 83 84 /** 85 * Test the initialise in different contexts 86 * 87 * @param string $context The context to setup for - course, module, system 88 * @param string $expectedfirstnode The expected first node 89 * @param string $header The expected string 90 * @param string $activenode The expected active node 91 * @param string $courseformat The used course format (only applicable in the course and module context). 92 * @return void 93 * @dataProvider setting_initialise_provider 94 */ 95 public function test_setting_initialise(string $context, string $expectedfirstnode, 96 string $header, string $activenode, string $courseformat = 'topics'): void { 97 global $PAGE, $SITE; 98 $this->resetAfterTest(); 99 $this->setAdminUser(); 100 $pagecourse = $SITE; 101 $pageurl = '/'; 102 switch ($context) { 103 case 'course': 104 $pagecourse = $this->getDataGenerator()->create_course(['format' => $courseformat]); 105 $contextrecord = \context_course::instance($pagecourse->id, MUST_EXIST); 106 if ($courseformat === 'singleactivity') { 107 $pageurl = new \moodle_url('/course/edit.php', ['id' => $pagecourse->id]); 108 } else { 109 $pageurl = new \moodle_url('/course/view.php', ['id' => $pagecourse->id]); 110 } 111 break; 112 case 'module': 113 $pagecourse = $this->getDataGenerator()->create_course(['format' => $courseformat]); 114 $assign = $this->getDataGenerator()->create_module('assign', ['course' => $pagecourse->id]); 115 $cm = get_coursemodule_from_id('assign', $assign->cmid); 116 $contextrecord = \context_module::instance($cm->id); 117 $pageurl = new \moodle_url('/mod/assign/view.php', ['id' => $cm->id]); 118 $PAGE->set_cm($cm); 119 break; 120 case 'system': 121 $contextrecord = \context_system::instance(); 122 $PAGE->set_pagelayout('admin'); 123 $pageurl = new \moodle_url('/admin/index.php'); 124 125 } 126 $PAGE->set_url($pageurl); 127 navigation_node::override_active_url($pageurl); 128 $PAGE->set_course($pagecourse); 129 $PAGE->set_context($contextrecord); 130 131 $node = new secondary($PAGE); 132 $node->initialise(); 133 $children = $node->get_children_key_list(); 134 $this->assertEquals($expectedfirstnode, $children[0]); 135 $this->assertEquals(get_string($header), $node->headertitle); 136 $this->assertEquals($activenode, $node->activenode->text); 137 } 138 139 /** 140 * Data provider for the test_setting_initialise function 141 * @return array 142 */ 143 public function setting_initialise_provider(): array { 144 return [ 145 'Testing in a course context' => ['course', 'coursehome', 'courseheader', 'Course'], 146 'Testing in a course context using a single activity course format' => 147 ['course', 'course', 'courseheader', 'Course', 'singleactivity'], 148 'Testing in a module context' => ['module', 'modulepage', 'activityheader', 'Assignment'], 149 'Testing in a module context using a single activity course format' => 150 ['module', 'course', 'activityheader', 'Activity', 'singleactivity'], 151 'Testing in a site admin' => ['system', 'siteadminnode', 'homeheader', 'General'], 152 ]; 153 } 154 155 /** 156 * Get the nav tree initialised to test active_node_scan. 157 * 158 * This is to test the secondary nav with navigation_node instance. 159 * 160 * @param string|null $seturl The url set for $PAGE. 161 * @return navigation_node The initialised nav tree. 162 */ 163 private function get_tree_initilised_to_set_activate(?string $seturl = null): navigation_node { 164 global $PAGE; 165 166 $node = new secondary($PAGE); 167 168 $node->key = 'mytestnode'; 169 $node->type = navigation_node::TYPE_SYSTEM; 170 $node->add('first child', null, navigation_node::TYPE_CUSTOM, 'firstchld', 'firstchild'); 171 $child2 = $node->add('second child', null, navigation_node::TYPE_COURSE, 'secondchld', 'secondchild'); 172 $child3 = $node->add('third child', null, navigation_node::TYPE_CONTAINER, 'thirdchld', 'thirdchild'); 173 $node->add('fourth child', null, navigation_node::TYPE_ACTIVITY, 'fourthchld', 'fourthchld'); 174 $node->add('fifth child', '/my', navigation_node::TYPE_CATEGORY, 'fifthchld', 'fifthchild'); 175 176 // If seturl is null then set actionurl of child6 to '/'. 177 if ($seturl === null) { 178 $child6actionurl = new \moodle_url('/'); 179 } else { 180 // If seturl is provided then set actionurl of child6 to '/foo'. 181 $child6actionurl = new \moodle_url('/foo'); 182 } 183 $child6 = $child2->add('sixth child', $child6actionurl, navigation_node::TYPE_COURSE, 'sixthchld', 'sixthchild'); 184 // Activate the sixthchild node. 185 $child6->make_active(); 186 $child2->add('seventh child', null, navigation_node::TYPE_COURSE, 'seventhchld', 'seventhchild'); 187 $child8 = $child2->add('eighth child', null, navigation_node::TYPE_CUSTOM, 'eighthchld', 'eighthchild'); 188 $child8->add('nineth child', null, navigation_node::TYPE_CUSTOM, 'ninethchld', 'ninethchild'); 189 $child3->add('tenth child', null, navigation_node::TYPE_CUSTOM, 'tenthchld', 'tenthchild'); 190 191 return $node; 192 } 193 194 /** 195 * Testing active_node_scan on navigation_node instance. 196 * 197 * @param string $expectedkey The expected node key. 198 * @param string|null $key The key set by user using set_secondary_active_tab. 199 * @param string|null $seturl The url set by user. 200 * @return void 201 * @dataProvider active_node_scan_provider 202 */ 203 public function test_active_node_scan(string $expectedkey, ?string $key = null, ?string $seturl = null): void { 204 global $PAGE; 205 206 if ($seturl !== null) { 207 navigation_node::override_active_url(new \moodle_url($seturl)); 208 } else { 209 $PAGE->set_url('/'); 210 navigation_node::override_active_url(new \moodle_url('/')); 211 } 212 if ($key !== null) { 213 $PAGE->set_secondary_active_tab($key); 214 } 215 216 $node = $this->get_tree_initilised_to_set_activate($seturl); 217 $secondary = new secondary($PAGE); 218 $method = new ReflectionMethod('core\navigation\views\secondary', 'active_node_scan'); 219 $method->setAccessible(true); 220 221 $result = $method->invoke($secondary, $node); 222 223 if ($expectedkey !== '') { 224 $this->assertInstanceOf('navigation_node', $result); 225 $this->assertEquals($expectedkey, $result->key); 226 } else { 227 $this->assertNull($result); 228 } 229 } 230 231 /** 232 * Data provider for the active_node_scan_provider 233 * 234 * @return array 235 */ 236 public function active_node_scan_provider(): array { 237 return [ 238 'Test by activating node adjacent to root node' 239 => ['firstchild', 'firstchild'], 240 'Activate a grand child node of the root' 241 => ['thirdchild', 'tenthchild'], 242 'When child node is activated the parent node is activated and returned' 243 => ['secondchild', null], 244 'Test by setting an empty string as node key to activate' => ['secondchild', ''], 245 'Activate a node which does not exist in the tree' 246 => ['', 'foobar'], 247 'Activate the leaf node of the tree' => ['secondchild', 'ninethchild', null, true], 248 'Changing the $PAGE url which is different from action url of child6 and not setting active tab manually' 249 => ['', null, '/foobar'], 250 'Having $PAGE url and child6 action url same and not setting active tab manually' 251 => ['secondchild', null, '/foo'], 252 ]; 253 } 254 255 /** 256 * Test the force_nodes_into_more_menu method. 257 * 258 * @param array $secondarynavnodesdata The array which contains the data used to generate the secondary navigation 259 * @param array $defaultmoremenunodes The array containing the keys of the navigation nodes which should be added 260 * to the "more" menu by default 261 * @param int|null $maxdisplayednodes The maximum limit of navigation nodes displayed in the secondary navigation 262 * @param array $expecedmoremenunodes The array containing the keys of the expected navigation nodes which are 263 * forced into the "more" menu 264 * @dataProvider force_nodes_into_more_menu_provider 265 */ 266 public function test_force_nodes_into_more_menu(array $secondarynavnodesdata, array $defaultmoremenunodes, 267 ?int $maxdisplayednodes, array $expecedmoremenunodes) { 268 global $PAGE; 269 270 // Create a dummy secondary navigation. 271 $secondary = new secondary($PAGE); 272 foreach ($secondarynavnodesdata as $nodedata) { 273 $secondary->add($nodedata['text'], '#', secondary::TYPE_SETTING, null, $nodedata['key']); 274 } 275 276 $method = new ReflectionMethod('core\navigation\views\secondary', 'force_nodes_into_more_menu'); 277 $method->setAccessible(true); 278 $method->invoke($secondary, $defaultmoremenunodes, $maxdisplayednodes); 279 280 $actualmoremenunodes = []; 281 foreach ($secondary->children as $node) { 282 if ($node->forceintomoremenu) { 283 $actualmoremenunodes[] = $node->key; 284 } 285 } 286 // Assert that the actual nodes forced into the "more" menu matches the expected ones. 287 $this->assertEquals($expecedmoremenunodes, $actualmoremenunodes); 288 } 289 290 /** 291 * Data provider for the test_force_nodes_into_more_menu function. 292 * 293 * @return array 294 */ 295 public function force_nodes_into_more_menu_provider(): array { 296 return [ 297 'The total number of navigation nodes exceeds the max display limit (5); ' . 298 'navnode2 and navnode4 are forced into "more" menu by default.' => 299 [ 300 [ 301 [ 'text' => 'Navigation node 1', 'key' => 'navnode1'], 302 [ 'text' => 'Navigation node 2', 'key' => 'navnode2'], 303 [ 'text' => 'Navigation node 3', 'key' => 'navnode3'], 304 [ 'text' => 'Navigation node 4', 'key' => 'navnode4'], 305 [ 'text' => 'Navigation node 5', 'key' => 'navnode5'], 306 [ 'text' => 'Navigation node 6', 'key' => 'navnode6'], 307 [ 'text' => 'Navigation node 7', 'key' => 'navnode7'], 308 [ 'text' => 'Navigation node 8', 'key' => 'navnode8'], 309 [ 'text' => 'Navigation node 9', 'key' => 'navnode9'], 310 ], 311 [ 312 'navnode2', 313 'navnode4', 314 ], 315 5, 316 [ 317 'navnode2', 318 'navnode4', 319 'navnode8', 320 'navnode9', 321 ], 322 ], 323 'The total number of navigation nodes does not exceed the max display limit (5); ' . 324 'navnode2 and navnode4 are forced into "more" menu by default.' => 325 [ 326 [ 327 [ 'text' => 'Navigation node 1', 'key' => 'navnode1'], 328 [ 'text' => 'Navigation node 2', 'key' => 'navnode2'], 329 [ 'text' => 'Navigation node 3', 'key' => 'navnode3'], 330 [ 'text' => 'Navigation node 4', 'key' => 'navnode4'], 331 [ 'text' => 'Navigation node 5', 'key' => 'navnode5'], 332 ], 333 [ 334 'navnode2', 335 'navnode4', 336 ], 337 5, 338 [ 339 'navnode2', 340 'navnode4', 341 ], 342 ], 343 'The max display limit of navigation nodes is not defined; ' . 344 'navnode2 and navnode4 are forced into "more" menu by default.' => 345 [ 346 [ 347 [ 'text' => 'Navigation node 1', 'key' => 'navnode1'], 348 [ 'text' => 'Navigation node 2', 'key' => 'navnode2'], 349 [ 'text' => 'Navigation node 3', 'key' => 'navnode3'], 350 [ 'text' => 'Navigation node 4', 'key' => 'navnode4'], 351 [ 'text' => 'Navigation node 5', 'key' => 'navnode5'], 352 ], 353 [ 354 'navnode2', 355 'navnode4', 356 ], 357 null, 358 [ 359 'navnode2', 360 'navnode4', 361 ], 362 ], 363 'The total number of navigation nodes exceeds the max display limit (5); ' . 364 'no forced navigation nodes into "more" menu by default.' => 365 [ 366 [ 367 [ 'text' => 'Navigation node 1', 'key' => 'navnode1'], 368 [ 'text' => 'Navigation node 2', 'key' => 'navnode2'], 369 [ 'text' => 'Navigation node 3', 'key' => 'navnode3'], 370 [ 'text' => 'Navigation node 4', 'key' => 'navnode4'], 371 [ 'text' => 'Navigation node 5', 'key' => 'navnode5'], 372 [ 'text' => 'Navigation node 6', 'key' => 'navnode6'], 373 [ 'text' => 'Navigation node 7', 'key' => 'navnode7'], 374 [ 'text' => 'Navigation node 8', 'key' => 'navnode8'], 375 ], 376 [], 377 5, 378 [ 379 'navnode6', 380 'navnode7', 381 'navnode8', 382 ], 383 ], 384 'The total number of navigation nodes does not exceed the max display limit (5); ' . 385 'no forced navigation nodes into "more" menu by default.' => 386 [ 387 [ 388 [ 'text' => 'Navigation node 1', 'key' => 'navnode1'], 389 [ 'text' => 'Navigation node 2', 'key' => 'navnode2'], 390 [ 'text' => 'Navigation node 3', 'key' => 'navnode3'], 391 [ 'text' => 'Navigation node 4', 'key' => 'navnode4'], 392 [ 'text' => 'Navigation node 5', 'key' => 'navnode5'], 393 [ 'text' => 'Navigation node 6', 'key' => 'navnode6'], 394 ], 395 [], 396 5, 397 [ 398 'navnode6', 399 ], 400 ], 401 'The max display limit of navigation nodes is not defined; ' . 402 'no forced navigation nodes into "more" menu by default.' => 403 [ 404 [ 405 [ 'text' => 'Navigation node 1', 'key' => 'navnode1'], 406 [ 'text' => 'Navigation node 2', 'key' => 'navnode2'], 407 [ 'text' => 'Navigation node 3', 'key' => 'navnode3'], 408 [ 'text' => 'Navigation node 4', 'key' => 'navnode4'], 409 [ 'text' => 'Navigation node 5', 'key' => 'navnode5'], 410 [ 'text' => 'Navigation node 6', 'key' => 'navnode6'], 411 ], 412 [], 413 null, 414 [], 415 ], 416 ]; 417 } 418 419 /** 420 * Recursive call to generate a navigation node given an array definition. 421 * 422 * @param array $structure 423 * @param string $parentkey 424 * @return navigation_node 425 */ 426 private function generate_node_tree_construct(array $structure, string $parentkey): navigation_node { 427 $node = navigation_node::create($parentkey, null, navigation_node::TYPE_CUSTOM, '', $parentkey); 428 foreach ($structure as $key => $value) { 429 if (is_array($value)) { 430 $children = $value['children'] ?? $value; 431 $child = $this->generate_node_tree_construct($children, $key); 432 if (isset($value['action'])) { 433 $child->action = new \moodle_url($value['action']); 434 } 435 $node->add_node($child); 436 } else { 437 $node->add($key, $value, navigation_node::TYPE_CUSTOM, '', $key); 438 } 439 } 440 441 return $node; 442 } 443 444 /** 445 * Test the nodes_match_current_url function. 446 * 447 * @param string $selectedurl 448 * @param string $expectednode 449 * @dataProvider nodes_match_current_url_provider 450 */ 451 public function test_nodes_match_current_url(string $selectedurl, string $expectednode) { 452 global $PAGE; 453 $structure = [ 454 'parentnode1' => [ 455 'child1' => '/my', 456 'child2' => [ 457 'child2.1' => '/view/course.php', 458 'child2.2' => '/view/admin.php', 459 ] 460 ] 461 ]; 462 $node = $this->generate_node_tree_construct($structure, 'primarynode'); 463 $node->action = new \moodle_url('/'); 464 465 $PAGE->set_url($selectedurl); 466 $secondary = new secondary($PAGE); 467 $method = new ReflectionMethod('core\navigation\views\secondary', 'nodes_match_current_url'); 468 $method->setAccessible(true); 469 $response = $method->invoke($secondary, $node); 470 471 $this->assertSame($response->key ?? null, $expectednode); 472 } 473 474 /** 475 * Provider for test_nodes_match_current_url 476 * 477 * @return \string[][] 478 */ 479 public function nodes_match_current_url_provider(): array { 480 return [ 481 "Match url to a node that is a deep nested" => [ 482 '/view/course.php', 483 'child2.1', 484 ], 485 "Match url to a parent node with children" => [ 486 '/', 'primarynode' 487 ], 488 "Match url to a child node" => [ 489 '/my', 'child1' 490 ], 491 ]; 492 } 493 494 /** 495 * Test the get_menu_array function 496 * 497 * @param string $selected 498 * @param array $expected 499 * @dataProvider get_menu_array_provider 500 */ 501 public function test_get_menu_array(string $selected, array $expected) { 502 global $PAGE; 503 504 // Custom nodes - mimicing nodes added via 3rd party plugins. 505 $structure = [ 506 'parentnode1' => [ 507 'child1' => '/my', 508 'child2' => [ 509 'action' => '/test.php', 510 'children' => [ 511 'child2.1' => '/view/course.php?child=2', 512 'child2.2' => '/view/admin.php?child=2', 513 'child2.3' => '/test.php', 514 ] 515 ], 516 'child3' => [ 517 'child3.1' => '/view/course.php?child=3', 518 'child3.2' => '/view/admin.php?child=3', 519 ] 520 ], 521 'parentnode2' => "/view/module.php" 522 ]; 523 524 $secondary = new secondary($PAGE); 525 $secondary->add_node($this->generate_node_tree_construct($structure, 'primarynode')); 526 $selectednode = $secondary->find($selected, null); 527 $response = \core\navigation\views\secondary::create_menu_element([$selectednode]); 528 529 $this->assertSame($expected, $response); 530 } 531 532 /** 533 * Provider for test_get_menu_array 534 * 535 * @return array[] 536 */ 537 public function get_menu_array_provider(): array { 538 return [ 539 "Fetch information from a node with action and no children" => [ 540 'child1', 541 [ 542 'https://www.example.com/moodle/my' => 'child1' 543 ], 544 ], 545 "Fetch information from a node with no action and children" => [ 546 'child3', 547 [ 548 'https://www.example.com/moodle/view/course.php?child=3' => 'child3.1', 549 'https://www.example.com/moodle/view/admin.php?child=3' => 'child3.2' 550 ], 551 ], 552 "Fetch information from a node with children" => [ 553 'child2', 554 [ 555 'https://www.example.com/moodle/test.php' => 'child2.3', 556 'https://www.example.com/moodle/view/course.php?child=2' => 'child2.1', 557 'https://www.example.com/moodle/view/admin.php?child=2' => 'child2.2' 558 ], 559 ], 560 "Fetch information from a node with an action and no children" => [ 561 'parentnode2', 562 ['https://www.example.com/moodle/view/module.php' => 'parentnode2'], 563 ], 564 "Fetch information from a node with an action and multiple nested children" => [ 565 'parentnode1', 566 [ 567 [ 568 'parentnode1' => [ 569 'https://www.example.com/moodle/my' => 'child1' 570 ], 571 'child2' => [ 572 'https://www.example.com/moodle/test.php' => 'child2', 573 'https://www.example.com/moodle/view/course.php?child=2' => 'child2.1', 574 'https://www.example.com/moodle/view/admin.php?child=2' => 'child2.2', 575 ], 576 'child3' => [ 577 'https://www.example.com/moodle/view/course.php?child=3' => 'child3.1', 578 'https://www.example.com/moodle/view/admin.php?child=3' => 'child3.2' 579 ] 580 ] 581 ], 582 ], 583 ]; 584 } 585 586 /** 587 * Test the get_node_with_first_action function 588 * 589 * @param string $selectedkey 590 * @param string|null $expectedkey 591 * @dataProvider get_node_with_first_action_provider 592 */ 593 public function test_get_node_with_first_action(string $selectedkey, ?string $expectedkey) { 594 global $PAGE; 595 $structure = [ 596 'parentnode1' => [ 597 'child1' => [ 598 'child1.1' => null 599 ], 600 'child2' => [ 601 'child2.1' => [ 602 'child2.1.1' => [ 603 'action' => '/test.php', 604 'children' => [ 605 'child2.1.1.1' => '/view/course.php?child=2', 606 'child2.1.1.2' => '/view/admin.php?child=2', 607 ] 608 ] 609 ] 610 ], 611 'child3' => [ 612 'child3.1' => '/view/course.php?child=3', 613 'child3.2' => '/view/admin.php?child=3', 614 ] 615 ], 616 'parentnode2' => "/view/module.php" 617 ]; 618 619 $nodes = $this->generate_node_tree_construct($structure, 'primarynode'); 620 $selectednode = $nodes->find($selectedkey, null); 621 622 $expected = null; 623 // Expected response will be the parent node with the action updated. 624 if ($expectedkey) { 625 $expectedbasenode = clone $selectednode; 626 $actionfromnode = $nodes->find($expectedkey, null); 627 $expectedbasenode->action = $actionfromnode->action; 628 $expected = $expectedbasenode; 629 } 630 631 $secondary = new secondary($PAGE); 632 $method = new ReflectionMethod('core\navigation\views\secondary', 'get_node_with_first_action'); 633 $method->setAccessible(true); 634 $response = $method->invoke($secondary, $selectednode, $selectednode); 635 $this->assertEquals($expected, $response); 636 } 637 638 /** 639 * Provider for test_get_node_with_first_action 640 * 641 * @return array 642 */ 643 public function get_node_with_first_action_provider(): array { 644 return [ 645 "Search for action when parent has no action and multiple children with actions" => [ 646 "child3", 647 "child3.1", 648 ], 649 "Search for action when parent child is deeply nested." => [ 650 "child2", 651 "child2.1.1" 652 ], 653 "No navigation node returned when node has no children" => [ 654 "parentnode2", 655 null 656 ], 657 "No navigation node returned when node has children but no actions available." => [ 658 "child1", 659 null 660 ], 661 ]; 662 } 663 664 /** 665 * Test the add_external_nodes_to_secondary function. 666 * 667 * @param array $structure The structure of the navigation node tree to setup with. 668 * @param array $expectednodes The expected nodes added to the secondary navigation 669 * @param bool $separatenode Whether or not to create a separate node to add nodes to. 670 * @dataProvider add_external_nodes_to_secondary_provider 671 */ 672 public function test_add_external_nodes_to_secondary(array $structure, array $expectednodes, bool $separatenode = false) { 673 global $PAGE; 674 675 $this->resetAfterTest(); 676 $course = $this->getDataGenerator()->create_course(); 677 $context = \context_course::instance($course->id); 678 $PAGE->set_context($context); 679 $PAGE->set_url('/'); 680 681 $node = $this->generate_node_tree_construct($structure, 'parentnode'); 682 $secondary = new secondary($PAGE); 683 $secondary->add_node($node); 684 $firstnode = $node->get('parentnode1'); 685 $customparent = null; 686 if ($separatenode) { 687 $customparent = navigation_node::create('Custom parent'); 688 } 689 690 $method = new ReflectionMethod('core\navigation\views\secondary', 'add_external_nodes_to_secondary'); 691 $method->setAccessible(true); 692 $method->invoke($secondary, $firstnode, $firstnode, $customparent); 693 694 $actualnodes = $separatenode ? $customparent->get_children_key_list() : $secondary->get_children_key_list(); 695 $this->assertEquals($expectednodes, $actualnodes); 696 } 697 698 /** 699 * Provider for the add_external_nodes_to_secondary function. 700 * 701 * @return array 702 */ 703 public function add_external_nodes_to_secondary_provider() { 704 return [ 705 "Container node with internal action and external children" => [ 706 [ 707 'parentnode1' => [ 708 'action' => '/test.php', 709 'children' => [ 710 'child2.1' => 'https://example.org', 711 'child2.2' => 'https://example.net', 712 ] 713 ] 714 ], 715 ['parentnode', 'parentnode1'] 716 ], 717 "Container node with external action and external children" => [ 718 [ 719 'parentnode1' => [ 720 'action' => 'https://example.com', 721 'children' => [ 722 'child2.1' => 'https://example.org', 723 'child2.2' => 'https://example.net', 724 ] 725 ] 726 ], 727 ['parentnode', 'parentnode1', 'child2.1', 'child2.2'] 728 ], 729 "Container node with external action and internal children" => [ 730 [ 731 'parentnode1' => [ 732 'action' => 'https://example.org', 733 'children' => [ 734 'child2.1' => '/view/course.php', 735 'child2.2' => '/view/admin.php', 736 ] 737 ] 738 ], 739 ['parentnode', 'parentnode1', 'child2.1', 'child2.2'] 740 ], 741 "Container node with internal actions and internal children" => [ 742 [ 743 'parentnode1' => [ 744 'action' => '/test.php', 745 'children' => [ 746 'child2.1' => '/course.php', 747 'child2.2' => '/admin.php', 748 ] 749 ] 750 ], 751 ['parentnode', 'parentnode1'] 752 ], 753 "Container node with internal action and external children adding to custom node" => [ 754 [ 755 'parentnode1' => [ 756 'action' => '/test.php', 757 'children' => [ 758 'child2.1' => 'https://example.org', 759 'child2.2' => 'https://example.net', 760 ] 761 ] 762 ], 763 ['parentnode1'], true 764 ], 765 "Container node with external action and external children adding to custom node" => [ 766 [ 767 'parentnode1' => [ 768 'action' => 'https://example.com', 769 'children' => [ 770 'child2.1' => 'https://example.org', 771 'child2.2' => 'https://example.net', 772 ] 773 ] 774 ], 775 ['parentnode1', 'child2.1', 'child2.2'], true 776 ], 777 "Container node with external action and internal children adding to custom node" => [ 778 [ 779 'parentnode1' => [ 780 'action' => 'https://example.org', 781 'children' => [ 782 'child2.1' => '/view/course.php', 783 'child2.2' => '/view/admin.php', 784 ] 785 ] 786 ], 787 ['parentnode1', 'child2.1', 'child2.2'], true 788 ], 789 "Container node with internal actions and internal children adding to custom node" => [ 790 [ 791 'parentnode1' => [ 792 'action' => '/test.php', 793 'children' => [ 794 'child2.1' => '/course.php', 795 'child2.2' => '/admin.php', 796 ] 797 ] 798 ], 799 ['parentnode1'], true 800 ], 801 ]; 802 } 803 804 /** 805 * Test the get_overflow_menu_data function 806 * 807 * @param string $selectedurl 808 * @param bool $expectednull 809 * @param bool $emptynode 810 * @dataProvider get_overflow_menu_data_provider 811 */ 812 public function test_get_overflow_menu_data(string $selectedurl, bool $expectednull, bool $emptynode = false) { 813 global $PAGE; 814 815 $this->resetAfterTest(); 816 // Custom nodes - mimicing nodes added via 3rd party plugins. 817 $structure = [ 818 'parentnode1' => [ 819 'child1' => '/my', 820 'child2' => [ 821 'action' => '/test.php', 822 'children' => [ 823 'child2.1' => '/view/course.php', 824 'child2.2' => '/view/admin.php', 825 ] 826 ] 827 ], 828 'parentnode2' => "/view/module.php" 829 ]; 830 831 $course = $this->getDataGenerator()->create_course(); 832 $context = \context_course::instance($course->id); 833 $PAGE->set_context($context); 834 835 $PAGE->set_url($selectedurl); 836 navigation_node::override_active_url(new \moodle_url($selectedurl)); 837 $node = $this->generate_node_tree_construct($structure, 'primarynode'); 838 $node->action = new \moodle_url('/'); 839 840 $secondary = new secondary($PAGE); 841 $secondary->add_node($node); 842 $PAGE->settingsnav->add_node(clone $node); 843 $secondary->add('Course home', '/coursehome.php', navigation_node::TYPE_CUSTOM, '', 'coursehome'); 844 $secondary->add('Course settings', '/course/settings.php', navigation_node::TYPE_CUSTOM, '', 'coursesettings'); 845 846 // Test for an empty node without children and action. 847 if ($emptynode) { 848 $node = $secondary->add('Course management', null, navigation_node::TYPE_CUSTOM, '', 'course'); 849 $node->make_active(); 850 } else { 851 // Set the correct node as active. 852 $method = new ReflectionMethod('core\navigation\views\secondary', 'scan_for_active_node'); 853 $method->setAccessible(true); 854 $method->invoke($secondary, $secondary); 855 } 856 857 $method = new ReflectionMethod('core\navigation\views\secondary', 'get_overflow_menu_data'); 858 $method->setAccessible(true); 859 $response = $method->invoke($secondary); 860 if ($expectednull) { 861 $this->assertNull($response); 862 } else { 863 $this->assertIsObject($response); 864 $this->assertInstanceOf('url_select', $response); 865 } 866 } 867 868 /** 869 * Data provider for test_get_overflow_menu_data 870 * 871 * @return string[] 872 */ 873 public function get_overflow_menu_data_provider(): array { 874 return [ 875 "Active node is the course home node" => [ 876 '/coursehome.php', 877 true 878 ], 879 "Active node is one with an action and no children" => [ 880 '/view/module.php', 881 false 882 ], 883 "Active node is one with an action and children" => [ 884 '/', 885 false 886 ], 887 "Active node is one without an action and children" => [ 888 '/', 889 true, 890 true, 891 ], 892 "Active node is one with an action and children but is NOT in settingsnav" => [ 893 '/course/settings.php', 894 true 895 ], 896 ]; 897 } 898 899 /** 900 * Test the course administration settings return an overflow menu. 901 * 902 * @dataProvider get_overflow_menu_data_course_admin_provider 903 * @param string $url Url of the page we are testing. 904 * @param string $contextidentifier id or contextid or something similar. 905 * @param bool $expected The expected return. True to return the overflow menu otherwise false for nothing. 906 */ 907 public function test_get_overflow_menu_data_course_admin(string $url, string $contextidentifier, bool $expected): void { 908 global $PAGE; 909 $this->resetAfterTest(); 910 $this->setAdminUser(); 911 912 $pagecourse = $this->getDataGenerator()->create_course(); 913 $contextrecord = \context_course::instance($pagecourse->id, MUST_EXIST); 914 915 $id = ($contextidentifier == 'contextid') ? $contextrecord->id : $pagecourse->id; 916 917 $pageurl = new \moodle_url($url, [$contextidentifier => $id]); 918 $PAGE->set_url($pageurl); 919 navigation_node::override_active_url($pageurl); 920 $PAGE->set_course($pagecourse); 921 $PAGE->set_context($contextrecord); 922 923 $node = new secondary($PAGE); 924 $node->initialise(); 925 $result = $node->get_overflow_menu_data(); 926 if ($expected) { 927 $this->assertInstanceOf('url_select', $result); 928 $this->assertTrue($pageurl->compare($result->selected)); 929 } else { 930 $this->assertNull($result); 931 } 932 } 933 934 /** 935 * Data provider for the other half of the method thing 936 * 937 * @return array Provider information. 938 */ 939 public function get_overflow_menu_data_course_admin_provider(): array { 940 return [ 941 "Backup page returns overflow" => [ 942 '/backup/backup.php', 943 'id', 944 true 945 ], 946 "Restore course page returns overflow" => [ 947 '/backup/restorefile.php', 948 'contextid', 949 true 950 ], 951 "Import course page returns overflow" => [ 952 '/backup/import.php', 953 'id', 954 true 955 ], 956 "Course copy page returns overflow" => [ 957 '/backup/copy.php', 958 'id', 959 true 960 ], 961 "Course reset page returns overflow" => [ 962 '/course/reset.php', 963 'id', 964 true 965 ], 966 // The following pages should not return the overflow menu. 967 "Course page returns nothing" => [ 968 '/course/view.php', 969 'id', 970 false 971 ], 972 "Question bank should return nothing" => [ 973 '/question/edit.php', 974 'courseid', 975 false 976 ], 977 "Reports should return nothing" => [ 978 '/report/log/index.php', 979 'id', 980 false 981 ], 982 "Participants page should return nothing" => [ 983 '/user/index.php', 984 'id', 985 false 986 ] 987 ]; 988 } 989 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body