MDL-67924 core: Now show the editing button everywhere
[moodle.git] / course / tests / behat / behat_course.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  * Behat course-related steps definitions.
19  *
20  * @package    core_course
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__ . '/../../../lib/behat/behat_base.php');
30 use Behat\Gherkin\Node\TableNode as TableNode,
31     Behat\Mink\Exception\ExpectationException as ExpectationException,
32     Behat\Mink\Exception\DriverException as DriverException,
33     Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
35 /**
36  * Course-related steps definitions.
37  *
38  * @package    core_course
39  * @category   test
40  * @copyright  2012 David MonllaĆ³
41  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42  */
43 class behat_course extends behat_base {
45     /**
46      * Return the list of partial named selectors.
47      *
48      * @return array
49      */
50     public static function get_partial_named_selectors(): array {
51         return [
52             new behat_component_named_selector(
53                 'Activity chooser screen', [
54                     "%core_course/activityChooser%//*[@data-region=%locator%][contains(concat(' ', @class, ' '), ' carousel-item ')]"
55                 ]
56             ),
57             new behat_component_named_selector(
58                 'Activity chooser tab', [
59                     "%core_course/activityChooser%//*[@data-region=%locator%][contains(concat(' ', @class, ' '), ' tab-pane ')]"
60                 ]
61             ),
62         ];
63     }
65     /**
66      * Return a list of the Mink named replacements for the component.
67      *
68      * Named replacements allow you to define parts of an xpath that can be reused multiple times, or in multiple
69      * xpaths.
70      *
71      * This method should return a list of {@link behat_component_named_replacement} and the docs on that class explain
72      * how it works.
73      *
74      * @return behat_component_named_replacement[]
75      */
76     public static function get_named_replacements(): array {
77         return [
78             new behat_component_named_replacement(
79                 'activityChooser',
80                 ".//*[contains(concat(' ', @class, ' '), ' modchooser ')][contains(concat(' ', @class, ' '), ' modal-dialog ')]"
81             ),
82         ];
83     }
85     /**
86      * Turns editing mode on.
87      * @Given /^I turn editing mode on$/
88      */
89     public function i_turn_editing_mode_on() {
91         try {
92             $this->execute("behat_forms::press_button", get_string('turneditingon'));
93         } catch (Exception $e) {
94             $this->execute("behat_navigation::i_navigate_to_in_current_page_administration", [get_string('turneditingon')]);
95         }
96     }
98     /**
99      * Turns editing mode off.
100      * @Given /^I turn editing mode off$/
101      */
102     public function i_turn_editing_mode_off() {
104         try {
105             $this->execute("behat_forms::press_button", get_string('turneditingoff'));
106         } catch (Exception $e) {
107             $this->execute("behat_navigation::i_navigate_to_in_current_page_administration", [get_string('turneditingoff')]);
108         }
109     }
111     /**
112      * Creates a new course with the provided table data matching course settings names with the desired values.
113      *
114      * @Given /^I create a course with:$/
115      * @param TableNode $table The course data
116      */
117     public function i_create_a_course_with(TableNode $table) {
119         // Go to course management page.
120         $this->i_go_to_the_courses_management_page();
121         // Ensure you are on course management page.
122         $this->execute("behat_course::i_should_see_the_courses_management_page", get_string('categories'));
124         // Select Miscellaneous category.
125         $this->i_click_on_category_in_the_management_interface(get_string('miscellaneous'));
126         $this->execute("behat_course::i_should_see_the_courses_management_page", get_string('categoriesandcourses'));
128         // Click create new course.
129         $this->execute('behat_general::i_click_on_in_the',
130             array(get_string('createnewcourse'), "link", "#course-listing", "css_element")
131         );
133         // If the course format is one of the fields we change how we
134         // fill the form as we need to wait for the form to be set.
135         $rowshash = $table->getRowsHash();
136         $formatfieldrefs = array(get_string('format'), 'format', 'id_format');
137         foreach ($formatfieldrefs as $fieldref) {
138             if (!empty($rowshash[$fieldref])) {
139                 $formatfield = $fieldref;
140             }
141         }
143         // Setting the format separately.
144         if (!empty($formatfield)) {
146             // Removing the format field from the TableNode.
147             $rows = $table->getRows();
148             $formatvalue = $rowshash[$formatfield];
149             foreach ($rows as $key => $row) {
150                 if ($row[0] == $formatfield) {
151                     unset($rows[$key]);
152                 }
153             }
154             $table = new TableNode($rows);
156             // Adding a forced wait until editors are loaded as otherwise selenium sometimes tries clicks on the
157             // format field when the editor is being rendered and the click misses the field coordinates.
158             $this->execute("behat_forms::i_expand_all_fieldsets");
160             $this->execute("behat_forms::i_set_the_field_to", array($formatfield, $formatvalue));
161         }
163         // Set form fields.
164         $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $table);
166         // Save course settings.
167         $this->execute("behat_forms::press_button", get_string('savechangesanddisplay'));
169     }
171     /**
172      * Goes to the system courses/categories management page.
173      *
174      * @Given /^I go to the courses management page$/
175      */
176     public function i_go_to_the_courses_management_page() {
178         $parentnodes = get_string('courses', 'admin');
180         // Go to home page.
181         $this->execute("behat_general::i_am_on_homepage");
183         // Navigate to course management via system administration.
184         $this->execute("behat_navigation::i_navigate_to_in_site_administration",
185             array($parentnodes . ' > ' . get_string('coursemgmt', 'admin'))
186         );
188     }
190     /**
191      * Adds the selected activity/resource filling the form data with the specified field/value pairs. Sections 0 and 1 are also allowed on frontpage.
192      *
193      * @When /^I add a "(?P<activity_or_resource_name_string>(?:[^"]|\\")*)" to section "(?P<section_number>\d+)" and I fill the form with:$/
194      * @param string $activity The activity name
195      * @param int $section The section number
196      * @param TableNode $data The activity field/value data
197      */
198     public function i_add_to_section_and_i_fill_the_form_with($activity, $section, TableNode $data) {
200         // Add activity to section.
201         $this->execute("behat_course::i_add_to_section",
202             array($this->escape($activity), $this->escape($section))
203         );
205         // Wait to be redirected.
206         $this->execute('behat_general::wait_until_the_page_is_ready');
208         // Set form fields.
209         $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $data);
211         // Save course settings.
212         $this->execute("behat_forms::press_button", get_string('savechangesandreturntocourse'));
213     }
215     /**
216      * Opens the activity chooser and opens the activity/resource form page. Sections 0 and 1 are also allowed on frontpage.
217      *
218      * @Given /^I add a "(?P<activity_or_resource_name_string>(?:[^"]|\\")*)" to section "(?P<section_number>\d+)"$/
219      * @throws ElementNotFoundException Thrown by behat_base::find
220      * @param string $activity
221      * @param int $section
222      */
223     public function i_add_to_section($activity, $section) {
225         if ($this->getSession()->getPage()->find('css', 'body#page-site-index') && (int)$section <= 1) {
226             // We are on the frontpage.
227             if ($section) {
228                 // Section 1 represents the contents on the frontpage.
229                 $sectionxpath = "//body[@id='page-site-index']" .
230                         "/descendant::div[contains(concat(' ',normalize-space(@class),' '),' sitetopic ')]";
231             } else {
232                 // Section 0 represents "Site main menu" block.
233                 $sectionxpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]";
234             }
235         } else {
236             // We are inside the course.
237             $sectionxpath = "//li[@id='section-" . $section . "']";
238         }
240         $activityliteral = behat_context_helper::escape(ucfirst($activity));
242         if ($this->running_javascript()) {
244             // Clicks add activity or resource section link.
245             $sectionxpath = $sectionxpath . "/descendant::div" .
246                     "[contains(concat(' ', normalize-space(@class) , ' '), ' section-modchooser ')]/button";
248             $this->execute('behat_general::i_click_on', [$sectionxpath, 'xpath']);
250             // Clicks the selected activity if it exists.
251             $activityxpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' modchooser ')]" .
252                     "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' optioninfo ')]" .
253                     "/descendant::p[contains(concat(' ', normalize-space(@class), ' '), ' optionname ')]" .
254                     "[normalize-space(.)=$activityliteral]" .
255                     "/parent::a";
257             $this->execute('behat_general::i_click_on', [$activityxpath, 'xpath']);
259         } else {
260             // Without Javascript.
262             // Selecting the option from the select box which contains the option.
263             $selectxpath = $sectionxpath . "/descendant::div" .
264                     "[contains(concat(' ', normalize-space(@class), ' '), ' section_add_menus ')]" .
265                     "/descendant::select[option[normalize-space(.)=$activityliteral]]";
266             $selectnode = $this->find('xpath', $selectxpath);
267             $selectnode->selectOption($activity);
269             // Go button.
270             $gobuttonxpath = $selectxpath . "/ancestor::form/descendant::input[@type='submit']";
271             $gobutton = $this->find('xpath', $gobuttonxpath);
272             $gobutton->click();
273         }
275     }
277     /**
278      * Opens a section edit menu if it is not already opened.
279      *
280      * @Given /^I open section "(?P<section_number>\d+)" edit menu$/
281      * @throws DriverException The step is not available when Javascript is disabled
282      * @param string $sectionnumber
283      */
284     public function i_open_section_edit_menu($sectionnumber) {
285         if (!$this->running_javascript()) {
286             throw new DriverException('Section edit menu not available when Javascript is disabled');
287         }
289         // Wait for section to be available, before clicking on the menu.
290         $this->i_wait_until_section_is_available($sectionnumber);
292         // If it is already opened we do nothing.
293         $xpath = $this->section_exists($sectionnumber);
294         $xpath .= "/descendant::div[contains(@class, 'section-actions')]/descendant::a[contains(@data-toggle, 'dropdown')]";
296         $exception = new ExpectationException('Section "' . $sectionnumber . '" was not found', $this->getSession());
297         $menu = $this->find('xpath', $xpath, $exception);
298         $menu->click();
299         $this->i_wait_until_section_is_available($sectionnumber);
300     }
302     /**
303      * Deletes course section.
304      *
305      * @Given /^I delete section "(?P<section_number>\d+)"$/
306      * @param int $sectionnumber The section number
307      */
308     public function i_delete_section($sectionnumber) {
309         // Ensures the section exists.
310         $xpath = $this->section_exists($sectionnumber);
312         // We need to know the course format as the text strings depends on them.
313         $courseformat = $this->get_course_format();
314         if (get_string_manager()->string_exists('deletesection', $courseformat)) {
315             $strdelete = get_string('deletesection', $courseformat);
316         } else {
317             $strdelete = get_string('deletesection');
318         }
320         // If javascript is on, link is inside a menu.
321         if ($this->running_javascript()) {
322             $this->i_open_section_edit_menu($sectionnumber);
323         }
325         // Click on delete link.
326         $this->execute('behat_general::i_click_on_in_the',
327             array($strdelete, "link", $this->escape($xpath), "xpath_element")
328         );
330     }
332     /**
333      * Turns course section highlighting on.
334      *
335      * @Given /^I turn section "(?P<section_number>\d+)" highlighting on$/
336      * @param int $sectionnumber The section number
337      */
338     public function i_turn_section_highlighting_on($sectionnumber) {
340         // Ensures the section exists.
341         $xpath = $this->section_exists($sectionnumber);
343         // If javascript is on, link is inside a menu.
344         if ($this->running_javascript()) {
345             $this->i_open_section_edit_menu($sectionnumber);
346         }
348         // Click on highlight topic link.
349         $this->execute('behat_general::i_click_on_in_the',
350             array(get_string('highlight'), "link", $this->escape($xpath), "xpath_element")
351         );
352     }
354     /**
355      * Turns course section highlighting off.
356      *
357      * @Given /^I turn section "(?P<section_number>\d+)" highlighting off$/
358      * @param int $sectionnumber The section number
359      */
360     public function i_turn_section_highlighting_off($sectionnumber) {
362         // Ensures the section exists.
363         $xpath = $this->section_exists($sectionnumber);
365         // If javascript is on, link is inside a menu.
366         if ($this->running_javascript()) {
367             $this->i_open_section_edit_menu($sectionnumber);
368         }
370         // Click on un-highlight topic link.
371         $this->execute('behat_general::i_click_on_in_the',
372             array(get_string('highlightoff'), "link", $this->escape($xpath), "xpath_element")
373         );
374     }
376     /**
377      * Shows the specified hidden section. You need to be in the course page and on editing mode.
378      *
379      * @Given /^I show section "(?P<section_number>\d+)"$/
380      * @param int $sectionnumber
381      */
382     public function i_show_section($sectionnumber) {
383         $showlink = $this->show_section_link_exists($sectionnumber);
385         // Ensure section edit menu is open before interacting with it.
386         if ($this->running_javascript()) {
387             $this->i_open_section_edit_menu($sectionnumber);
388         }
389         $showlink->click();
391         if ($this->running_javascript()) {
392             $this->getSession()->wait(self::get_timeout() * 1000, self::PAGE_READY_JS);
393             $this->i_wait_until_section_is_available($sectionnumber);
394         }
395     }
397     /**
398      * Hides the specified visible section. You need to be in the course page and on editing mode.
399      *
400      * @Given /^I hide section "(?P<section_number>\d+)"$/
401      * @param int $sectionnumber
402      */
403     public function i_hide_section($sectionnumber) {
404         // Ensures the section exists.
405         $xpath = $this->section_exists($sectionnumber);
407         // We need to know the course format as the text strings depends on them.
408         $courseformat = $this->get_course_format();
409         if (get_string_manager()->string_exists('hidefromothers', $courseformat)) {
410             $strhide = get_string('hidefromothers', $courseformat);
411         } else {
412             $strhide = get_string('hidesection');
413         }
415         // If javascript is on, link is inside a menu.
416         if ($this->running_javascript()) {
417             $this->i_open_section_edit_menu($sectionnumber);
418         }
420         // Click on delete link.
421         $this->execute('behat_general::i_click_on_in_the',
422               array($strhide, "link", $this->escape($xpath), "xpath_element")
423         );
425         if ($this->running_javascript()) {
426             $this->getSession()->wait(self::get_timeout() * 1000, self::PAGE_READY_JS);
427             $this->i_wait_until_section_is_available($sectionnumber);
428         }
429     }
431     /**
432      * Go to editing section page for specified section number. You need to be in the course page and on editing mode.
433      *
434      * @Given /^I edit the section "(?P<section_number>\d+)"$/
435      * @param int $sectionnumber
436      */
437     public function i_edit_the_section($sectionnumber) {
438         // If javascript is on, link is inside a menu.
439         if ($this->running_javascript()) {
440             $this->i_open_section_edit_menu($sectionnumber);
441         }
443         // We need to know the course format as the text strings depends on them.
444         $courseformat = $this->get_course_format();
445         if ($sectionnumber > 0 && get_string_manager()->string_exists('editsection', $courseformat)) {
446             $stredit = get_string('editsection', $courseformat);
447         } else {
448             $stredit = get_string('editsection');
449         }
451         // Click on un-highlight topic link.
452         $this->execute('behat_general::i_click_on_in_the',
453             array($stredit, "link", "#section-" . $sectionnumber, "css_element")
454         );
456     }
458     /**
459      * Edit specified section and fill the form data with the specified field/value pairs.
460      *
461      * @When /^I edit the section "(?P<section_number>\d+)" and I fill the form with:$/
462      * @param int $sectionnumber The section number
463      * @param TableNode $data The activity field/value data
464      */
465     public function i_edit_the_section_and_i_fill_the_form_with($sectionnumber, TableNode $data) {
467         // Edit given section.
468         $this->execute("behat_course::i_edit_the_section", $sectionnumber);
470         // Set form fields.
471         $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $data);
473         // Save section settings.
474         $this->execute("behat_forms::press_button", get_string('savechanges'));
475     }
477     /**
478      * Checks if the specified course section hightlighting is turned on. You need to be in the course page on editing mode.
479      *
480      * @Then /^section "(?P<section_number>\d+)" should be highlighted$/
481      * @throws ExpectationException
482      * @param int $sectionnumber The section number
483      */
484     public function section_should_be_highlighted($sectionnumber) {
486         // Ensures the section exists.
487         $xpath = $this->section_exists($sectionnumber);
489         // The important checking, we can not check the img.
490         $this->execute('behat_general::should_exist_in_the', ['Remove highlight', 'link', $xpath, 'xpath_element']);
491     }
493     /**
494      * Checks if the specified course section highlighting is turned off. You need to be in the course page on editing mode.
495      *
496      * @Then /^section "(?P<section_number>\d+)" should not be highlighted$/
497      * @throws ExpectationException
498      * @param int $sectionnumber The section number
499      */
500     public function section_should_not_be_highlighted($sectionnumber) {
502         // We only catch ExpectationException, ElementNotFoundException should be thrown if the specified section does not exist.
503         try {
504             $this->section_should_be_highlighted($sectionnumber);
505         } catch (ExpectationException $e) {
506             // ExpectedException means that it is not highlighted.
507             return;
508         }
510         throw new ExpectationException('The "' . $sectionnumber . '" section is highlighted', $this->getSession());
511     }
513     /**
514      * Checks that the specified section is visible. You need to be in the course page. It can be used being logged as a student and as a teacher on editing mode.
515      *
516      * @Then /^section "(?P<section_number>\d+)" should be hidden$/
517      * @throws ExpectationException
518      * @throws ElementNotFoundException Thrown by behat_base::find
519      * @param int $sectionnumber
520      */
521     public function section_should_be_hidden($sectionnumber) {
523         $sectionxpath = $this->section_exists($sectionnumber);
525         // Preventive in case there is any action in progress.
526         // Adding it here because we are interacting (click) with
527         // the elements, not necessary when we just find().
528         $this->i_wait_until_section_is_available($sectionnumber);
530         // Section should be hidden.
531         $exception = new ExpectationException('The section is not hidden', $this->getSession());
532         $this->find('xpath', $sectionxpath . "[contains(concat(' ', normalize-space(@class), ' '), ' hidden ')]", $exception);
533     }
535     /**
536      * Checks that all actiities in the specified section are hidden. You need to be in the course page. It can be used being logged as a student and as a teacher on editing mode.
537      *
538      * @Then /^all activities in section "(?P<section_number>\d+)" should be hidden$/
539      * @throws ExpectationException
540      * @throws ElementNotFoundException Thrown by behat_base::find
541      * @param int $sectionnumber
542      */
543     public function section_activities_should_be_hidden($sectionnumber) {
544         $sectionxpath = $this->section_exists($sectionnumber);
546         // Preventive in case there is any action in progress.
547         // Adding it here because we are interacting (click) with
548         // the elements, not necessary when we just find().
549         $this->i_wait_until_section_is_available($sectionnumber);
551         // The checking are different depending on user permissions.
552         if ($this->is_course_editor()) {
554             // The section must be hidden.
555             $this->show_section_link_exists($sectionnumber);
557             // If there are activities they should be hidden and the visibility icon should not be available.
558             if ($activities = $this->get_section_activities($sectionxpath)) {
560                 $dimmedexception = new ExpectationException('There are activities that are not dimmed', $this->getSession());
561                 foreach ($activities as $activity) {
562                     // Dimmed.
563                     $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' activityinstance ')]" .
564                         "//a[contains(concat(' ', normalize-space(@class), ' '), ' dimmed ')]", $dimmedexception, $activity);
565                 }
566             }
567         } else {
568             // There shouldn't be activities.
569             if ($this->get_section_activities($sectionxpath)) {
570                 throw new ExpectationException('There are activities in the section and they should be hidden', $this->getSession());
571             }
572         }
574     }
576     /**
577      * Checks that the specified section is visible. You need to be in the course page. It can be used being logged as a student and as a teacher on editing mode.
578      *
579      * @Then /^section "(?P<section_number>\d+)" should be visible$/
580      * @throws ExpectationException
581      * @param int $sectionnumber
582      */
583     public function section_should_be_visible($sectionnumber) {
585         $sectionxpath = $this->section_exists($sectionnumber);
587         // Section should not be hidden.
588         $xpath = $sectionxpath . "[not(contains(concat(' ', normalize-space(@class), ' '), ' hidden '))]";
589         if (!$this->getSession()->getPage()->find('xpath', $xpath)) {
590             throw new ExpectationException('The section is hidden', $this->getSession());
591         }
593         // Edit menu should be visible.
594         if ($this->is_course_editor()) {
595             $xpath = $sectionxpath .
596                     "/descendant::div[contains(@class, 'section-actions')]" .
597                     "/descendant::a[contains(@data-toggle, 'dropdown')]";
598             if (!$this->getSession()->getPage()->find('xpath', $xpath)) {
599                 throw new ExpectationException('The section edit menu is not available', $this->getSession());
600             }
601         }
602     }
604     /**
605      * Moves up the specified section, this step only works with Javascript disabled. Editing mode should be on.
606      *
607      * @Given /^I move up section "(?P<section_number>\d+)"$/
608      * @throws DriverException Step not available when Javascript is enabled
609      * @param int $sectionnumber
610      */
611     public function i_move_up_section($sectionnumber) {
613         if ($this->running_javascript()) {
614             throw new DriverException('Move a section up step is not available with Javascript enabled');
615         }
617         // Ensures the section exists.
618         $sectionxpath = $this->section_exists($sectionnumber);
620         // If javascript is on, link is inside a menu.
621         if ($this->running_javascript()) {
622             $this->i_open_section_edit_menu($sectionnumber);
623         }
625         // Follows the link
626         $moveuplink = $this->get_node_in_container('link', get_string('moveup'), 'xpath_element', $sectionxpath);
627         $moveuplink->click();
628     }
630     /**
631      * Moves down the specified section, this step only works with Javascript disabled. Editing mode should be on.
632      *
633      * @Given /^I move down section "(?P<section_number>\d+)"$/
634      * @throws DriverException Step not available when Javascript is enabled
635      * @param int $sectionnumber
636      */
637     public function i_move_down_section($sectionnumber) {
639         if ($this->running_javascript()) {
640             throw new DriverException('Move a section down step is not available with Javascript enabled');
641         }
643         // Ensures the section exists.
644         $sectionxpath = $this->section_exists($sectionnumber);
646         // If javascript is on, link is inside a menu.
647         if ($this->running_javascript()) {
648             $this->i_open_section_edit_menu($sectionnumber);
649         }
651         // Follows the link
652         $movedownlink = $this->get_node_in_container('link', get_string('movedown'), 'xpath_element', $sectionxpath);
653         $movedownlink->click();
654     }
656     /**
657      * Checks that the specified activity is visible. You need to be in the course page. It can be used being logged as a student and as a teacher on editing mode.
658      *
659      * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" activity should be visible$/
660      * @param string $activityname
661      * @throws ExpectationException
662      */
663     public function activity_should_be_visible($activityname) {
665         // The activity must exists and be visible.
666         $activitynode = $this->get_activity_node($activityname);
668         if ($this->is_course_editor()) {
670             // The activity should not be dimmed.
671             try {
672                 $xpath = "/descendant-or-self::a[contains(concat(' ', normalize-space(@class), ' '), ' dimmed ')] | ".
673                          "/descendant-or-self::div[contains(concat(' ', normalize-space(@class), ' '), ' dimmed_text ')]";
674                 $this->find('xpath', $xpath, false, $activitynode);
675                 throw new ExpectationException('"' . $activityname . '" is hidden', $this->getSession());
676             } catch (ElementNotFoundException $e) {
677                 // All ok.
678             }
680             // Additional check if this is a teacher in editing mode.
681             if ($this->is_editing_on()) {
682                 // The 'Hide' button should be available.
683                 $nohideexception = new ExpectationException('"' . $activityname . '" doesn\'t have a "' .
684                     get_string('hide') . '" icon', $this->getSession());
685                 $this->find('named_partial', array('link', get_string('hide')), $nohideexception, $activitynode);
686             }
687         }
688     }
690     /**
691      * Checks that the specified activity is visible. You need to be in the course page.
692      * It can be used being logged as a student and as a teacher on editing mode.
693      *
694      * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" activity should be available but hidden from course page$/
695      * @param string $activityname
696      * @throws ExpectationException
697      */
698     public function activity_should_be_available_but_hidden_from_course_page($activityname) {
700         if ($this->is_course_editor()) {
702             // The activity must exists and be visible.
703             $activitynode = $this->get_activity_node($activityname);
705             // The activity should not be dimmed.
706             try {
707                 $xpath = "/descendant-or-self::a[contains(concat(' ', normalize-space(@class), ' '), ' dimmed ')] | " .
708                     "/descendant-or-self::div[contains(concat(' ', normalize-space(@class), ' '), ' dimmed_text ')]";
709                 $this->find('xpath', $xpath, false, $activitynode);
710                 throw new ExpectationException('"' . $activityname . '" is hidden', $this->getSession());
711             } catch (ElementNotFoundException $e) {
712                 // All ok.
713             }
715             // Should has "stealth" class.
716             $exception = new ExpectationException('"' . $activityname . '" does not have CSS class "stealth"', $this->getSession());
717             $xpath = "/descendant-or-self::a[contains(concat(' ', normalize-space(@class), ' '), ' stealth ')]";
718             $this->find('xpath', $xpath, $exception, $activitynode);
720             // Additional check if this is a teacher in editing mode.
721             if ($this->is_editing_on()) {
722                 // Also has either 'Hide' or 'Make unavailable' edit control.
723                 $nohideexception = new ExpectationException('"' . $activityname . '" has neither "' . get_string('hide') .
724                     '" nor "' . get_string('makeunavailable') . '" icons', $this->getSession());
725                 try {
726                     $this->find('named_partial', array('link', get_string('hide')), false, $activitynode);
727                 } catch (ElementNotFoundException $e) {
728                     $this->find('named_partial', array('link', get_string('makeunavailable')), $nohideexception, $activitynode);
729                 }
730             }
732         } else {
734             // Student should not see the activity at all.
735             try {
736                 $this->get_activity_node($activityname);
737                 throw new ExpectationException('The "' . $activityname . '" should not appear', $this->getSession());
738             } catch (ElementNotFoundException $e) {
739                 // This is good, the activity should not be there.
740             }
741         }
742     }
744     /**
745      * Checks that the specified activity is hidden. You need to be in the course page. It can be used being logged as a student and as a teacher on editing mode.
746      *
747      * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" activity should be hidden$/
748      * @param string $activityname
749      * @throws ExpectationException
750      */
751     public function activity_should_be_hidden($activityname) {
753         if ($this->is_course_editor()) {
755             // The activity should exist.
756             $activitynode = $this->get_activity_node($activityname);
758             // Should be hidden.
759             $exception = new ExpectationException('"' . $activityname . '" is not dimmed', $this->getSession());
760             $xpath = "/descendant-or-self::a[contains(concat(' ', normalize-space(@class), ' '), ' dimmed ')] | ".
761                      "/descendant-or-self::div[contains(concat(' ', normalize-space(@class), ' '), ' dimmed_text ')]";
762             $this->find('xpath', $xpath, $exception, $activitynode);
764             // Additional check if this is a teacher in editing mode.
765             if ($this->is_editing_on()) {
766                 // Also has either 'Show' or 'Make available' edit control.
767                 $noshowexception = new ExpectationException('"' . $activityname . '" has neither "' . get_string('show') .
768                     '" nor "' . get_string('makeavailable') . '" icons', $this->getSession());
769                 try {
770                     $this->find('named_partial', array('link', get_string('show')), false, $activitynode);
771                 } catch (ElementNotFoundException $e) {
772                     $this->find('named_partial', array('link', get_string('makeavailable')), $noshowexception, $activitynode);
773                 }
774             }
776         } else {
778             // It should not exist at all.
779             try {
780                 $this->get_activity_node($activityname);
781                 throw new ExpectationException('The "' . $activityname . '" should not appear', $this->getSession());
782             } catch (ElementNotFoundException $e) {
783                 // This is good, the activity should not be there.
784             }
785         }
787     }
789     /**
790      * Checks that the specified activity is dimmed. You need to be in the course page.
791      *
792      * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" activity should be dimmed$/
793      * @param string $activityname
794      * @throws ExpectationException
795      */
796     public function activity_should_be_dimmed($activityname) {
798         // The activity should exist.
799         $activitynode = $this->get_activity_node($activityname);
801         // Should be hidden.
802         $exception = new ExpectationException('"' . $activityname . '" is not dimmed', $this->getSession());
803         $xpath = "/descendant-or-self::a[contains(concat(' ', normalize-space(@class), ' '), ' dimmed ')] | ".
804             "/descendant-or-self::div[contains(concat(' ', normalize-space(@class), ' '), ' dimmed_text ')]";
805         $this->find('xpath', $xpath, $exception, $activitynode);
807     }
809     /**
810      * Moves the specified activity to the first slot of a section. This step is experimental when using it in Javascript tests. Editing mode should be on.
811      *
812      * @Given /^I move "(?P<activity_name_string>(?:[^"]|\\")*)" activity to section "(?P<section_number>\d+)"$/
813      * @param string $activityname The activity name
814      * @param int $sectionnumber The number of section
815      */
816     public function i_move_activity_to_section($activityname, $sectionnumber) {
818         // Ensure the destination is valid.
819         $sectionxpath = $this->section_exists($sectionnumber);
821         // JS enabled.
822         if ($this->running_javascript()) {
824             $activitynode = $this->get_activity_element('Move', 'icon', $activityname);
825             $destinationxpath = $sectionxpath . "/descendant::ul[contains(concat(' ', normalize-space(@class), ' '), ' yui3-dd-drop ')]";
827             $this->execute("behat_general::i_drag_and_i_drop_it_in",
828                 array($this->escape($activitynode->getXpath()), "xpath_element",
829                     $this->escape($destinationxpath), "xpath_element")
830             );
832         } else {
833             // Following links with no-JS.
835             // Moving to the fist spot of the section (before all other section's activities).
836             $this->execute('behat_course::i_click_on_in_the_activity',
837                 array("a.editing_move", "css_element", $this->escape($activityname))
838             );
840             $this->execute('behat_general::i_click_on_in_the',
841                 array("li.movehere a", "css_element", $this->escape($sectionxpath), "xpath_element")
842             );
843         }
844     }
846     /**
847      * Edits the activity name through the edit activity; this step only works with Javascript enabled. Editing mode should be on.
848      *
849      * @Given /^I change "(?P<activity_name_string>(?:[^"]|\\")*)" activity name to "(?P<new_name_string>(?:[^"]|\\")*)"$/
850      * @throws DriverException Step not available when Javascript is disabled
851      * @param string $activityname
852      * @param string $newactivityname
853      */
854     public function i_change_activity_name_to($activityname, $newactivityname) {
856         if (!$this->running_javascript()) {
857             throw new DriverException('Change activity name step is not available with Javascript disabled');
858         }
860         $activity = $this->escape($activityname);
862         $this->execute('behat_course::i_click_on_in_the_activity',
863             array(get_string('edittitle'), "link", $activity)
864         );
866         // Adding chr(10) to save changes.
867         $this->execute('behat_forms::i_set_the_field_to',
868             array('title', $this->escape($newactivityname) . chr(10))
869         );
871     }
873     /**
874      * Opens an activity actions menu if it is not already opened.
875      *
876      * @Given /^I open "(?P<activity_name_string>(?:[^"]|\\")*)" actions menu$/
877      * @throws DriverException The step is not available when Javascript is disabled
878      * @param string $activityname
879      */
880     public function i_open_actions_menu($activityname) {
882         if (!$this->running_javascript()) {
883             throw new DriverException('Activities actions menu not available when Javascript is disabled');
884         }
886         // If it is already opened we do nothing.
887         $activitynode = $this->get_activity_node($activityname);
889         // Find the menu.
890         $menunode = $activitynode->find('css', 'a[data-toggle=dropdown]');
891         if (!$menunode) {
892             throw new ExpectationException(sprintf('Could not find actions menu for the activity "%s"', $activityname),
893                     $this->getSession());
894         }
895         $expanded = $menunode->getAttribute('aria-expanded');
896         if ($expanded == 'true') {
897             return;
898         }
900         $this->execute('behat_course::i_click_on_in_the_activity',
901                 array("a[data-toggle='dropdown']", "css_element", $this->escape($activityname))
902         );
904         $this->actions_menu_should_be_open($activityname);
905     }
907     /**
908      * Closes an activity actions menu if it is not already closed.
909      *
910      * @Given /^I close "(?P<activity_name_string>(?:[^"]|\\")*)" actions menu$/
911      * @throws DriverException The step is not available when Javascript is disabled
912      * @param string $activityname
913      */
914     public function i_close_actions_menu($activityname) {
916         if (!$this->running_javascript()) {
917             throw new DriverException('Activities actions menu not available when Javascript is disabled');
918         }
920         // If it is already closed we do nothing.
921         $activitynode = $this->get_activity_node($activityname);
922         // Find the menu.
923         $menunode = $activitynode->find('css', 'a[data-toggle=dropdown]');
924         if (!$menunode) {
925             throw new ExpectationException(sprintf('Could not find actions menu for the activity "%s"', $activityname),
926                     $this->getSession());
927         }
928         $expanded = $menunode->getAttribute('aria-expanded');
929         if ($expanded != 'true') {
930             return;
931         }
933         $this->execute('behat_course::i_click_on_in_the_activity',
934                 array("a[data-toggle='dropdown']", "css_element", $this->escape($activityname))
935         );
936     }
938     /**
939      * Checks that the specified activity's action menu is open.
940      *
941      * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" actions menu should be open$/
942      * @throws DriverException The step is not available when Javascript is disabled
943      * @param string $activityname
944      */
945     public function actions_menu_should_be_open($activityname) {
947         if (!$this->running_javascript()) {
948             throw new DriverException('Activities actions menu not available when Javascript is disabled');
949         }
951         $activitynode = $this->get_activity_node($activityname);
952         // Find the menu.
953         $menunode = $activitynode->find('css', 'a[data-toggle=dropdown]');
954         if (!$menunode) {
955             throw new ExpectationException(sprintf('Could not find actions menu for the activity "%s"', $activityname),
956                     $this->getSession());
957         }
958         $expanded = $menunode->getAttribute('aria-expanded');
959         if ($expanded != 'true') {
960             throw new ExpectationException(sprintf("The action menu for '%s' is not open", $activityname), $this->getSession());
961         }
962     }
964     /**
965      * Checks that the specified activity's action menu contains an item.
966      *
967      * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" actions menu should have "(?P<menu_item_string>(?:[^"]|\\")*)" item$/
968      * @throws DriverException The step is not available when Javascript is disabled
969      * @param string $activityname
970      * @param string $menuitem
971      */
972     public function actions_menu_should_have_item($activityname, $menuitem) {
973         $activitynode = $this->get_activity_node($activityname);
975         $notfoundexception = new ExpectationException('"' . $activityname . '" doesn\'t have a "' .
976             $menuitem . '" item', $this->getSession());
977         $this->find('named_partial', array('link', $menuitem), $notfoundexception, $activitynode);
978     }
980     /**
981      * Checks that the specified activity's action menu does not contains an item.
982      *
983      * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" actions menu should not have "(?P<menu_item_string>(?:[^"]|\\")*)" item$/
984      * @throws DriverException The step is not available when Javascript is disabled
985      * @param string $activityname
986      * @param string $menuitem
987      */
988     public function actions_menu_should_not_have_item($activityname, $menuitem) {
989         $activitynode = $this->get_activity_node($activityname);
991         try {
992             $this->find('named_partial', array('link', $menuitem), false, $activitynode);
993             throw new ExpectationException('"' . $activityname . '" has a "' . $menuitem .
994                 '" item when it should not', $this->getSession());
995         } catch (ElementNotFoundException $e) {
996             // This is good, the menu item should not be there.
997         }
998     }
1000     /**
1001      * Indents to the right the activity or resource specified by it's name. Editing mode should be on.
1002      *
1003      * @Given /^I indent right "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
1004      * @param string $activityname
1005      */
1006     public function i_indent_right_activity($activityname) {
1008         $activity = $this->escape($activityname);
1009         if ($this->running_javascript()) {
1010             $this->i_open_actions_menu($activity);
1011         }
1013         $this->execute('behat_course::i_click_on_in_the_activity',
1014             array(get_string('moveright'), "link", $this->escape($activity))
1015         );
1017     }
1019     /**
1020      * Indents to the left the activity or resource specified by it's name. Editing mode should be on.
1021      *
1022      * @Given /^I indent left "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
1023      * @param string $activityname
1024      */
1025     public function i_indent_left_activity($activityname) {
1027         $activity = $this->escape($activityname);
1028         if ($this->running_javascript()) {
1029             $this->i_open_actions_menu($activity);
1030         }
1032         $this->execute('behat_course::i_click_on_in_the_activity',
1033             array(get_string('moveleft'), "link", $this->escape($activity))
1034         );
1036     }
1038     /**
1039      * Deletes the activity or resource specified by it's name. This step is experimental when using it in Javascript tests. You should be in the course page with editing mode on.
1040      *
1041      * @Given /^I delete "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
1042      * @param string $activityname
1043      */
1044     public function i_delete_activity($activityname) {
1045         $steps = array();
1046         $activity = $this->escape($activityname);
1047         if ($this->running_javascript()) {
1048             $this->i_open_actions_menu($activity);
1049         }
1051         $this->execute('behat_course::i_click_on_in_the_activity',
1052             array(get_string('delete'), "link", $this->escape($activity))
1053         );
1055         // JS enabled.
1056         // Not using chain steps here because the exceptions catcher have problems detecting
1057         // JS modal windows and avoiding interacting them at the same time.
1058         if ($this->running_javascript()) {
1059             $this->execute('behat_general::i_click_on_in_the',
1060                 array(get_string('yes'), "button", "Confirm", "dialogue")
1061             );
1062         } else {
1063             $this->execute("behat_forms::press_button", get_string('yes'));
1064         }
1066         return $steps;
1067     }
1069     /**
1070      * Duplicates the activity or resource specified by it's name. You should be in the course page with editing mode on.
1071      *
1072      * @Given /^I duplicate "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
1073      * @param string $activityname
1074      */
1075     public function i_duplicate_activity($activityname) {
1076         $steps = array();
1077         $activity = $this->escape($activityname);
1078         if ($this->running_javascript()) {
1079             $this->i_open_actions_menu($activity);
1080         }
1081         $this->execute('behat_course::i_click_on_in_the_activity',
1082             array(get_string('duplicate'), "link", $activity)
1083         );
1085     }
1087     /**
1088      * Duplicates the activity or resource and modifies the new activity with the provided data. You should be in the course page with editing mode on.
1089      *
1090      * @Given /^I duplicate "(?P<activity_name_string>(?:[^"]|\\")*)" activity editing the new copy with:$/
1091      * @param string $activityname
1092      * @param TableNode $data
1093      */
1094     public function i_duplicate_activity_editing_the_new_copy_with($activityname, TableNode $data) {
1096         $activity = $this->escape($activityname);
1097         $activityliteral = behat_context_helper::escape($activityname);
1099         $this->execute("behat_course::i_duplicate_activity", $activity);
1101         // Determine the future new activity xpath from the former one.
1102         $duplicatedxpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]" .
1103                 "[contains(., $activityliteral)]/following-sibling::li";
1104         $duplicatedactionsmenuxpath = $duplicatedxpath . "/descendant::a[@data-toggle='dropdown']";
1106         if ($this->running_javascript()) {
1107             // We wait until the AJAX request finishes and the section is visible again.
1108             $hiddenlightboxxpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]" .
1109                     "[contains(., $activityliteral)]" .
1110                     "/ancestor::li[contains(concat(' ', normalize-space(@class), ' '), ' section ')]" .
1111                     "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]";
1113             $this->execute("behat_general::wait_until_exists",
1114                     array($this->escape($hiddenlightboxxpath), "xpath_element")
1115             );
1117             // Close the original activity actions menu.
1118             $this->i_close_actions_menu($activity);
1120             // The next sibling of the former activity will be the duplicated one, so we click on it from it's xpath as, at
1121             // this point, it don't even exists in the DOM (the steps are executed when we return them).
1122             $this->execute('behat_general::i_click_on',
1123                     array($this->escape($duplicatedactionsmenuxpath), "xpath_element")
1124             );
1125         }
1127         // We force the xpath as otherwise mink tries to interact with the former one.
1128         $this->execute('behat_general::i_click_on_in_the',
1129                 array(get_string('editsettings'), "link", $this->escape($duplicatedxpath), "xpath_element")
1130         );
1132         $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $data);
1133         $this->execute("behat_forms::press_button", get_string('savechangesandreturntocourse'));
1135     }
1137     /**
1138      * Waits until the section is available to interact with it. Useful when the section is performing an action and the section is overlayed with a loading layout.
1139      *
1140      * Using the protected method as this method will be usually
1141      * called by other methods which are not returning a set of
1142      * steps and performs the actions directly, so it would not
1143      * be executed if it returns another step.
1144      *
1145      * Hopefully we would not require test writers to use this step
1146      * and we will manage it from other step definitions.
1147      *
1148      * @Given /^I wait until section "(?P<section_number>\d+)" is available$/
1149      * @param int $sectionnumber
1150      * @return void
1151      */
1152     public function i_wait_until_section_is_available($sectionnumber) {
1154         // Looks for a hidden lightbox or a non-existent lightbox in that section.
1155         $sectionxpath = $this->section_exists($sectionnumber);
1156         $hiddenlightboxxpath = $sectionxpath . "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]" .
1157             " | " .
1158             $sectionxpath . "[count(child::div[contains(@class, 'lightbox')]) = 0]";
1160         $this->ensure_element_exists($hiddenlightboxxpath, 'xpath_element');
1161     }
1163     /**
1164      * Clicks on the specified element of the activity. You should be in the course page with editing mode turned on.
1165      *
1166      * @Given /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" in the "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
1167      * @param string $element
1168      * @param string $selectortype
1169      * @param string $activityname
1170      */
1171     public function i_click_on_in_the_activity($element, $selectortype, $activityname) {
1172         $element = $this->get_activity_element($element, $selectortype, $activityname);
1173         $element->click();
1174     }
1176     /**
1177      * Clicks on the specified element inside the activity container.
1178      *
1179      * @throws ElementNotFoundException
1180      * @param string $element
1181      * @param string $selectortype
1182      * @param string $activityname
1183      * @return NodeElement
1184      */
1185     protected function get_activity_element($element, $selectortype, $activityname) {
1186         $activitynode = $this->get_activity_node($activityname);
1188         $exception = new ElementNotFoundException($this->getSession(), "'{$element}' '{$selectortype}' in '${activityname}'");
1189         return $this->find($selectortype, $element, $exception, $activitynode);
1190     }
1192     /**
1193      * Checks if the course section exists.
1194      *
1195      * @throws ElementNotFoundException Thrown by behat_base::find
1196      * @param int $sectionnumber
1197      * @return string The xpath of the section.
1198      */
1199     protected function section_exists($sectionnumber) {
1201         // Just to give more info in case it does not exist.
1202         $xpath = "//li[@id='section-" . $sectionnumber . "']";
1203         $exception = new ElementNotFoundException($this->getSession(), "Section $sectionnumber ");
1204         $this->find('xpath', $xpath, $exception);
1206         return $xpath;
1207     }
1209     /**
1210      * Returns the show section icon or throws an exception.
1211      *
1212      * @throws ElementNotFoundException Thrown by behat_base::find
1213      * @param int $sectionnumber
1214      * @return NodeElement
1215      */
1216     protected function show_section_link_exists($sectionnumber) {
1218         // Gets the section xpath and ensure it exists.
1219         $xpath = $this->section_exists($sectionnumber);
1221         // We need to know the course format as the text strings depends on them.
1222         $courseformat = $this->get_course_format();
1224         // Checking the show button alt text and show icon.
1225         $showtext = get_string('showfromothers', $courseformat);
1226         $linkxpath = $xpath . "//a[*[contains(text(), " . behat_context_helper::escape($showtext) . ")]]";
1228         $exception = new ElementNotFoundException($this->getSession(), 'Show section link');
1230         // Returing the link so both Non-JS and JS browsers can interact with it.
1231         return $this->find('xpath', $linkxpath, $exception);
1232     }
1234     /**
1235      * Returns the hide section icon link if it exists or throws exception.
1236      *
1237      * @throws ElementNotFoundException Thrown by behat_base::find
1238      * @param int $sectionnumber
1239      * @return NodeElement
1240      */
1241     protected function hide_section_link_exists($sectionnumber) {
1243         // Gets the section xpath and ensure it exists.
1244         $xpath = $this->section_exists($sectionnumber);
1246         // We need to know the course format as the text strings depends on them.
1247         $courseformat = $this->get_course_format();
1249         // Checking the hide button alt text and hide icon.
1250         $hidetext = behat_context_helper::escape(get_string('hidefromothers', $courseformat));
1251         $linkxpath = $xpath . "/descendant::a[@title=$hidetext]";
1253         $exception = new ElementNotFoundException($this->getSession(), 'Hide section icon ');
1254         $this->find('icon', 'Hide', $exception);
1256         // Returing the link so both Non-JS and JS browsers can interact with it.
1257         return $this->find('xpath', $linkxpath, $exception);
1258     }
1260     /**
1261      * Gets the current course format.
1262      *
1263      * @throws ExpectationException If we are not in the course view page.
1264      * @return string The course format in a frankenstyled name.
1265      */
1266     protected function get_course_format() {
1268         $exception = new ExpectationException('You are not in a course page', $this->getSession());
1270         // The moodle body's id attribute contains the course format.
1271         $node = $this->getSession()->getPage()->find('css', 'body');
1272         if (!$node) {
1273             throw $exception;
1274         }
1276         if (!$bodyid = $node->getAttribute('id')) {
1277             throw $exception;
1278         }
1280         if (strstr($bodyid, 'page-course-view-') === false) {
1281             throw $exception;
1282         }
1284         return 'format_' . str_replace('page-course-view-', '', $bodyid);
1285     }
1287     /**
1288      * Gets the section's activites DOM nodes.
1289      *
1290      * @param string $sectionxpath
1291      * @return array NodeElement instances
1292      */
1293     protected function get_section_activities($sectionxpath) {
1295         $xpath = $sectionxpath . "/descendant::li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]";
1297         // We spin here, as activities usually require a lot of time to load.
1298         try {
1299             $activities = $this->find_all('xpath', $xpath);
1300         } catch (ElementNotFoundException $e) {
1301             return false;
1302         }
1304         return $activities;
1305     }
1307     /**
1308      * Returns the DOM node of the activity from <li>.
1309      *
1310      * @throws ElementNotFoundException Thrown by behat_base::find
1311      * @param string $activityname The activity name
1312      * @return NodeElement
1313      */
1314     protected function get_activity_node($activityname) {
1316         $activityname = behat_context_helper::escape($activityname);
1317         $xpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][contains(., $activityname)]";
1319         return $this->find('xpath', $xpath);
1320     }
1322     /**
1323      * Gets the activity instance name from the activity node.
1324      *
1325      * @throws ElementNotFoundException
1326      * @param NodeElement $activitynode
1327      * @return string
1328      */
1329     protected function get_activity_name($activitynode) {
1330         $instancenamenode = $this->find('xpath', "//span[contains(concat(' ', normalize-space(@class), ' '), ' instancename ')]", false, $activitynode);
1331         return $instancenamenode->getText();
1332     }
1334     /**
1335      * Returns whether the user can edit the course contents or not.
1336      *
1337      * @return bool
1338      */
1339     protected function is_course_editor() {
1341         // We don't need to behat_base::spin() here as all is already loaded.
1342         if (!$this->getSession()->getPage()->findButton(get_string('turneditingoff')) &&
1343                 !$this->getSession()->getPage()->findButton(get_string('turneditingon'))) {
1344             return false;
1345         }
1347         return true;
1348     }
1350     /**
1351      * Returns whether the user can edit the course contents and the editing mode is on.
1352      *
1353      * @return bool
1354      */
1355     protected function is_editing_on() {
1356         return $this->getSession()->getPage()->findButton(get_string('turneditingoff')) ? true : false;
1357     }
1359     /**
1360      * Returns the id of the category with the given idnumber.
1361      *
1362      * Please note that this function requires the category to exist. If it does not exist an ExpectationException is thrown.
1363      *
1364      * @param string $idnumber
1365      * @return string
1366      * @throws ExpectationException
1367      */
1368     protected function get_category_id($idnumber) {
1369         global $DB;
1370         try {
1371             return $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber), MUST_EXIST);
1372         } catch (dml_missing_record_exception $ex) {
1373             throw new ExpectationException(sprintf("There is no category in the database with the idnumber '%s'", $idnumber));
1374         }
1375     }
1377     /**
1378      * Returns the id of the course with the given idnumber.
1379      *
1380      * Please note that this function requires the category to exist. If it does not exist an ExpectationException is thrown.
1381      *
1382      * @param string $idnumber
1383      * @return string
1384      * @throws ExpectationException
1385      */
1386     protected function get_course_id($idnumber) {
1387         global $DB;
1388         try {
1389             return $DB->get_field('course', 'id', array('idnumber' => $idnumber), MUST_EXIST);
1390         } catch (dml_missing_record_exception $ex) {
1391             throw new ExpectationException(sprintf("There is no course in the database with the idnumber '%s'", $idnumber));
1392         }
1393     }
1395     /**
1396      * Returns the category node from within the listing on the management page.
1397      *
1398      * @param string $idnumber
1399      * @return \Behat\Mink\Element\NodeElement
1400      */
1401     protected function get_management_category_listing_node_by_idnumber($idnumber) {
1402         $id = $this->get_category_id($idnumber);
1403         $selector = sprintf('#category-listing .listitem-category[data-id="%d"] > div', $id);
1404         return $this->find('css', $selector);
1405     }
1407     /**
1408      * Returns a category node from within the management interface.
1409      *
1410      * @param string $name The name of the category.
1411      * @param bool $link If set to true we'll resolve to the link rather than just the node.
1412      * @return \Behat\Mink\Element\NodeElement
1413      */
1414     protected function get_management_category_listing_node_by_name($name, $link = false) {
1415         $selector = "//div[@id='category-listing']//li[contains(concat(' ', normalize-space(@class), ' '), ' listitem-category ')]//a[text()='{$name}']";
1416         if ($link === false) {
1417             $selector .= "/ancestor::li[@data-id][1]";
1418         }
1419         return $this->find('xpath', $selector);
1420     }
1422     /**
1423      * Returns a course node from within the management interface.
1424      *
1425      * @param string $name The name of the course.
1426      * @param bool $link If set to true we'll resolve to the link rather than just the node.
1427      * @return \Behat\Mink\Element\NodeElement
1428      */
1429     protected function get_management_course_listing_node_by_name($name, $link = false) {
1430         $selector = "//div[@id='course-listing']//li[contains(concat(' ', @class, ' '), ' listitem-course ')]//a[text()='{$name}']";
1431         if ($link === false) {
1432             $selector .= "/ancestor::li[@data-id]";
1433         }
1434         return $this->find('xpath', $selector);
1435     }
1437     /**
1438      * Returns the course node from within the listing on the management page.
1439      *
1440      * @param string $idnumber
1441      * @return \Behat\Mink\Element\NodeElement
1442      */
1443     protected function get_management_course_listing_node_by_idnumber($idnumber) {
1444         $id = $this->get_course_id($idnumber);
1445         $selector = sprintf('#course-listing .listitem-course[data-id="%d"] > div', $id);
1446         return $this->find('css', $selector);
1447     }
1449     /**
1450      * Clicks on a category in the management interface.
1451      *
1452      * @Given /^I click on category "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1453      * @param string $name
1454      */
1455     public function i_click_on_category_in_the_management_interface($name) {
1456         $node = $this->get_management_category_listing_node_by_name($name, true);
1457         $node->click();
1458     }
1460     /**
1461      * Clicks on a course in the management interface.
1462      *
1463      * @Given /^I click on course "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1464      * @param string $name
1465      */
1466     public function i_click_on_course_in_the_management_interface($name) {
1467         $node = $this->get_management_course_listing_node_by_name($name, true);
1468         $node->click();
1469     }
1471     /**
1472      * Clicks on a category checkbox in the management interface, if not checked.
1473      *
1474      * @Given /^I select category "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1475      * @param string $name
1476      */
1477     public function i_select_category_in_the_management_interface($name) {
1478         $node = $this->get_management_category_listing_node_by_name($name);
1479         $node = $node->findField('bcat[]');
1480         if (!$node->isChecked()) {
1481             $node->click();
1482         }
1483     }
1485     /**
1486      * Clicks on a category checkbox in the management interface, if checked.
1487      *
1488      * @Given /^I unselect category "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1489      * @param string $name
1490      */
1491     public function i_unselect_category_in_the_management_interface($name) {
1492         $node = $this->get_management_category_listing_node_by_name($name);
1493         $node = $node->findField('bcat[]');
1494         if ($node->isChecked()) {
1495             $node->click();
1496         }
1497     }
1499     /**
1500      * Clicks course checkbox in the management interface, if not checked.
1501      *
1502      * @Given /^I select course "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1503      * @param string $name
1504      */
1505     public function i_select_course_in_the_management_interface($name) {
1506         $node = $this->get_management_course_listing_node_by_name($name);
1507         $node = $node->findField('bc[]');
1508         if (!$node->isChecked()) {
1509             $node->click();
1510         }
1511     }
1513     /**
1514      * Clicks course checkbox in the management interface, if checked.
1515      *
1516      * @Given /^I unselect course "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1517      * @param string $name
1518      */
1519     public function i_unselect_course_in_the_management_interface($name) {
1520         $node = $this->get_management_course_listing_node_by_name($name);
1521         $node = $node->findField('bc[]');
1522         if ($node->isChecked()) {
1523             $node->click();
1524         }
1525     }
1527     /**
1528      * Move selected categories to top level in the management interface.
1529      *
1530      * @Given /^I move category "(?P<name_string>(?:[^"]|\\")*)" to top level in the management interface$/
1531      * @param string $name
1532      */
1533     public function i_move_category_to_top_level_in_the_management_interface($name) {
1534         $this->i_select_category_in_the_management_interface($name);
1536         $this->execute('behat_forms::i_set_the_field_to',
1537             array('menumovecategoriesto', core_course_category::get(0)->get_formatted_name())
1538         );
1540         // Save event.
1541         $this->execute("behat_forms::press_button", "bulkmovecategories");
1542     }
1544     /**
1545      * Checks that a category is a subcategory of specific category.
1546      *
1547      * @Given /^I should see category "(?P<subcatidnumber_string>(?:[^"]|\\")*)" as subcategory of "(?P<catidnumber_string>(?:[^"]|\\")*)" in the management interface$/
1548      * @throws ExpectationException
1549      * @param string $subcatidnumber
1550      * @param string $catidnumber
1551      */
1552     public function i_should_see_category_as_subcategory_of_in_the_management_interface($subcatidnumber, $catidnumber) {
1553         $categorynodeid = $this->get_category_id($catidnumber);
1554         $subcategoryid = $this->get_category_id($subcatidnumber);
1555         $exception = new ExpectationException('The category '.$subcatidnumber.' is not a subcategory of '.$catidnumber, $this->getSession());
1556         $selector = sprintf('#category-listing .listitem-category[data-id="%d"] .listitem-category[data-id="%d"]', $categorynodeid, $subcategoryid);
1557         $this->find('css', $selector, $exception);
1558     }
1560     /**
1561      * Checks that a category is not a subcategory of specific category.
1562      *
1563      * @Given /^I should not see category "(?P<subcatidnumber_string>(?:[^"]|\\")*)" as subcategory of "(?P<catidnumber_string>(?:[^"]|\\")*)" in the management interface$/
1564      * @throws ExpectationException
1565      * @param string $subcatidnumber
1566      * @param string $catidnumber
1567      */
1568     public function i_should_not_see_category_as_subcategory_of_in_the_management_interface($subcatidnumber, $catidnumber) {
1569         try {
1570             $this->i_should_see_category_as_subcategory_of_in_the_management_interface($subcatidnumber, $catidnumber);
1571         } catch (ExpectationException $e) {
1572             // ExpectedException means that it is not highlighted.
1573             return;
1574         }
1575         throw new ExpectationException('The category '.$subcatidnumber.' is a subcategory of '.$catidnumber, $this->getSession());
1576     }
1578     /**
1579      * Click to expand a category revealing its sub categories within the management UI.
1580      *
1581      * @Given /^I click to expand category "(?P<idnumber_string>(?:[^"]|\\")*)" in the management interface$/
1582      * @param string $idnumber
1583      */
1584     public function i_click_to_expand_category_in_the_management_interface($idnumber) {
1585         $categorynode = $this->get_management_category_listing_node_by_idnumber($idnumber);
1586         $exception = new ExpectationException('Category "' . $idnumber . '" does not contain an expand or collapse toggle.', $this->getSession());
1587         $togglenode = $this->find('css', 'a[data-action=collapse],a[data-action=expand]', $exception, $categorynode);
1588         $togglenode->click();
1589     }
1591     /**
1592      * Checks that a category within the management interface is visible.
1593      *
1594      * @Given /^category in management listing should be visible "(?P<idnumber_string>(?:[^"]|\\")*)"$/
1595      * @param string $idnumber
1596      */
1597     public function category_in_management_listing_should_be_visible($idnumber) {
1598         $id = $this->get_category_id($idnumber);
1599         $exception = new ExpectationException('The category '.$idnumber.' is not visible.', $this->getSession());
1600         $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible="1"]', $id);
1601         $this->find('css', $selector, $exception);
1602     }
1604     /**
1605      * Checks that a category within the management interface is dimmed.
1606      *
1607      * @Given /^category in management listing should be dimmed "(?P<idnumber_string>(?:[^"]|\\")*)"$/
1608      * @param string $idnumber
1609      */
1610     public function category_in_management_listing_should_be_dimmed($idnumber) {
1611         $id = $this->get_category_id($idnumber);
1612         $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible="0"]', $id);
1613         $exception = new ExpectationException('The category '.$idnumber.' is visible.', $this->getSession());
1614         $this->find('css', $selector, $exception);
1615     }
1617     /**
1618      * Checks that a course within the management interface is visible.
1619      *
1620      * @Given /^course in management listing should be visible "(?P<idnumber_string>(?:[^"]|\\")*)"$/
1621      * @param string $idnumber
1622      */
1623     public function course_in_management_listing_should_be_visible($idnumber) {
1624         $id = $this->get_course_id($idnumber);
1625         $exception = new ExpectationException('The course '.$idnumber.' is not visible.', $this->getSession());
1626         $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible="1"]', $id);
1627         $this->find('css', $selector, $exception);
1628     }
1630     /**
1631      * Checks that a course within the management interface is dimmed.
1632      *
1633      * @Given /^course in management listing should be dimmed "(?P<idnumber_string>(?:[^"]|\\")*)"$/
1634      * @param string $idnumber
1635      */
1636     public function course_in_management_listing_should_be_dimmed($idnumber) {
1637         $id = $this->get_course_id($idnumber);
1638         $exception = new ExpectationException('The course '.$idnumber.' is visible.', $this->getSession());
1639         $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible="0"]', $id);
1640         $this->find('css', $selector, $exception);
1641     }
1643     /**
1644      * Toggles the visibility of a course in the management UI.
1645      *
1646      * If it was visible it will be hidden. If it is hidden it will be made visible.
1647      *
1648      * @Given /^I toggle visibility of course "(?P<idnumber_string>(?:[^"]|\\")*)" in management listing$/
1649      * @param string $idnumber
1650      */
1651     public function i_toggle_visibility_of_course_in_management_listing($idnumber) {
1652         $id = $this->get_course_id($idnumber);
1653         $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible]', $id);
1654         $node = $this->find('css', $selector);
1655         $exception = new ExpectationException('Course listing "' . $idnumber . '" does not contain a show or hide toggle.', $this->getSession());
1656         if ($node->getAttribute('data-visible') === '1') {
1657             $toggle = $this->find('css', '.action-hide', $exception, $node);
1658         } else {
1659             $toggle = $this->find('css', '.action-show', $exception, $node);
1660         }
1661         $toggle->click();
1662     }
1664     /**
1665      * Toggles the visibility of a category in the management UI.
1666      *
1667      * If it was visible it will be hidden. If it is hidden it will be made visible.
1668      *
1669      * @Given /^I toggle visibility of category "(?P<idnumber_string>(?:[^"]|\\")*)" in management listing$/
1670      */
1671     public function i_toggle_visibility_of_category_in_management_listing($idnumber) {
1672         $id = $this->get_category_id($idnumber);
1673         $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible]', $id);
1674         $node = $this->find('css', $selector);
1675         $exception = new ExpectationException('Category listing "' . $idnumber . '" does not contain a show or hide toggle.', $this->getSession());
1676         if ($node->getAttribute('data-visible') === '1') {
1677             $toggle = $this->find('css', '.action-hide', $exception, $node);
1678         } else {
1679             $toggle = $this->find('css', '.action-show', $exception, $node);
1680         }
1681         $toggle->click();
1682     }
1684     /**
1685      * Moves a category displayed in the management interface up or down one place.
1686      *
1687      * @Given /^I click to move category "(?P<idnumber_string>(?:[^"]|\\")*)" (?P<direction>up|down) one$/
1688      *
1689      * @param string $idnumber The category idnumber
1690      * @param string $direction The direction to move in, either up or down
1691      */
1692     public function i_click_to_move_category_by_one($idnumber, $direction) {
1693         $node = $this->get_management_category_listing_node_by_idnumber($idnumber);
1694         $this->user_moves_listing_by_one('category', $node, $direction);
1695     }
1697     /**
1698      * Moves a course displayed in the management interface up or down one place.
1699      *
1700      * @Given /^I click to move course "(?P<idnumber_string>(?:[^"]|\\")*)" (?P<direction>up|down) one$/
1701      *
1702      * @param string $idnumber The course idnumber
1703      * @param string $direction The direction to move in, either up or down
1704      */
1705     public function i_click_to_move_course_by_one($idnumber, $direction) {
1706         $node = $this->get_management_course_listing_node_by_idnumber($idnumber);
1707         $this->user_moves_listing_by_one('course', $node, $direction);
1708     }
1710     /**
1711      * Moves a course or category listing within the management interface up or down by one.
1712      *
1713      * @param string $listingtype One of course or category
1714      * @param \Behat\Mink\Element\NodeElement $listingnode
1715      * @param string $direction One of up or down.
1716      * @param bool $highlight If set to false we don't check the node has been highlighted.
1717      */
1718     protected function user_moves_listing_by_one($listingtype, $listingnode, $direction, $highlight = true) {
1719         $up = (strtolower($direction) === 'up');
1720         if ($up) {
1721             $exception = new ExpectationException($listingtype.' listing does not contain a moveup button.', $this->getSession());
1722             $button = $this->find('css', 'a.action-moveup', $exception, $listingnode);
1723         } else {
1724             $exception = new ExpectationException($listingtype.' listing does not contain a movedown button.', $this->getSession());
1725             $button = $this->find('css', 'a.action-movedown', $exception, $listingnode);
1726         }
1727         $button->click();
1728         if ($this->running_javascript() && $highlight) {
1729             $listitem = $listingnode->getParent();
1730             $exception = new ExpectationException('Nothing was highlighted, ajax didn\'t occur or didn\'t succeed.', $this->getSession());
1731             $this->spin(array($this, 'listing_is_highlighted'), $listitem->getTagName().'#'.$listitem->getAttribute('id'), 2, $exception, true);
1732         }
1733     }
1735     /**
1736      * Used by spin to determine the callback has been highlighted.
1737      *
1738      * @param behat_course $self A self reference (default first arg from a spin callback)
1739      * @param \Behat\Mink\Element\NodeElement $selector
1740      * @return bool
1741      */
1742     protected function listing_is_highlighted($self, $selector) {
1743         $listitem = $this->find('css', $selector);
1744         return $listitem->hasClass('highlight');
1745     }
1747     /**
1748      * Check that one course appears before another in the course category management listings.
1749      *
1750      * @Given /^I should see course listing "(?P<preceedingcourse_string>(?:[^"]|\\")*)" before "(?P<followingcourse_string>(?:[^"]|\\")*)"$/
1751      *
1752      * @param string $preceedingcourse The first course to find
1753      * @param string $followingcourse The second course to find (should be AFTER the first course)
1754      * @throws ExpectationException
1755      */
1756     public function i_should_see_course_listing_before($preceedingcourse, $followingcourse) {
1757         $xpath = "//div[@id='course-listing']//li[contains(concat(' ', @class, ' '), ' listitem-course ')]//a[text()='{$preceedingcourse}']/ancestor::li[@data-id]//following::a[text()='{$followingcourse}']";
1758         $msg = "{$preceedingcourse} course does not appear before {$followingcourse} course";
1759         if (!$this->getSession()->getDriver()->find($xpath)) {
1760             throw new ExpectationException($msg, $this->getSession());
1761         }
1762     }
1764     /**
1765      * Check that one category appears before another in the course category management listings.
1766      *
1767      * @Given /^I should see category listing "(?P<preceedingcategory_string>(?:[^"]|\\")*)" before "(?P<followingcategory_string>(?:[^"]|\\")*)"$/
1768      *
1769      * @param string $preceedingcategory The first category to find
1770      * @param string $followingcategory The second category to find (should be after the first category)
1771      * @throws ExpectationException
1772      */
1773     public function i_should_see_category_listing_before($preceedingcategory, $followingcategory) {
1774         $xpath = "//div[@id='category-listing']//li[contains(concat(' ', @class, ' '), ' listitem-category ')]//a[text()='{$preceedingcategory}']/ancestor::li[@data-id]//following::a[text()='{$followingcategory}']";
1775         $msg = "{$preceedingcategory} category does not appear before {$followingcategory} category";
1776         if (!$this->getSession()->getDriver()->find($xpath)) {
1777             throw new ExpectationException($msg, $this->getSession());
1778         }
1779     }
1781     /**
1782      * Checks that we are on the course management page that we expect to be on and that no course has been selected.
1783      *
1784      * @Given /^I should see the "(?P<mode_string>(?:[^"]|\\")*)" management page$/
1785      * @param string $mode The mode to expected. One of 'Courses', 'Course categories' or 'Course categories and courses'
1786      */
1787     public function i_should_see_the_courses_management_page($mode) {
1788         $this->execute("behat_general::assert_element_contains_text",
1789             array("Course and category management", "h2", "css_element")
1790         );
1792         switch ($mode) {
1793             case "Courses":
1794                 $this->execute("behat_general::should_not_exist", array("#category-listing", "css_element"));
1795                 $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
1796                 break;
1798             case "Course categories":
1799                 $this->execute("behat_general::should_exist", array("#category-listing", "css_element"));
1800                 $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
1801                 break;
1803             case "Courses categories and courses":
1804             default:
1805                 $this->execute("behat_general::should_exist", array("#category-listing", "css_element"));
1806                 $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
1807                 break;
1808         }
1810         $this->execute("behat_general::should_not_exist", array("#course-detail", "css_element"));
1811     }
1813     /**
1814      * Checks that we are on the course management page that we expect to be on and that a course has been selected.
1815      *
1816      * @Given /^I should see the "(?P<mode_string>(?:[^"]|\\")*)" management page with a course selected$/
1817      * @param string $mode The mode to expected. One of 'Courses', 'Course categories' or 'Course categories and courses'
1818      */
1819     public function i_should_see_the_courses_management_page_with_a_course_selected($mode) {
1820         $this->execute("behat_general::assert_element_contains_text",
1821             array("Course and category management", "h2", "css_element"));
1823         switch ($mode) {
1824             case "Courses":
1825                 $this->execute("behat_general::should_not_exist", array("#category-listing", "css_element"));
1826                 $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
1827                 break;
1829             case "Course categories":
1830                 $this->execute("behat_general::should_exist", array("#category-listing", "css_element"));
1831                 $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
1832                 break;
1834             case "Courses categories and courses":
1835             default:
1836                 $this->execute("behat_general::should_exist", array("#category-listing", "css_element"));
1837                 $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
1838                 break;
1839         }
1841         $this->execute("behat_general::should_exist", array("#course-detail", "css_element"));
1842     }
1844     /**
1845      * Locates a course in the course category management interface and then triggers an action for it.
1846      *
1847      * @Given /^I click on "(?P<action_string>(?:[^"]|\\")*)" action for "(?P<name_string>(?:[^"]|\\")*)" in management course listing$/
1848      *
1849      * @param string $action The action to take. One of
1850      * @param string $name The name of the course as it is displayed in the management interface.
1851      */
1852     public function i_click_on_action_for_item_in_management_course_listing($action, $name) {
1853         $node = $this->get_management_course_listing_node_by_name($name);
1854         $this->user_clicks_on_management_listing_action('course', $node, $action);
1855     }
1857     /**
1858      * Locates a category in the course category management interface and then triggers an action for it.
1859      *
1860      * @Given /^I click on "(?P<action_string>(?:[^"]|\\")*)" action for "(?P<name_string>(?:[^"]|\\")*)" in management category listing$/
1861      *
1862      * @param string $action The action to take. One of
1863      * @param string $name The name of the category as it is displayed in the management interface.
1864      */
1865     public function i_click_on_action_for_item_in_management_category_listing($action, $name) {
1866         $node = $this->get_management_category_listing_node_by_name($name);
1867         $this->user_clicks_on_management_listing_action('category', $node, $action);
1868     }
1870     /**
1871      * Clicks to expand or collapse a category displayed on the frontpage
1872      *
1873      * @Given /^I toggle "(?P<categoryname_string>(?:[^"]|\\")*)" category children visibility in frontpage$/
1874      * @throws ExpectationException
1875      * @param string $categoryname
1876      */
1877     public function i_toggle_category_children_visibility_in_frontpage($categoryname) {
1879         $headingtags = array();
1880         for ($i = 1; $i <= 6; $i++) {
1881             $headingtags[] = 'self::h' . $i;
1882         }
1884         $exception = new ExpectationException('"' . $categoryname . '" category can not be found', $this->getSession());
1885         $categoryliteral = behat_context_helper::escape($categoryname);
1886         $xpath = "//div[@class='info']/descendant::*[" . implode(' or ', $headingtags) . "][@class='categoryname'][./descendant::a[.=$categoryliteral]]";
1887         $node = $this->find('xpath', $xpath, $exception);
1888         $node->click();
1890         // Smooth expansion.
1891         $this->getSession()->wait(1000);
1892     }
1894     /**
1895      * Finds the node to use for a management listitem action and clicks it.
1896      *
1897      * @param string $listingtype Either course or category.
1898      * @param \Behat\Mink\Element\NodeElement $listingnode
1899      * @param string $action The action being taken
1900      * @throws Behat\Mink\Exception\ExpectationException
1901      */
1902     protected function user_clicks_on_management_listing_action($listingtype, $listingnode, $action) {
1903         $actionsnode = $listingnode->find('xpath', "//*" .
1904                 "[contains(concat(' ', normalize-space(@class), ' '), '{$listingtype}-item-actions')]");
1905         if (!$actionsnode) {
1906             throw new ExpectationException("Could not find the actions for $listingtype", $this->getSession());
1907         }
1908         $actionnode = $actionsnode->find('css', '.action-'.$action);
1909         if (!$actionnode) {
1910             throw new ExpectationException("Expected action was not available or not found ($action)", $this->getSession());
1911         }
1912         if ($this->running_javascript() && !$actionnode->isVisible()) {
1913             $actionsnode->find('css', 'a[data-toggle=dropdown]')->click();
1914             $actionnode = $actionsnode->find('css', '.action-'.$action);
1915         }
1916         $actionnode->click();
1917     }
1919     /**
1920      * Clicks on a category in the management interface.
1921      *
1922      * @Given /^I click on "(?P<categoryname_string>(?:[^"]|\\")*)" category in the management category listing$/
1923      * @param string $name The name of the category to click.
1924      */
1925     public function i_click_on_category_in_the_management_category_listing($name) {
1926         $node = $this->get_management_category_listing_node_by_name($name);
1927         $node->find('css', 'a.categoryname')->click();
1928     }
1930     /**
1931      * Go to the course participants
1932      *
1933      * @Given /^I navigate to course participants$/
1934      */
1935     public function i_navigate_to_course_participants() {
1936         $this->execute('behat_navigation::i_select_from_flat_navigation_drawer', get_string('participants'));
1937     }
1939     /**
1940      * Check that one teacher appears before another in the course contacts.
1941      *
1942      * @Given /^I should see teacher "(?P<pteacher_string>(?:[^"]|\\")*)" before "(?P<fteacher_string>(?:[^"]|\\")*)" in the course contact listing$/
1943      *
1944      * @param string $pteacher The first teacher to find
1945      * @param string $fteacher The second teacher to find (should be after the first teacher)
1946      *
1947      * @throws ExpectationException
1948      */
1949     public function i_should_see_teacher_before($pteacher, $fteacher) {
1950         $xpath = "//ul[contains(@class,'teachers')]//li//a[text()='{$pteacher}']/ancestor::li//following::a[text()='{$fteacher}']";
1951         $msg = "Teacher {$pteacher} does not appear before Teacher {$fteacher}";
1952         if (!$this->getSession()->getDriver()->find($xpath)) {
1953             throw new ExpectationException($msg, $this->getSession());
1954         }
1955     }
1957     /**
1958      * Check that one teacher oes not appears after another in the course contacts.
1959      *
1960      * @Given /^I should not see teacher "(?P<fteacher_string>(?:[^"]|\\")*)" after "(?P<pteacher_string>(?:[^"]|\\")*)" in the course contact listing$/
1961      *
1962      * @param string $fteacher The teacher that should not be found (after the other teacher)
1963      * @param string $pteacher The teacher after who the other should not be found (this teacher must be found!)
1964      *
1965      * @throws ExpectationException
1966      */
1967     public function i_should_not_see_teacher_after($fteacher, $pteacher) {
1968         $xpathliteral = behat_context_helper::escape($pteacher);
1969         $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
1970                 "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
1971         try {
1972             $nodes = $this->find_all('xpath', $xpath);
1973         } catch (ElementNotFoundException $e) {
1974             throw new ExpectationException('"' . $pteacher . '" text was not found in the page', $this->getSession());
1975         }
1976         $xpath = "//ul[contains(@class,'teachers')]//li//a[text()='{$pteacher}']/ancestor::li//following::a[text()='{$fteacher}']";
1977         $msg = "Teacher {$fteacher} appears after Teacher {$pteacher}";
1978         if ($this->getSession()->getDriver()->find($xpath)) {
1979             throw new ExpectationException($msg, $this->getSession());
1980         }
1981     }
1983     /**
1984      * Open the activity chooser in a course.
1985      *
1986      * @Given /^I open the activity chooser$/
1987      */
1988     public function i_open_the_activity_chooser() {
1989         $this->execute('behat_general::i_click_on',
1990             array('//button[@data-action="open-chooser"]', 'xpath_element'));
1992         $node = $this->get_selected_node('xpath_element', '//div[@data-region="modules"]');
1993         $this->ensure_node_is_visible($node);
1994     }