Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.
   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  }