Differences Between: [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]
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\output; 18 19 use ReflectionMethod; 20 21 /** 22 * Primary navigation renderable test 23 * 24 * @package core 25 * @category navigation 26 * @copyright 2021 onwards Peter Dias 27 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 28 */ 29 class primary_test extends \advanced_testcase { 30 /** 31 * Basic setup to make sure the nav objects gets generated without any issues. 32 */ 33 public function setUp(): void { 34 global $PAGE; 35 $this->resetAfterTest(); 36 $pagecourse = $this->getDataGenerator()->create_course(); 37 $assign = $this->getDataGenerator()->create_module('assign', ['course' => $pagecourse->id]); 38 $cm = get_coursemodule_from_id('assign', $assign->cmid); 39 $contextrecord = \context_module::instance($cm->id); 40 $pageurl = new \moodle_url('/mod/assign/view.php', ['id' => $cm->instance]); 41 $PAGE->set_cm($cm); 42 $PAGE->set_url($pageurl); 43 $PAGE->set_course($pagecourse); 44 $PAGE->set_context($contextrecord); 45 } 46 47 /** 48 * Test the primary export to confirm we are getting the nodes 49 * 50 * @dataProvider primary_export_provider 51 * @param bool $withcustom Setup with custom menu 52 * @param bool $withlang Setup with langs 53 * @param string $userloggedin The type of user ('admin' or 'guest') if creating setup with logged in user, 54 * otherwise consider the user as non-logged in 55 * @param array $expecteditems An array of nodes expected with content in them. 56 */ 57 public function test_primary_export(bool $withcustom, bool $withlang, string $userloggedin, array $expecteditems) { 58 global $PAGE, $CFG; 59 if ($withcustom) { 60 $CFG->custommenuitems = "Course search|/course/search.php 61 Google|https://google.com.au/ 62 Netflix|https://netflix.com/au"; 63 } 64 if ($userloggedin === 'admin') { 65 $this->setAdminUser(); 66 } else if ($userloggedin === 'guest') { 67 $this->setGuestUser(); 68 } else { 69 $this->setUser(0); 70 } 71 72 // Mimic multiple langs installed. To trigger responses 'get_list_of_translations'. 73 // Note: The text/title of the nodes generated will be 'English(fr), English(de)' but we don't care about this. 74 // We are testing whether the nodes gets generated when the lang menu is available. 75 if ($withlang) { 76 mkdir("$CFG->dataroot/lang/de", 0777, true); 77 mkdir("$CFG->dataroot/lang/fr", 0777, true); 78 // Ensure the new langs are picked up and not taken from the cache. 79 $stringmanager = get_string_manager(); 80 $stringmanager->reset_caches(true); 81 } 82 83 $primary = new primary($PAGE); 84 $renderer = $PAGE->get_renderer('core'); 85 $data = array_filter($primary->export_for_template($renderer)); 86 87 // Assert that the number of returned menu items equals the expected result. 88 $this->assertCount(count($expecteditems), $data); 89 // Assert that returned menu items match the expected items. 90 foreach ($data as $menutype => $value) { 91 $this->assertTrue(in_array($menutype, $expecteditems)); 92 } 93 // When the user is logged in (excluding guest access), assert that lang menu is included as a part of the 94 // user menu when multiple languages are installed. 95 if (isloggedin() && !isguestuser()) { 96 // Look for a language menu item within the user menu items. 97 $usermenulang = array_filter($data['user']['items'], function($usermenuitem) { 98 return $usermenuitem->itemtype !== 'divider' && $usermenuitem->title === get_string('language'); 99 }); 100 if ($withlang) { // If multiple languages are installed. 101 // Assert that the language menu exists within the user menu. 102 $this->assertNotEmpty($usermenulang); 103 } else { // If the aren't any additional installed languages. 104 $this->assertEmpty($usermenulang); 105 } 106 } else { // Otherwise assert that the user menu does not contain any items. 107 $this->assertArrayNotHasKey('items', $data['user']); 108 } 109 } 110 111 /** 112 * Provider for the test_primary_export function. 113 * 114 * @return array 115 */ 116 public function primary_export_provider(): array { 117 return [ 118 "Export the menu data when: custom menu exists; multiple langs installed; user is not logged in." => [ 119 true, true, '', ['mobileprimarynav', 'moremenu', 'lang', 'user'] 120 ], 121 "Export the menu data when: custom menu exists; langs not installed; user is not logged in." => [ 122 true, false, '', ['mobileprimarynav', 'moremenu', 'user'] 123 ], 124 "Export the menu data when: custom menu exists; multiple langs installed; logged in as admin." => [ 125 true, true, 'admin', ['mobileprimarynav', 'moremenu', 'user'] 126 ], 127 "Export the menu data when: custom menu exists; langs not installed; logged in as admin." => [ 128 true, false, 'admin', ['mobileprimarynav', 'moremenu', 'user'] 129 ], 130 "Export the menu data when: custom menu exists; multiple langs installed; logged in as guest." => [ 131 true, true, 'guest', ['mobileprimarynav', 'moremenu', 'lang', 'user'] 132 ], 133 "Export the menu data when: custom menu exists; langs not installed; logged in as guest." => [ 134 true, false, 'guest', ['mobileprimarynav', 'moremenu', 'user'] 135 ], 136 "Export the menu data when: custom menu does not exist; multiple langs installed; logged in as guest." => [ 137 false, true, 'guest', ['mobileprimarynav', 'moremenu', 'lang', 'user'] 138 ], 139 "Export the menu data when: custom menu does not exist; multiple langs installed; logged in as admin." => [ 140 false, true, 'admin', ['mobileprimarynav', 'moremenu', 'user'] 141 ], 142 "Export the menu data when: custom menu does not exist; langs not installed; user is not logged in." => [ 143 false, false, '', ['mobileprimarynav', 'moremenu', 'user'] 144 ], 145 ]; 146 } 147 148 /** 149 * Test the custom menu getter to confirm the nodes gets generated and are returned correctly. 150 * 151 * @dataProvider custom_menu_provider 152 * @param string $config 153 * @param array $expected 154 */ 155 public function test_get_custom_menu(string $config, array $expected) { 156 $actual = $this->get_custom_menu($config); 157 $this->assertEquals($expected, $actual); 158 } 159 160 /** 161 * Helper method to get the template data for the custommenuitem that is set here via parameter. 162 * @param string $config 163 * @return array 164 * @throws \ReflectionException 165 */ 166 protected function get_custom_menu(string $config): array { 167 global $CFG, $PAGE; 168 $CFG->custommenuitems = $config; 169 $output = new primary($PAGE); 170 $method = new ReflectionMethod('core\navigation\output\primary', 'get_custom_menu'); 171 $method->setAccessible(true); 172 $renderer = $PAGE->get_renderer('core'); 173 174 // We can't assert the value of each menuitem "moremenuid" property (because it's random). 175 $custommenufilter = static function(array $custommenu) use (&$custommenufilter): void { 176 foreach ($custommenu as $menuitem) { 177 unset($menuitem->moremenuid); 178 // Recursively move through child items. 179 $custommenufilter($menuitem->children); 180 } 181 }; 182 183 $actual = $method->invoke($output, $renderer); 184 $custommenufilter($actual); 185 return $actual; 186 } 187 188 /** 189 * Provider for test_get_custom_menu 190 * 191 * @return array 192 */ 193 public function custom_menu_provider(): array { 194 return [ 195 'Simple custom menu' => [ 196 "Course search|/course/search.php 197 Google|https://google.com.au/ 198 Netflix|https://netflix.com/au", [ 199 (object) [ 200 'text' => 'Course search', 201 'url' => 'https://www.example.com/moodle/course/search.php', 202 'title' => '', 203 'sort' => 1, 204 'children' => [], 205 'haschildren' => false, 206 ], 207 (object) [ 208 'text' => 'Google', 209 'url' => 'https://google.com.au/', 210 'title' => '', 211 'sort' => 2, 212 'children' => [], 213 'haschildren' => false, 214 ], 215 (object) [ 216 'text' => 'Netflix', 217 'url' => 'https://netflix.com/au', 218 'title' => '', 219 'sort' => 3, 220 'children' => [], 221 'haschildren' => false, 222 ], 223 ] 224 ], 225 'Complex, nested custom menu' => [ 226 "Moodle community|http://moodle.org 227 -Moodle free support|http://moodle.org/support 228 -Moodle development|http://moodle.org/development 229 --Moodle Tracker|http://tracker.moodle.org 230 --Moodle Docs|https://docs.moodle.org 231 -Moodle News|http://moodle.org/news 232 Moodle company 233 -Moodle commercial hosting|http://moodle.com/hosting 234 -Moodle commercial support|http://moodle.com/support", [ 235 (object) [ 236 'text' => 'Moodle community', 237 'url' => 'http://moodle.org', 238 'title' => '', 239 'sort' => 1, 240 'children' => [ 241 (object) [ 242 'text' => 'Moodle free support', 243 'url' => 'http://moodle.org/support', 244 'title' => '', 245 'sort' => 2, 246 'children' => [], 247 'haschildren' => false, 248 ], 249 (object) [ 250 'text' => 'Moodle development', 251 'url' => 'http://moodle.org/development', 252 'title' => '', 253 'sort' => 3, 254 'children' => [ 255 (object) [ 256 'text' => 'Moodle Tracker', 257 'url' => 'http://tracker.moodle.org', 258 'title' => '', 259 'sort' => 4, 260 'children' => [], 261 'haschildren' => false, 262 ], 263 (object) [ 264 'text' => 'Moodle Docs', 265 'url' => 'https://docs.moodle.org', 266 'title' => '', 267 'sort' => 5, 268 'children' => [], 269 'haschildren' => false, 270 ], 271 ], 272 'haschildren' => true, 273 ], 274 (object) [ 275 'text' => 'Moodle News', 276 'url' => 'http://moodle.org/news', 277 'title' => '', 278 'sort' => 6, 279 'children' => [], 280 'haschildren' => false, 281 ], 282 ], 283 'haschildren' => true, 284 ], 285 (object) [ 286 'text' => 'Moodle company', 287 'url' => null, 288 'title' => '', 289 'sort' => 7, 290 'children' => [ 291 (object) [ 292 'text' => 'Moodle commercial hosting', 293 'url' => 'http://moodle.com/hosting', 294 'title' => '', 295 'sort' => 8, 296 'children' => [], 297 'haschildren' => false, 298 ], 299 (object) [ 300 'text' => 'Moodle commercial support', 301 'url' => 'http://moodle.com/support', 302 'title' => '', 303 'sort' => 9, 304 'children' => [], 305 'haschildren' => false, 306 ], 307 ], 308 'haschildren' => true, 309 ], 310 ] 311 ] 312 ]; 313 } 314 315 /** 316 * Test the merge_primary_and_custom and the eval_is_active method. Merge primary and custom menu with different 317 * page urls and check that the correct nodes are active and open, depending on the data for each menu. 318 * 319 * @covers \core\navigation\output\primary::merge_primary_and_custom 320 * @covers \core\navigation\output\primary::flag_active_nodes 321 * @return void 322 * @throws \ReflectionException 323 * @throws \moodle_exception 324 */ 325 public function test_merge_primary_and_custom() { 326 global $PAGE; 327 328 $menu = $this->merge_and_render_menus(); 329 330 $this->assertEquals(4, count(\array_keys($menu))); 331 $msg = 'No active nodes for page ' . $PAGE->url; 332 $this->assertEmpty($this->get_menu_item_names_by_type($menu, 'isactive'), $msg); 333 $this->assertEmpty($this->get_menu_item_names_by_type($menu, 'isopen'), str_replace('active', 'open', $msg)); 334 335 $msg = 'Active nodes desktop for /course/search.php'; 336 $menu = $this->merge_and_render_menus('/course/search.php'); 337 $isactive = $this->get_menu_item_names_by_type($menu, 'isactive'); 338 $this->assertEquals(['Courses', 'Course search'], $isactive, $msg); 339 $this->assertEmpty($this->get_menu_item_names_by_type($menu, 'isopem'), str_replace('Active', 'Open', $msg)); 340 341 $msg = 'Active nodes mobile for /course/search.php'; 342 $menu = $this->merge_and_render_menus('/course/search.php', true); 343 $isactive = $this->get_menu_item_names_by_type($menu, 'isactive'); 344 $this->assertEquals(['Course search'], $isactive, $msg); 345 $isopen = $this->get_menu_item_names_by_type($menu, 'isopen'); 346 $this->assertEquals(['Courses'], $isopen, str_replace('Active', 'Open', $msg)); 347 348 $msg = 'Active nodes desktop for /course/search.php?areaids=core_course-course&q=test'; 349 $menu = $this->merge_and_render_menus('/course/search.php?areaids=core_course-course&q=test'); 350 $isactive = $this->get_menu_item_names_by_type($menu, 'isactive'); 351 $this->assertEquals(['Courses', 'Course search'], $isactive, $msg); 352 353 $msg = 'Active nodes desktop for /?theme=boost'; 354 $menu = $this->merge_and_render_menus('/?theme=boost'); 355 $isactive = $this->get_menu_item_names_by_type($menu, 'isactive'); 356 $this->assertEquals(['Theme', 'Boost'], $isactive, $msg); 357 } 358 359 /** 360 * Internal function to get an array of top menu items from the primary and the custom menu. The latter is defined 361 * in this function. 362 * @param string|null $url 363 * @param bool|null $ismobile 364 * @return array 365 * @throws \ReflectionException 366 * @throws \coding_exception 367 */ 368 protected function merge_and_render_menus(?string $url = null, ?bool $ismobile = false): array { 369 global $PAGE, $FULLME; 370 371 if ($url !== null) { 372 $PAGE->set_url($url); 373 $FULLME = $PAGE->url->out(); 374 } 375 $primary = new primary($PAGE); 376 377 $method = new ReflectionMethod('core\navigation\output\primary', 'get_primary_nav'); 378 $method->setAccessible(true); 379 $dataprimary = $method->invoke($primary); 380 381 // Take this custom menu that would come from the setting custommenitems. 382 $custommenuitems = <<< ENDMENU 383 Theme 384 -Boost|/?theme=boost 385 -Classic|/?theme=classic 386 -Purge Cache|/admin/purgecaches.php 387 Courses 388 -All courses|/course/ 389 -Course search|/course/search.php 390 -### 391 -FAQ|https://example.org/faq 392 -My Important Course|/course/view.php?id=4 393 Mobile app|https://example.org/app|Download our app 394 ENDMENU; 395 396 $datacustom = $this->get_custom_menu($custommenuitems); 397 $method = new ReflectionMethod('core\navigation\output\primary', 'merge_primary_and_custom'); 398 $method->setAccessible(true); 399 $menucomplete = $method->invoke($primary, $dataprimary, $datacustom, $ismobile); 400 return $menucomplete; 401 } 402 403 /** 404 * Traverse the menu array structure (all nodes recursively) and fetch the node texts from the menu nodes that are 405 * active/open (determined via param $nodetype that can be "inactive" or "isopen"). The returned array contains a 406 * list of nade names that match this criterion. 407 * @param array $menu 408 * @param string $nodetype 409 * @return array 410 */ 411 protected function get_menu_item_names_by_type(array $menu, string $nodetype): array { 412 $matchednodes = []; 413 foreach ($menu as $menuitem) { 414 // Either the node is an array. 415 if (is_array($menuitem)) { 416 if ($menuitem[$nodetype] ?? false) { 417 $matchednodes[] = $menuitem['text']; 418 } 419 // Recursively move through child items. 420 if (array_key_exists('children', $menuitem) && count($menuitem['children'])) { 421 $matchednodes = array_merge($matchednodes, $this->get_menu_item_names_by_type($menuitem['children'], $nodetype)); 422 } 423 } else { 424 // Otherwise the node is a standard object. 425 if (isset($menuitem->{$nodetype}) && $menuitem->{$nodetype} === true) { 426 $matchednodes[] = $menuitem->text; 427 } 428 // Recursively move through child items. 429 if (isset($menuitem->children) && is_array($menuitem->children) && !empty($menuitem->children)) { 430 $matchednodes = array_merge($matchednodes, $this->get_menu_item_names_by_type($menuitem->children, $nodetype)); 431 } 432 } 433 } 434 return $matchednodes; 435 } 436 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body