MDL-58428 theme: Shift templates ready for Bootstrapbase removal
[moodle.git] / lib / tests / behat / behat_navigation.php
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/>.
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  */
26 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
28 require_once(__DIR__ . '/../../behat/behat_base.php');
30 use Behat\Mink\Exception\ExpectationException as ExpectationException;
31 use Behat\Mink\Exception\DriverException as DriverException;
32 use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
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 {
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         }
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 = "p[@aria-expanded='false']";
68         $notcollapsed = "p[@aria-expanded='true']";
69         $match = "[normalize-space(.)={$nodetextliteral}]";
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 = 'p';
79         }
81         // First check root nodes, it can be a span or link.
82         $xpath  = "//ul{$hasblocktree}/li/{$hascollapsed}{$isbranch}/span{$match}|";
83         $xpath  .= "//ul{$hasblocktree}/li/{$hascollapsed}{$isbranch}/a{$match}|";
85         // Next search for the node containing the text within a link.
86         $xpath .= "//ul{$hasblocktree}//ul/li/{$iscollapsed}{$isbranch}/a{$match}|";
88         // Finally search for the node containing the text within a span.
89         $xpath .= "//ul{$hasblocktree}//ul/li/{$iscollapsed}{$isbranch}/span{$match}";
91         $node = $this->find('xpath', $xpath, $exception);
92         $this->ensure_node_is_visible($node);
93         return $node;
94     }
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         }
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         }
117         return true;
118     }
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         }
135         $node = $this->get_node_text_node($nodetext);
136         $node = $node->getParent();
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     }
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) {
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         }
158         // Now select the link.
159         // The CSS path is always present, with or without JS.
160         $csspath = ".usermenu .dropdown-menu";
162         $this->execute('behat_general::i_click_on_in_the',
163                 array($nodetext, "link", $csspath, "css_element")
164         );
165     }
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) {
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         }
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         $node->click();
198     }
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) {
210         // No collapsible nodes with non-JS browsers.
211         if (!$this->running_javascript()) {
212             return true;
213         }
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         $node->click();
222     }
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');
237         // Create array of all parentnodes.
238         $countparentnode = count($parentnodes);
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                     $siteadminlink->click();
249                 }
250             }
251         }
253         // Get top level node.
254         $node = $this->get_top_navigation_node($parentnodes[0]);
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             }
263             // The p node contains the aria jazz.
264             $pnodexpath = "/p[contains(concat(' ', normalize-space(@class), ' '), ' tree_item ')]";
265             $pnode = $node->find('xpath', $pnodexpath);
267             // Keep expanding all sub-parents if js enabled.
268             if ($pnode && $this->running_javascript() && $pnode->hasAttribute('aria-expanded') &&
269                 ($pnode->getAttribute('aria-expanded') == "false")) {
271                 $this->js_trigger_click($pnode);
273                 // Wait for node to load, if not loaded before.
274                 if ($pnode->hasAttribute('data-loaded') && $pnode->getAttribute('data-loaded') == "false") {
275                     $jscondition = '(document.evaluate("' . $pnode->getXpath() . '", document, null, '.
276                         'XPathResult.ANY_TYPE, null).iterateNext().getAttribute(\'data-loaded\') == "true")';
278                     $this->getSession()->wait(self::EXTENDED_TIMEOUT * 1000, $jscondition);
279                 }
280             }
281         }
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     }
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         $nodetoclick->click();
306     }
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) {
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());
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 .")]]";
334         $node = $this->find('xpath', $xpath, $exception);
336         return $node;
337     }
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) {
349         // Avoid problems with quotes.
350         $nodetextliteral = behat_context_helper::escape($nodetext);
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         }
363         if (!$node) {
364             throw new ExpectationException('Sub-navigation node "' . $nodetext . '" not found under "' .
365                 $parentnode->getText() . '"', $this->getSession());
366         }
367         return $node;
368     }
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() {
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.
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         )";
393         // Adding an extra click we need to show the 'Log in' link.
394         if (!$this->getSession()->getDriver()->evaluateScript($navbuttonjs)) {
395             return false;
396         }
398         $this->execute('behat_general::i_click_on', array(".btn-navbar", "css_element"));
399     }
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     }
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);
432         // Find administration menu.
433         $menuxpath = $this->find_header_administration_menu() ?: $this->find_page_administration_menu(true);
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     }
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);
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         }
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     }
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     }
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;
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.
496         $systemcontext = context_system::instance();
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         }
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                     }
527                 }
528             }
529             $this->getSession()->visit($this->locate_path($url->out_as_local_url()));
530         }
532         // Restore global user variable.
533         $USER = $globuser;
534     }
536     /**
537      * Opens the course homepage.
538      *
539      * @Given /^I am on "(?P<coursefullname_string>(?:[^"]|\\")*)" course homepage$/
540      * @throws coding_exception
541      * @param string $coursefullname The full name of the course.
542      * @return void
543      */
544     public function i_am_on_course_homepage($coursefullname) {
545         global $DB;
546         $course = $DB->get_record("course", array("fullname" => $coursefullname), 'id', MUST_EXIST);
547         $url = new moodle_url('/course/view.php', ['id' => $course->id]);
548         $this->getSession()->visit($this->locate_path($url->out_as_local_url(false)));
549     }
551     /**
552      * Opens the course homepage with editing mode on.
553      *
554      * @Given /^I am on "(?P<coursefullname_string>(?:[^"]|\\")*)" course homepage with editing mode on$/
555      * @throws coding_exception
556      * @param string $coursefullname The course full name of the course.
557      * @return void
558      */
559     public function i_am_on_course_homepage_with_editing_mode_on($coursefullname) {
560         global $DB;
561         $course = $DB->get_record("course", array("fullname" => $coursefullname), 'id', MUST_EXIST);
562         $url = new moodle_url('/course/view.php', ['id' => $course->id]);
563         $this->getSession()->visit($this->locate_path($url->out_as_local_url(false)));
564         try {
565             $this->execute("behat_forms::press_button", get_string('turneditingon'));
566         } catch (Exception $e) {
567             $this->execute("behat_navigation::i_navigate_to_in_current_page_administration", [get_string('turneditingon')]);
568         }
569     }
571     /**
572      * Opens the flat navigation drawer if it is not already open
573      *
574      * @When /^I open flat navigation drawer$/
575      * @throws ElementNotFoundException Thrown by behat_base::find
576      */
577     public function i_open_flat_navigation_drawer() {
578         if (!$this->running_javascript()) {
579             // Navigation drawer is always open without JS.
580             return;
581         }
582         $xpath = "//button[contains(@data-action,'toggle-drawer')]";
583         $node = $this->find('xpath', $xpath);
584         $expanded = $node->getAttribute('aria-expanded');
585         if ($expanded === 'false') {
586             $node->click();
587             $this->ensure_node_attribute_is_set($node, 'aria-expanded', 'true');
588             $this->wait_for_pending_js();
589         }
590     }
592     /**
593      * Closes the flat navigation drawer if it is open (does nothing if JS disabled)
594      *
595      * @When /^I close flat navigation drawer$/
596      * @throws ElementNotFoundException Thrown by behat_base::find
597      */
598     public function i_close_flat_navigation_drawer() {
599         if (!$this->running_javascript()) {
600             // Navigation drawer can not be closed without JS.
601             return;
602         }
603         $xpath = "//button[contains(@data-action,'toggle-drawer')]";
604         $node = $this->find('xpath', $xpath);
605         $expanded = $node->getAttribute('aria-expanded');
606         if ($expanded === 'true') {
607             $node->click();
608             $this->wait_for_pending_js();
609         }
610     }
612     /**
613      * Clicks link with specified id|title|alt|text in the flat navigation drawer.
614      *
615      * @When /^I select "(?P<link_string>(?:[^"]|\\")*)" from flat navigation drawer$/
616      * @throws ElementNotFoundException Thrown by behat_base::find
617      * @param string $link
618      */
619     public function i_select_from_flat_navigation_drawer($link) {
620         $this->i_open_flat_navigation_drawer();
621         $this->execute('behat_general::i_click_on_in_the', [$link, 'link', '#nav-drawer', 'css_element']);
622     }
624     /**
625      * If we are not on the course main page, click on the course link in the navbar
626      */
627     protected function go_to_main_course_page() {
628         $url = $this->getSession()->getCurrentUrl();
629         if (!preg_match('|/course/view.php\?id=[\d]+$|', $url)) {
630             $this->find('xpath', '//header//div[@id=\'page-navbar\']//a[contains(@href,\'/course/view.php?id=\')]')->click();
631             $this->execute('behat_general::wait_until_the_page_is_ready');
632         }
633     }
635     /**
636      * Finds and clicks a link on the admin page (site administration or course administration)
637      *
638      * @param array $nodelist
639      */
640     protected function select_on_administration_page($nodelist) {
641         $parentnodes = $nodelist;
642         $lastnode = array_pop($parentnodes);
643         $xpath = '//section[@id=\'region-main\']';
645         // Check if there is a separate tab for this submenu of the page. If found go to it.
646         if ($parentnodes) {
647             $tabname = behat_context_helper::escape($parentnodes[0]);
648             $tabxpath = '//ul[@role=\'tablist\']/li/a[contains(normalize-space(.), ' . $tabname . ')]';
649             if ($node = $this->getSession()->getPage()->find('xpath', $tabxpath)) {
650                 if ($this->running_javascript()) {
651                     // Click on the tab and add 'active' tab to the xpath.
652                     $node->click();
653                     $xpath .= '//div[contains(@class,\'active\')]';
654                 } else {
655                     // Add the tab content selector to the xpath.
656                     $tabid = behat_context_helper::escape(ltrim($node->getAttribute('href'), '#'));
657                     $xpath .= '//div[@id = ' . $tabid . ']';
658                 }
659                 array_shift($parentnodes);
660             }
661         }
663         // Find a section with the parent name in it.
664         if ($parentnodes) {
665             // Find the section on the page (links may be repeating in different sections).
666             $section = behat_context_helper::escape($parentnodes[0]);
667             $xpath .= '//div[@class=\'row\' and contains(.,'.$section.')]';
668         }
670         // Find a link and click on it.
671         $linkname = behat_context_helper::escape($lastnode);
672         $xpath .= '//a[contains(normalize-space(.), ' . $linkname . ')]';
673         if (!$node = $this->getSession()->getPage()->find('xpath', $xpath)) {
674             throw new ElementNotFoundException($this->getSession(), 'Link "' . join(' > ', $nodelist) . '"" not found on the page');
675         }
676         $node->click();
677         $this->wait_for_pending_js();
678     }
680     /**
681      * Locates the administration menu in the <header> element and returns its xpath
682      *
683      * @param bool $mustexist if specified throws an exception if menu is not found
684      * @return null|string
685      */
686     protected function find_header_administration_menu($mustexist = false) {
687         $menuxpath = '//header[@id=\'page-header\']//div[contains(@class,\'moodle-actionmenu\')]';
688         if ($mustexist) {
689             $exception = new ElementNotFoundException($this->getSession(), 'Page header administration menu is not found');
690             $this->find('xpath', $menuxpath, $exception);
691         } else if (!$this->getSession()->getPage()->find('xpath', $menuxpath)) {
692             return null;
693         }
694         return $menuxpath;
695     }
697     /**
698      * Locates the administration menu on the page (but not in the header) and returns its xpath
699      *
700      * @param bool $mustexist if specified throws an exception if menu is not found
701      * @return null|string
702      */
703     protected function find_page_administration_menu($mustexist = false) {
704         $menuxpath = '//div[@id=\'region-main-settings-menu\']';
705         if ($mustexist) {
706             $exception = new ElementNotFoundException($this->getSession(), 'Page administration menu is not found');
707             $this->find('xpath', $menuxpath, $exception);
708         } else if (!$this->getSession()->getPage()->find('xpath', $menuxpath)) {
709             return null;
710         }
711         return $menuxpath;
712     }
714     /**
715      * Toggles administration menu
716      *
717      * @param string $menuxpath (optional) xpath to the page administration menu if already known
718      */
719     protected function toggle_page_administration_menu($menuxpath = null) {
720         if (!$menuxpath) {
721             $menuxpath = $this->find_header_administration_menu() ?: $this->find_page_administration_menu();
722         }
723         if ($menuxpath && $this->running_javascript()) {
724             $this->find('xpath', $menuxpath . '//a[@data-toggle=\'dropdown\']')->click();
725             $this->wait_for_pending_js();
726         }
727     }
729     /**
730      * Finds a page edit cog and select an item from it
731      *
732      * If the page edit cog is in the page header and the item is not found there, click "More..." link
733      * and find the item on the course/frontpage administration page
734      *
735      * @param array $nodelist
736      * @throws ElementNotFoundException
737      */
738     protected function select_from_administration_menu($nodelist) {
739         // Find administration menu.
740         if ($menuxpath = $this->find_header_administration_menu()) {
741             $isheader = true;
742         } else {
743             $menuxpath = $this->find_page_administration_menu(true);
744             $isheader = false;
745         }
747         $this->toggle_page_administration_menu($menuxpath);
749         if (!$isheader || count($nodelist) == 1) {
750             $lastnode = end($nodelist);
751             $linkname = behat_context_helper::escape($lastnode);
752             $link = $this->getSession()->getPage()->find('xpath', $menuxpath . '//a[contains(normalize-space(.), ' . $linkname . ')]');
753             if ($link) {
754                 $link->click();
755                 $this->wait_for_pending_js();
756                 return;
757             }
758         }
760         if ($isheader) {
761             // Course administration and Front page administration will have subnodes under "More...".
762             $linkname = behat_context_helper::escape(get_string('morenavigationlinks'));
763             $link = $this->getSession()->getPage()->find('xpath', $menuxpath . '//a[contains(normalize-space(.), ' . $linkname . ')]');
764             if ($link) {
765                 $link->click();
766                 $this->execute('behat_general::wait_until_the_page_is_ready');
767                 $this->select_on_administration_page($nodelist);
768                 return;
769             }
770         }
772         throw new ElementNotFoundException($this->getSession(),
773                 'Link "' . join(' > ', $nodelist) . '" not found in the current page edit menu"');
774     }