Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 9 May 2022 (12 months).
  • Bug fixes for security issues in 3.11.x will end 14 November 2022 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • Differences Between: [Versions 310 and 311] [Versions 35 and 311] [Versions 36 and 311] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

       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 id of the category with the given idnumber.
    1351       *
    1352       * Please note that this function requires the category to exist. If it does not exist an ExpectationException is thrown.
    1353       *
    1354       * @param string $idnumber
    1355       * @return string
    1356       * @throws ExpectationException
    1357       */
    1358      protected function get_category_id($idnumber) {
    1359          global $DB;
    1360          try {
    1361              return $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber), MUST_EXIST);
    1362          } catch (dml_missing_record_exception $ex) {
    1363              throw new ExpectationException(sprintf("There is no category in the database with the idnumber '%s'", $idnumber),
    1364                  $this->getSession());
    1365          }
    1366      }
    1367  
    1368      /**
    1369       * Returns the id of the course with the given idnumber.
    1370       *
    1371       * Please note that this function requires the category to exist. If it does not exist an ExpectationException is thrown.
    1372       *
    1373       * @param string $idnumber
    1374       * @return string
    1375       * @throws ExpectationException
    1376       */
    1377      protected function get_course_id($idnumber) {
    1378          global $DB;
    1379          try {
    1380              return $DB->get_field('course', 'id', array('idnumber' => $idnumber), MUST_EXIST);
    1381          } catch (dml_missing_record_exception $ex) {
    1382              throw new ExpectationException(sprintf("There is no course in the database with the idnumber '%s'", $idnumber),
    1383                  $this->getSession());
    1384          }
    1385      }
    1386  
    1387      /**
    1388       * Returns the category node from within the listing on the management page.
    1389       *
    1390       * @param string $idnumber
    1391       * @return \Behat\Mink\Element\NodeElement
    1392       */
    1393      protected function get_management_category_listing_node_by_idnumber($idnumber) {
    1394          $id = $this->get_category_id($idnumber);
    1395          $selector = sprintf('#category-listing .listitem-category[data-id="%d"] > div', $id);
    1396          return $this->find('css', $selector);
    1397      }
    1398  
    1399      /**
    1400       * Returns a category node from within the management interface.
    1401       *
    1402       * @param string $name The name of the category.
    1403       * @param bool $link If set to true we'll resolve to the link rather than just the node.
    1404       * @return \Behat\Mink\Element\NodeElement
    1405       */
    1406      protected function get_management_category_listing_node_by_name($name, $link = false) {
    1407          $selector = "//div[@id='category-listing']//li[contains(concat(' ', normalize-space(@class), ' '), ' listitem-category ')]//a[text()='{$name}']";
    1408          if ($link === false) {
    1409              $selector .= "/ancestor::li[@data-id][1]";
    1410          }
    1411          return $this->find('xpath', $selector);
    1412      }
    1413  
    1414      /**
    1415       * Returns a course node from within the management interface.
    1416       *
    1417       * @param string $name The name of the course.
    1418       * @param bool $link If set to true we'll resolve to the link rather than just the node.
    1419       * @return \Behat\Mink\Element\NodeElement
    1420       */
    1421      protected function get_management_course_listing_node_by_name($name, $link = false) {
    1422          $selector = "//div[@id='course-listing']//li[contains(concat(' ', @class, ' '), ' listitem-course ')]//a[text()='{$name}']";
    1423          if ($link === false) {
    1424              $selector .= "/ancestor::li[@data-id]";
    1425          }
    1426          return $this->find('xpath', $selector);
    1427      }
    1428  
    1429      /**
    1430       * Returns the course node from within the listing on the management page.
    1431       *
    1432       * @param string $idnumber
    1433       * @return \Behat\Mink\Element\NodeElement
    1434       */
    1435      protected function get_management_course_listing_node_by_idnumber($idnumber) {
    1436          $id = $this->get_course_id($idnumber);
    1437          $selector = sprintf('#course-listing .listitem-course[data-id="%d"] > div', $id);
    1438          return $this->find('css', $selector);
    1439      }
    1440  
    1441      /**
    1442       * Clicks on a category in the management interface.
    1443       *
    1444       * @Given /^I click on category "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
    1445       * @param string $name
    1446       */
    1447      public function i_click_on_category_in_the_management_interface($name) {
    1448          $node = $this->get_management_category_listing_node_by_name($name, true);
    1449          $node->click();
    1450      }
    1451  
    1452      /**
    1453       * Clicks on a course in the management interface.
    1454       *
    1455       * @Given /^I click on course "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
    1456       * @param string $name
    1457       */
    1458      public function i_click_on_course_in_the_management_interface($name) {
    1459          $node = $this->get_management_course_listing_node_by_name($name, true);
    1460          $node->click();
    1461      }
    1462  
    1463      /**
    1464       * Clicks on a category checkbox in the management interface, if not checked.
    1465       *
    1466       * @Given /^I select category "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
    1467       * @param string $name
    1468       */
    1469      public function i_select_category_in_the_management_interface($name) {
    1470          $node = $this->get_management_category_listing_node_by_name($name);
    1471          $node = $node->findField('bcat[]');
    1472          if (!$node->isChecked()) {
    1473              $node->click();
    1474          }
    1475      }
    1476  
    1477      /**
    1478       * Clicks on a category checkbox in the management interface, if checked.
    1479       *
    1480       * @Given /^I unselect category "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
    1481       * @param string $name
    1482       */
    1483      public function i_unselect_category_in_the_management_interface($name) {
    1484          $node = $this->get_management_category_listing_node_by_name($name);
    1485          $node = $node->findField('bcat[]');
    1486          if ($node->isChecked()) {
    1487              $node->click();
    1488          }
    1489      }
    1490  
    1491      /**
    1492       * Clicks course checkbox in the management interface, if not checked.
    1493       *
    1494       * @Given /^I select course "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
    1495       * @param string $name
    1496       */
    1497      public function i_select_course_in_the_management_interface($name) {
    1498          $node = $this->get_management_course_listing_node_by_name($name);
    1499          $node = $node->findField('bc[]');
    1500          if (!$node->isChecked()) {
    1501              $node->click();
    1502          }
    1503      }
    1504  
    1505      /**
    1506       * Clicks course checkbox in the management interface, if checked.
    1507       *
    1508       * @Given /^I unselect course "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
    1509       * @param string $name
    1510       */
    1511      public function i_unselect_course_in_the_management_interface($name) {
    1512          $node = $this->get_management_course_listing_node_by_name($name);
    1513          $node = $node->findField('bc[]');
    1514          if ($node->isChecked()) {
    1515              $node->click();
    1516          }
    1517      }
    1518  
    1519      /**
    1520       * Move selected categories to top level in the management interface.
    1521       *
    1522       * @Given /^I move category "(?P<name_string>(?:[^"]|\\")*)" to top level in the management interface$/
    1523       * @param string $name
    1524       */
    1525      public function i_move_category_to_top_level_in_the_management_interface($name) {
    1526          $this->i_select_category_in_the_management_interface($name);
    1527  
    1528          $this->execute('behat_forms::i_set_the_field_to',
    1529              array('menumovecategoriesto', core_course_category::get(0)->get_formatted_name())
    1530          );
    1531  
    1532          // Save event.
    1533          $this->execute("behat_forms::press_button", "bulkmovecategories");
    1534      }
    1535  
    1536      /**
    1537       * Checks that a category is a subcategory of specific category.
    1538       *
    1539       * @Given /^I should see category "(?P<subcatidnumber_string>(?:[^"]|\\")*)" as subcategory of "(?P<catidnumber_string>(?:[^"]|\\")*)" in the management interface$/
    1540       * @throws ExpectationException
    1541       * @param string $subcatidnumber
    1542       * @param string $catidnumber
    1543       */
    1544      public function i_should_see_category_as_subcategory_of_in_the_management_interface($subcatidnumber, $catidnumber) {
    1545          $categorynodeid = $this->get_category_id($catidnumber);
    1546          $subcategoryid = $this->get_category_id($subcatidnumber);
    1547          $exception = new ExpectationException('The category '.$subcatidnumber.' is not a subcategory of '.$catidnumber, $this->getSession());
    1548          $selector = sprintf('#category-listing .listitem-category[data-id="%d"] .listitem-category[data-id="%d"]', $categorynodeid, $subcategoryid);
    1549          $this->find('css', $selector, $exception);
    1550      }
    1551  
    1552      /**
    1553       * Checks that a category is not a subcategory of specific category.
    1554       *
    1555       * @Given /^I should not see category "(?P<subcatidnumber_string>(?:[^"]|\\")*)" as subcategory of "(?P<catidnumber_string>(?:[^"]|\\")*)" in the management interface$/
    1556       * @throws ExpectationException
    1557       * @param string $subcatidnumber
    1558       * @param string $catidnumber
    1559       */
    1560      public function i_should_not_see_category_as_subcategory_of_in_the_management_interface($subcatidnumber, $catidnumber) {
    1561          try {
    1562              $this->i_should_see_category_as_subcategory_of_in_the_management_interface($subcatidnumber, $catidnumber);
    1563          } catch (ExpectationException $e) {
    1564              // ExpectedException means that it is not highlighted.
    1565              return;
    1566          }
    1567          throw new ExpectationException('The category '.$subcatidnumber.' is a subcategory of '.$catidnumber, $this->getSession());
    1568      }
    1569  
    1570      /**
    1571       * Click to expand a category revealing its sub categories within the management UI.
    1572       *
    1573       * @Given /^I click to expand category "(?P<idnumber_string>(?:[^"]|\\")*)" in the management interface$/
    1574       * @param string $idnumber
    1575       */
    1576      public function i_click_to_expand_category_in_the_management_interface($idnumber) {
    1577          $categorynode = $this->get_management_category_listing_node_by_idnumber($idnumber);
    1578          $exception = new ExpectationException('Category "' . $idnumber . '" does not contain an expand or collapse toggle.', $this->getSession());
    1579          $togglenode = $this->find('css', 'a[data-action=collapse],a[data-action=expand]', $exception, $categorynode);
    1580          $togglenode->click();
    1581      }
    1582  
    1583      /**
    1584       * Checks that a category within the management interface is visible.
    1585       *
    1586       * @Given /^category in management listing should be visible "(?P<idnumber_string>(?:[^"]|\\")*)"$/
    1587       * @param string $idnumber
    1588       */
    1589      public function category_in_management_listing_should_be_visible($idnumber) {
    1590          $id = $this->get_category_id($idnumber);
    1591          $exception = new ExpectationException('The category '.$idnumber.' is not visible.', $this->getSession());
    1592          $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible="1"]', $id);
    1593          $this->find('css', $selector, $exception);
    1594      }
    1595  
    1596      /**
    1597       * Checks that a category within the management interface is dimmed.
    1598       *
    1599       * @Given /^category in management listing should be dimmed "(?P<idnumber_string>(?:[^"]|\\")*)"$/
    1600       * @param string $idnumber
    1601       */
    1602      public function category_in_management_listing_should_be_dimmed($idnumber) {
    1603          $id = $this->get_category_id($idnumber);
    1604          $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible="0"]', $id);
    1605          $exception = new ExpectationException('The category '.$idnumber.' is visible.', $this->getSession());
    1606          $this->find('css', $selector, $exception);
    1607      }
    1608  
    1609      /**
    1610       * Checks that a course within the management interface is visible.
    1611       *
    1612       * @Given /^course in management listing should be visible "(?P<idnumber_string>(?:[^"]|\\")*)"$/
    1613       * @param string $idnumber
    1614       */
    1615      public function course_in_management_listing_should_be_visible($idnumber) {
    1616          $id = $this->get_course_id($idnumber);
    1617          $exception = new ExpectationException('The course '.$idnumber.' is not visible.', $this->getSession());
    1618          $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible="1"]', $id);
    1619          $this->find('css', $selector, $exception);
    1620      }
    1621  
    1622      /**
    1623       * Checks that a course within the management interface is dimmed.
    1624       *
    1625       * @Given /^course in management listing should be dimmed "(?P<idnumber_string>(?:[^"]|\\")*)"$/
    1626       * @param string $idnumber
    1627       */
    1628      public function course_in_management_listing_should_be_dimmed($idnumber) {
    1629          $id = $this->get_course_id($idnumber);
    1630          $exception = new ExpectationException('The course '.$idnumber.' is visible.', $this->getSession());
    1631          $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible="0"]', $id);
    1632          $this->find('css', $selector, $exception);
    1633      }
    1634  
    1635      /**
    1636       * Toggles the visibility of a course in the management UI.
    1637       *
    1638       * If it was visible it will be hidden. If it is hidden it will be made visible.
    1639       *
    1640       * @Given /^I toggle visibility of course "(?P<idnumber_string>(?:[^"]|\\")*)" in management listing$/
    1641       * @param string $idnumber
    1642       */
    1643      public function i_toggle_visibility_of_course_in_management_listing($idnumber) {
    1644          $id = $this->get_course_id($idnumber);
    1645          $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible]', $id);
    1646          $node = $this->find('css', $selector);
    1647          $exception = new ExpectationException('Course listing "' . $idnumber . '" does not contain a show or hide toggle.', $this->getSession());
    1648          if ($node->getAttribute('data-visible') === '1') {
    1649              $toggle = $this->find('css', '.action-hide', $exception, $node);
    1650          } else {
    1651              $toggle = $this->find('css', '.action-show', $exception, $node);
    1652          }
    1653          $toggle->click();
    1654      }
    1655  
    1656      /**
    1657       * Toggles the visibility of a category in the management UI.
    1658       *
    1659       * If it was visible it will be hidden. If it is hidden it will be made visible.
    1660       *
    1661       * @Given /^I toggle visibility of category "(?P<idnumber_string>(?:[^"]|\\")*)" in management listing$/
    1662       */
    1663      public function i_toggle_visibility_of_category_in_management_listing($idnumber) {
    1664          $id = $this->get_category_id($idnumber);
    1665          $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible]', $id);
    1666          $node = $this->find('css', $selector);
    1667          $exception = new ExpectationException('Category listing "' . $idnumber . '" does not contain a show or hide toggle.', $this->getSession());
    1668          if ($node->getAttribute('data-visible') === '1') {
    1669              $toggle = $this->find('css', '.action-hide', $exception, $node);
    1670          } else {
    1671              $toggle = $this->find('css', '.action-show', $exception, $node);
    1672          }
    1673          $toggle->click();
    1674      }
    1675  
    1676      /**
    1677       * Moves a category displayed in the management interface up or down one place.
    1678       *
    1679       * @Given /^I click to move category "(?P<idnumber_string>(?:[^"]|\\")*)" (?P<direction>up|down) one$/
    1680       *
    1681       * @param string $idnumber The category idnumber
    1682       * @param string $direction The direction to move in, either up or down
    1683       */
    1684      public function i_click_to_move_category_by_one($idnumber, $direction) {
    1685          $node = $this->get_management_category_listing_node_by_idnumber($idnumber);
    1686          $this->user_moves_listing_by_one('category', $node, $direction);
    1687      }
    1688  
    1689      /**
    1690       * Moves a course displayed in the management interface up or down one place.
    1691       *
    1692       * @Given /^I click to move course "(?P<idnumber_string>(?:[^"]|\\")*)" (?P<direction>up|down) one$/
    1693       *
    1694       * @param string $idnumber The course idnumber
    1695       * @param string $direction The direction to move in, either up or down
    1696       */
    1697      public function i_click_to_move_course_by_one($idnumber, $direction) {
    1698          $node = $this->get_management_course_listing_node_by_idnumber($idnumber);
    1699          $this->user_moves_listing_by_one('course', $node, $direction);
    1700      }
    1701  
    1702      /**
    1703       * Moves a course or category listing within the management interface up or down by one.
    1704       *
    1705       * @param string $listingtype One of course or category
    1706       * @param \Behat\Mink\Element\NodeElement $listingnode
    1707       * @param string $direction One of up or down.
    1708       * @param bool $highlight If set to false we don't check the node has been highlighted.
    1709       */
    1710      protected function user_moves_listing_by_one($listingtype, $listingnode, $direction, $highlight = true) {
    1711          $up = (strtolower($direction) === 'up');
    1712          if ($up) {
    1713              $exception = new ExpectationException($listingtype.' listing does not contain a moveup button.', $this->getSession());
    1714              $button = $this->find('css', 'a.action-moveup', $exception, $listingnode);
    1715          } else {
    1716              $exception = new ExpectationException($listingtype.' listing does not contain a movedown button.', $this->getSession());
    1717              $button = $this->find('css', 'a.action-movedown', $exception, $listingnode);
    1718          }
    1719          $button->click();
    1720          if ($this->running_javascript() && $highlight) {
    1721              $listitem = $listingnode->getParent();
    1722              $exception = new ExpectationException('Nothing was highlighted, ajax didn\'t occur or didn\'t succeed.', $this->getSession());
    1723              $this->spin(array($this, 'listing_is_highlighted'), $listitem->getTagName().'#'.$listitem->getAttribute('id'), 2, $exception, true);
    1724          }
    1725      }
    1726  
    1727      /**
    1728       * Used by spin to determine the callback has been highlighted.
    1729       *
    1730       * @param behat_course $self A self reference (default first arg from a spin callback)
    1731       * @param \Behat\Mink\Element\NodeElement $selector
    1732       * @return bool
    1733       */
    1734      protected function listing_is_highlighted($self, $selector) {
    1735          $listitem = $this->find('css', $selector);
    1736          return $listitem->hasClass('highlight');
    1737      }
    1738  
    1739      /**
    1740       * Check that one course appears before another in the course category management listings.
    1741       *
    1742       * @Given /^I should see course listing "(?P<preceedingcourse_string>(?:[^"]|\\")*)" before "(?P<followingcourse_string>(?:[^"]|\\")*)"$/
    1743       *
    1744       * @param string $preceedingcourse The first course to find
    1745       * @param string $followingcourse The second course to find (should be AFTER the first course)
    1746       * @throws ExpectationException
    1747       */
    1748      public function i_should_see_course_listing_before($preceedingcourse, $followingcourse) {
    1749          $xpath = "//div[@id='course-listing']//li[contains(concat(' ', @class, ' '), ' listitem-course ')]//a[text()='{$preceedingcourse}']/ancestor::li[@data-id]//following::a[text()='{$followingcourse}']";
    1750          $msg = "{$preceedingcourse} course does not appear before {$followingcourse} course";
    1751          if (!$this->getSession()->getDriver()->find($xpath)) {
    1752              throw new ExpectationException($msg, $this->getSession());
    1753          }
    1754      }
    1755  
    1756      /**
    1757       * Check that one category appears before another in the course category management listings.
    1758       *
    1759       * @Given /^I should see category listing "(?P<preceedingcategory_string>(?:[^"]|\\")*)" before "(?P<followingcategory_string>(?:[^"]|\\")*)"$/
    1760       *
    1761       * @param string $preceedingcategory The first category to find
    1762       * @param string $followingcategory The second category to find (should be after the first category)
    1763       * @throws ExpectationException
    1764       */
    1765      public function i_should_see_category_listing_before($preceedingcategory, $followingcategory) {
    1766          $xpath = "//div[@id='category-listing']//li[contains(concat(' ', @class, ' '), ' listitem-category ')]//a[text()='{$preceedingcategory}']/ancestor::li[@data-id]//following::a[text()='{$followingcategory}']";
    1767          $msg = "{$preceedingcategory} category does not appear before {$followingcategory} category";
    1768          if (!$this->getSession()->getDriver()->find($xpath)) {
    1769              throw new ExpectationException($msg, $this->getSession());
    1770          }
    1771      }
    1772  
    1773      /**
    1774       * Checks that we are on the course management page that we expect to be on and that no course has been selected.
    1775       *
    1776       * @Given /^I should see the "(?P<mode_string>(?:[^"]|\\")*)" management page$/
    1777       * @param string $mode The mode to expected. One of 'Courses', 'Course categories' or 'Course categories and courses'
    1778       */
    1779      public function i_should_see_the_courses_management_page($mode) {
    1780          $this->execute("behat_general::assert_element_contains_text",
    1781              array("Course and category management", "h2", "css_element")
    1782          );
    1783  
    1784          switch ($mode) {
    1785              case "Courses":
    1786                  $this->execute("behat_general::should_not_exist", array("#category-listing", "css_element"));
    1787                  $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
    1788                  break;
    1789  
    1790              case "Course categories":
    1791                  $this->execute("behat_general::should_exist", array("#category-listing", "css_element"));
    1792                  $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
    1793                  break;
    1794  
    1795              case "Courses categories and courses":
    1796              default:
    1797                  $this->execute("behat_general::should_exist", array("#category-listing", "css_element"));
    1798                  $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
    1799                  break;
    1800          }
    1801  
    1802          $this->execute("behat_general::should_not_exist", array("#course-detail", "css_element"));
    1803      }
    1804  
    1805      /**
    1806       * Checks that we are on the course management page that we expect to be on and that a course has been selected.
    1807       *
    1808       * @Given /^I should see the "(?P<mode_string>(?:[^"]|\\")*)" management page with a course selected$/
    1809       * @param string $mode The mode to expected. One of 'Courses', 'Course categories' or 'Course categories and courses'
    1810       */
    1811      public function i_should_see_the_courses_management_page_with_a_course_selected($mode) {
    1812          $this->execute("behat_general::assert_element_contains_text",
    1813              array("Course and category management", "h2", "css_element"));
    1814  
    1815          switch ($mode) {
    1816              case "Courses":
    1817                  $this->execute("behat_general::should_not_exist", array("#category-listing", "css_element"));
    1818                  $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
    1819                  break;
    1820  
    1821              case "Course categories":
    1822                  $this->execute("behat_general::should_exist", array("#category-listing", "css_element"));
    1823                  $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
    1824                  break;
    1825  
    1826              case "Courses categories and courses":
    1827              default:
    1828                  $this->execute("behat_general::should_exist", array("#category-listing", "css_element"));
    1829                  $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
    1830                  break;
    1831          }
    1832  
    1833          $this->execute("behat_general::should_exist", array("#course-detail", "css_element"));
    1834      }
    1835  
    1836      /**
    1837       * Locates a course in the course category management interface and then triggers an action for it.
    1838       *
    1839       * @Given /^I click on "(?P<action_string>(?:[^"]|\\")*)" action for "(?P<name_string>(?:[^"]|\\")*)" in management course listing$/
    1840       *
    1841       * @param string $action The action to take. One of
    1842       * @param string $name The name of the course as it is displayed in the management interface.
    1843       */
    1844      public function i_click_on_action_for_item_in_management_course_listing($action, $name) {
    1845          $node = $this->get_management_course_listing_node_by_name($name);
    1846          $this->user_clicks_on_management_listing_action('course', $node, $action);
    1847      }
    1848  
    1849      /**
    1850       * Locates a category in the course category management interface and then triggers an action for it.
    1851       *
    1852       * @Given /^I click on "(?P<action_string>(?:[^"]|\\")*)" action for "(?P<name_string>(?:[^"]|\\")*)" in management category listing$/
    1853       *
    1854       * @param string $action The action to take. One of
    1855       * @param string $name The name of the category as it is displayed in the management interface.
    1856       */
    1857      public function i_click_on_action_for_item_in_management_category_listing($action, $name) {
    1858          $node = $this->get_management_category_listing_node_by_name($name);
    1859          $this->user_clicks_on_management_listing_action('category', $node, $action);
    1860      }
    1861  
    1862      /**
    1863       * Clicks to expand or collapse a category displayed on the frontpage
    1864       *
    1865       * @Given /^I toggle "(?P<categoryname_string>(?:[^"]|\\")*)" category children visibility in frontpage$/
    1866       * @throws ExpectationException
    1867       * @param string $categoryname
    1868       */
    1869      public function i_toggle_category_children_visibility_in_frontpage($categoryname) {
    1870  
    1871          $headingtags = array();
    1872          for ($i = 1; $i <= 6; $i++) {
    1873              $headingtags[] = 'self::h' . $i;
    1874          }
    1875  
    1876          $exception = new ExpectationException('"' . $categoryname . '" category can not be found', $this->getSession());
    1877          $categoryliteral = behat_context_helper::escape($categoryname);
    1878          $xpath = "//div[@class='info']/descendant::*[" . implode(' or ', $headingtags) .
    1879              "][contains(@class,'categoryname')][./descendant::a[.=$categoryliteral]]";
    1880          $node = $this->find('xpath', $xpath, $exception);
    1881          $node->click();
    1882  
    1883          // Smooth expansion.
    1884          $this->getSession()->wait(1000);
    1885      }
    1886  
    1887      /**
    1888       * Finds the node to use for a management listitem action and clicks it.
    1889       *
    1890       * @param string $listingtype Either course or category.
    1891       * @param \Behat\Mink\Element\NodeElement $listingnode
    1892       * @param string $action The action being taken
    1893       * @throws Behat\Mink\Exception\ExpectationException
    1894       */
    1895      protected function user_clicks_on_management_listing_action($listingtype, $listingnode, $action) {
    1896          $actionsnode = $listingnode->find('xpath', "//*" .
    1897                  "[contains(concat(' ', normalize-space(@class), ' '), '{$listingtype}-item-actions')]");
    1898          if (!$actionsnode) {
    1899              throw new ExpectationException("Could not find the actions for $listingtype", $this->getSession());
    1900          }
    1901          $actionnode = $actionsnode->find('css', '.action-'.$action);
    1902          if (!$actionnode) {
    1903              throw new ExpectationException("Expected action was not available or not found ($action)", $this->getSession());
    1904          }
    1905          if ($this->running_javascript() && !$actionnode->isVisible()) {
    1906              $actionsnode->find('css', 'a[data-toggle=dropdown]')->click();
    1907              $actionnode = $actionsnode->find('css', '.action-'.$action);
    1908          }
    1909          $actionnode->click();
    1910      }
    1911  
    1912      /**
    1913       * Clicks on a category in the management interface.
    1914       *
    1915       * @Given /^I click on "(?P<categoryname_string>(?:[^"]|\\")*)" category in the management category listing$/
    1916       * @param string $name The name of the category to click.
    1917       */
    1918      public function i_click_on_category_in_the_management_category_listing($name) {
    1919          $node = $this->get_management_category_listing_node_by_name($name);
    1920          $node->find('css', 'a.categoryname')->click();
    1921      }
    1922  
    1923      /**
    1924       * Locates a category in the course category management interface and then opens action menu for it.
    1925       *
    1926       * @Given /^I open the action menu for "(?P<name_string>(?:[^"]|\\")*)" in management category listing$/
    1927       *
    1928       * @param string $name The name of the category as it is displayed in the management interface.
    1929       */
    1930      public function i_open_the_action_menu_for_item_in_management_category_listing($name) {
    1931          $node = $this->get_management_category_listing_node_by_name($name);
    1932          $node->find('xpath', "//*[contains(@class, 'category-item-actions')]//a[@data-toggle='dropdown']")->click();
    1933      }
    1934  
    1935      /**
    1936       * Checks that the specified category actions menu contains an item.
    1937       *
    1938       * @Then /^"(?P<name_string>(?:[^"]|\\")*)" category actions menu should have "(?P<menu_item_string>(?:[^"]|\\")*)" item$/
    1939       *
    1940       * @param string $name
    1941       * @param string $menuitem
    1942       * @throws Behat\Mink\Exception\ExpectationException
    1943       */
    1944      public function category_actions_menu_should_have_item($name, $menuitem) {
    1945          $node = $this->get_management_category_listing_node_by_name($name);
    1946  
    1947          $notfoundexception = new ExpectationException('"' . $name . '" doesn\'t have a "' .
    1948              $menuitem . '" item', $this->getSession());
    1949          $this->find('named_partial', ['link', $menuitem], $notfoundexception, $node);
    1950      }
    1951  
    1952      /**
    1953       * Checks that the specified category actions menu does not contain an item.
    1954       *
    1955       * @Then /^"(?P<name_string>(?:[^"]|\\")*)" category actions menu should not have "(?P<menu_item_string>(?:[^"]|\\")*)" item$/
    1956       *
    1957       * @param string $name
    1958       * @param string $menuitem
    1959       * @throws Behat\Mink\Exception\ExpectationException
    1960       */
    1961      public function category_actions_menu_should_not_have_item($name, $menuitem) {
    1962          $node = $this->get_management_category_listing_node_by_name($name);
    1963  
    1964          try {
    1965              $this->find('named_partial', ['link', $menuitem], false, $node);
    1966              throw new ExpectationException('"' . $name . '" has a "' . $menuitem .
    1967                  '" item when it should not', $this->getSession());
    1968          } catch (ElementNotFoundException $e) {
    1969              // This is good, the menu item should not be there.
    1970          }
    1971      }
    1972  
    1973      /**
    1974       * Go to the course participants
    1975       *
    1976       * @Given /^I navigate to course participants$/
    1977       */
    1978      public function i_navigate_to_course_participants() {
    1979          $this->execute('behat_navigation::i_select_from_flat_navigation_drawer', get_string('participants'));
    1980      }
    1981  
    1982      /**
    1983       * Check that one teacher appears before another in the course contacts.
    1984       *
    1985       * @Given /^I should see teacher "(?P<pteacher_string>(?:[^"]|\\")*)" before "(?P<fteacher_string>(?:[^"]|\\")*)" in the course contact listing$/
    1986       *
    1987       * @param string $pteacher The first teacher to find
    1988       * @param string $fteacher The second teacher to find (should be after the first teacher)
    1989       *
    1990       * @throws ExpectationException
    1991       */
    1992      public function i_should_see_teacher_before($pteacher, $fteacher) {
    1993          $xpath = "//ul[contains(@class,'teachers')]//li//a[text()='{$pteacher}']/ancestor::li//following::a[text()='{$fteacher}']";
    1994          $msg = "Teacher {$pteacher} does not appear before Teacher {$fteacher}";
    1995          if (!$this->getSession()->getDriver()->find($xpath)) {
    1996              throw new ExpectationException($msg, $this->getSession());
    1997          }
    1998      }
    1999  
    2000      /**
    2001       * Check that one teacher oes not appears after another in the course contacts.
    2002       *
    2003       * @Given /^I should not see teacher "(?P<fteacher_string>(?:[^"]|\\")*)" after "(?P<pteacher_string>(?:[^"]|\\")*)" in the course contact listing$/
    2004       *
    2005       * @param string $fteacher The teacher that should not be found (after the other teacher)
    2006       * @param string $pteacher The teacher after who the other should not be found (this teacher must be found!)
    2007       *
    2008       * @throws ExpectationException
    2009       */
    2010      public function i_should_not_see_teacher_after($fteacher, $pteacher) {
    2011          $xpathliteral = behat_context_helper::escape($pteacher);
    2012          $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
    2013                  "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
    2014          try {
    2015              $nodes = $this->find_all('xpath', $xpath);
    2016          } catch (ElementNotFoundException $e) {
    2017              throw new ExpectationException('"' . $pteacher . '" text was not found in the page', $this->getSession());
    2018          }
    2019          $xpath = "//ul[contains(@class,'teachers')]//li//a[text()='{$pteacher}']/ancestor::li//following::a[text()='{$fteacher}']";
    2020          $msg = "Teacher {$fteacher} appears after Teacher {$pteacher}";
    2021          if ($this->getSession()->getDriver()->find($xpath)) {
    2022              throw new ExpectationException($msg, $this->getSession());
    2023          }
    2024      }
    2025  
    2026      /**
    2027       * Open the activity chooser in a course.
    2028       *
    2029       * @Given /^I open the activity chooser$/
    2030       */
    2031      public function i_open_the_activity_chooser() {
    2032          $this->execute('behat_general::i_click_on',
    2033              array('//button[@data-action="open-chooser"]', 'xpath_element'));
    2034  
    2035          $node = $this->get_selected_node('xpath_element', '//div[@data-region="modules"]');
    2036          $this->ensure_node_is_visible($node);
    2037      }
    2038  
    2039      /**
    2040       * Checks the presence of the given text in the activity's displayed dates.
    2041       *
    2042       * @Given /^the activity date in "(?P<activityname>(?:[^"]|\\")*)" should contain "(?P<text>(?:[^"]|\\")*)"$/
    2043       * @param string $activityname The activity name.
    2044       * @param string $text The text to be searched in the activity date.
    2045       */
    2046      public function activity_date_in_activity_should_contain_text(string $activityname, string $text): void {
    2047          $containerselector = "//div[@data-region='activity-information'][@data-activityname='$activityname']";
    2048          $containerselector .= " /div[@data-region='activity-dates']";
    2049  
    2050          $params = [$text, $containerselector, 'xpath_element'];
    2051          $this->execute("behat_general::assert_element_contains_text", $params);
    2052      }
    2053  
    2054      /**
    2055       * Checks the presence of activity dates information in the activity information output component.
    2056       *
    2057       * @Given /^the activity date information in "(?P<activityname>(?:[^"]|\\")*)" should exist$/
    2058       * @param string $activityname The activity name.
    2059       */
    2060      public function activity_dates_information_in_activity_should_exist(string $activityname): void {
    2061          $containerselector = "//div[@data-region='activity-information'][@data-activityname='$activityname']";
    2062          $elementselector = "/div[@data-region='activity-dates']";
    2063          $params = [$elementselector, "xpath_element", $containerselector, "xpath_element"];
    2064          $this->execute("behat_general::should_exist_in_the", $params);
    2065      }
    2066  
    2067      /**
    2068       * Checks the absence of activity dates information in the activity information output component.
    2069       *
    2070       * @Given /^the activity date information in "(?P<activityname>(?:[^"]|\\")*)" should not exist$/
    2071       * @param string $activityname The activity name.
    2072       */
    2073      public function activity_dates_information_in_activity_should_not_exist(string $activityname): void {
    2074          $containerselector = "//div[@data-region='activity-information'][@data-activityname='$activityname']";
    2075          try {
    2076              $this->find('xpath_element', $containerselector);
    2077          } catch (ElementNotFoundException $e) {
    2078              // If activity information container does not exist (activity dates not shown, completion info not shown), all good.
    2079              return;
    2080          }
    2081  
    2082          // Otherwise, ensure that the completion information does not exist.
    2083          $elementselector = "//div[@data-region='activity-dates']";
    2084          $params = [$elementselector, "xpath_element", $containerselector, "xpath_element"];
    2085          $this->execute("behat_general::should_not_exist_in_the", $params);
    2086      }
    2087  }