Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Navigation steps definitions. 19 * 20 * @package core 21 * @category test 22 * @copyright 2012 David MonllaĆ³ 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. 27 28 require_once (__DIR__ . '/../../behat/behat_base.php'); 29 30 use Behat\Mink\Exception\ExpectationException as ExpectationException; 31 use Behat\Mink\Exception\DriverException as DriverException; 32 use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException; 33 34 /** 35 * Steps definitions to navigate through the navigation tree nodes. 36 * 37 * @package core 38 * @category test 39 * @copyright 2012 David MonllaĆ³ 40 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 41 */ 42 class behat_navigation extends behat_base { 43 44 /** 45 * Helper function to get a navigation nodes text element given its text from within the navigation block. 46 * 47 * This function finds the node with the given text from within the navigation block. 48 * It checks to make sure the node is visible, and then returns it. 49 * 50 * @param string $text 51 * @param bool $branch Set this true if you're only interested in the node if its a branch. 52 * @param null|bool $collapsed Set this to true or false if you want the node to either be collapsed or not. 53 * If its left as null then we don't worry about it. 54 * @param null|string|Exception|false $exception The exception to throw if the node is not found. 55 * @return \Behat\Mink\Element\NodeElement 56 */ 57 protected function get_node_text_node($text, $branch = false, $collapsed = null, $exception = null) { 58 if ($exception === null) { 59 $exception = new ExpectationException('The "' . $text . '" node could not be found', $this->getSession()); 60 } else if (is_string($exception)) { 61 $exception = new ExpectationException($exception, $this->getSession()); 62 } 63 64 $nodetextliteral = behat_context_helper::escape($text); 65 $hasblocktree = "[contains(concat(' ', normalize-space(@class), ' '), ' block_tree ')]"; 66 $hasbranch = "[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]"; 67 $hascollapsed = "li[@aria-expanded='false']/p"; 68 $notcollapsed = "li[@aria-expanded='true']/p"; 69 $match = "[normalize-space(.)={$nodetextliteral}]"; 70 71 // Avoid problems with quotes. 72 $isbranch = ($branch) ? $hasbranch : ''; 73 if ($collapsed === true) { 74 $iscollapsed = $hascollapsed; 75 } else if ($collapsed === false) { 76 $iscollapsed = $notcollapsed; 77 } else { 78 $iscollapsed = 'li/p'; 79 } 80 81 // First check root nodes, it can be a span or link. 82 $xpath = "//ul{$hasblocktree}/{$hascollapsed}{$isbranch}/span{$match}|"; 83 $xpath .= "//ul{$hasblocktree}/{$hascollapsed}{$isbranch}/a{$match}|"; 84 85 // Next search for the node containing the text within a link. 86 $xpath .= "//ul{$hasblocktree}//ul/{$iscollapsed}{$isbranch}/a{$match}|"; 87 88 // Finally search for the node containing the text within a span. 89 $xpath .= "//ul{$hasblocktree}//ul/{$iscollapsed}{$isbranch}/span{$match}"; 90 91 $node = $this->find('xpath', $xpath, $exception); 92 $this->ensure_node_is_visible($node); 93 return $node; 94 } 95 96 /** 97 * Returns true if the navigation node with the given text is expandable. 98 * 99 * @Given /^navigation node "([^"]*)" should be expandable$/ 100 * 101 * @throws ExpectationException 102 * @param string $nodetext 103 * @return bool 104 */ 105 public function navigation_node_should_be_expandable($nodetext) { 106 if (!$this->running_javascript()) { 107 // Nodes are only expandable when JavaScript is enabled. 108 return false; 109 } 110 111 $node = $this->get_node_text_node($nodetext, true); 112 $node = $node->getParent(); 113 if ($node->hasClass('emptybranch')) { 114 throw new ExpectationException('The "' . $nodetext . '" node is not expandable', $this->getSession()); 115 } 116 117 return true; 118 } 119 120 /** 121 * Returns true if the navigation node with the given text is not expandable. 122 * 123 * @Given /^navigation node "([^"]*)" should not be expandable$/ 124 * 125 * @throws ExpectationException 126 * @param string $nodetext 127 * @return bool 128 */ 129 public function navigation_node_should_not_be_expandable($nodetext) { 130 if (!$this->running_javascript()) { 131 // Nodes are only expandable when JavaScript is enabled. 132 return false; 133 } 134 135 $node = $this->get_node_text_node($nodetext); 136 $node = $node->getParent(); 137 138 if ($node->hasClass('emptybranch') || $node->hasClass('tree_item')) { 139 return true; 140 } 141 throw new ExpectationException('The "' . $nodetext . '" node is expandable', $this->getSession()); 142 } 143 144 /** 145 * Click on an entry in the user menu. 146 * @Given /^I follow "(?P<nodetext_string>(?:[^"]|\\")*)" in the user menu$/ 147 * 148 * @param string $nodetext 149 */ 150 public function i_follow_in_the_user_menu($nodetext) { 151 152 if ($this->running_javascript()) { 153 // The user menu must be expanded when JS is enabled. 154 $xpath = "//div[contains(concat(' ', @class, ' '), ' usermenu ')]//a[contains(concat(' ', @class, ' '), ' dropdown-toggle ')]"; 155 $this->execute("behat_general::i_click_on", array($this->escape($xpath), "xpath_element")); 156 } 157 158 // Now select the link. 159 // The CSS path is always present, with or without JS. 160 $csspath = ".usermenu .dropdown-menu"; 161 162 $this->execute('behat_general::i_click_on_in_the', 163 array($nodetext, "link", $csspath, "css_element") 164 ); 165 } 166 167 /** 168 * Expands the selected node of the navigation tree that matches the text. 169 * @Given /^I expand "(?P<nodetext_string>(?:[^"]|\\")*)" node$/ 170 * 171 * @throws ExpectationException 172 * @param string $nodetext 173 * @return bool|void 174 */ 175 public function i_expand_node($nodetext) { 176 177 // This step is useless with Javascript disabled as Moodle auto expands 178 // all of tree's nodes; adding this because of scenarios that shares the 179 // same steps with and without Javascript enabled. 180 if (!$this->running_javascript()) { 181 if ($nodetext === get_string('administrationsite')) { 182 // Administration menu is not loaded by default any more. Click the link to expand. 183 $this->execute('behat_general::i_click_on_in_the', 184 array($nodetext, "link", get_string('administration'), "block") 185 ); 186 return true; 187 } 188 return true; 189 } 190 191 $node = $this->get_node_text_node($nodetext, true, true, 'The "' . $nodetext . '" node can not be expanded'); 192 // Check if the node is a link AND a branch. 193 if (strtolower($node->getTagName()) === 'a') { 194 // We just want to expand the node, we don't want to follow it. 195 $node = $node->getParent(); 196 } 197 $this->execute('behat_general::i_click_on', [$node, 'NodeElement']); 198 } 199 200 /** 201 * Collapses the selected node of the navigation tree that matches the text. 202 * 203 * @Given /^I collapse "(?P<nodetext_string>(?:[^"]|\\")*)" node$/ 204 * @throws ExpectationException 205 * @param string $nodetext 206 * @return bool|void 207 */ 208 public function i_collapse_node($nodetext) { 209 210 // No collapsible nodes with non-JS browsers. 211 if (!$this->running_javascript()) { 212 return true; 213 } 214 215 $node = $this->get_node_text_node($nodetext, true, false, 'The "' . $nodetext . '" node can not be collapsed'); 216 // Check if the node is a link AND a branch. 217 if (strtolower($node->getTagName()) === 'a') { 218 // We just want to expand the node, we don't want to follow it. 219 $node = $node->getParent(); 220 } 221 $this->execute('behat_general::i_click_on', [$node, 'NodeElement']); 222 } 223 224 /** 225 * Finds a node in the Navigation or Administration tree 226 * 227 * @param string $nodetext 228 * @param array $parentnodes 229 * @param string $nodetype node type (link or text) 230 * @return NodeElement|null 231 * @throws ExpectationException when one of the parent nodes is not found 232 */ 233 protected function find_node_in_navigation($nodetext, $parentnodes, $nodetype = 'link') { 234 // Site admin is different and needs special treatment. 235 $siteadminstr = get_string('administrationsite'); 236 237 // Create array of all parentnodes. 238 $countparentnode = count($parentnodes); 239 240 // If JS is disabled and Site administration is not expanded we 241 // should follow it, so all the lower-level nodes are available. 242 if (!$this->running_javascript()) { 243 if ($parentnodes[0] === $siteadminstr) { 244 // We don't know if there if Site admin is already expanded so 245 // don't wait, it is non-JS and we already waited for the DOM. 246 $siteadminlink = $this->getSession()->getPage()->find('named_exact', array('link', "'" . $siteadminstr . "'")); 247 if ($siteadminlink) { 248 $this->execute('behat_general::i_click_on', [$siteadminlink, 'NodeElement']); 249 } 250 } 251 } 252 253 // Get top level node. 254 $node = $this->get_top_navigation_node($parentnodes[0]); 255 256 // Expand all nodes. 257 for ($i = 0; $i < $countparentnode; $i++) { 258 if ($i > 0) { 259 // Sub nodes within top level node. 260 $node = $this->get_navigation_node($parentnodes[$i], $node); 261 } 262 263 // The p node contains the aria jazz. 264 $pnodexpath = "/p[contains(concat(' ', normalize-space(@class), ' '), ' tree_item ')]"; 265 $pnode = $node->find('xpath', $pnodexpath); 266 $linode = $pnode->getParent(); 267 268 // Keep expanding all sub-parents if js enabled. 269 if ($pnode && $this->running_javascript() && $linode->hasAttribute('aria-expanded') && 270 ($linode->getAttribute('aria-expanded') == "false")) { 271 $this->js_trigger_click($pnode); 272 273 // Wait for node to load, if not loaded before. 274 if ($linode->hasAttribute('data-loaded') && $linode->getAttribute('data-loaded') == "false") { 275 $jscondition = '(document.evaluate("' . $linode->getXpath() . '", document, null, '. 276 'XPathResult.ANY_TYPE, null).iterateNext().getAttribute(\'data-loaded\') == "true")'; 277 278 $this->getSession()->wait(behat_base::get_extended_timeout() * 1000, $jscondition); 279 } 280 } 281 } 282 283 // Finally, click on requested node under navigation. 284 $nodetextliteral = behat_context_helper::escape($nodetext); 285 $tagname = ($nodetype === 'link') ? 'a' : 'span'; 286 $xpath = "/ul/li/p[contains(concat(' ', normalize-space(@class), ' '), ' tree_item ')]" . 287 "/{$tagname}[normalize-space(.)=" . $nodetextliteral . "]"; 288 return $node->find('xpath', $xpath); 289 } 290 291 /** 292 * Finds a node in the Navigation or Administration tree and clicks on it. 293 * 294 * @param string $nodetext 295 * @param array $parentnodes 296 * @throws ExpectationException 297 */ 298 protected function select_node_in_navigation($nodetext, $parentnodes) { 299 $nodetoclick = $this->find_node_in_navigation($nodetext, $parentnodes); 300 // Throw exception if no node found. 301 if (!$nodetoclick) { 302 throw new ExpectationException('Navigation node "' . $nodetext . '" not found under "' . 303 implode(' > ', $parentnodes) . '"', $this->getSession()); 304 } 305 $this->execute('behat_general::i_click_on', [$nodetoclick, 'NodeElement']); 306 } 307 308 /** 309 * Helper function to get top navigation node in tree. 310 * 311 * @throws ExpectationException if note not found. 312 * @param string $nodetext name of top navigation node in tree. 313 * @return NodeElement 314 */ 315 protected function get_top_navigation_node($nodetext) { 316 317 // Avoid problems with quotes. 318 $nodetextliteral = behat_context_helper::escape($nodetext); 319 $exception = new ExpectationException('Top navigation node "' . $nodetext . ' not found in "', $this->getSession()); 320 321 // First find in navigation block. 322 $xpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' card-text ')]" . 323 "/ul[contains(concat(' ', normalize-space(@class), ' '), ' block_tree ')]" . 324 "/li[contains(concat(' ', normalize-space(@class), ' '), ' contains_branch ')]" . 325 "/ul/li[contains(concat(' ', normalize-space(@class), ' '), ' contains_branch ')]" . 326 "[p[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]" . 327 "/*[contains(normalize-space(.), " . $nodetextliteral .")]]" . 328 "|" . 329 "//div[contains(concat(' ', normalize-space(@class), ' '), ' card-text ')]/div" . 330 "/ul[contains(concat(' ', normalize-space(@class), ' '), ' block_tree ')]" . 331 "/li[p[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]" . 332 "/*[contains(normalize-space(.), " . $nodetextliteral .")]]"; 333 334 $node = $this->find('xpath', $xpath, $exception); 335 336 return $node; 337 } 338 339 /** 340 * Helper function to get sub-navigation node. 341 * 342 * @throws ExpectationException if note not found. 343 * @param string $nodetext node to find. 344 * @param NodeElement $parentnode parent navigation node. 345 * @return NodeElement. 346 */ 347 protected function get_navigation_node($nodetext, $parentnode = null) { 348 349 // Avoid problems with quotes. 350 $nodetextliteral = behat_context_helper::escape($nodetext); 351 352 $xpath = "/ul/li[contains(concat(' ', normalize-space(@class), ' '), ' contains_branch ')]" . 353 "[child::p[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]" . 354 "/child::span[normalize-space(.)=" . $nodetextliteral ."]]"; 355 $node = $parentnode->find('xpath', $xpath); 356 if (!$node) { 357 $xpath = "/ul/li[contains(concat(' ', normalize-space(@class), ' '), ' contains_branch ')]" . 358 "[child::p[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]" . 359 "/child::a[normalize-space(.)=" . $nodetextliteral ."]]"; 360 $node = $parentnode->find('xpath', $xpath); 361 } 362 363 if (!$node) { 364 throw new ExpectationException('Sub-navigation node "' . $nodetext . '" not found under "' . 365 $parentnode->getText() . '"', $this->getSession()); 366 } 367 return $node; 368 } 369 370 /** 371 * Step to open the navigation bar if it is needed. 372 * 373 * The top log in and log out links are hidden when middle or small 374 * size windows (or devices) are used. This step returns a step definition 375 * clicking to expand the navbar if it is hidden. 376 * 377 * @Given /^I expand navigation bar$/ 378 */ 379 public function get_expand_navbar_step() { 380 381 // Checking if we need to click the navbar button to show the navigation menu, it 382 // is hidden by default when using clean theme and a medium or small screen size. 383 384 // The DOM and the JS should be all ready and loaded. Running without spinning 385 // as this is a widely used step and we can not spend time here trying to see 386 // a DOM node that is not always there (at the moment clean is not even the 387 // default theme...). 388 $navbuttonjs = "return ( 389 Y.one('.btn-navbar') && 390 Y.one('.btn-navbar').getComputedStyle('display') !== 'none' 391 )"; 392 393 // Adding an extra click we need to show the 'Log in' link. 394 if (!$this->evaluate_script($navbuttonjs)) { 395 return false; 396 } 397 398 $this->execute('behat_general::i_click_on', array(".btn-navbar", "css_element")); 399 } 400 401 /** 402 * Go to current page setting item 403 * 404 * This can be used on front page, course, category or modules pages. 405 * 406 * @Given /^I navigate to "(?P<nodetext_string>(?:[^"]|\\")*)" in current page administration$/ 407 * 408 * @throws ExpectationException 409 * @param string $nodetext navigation node to click, may contain path, for example "Reports > Overview" 410 * @return void 411 */ 412 public function i_navigate_to_in_current_page_administration($nodetext) { 413 $nodelist = array_map('trim', explode('>', $nodetext)); 414 $this->select_from_administration_menu($nodelist); 415 } 416 417 /** 418 * Checks that current page administration contains text 419 * 420 * @Given /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should exist in current page administration$/ 421 * 422 * @throws ExpectationException 423 * @param string $element The locator of the specified selector. 424 * This may be a path, for example "Subscription mode > Forced subscription" 425 * @param string $selectortype The selector type (link or text) 426 * @return void 427 */ 428 public function should_exist_in_current_page_administration($element, $selectortype) { 429 $nodes = array_map('trim', explode('>', $element)); 430 $nodetext = end($nodes); 431 432 // Find administration menu. 433 $menuxpath = $this->find_header_administration_menu() ?: $this->find_page_administration_menu(true); 434 435 $this->toggle_page_administration_menu($menuxpath); 436 $this->execute('behat_general::should_exist_in_the', [$nodetext, $selectortype, $menuxpath, 'xpath_element']); 437 $this->toggle_page_administration_menu($menuxpath); 438 } 439 440 /** 441 * Checks that current page administration contains text 442 * 443 * @Given /^"(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" should not exist in current page administration$/ 444 * 445 * @throws ExpectationException 446 * @param string $element The locator of the specified selector. 447 * This may be a path, for example "Subscription mode > Forced subscription" 448 * @param string $selectortype The selector type (link or text) 449 * @return void 450 */ 451 public function should_not_exist_in_current_page_administration($element, $selectortype) { 452 $nodes = array_map('trim', explode('>', $element)); 453 $nodetext = end($nodes); 454 455 // Find administration menu. 456 $menuxpath = $this->find_header_administration_menu() ?: $this->find_page_administration_menu(); 457 if (!$menuxpath) { 458 // Menu not found, exit. 459 return; 460 } 461 462 $this->toggle_page_administration_menu($menuxpath); 463 $this->execute('behat_general::should_not_exist_in_the', [$nodetext, $selectortype, $menuxpath, 'xpath_element']); 464 $this->toggle_page_administration_menu($menuxpath); 465 } 466 467 /** 468 * Go to site administration item 469 * 470 * @Given /^I navigate to "(?P<nodetext_string>(?:[^"]|\\")*)" in site administration$/ 471 * 472 * @throws ExpectationException 473 * @param string $nodetext navigation node to click, may contain path, for example "Reports > Overview" 474 * @return void 475 */ 476 public function i_navigate_to_in_site_administration($nodetext) { 477 $nodelist = array_map('trim', explode('>', $nodetext)); 478 $this->i_select_from_flat_navigation_drawer(get_string('administrationsite')); 479 $this->select_on_administration_page($nodelist); 480 } 481 482 /** 483 * Opens the current users profile page in edit mode. 484 * 485 * @Given /^I open my profile in edit mode$/ 486 * @throws coding_exception 487 * @return void 488 */ 489 public function i_open_my_profile_in_edit_mode() { 490 global $USER; 491 492 $user = $this->get_session_user(); 493 $globuser = $USER; 494 $USER = $user; // We need this set to the behat session user so we can call isloggedin. 495 496 $systemcontext = context_system::instance(); 497 498 $bodynode = $this->find('xpath', 'body'); 499 $bodyclass = $bodynode->getAttribute('class'); 500 $matches = []; 501 if (preg_match('/(?<=^course-|\scourse-)\d+/', $bodyclass, $matches) && !empty($matches)) { 502 $courseid = intval($matches[0]); 503 } else { 504 $courseid = SITEID; 505 } 506 507 if (isloggedin() && !isguestuser($user) && !is_mnet_remote_user($user)) { 508 if (is_siteadmin($user) || has_capability('moodle/user:update', $systemcontext)) { 509 $url = new moodle_url('/user/editadvanced.php', array('id' => $user->id, 'course' => SITEID, 510 'returnto' => 'profile')); 511 } else if (has_capability('moodle/user:editownprofile', $systemcontext)) { 512 $userauthplugin = false; 513 if (!empty($user->auth)) { 514 $userauthplugin = get_auth_plugin($user->auth); 515 } 516 if ($userauthplugin && $userauthplugin->can_edit_profile()) { 517 $url = $userauthplugin->edit_profile_url(); 518 if (empty($url)) { 519 if (empty($course)) { 520 $url = new moodle_url('/user/edit.php', array('id' => $user->id, 'returnto' => 'profile')); 521 } else { 522 $url = new moodle_url('/user/edit.php', array('id' => $user->id, 'course' => $courseid, 523 'returnto' => 'profile')); 524 } 525 } 526 527 } 528 } 529 $this->execute('behat_general::i_visit', [$url]); 530 } 531 532 // Restore global user variable. 533 $USER = $globuser; 534 } 535 536 /** 537 * Open a given page, belonging to a plugin or core component. 538 * 539 * The page-type are interpreted by each plugin to work out the 540 * corresponding URL. See the resolve_url method in each class like 541 * behat_mod_forum. That method should document which page types are 542 * recognised, and how the name identifies them. 543 * 544 * For pages belonging to core, the 'core > ' bit is omitted. 545 * 546 * @When /^I am on the (?<page>[^ "]*) page$/ 547 * @When /^I am on the "(?<page>[^"]*)" page$/ 548 * 549 * @param string $page the component and page name. 550 * E.g. 'Admin notifications' or 'core_user > Preferences'. 551 * @throws Exception if the specified page cannot be determined. 552 */ 553 public function i_am_on_page(string $page) { 554 $this->execute('behat_general::i_visit', [$this->resolve_page_helper($page)]); 555 } 556 557 /** 558 * Open a given page logged in as a given user. 559 * 560 * This is like the combination 561 * When I log in as "..." 562 * And I am on the "..." page 563 * but with the advantage that you go straight to the desired page, without 564 * having to wait for the Dashboard to load. 565 * 566 * @When /^I am on the (?<page>[^ "]*) page logged in as (?<username>[^ "]*)$/ 567 * @When /^I am on the "(?<page>[^"]*)" page logged in as (?<username>[^ "]*)$/ 568 * @When /^I am on the (?<page>[^ "]*) page logged in as "(?<username>[^ "]*)"$/ 569 * @When /^I am on the "(?<page>[^"]*)" page logged in as "(?<username>[^ "]*)"$/ 570 * 571 * @param string $page the type of page. E.g. 'Admin notifications' or 'core_user > Preferences'. 572 * @param string $username the name of the user to log in as. E.g. 'admin'. 573 * @throws Exception if the specified page cannot be determined. 574 */ 575 public function i_am_on_page_logged_in_as(string $page, string $username) { 576 self::execute('behat_auth::i_log_in_as', [$username, $this->resolve_page_helper($page)]); 577 } 578 579 /** 580 * Helper used by i_am_on_page() and i_am_on_page_logged_in_as(). 581 * 582 * @param string $page the type of page. E.g. 'Admin notifications' or 'core_user > Preferences'. 583 * @return moodle_url the corresponding URL. 584 */ 585 protected function resolve_page_helper(string $page): moodle_url { 586 list($component, $name) = $this->parse_page_name($page); 587 if ($component === 'core') { 588 return $this->resolve_core_page_url($name); 589 } else { 590 $context = behat_context_helper::get('behat_' . $component); 591 return $context->resolve_page_url($name); 592 } 593 } 594 595 /** 596 * Parse a full page name like 'Admin notifications' or 'core_user > Preferences'. 597 * 598 * E.g. parsing 'mod_quiz > View' gives ['mod_quiz', 'View']. 599 * 600 * @param string $page the full page name 601 * @return array with two elements, component and page name. 602 */ 603 protected function parse_page_name(string $page): array { 604 $dividercount = substr_count($page, ' > '); 605 if ($dividercount === 0) { 606 return ['core', $page]; 607 } else if ($dividercount >= 1) { 608 [$component, $name] = explode(' > ', $page, 2); 609 if ($component === 'core') { 610 throw new coding_exception('Do not specify the component "core > ..." for core pages.'); 611 } 612 return [$component, $name]; 613 } else { 614 throw new coding_exception('The page name must be in the form ' . 615 '"{page-name}" for core pages, or "{component} > {page-name}" ' . 616 'for pages belonging to other components. ' . 617 'For example "Admin notifications" or "mod_quiz > View".'); 618 } 619 } 620 621 /** 622 * Open a given instance of a page, belonging to a plugin or core component. 623 * 624 * The instance identifier and page-type are interpreted by each plugin to 625 * work out the corresponding URL. See the resolve_page_instance_url method 626 * in each class like behat_mod_forum. That method should document which page 627 * types are recognised, and how the name identifies them. 628 * 629 * For pages belonging to core, the 'core > ' bit is omitted. 630 * 631 * @When /^I am on the (?<identifier>[^ "]*) (?<type>[^ "]*) page$/ 632 * @When /^I am on the "(?<identifier>[^"]*)" "(?<type>[^"]*)" page$/ 633 * @When /^I am on the (?<identifier>[^ "]*) "(?<type>[^"]*)" page$/ 634 * @When /^I am on the "(?<identifier>[^"]*)" (?<type>[^ "]*) page$/ 635 * 636 * @param string $identifier identifies the particular page. E.g. 'Test quiz'. 637 * @param string $type the component and page type. E.g. 'mod_quiz > View'. 638 * @throws Exception if the specified page cannot be determined. 639 */ 640 public function i_am_on_page_instance(string $identifier, string $type) { 641 $this->execute('behat_general::i_visit', [$this->resolve_page_instance_helper($identifier, $type)]); 642 } 643 644 /** 645 * Open a given page logged in as a given user. 646 * 647 * This is like the combination 648 * When I log in as "..." 649 * And I am on the "..." "..." page 650 * but with the advantage that you go straight to the desired page, without 651 * having to wait for the Dashboard to load. 652 * 653 * @When /^I am on the (?<identifier>[^ "]*) (?<type>[^ "]*) page logged in as (?<username>[^ "]*)$/ 654 * @When /^I am on the "(?<identifier>[^"]*)" "(?<type>[^"]*)" page logged in as (?<username>[^ "]*)$/ 655 * @When /^I am on the (?<identifier>[^ "]*) "(?<type>[^"]*)" page logged in as (?<username>[^ "]*)$/ 656 * @When /^I am on the "(?<identifier>[^"]*)" (?<type>[^ "]*) page logged in as (?<username>[^ "]*)$/ 657 * @When /^I am on the (?<identifier>[^ "]*) (?<type>[^ "]*) page logged in as "(?<username>[^"]*)"$/ 658 * @When /^I am on the "(?<identifier>[^"]*)" "(?<type>[^"]*)" page logged in as "(?<username>[^"]*)"$/ 659 * @When /^I am on the (?<identifier>[^ "]*) "(?<type>[^"]*)" page logged in as "(?<username>[^"]*)"$/ 660 * @When /^I am on the "(?<identifier>[^"]*)" (?<type>[^ "]*) page logged in as "(?<username>[^"]*)"$/ 661 * 662 * @param string $identifier identifies the particular page. E.g. 'Test quiz'. 663 * @param string $type the component and page type. E.g. 'mod_quiz > View'. 664 * @param string $username the name of the user to log in as. E.g. 'student'. 665 * @throws Exception if the specified page cannot be determined. 666 */ 667 public function i_am_on_page_instance_logged_in_as(string $identifier, 668 string $type, string $username) { 669 self::execute('behat_auth::i_log_in_as', 670 [$username, $this->resolve_page_instance_helper($identifier, $type)]); 671 } 672 673 /** 674 * Helper used by i_am_on_page() and i_am_on_page_logged_in_as(). 675 * 676 * @param string $identifier identifies the particular page. E.g. 'Test quiz'. 677 * @param string $pagetype the component and page type. E.g. 'mod_quiz > View'. 678 * @return moodle_url the corresponding URL. 679 */ 680 protected function resolve_page_instance_helper(string $identifier, string $pagetype): moodle_url { 681 list($component, $type) = $this->parse_page_name($pagetype); 682 if ($component === 'core') { 683 return $this->resolve_core_page_instance_url($type, $identifier); 684 } else { 685 $context = behat_context_helper::get('behat_' . $component); 686 return $context->resolve_page_instance_url($type, $identifier); 687 } 688 } 689 690 /** 691 * Convert core page names to URLs for steps like 'When I am on the "[page name]" page'. 692 * 693 * Recognised page names are: 694 * | Homepage | Homepage (normally dashboard). | 695 * | Admin notifications | Admin notification screen. | 696 * 697 * @param string $name identifies which identifies this page, e.g. 'Homepage', 'Admin notifications'. 698 * @return moodle_url the corresponding URL. 699 * @throws Exception with a meaningful error message if the specified page cannot be found. 700 */ 701 protected function resolve_core_page_url(string $name): moodle_url { 702 switch ($name) { 703 case 'Homepage': 704 return new moodle_url('/'); 705 706 case 'Admin notifications': 707 return new moodle_url('/admin/'); 708 709 default: 710 throw new Exception('Unrecognised core page type "' . $name . '."'); 711 } 712 } 713 714 /** 715 * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'. 716 * 717 * Recognised page names are: 718 * | Page type | Identifier meaning | description | 719 * | Category | category idnumber | List of courses in that category. | 720 * | Course | course shortname | Main course home pag | 721 * | Course editing | course shortname | Edit settings page for the course | 722 * | Activity | activity idnumber | Start page for that activity | 723 * | Activity editing | activity idnumber | Edit settings page for that activity | 724 * | [modname] Activity | activity name or idnumber | Start page for that activity | 725 * | [modname] Activity editing | activity name or idnumber | Edit settings page for that activity | 726 * 727 * Examples: 728 * 729 * When I am on the "Welcome to ECON101" "forum activity" page logged in as student1 730 * 731 * @param string $type identifies which type of page this is, e.g. 'Category page'. 732 * @param string $identifier identifies the particular page, e.g. 'test-cat'. 733 * @return moodle_url the corresponding URL. 734 * @throws Exception with a meaningful error message if the specified page cannot be found. 735 */ 736 protected function resolve_core_page_instance_url(string $type, string $identifier): moodle_url { 737 $type = strtolower($type); 738 739 switch ($type) { 740 case 'category': 741 $categoryid = $this->get_category_id($identifier); 742 if (!$categoryid) { 743 throw new Exception('The specified category with idnumber "' . $identifier . '" does not exist'); 744 } 745 return new moodle_url('/course/index.php', ['categoryid' => $categoryid]); 746 747 case 'course editing': 748 $courseid = $this->get_course_id($identifier); 749 if (!$courseid) { 750 throw new Exception('The specified course with shortname, fullname, or idnumber "' . 751 $identifier . '" does not exist'); 752 } 753 return new moodle_url('/course/edit.php', ['id' => $courseid]); 754 755 case 'course': 756 $courseid = $this->get_course_id($identifier); 757 if (!$courseid) { 758 throw new Exception('The specified course with shortname, fullname, or idnumber "' . 759 $identifier . '" does not exist'); 760 } 761 return new moodle_url('/course/view.php', ['id' => $courseid]); 762 763 case 'activity': 764 $cm = $this->get_course_module_for_identifier($identifier); 765 if (!$cm) { 766 throw new Exception('The specified activity with idnumber "' . $identifier . '" does not exist'); 767 } 768 return $cm->url; 769 770 case 'activity editing': 771 $cm = $this->get_course_module_for_identifier($identifier); 772 if (!$cm) { 773 throw new Exception('The specified activity with idnumber "' . $identifier . '" does not exist'); 774 } 775 return new moodle_url('/course/modedit.php', [ 776 'update' => $cm->id, 777 ]); 778 } 779 780 $parts = explode(' ', $type); 781 if (count($parts) > 1) { 782 if ($parts[1] === 'activity') { 783 $modname = $parts[0]; 784 $cm = $this->get_cm_by_activity_name($modname, $identifier); 785 786 if (count($parts) == 2) { 787 // View page. 788 return new moodle_url($cm->url); 789 } 790 791 if ($parts[2] === 'editing') { 792 // Edit settings page. 793 return new moodle_url('/course/modedit.php', ['update' => $cm->id]); 794 } 795 796 if ($parts[2] === 'roles') { 797 // Locally assigned roles page. 798 return new moodle_url('/admin/roles/assign.php', ['contextid' => $cm->context->id]); 799 } 800 801 if ($parts[2] === 'permissions') { 802 // Permissions page. 803 return new moodle_url('/admin/roles/permissions.php', ['contextid' => $cm->context->id]); 804 } 805 } 806 } 807 808 throw new Exception('Unrecognised core page type "' . $type . '."'); 809 } 810 811 /** 812 * Opens the course homepage. (Consider using 'I am on the "shortname" "Course" page' step instead.) 813 * 814 * @Given /^I am on "(?P<coursefullname_string>(?:[^"]|\\")*)" course homepage$/ 815 * @throws coding_exception 816 * @param string $coursefullname The full name of the course. 817 * @return void 818 */ 819 public function i_am_on_course_homepage($coursefullname) { 820 $courseid = $this->get_course_id($coursefullname); 821 $url = new moodle_url('/course/view.php', ['id' => $courseid]); 822 $this->execute('behat_general::i_visit', [$url]); 823 } 824 825 /** 826 * Open the course homepage with editing mode enabled. 827 * 828 * @Given /^I am on "(?P<coursefullname_string>(?:[^"]|\\")*)" course homepage with editing mode on$/ 829 * @throws coding_exception 830 * @param string $coursefullname The course full name of the course. 831 * @return void 832 */ 833 public function i_am_on_course_homepage_with_editing_mode_on($coursefullname) { 834 $courseid = $this->get_course_id($coursefullname); 835 $url = new moodle_url('/course/view.php', ['id' => $courseid]); 836 837 if ($this->running_javascript() && $sesskey = $this->get_sesskey()) { 838 // Javascript is running so it is possible to grab the session ket and jump straight to editing mode. 839 $url->param('edit', 1); 840 $url->param('sesskey', $sesskey); 841 $this->execute('behat_general::i_visit', [$url]); 842 843 return; 844 } 845 846 // Visit the course page. 847 $this->execute('behat_general::i_visit', [$url]); 848 849 try { 850 $this->execute("behat_forms::press_button", get_string('turneditingon')); 851 } catch (Exception $e) { 852 $this->execute("behat_navigation::i_navigate_to_in_current_page_administration", [get_string('turneditingon')]); 853 } 854 } 855 856 /** 857 * Opens the flat navigation drawer if it is not already open 858 * 859 * @When /^I open flat navigation drawer$/ 860 * @throws ElementNotFoundException Thrown by behat_base::find 861 */ 862 public function i_open_flat_navigation_drawer() { 863 if (!$this->running_javascript()) { 864 // Navigation drawer is always open without JS. 865 return; 866 } 867 $xpath = "//button[contains(@data-action,'toggle-drawer')]"; 868 $node = $this->find('xpath', $xpath); 869 $expanded = $node->getAttribute('aria-expanded'); 870 if ($expanded === 'false') { 871 $this->execute('behat_general::i_click_on', [$node, 'NodeElement']); 872 $this->ensure_node_attribute_is_set($node, 'aria-expanded', 'true'); 873 } 874 } 875 876 /** 877 * Closes the flat navigation drawer if it is open (does nothing if JS disabled) 878 * 879 * @When /^I close flat navigation drawer$/ 880 * @throws ElementNotFoundException Thrown by behat_base::find 881 */ 882 public function i_close_flat_navigation_drawer() { 883 if (!$this->running_javascript()) { 884 // Navigation drawer can not be closed without JS. 885 return; 886 } 887 $xpath = "//button[contains(@data-action,'toggle-drawer')]"; 888 $node = $this->find('xpath', $xpath); 889 $expanded = $node->getAttribute('aria-expanded'); 890 if ($expanded === 'true') { 891 $this->execute('behat_general::i_click_on', [$node, 'NodeElement']); 892 } 893 } 894 895 /** 896 * Clicks link with specified id|title|alt|text in the flat navigation drawer. 897 * 898 * @When /^I select "(?P<link_string>(?:[^"]|\\")*)" from flat navigation drawer$/ 899 * @throws ElementNotFoundException Thrown by behat_base::find 900 * @param string $link 901 */ 902 public function i_select_from_flat_navigation_drawer($link) { 903 $this->i_open_flat_navigation_drawer(); 904 $this->execute('behat_general::i_click_on_in_the', [$link, 'link', '#nav-drawer', 'css_element']); 905 } 906 907 /** 908 * If we are not on the course main page, click on the course link in the navbar 909 */ 910 protected function go_to_main_course_page() { 911 $url = $this->getSession()->getCurrentUrl(); 912 if (!preg_match('|/course/view.php\?id=[\d]+$|', $url)) { 913 $node = $this->find('xpath', '//header//div[@id=\'page-navbar\']//a[contains(@href,\'/course/view.php?id=\')]'); 914 $this->execute('behat_general::i_click_on', [$node, 'NodeElement']); 915 } 916 } 917 918 /** 919 * Finds and clicks a link on the admin page (site administration or course administration) 920 * 921 * @param array $nodelist 922 */ 923 protected function select_on_administration_page($nodelist) { 924 $parentnodes = $nodelist; 925 $lastnode = array_pop($parentnodes); 926 $xpath = '//section[@id=\'region-main\']'; 927 928 // Check if there is a separate tab for this submenu of the page. If found go to it. 929 if ($parentnodes) { 930 $tabname = behat_context_helper::escape($parentnodes[0]); 931 $tabxpath = '//ul[@role=\'tablist\']/li/a[contains(normalize-space(.), ' . $tabname . ')]'; 932 if ($node = $this->getSession()->getPage()->find('xpath', $tabxpath)) { 933 if ($this->running_javascript()) { 934 $this->execute('behat_general::i_click_on', [$node, 'NodeElement']); 935 // Click on the tab and add 'active' tab to the xpath. 936 $xpath .= '//div[contains(@class,\'active\')]'; 937 } else { 938 // Add the tab content selector to the xpath. 939 $tabid = behat_context_helper::escape(ltrim($node->getAttribute('href'), '#')); 940 $xpath .= '//div[@id = ' . $tabid . ']'; 941 } 942 array_shift($parentnodes); 943 } 944 } 945 946 // Find a section with the parent name in it. 947 if ($parentnodes) { 948 // Find the section on the page (links may be repeating in different sections). 949 $section = behat_context_helper::escape($parentnodes[0]); 950 $xpath .= '//div[@class=\'row\' and contains(.,'.$section.')]'; 951 } 952 953 // Find a link and click on it. 954 $linkname = behat_context_helper::escape($lastnode); 955 $xpath .= '//a[contains(normalize-space(.), ' . $linkname . ')]'; 956 if (!$node = $this->getSession()->getPage()->find('xpath', $xpath)) { 957 throw new ElementNotFoundException($this->getSession(), 'Link "' . join(' > ', $nodelist) . '"'); 958 } 959 $this->execute('behat_general::i_click_on', [$node, 'NodeElement']); 960 } 961 962 /** 963 * Locates the administration menu in the <header> element and returns its xpath 964 * 965 * @param bool $mustexist if specified throws an exception if menu is not found 966 * @return null|string 967 */ 968 protected function find_header_administration_menu($mustexist = false) { 969 $menuxpath = '//header[@id=\'page-header\']//div[contains(@class,\'moodle-actionmenu\')]'; 970 if ($mustexist) { 971 $exception = new ElementNotFoundException($this->getSession(), 'Page header administration menu'); 972 $this->find('xpath', $menuxpath, $exception); 973 } else if (!$this->getSession()->getPage()->find('xpath', $menuxpath)) { 974 return null; 975 } 976 return $menuxpath; 977 } 978 979 /** 980 * Locates the administration menu on the page (but not in the header) and returns its xpath 981 * 982 * @param bool $mustexist if specified throws an exception if menu is not found 983 * @return null|string 984 */ 985 protected function find_page_administration_menu($mustexist = false) { 986 $menuxpath = '//div[@id=\'region-main-settings-menu\']'; 987 if ($mustexist) { 988 $exception = new ElementNotFoundException($this->getSession(), 'Page administration menu'); 989 $this->find('xpath', $menuxpath, $exception); 990 } else if (!$this->getSession()->getPage()->find('xpath', $menuxpath)) { 991 return null; 992 } 993 return $menuxpath; 994 } 995 996 /** 997 * Toggles administration menu 998 * 999 * @param string $menuxpath (optional) xpath to the page administration menu if already known 1000 */ 1001 protected function toggle_page_administration_menu($menuxpath = null) { 1002 if (!$menuxpath) { 1003 $menuxpath = $this->find_header_administration_menu() ?: $this->find_page_administration_menu(); 1004 } 1005 if ($menuxpath && $this->running_javascript()) { 1006 $node = $this->find('xpath', $menuxpath . '//a[@data-toggle=\'dropdown\']'); 1007 $this->execute('behat_general::i_click_on', [$node, 'NodeElement']); 1008 } 1009 } 1010 1011 /** 1012 * Finds a page edit cog and select an item from it 1013 * 1014 * If the page edit cog is in the page header and the item is not found there, click "More..." link 1015 * and find the item on the course/frontpage administration page 1016 * 1017 * @param array $nodelist 1018 * @throws ElementNotFoundException 1019 */ 1020 protected function select_from_administration_menu($nodelist) { 1021 // Find administration menu. 1022 if ($menuxpath = $this->find_header_administration_menu()) { 1023 $isheader = true; 1024 } else { 1025 $menuxpath = $this->find_page_administration_menu(true); 1026 $isheader = false; 1027 } 1028 1029 $this->execute('behat_navigation::toggle_page_administration_menu', [$menuxpath]); 1030 1031 if (!$isheader || count($nodelist) == 1) { 1032 $lastnode = end($nodelist); 1033 $linkname = behat_context_helper::escape($lastnode); 1034 $link = $this->getSession()->getPage()->find('xpath', $menuxpath . '//a[contains(normalize-space(.), ' . $linkname . ')]'); 1035 if ($link) { 1036 $this->execute('behat_general::i_click_on', [$link, 'NodeElement']); 1037 return; 1038 } 1039 } 1040 1041 if ($isheader) { 1042 // Course administration and Front page administration will have subnodes under "More...". 1043 $linkname = behat_context_helper::escape(get_string('morenavigationlinks')); 1044 $link = $this->getSession()->getPage()->find('xpath', $menuxpath . '//a[contains(normalize-space(.), ' . $linkname . ')]'); 1045 if ($link) { 1046 $this->execute('behat_general::i_click_on', [$link, 'NodeElement']); 1047 $this->select_on_administration_page($nodelist); 1048 return; 1049 } 1050 } 1051 1052 throw new ElementNotFoundException($this->getSession(), 1053 'Link "' . join(' > ', $nodelist) . '" in the current page edit menu"'); 1054 } 1055 1056 /** 1057 * Visit a fixture page for testing stuff that is not available in core. 1058 * 1059 * Please always, to prevent unwanted requests, protect behat fixture files with: 1060 * defined('BEHAT_SITE_RUNNING') || die(); 1061 * 1062 * @Given /^I am on fixture page "(?P<url_string>(?:[^"]|\\")*)"$/ 1063 * @param string $url local path to fixture page 1064 */ 1065 public function i_am_on_fixture_page($url) { 1066 $fixtureregex = '|^/[a-z0-9_\-/]*/tests/behat/fixtures/[a-z0-9_\-]*\.php$|'; 1067 if (!preg_match($fixtureregex, $url)) { 1068 throw new coding_exception("URL {$url} is not a fixture URL"); 1069 } 1070 $this->execute('behat_general::i_visit', [$url]); 1071 } 1072 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body