67833867e17ff08ad604d114eaacd7da4a3a7fe7
[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) {
70         return array(
71             new Given('I go to the courses management page'),
72             new Given('I should see the "Course categories" management page'),
73             new Given('I click on "Miscellaneous" category listing'),
74             new Given('I should see the "Course categories and courses" management page'),
75             new Given('I click on "New course" "link" in the "#course-listing" "css_element"'),
76             new Given('I fill the moodle form with:', $table),
77             new Given('I press "' . get_string('savechanges') . '"')
78         );
79     }
81     /**
82      * Goes to the system courses/categories management page.
83      *
84      * @Given /^I go to the courses management page$/
85      * @return Given[]
86      */
87     public function i_go_to_the_courses_management_page() {
89         return array(
90             new Given('I am on homepage'),
91             new Given('I expand "' . get_string('administrationsite') . '" node'),
92             new Given('I expand "' . get_string('courses', 'admin') . '" node'),
93             new Given('I follow "' . get_string('coursemgmt', 'admin') . '"'),
94         );
95     }
97     /**
98      * Adds the selected activity/resource filling the form data with the specified field/value pairs.
99      *
100      * @When /^I add a "(?P<activity_or_resource_name_string>(?:[^"]|\\")*)" to section "(?P<section_number>\d+)" and I fill the form with:$/
101      * @param string $activity The activity name
102      * @param int $section The section number
103      * @param TableNode $data The activity field/value data
104      * @return Given[]
105      */
106     public function i_add_to_section_and_i_fill_the_form_with($activity, $section, TableNode $data) {
108         return array(
109             new Given('I add a "' . $this->escape($activity) . '" to section "' . $this->escape($section) . '"'),
110             new Given('I fill the moodle form with:', $data),
111             new Given('I press "' . get_string('savechangesandreturntocourse') . '"')
112         );
113     }
115     /**
116      * Opens the activity chooser and opens the activity/resource form page.
117      *
118      * @Given /^I add a "(?P<activity_or_resource_name_string>(?:[^"]|\\")*)" to section "(?P<section_number>\d+)"$/
119      * @throws ElementNotFoundException Thrown by behat_base::find
120      * @param string $activity
121      * @param int $section
122      */
123     public function i_add_to_section($activity, $section) {
125         $sectionxpath = "//li[@id='section-" . $section . "']";
127         $activityliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral(ucfirst($activity));
129         if ($this->running_javascript()) {
131             // Clicks add activity or resource section link.
132             $sectionxpath = $sectionxpath . "/descendant::div[@class='section-modchooser']/span/a";
133             $sectionnode = $this->find('xpath', $sectionxpath);
134             $sectionnode->click();
136             // Clicks the selected activity if it exists.
137             $activityxpath = "//div[@id='chooseform']/descendant::label" .
138                 "/descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' typename ')]" .
139                 "[contains(., $activityliteral)]" .
140                 "/parent::label/child::input";
141             $activitynode = $this->find('xpath', $activityxpath);
142             $activitynode->doubleClick();
144         } else {
145             // Without Javascript.
147             // Selecting the option from the select box which contains the option.
148             $selectxpath = $sectionxpath . "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' section_add_menus ')]" .
149                 "/descendant::select[contains(., $activityliteral)]";
150             $selectnode = $this->find('xpath', $selectxpath);
151             $selectnode->selectOption($activity);
153             // Go button.
154             $gobuttonxpath = $selectxpath . "/ancestor::form/descendant::input[@type='submit']";
155             $gobutton = $this->find('xpath', $gobuttonxpath);
156             $gobutton->click();
157         }
159     }
161     /**
162      * Turns course section highlighting on.
163      *
164      * @Given /^I turn section "(?P<section_number>\d+)" highlighting on$/
165      * @param int $sectionnumber The section number
166      * @return Given[]
167      */
168     public function i_turn_section_highlighting_on($sectionnumber) {
170         // Ensures the section exists.
171         $xpath = $this->section_exists($sectionnumber);
173         return array(
174             new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'),
175             new Given('I wait "2" seconds')
176         );
177     }
179     /**
180      * Turns course section highlighting off.
181      *
182      * @Given /^I turn section "(?P<section_number>\d+)" highlighting off$/
183      * @param int $sectionnumber The section number
184      * @return Given[]
185      */
186     public function i_turn_section_highlighting_off($sectionnumber) {
188         // Ensures the section exists.
189         $xpath = $this->section_exists($sectionnumber);
191         return array(
192             new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'),
193             new Given('I wait "2" seconds')
194         );
195     }
197     /**
198      * Shows the specified hidden section. You need to be in the course page and on editing mode.
199      *
200      * @Given /^I show section "(?P<section_number>\d+)"$/
201      * @param int $sectionnumber
202      */
203     public function i_show_section($sectionnumber) {
204         $showlink = $this->show_section_icon_exists($sectionnumber);
205         $showlink->click();
207         // It requires time.
208         if ($this->running_javascript()) {
209             $this->getSession()->wait(5000, false);
210         }
211     }
213     /**
214      * Hides the specified visible section. You need to be in the course page and on editing mode.
215      *
216      * @Given /^I hide section "(?P<section_number>\d+)"$/
217      * @param int $sectionnumber
218      */
219     public function i_hide_section($sectionnumber) {
220         $hidelink = $this->hide_section_icon_exists($sectionnumber);
221         $hidelink->click();
223         // It requires time.
224         if ($this->running_javascript()) {
225             $this->getSession()->wait(5000, false);
226         }
227     }
229     /**
230      * Checks if the specified course section hightlighting is turned on. You need to be in the course page on editing mode.
231      *
232      * @Then /^section "(?P<section_number>\d+)" should be highlighted$/
233      * @throws ExpectationException
234      * @param int $sectionnumber The section number
235      */
236     public function section_should_be_highlighted($sectionnumber) {
238         // Ensures the section exists.
239         $xpath = $this->section_exists($sectionnumber);
241         // The important checking, we can not check the img.
242         $xpath = $xpath . "/descendant::img[@alt='" . get_string('markedthistopic') . "'][contains(@src, 'marked')]";
243         $exception = new ExpectationException('The "' . $sectionnumber . '" section is not highlighted', $this->getSession());
244         $this->find('xpath', $xpath, $exception);
245     }
247     /**
248      * Checks if the specified course section highlighting is turned off. You need to be in the course page on editing mode.
249      *
250      * @Then /^section "(?P<section_number>\d+)" should not be highlighted$/
251      * @throws ExpectationException
252      * @param int $sectionnumber The section number
253      */
254     public function section_should_not_be_highlighted($sectionnumber) {
256         // We only catch ExpectationException, ElementNotFoundException should be thrown if the specified section does not exist.
257         try {
258             $this->section_should_be_highlighted($sectionnumber);
259         } catch (ExpectationException $e) {
260             // ExpectedException means that it is not highlighted.
261             return;
262         }
264         throw new ExpectationException('The "' . $sectionnumber . '" section is highlighted', $this->getSession());
265     }
267     /**
268      * 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.
269      *
270      * @Then /^section "(?P<section_number>\d+)" should be hidden$/
271      * @throws ExpectationException
272      * @throws ElementNotFoundException Thrown by behat_base::find
273      * @param int $sectionnumber
274      */
275     public function section_should_be_hidden($sectionnumber) {
277         $sectionxpath = $this->section_exists($sectionnumber);
279         // Section should be hidden.
280         $exception = new ExpectationException('The section is not hidden', $this->getSession());
281         $this->find('xpath', $sectionxpath . "[contains(concat(' ', normalize-space(@class), ' '), ' hidden ')]", $exception);
283         // The checking are different depending on user permissions.
284         if ($this->is_course_editor()) {
286             // The section must be hidden.
287             $this->show_section_icon_exists($sectionnumber);
289             // If there are activities they should be hidden and the visibility icon should not be available.
290             if ($activities = $this->get_section_activities($sectionxpath)) {
292                 $dimmedexception = new ExpectationException('There are activities that are not dimmed', $this->getSession());
293                 $visibilityexception = new ExpectationException('There are activities which visibility icons are clickable', $this->getSession());
294                 foreach ($activities as $activity) {
296                     // Dimmed.
297                     $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' activityinstance ')]" .
298                         "/a[contains(concat(' ', normalize-space(@class), ' '), ' dimmed ')]", $dimmedexception, $activity);
300                     // Non-JS browsers can not click on img elements.
301                     if ($this->running_javascript()) {
302                         // To check that the visibility is not clickable we check the funcionality rather than the applied style.
303                         $visibilityiconnode = $this->find('css', 'a.editing_show img', false, $activity);
304                         $visibilityiconnode->click();
305                     }
307                     // We ensure that we still see the show icon.
308                     $visibilityiconnode = $this->find('css', 'a.editing_show img', $visibilityexception, $activity);
309                 }
310             }
312         } else {
313             // There shouldn't be activities.
314             if ($this->get_section_activities($sectionxpath)) {
315                 throw new ExpectationException('There are activities in the section and they should be hidden', $this->getSession());
316             }
317         }
318     }
320     /**
321      * 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.
322      *
323      * @Then /^section "(?P<section_number>\d+)" should be visible$/
324      * @throws ExpectationException
325      * @param int $sectionnumber
326      */
327     public function section_should_be_visible($sectionnumber) {
329         $sectionxpath = $this->section_exists($sectionnumber);
331         // Section should not be hidden.
332         $xpath = $sectionxpath . "[not(contains(concat(' ', normalize-space(@class), ' '), ' hidden '))]";
333         if (!$this->getSession()->getPage()->find('xpath', $xpath)) {
334             throw new ExpectationException('The section is hidden', $this->getSession());
335         }
337         // Hide section button should be visible.
338         if ($this->is_course_editor()) {
339             $this->hide_section_icon_exists($sectionnumber);
340         }
341     }
343     /**
344      * Moves up the specified section, this step only works with Javascript disabled. Editing mode should be on.
345      *
346      * @Given /^I move up section "(?P<section_number>\d+)"$/
347      * @throws DriverException Step not available when Javascript is enabled
348      * @param int $sectionnumber
349      */
350     public function i_move_up_section($sectionnumber) {
352         if ($this->running_javascript()) {
353             throw new DriverException('Move a section up step is not available with Javascript enabled');
354         }
356         // Ensures the section exists.
357         $sectionxpath = $this->section_exists($sectionnumber);
359         // Follows the link
360         $moveuplink = $this->get_node_in_container('link', get_string('moveup'), 'xpath_element', $sectionxpath);
361         $moveuplink->click();
362     }
364     /**
365      * Moves down the specified section, this step only works with Javascript disabled. Editing mode should be on.
366      *
367      * @Given /^I move down section "(?P<section_number>\d+)"$/
368      * @throws DriverException Step not available when Javascript is enabled
369      * @param int $sectionnumber
370      */
371     public function i_move_down_section($sectionnumber) {
373         if ($this->running_javascript()) {
374             throw new DriverException('Move a section down step is not available with Javascript enabled');
375         }
377         // Ensures the section exists.
378         $sectionxpath = $this->section_exists($sectionnumber);
380         // Follows the link
381         $movedownlink = $this->get_node_in_container('link', get_string('movedown'), 'xpath_element', $sectionxpath);
382         $movedownlink->click();
383     }
385     /**
386      * 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.
387      *
388      * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" activity should be visible$/
389      * @param string $activityname
390      * @throws ExpectationException
391      */
392     public function activity_should_be_visible($activityname) {
394         // The activity must exists and be visible.
395         $activitynode = $this->get_activity_node($activityname);
397         if ($this->is_course_editor()) {
399             // The activity should not be dimmed.
400             try {
401                 $this->find('css', 'a.dimmed', false, $activitynode);
402                 throw new ExpectationException('"' . $activityname . '" is hidden', $this->getSession());
403             } catch (ElementNotFoundException $e) {
404                 // All ok.
405             }
407             // The 'Hide' button should be available.
408             $nohideexception = new ExpectationException('"' . $activityname . '" don\'t have a "' . get_string('hide') . '" icon', $this->getSession());
409             $this->find('named', array('link', get_string('hide')), $nohideexception, $activitynode);
410         }
411     }
413     /**
414      * 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.
415      *
416      * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" activity should be hidden$/
417      * @param string $activityname
418      * @throws ExpectationException
419      */
420     public function activity_should_be_hidden($activityname) {
422         if ($this->is_course_editor()) {
424             // The activity should exists.
425             $activitynode = $this->get_activity_node($activityname);
427             // Should be hidden.
428             $exception = new ExpectationException('"' . $activityname . '" is not dimmed', $this->getSession());
429             $this->find('css', 'a.dimmed', $exception, $activitynode);
431             // Also 'Show' icon.
432             $noshowexception = new ExpectationException('"' . $activityname . '" don\'t have a "' . get_string('show') . '" icon', $this->getSession());
433             $this->find('named', array('link', get_string('show')), $noshowexception, $activitynode);
435         } else {
437             // It should not exists at all.
438             try {
439                 $this->find_link($activityname);
440                 throw new ExpectationException('The "' . $activityname . '" should not appear');
441             } catch (ElementNotFoundException $e) {
442                 // This is good, the activity should not be there.
443             }
444         }
446     }
448     /**
449      * 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.
450      *
451      * @Given /^I move "(?P<activity_name_string>(?:[^"]|\\")*)" activity to section "(?P<section_number>\d+)"$/
452      * @param string $activityname The activity name
453      * @param int $sectionnumber The number of section
454      * @return Given[]
455      */
456     public function i_move_activity_to_section($activityname, $sectionnumber) {
458         // Ensure the destination is valid.
459         $sectionxpath = $this->section_exists($sectionnumber);
461         $activitynode = $this->get_activity_element('.editing_move img', 'css_element', $activityname);
463         // JS enabled.
464         if ($this->running_javascript()) {
466             $destinationxpath = $sectionxpath . "/descendant::ul[contains(concat(' ', normalize-space(@class), ' '), ' yui3-dd-drop ')]";
468             return array(
469                 new Given('I drag "' . $this->escape($activitynode->getXpath()) . '" "xpath_element" ' .
470                     'and I drop it in "' . $this->escape($destinationxpath) . '" "xpath_element"'),
471             );
473         } else {
474             // Following links with no-JS.
476             // Moving to the fist spot of the section (before all other section's activities).
477             return array(
478                 new Given('I click on "a.editing_move" "css_element" in the "' . $this->escape($activityname) . '" activity'),
479                 new Given('I click on "li.movehere a" "css_element" in the "' . $this->escape($sectionxpath) . '" "xpath_element"'),
480             );
481         }
482     }
484     /**
485      * Edits the activity name through the edit activity; this step only works with Javascript enabled. Editing mode should be on.
486      *
487      * @Given /^I change "(?P<activity_name_string>(?:[^"]|\\")*)" activity name to "(?P<new_name_string>(?:[^"]|\\")*)"$/
488      * @throws DriverException Step not available when Javascript is disabled
489      * @param string $activityname
490      * @param string $newactivityname
491      * @return Given[]
492      */
493     public function i_change_activity_name_to($activityname, $newactivityname) {
495         if (!$this->running_javascript()) {
496             throw new DriverException('Change activity name step is not available with Javascript disabled');
497         }
499         // Adding chr(10) to save changes.
500         $activity = $this->escape($activityname);
501         return array(
502             new Given('I click on "' . get_string('actions', 'moodle') . '" "link" in the "' . $activity . '" activity'),
503             new Given('I click on "' . get_string('edittitle') . '" "link" in the "' . $activity .'" activity'),
504             new Given('I fill in "title" with "' . $this->escape($newactivityname) . chr(10) . '"'),
505             new Given('I wait "2" seconds')
506         );
507     }
509     /**
510      * Indents to the right the activity or resource specified by it's name. Editing mode should be on.
511      *
512      * @Given /^I indent right "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
513      * @param string $activityname
514      * @return Given[]
515      */
516     public function i_indent_right_activity($activityname) {
518         $steps = array();
519         $activity = $this->escape($activityname);
520         if ($this->running_javascript()) {
521             $steps[] = new Given('I click on "' . get_string('actions', 'moodle') . '" "link" in the "' . $activity . '" activity');
522         }
523         $steps[] = new Given('I click on "' . get_string('moveright') . '" "link" in the "' . $activity . '" activity');
525         if ($this->running_javascript()) {
526             $steps[] = new Given('I wait "2" seconds');
527         }
529         return $steps;
530     }
532     /**
533      * Indents to the left the activity or resource specified by it's name. Editing mode should be on.
534      *
535      * @Given /^I indent left "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
536      * @param string $activityname
537      * @return Given[]
538      */
539     public function i_indent_left_activity($activityname) {
541         $steps = array();
542         $activity = $this->escape($activityname);
543         if ($this->running_javascript()) {
544             $steps[] = new Given('I click on "' . get_string('actions', 'moodle') . '" "link" in the "' . $activity . '" activity');
545         }
546         $steps[] = new Given('I click on "' . get_string('moveleft') . '" "link" in the "' . $activity . '" activity');
548         if ($this->running_javascript()) {
549             $steps[] = new Given('I wait "2" seconds');
550         }
552         return $steps;
554     }
556     /**
557      * 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.
558      *
559      * @Given /^I delete "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
560      * @param string $activityname
561      * @return Given[]
562      */
563     public function i_delete_activity($activityname) {
565         $deletestring = get_string('delete');
567         // JS enabled.
568         // Not using chain steps here because the exceptions catcher have problems detecting
569         // JS modal windows and avoiding interacting them at the same time.
570         if ($this->running_javascript()) {
572             $element = $this->get_activity_element($deletestring, 'link', $activityname);
573             $element->click();
575             $this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
577             $this->getSession()->wait(2 * 1000, false);
579         } else {
581             // With JS disabled.
582             $steps = array(
583                 new Given('I click on "' . $this->escape($deletestring) . '" "link" in the "' . $this->escape($activityname) . '" activity'),
584                 new Given('I press "' . get_string('yes') . '"')
585             );
587             return $steps;
588         }
589     }
591     /**
592      * Duplicates the activity or resource specified by it's name. You should be in the course page with editing mode on.
593      *
594      * @Given /^I duplicate "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
595      * @param string $activityname
596      * @return Given[]
597      */
598     public function i_duplicate_activity($activityname) {
599         $steps = array();
600         $activity = $this->escape($activityname);
601         if ($this->running_javascript()) {
602             $steps[] = new Given('I click on "' . get_string('actions', 'moodle') . '" "link" in the "' . $activity . '" activity');
603         }
604         $steps[] = new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activity . '" activity');
605         $steps[] = new Given('I press "' . get_string('continue') .'"');
606         $steps[] = new Given('I press "' . get_string('duplicatecontcourse') .'"');
607         return $steps;
608     }
610     /**
611      * 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.
612      *
613      * @Given /^I duplicate "(?P<activity_name_string>(?:[^"]|\\")*)" activity editing the new copy with:$/
614      * @param string $activityname
615      * @param TableNode $data
616      * @return Given[]
617      */
618     public function i_duplicate_activity_editing_the_new_copy_with($activityname, TableNode $data) {
619         $steps = array();
620         $activity = $this->escape($activityname);
621         if ($this->running_javascript()) {
622             $steps[] = new Given('I click on "' . get_string('actions', 'moodle') . '" "link" in the "' . $activity . '" activity');
623         }
624         $steps[] = new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activity . '" activity');
625         $steps[] = new Given('I press "' . get_string('continue') .'"');
626         $steps[] = new Given('I press "' . get_string('duplicatecontedit') . '"');
627         $steps[] = new Given('I fill the moodle form with:', $data);
628         $steps[] = new Given('I press "' . get_string('savechangesandreturntocourse') . '"');
629         return $steps;
630     }
632     /**
633      * Clicks on the specified element of the activity. You should be in the course page with editing mode turned on.
634      *
635      * @Given /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" in the "(?P<activity_name_string>[^"]*)" activity$/
636      * @param string $element
637      * @param string $selectortype
638      * @param string $activityname
639      */
640     public function i_click_on_in_the_activity($element, $selectortype, $activityname) {
641         $element = $this->get_activity_element($element, $selectortype, $activityname);
642         $element->click();
643     }
645     /**
646      * Clicks on the specified element inside the activity container.
647      *
648      * @throws ElementNotFoundException
649      * @param string $element
650      * @param string $selectortype
651      * @param string $activityname
652      * @return NodeElement
653      */
654     protected function get_activity_element($element, $selectortype, $activityname) {
655         $activitynode = $this->get_activity_node($activityname);
657         // Transforming to Behat selector/locator.
658         list($selector, $locator) = $this->transform_selector($selectortype, $element);
659         $exception = new ElementNotFoundException($this->getSession(), '"' . $element . '" "' . $selectortype . '" in "' . $activityname . '" ');
661         return $this->find($selector, $locator, $exception, $activitynode);
662     }
664     /**
665      * Checks if the course section exists.
666      *
667      * @throws ElementNotFoundException Thrown by behat_base::find
668      * @param int $sectionnumber
669      * @return string The xpath of the section.
670      */
671     protected function section_exists($sectionnumber) {
673         // Just to give more info in case it does not exist.
674         $xpath = "//li[@id='section-" . $sectionnumber . "']";
675         $exception = new ElementNotFoundException($this->getSession(), "Section $sectionnumber ");
676         $this->find('xpath', $xpath, $exception);
678         return $xpath;
679     }
681     /**
682      * Returns the show section icon or throws an exception.
683      *
684      * @throws ElementNotFoundException Thrown by behat_base::find
685      * @param int $sectionnumber
686      * @return NodeElement
687      */
688     protected function show_section_icon_exists($sectionnumber) {
690         // Gets the section xpath and ensure it exists.
691         $xpath = $this->section_exists($sectionnumber);
693         // We need to know the course format as the text strings depends on them.
694         $courseformat = $this->get_course_format();
696         // Checking the show button alt text and show icon.
697         $showtext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('showfromothers', $courseformat));
698         $linkxpath = $xpath . "/descendant::a[@title=$showtext]";
699         $imgxpath = $linkxpath . "/descendant::img[@alt=$showtext][contains(@src, 'show')]";
701         $exception = new ElementNotFoundException($this->getSession(), 'Show section icon ');
702         $this->find('xpath', $imgxpath, $exception);
704         // Returing the link so both Non-JS and JS browsers can interact with it.
705         return $this->find('xpath', $linkxpath, $exception);
706     }
708     /**
709      * Returns the hide section icon link if it exists or throws exception.
710      *
711      * @throws ElementNotFoundException Thrown by behat_base::find
712      * @param int $sectionnumber
713      * @return NodeElement
714      */
715     protected function hide_section_icon_exists($sectionnumber) {
717         // Gets the section xpath and ensure it exists.
718         $xpath = $this->section_exists($sectionnumber);
720         // We need to know the course format as the text strings depends on them.
721         $courseformat = $this->get_course_format();
723         // Checking the hide button alt text and hide icon.
724         $hidetext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('hidefromothers', $courseformat));
725         $linkxpath = $xpath . "/descendant::a[@title=$hidetext]";
726         $imgxpath = $linkxpath . "/descendant::img[@alt=$hidetext][contains(@src, 'hide')]";
728         $exception = new ElementNotFoundException($this->getSession(), 'Hide section icon ');
729         $this->find('xpath', $imgxpath, $exception);
731         // Returing the link so both Non-JS and JS browsers can interact with it.
732         return $this->find('xpath', $linkxpath, $exception);
733     }
735     /**
736      * Gets the current course format.
737      *
738      * @throws ExpectationException If we are not in the course view page.
739      * @return string The course format in a frankenstyled name.
740      */
741     protected function get_course_format() {
743         $exception = new ExpectationException('You are not in a course page', $this->getSession());
745         // The moodle body's id attribute contains the course format.
746         $node = $this->getSession()->getPage()->find('css', 'body');
747         if (!$node) {
748             throw $exception;
749         }
751         if (!$bodyid = $node->getAttribute('id')) {
752             throw $exception;
753         }
755         if (strstr($bodyid, 'page-course-view-') === false) {
756             throw $exception;
757         }
759         return 'format_' . str_replace('page-course-view-', '', $bodyid);
760     }
762     /**
763      * Gets the section's activites DOM nodes.
764      *
765      * @param string $sectionxpath
766      * @return array NodeElement instances
767      */
768     protected function get_section_activities($sectionxpath) {
770         $xpath = $sectionxpath . "/descendant::li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]";
772         // We spin here, as activities usually require a lot of time to load.
773         try {
774             $activities = $this->find_all('xpath', $xpath);
775         } catch (ElementNotFoundException $e) {
776             return false;
777         }
779         return $activities;
780     }
782     /**
783      * Returns the DOM node of the activity from <li>.
784      *
785      * @throws ElementNotFoundException Thrown by behat_base::find
786      * @param string $activityname The activity name
787      * @return NodeElement
788      */
789     protected function get_activity_node($activityname) {
791         $activityname = $this->getSession()->getSelectorsHandler()->xpathLiteral($activityname);
792         $xpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][contains(., $activityname)]";
794         return $this->find('xpath', $xpath);
795     }
797     /**
798      * Returns whether the user can edit the course contents or not.
799      *
800      * @return bool
801      */
802     protected function is_course_editor() {
804         // We don't need to behat_base::spin() here as all is already loaded.
805         if (!$this->getSession()->getPage()->findButton(get_string('turneditingoff')) &&
806                 !$this->getSession()->getPage()->findButton(get_string('turneditingon'))) {
807             return false;
808         }
810         return true;
811     }
813     /**
814      * Returns the id of the category with the given idnumber.
815      * @param string $idnumber
816      * @return string
817      */
818     protected function get_category_id($idnumber) {
819         global $DB;
820         return $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber), MUST_EXIST);
821     }
823     /**
824      * Returns the id of the course with the given idnumber.
825      * @param string $idnumber
826      * @return string
827      */
828     protected function get_course_id($idnumber) {
829         global $DB;
830         return $DB->get_field('course', 'id', array('idnumber' => $idnumber), MUST_EXIST);
831     }
833     /**
834      * Returns the category node from within the listing on the management page.
835      *
836      * @param string $idnumber
837      * @return \Behat\Mink\Element\NodeElement
838      */
839     protected function get_management_category_listing_node_by_idnumber($idnumber) {
840         $id = $this->get_category_id($idnumber);
841         $selector = sprintf('#category-listing .listitem-category[data-id="%d"] > div', $id);
842         return $this->find('css', $selector);
843     }
845     /**
846      * @param $name
847      * @return \Behat\Mink\Element\NodeElement
848      */
849     protected function get_management_category_listing_node_by_name($name) {
850         $selector = "//div[@id='category-listing']//li[contains(concat(' ', normalize-space(@class), ' '), ' listitem-category ')]//a[text()='{$name}']/ancestor::li[@data-id]";
851         return $this->find('xpath', $selector);
852     }
854     /**
855      * @param $name
856      * @return \Behat\Mink\Element\NodeElement
857      */
858     protected function get_management_course_listing_node_by_name($name) {
859         $selector = "//div[@id='course-listing']//li[contains(concat(' ', @class, ' '), ' listitem-course ')]//a[text()='{$name}']/ancestor::li[@data-id]";
860         return $this->find('xpath', $selector);
861     }
863     /**
864      * Returns the course node from within the listing on the management page.
865      *
866      * @param string $idnumber
867      * @return \Behat\Mink\Element\NodeElement
868      */
869     protected function get_management_course_listing_node_by_idnumber($idnumber) {
870         $id = $this->get_course_id($idnumber);
871         $selector = sprintf('#course-listing .listitem-course[data-id="%d"] > div', $id);
872         return $this->find('css', $selector);
873     }
875     /**
876      * Toggle the expansion of a category revealing its sub categories within the management UI.
877      *
878      * @Given /^I click on "(?P<name>[^"]*)" (?P<listing>course|category) listing$/
879      * @param string $name
880      * @param string $listing
881      */
882     public function i_click_on_listing($name, $listing) {
883         if ($listing === 'course') {
884             $node = $this->get_management_course_listing_node_by_name($name);
885             $node->find('css', 'a.coursename')->click();
886         } else {
887             $node = $this->get_management_category_listing_node_by_name($name);
888             $node->find('css', 'a.categoryname')->click();
889         }
890     }
892     /**
893      * Toggle the expansion of a category revealing its sub categories within the management UI.
894      *
895      * @Given /^I click to toggle subcategories expansion "(?P<idnumber>[^"]*)"$/
896      * @param string $idnumber
897      */
898     public function i_click_to_toggle_subcategories_expansion($idnumber) {
899         $categorynode = $this->get_management_category_listing_node_by_idnumber($idnumber);
900         $exception = new ExpectationException('Category "' . $idnumber . '" does not contain an expand or collapse toggle.', $this->getSession());
901         $togglenode = $this->find('css', 'a[data-action=collapse],a[data-action=expand]', $exception, $categorynode);
902         $togglenode->click();
903     }
905     /**
906      * Throws an exception if the category with the matching idnumber is not "visible" in the management UI.
907      *
908      * @Given /^category in management listing should be visible "(?P<idnumber>[^"]*)"$/
909      * @param string $idnumber
910      */
911     public function category_in_management_listing_should_be_visible($idnumber) {
912         $id = $this->get_category_id($idnumber);
913         $exception = new ExpectationException('The category '.$idnumber.' is not visible.', $this->getSession());
914         $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible="1"]', $id);
915         $this->find('css', $selector, $exception);
916     }
918     /**
919      * Throws an exception if the category with the matching idnumber is "visible" in the management UI.
920      *
921      * @Given /^category in management listing should be dimmed "(?P<idnumber>[^"]*)"$/
922      * @param string $idnumber
923      */
924     public function category_in_management_listing_should_be_dimmed($idnumber) {
925         $id = $this->get_category_id($idnumber);
926         $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible="0"]', $id);
927         $exception = new ExpectationException('The category '.$idnumber.' is visible.', $this->getSession());
928         $this->find('css', $selector, $exception);
929     }
931     /**
932      * Throws an exception if the course with the matching idnumber is not "visible" in the management UI.
933      *
934      * @Given /^course in management listing should be visible "(?P<idnumber>[^"]*)"$/
935      * @param string $idnumber
936      */
937     public function course_in_management_listing_should_be_visible($idnumber) {
938         $id = $this->get_course_id($idnumber);
939         $exception = new ExpectationException('The course '.$idnumber.' is not visible.', $this->getSession());
940         $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible="1"]', $id);
941         $this->find('css', $selector, $exception);
942     }
944     /**
945      * Throws an exception if the course with the matching idnumber is "visible" in the management UI.
946      *
947      * @Given /^course in management listing should be dimmed "(?P<idnumber>[^"]*)"$/
948      * @param string $idnumber
949      */
950     public function course_in_management_listing_should_be_dimmed($idnumber) {
951         $id = $this->get_course_id($idnumber);
952         $exception = new ExpectationException('The course '.$idnumber.' is visible.', $this->getSession());
953         $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible="0"]', $id);
954         $this->find('css', $selector, $exception);
955     }
957     /**
958      * Toggles the visibility of a course in the management UI.
959      *
960      * If it was visible it will be hidden. If it is hidden it will be made visible.
961      *
962      * @Given /^I toggle visibility of course "(?P<idnumber>[^"]*)" in management listing$/
963      * @param string $idnumber
964      */
965     public function i_toggle_visibility_of_course_in_management_listing($idnumber) {
966         $id = $this->get_course_id($idnumber);
967         $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible]', $id);
968         $node = $this->find('css', $selector);
969         $exception = new ExpectationException('Course listing "' . $idnumber . '" does not contain a show or hide toggle.', $this->getSession());
970         if ($node->getAttribute('data-visible') === '1') {
971             $toggle = $this->find('css', '.action-hide', $exception, $node);
972         } else {
973             $toggle = $this->find('css', '.action-show', $exception, $node);
974         }
975         $toggle->click();
976     }
978     /**
979      * Toggles the visibility of a category in the management UI.
980      *
981      * If it was visible it will be hidden. If it is hidden it will be made visible.
982      *
983      * @Given /^I toggle visibility of category "(?P<idnumber>[^"]*)" in management listing$/
984      */
985     public function i_toggle_visibility_of_category_in_management_listing($idnumber) {
986         $id = $this->get_category_id($idnumber);
987         $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible]', $id);
988         $node = $this->find('css', $selector);
989         $exception = new ExpectationException('Category listing "' . $idnumber . '" does not contain a show or hide toggle.', $this->getSession());
990         if ($node->getAttribute('data-visible') === '1') {
991             $toggle = $this->find('css', '.action-hide', $exception, $node);
992         } else {
993             $toggle = $this->find('css', '.action-show', $exception, $node);
994         }
995         $toggle->click();
996     }
998     /**
999      * @Given /^I click to move (?P<listing>category|course) "(?P<idnumber>[^"]*)" (?P<direction>up|down) one(?P<nohighlight> without highlight)?$/
1000      * @param $listing
1001      * @param $idnumber
1002      * @param $direction
1003      */
1004     public function i_click_to_move_listing_by_one($listing, $idnumber, $direction, $nohighlight = false) {
1005         $up = ($direction === 'up');
1006         if ($listing === 'category') {
1007             $node = $this->get_management_category_listing_node_by_idnumber($idnumber);
1008         } else {
1009             $node = $this->get_management_course_listing_node_by_idnumber($idnumber);
1010         }
1011         if ($up) {
1012             $exception = new ExpectationException($listing.' listing "' . $idnumber . '" does not contain a moveup button.', $this->getSession());
1013             $button = $this->find('css', 'a.action-moveup', $exception, $node);
1014         } else {
1015             $exception = new ExpectationException($listing.' listing "' . $idnumber . '" does not contain a movedown button.', $this->getSession());
1016             $button = $this->find('css', 'a.action-movedown', $exception, $node);
1017         }
1018         $button->click();
1019         if ($this->running_javascript() && empty($nohighlight)) {
1020             $listitem = $node->getParent();
1021             $exception = new ExpectationException('Nothing was highlighted, ajax didn\'t occur or didn\'t succeed.', $this->getSession());
1022             $this->spin(array($this, 'listing_is_highlighted'), $listitem->getTagName().'#'.$listitem->getAttribute('id'), 2, $exception, true);
1023         }
1024     }
1026     /**
1027      * @param \Behat\Mink\Element\NodeElement $listitem
1028      * @return mixed
1029      */
1030     protected function listing_is_highlighted($self, $selector) {
1031         $listitem = $this->find('css', $selector);
1032         return $listitem->hasClass('highlight');
1033     }
1035     /**
1036      * Confirms that listings appear in a specific order.
1037      *
1038      * @Given /^I should see (?P<listing>category|course) listing "(?P<before>[^"]*)" before "(?P<after>[^"]*)"$/
1039      * @param string $listing Is either category or course
1040      * @param string $before The name of the before listitem.
1041      * @string string $after The name of the after listitem.
1042      */
1043     public function i_should_see_listing_before($listing, $before, $after) {
1044         $xpath = "//div[@id='{$listing}-listing']//li[contains(concat(' ', @class, ' '), ' listitem-{$listing} ')]//a[text()='{$before}']/ancestor::li[@data-id]//following::a[text()='{$after}']";
1045         $msg = "{$before} {$listing} does not appear before {$after} {$listing}";
1046         if (!$this->getSession()->getDriver()->find($xpath)) {
1047             throw new ExpectationException($msg, $this->getSession());
1048         }
1049     }
1051     /**
1052      * Returns an array of checks to be performed to make sure we are on the management page with the expected components.
1053      *
1054      * @Given /^I should see the "(?P<mode>[^"]*)" management page(?P<withcourse> with a course selected)?$/
1055      * @param string $mode
1056      * @param bool $withcourse
1057      * @return Given[]
1058      */
1059     public function i_should_see_the_courses_management_page($mode, $withcourse = false) {
1060         $return = array(
1061             new Given('I should see "Course and category management" in the "h2" "css_element"')
1062         );
1063         switch ($mode) {
1064             case "Courses":
1065                 $return[] = new Given('"#category-listing" "css_element" should not exists');
1066                 $return[] = new Given('"#course-listing" "css_element" should exists');
1067                 break;
1068             case "Course categories":
1069                 $return[] = new Given('"#category-listing" "css_element" should exists');
1070                 $return[] = new Given('"#course-listing" "css_element" should not exists');
1071                 break;
1072             case "Courses categories and courses":
1073             default:
1074                 $return[] = new Given('"#category-listing" "css_element" should exists');
1075                 $return[] = new Given('"#course-listing" "css_element" should exists');
1076                 break;
1077         }
1078         if (!empty($withcourse)) {
1079             $return[] = new Given('"#course-detail" "css_element" should exists');
1080         } else {
1081             $return[] = new Given('"#course-detail" "css_element" should not exists');
1082         }
1083         return $return;
1084     }
1086     /**
1087      * @Given /^I click on "(?P<action>[^"]*)" action for "(?P<name>[^"]*)" in management (?P<listing>course|category) listing$/
1088      */
1089     public function i_click_on_action_for_item_in_management_course_listing($action, $name, $listing) {
1090         if ($listing === 'category') {
1091             $node = $this->get_management_category_listing_node_by_name($name);
1092         } else {
1093             $node = $this->get_management_course_listing_node_by_name($name);
1094             $listing = 'course';
1095         }
1096         $actionsnode = $node->find('xpath', "//*[contains(concat(' ', normalize-space(@class), ' '), '{$listing}-item-actions')]");
1097         if (!$actionsnode) {
1098             throw new ExpectationException("Could not find the actions for $listing $name", $this->getSession());
1099         }
1100         $actionnode = $actionsnode->find('css', '.action-'.$action);
1101         if ($actionnode === null && $this->running_javascript()) {
1102             $actionsnode->find('css', 'a.toggle-display')->click();
1103             if ($actionnode) {
1104                 $actionnode = $node->find('css', '.action-'.$action);
1105             }
1106         }
1107         if (!$actionnode) {
1108             throw new ExpectationException("Expected action was not available or not found ($action)", $this->getSession());
1109         }
1110         $actionnode->click();
1111     }