2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Behat course-related steps definitions.
20 * @package core_course
22 * @copyright 2012 David Monllaó
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
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;
37 * Course-related steps definitions.
39 * @package core_course
41 * @copyright 2012 David Monllaó
42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44 class behat_course extends behat_base {
47 * Turns editing mode on.
48 * @Given /^I turn editing mode on$/
50 public function i_turn_editing_mode_on() {
51 return new Given('I press "' . get_string('turneditingon') . '"');
55 * Turns editing mode off.
56 * @Given /^I turn editing mode off$/
58 public function i_turn_editing_mode_off() {
59 return new Given('I press "' . get_string('turneditingoff') . '"');
63 * Creates a new course with the provided table data matching course settings names with the desired values.
65 * @Given /^I create a course with:$/
66 * @param TableNode $table The course data
69 public function i_create_a_course_with(TableNode $table) {
71 new Given('I go to the courses management page'),
72 new Given('I should see the "'.get_string('categories').'" management page'),
73 new Given('I click on category "'.get_string('miscellaneous').'" in the management interface'),
74 new Given('I should see the "'.get_string('categoriesandcoures').'" management page'),
75 new Given('I click on "'.get_string('newcourse').'" "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') . '"')
82 * Goes to the system courses/categories management page.
84 * @Given /^I go to the courses management page$/
87 public function i_go_to_the_courses_management_page() {
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') . '"'),
98 * Adds the selected activity/resource filling the form data with the specified field/value pairs.
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
106 public function i_add_to_section_and_i_fill_the_form_with($activity, $section, TableNode $data) {
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') . '"')
116 * Opens the activity chooser and opens the activity/resource form page.
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
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();
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);
154 $gobuttonxpath = $selectxpath . "/ancestor::form/descendant::input[@type='submit']";
155 $gobutton = $this->find('xpath', $gobuttonxpath);
162 * Turns course section highlighting on.
164 * @Given /^I turn section "(?P<section_number>\d+)" highlighting on$/
165 * @param int $sectionnumber The section number
168 public function i_turn_section_highlighting_on($sectionnumber) {
170 // Ensures the section exists.
171 $xpath = $this->section_exists($sectionnumber);
174 new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'),
175 new Given('I wait "2" seconds')
180 * Turns course section highlighting off.
182 * @Given /^I turn section "(?P<section_number>\d+)" highlighting off$/
183 * @param int $sectionnumber The section number
186 public function i_turn_section_highlighting_off($sectionnumber) {
188 // Ensures the section exists.
189 $xpath = $this->section_exists($sectionnumber);
192 new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'),
193 new Given('I wait "2" seconds')
198 * Shows the specified hidden section. You need to be in the course page and on editing mode.
200 * @Given /^I show section "(?P<section_number>\d+)"$/
201 * @param int $sectionnumber
203 public function i_show_section($sectionnumber) {
204 $showlink = $this->show_section_icon_exists($sectionnumber);
208 if ($this->running_javascript()) {
209 $this->getSession()->wait(5000, false);
214 * Hides the specified visible section. You need to be in the course page and on editing mode.
216 * @Given /^I hide section "(?P<section_number>\d+)"$/
217 * @param int $sectionnumber
219 public function i_hide_section($sectionnumber) {
220 $hidelink = $this->hide_section_icon_exists($sectionnumber);
224 if ($this->running_javascript()) {
225 $this->getSession()->wait(5000, false);
230 * Checks if the specified course section hightlighting is turned on. You need to be in the course page on editing mode.
232 * @Then /^section "(?P<section_number>\d+)" should be highlighted$/
233 * @throws ExpectationException
234 * @param int $sectionnumber The section number
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);
248 * Checks if the specified course section highlighting is turned off. You need to be in the course page on editing mode.
250 * @Then /^section "(?P<section_number>\d+)" should not be highlighted$/
251 * @throws ExpectationException
252 * @param int $sectionnumber The section number
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.
258 $this->section_should_be_highlighted($sectionnumber);
259 } catch (ExpectationException $e) {
260 // ExpectedException means that it is not highlighted.
264 throw new ExpectationException('The "' . $sectionnumber . '" section is highlighted', $this->getSession());
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.
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
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) {
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();
307 // We ensure that we still see the show icon.
308 $visibilityiconnode = $this->find('css', 'a.editing_show img', $visibilityexception, $activity);
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());
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.
323 * @Then /^section "(?P<section_number>\d+)" should be visible$/
324 * @throws ExpectationException
325 * @param int $sectionnumber
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());
337 // Hide section button should be visible.
338 if ($this->is_course_editor()) {
339 $this->hide_section_icon_exists($sectionnumber);
344 * Moves up the specified section, this step only works with Javascript disabled. Editing mode should be on.
346 * @Given /^I move up section "(?P<section_number>\d+)"$/
347 * @throws DriverException Step not available when Javascript is enabled
348 * @param int $sectionnumber
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');
356 // Ensures the section exists.
357 $sectionxpath = $this->section_exists($sectionnumber);
360 $moveuplink = $this->get_node_in_container('link', get_string('moveup'), 'xpath_element', $sectionxpath);
361 $moveuplink->click();
365 * Moves down the specified section, this step only works with Javascript disabled. Editing mode should be on.
367 * @Given /^I move down section "(?P<section_number>\d+)"$/
368 * @throws DriverException Step not available when Javascript is enabled
369 * @param int $sectionnumber
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');
377 // Ensures the section exists.
378 $sectionxpath = $this->section_exists($sectionnumber);
381 $movedownlink = $this->get_node_in_container('link', get_string('movedown'), 'xpath_element', $sectionxpath);
382 $movedownlink->click();
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.
388 * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" activity should be visible$/
389 * @param string $activityname
390 * @throws ExpectationException
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.
401 $this->find('css', 'a.dimmed', false, $activitynode);
402 throw new ExpectationException('"' . $activityname . '" is hidden', $this->getSession());
403 } catch (ElementNotFoundException $e) {
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);
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.
416 * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" activity should be hidden$/
417 * @param string $activityname
418 * @throws ExpectationException
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);
428 $exception = new ExpectationException('"' . $activityname . '" is not dimmed', $this->getSession());
429 $this->find('css', 'a.dimmed', $exception, $activitynode);
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);
437 // It should not exists at all.
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.
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.
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
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);
464 if ($this->running_javascript()) {
466 $destinationxpath = $sectionxpath . "/descendant::ul[contains(concat(' ', normalize-space(@class), ' '), ' yui3-dd-drop ')]";
469 new Given('I drag "' . $this->escape($activitynode->getXpath()) . '" "xpath_element" ' .
470 'and I drop it in "' . $this->escape($destinationxpath) . '" "xpath_element"'),
474 // Following links with no-JS.
476 // Moving to the fist spot of the section (before all other section's activities).
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"'),
485 * Edits the activity name through the edit activity; this step only works with Javascript enabled. Editing mode should be on.
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
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');
499 // Adding chr(10) to save changes.
500 $activity = $this->escape($activityname);
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')
510 * Indents to the right the activity or resource specified by it's name. Editing mode should be on.
512 * @Given /^I indent right "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
513 * @param string $activityname
516 public function i_indent_right_activity($activityname) {
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');
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');
533 * Indents to the left the activity or resource specified by it's name. Editing mode should be on.
535 * @Given /^I indent left "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
536 * @param string $activityname
539 public function i_indent_left_activity($activityname) {
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');
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');
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.
559 * @Given /^I delete "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
560 * @param string $activityname
563 public function i_delete_activity($activityname) {
565 $deletestring = get_string('delete');
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);
575 $this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
577 $this->getSession()->wait(2 * 1000, false);
583 new Given('I click on "' . $this->escape($deletestring) . '" "link" in the "' . $this->escape($activityname) . '" activity'),
584 new Given('I press "' . get_string('yes') . '"')
592 * Duplicates the activity or resource specified by it's name. You should be in the course page with editing mode on.
594 * @Given /^I duplicate "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
595 * @param string $activityname
598 public function i_duplicate_activity($activityname) {
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');
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') .'"');
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.
613 * @Given /^I duplicate "(?P<activity_name_string>(?:[^"]|\\")*)" activity editing the new copy with:$/
614 * @param string $activityname
615 * @param TableNode $data
618 public function i_duplicate_activity_editing_the_new_copy_with($activityname, TableNode $data) {
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');
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') . '"');
633 * Clicks on the specified element of the activity. You should be in the course page with editing mode turned on.
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
640 public function i_click_on_in_the_activity($element, $selectortype, $activityname) {
641 $element = $this->get_activity_element($element, $selectortype, $activityname);
646 * Clicks on the specified element inside the activity container.
648 * @throws ElementNotFoundException
649 * @param string $element
650 * @param string $selectortype
651 * @param string $activityname
652 * @return NodeElement
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);
665 * Checks if the course section exists.
667 * @throws ElementNotFoundException Thrown by behat_base::find
668 * @param int $sectionnumber
669 * @return string The xpath of the section.
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);
682 * Returns the show section icon or throws an exception.
684 * @throws ElementNotFoundException Thrown by behat_base::find
685 * @param int $sectionnumber
686 * @return NodeElement
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);
709 * Returns the hide section icon link if it exists or throws exception.
711 * @throws ElementNotFoundException Thrown by behat_base::find
712 * @param int $sectionnumber
713 * @return NodeElement
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);
736 * Gets the current course format.
738 * @throws ExpectationException If we are not in the course view page.
739 * @return string The course format in a frankenstyled name.
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');
751 if (!$bodyid = $node->getAttribute('id')) {
755 if (strstr($bodyid, 'page-course-view-') === false) {
759 return 'format_' . str_replace('page-course-view-', '', $bodyid);
763 * Gets the section's activites DOM nodes.
765 * @param string $sectionxpath
766 * @return array NodeElement instances
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.
774 $activities = $this->find_all('xpath', $xpath);
775 } catch (ElementNotFoundException $e) {
783 * Returns the DOM node of the activity from <li>.
785 * @throws ElementNotFoundException Thrown by behat_base::find
786 * @param string $activityname The activity name
787 * @return NodeElement
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);
798 * Returns whether the user can edit the course contents or not.
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'))) {
814 * Returns the id of the category with the given idnumber.
816 * Please note that this function requires the category to exist. If it does not exist an ExpectationException is thrown.
818 * @param string $idnumber
820 * @throws ExpectationException
822 protected function get_category_id($idnumber) {
825 return $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber), MUST_EXIST);
826 } catch (dml_missing_record_exception $ex) {
827 throw new ExpectationException(sprintf("There is no category in the database with the idnumber '%s'", $idnumber));
832 * Returns the id of the course with the given idnumber.
834 * Please note that this function requires the category to exist. If it does not exist an ExpectationException is thrown.
836 * @param string $idnumber
838 * @throws ExpectationException
840 protected function get_course_id($idnumber) {
843 return $DB->get_field('course', 'id', array('idnumber' => $idnumber), MUST_EXIST);
844 } catch (dml_missing_record_exception $ex) {
845 throw new ExpectationException(sprintf("There is no course in the database with the idnumber '%s'", $idnumber));
850 * Returns the category node from within the listing on the management page.
852 * @param string $idnumber
853 * @return \Behat\Mink\Element\NodeElement
855 protected function get_management_category_listing_node_by_idnumber($idnumber) {
856 $id = $this->get_category_id($idnumber);
857 $selector = sprintf('#category-listing .listitem-category[data-id="%d"] > div', $id);
858 return $this->find('css', $selector);
862 * Returns a category node from within the management interface.
864 * @param string $name The name of the category.
865 * @param bool $link If set to true we'll resolve to the link rather than just the node.
866 * @return \Behat\Mink\Element\NodeElement
868 protected function get_management_category_listing_node_by_name($name, $link = false) {
869 $selector = "//div[@id='category-listing']//li[contains(concat(' ', normalize-space(@class), ' '), ' listitem-category ')]//a[text()='{$name}']";
870 if ($link === false) {
871 $selector .= "/ancestor::li[@data-id]";
873 return $this->find('xpath', $selector);
877 * Returns a course node from within the management interface.
879 * @param string $name The name of the course.
880 * @param bool $link If set to true we'll resolve to the link rather than just the node.
881 * @return \Behat\Mink\Element\NodeElement
883 protected function get_management_course_listing_node_by_name($name, $link = false) {
884 $selector = "//div[@id='course-listing']//li[contains(concat(' ', @class, ' '), ' listitem-course ')]//a[text()='{$name}']";
885 if ($link === false) {
886 $selector .= "/ancestor::li[@data-id]";
888 return $this->find('xpath', $selector);
892 * Returns the course node from within the listing on the management page.
894 * @param string $idnumber
895 * @return \Behat\Mink\Element\NodeElement
897 protected function get_management_course_listing_node_by_idnumber($idnumber) {
898 $id = $this->get_course_id($idnumber);
899 $selector = sprintf('#course-listing .listitem-course[data-id="%d"] > div', $id);
900 return $this->find('css', $selector);
904 * Clicks on a category in the management interface.
906 * @Given /^I click on category "(?P<name>[^"]*)" in the management interface$/
907 * @param string $name
909 public function i_click_on_category_in_the_management_interface($name) {
910 $node = $this->get_management_category_listing_node_by_name($name, true);
915 * Clicks on a course in the management interface.
917 * @Given /^I click on course "(?P<name>[^"]*)" in the management interface$/
918 * @param string $name
920 public function i_click_on_course_in_the_management_interface($name) {
921 $node = $this->get_management_course_listing_node_by_name($name, true);
926 * Click to expand a category revealing its sub categories within the management UI.
928 * @Given /^I click to expand category "(?P<idnumber>[^"]*)" in the management interface$/
929 * @param string $idnumber
931 public function i_click_to_expand_category_in_the_management_interface($idnumber) {
932 $categorynode = $this->get_management_category_listing_node_by_idnumber($idnumber);
933 $exception = new ExpectationException('Category "' . $idnumber . '" does not contain an expand or collapse toggle.', $this->getSession());
934 $togglenode = $this->find('css', 'a[data-action=collapse],a[data-action=expand]', $exception, $categorynode);
935 $togglenode->click();
939 * Checks that a category within the management interface is visible.
941 * @Given /^category in management listing should be visible "(?P<idnumber>[^"]*)"$/
942 * @param string $idnumber
944 public function category_in_management_listing_should_be_visible($idnumber) {
945 $id = $this->get_category_id($idnumber);
946 $exception = new ExpectationException('The category '.$idnumber.' is not visible.', $this->getSession());
947 $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible="1"]', $id);
948 $this->find('css', $selector, $exception);
952 * Checks that a category within the management interface is dimmed.
954 * @Given /^category in management listing should be dimmed "(?P<idnumber>[^"]*)"$/
955 * @param string $idnumber
957 public function category_in_management_listing_should_be_dimmed($idnumber) {
958 $id = $this->get_category_id($idnumber);
959 $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible="0"]', $id);
960 $exception = new ExpectationException('The category '.$idnumber.' is visible.', $this->getSession());
961 $this->find('css', $selector, $exception);
965 * Checks that a course within the management interface is visible.
967 * @Given /^course in management listing should be visible "(?P<idnumber>[^"]*)"$/
968 * @param string $idnumber
970 public function course_in_management_listing_should_be_visible($idnumber) {
971 $id = $this->get_course_id($idnumber);
972 $exception = new ExpectationException('The course '.$idnumber.' is not visible.', $this->getSession());
973 $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible="1"]', $id);
974 $this->find('css', $selector, $exception);
978 * Checks that a course within the management interface is dimmed.
980 * @Given /^course in management listing should be dimmed "(?P<idnumber>[^"]*)"$/
981 * @param string $idnumber
983 public function course_in_management_listing_should_be_dimmed($idnumber) {
984 $id = $this->get_course_id($idnumber);
985 $exception = new ExpectationException('The course '.$idnumber.' is visible.', $this->getSession());
986 $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible="0"]', $id);
987 $this->find('css', $selector, $exception);
991 * Toggles the visibility of a course in the management UI.
993 * If it was visible it will be hidden. If it is hidden it will be made visible.
995 * @Given /^I toggle visibility of course "(?P<idnumber>[^"]*)" in management listing$/
996 * @param string $idnumber
998 public function i_toggle_visibility_of_course_in_management_listing($idnumber) {
999 $id = $this->get_course_id($idnumber);
1000 $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible]', $id);
1001 $node = $this->find('css', $selector);
1002 $exception = new ExpectationException('Course listing "' . $idnumber . '" does not contain a show or hide toggle.', $this->getSession());
1003 if ($node->getAttribute('data-visible') === '1') {
1004 $toggle = $this->find('css', '.action-hide', $exception, $node);
1006 $toggle = $this->find('css', '.action-show', $exception, $node);
1012 * Toggles the visibility of a category in the management UI.
1014 * If it was visible it will be hidden. If it is hidden it will be made visible.
1016 * @Given /^I toggle visibility of category "(?P<idnumber>[^"]*)" in management listing$/
1018 public function i_toggle_visibility_of_category_in_management_listing($idnumber) {
1019 $id = $this->get_category_id($idnumber);
1020 $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible]', $id);
1021 $node = $this->find('css', $selector);
1022 $exception = new ExpectationException('Category listing "' . $idnumber . '" does not contain a show or hide toggle.', $this->getSession());
1023 if ($node->getAttribute('data-visible') === '1') {
1024 $toggle = $this->find('css', '.action-hide', $exception, $node);
1026 $toggle = $this->find('css', '.action-show', $exception, $node);
1032 * Moves a category displayed in the management interface up or down one place.
1034 * @Given /^I click to move category "(?P<idnumber>[^"]*)" (?P<direction>up|down) one$/
1036 * @param string $idnumber The category idnumber
1037 * @param string $direction The direction to move in, either up or down
1039 public function i_click_to_move_category_by_one($idnumber, $direction) {
1040 $node = $this->get_management_category_listing_node_by_idnumber($idnumber);
1041 $this->user_moves_listing_by_one('category', $node, $direction);
1045 * Moves a course displayed in the management interface up or down one place.
1047 * @Given /^I click to move course "(?P<idnumber>[^"]*)" (?P<direction>up|down) one$/
1049 * @param string $idnumber The course idnumber
1050 * @param string $direction The direction to move in, either up or down
1052 public function i_click_to_move_course_by_one($idnumber, $direction) {
1053 $node = $this->get_management_course_listing_node_by_idnumber($idnumber);
1054 $this->user_moves_listing_by_one('course', $node, $direction);
1058 * Moves a course or category listing within the management interface up or down by one.
1060 * @param string $listingtype One of course or category
1061 * @param \Behat\Mink\Element\NodeElement $listingnode
1062 * @param string $direction One of up or down.
1063 * @param bool $highlight If set to false we don't check the node has been highlighted.
1065 protected function user_moves_listing_by_one($listingtype, $listingnode, $direction, $highlight = true) {
1066 $up = (strtolower($direction) === 'up');
1068 $exception = new ExpectationException($listingtype.' listing does not contain a moveup button.', $this->getSession());
1069 $button = $this->find('css', 'a.action-moveup', $exception, $listingnode);
1071 $exception = new ExpectationException($listingtype.' listing does not contain a movedown button.', $this->getSession());
1072 $button = $this->find('css', 'a.action-movedown', $exception, $listingnode);
1075 if ($this->running_javascript() && $highlight) {
1076 $listitem = $listingnode->getParent();
1077 $exception = new ExpectationException('Nothing was highlighted, ajax didn\'t occur or didn\'t succeed.', $this->getSession());
1078 $this->spin(array($this, 'listing_is_highlighted'), $listitem->getTagName().'#'.$listitem->getAttribute('id'), 2, $exception, true);
1083 * Used by spin to determine the callback has been highlighted.
1085 * @param behat_course $self A self reference (default first arg from a spin callback)
1086 * @param \Behat\Mink\Element\NodeElement $selector
1089 protected function listing_is_highlighted($self, $selector) {
1090 $listitem = $this->find('css', $selector);
1091 return $listitem->hasClass('highlight');
1095 * Check that one course appears before another in the course category management listings.
1097 * @Given /^I should see course listing "(?P<preceedingcourse>[^"]*)" before "(?P<followingcourse>[^"]*)"$/
1099 * @param string $preceedingcourse The first course to find
1100 * @param string $followingcourse The second course to find (should be AFTER the first course)
1101 * @throws ExpectationException
1103 public function i_should_see_course_listing_before($preceedingcourse, $followingcourse) {
1104 $xpath = "//div[@id='course-listing']//li[contains(concat(' ', @class, ' '), ' listitem-course ')]//a[text()='{$preceedingcourse}']/ancestor::li[@data-id]//following::a[text()='{$followingcourse}']";
1105 $msg = "{$preceedingcourse} course does not appear before {$followingcourse} course";
1106 if (!$this->getSession()->getDriver()->find($xpath)) {
1107 throw new ExpectationException($msg, $this->getSession());
1112 * Check that one category appears before another in the course category management listings.
1114 * @Given /^I should see category listing "(?P<preceedingcategory>[^"]*)" before "(?P<followingcategory>[^"]*)"$/
1116 * @param string $preceedingcategory The first category to find
1117 * @param string $followingcategory The second category to find (should be after the first category)
1118 * @throws ExpectationException
1120 public function i_should_see_category_listing_before($preceedingcategory, $followingcategory) {
1121 $xpath = "//div[@id='category-listing']//li[contains(concat(' ', @class, ' '), ' listitem-category ')]//a[text()='{$preceedingcategory}']/ancestor::li[@data-id]//following::a[text()='{$followingcategory}']";
1122 $msg = "{$preceedingcategory} category does not appear before {$followingcategory} category";
1123 if (!$this->getSession()->getDriver()->find($xpath)) {
1124 throw new ExpectationException($msg, $this->getSession());
1129 * Checks that we are on the course management page that we expect to be on and that no course has been selected.
1131 * @Given /^I should see the "(?P<mode>[^"]*)" management page$/
1132 * @param string $mode The mode to expected. One of 'Courses', 'Course categories' or 'Course categories and courses'
1135 public function i_should_see_the_courses_management_page($mode) {
1137 new Given('I should see "Course and category management" in the "h2" "css_element"')
1141 $return[] = new Given('"#category-listing" "css_element" should not exists');
1142 $return[] = new Given('"#course-listing" "css_element" should exists');
1144 case "Course categories":
1145 $return[] = new Given('"#category-listing" "css_element" should exists');
1146 $return[] = new Given('"#course-listing" "css_element" should not exists');
1148 case "Courses categories and courses":
1150 $return[] = new Given('"#category-listing" "css_element" should exists');
1151 $return[] = new Given('"#course-listing" "css_element" should exists');
1154 $return[] = new Given('"#course-detail" "css_element" should not exists');
1159 * Checks that we are on the course management page that we expect to be on and that a course has been selected.
1161 * @Given /^I should see the "(?P<mode>[^"]*)" management page with a course selected$/
1162 * @param string $mode The mode to expected. One of 'Courses', 'Course categories' or 'Course categories and courses'
1165 public function i_should_see_the_courses_management_page_with_a_course_selected($mode) {
1166 $return = $this->i_should_see_the_courses_management_page($mode);
1168 $return[] = new Given('"#course-detail" "css_element" should exists');
1173 * Locates a course in the course category management interface and then triggers an action for it.
1175 * @Given /^I click on "(?P<action>[^"]*)" action for "(?P<name>[^"]*)" in management course listing$/
1177 * @param string $action The action to take. One of
1178 * @param string $name The name of the course as it is displayed in the management interface.
1180 public function i_click_on_action_for_item_in_management_course_listing($action, $name) {
1181 $node = $this->get_management_course_listing_node_by_name($name);
1182 $this->user_clicks_on_management_listing_action('course', $node, $action);
1186 * Locates a category in the course category management interface and then triggers an action for it.
1188 * @Given /^I click on "(?P<action>[^"]*)" action for "(?P<name>[^"]*)" in management category listing$/
1190 * @param string $action The action to take. One of
1191 * @param string $name The name of the category as it is displayed in the management interface.
1193 public function i_click_on_action_for_item_in_management_category_listing($action, $name) {
1194 $node = $this->get_management_category_listing_node_by_name($name);
1195 $this->user_clicks_on_management_listing_action('category', $node, $action);
1199 * Finds the node to use for a management listitem action and clicks it.
1201 * @param string $listingtype Either course or category.
1202 * @param \Behat\Mink\Element\NodeElement $listingnode
1203 * @param string $action The action being taken
1204 * @throws Behat\Mink\Exception\ExpectationException
1206 protected function user_clicks_on_management_listing_action($listingtype, $listingnode, $action) {
1207 $actionsnode = $listingnode->find('xpath', "//*[contains(concat(' ', normalize-space(@class), ' '), '{$listingtype}-item-actions')]");
1208 if (!$actionsnode) {
1209 throw new ExpectationException("Could not find the actions for $listingtype", $this->getSession());
1211 $actionnode = $actionsnode->find('css', '.action-'.$action);
1212 if ($actionnode === null && $this->running_javascript()) {
1213 $actionsnode->find('css', 'a.toggle-display')->click();
1215 $actionnode = $listingnode->find('css', '.action-'.$action);
1219 throw new ExpectationException("Expected action was not available or not found ($action)", $this->getSession());
1221 $actionnode->click();