Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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

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