Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]

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