MDL-52839 behat: Fixed xpath and wait to ensure element is visible
[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\Behat\Context\Step\Given as Given,
31     Behat\Gherkin\Node\TableNode as TableNode,
32     Behat\Mink\Exception\ExpectationException as ExpectationException,
33     Behat\Mink\Exception\DriverException as DriverException,
34     Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
36 /**
37  * Course-related steps definitions.
38  *
39  * @package    core_course
40  * @category   test
41  * @copyright  2012 David MonllaĆ³
42  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43  */
44 class behat_course extends behat_base {
46     /**
47      * Turns editing mode on.
48      * @Given /^I turn editing mode on$/
49      */
50     public function i_turn_editing_mode_on() {
51         return new Given('I press "' . get_string('turneditingon') . '"');
52     }
54     /**
55      * Turns editing mode off.
56      * @Given /^I turn editing mode off$/
57      */
58     public function i_turn_editing_mode_off() {
59         return new Given('I press "' . get_string('turneditingoff') . '"');
60     }
62     /**
63      * Creates a new course with the provided table data matching course settings names with the desired values.
64      *
65      * @Given /^I create a course with:$/
66      * @param TableNode $table The course data
67      * @return Given[]
68      */
69     public function i_create_a_course_with(TableNode $table) {
71         $steps = array(
72             new Given('I go to the courses management page'),
73             new Given('I should see the "'.get_string('categories').'" management page'),
74             new Given('I click on category "'.get_string('miscellaneous').'" in the management interface'),
75             new Given('I should see the "'.get_string('categoriesandcoures').'" management page'),
76             new Given('I click on "'.get_string('createnewcourse').'" "link" in the "#course-listing" "css_element"')
77         );
79         // If the course format is one of the fields we change how we
80         // fill the form as we need to wait for the form to be set.
81         $rowshash = $table->getRowsHash();
82         $formatfieldrefs = array(get_string('format'), 'format', 'id_format');
83         foreach ($formatfieldrefs as $fieldref) {
84             if (!empty($rowshash[$fieldref])) {
85                 $formatfield = $fieldref;
86             }
87         }
89         // Setting the format separately.
90         if (!empty($formatfield)) {
92             // Removing the format field from the TableNode.
93             $rows = $table->getRows();
94             $formatvalue = $rowshash[$formatfield];
95             foreach ($rows as $key => $row) {
96                 if ($row[0] == $formatfield) {
97                     unset($rows[$key]);
98                 }
99             }
100             $table->setRows($rows);
102             // Adding a forced wait until editors are loaded as otherwise selenium sometimes tries clicks on the
103             // format field when the editor is being rendered and the click misses the field coordinates.
104             $steps[] = new Given('I expand all fieldsets');
105             $steps[] = new Given('I set the field "' . $formatfield . '" to "' . $formatvalue . '"');
106             $steps[] = new Given('I set the following fields to these values:', $table);
107         } else {
108             $steps[] = new Given('I set the following fields to these values:', $table);
109         }
111         $steps[] = new Given('I press "' . get_string('savechangesanddisplay') . '"');
113         return $steps;
114     }
116     /**
117      * Goes to the system courses/categories management page.
118      *
119      * @Given /^I go to the courses management page$/
120      * @return Given[]
121      */
122     public function i_go_to_the_courses_management_page() {
123         return array(
124             new Given('I am on homepage'),
125             new Given('I navigate to "' . get_string('coursemgmt', 'admin') . '" node in "' . get_string('administrationsite') . ' > ' . get_string('courses', 'admin') . '"')
126         );
127     }
129     /**
130      * Adds the selected activity/resource filling the form data with the specified field/value pairs. Sections 0 and 1 are also allowed on frontpage.
131      *
132      * @When /^I add a "(?P<activity_or_resource_name_string>(?:[^"]|\\")*)" to section "(?P<section_number>\d+)" and I fill the form with:$/
133      * @param string $activity The activity name
134      * @param int $section The section number
135      * @param TableNode $data The activity field/value data
136      * @return Given[]
137      */
138     public function i_add_to_section_and_i_fill_the_form_with($activity, $section, TableNode $data) {
140         return array(
141             new Given('I add a "' . $this->escape($activity) . '" to section "' . $this->escape($section) . '"'),
142             new Given('I set the following fields to these values:', $data),
143             new Given('I press "' . get_string('savechangesandreturntocourse') . '"')
144         );
145     }
147     /**
148      * Opens the activity chooser and opens the activity/resource form page. Sections 0 and 1 are also allowed on frontpage.
149      *
150      * @Given /^I add a "(?P<activity_or_resource_name_string>(?:[^"]|\\")*)" to section "(?P<section_number>\d+)"$/
151      * @throws ElementNotFoundException Thrown by behat_base::find
152      * @param string $activity
153      * @param int $section
154      */
155     public function i_add_to_section($activity, $section) {
157         if ($this->getSession()->getPage()->find('css', 'body#page-site-index') && (int)$section <= 1) {
158             // We are on the frontpage.
159             if ($section) {
160                 // Section 1 represents the contents on the frontpage.
161                 $sectionxpath = "//body[@id='page-site-index']/descendant::div[contains(concat(' ',normalize-space(@class),' '),' sitetopic ')]";
162             } else {
163                 // Section 0 represents "Site main menu" block.
164                 $sectionxpath = "//div[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]";
165             }
166         } else {
167             // We are inside the course.
168             $sectionxpath = "//li[@id='section-" . $section . "']";
169         }
171         $activityliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral(ucfirst($activity));
173         if ($this->running_javascript()) {
175             // Clicks add activity or resource section link.
176             $sectionxpath = $sectionxpath . "/descendant::div[@class='section-modchooser']/span/a";
177             $sectionnode = $this->find('xpath', $sectionxpath);
178             $sectionnode->click();
180             // Clicks the selected activity if it exists.
181             $activityxpath = "//div[@id='chooseform']/descendant::label" .
182                 "/descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' typename ')]" .
183                 "[normalize-space(.)=$activityliteral]" .
184                 "/parent::label/child::input";
185             $activitynode = $this->find('xpath', $activityxpath);
186             $activitynode->doubleClick();
188         } else {
189             // Without Javascript.
191             // Selecting the option from the select box which contains the option.
192             $selectxpath = $sectionxpath . "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' section_add_menus ')]" .
193                 "/descendant::select[option[normalize-space(.)=$activityliteral]]";
194             $selectnode = $this->find('xpath', $selectxpath);
195             $selectnode->selectOption($activity);
197             // Go button.
198             $gobuttonxpath = $selectxpath . "/ancestor::form/descendant::input[@type='submit']";
199             $gobutton = $this->find('xpath', $gobuttonxpath);
200             $gobutton->click();
201         }
203     }
206     /**
207      * Opens a section edit menu if it is not already opened.
208      *
209      * @Given /^I open section "(?P<section_number>\d+)" edit menu$/
210      * @throws DriverException The step is not available when Javascript is disabled
211      * @param string $sectionnumber
212      */
213     public function i_open_section_edit_menu($sectionnumber) {
214         if (!$this->running_javascript()) {
215             throw new DriverException('Section edit menu not available when Javascript is disabled');
216         }
218         // Wait for section to be available, before clicking on the menu.
219         $this->i_wait_until_section_is_available($sectionnumber);
221         // If it is already opened we do nothing.
222         $xpath = $this->section_exists($sectionnumber);
223         $xpath .= "/descendant::div[contains(@class, 'section-actions')]/descendant::a[contains(@class, 'textmenu')]";
225         $exception = new ExpectationException('Section "' . $sectionnumber . '" was not found', $this->getSession());
226         $menu = $this->find('xpath', $xpath, $exception);
227         $menu->click();
228         $this->i_wait_until_section_is_available($sectionnumber);
229     }
231     /**
232      * Deletes course section.
233      *
234      * @Given /^I delete section "(?P<section_number>\d+)"$/
235      * @param int $sectionnumber The section number
236      * @return Given[]
237      */
238     public function i_delete_section($sectionnumber) {
239         // Ensures the section exists.
240         $xpath = $this->section_exists($sectionnumber);
242         // We need to know the course format as the text strings depends on them.
243         $courseformat = $this->get_course_format();
244         if (get_string_manager()->string_exists('deletesection', $courseformat)) {
245             $strdelete = get_string('deletesection', $courseformat);
246         } else {
247             $strdelete = get_string('deletesection');
248         }
250         // If javascript is on, link is inside a menu.
251         if ($this->running_javascript()) {
252             $this->i_open_section_edit_menu($sectionnumber);
253         }
255         return new Given('I click on "' . $strdelete . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"');
256     }
258     /**
259      * Turns course section highlighting on.
260      *
261      * @Given /^I turn section "(?P<section_number>\d+)" highlighting on$/
262      * @param int $sectionnumber The section number
263      * @return Given[]
264      */
265     public function i_turn_section_highlighting_on($sectionnumber) {
267         // Ensures the section exists.
268         $xpath = $this->section_exists($sectionnumber);
270         // If javascript is on, link is inside a menu.
271         if ($this->running_javascript()) {
272             $this->i_open_section_edit_menu($sectionnumber);
273         }
275         return new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"');
276     }
278     /**
279      * Turns course section highlighting off.
280      *
281      * @Given /^I turn section "(?P<section_number>\d+)" highlighting off$/
282      * @param int $sectionnumber The section number
283      * @return Given[]
284      */
285     public function i_turn_section_highlighting_off($sectionnumber) {
287         // Ensures the section exists.
288         $xpath = $this->section_exists($sectionnumber);
290         // If javascript is on, link is inside a menu.
291         if ($this->running_javascript()) {
292             $this->i_open_section_edit_menu($sectionnumber);
293         }
295         return new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"');
296     }
298     /**
299      * Shows the specified hidden section. You need to be in the course page and on editing mode.
300      *
301      * @Given /^I show section "(?P<section_number>\d+)"$/
302      * @param int $sectionnumber
303      */
304     public function i_show_section($sectionnumber) {
305         $showlink = $this->show_section_icon_exists($sectionnumber);
307         // Ensure section edit menu is open before interacting with it.
308         if ($this->running_javascript()) {
309             $this->i_open_section_edit_menu($sectionnumber);
310         }
311         $showlink->click();
313         if ($this->running_javascript()) {
314             $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
315             $this->i_wait_until_section_is_available($sectionnumber);
316         }
317     }
319     /**
320      * Hides the specified visible section. You need to be in the course page and on editing mode.
321      *
322      * @Given /^I hide section "(?P<section_number>\d+)"$/
323      * @param int $sectionnumber
324      */
325     public function i_hide_section($sectionnumber) {
326         $hidelink = $this->hide_section_icon_exists($sectionnumber);
328         // Ensure section edit menu is open before interacting with it.
329         if ($this->running_javascript()) {
330             $this->i_open_section_edit_menu($sectionnumber);
331         }
332         $hidelink->click();
334         if ($this->running_javascript()) {
335             $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
336             $this->i_wait_until_section_is_available($sectionnumber);
337         }
338     }
340     /**
341      * Go to editing section page for specified section number. You need to be in the course page and on editing mode.
342      *
343      * @Given /^I edit the section "(?P<section_number>\d+)"$/
344      * @param int $sectionnumber
345      */
346     public function i_edit_the_section($sectionnumber) {
347         // If javascript is on, link is inside a menu.
348         if ($this->running_javascript()) {
349             $this->i_open_section_edit_menu($sectionnumber);
350         }
352         // We need to know the course format as the text strings depends on them.
353         $courseformat = $this->get_course_format();
354         if (get_string_manager()->string_exists('editsection', $courseformat)) {
355             $stredit = get_string('editsection', $courseformat);
356         } else {
357             $stredit = get_string('editsection');
358         }
360         return new Given('I click on "' . $stredit . '" "link" in the "#section-' . $sectionnumber . '" "css_element"');
361     }
363     /**
364      * Edit specified section and fill the form data with the specified field/value pairs.
365      *
366      * @When /^I edit the section "(?P<section_number>\d+)" and I fill the form with:$/
367      * @param int $sectionnumber The section number
368      * @param TableNode $data The activity field/value data
369      * @return Given[]
370      */
371     public function i_edit_the_section_and_i_fill_the_form_with($sectionnumber, TableNode $data) {
373         return array(
374             new Given('I edit the section "' . $sectionnumber . '"'),
375             new Given('I set the following fields to these values:', $data),
376             new Given('I press "' . get_string('savechanges') . '"')
377         );
378     }
380     /**
381      * Checks if the specified course section hightlighting is turned on. You need to be in the course page on editing mode.
382      *
383      * @Then /^section "(?P<section_number>\d+)" should be highlighted$/
384      * @throws ExpectationException
385      * @param int $sectionnumber The section number
386      */
387     public function section_should_be_highlighted($sectionnumber) {
389         // Ensures the section exists.
390         $xpath = $this->section_exists($sectionnumber);
392         // The important checking, we can not check the img.
393         $xpath = $xpath . "/descendant::img[contains(@src, 'marked')]";
394         $exception = new ExpectationException('The "' . $sectionnumber . '" section is not highlighted', $this->getSession());
395         $this->find('xpath', $xpath, $exception);
396     }
398     /**
399      * Checks if the specified course section highlighting is turned off. You need to be in the course page on editing mode.
400      *
401      * @Then /^section "(?P<section_number>\d+)" should not be highlighted$/
402      * @throws ExpectationException
403      * @param int $sectionnumber The section number
404      */
405     public function section_should_not_be_highlighted($sectionnumber) {
407         // We only catch ExpectationException, ElementNotFoundException should be thrown if the specified section does not exist.
408         try {
409             $this->section_should_be_highlighted($sectionnumber);
410         } catch (ExpectationException $e) {
411             // ExpectedException means that it is not highlighted.
412             return;
413         }
415         throw new ExpectationException('The "' . $sectionnumber . '" section is highlighted', $this->getSession());
416     }
418     /**
419      * 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.
420      *
421      * @Then /^section "(?P<section_number>\d+)" should be hidden$/
422      * @throws ExpectationException
423      * @throws ElementNotFoundException Thrown by behat_base::find
424      * @param int $sectionnumber
425      */
426     public function section_should_be_hidden($sectionnumber) {
428         $sectionxpath = $this->section_exists($sectionnumber);
430         // Preventive in case there is any action in progress.
431         // Adding it here because we are interacting (click) with
432         // the elements, not necessary when we just find().
433         $this->i_wait_until_section_is_available($sectionnumber);
435         // Section should be hidden.
436         $exception = new ExpectationException('The section is not hidden', $this->getSession());
437         $this->find('xpath', $sectionxpath . "[contains(concat(' ', normalize-space(@class), ' '), ' hidden ')]", $exception);
438     }
440     /**
441      * 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.
442      *
443      * @Then /^all activities in section "(?P<section_number>\d+)" should be hidden$/
444      * @throws ExpectationException
445      * @throws ElementNotFoundException Thrown by behat_base::find
446      * @param int $sectionnumber
447      */
448     public function section_activities_should_be_hidden($sectionnumber) {
449         $sectionxpath = $this->section_exists($sectionnumber);
451         // Preventive in case there is any action in progress.
452         // Adding it here because we are interacting (click) with
453         // the elements, not necessary when we just find().
454         $this->i_wait_until_section_is_available($sectionnumber);
456         // The checking are different depending on user permissions.
457         if ($this->is_course_editor()) {
459             // The section must be hidden.
460             $this->show_section_icon_exists($sectionnumber);
462             // If there are activities they should be hidden and the visibility icon should not be available.
463             if ($activities = $this->get_section_activities($sectionxpath)) {
465                 $dimmedexception = new ExpectationException('There are activities that are not dimmed', $this->getSession());
466                 foreach ($activities as $activity) {
467                     // Dimmed.
468                     $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' activityinstance ')]" .
469                         "/a[contains(concat(' ', normalize-space(@class), ' '), ' dimmed ')]", $dimmedexception, $activity);
470                 }
471             }
472         } else {
473             // There shouldn't be activities.
474             if ($this->get_section_activities($sectionxpath)) {
475                 throw new ExpectationException('There are activities in the section and they should be hidden', $this->getSession());
476             }
477         }
479     }
481     /**
482      * 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.
483      *
484      * @Then /^section "(?P<section_number>\d+)" should be visible$/
485      * @throws ExpectationException
486      * @param int $sectionnumber
487      */
488     public function section_should_be_visible($sectionnumber) {
490         $sectionxpath = $this->section_exists($sectionnumber);
492         // Section should not be hidden.
493         $xpath = $sectionxpath . "[not(contains(concat(' ', normalize-space(@class), ' '), ' hidden '))]";
494         if (!$this->getSession()->getPage()->find('xpath', $xpath)) {
495             throw new ExpectationException('The section is hidden', $this->getSession());
496         }
498         // Edit menu should be visible.
499         if ($this->is_course_editor()) {
500             $xpath = $sectionxpath .
501                      "/descendant::div[contains(@class, 'section-actions')]" .
502                      "/descendant::a[contains(@class, 'textmenu')]";
503             if (!$this->getSession()->getPage()->find('xpath', $xpath)) {
504                 throw new ExpectationException('The section edit menu is not available', $this->getSession());
505             }
506         }
507     }
509     /**
510      * Moves up the specified section, this step only works with Javascript disabled. Editing mode should be on.
511      *
512      * @Given /^I move up section "(?P<section_number>\d+)"$/
513      * @throws DriverException Step not available when Javascript is enabled
514      * @param int $sectionnumber
515      */
516     public function i_move_up_section($sectionnumber) {
518         if ($this->running_javascript()) {
519             throw new DriverException('Move a section up step is not available with Javascript enabled');
520         }
522         // Ensures the section exists.
523         $sectionxpath = $this->section_exists($sectionnumber);
525         // If javascript is on, link is inside a menu.
526         if ($this->running_javascript()) {
527             $this->i_open_section_edit_menu($sectionnumber);
528         }
530         // Follows the link
531         $moveuplink = $this->get_node_in_container('link', get_string('moveup'), 'xpath_element', $sectionxpath);
532         $moveuplink->click();
533     }
535     /**
536      * Moves down the specified section, this step only works with Javascript disabled. Editing mode should be on.
537      *
538      * @Given /^I move down section "(?P<section_number>\d+)"$/
539      * @throws DriverException Step not available when Javascript is enabled
540      * @param int $sectionnumber
541      */
542     public function i_move_down_section($sectionnumber) {
544         if ($this->running_javascript()) {
545             throw new DriverException('Move a section down step is not available with Javascript enabled');
546         }
548         // Ensures the section exists.
549         $sectionxpath = $this->section_exists($sectionnumber);
551         // If javascript is on, link is inside a menu.
552         if ($this->running_javascript()) {
553             $this->i_open_section_edit_menu($sectionnumber);
554         }
556         // Follows the link
557         $movedownlink = $this->get_node_in_container('link', get_string('movedown'), 'xpath_element', $sectionxpath);
558         $movedownlink->click();
559     }
561     /**
562      * 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.
563      *
564      * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" activity should be visible$/
565      * @param string $activityname
566      * @throws ExpectationException
567      */
568     public function activity_should_be_visible($activityname) {
570         // The activity must exists and be visible.
571         $activitynode = $this->get_activity_node($activityname);
573         if ($this->is_course_editor()) {
575             // The activity should not be dimmed.
576             try {
577                 $xpath = "/descendant-or-self::a[contains(concat(' ', normalize-space(@class), ' '), ' dimmed ')] | ".
578                          "/descendant-or-self::div[contains(concat(' ', normalize-space(@class), ' '), ' dimmed_text ')]";
579                 $this->find('xpath', $xpath, false, $activitynode);
580                 throw new ExpectationException('"' . $activityname . '" is hidden', $this->getSession());
581             } catch (ElementNotFoundException $e) {
582                 // All ok.
583             }
585             // The 'Hide' button should be available.
586             $nohideexception = new ExpectationException('"' . $activityname . '" don\'t have a "' . get_string('hide') . '" icon', $this->getSession());
587             $this->find('named', array('link', get_string('hide')), $nohideexception, $activitynode);
588         }
589     }
591     /**
592      * 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.
593      *
594      * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" activity should be hidden$/
595      * @param string $activityname
596      * @throws ExpectationException
597      */
598     public function activity_should_be_hidden($activityname) {
600         if ($this->is_course_editor()) {
602             // The activity should exist.
603             $activitynode = $this->get_activity_node($activityname);
605             // Should be hidden.
606             $exception = new ExpectationException('"' . $activityname . '" is not dimmed', $this->getSession());
607             $xpath = "/descendant-or-self::a[contains(concat(' ', normalize-space(@class), ' '), ' dimmed ')] | ".
608                      "/descendant-or-self::div[contains(concat(' ', normalize-space(@class), ' '), ' dimmed_text ')]";
609             $this->find('xpath', $xpath, $exception, $activitynode);
611             // Also 'Show' icon.
612             $noshowexception = new ExpectationException('"' . $activityname . '" don\'t have a "' . get_string('show') . '" icon', $this->getSession());
613             $this->find('named', array('link', get_string('show')), $noshowexception, $activitynode);
615         } else {
617             // It should not exist at all.
618             try {
619                 $this->find_link($activityname);
620                 throw new ExpectationException('The "' . $activityname . '" should not appear');
621             } catch (ElementNotFoundException $e) {
622                 // This is good, the activity should not be there.
623             }
624         }
626     }
628     /**
629      * 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.
630      *
631      * @Given /^I move "(?P<activity_name_string>(?:[^"]|\\")*)" activity to section "(?P<section_number>\d+)"$/
632      * @param string $activityname The activity name
633      * @param int $sectionnumber The number of section
634      * @return Given[]
635      */
636     public function i_move_activity_to_section($activityname, $sectionnumber) {
638         // Ensure the destination is valid.
639         $sectionxpath = $this->section_exists($sectionnumber);
641         $activitynode = $this->get_activity_element('.editing_move img', 'css_element', $activityname);
643         // JS enabled.
644         if ($this->running_javascript()) {
646             $destinationxpath = $sectionxpath . "/descendant::ul[contains(concat(' ', normalize-space(@class), ' '), ' yui3-dd-drop ')]";
648             return array(
649                 new Given('I drag "' . $this->escape($activitynode->getXpath()) . '" "xpath_element" ' .
650                     'and I drop it in "' . $this->escape($destinationxpath) . '" "xpath_element"'),
651             );
653         } else {
654             // Following links with no-JS.
656             // Moving to the fist spot of the section (before all other section's activities).
657             return array(
658                 new Given('I click on "a.editing_move" "css_element" in the "' . $this->escape($activityname) . '" activity'),
659                 new Given('I click on "li.movehere a" "css_element" in the "' . $this->escape($sectionxpath) . '" "xpath_element"'),
660             );
661         }
662     }
664     /**
665      * Edits the activity name through the edit activity; this step only works with Javascript enabled. Editing mode should be on.
666      *
667      * @Given /^I change "(?P<activity_name_string>(?:[^"]|\\")*)" activity name to "(?P<new_name_string>(?:[^"]|\\")*)"$/
668      * @throws DriverException Step not available when Javascript is disabled
669      * @param string $activityname
670      * @param string $newactivityname
671      * @return Given[]
672      */
673     public function i_change_activity_name_to($activityname, $newactivityname) {
675         if (!$this->running_javascript()) {
676             throw new DriverException('Change activity name step is not available with Javascript disabled');
677         }
679         // Adding chr(10) to save changes.
680         $activity = $this->escape($activityname);
681         return array(
682             new Given('I click on "' . get_string('edittitle') . '" "link" in the "' . $activity .'" activity'),
683             new Given('I set the field "title" to "' . $this->escape($newactivityname) . chr(10) . '"')
684         );
685     }
687     /**
688      * Opens an activity actions menu if it is not already opened.
689      *
690      * @Given /^I open "(?P<activity_name_string>(?:[^"]|\\")*)" actions menu$/
691      * @throws DriverException The step is not available when Javascript is disabled
692      * @param string $activityname
693      * @return Given
694      */
695     public function i_open_actions_menu($activityname) {
697         if (!$this->running_javascript()) {
698             throw new DriverException('Activities actions menu not available when Javascript is disabled');
699         }
701         // If it is already opened we do nothing.
702         $activitynode = $this->get_activity_node($activityname);
703         $classes = array_flip(explode(' ', $activitynode->getAttribute('class')));
704         if (!empty($classes['action-menu-shown'])) {
705             return;
706         }
708         return new Given('I click on "a[role=\'menuitem\']" "css_element" in the "' . $this->escape($activityname) . '" activity');
709     }
711     /**
712      * Closes an activity actions menu if it is not already closed.
713      *
714      * @Given /^I close "(?P<activity_name_string>(?:[^"]|\\")*)" actions menu$/
715      * @throws DriverException The step is not available when Javascript is disabled
716      * @param string $activityname
717      * @return Given
718      */
719     public function i_close_actions_menu($activityname) {
721         if (!$this->running_javascript()) {
722             throw new DriverException('Activities actions menu not available when Javascript is disabled');
723         }
725         // If it is already closed we do nothing.
726         $activitynode = $this->get_activity_node($activityname);
727         $classes = array_flip(explode(' ', $activitynode->getAttribute('class')));
728         if (empty($classes['action-menu-shown'])) {
729             return;
730         }
732         return new Given('I click on "a[role=\'menuitem\']" "css_element" in the "' . $this->escape($activityname) . '" activity');
733     }
735     /**
736      * Checks that the specified activity's action menu is open.
737      *
738      * @Then /^"(?P<activity_name_string>(?:[^"]|\\")*)" actions menu should be open$/
739      * @throws DriverException The step is not available when Javascript is disabled
740      * @param string $activityname
741      */
742     public function actions_menu_should_be_open($activityname) {
744         if (!$this->running_javascript()) {
745             throw new DriverException('Activities actions menu not available when Javascript is disabled');
746         }
748         // If it is already closed we do nothing.
749         $activitynode = $this->get_activity_node($activityname);
750         $classes = array_flip(explode(' ', $activitynode->getAttribute('class')));
751         if (empty($classes['action-menu-shown'])) {
752             throw new ExpectationException(sprintf("The action menu for '%s' is not open", $activityname), $this->getSession());
753         }
755         return;
756     }
758     /**
759      * Indents to the right the activity or resource specified by it's name. Editing mode should be on.
760      *
761      * @Given /^I indent right "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
762      * @param string $activityname
763      * @return Given[]
764      */
765     public function i_indent_right_activity($activityname) {
767         $steps = array();
768         $activity = $this->escape($activityname);
769         if ($this->running_javascript()) {
770             $steps[] = new Given('I open "' . $activity . '" actions menu');
771         }
772         $steps[] = new Given('I click on "' . get_string('moveright') . '" "link" in the "' . $activity . '" activity');
774         return $steps;
775     }
777     /**
778      * Indents to the left the activity or resource specified by it's name. Editing mode should be on.
779      *
780      * @Given /^I indent left "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
781      * @param string $activityname
782      * @return Given[]
783      */
784     public function i_indent_left_activity($activityname) {
786         $steps = array();
787         $activity = $this->escape($activityname);
788         if ($this->running_javascript()) {
789             $steps[] = new Given('I open "' . $activity . '" actions menu');
790         }
791         $steps[] = new Given('I click on "' . get_string('moveleft') . '" "link" in the "' . $activity . '" activity');
793         return $steps;
795     }
797     /**
798      * 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.
799      *
800      * @Given /^I delete "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
801      * @param string $activityname
802      * @return Given[]
803      */
804     public function i_delete_activity($activityname) {
805         $steps = array();
806         $activity = $this->escape($activityname);
807         if ($this->running_javascript()) {
808             $steps[] = new Given('I open "' . $activity . '" actions menu');
809         }
810         $steps[] = new Given('I click on "' . get_string('delete') . '" "link" in the "' . $activity . '" activity');
812         // JS enabled.
813         // Not using chain steps here because the exceptions catcher have problems detecting
814         // JS modal windows and avoiding interacting them at the same time.
815         if ($this->running_javascript()) {
816             $steps[] = new Given('I click on "' . get_string('yes') . '" "button" in the "Confirm" "dialogue"');
817         } else {
818             $steps[] = new Given('I press "' . get_string('yes') . '"');
819         }
821         return $steps;
822     }
824     /**
825      * Duplicates the activity or resource specified by it's name. You should be in the course page with editing mode on.
826      *
827      * @Given /^I duplicate "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
828      * @param string $activityname
829      * @return Given[]
830      */
831     public function i_duplicate_activity($activityname) {
832         $steps = array();
833         $activity = $this->escape($activityname);
834         if ($this->running_javascript()) {
835             $steps[] = new Given('I open "' . $activity . '" actions menu');
836         }
837         $steps[] = new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activity . '" activity');
838         return $steps;
839     }
841     /**
842      * 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.
843      *
844      * @Given /^I duplicate "(?P<activity_name_string>(?:[^"]|\\")*)" activity editing the new copy with:$/
845      * @param string $activityname
846      * @param TableNode $data
847      * @return Given[]
848      */
849     public function i_duplicate_activity_editing_the_new_copy_with($activityname, TableNode $data) {
851         $steps = array();
853         $activity = $this->escape($activityname);
854         $activityliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($activityname);
856         $steps[] = new Given('I duplicate "' . $activity . '" activity');
858         // Determine the future new activity xpath from the former one.
859         $duplicatedxpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]" .
860             "[contains(., $activityliteral)]/following-sibling::li";
861         $duplicatedactionsmenuxpath = $duplicatedxpath . "/descendant::a[@role='menuitem']";
863         if ($this->running_javascript()) {
864             // We wait until the AJAX request finishes and the section is visible again.
865             $hiddenlightboxxpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]" .
866                 "[contains(., $activityliteral)]" .
867                 "/ancestor::li[contains(concat(' ', normalize-space(@class), ' '), ' section ')]" .
868                 "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]";
870             $steps[] = new Given('I wait until the page is ready');
871             $steps[] = new Given('I wait until "' . $this->escape($hiddenlightboxxpath) .'" "xpath_element" exists');
873             // Close the original activity actions menu.
874             $steps[] = new Given('I close "' . $activity . '" actions menu');
876             // The next sibling of the former activity will be the duplicated one, so we click on it from it's xpath as, at
877             // this point, it don't even exists in the DOM (the steps are executed when we return them).
878             $steps[] = new Given('I click on "' . $this->escape($duplicatedactionsmenuxpath) . '" "xpath_element"');
879         }
881         // We force the xpath as otherwise mink tries to interact with the former one.
882         $steps[] = new Given('I click on "' . get_string('editsettings') . '" "link" in the "' .
883             $this->escape($duplicatedxpath) . '" "xpath_element"');
885         $steps[] = new Given('I set the following fields to these values:', $data);
886         $steps[] = new Given('I press "' . get_string('savechangesandreturntocourse') . '"');
887         return $steps;
888     }
890     /**
891      * 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.
892      *
893      * Using the protected method as this method will be usually
894      * called by other methods which are not returning a set of
895      * steps and performs the actions directly, so it would not
896      * be executed if it returns another step.
897      *
898      * Hopefully we would not require test writers to use this step
899      * and we will manage it from other step definitions.
900      *
901      * @Given /^I wait until section "(?P<section_number>\d+)" is available$/
902      * @param int $sectionnumber
903      * @return void
904      */
905     public function i_wait_until_section_is_available($sectionnumber) {
907         // Looks for a hidden lightbox or a non-existent lightbox in that section.
908         $sectionxpath = $this->section_exists($sectionnumber);
909         $hiddenlightboxxpath = $sectionxpath . "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]" .
910             " | " .
911             $sectionxpath . "[count(child::div[contains(@class, 'lightbox')]) = 0]";
913         $this->ensure_element_exists($hiddenlightboxxpath, 'xpath_element');
914     }
916     /**
917      * Clicks on the specified element of the activity. You should be in the course page with editing mode turned on.
918      *
919      * @Given /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" in the "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
920      * @param string $element
921      * @param string $selectortype
922      * @param string $activityname
923      */
924     public function i_click_on_in_the_activity($element, $selectortype, $activityname) {
925         $element = $this->get_activity_element($element, $selectortype, $activityname);
926         $element->click();
927     }
929     /**
930      * Clicks on the specified element inside the activity container.
931      *
932      * @throws ElementNotFoundException
933      * @param string $element
934      * @param string $selectortype
935      * @param string $activityname
936      * @return NodeElement
937      */
938     protected function get_activity_element($element, $selectortype, $activityname) {
939         $activitynode = $this->get_activity_node($activityname);
941         // Transforming to Behat selector/locator.
942         list($selector, $locator) = $this->transform_selector($selectortype, $element);
943         $exception = new ElementNotFoundException($this->getSession(), '"' . $element . '" "' . $selectortype . '" in "' . $activityname . '" ');
945         return $this->find($selector, $locator, $exception, $activitynode);
946     }
948     /**
949      * Checks if the course section exists.
950      *
951      * @throws ElementNotFoundException Thrown by behat_base::find
952      * @param int $sectionnumber
953      * @return string The xpath of the section.
954      */
955     protected function section_exists($sectionnumber) {
957         // Just to give more info in case it does not exist.
958         $xpath = "//li[@id='section-" . $sectionnumber . "']";
959         $exception = new ElementNotFoundException($this->getSession(), "Section $sectionnumber ");
960         $this->find('xpath', $xpath, $exception);
962         return $xpath;
963     }
965     /**
966      * Returns the show section icon or throws an exception.
967      *
968      * @throws ElementNotFoundException Thrown by behat_base::find
969      * @param int $sectionnumber
970      * @return NodeElement
971      */
972     protected function show_section_icon_exists($sectionnumber) {
974         // Gets the section xpath and ensure it exists.
975         $xpath = $this->section_exists($sectionnumber);
977         // We need to know the course format as the text strings depends on them.
978         $courseformat = $this->get_course_format();
980         // Checking the show button alt text and show icon.
981         $showtext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('showfromothers', $courseformat));
982         $linkxpath = $xpath . "/descendant::a[@title=$showtext]";
983         $imgxpath = $linkxpath . "/descendant::img[contains(@src, 'show')]";
985         $exception = new ElementNotFoundException($this->getSession(), 'Show section icon ');
986         $this->find('xpath', $imgxpath, $exception);
988         // Returing the link so both Non-JS and JS browsers can interact with it.
989         return $this->find('xpath', $linkxpath, $exception);
990     }
992     /**
993      * Returns the hide section icon link if it exists or throws exception.
994      *
995      * @throws ElementNotFoundException Thrown by behat_base::find
996      * @param int $sectionnumber
997      * @return NodeElement
998      */
999     protected function hide_section_icon_exists($sectionnumber) {
1001         // Gets the section xpath and ensure it exists.
1002         $xpath = $this->section_exists($sectionnumber);
1004         // We need to know the course format as the text strings depends on them.
1005         $courseformat = $this->get_course_format();
1007         // Checking the hide button alt text and hide icon.
1008         $hidetext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('hidefromothers', $courseformat));
1009         $linkxpath = $xpath . "/descendant::a[@title=$hidetext]";
1010         $imgxpath = $linkxpath . "/descendant::img[contains(@src, 'hide')]";
1012         $exception = new ElementNotFoundException($this->getSession(), 'Hide section icon ');
1013         $this->find('xpath', $imgxpath, $exception);
1015         // Returing the link so both Non-JS and JS browsers can interact with it.
1016         return $this->find('xpath', $linkxpath, $exception);
1017     }
1019     /**
1020      * Gets the current course format.
1021      *
1022      * @throws ExpectationException If we are not in the course view page.
1023      * @return string The course format in a frankenstyled name.
1024      */
1025     protected function get_course_format() {
1027         $exception = new ExpectationException('You are not in a course page', $this->getSession());
1029         // The moodle body's id attribute contains the course format.
1030         $node = $this->getSession()->getPage()->find('css', 'body');
1031         if (!$node) {
1032             throw $exception;
1033         }
1035         if (!$bodyid = $node->getAttribute('id')) {
1036             throw $exception;
1037         }
1039         if (strstr($bodyid, 'page-course-view-') === false) {
1040             throw $exception;
1041         }
1043         return 'format_' . str_replace('page-course-view-', '', $bodyid);
1044     }
1046     /**
1047      * Gets the section's activites DOM nodes.
1048      *
1049      * @param string $sectionxpath
1050      * @return array NodeElement instances
1051      */
1052     protected function get_section_activities($sectionxpath) {
1054         $xpath = $sectionxpath . "/descendant::li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]";
1056         // We spin here, as activities usually require a lot of time to load.
1057         try {
1058             $activities = $this->find_all('xpath', $xpath);
1059         } catch (ElementNotFoundException $e) {
1060             return false;
1061         }
1063         return $activities;
1064     }
1066     /**
1067      * Returns the DOM node of the activity from <li>.
1068      *
1069      * @throws ElementNotFoundException Thrown by behat_base::find
1070      * @param string $activityname The activity name
1071      * @return NodeElement
1072      */
1073     protected function get_activity_node($activityname) {
1075         $activityname = $this->getSession()->getSelectorsHandler()->xpathLiteral($activityname);
1076         $xpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][contains(., $activityname)]";
1078         return $this->find('xpath', $xpath);
1079     }
1081     /**
1082      * Gets the activity instance name from the activity node.
1083      *
1084      * @throws ElementNotFoundException
1085      * @param NodeElement $activitynode
1086      * @return string
1087      */
1088     protected function get_activity_name($activitynode) {
1089         $instancenamenode = $this->find('xpath', "//span[contains(concat(' ', normalize-space(@class), ' '), ' instancename ')]", false, $activitynode);
1090         return $instancenamenode->getText();
1091     }
1093     /**
1094      * Returns whether the user can edit the course contents or not.
1095      *
1096      * @return bool
1097      */
1098     protected function is_course_editor() {
1100         // We don't need to behat_base::spin() here as all is already loaded.
1101         if (!$this->getSession()->getPage()->findButton(get_string('turneditingoff')) &&
1102                 !$this->getSession()->getPage()->findButton(get_string('turneditingon'))) {
1103             return false;
1104         }
1106         return true;
1107     }
1109     /**
1110      * Returns the id of the category with the given idnumber.
1111      *
1112      * Please note that this function requires the category to exist. If it does not exist an ExpectationException is thrown.
1113      *
1114      * @param string $idnumber
1115      * @return string
1116      * @throws ExpectationException
1117      */
1118     protected function get_category_id($idnumber) {
1119         global $DB;
1120         try {
1121             return $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber), MUST_EXIST);
1122         } catch (dml_missing_record_exception $ex) {
1123             throw new ExpectationException(sprintf("There is no category in the database with the idnumber '%s'", $idnumber));
1124         }
1125     }
1127     /**
1128      * Returns the id of the course with the given idnumber.
1129      *
1130      * Please note that this function requires the category to exist. If it does not exist an ExpectationException is thrown.
1131      *
1132      * @param string $idnumber
1133      * @return string
1134      * @throws ExpectationException
1135      */
1136     protected function get_course_id($idnumber) {
1137         global $DB;
1138         try {
1139             return $DB->get_field('course', 'id', array('idnumber' => $idnumber), MUST_EXIST);
1140         } catch (dml_missing_record_exception $ex) {
1141             throw new ExpectationException(sprintf("There is no course in the database with the idnumber '%s'", $idnumber));
1142         }
1143     }
1145     /**
1146      * Returns the category node from within the listing on the management page.
1147      *
1148      * @param string $idnumber
1149      * @return \Behat\Mink\Element\NodeElement
1150      */
1151     protected function get_management_category_listing_node_by_idnumber($idnumber) {
1152         $id = $this->get_category_id($idnumber);
1153         $selector = sprintf('#category-listing .listitem-category[data-id="%d"] > div', $id);
1154         return $this->find('css', $selector);
1155     }
1157     /**
1158      * Returns a category node from within the management interface.
1159      *
1160      * @param string $name The name of the category.
1161      * @param bool $link If set to true we'll resolve to the link rather than just the node.
1162      * @return \Behat\Mink\Element\NodeElement
1163      */
1164     protected function get_management_category_listing_node_by_name($name, $link = false) {
1165         $selector = "//div[@id='category-listing']//li[contains(concat(' ', normalize-space(@class), ' '), ' listitem-category ')]//a[text()='{$name}']";
1166         if ($link === false) {
1167             $selector .= "/ancestor::li[@data-id][1]";
1168         }
1169         return $this->find('xpath', $selector);
1170     }
1172     /**
1173      * Returns a course node from within the management interface.
1174      *
1175      * @param string $name The name of the course.
1176      * @param bool $link If set to true we'll resolve to the link rather than just the node.
1177      * @return \Behat\Mink\Element\NodeElement
1178      */
1179     protected function get_management_course_listing_node_by_name($name, $link = false) {
1180         $selector = "//div[@id='course-listing']//li[contains(concat(' ', @class, ' '), ' listitem-course ')]//a[text()='{$name}']";
1181         if ($link === false) {
1182             $selector .= "/ancestor::li[@data-id]";
1183         }
1184         return $this->find('xpath', $selector);
1185     }
1187     /**
1188      * Returns the course node from within the listing on the management page.
1189      *
1190      * @param string $idnumber
1191      * @return \Behat\Mink\Element\NodeElement
1192      */
1193     protected function get_management_course_listing_node_by_idnumber($idnumber) {
1194         $id = $this->get_course_id($idnumber);
1195         $selector = sprintf('#course-listing .listitem-course[data-id="%d"] > div', $id);
1196         return $this->find('css', $selector);
1197     }
1199     /**
1200      * Clicks on a category in the management interface.
1201      *
1202      * @Given /^I click on category "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1203      * @param string $name
1204      */
1205     public function i_click_on_category_in_the_management_interface($name) {
1206         $node = $this->get_management_category_listing_node_by_name($name, true);
1207         $node->click();
1208     }
1210     /**
1211      * Clicks on a course in the management interface.
1212      *
1213      * @Given /^I click on course "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1214      * @param string $name
1215      */
1216     public function i_click_on_course_in_the_management_interface($name) {
1217         $node = $this->get_management_course_listing_node_by_name($name, true);
1218         $node->click();
1219     }
1221     /**
1222      * Clicks on a category checkbox in the management interface, if not checked.
1223      *
1224      * @Given /^I select category "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1225      * @param string $name
1226      */
1227     public function i_select_category_in_the_management_interface($name) {
1228         $node = $this->get_management_category_listing_node_by_name($name);
1229         $node = $node->findField('bcat[]');
1230         if (!$node->isChecked()) {
1231             $node->click();
1232         }
1233     }
1235     /**
1236      * Clicks on a category checkbox in the management interface, if checked.
1237      *
1238      * @Given /^I unselect category "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1239      * @param string $name
1240      */
1241     public function i_unselect_category_in_the_management_interface($name) {
1242         $node = $this->get_management_category_listing_node_by_name($name);
1243         $node = $node->findField('bcat[]');
1244         if ($node->isChecked()) {
1245             $node->click();
1246         }
1247     }
1249     /**
1250      * Clicks course checkbox in the management interface, if not checked.
1251      *
1252      * @Given /^I select course "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1253      * @param string $name
1254      */
1255     public function i_select_course_in_the_management_interface($name) {
1256         $node = $this->get_management_course_listing_node_by_name($name);
1257         $node = $node->findField('bc[]');
1258         if (!$node->isChecked()) {
1259             $node->click();
1260         }
1261     }
1263     /**
1264      * Clicks course checkbox in the management interface, if checked.
1265      *
1266      * @Given /^I unselect course "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1267      * @param string $name
1268      */
1269     public function i_unselect_course_in_the_management_interface($name) {
1270         $node = $this->get_management_course_listing_node_by_name($name);
1271         $node = $node->findField('bc[]');
1272         if ($node->isChecked()) {
1273             $node->click();
1274         }
1275     }
1277     /**
1278      * Move selected categories to top level in the management interface.
1279      *
1280      * @Given /^I move category "(?P<name_string>(?:[^"]|\\")*)" to top level in the management interface$/
1281      * @param string $name
1282      * @return Given[]
1283      */
1284     public function i_move_category_to_top_level_in_the_management_interface($name) {
1285         $this->i_select_category_in_the_management_interface($name);
1286         return array(
1287             new Given('I set the field "menumovecategoriesto" to "' .  coursecat::get(0)->get_formatted_name() . '"'),
1288             new Given('I press "bulkmovecategories"'),
1289         );
1290     }
1292     /**
1293      * Checks that a category is a subcategory of specific category.
1294      *
1295      * @Given /^I should see category "(?P<subcatidnumber_string>(?:[^"]|\\")*)" as subcategory of "(?P<catidnumber_string>(?:[^"]|\\")*)" in the management interface$/
1296      * @throws ExpectationException
1297      * @param string $subcatidnumber
1298      * @param string $catidnumber
1299      */
1300     public function i_should_see_category_as_subcategory_of_in_the_management_interface($subcatidnumber, $catidnumber) {
1301         $categorynodeid = $this->get_category_id($catidnumber);
1302         $subcategoryid = $this->get_category_id($subcatidnumber);
1303         $exception = new ExpectationException('The category '.$subcatidnumber.' is not a subcategory of '.$catidnumber, $this->getSession());
1304         $selector = sprintf('#category-listing .listitem-category[data-id="%d"] .listitem-category[data-id="%d"]', $categorynodeid, $subcategoryid);
1305         $this->find('css', $selector, $exception);
1306     }
1308     /**
1309      * Checks that a category is not a subcategory of specific category.
1310      *
1311      * @Given /^I should not see category "(?P<subcatidnumber_string>(?:[^"]|\\")*)" as subcategory of "(?P<catidnumber_string>(?:[^"]|\\")*)" in the management interface$/
1312      * @throws ExpectationException
1313      * @param string $subcatidnumber
1314      * @param string $catidnumber
1315      */
1316     public function i_should_not_see_category_as_subcategory_of_in_the_management_interface($subcatidnumber, $catidnumber) {
1317         try {
1318             $this->i_should_see_category_as_subcategory_of_in_the_management_interface($subcatidnumber, $catidnumber);
1319         } catch (ExpectationException $e) {
1320             // ExpectedException means that it is not highlighted.
1321             return;
1322         }
1323         throw new ExpectationException('The category '.$subcatidnumber.' is a subcategory of '.$catidnumber, $this->getSession());
1324     }
1326     /**
1327      * Click to expand a category revealing its sub categories within the management UI.
1328      *
1329      * @Given /^I click to expand category "(?P<idnumber_string>(?:[^"]|\\")*)" in the management interface$/
1330      * @param string $idnumber
1331      */
1332     public function i_click_to_expand_category_in_the_management_interface($idnumber) {
1333         $categorynode = $this->get_management_category_listing_node_by_idnumber($idnumber);
1334         $exception = new ExpectationException('Category "' . $idnumber . '" does not contain an expand or collapse toggle.', $this->getSession());
1335         $togglenode = $this->find('css', 'a[data-action=collapse],a[data-action=expand]', $exception, $categorynode);
1336         $togglenode->click();
1337     }
1339     /**
1340      * Checks that a category within the management interface is visible.
1341      *
1342      * @Given /^category in management listing should be visible "(?P<idnumber_string>(?:[^"]|\\")*)"$/
1343      * @param string $idnumber
1344      */
1345     public function category_in_management_listing_should_be_visible($idnumber) {
1346         $id = $this->get_category_id($idnumber);
1347         $exception = new ExpectationException('The category '.$idnumber.' is not visible.', $this->getSession());
1348         $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible="1"]', $id);
1349         $this->find('css', $selector, $exception);
1350     }
1352     /**
1353      * Checks that a category within the management interface is dimmed.
1354      *
1355      * @Given /^category in management listing should be dimmed "(?P<idnumber_string>(?:[^"]|\\")*)"$/
1356      * @param string $idnumber
1357      */
1358     public function category_in_management_listing_should_be_dimmed($idnumber) {
1359         $id = $this->get_category_id($idnumber);
1360         $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible="0"]', $id);
1361         $exception = new ExpectationException('The category '.$idnumber.' is visible.', $this->getSession());
1362         $this->find('css', $selector, $exception);
1363     }
1365     /**
1366      * Checks that a course within the management interface is visible.
1367      *
1368      * @Given /^course in management listing should be visible "(?P<idnumber_string>(?:[^"]|\\")*)"$/
1369      * @param string $idnumber
1370      */
1371     public function course_in_management_listing_should_be_visible($idnumber) {
1372         $id = $this->get_course_id($idnumber);
1373         $exception = new ExpectationException('The course '.$idnumber.' is not visible.', $this->getSession());
1374         $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible="1"]', $id);
1375         $this->find('css', $selector, $exception);
1376     }
1378     /**
1379      * Checks that a course within the management interface is dimmed.
1380      *
1381      * @Given /^course in management listing should be dimmed "(?P<idnumber_string>(?:[^"]|\\")*)"$/
1382      * @param string $idnumber
1383      */
1384     public function course_in_management_listing_should_be_dimmed($idnumber) {
1385         $id = $this->get_course_id($idnumber);
1386         $exception = new ExpectationException('The course '.$idnumber.' is visible.', $this->getSession());
1387         $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible="0"]', $id);
1388         $this->find('css', $selector, $exception);
1389     }
1391     /**
1392      * Toggles the visibility of a course in the management UI.
1393      *
1394      * If it was visible it will be hidden. If it is hidden it will be made visible.
1395      *
1396      * @Given /^I toggle visibility of course "(?P<idnumber_string>(?:[^"]|\\")*)" in management listing$/
1397      * @param string $idnumber
1398      */
1399     public function i_toggle_visibility_of_course_in_management_listing($idnumber) {
1400         $id = $this->get_course_id($idnumber);
1401         $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible]', $id);
1402         $node = $this->find('css', $selector);
1403         $exception = new ExpectationException('Course listing "' . $idnumber . '" does not contain a show or hide toggle.', $this->getSession());
1404         if ($node->getAttribute('data-visible') === '1') {
1405             $toggle = $this->find('css', '.action-hide', $exception, $node);
1406         } else {
1407             $toggle = $this->find('css', '.action-show', $exception, $node);
1408         }
1409         $toggle->click();
1410     }
1412     /**
1413      * Toggles the visibility of a category in the management UI.
1414      *
1415      * If it was visible it will be hidden. If it is hidden it will be made visible.
1416      *
1417      * @Given /^I toggle visibility of category "(?P<idnumber_string>(?:[^"]|\\")*)" in management listing$/
1418      */
1419     public function i_toggle_visibility_of_category_in_management_listing($idnumber) {
1420         $id = $this->get_category_id($idnumber);
1421         $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible]', $id);
1422         $node = $this->find('css', $selector);
1423         $exception = new ExpectationException('Category listing "' . $idnumber . '" does not contain a show or hide toggle.', $this->getSession());
1424         if ($node->getAttribute('data-visible') === '1') {
1425             $toggle = $this->find('css', '.action-hide', $exception, $node);
1426         } else {
1427             $toggle = $this->find('css', '.action-show', $exception, $node);
1428         }
1429         $toggle->click();
1430     }
1432     /**
1433      * Moves a category displayed in the management interface up or down one place.
1434      *
1435      * @Given /^I click to move category "(?P<idnumber_string>(?:[^"]|\\")*)" (?P<direction>up|down) one$/
1436      *
1437      * @param string $idnumber The category idnumber
1438      * @param string $direction The direction to move in, either up or down
1439      */
1440     public function i_click_to_move_category_by_one($idnumber, $direction) {
1441         $node = $this->get_management_category_listing_node_by_idnumber($idnumber);
1442         $this->user_moves_listing_by_one('category', $node, $direction);
1443     }
1445     /**
1446      * Moves a course displayed in the management interface up or down one place.
1447      *
1448      * @Given /^I click to move course "(?P<idnumber_string>(?:[^"]|\\")*)" (?P<direction>up|down) one$/
1449      *
1450      * @param string $idnumber The course idnumber
1451      * @param string $direction The direction to move in, either up or down
1452      */
1453     public function i_click_to_move_course_by_one($idnumber, $direction) {
1454         $node = $this->get_management_course_listing_node_by_idnumber($idnumber);
1455         $this->user_moves_listing_by_one('course', $node, $direction);
1456     }
1458     /**
1459      * Moves a course or category listing within the management interface up or down by one.
1460      *
1461      * @param string $listingtype One of course or category
1462      * @param \Behat\Mink\Element\NodeElement $listingnode
1463      * @param string $direction One of up or down.
1464      * @param bool $highlight If set to false we don't check the node has been highlighted.
1465      */
1466     protected function user_moves_listing_by_one($listingtype, $listingnode, $direction, $highlight = true) {
1467         $up = (strtolower($direction) === 'up');
1468         if ($up) {
1469             $exception = new ExpectationException($listingtype.' listing does not contain a moveup button.', $this->getSession());
1470             $button = $this->find('css', 'a.action-moveup', $exception, $listingnode);
1471         } else {
1472             $exception = new ExpectationException($listingtype.' listing does not contain a movedown button.', $this->getSession());
1473             $button = $this->find('css', 'a.action-movedown', $exception, $listingnode);
1474         }
1475         $button->click();
1476         if ($this->running_javascript() && $highlight) {
1477             $listitem = $listingnode->getParent();
1478             $exception = new ExpectationException('Nothing was highlighted, ajax didn\'t occur or didn\'t succeed.', $this->getSession());
1479             $this->spin(array($this, 'listing_is_highlighted'), $listitem->getTagName().'#'.$listitem->getAttribute('id'), 2, $exception, true);
1480         }
1481     }
1483     /**
1484      * Used by spin to determine the callback has been highlighted.
1485      *
1486      * @param behat_course $self A self reference (default first arg from a spin callback)
1487      * @param \Behat\Mink\Element\NodeElement $selector
1488      * @return bool
1489      */
1490     protected function listing_is_highlighted($self, $selector) {
1491         $listitem = $this->find('css', $selector);
1492         return $listitem->hasClass('highlight');
1493     }
1495     /**
1496      * Check that one course appears before another in the course category management listings.
1497      *
1498      * @Given /^I should see course listing "(?P<preceedingcourse_string>(?:[^"]|\\")*)" before "(?P<followingcourse_string>(?:[^"]|\\")*)"$/
1499      *
1500      * @param string $preceedingcourse The first course to find
1501      * @param string $followingcourse The second course to find (should be AFTER the first course)
1502      * @throws ExpectationException
1503      */
1504     public function i_should_see_course_listing_before($preceedingcourse, $followingcourse) {
1505         $xpath = "//div[@id='course-listing']//li[contains(concat(' ', @class, ' '), ' listitem-course ')]//a[text()='{$preceedingcourse}']/ancestor::li[@data-id]//following::a[text()='{$followingcourse}']";
1506         $msg = "{$preceedingcourse} course does not appear before {$followingcourse} course";
1507         if (!$this->getSession()->getDriver()->find($xpath)) {
1508             throw new ExpectationException($msg, $this->getSession());
1509         }
1510     }
1512     /**
1513      * Check that one category appears before another in the course category management listings.
1514      *
1515      * @Given /^I should see category listing "(?P<preceedingcategory_string>(?:[^"]|\\")*)" before "(?P<followingcategory_string>(?:[^"]|\\")*)"$/
1516      *
1517      * @param string $preceedingcategory The first category to find
1518      * @param string $followingcategory The second category to find (should be after the first category)
1519      * @throws ExpectationException
1520      */
1521     public function i_should_see_category_listing_before($preceedingcategory, $followingcategory) {
1522         $xpath = "//div[@id='category-listing']//li[contains(concat(' ', @class, ' '), ' listitem-category ')]//a[text()='{$preceedingcategory}']/ancestor::li[@data-id]//following::a[text()='{$followingcategory}']";
1523         $msg = "{$preceedingcategory} category does not appear before {$followingcategory} category";
1524         if (!$this->getSession()->getDriver()->find($xpath)) {
1525             throw new ExpectationException($msg, $this->getSession());
1526         }
1527     }
1529     /**
1530      * Checks that we are on the course management page that we expect to be on and that no course has been selected.
1531      *
1532      * @Given /^I should see the "(?P<mode_string>(?:[^"]|\\")*)" management page$/
1533      * @param string $mode The mode to expected. One of 'Courses', 'Course categories' or 'Course categories and courses'
1534      * @return Given[]
1535      */
1536     public function i_should_see_the_courses_management_page($mode) {
1537         $return = array(
1538             new Given('I should see "Course and category management" in the "h2" "css_element"')
1539         );
1540         switch ($mode) {
1541             case "Courses":
1542                 $return[] = new Given('"#category-listing" "css_element" should not exist');
1543                 $return[] = new Given('"#course-listing" "css_element" should exist');
1544                 break;
1545             case "Course categories":
1546                 $return[] = new Given('"#category-listing" "css_element" should exist');
1547                 $return[] = new Given('"#course-listing" "css_element" should exist');
1548                 break;
1549             case "Courses categories and courses":
1550             default:
1551                 $return[] = new Given('"#category-listing" "css_element" should exist');
1552                 $return[] = new Given('"#course-listing" "css_element" should exist');
1553                 break;
1554         }
1555         $return[] = new Given('"#course-detail" "css_element" should not exist');
1556         return $return;
1557     }
1559     /**
1560      * Checks that we are on the course management page that we expect to be on and that a course has been selected.
1561      *
1562      * @Given /^I should see the "(?P<mode_string>(?:[^"]|\\")*)" management page with a course selected$/
1563      * @param string $mode The mode to expected. One of 'Courses', 'Course categories' or 'Course categories and courses'
1564      * @return Given[]
1565      */
1566     public function i_should_see_the_courses_management_page_with_a_course_selected($mode) {
1567         $return = $this->i_should_see_the_courses_management_page($mode);
1568         array_pop($return);
1569         $return[] = new Given('"#course-detail" "css_element" should exist');
1570         return $return;
1571     }
1573     /**
1574      * Locates a course in the course category management interface and then triggers an action for it.
1575      *
1576      * @Given /^I click on "(?P<action_string>(?:[^"]|\\")*)" action for "(?P<name_string>(?:[^"]|\\")*)" in management course listing$/
1577      *
1578      * @param string $action The action to take. One of
1579      * @param string $name The name of the course as it is displayed in the management interface.
1580      */
1581     public function i_click_on_action_for_item_in_management_course_listing($action, $name) {
1582         $node = $this->get_management_course_listing_node_by_name($name);
1583         $this->user_clicks_on_management_listing_action('course', $node, $action);
1584     }
1586     /**
1587      * Locates a category in the course category management interface and then triggers an action for it.
1588      *
1589      * @Given /^I click on "(?P<action_string>(?:[^"]|\\")*)" action for "(?P<name_string>(?:[^"]|\\")*)" in management category listing$/
1590      *
1591      * @param string $action The action to take. One of
1592      * @param string $name The name of the category as it is displayed in the management interface.
1593      */
1594     public function i_click_on_action_for_item_in_management_category_listing($action, $name) {
1595         $node = $this->get_management_category_listing_node_by_name($name);
1596         $this->user_clicks_on_management_listing_action('category', $node, $action);
1597     }
1599     /**
1600      * Clicks to expand or collapse a category displayed on the frontpage
1601      *
1602      * @Given /^I toggle "(?P<categoryname_string>(?:[^"]|\\")*)" category children visibility in frontpage$/
1603      * @throws ExpectationException
1604      * @param string $categoryname
1605      */
1606     public function i_toggle_category_children_visibility_in_frontpage($categoryname) {
1608         $headingtags = array();
1609         for ($i = 1; $i <= 6; $i++) {
1610             $headingtags[] = 'self::h' . $i;
1611         }
1613         $exception = new ExpectationException('"' . $categoryname . '" category can not be found', $this->getSession());
1614         $categoryliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($categoryname);
1615         $xpath = "//div[@class='info']/descendant::*[" . implode(' or ', $headingtags) . "][@class='categoryname'][./descendant::a[.=$categoryliteral]]";
1616         $node = $this->find('xpath', $xpath, $exception);
1617         $node->click();
1619         // Smooth expansion.
1620         $this->getSession()->wait(1000, false);
1621     }
1623     /**
1624      * Finds the node to use for a management listitem action and clicks it.
1625      *
1626      * @param string $listingtype Either course or category.
1627      * @param \Behat\Mink\Element\NodeElement $listingnode
1628      * @param string $action The action being taken
1629      * @throws Behat\Mink\Exception\ExpectationException
1630      */
1631     protected function user_clicks_on_management_listing_action($listingtype, $listingnode, $action) {
1632         $actionsnode = $listingnode->find('xpath', "//*[contains(concat(' ', normalize-space(@class), ' '), '{$listingtype}-item-actions')]");
1633         if (!$actionsnode) {
1634             throw new ExpectationException("Could not find the actions for $listingtype", $this->getSession());
1635         }
1636         $actionnode = $actionsnode->find('css', '.action-'.$action);
1637         if (!$actionnode) {
1638             throw new ExpectationException("Expected action was not available or not found ($action)", $this->getSession());
1639         }
1640         if ($this->running_javascript() && !$actionnode->isVisible()) {
1641             $actionsnode->find('css', 'a.toggle-display')->click();
1642             $actionnode = $actionsnode->find('css', '.action-'.$action);
1643         }
1644         $actionnode->click();
1645     }
1647     /**
1648      * Clicks on a category in the management interface.
1649      *
1650      * @Given /^I click on "(?P<categoryname_string>(?:[^"]|\\")*)" category in the management category listing$/
1651      * @param string $name The name of the category to click.
1652      */
1653     public function i_click_on_category_in_the_management_category_listing($name) {
1654         $node = $this->get_management_category_listing_node_by_name($name);
1655         $node->find('css', 'a.categoryname')->click();
1656     }