Search moodle.org's
Developer Documentation

See Release Notes

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