Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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              "//button[@data-action='open-chooser' and not(@data-beforemod)]",
 219              'xpath',
 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_action_menu_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_action_menu_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       * Returns the DOM node of the activity action menu.
 984       *
 985       * @throws ElementNotFoundException Thrown by behat_base::find
 986       * @param string $activityname The activity name
 987       * @return \Behat\Mink\Element\NodeElement
 988       */
 989      protected function get_activity_action_menu_node($activityname) {
 990          $activityname = behat_context_helper::escape($activityname);
 991          $xpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][contains(., $activityname)]" .
 992              "//div[contains(@class, 'action-menu')]";
 993          return $this->find('xpath', $xpath);
 994      }
 995  
 996      /**
 997       * Indents to the right the activity or resource specified by it's name. Editing mode should be on.
 998       *
 999       * @Given /^I indent right "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
1000       * @param string $activityname
1001       */
1002      public function i_indent_right_activity($activityname) {
1003  
1004          $activity = $this->escape($activityname);
1005          if ($this->running_javascript()) {
1006              $this->i_open_actions_menu($activity);
1007          }
1008  
1009          $this->execute('behat_course::i_click_on_in_the_activity',
1010              array(get_string('moveright'), "link", $this->escape($activity))
1011          );
1012  
1013      }
1014  
1015      /**
1016       * Indents to the left the activity or resource specified by it's name. Editing mode should be on.
1017       *
1018       * @Given /^I indent left "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
1019       * @param string $activityname
1020       */
1021      public function i_indent_left_activity($activityname) {
1022  
1023          $activity = $this->escape($activityname);
1024          if ($this->running_javascript()) {
1025              $this->i_open_actions_menu($activity);
1026          }
1027  
1028          $this->execute('behat_course::i_click_on_in_the_activity',
1029              array(get_string('moveleft'), "link", $this->escape($activity))
1030          );
1031  
1032      }
1033  
1034      /**
1035       * 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.
1036       *
1037       * @Given /^I delete "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
1038       * @param string $activityname
1039       */
1040      public function i_delete_activity($activityname) {
1041          $steps = array();
1042          $activity = $this->escape($activityname);
1043          if ($this->running_javascript()) {
1044              $this->i_open_actions_menu($activity);
1045          }
1046  
1047          $this->execute('behat_course::i_click_on_in_the_activity',
1048              array(get_string('delete'), "link", $this->escape($activity))
1049          );
1050  
1051          // JS enabled.
1052          // Not using chain steps here because the exceptions catcher have problems detecting
1053          // JS modal windows and avoiding interacting them at the same time.
1054          if ($this->running_javascript()) {
1055              $this->execute(
1056                  'behat_general::i_click_on_in_the',
1057                  [
1058                      get_string('delete'),
1059                      "button",
1060                      get_string('cmdelete_title', 'core_courseformat'),
1061                      "dialogue"
1062                  ]
1063              );
1064          } else {
1065              $this->execute("behat_forms::press_button", get_string('yes'));
1066          }
1067  
1068          return $steps;
1069      }
1070  
1071      /**
1072       * Duplicates the activity or resource specified by it's name. You should be in the course page with editing mode on.
1073       *
1074       * @Given /^I duplicate "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
1075       * @param string $activityname
1076       */
1077      public function i_duplicate_activity($activityname) {
1078          $steps = array();
1079          $activity = $this->escape($activityname);
1080          if ($this->running_javascript()) {
1081              $this->i_open_actions_menu($activity);
1082          }
1083          $this->execute('behat_course::i_click_on_in_the_activity',
1084              array(get_string('duplicate'), "link", $activity)
1085          );
1086  
1087      }
1088  
1089      /**
1090       * 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.
1091       *
1092       * @Given /^I duplicate "(?P<activity_name_string>(?:[^"]|\\")*)" activity editing the new copy with:$/
1093       * @param string $activityname
1094       * @param TableNode $data
1095       */
1096      public function i_duplicate_activity_editing_the_new_copy_with($activityname, TableNode $data) {
1097  
1098          $activity = $this->escape($activityname);
1099          $activityliteral = behat_context_helper::escape($activityname);
1100  
1101          $this->execute("behat_course::i_duplicate_activity", $activity);
1102  
1103          // Determine the future new activity xpath from the former one.
1104          $duplicatedxpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]" .
1105                  "[contains(., $activityliteral)]/following-sibling::li";
1106          $duplicatedactionsmenuxpath = $duplicatedxpath . "/descendant::a[@data-toggle='dropdown']";
1107  
1108          if ($this->running_javascript()) {
1109              // We wait until the AJAX request finishes and the section is visible again.
1110              $hiddenlightboxxpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]" .
1111                      "[contains(., $activityliteral)]" .
1112                      "/ancestor::li[contains(concat(' ', normalize-space(@class), ' '), ' section ')]" .
1113                      "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]";
1114  
1115              // Component based courses do not use lightboxes anymore but js depending.
1116              $sectionreadyxpath = "//*[contains(@id,'page-content')]" .
1117                      "/descendant::*[contains(concat(' ', normalize-space(@class), ' '), ' stateready ')]";
1118  
1119              $duplicationreadyxpath = "$hiddenlightboxxpath | $sectionreadyxpath";
1120              $this->execute(
1121                  "behat_general::wait_until_exists",
1122                  [$this->escape($duplicationreadyxpath), "xpath_element"]
1123              );
1124  
1125              // Close the original activity actions menu.
1126              $this->i_close_actions_menu($activity);
1127  
1128              // The next sibling of the former activity will be the duplicated one, so we click on it from it's xpath as, at
1129              // this point, it don't even exists in the DOM (the steps are executed when we return them).
1130              $this->execute('behat_general::i_click_on',
1131                      array($this->escape($duplicatedactionsmenuxpath), "xpath_element")
1132              );
1133          }
1134  
1135          // We force the xpath as otherwise mink tries to interact with the former one.
1136          $this->execute('behat_general::i_click_on_in_the',
1137                  array(get_string('editsettings'), "link", $this->escape($duplicatedxpath), "xpath_element")
1138          );
1139  
1140          $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $data);
1141          $this->execute("behat_forms::press_button", get_string('savechangesandreturntocourse'));
1142  
1143      }
1144  
1145      /**
1146       * 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.
1147       *
1148       * Using the protected method as this method will be usually
1149       * called by other methods which are not returning a set of
1150       * steps and performs the actions directly, so it would not
1151       * be executed if it returns another step.
1152       *
1153       * Hopefully we would not require test writers to use this step
1154       * and we will manage it from other step definitions.
1155       *
1156       * @Given /^I wait until section "(?P<section_number>\d+)" is available$/
1157       * @param int $sectionnumber
1158       * @return void
1159       */
1160      public function i_wait_until_section_is_available($sectionnumber) {
1161  
1162          // Looks for a hidden lightbox or a non-existent lightbox in that section.
1163          $sectionxpath = $this->section_exists($sectionnumber);
1164          $hiddenlightboxxpath = $sectionxpath . "/descendant::div[contains(concat(' ', @class, ' '), ' lightbox ')][contains(@style, 'display: none')]" .
1165              " | " .
1166              $sectionxpath . "[count(child::div[contains(@class, 'lightbox')]) = 0]";
1167  
1168          $this->ensure_element_exists($hiddenlightboxxpath, 'xpath_element');
1169      }
1170  
1171      /**
1172       * Clicks on the specified element of the activity. You should be in the course page with editing mode turned on.
1173       *
1174       * @Given /^I click on "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>(?:[^"]|\\")*)" in the "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
1175       * @param string $element
1176       * @param string $selectortype
1177       * @param string $activityname
1178       */
1179      public function i_click_on_in_the_activity($element, $selectortype, $activityname) {
1180          $element = $this->get_activity_element($element, $selectortype, $activityname);
1181          $element->click();
1182      }
1183  
1184      /**
1185       * Clicks on the specified element inside the activity container.
1186       *
1187       * @throws ElementNotFoundException
1188       * @param string $element
1189       * @param string $selectortype
1190       * @param string $activityname
1191       * @return NodeElement
1192       */
1193      protected function get_activity_element($element, $selectortype, $activityname) {
1194          $activitynode = $this->get_activity_node($activityname);
1195  
1196          $exception = new ElementNotFoundException($this->getSession(), "'{$element}' '{$selectortype}' in '{$activityname}'");
1197          return $this->find($selectortype, $element, $exception, $activitynode);
1198      }
1199  
1200      /**
1201       * Checks if the course section exists.
1202       *
1203       * @throws ElementNotFoundException Thrown by behat_base::find
1204       * @param int $sectionnumber
1205       * @return string The xpath of the section.
1206       */
1207      protected function section_exists($sectionnumber) {
1208  
1209          // Just to give more info in case it does not exist.
1210          $xpath = "//li[@id='section-" . $sectionnumber . "']";
1211          $exception = new ElementNotFoundException($this->getSession(), "Section $sectionnumber ");
1212          $this->find('xpath', $xpath, $exception);
1213  
1214          return $xpath;
1215      }
1216  
1217      /**
1218       * Returns the show section icon or throws an exception.
1219       *
1220       * @throws ElementNotFoundException Thrown by behat_base::find
1221       * @param int $sectionnumber
1222       * @return NodeElement
1223       */
1224      protected function show_section_link_exists($sectionnumber) {
1225  
1226          // Gets the section xpath and ensure it exists.
1227          $xpath = $this->section_exists($sectionnumber);
1228  
1229          // We need to know the course format as the text strings depends on them.
1230          $courseformat = $this->get_course_format();
1231  
1232          // Checking the show button alt text and show icon.
1233          $showtext = get_string('showfromothers', $courseformat);
1234          $linkxpath = $xpath . "//a[*[contains(text(), " . behat_context_helper::escape($showtext) . ")]]";
1235  
1236          $exception = new ElementNotFoundException($this->getSession(), 'Show section link');
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       * Returns the hide section icon link if it exists or throws exception.
1244       *
1245       * @throws ElementNotFoundException Thrown by behat_base::find
1246       * @param int $sectionnumber
1247       * @return NodeElement
1248       */
1249      protected function hide_section_link_exists($sectionnumber) {
1250  
1251          // Gets the section xpath and ensure it exists.
1252          $xpath = $this->section_exists($sectionnumber);
1253  
1254          // We need to know the course format as the text strings depends on them.
1255          $courseformat = $this->get_course_format();
1256  
1257          // Checking the hide button alt text and hide icon.
1258          $hidetext = behat_context_helper::escape(get_string('hidefromothers', $courseformat));
1259          $linkxpath = $xpath . "/descendant::a[@title=$hidetext]";
1260  
1261          $exception = new ElementNotFoundException($this->getSession(), 'Hide section icon ');
1262          $this->find('icon', 'Hide', $exception);
1263  
1264          // Returing the link so both Non-JS and JS browsers can interact with it.
1265          return $this->find('xpath', $linkxpath, $exception);
1266      }
1267  
1268      /**
1269       * Gets the current course format.
1270       *
1271       * @throws ExpectationException If we are not in the course view page.
1272       * @return string The course format in a frankenstyled name.
1273       */
1274      protected function get_course_format() {
1275  
1276          $exception = new ExpectationException('You are not in a course page', $this->getSession());
1277  
1278          // The moodle body's id attribute contains the course format.
1279          $node = $this->getSession()->getPage()->find('css', 'body');
1280          if (!$node) {
1281              throw $exception;
1282          }
1283  
1284          if (!$bodyid = $node->getAttribute('id')) {
1285              throw $exception;
1286          }
1287  
1288          if (strstr($bodyid, 'page-course-view-') === false) {
1289              throw $exception;
1290          }
1291  
1292          return 'format_' . str_replace('page-course-view-', '', $bodyid);
1293      }
1294  
1295      /**
1296       * Gets the section's activites DOM nodes.
1297       *
1298       * @param string $sectionxpath
1299       * @return array NodeElement instances
1300       */
1301      protected function get_section_activities($sectionxpath) {
1302  
1303          $xpath = $sectionxpath . "/descendant::li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]";
1304  
1305          // We spin here, as activities usually require a lot of time to load.
1306          try {
1307              $activities = $this->find_all('xpath', $xpath);
1308          } catch (ElementNotFoundException $e) {
1309              return false;
1310          }
1311  
1312          return $activities;
1313      }
1314  
1315      /**
1316       * Returns the DOM node of the activity from <li>.
1317       *
1318       * @throws ElementNotFoundException Thrown by behat_base::find
1319       * @param string $activityname The activity name
1320       * @return NodeElement
1321       */
1322      protected function get_activity_node($activityname) {
1323  
1324          $activityname = behat_context_helper::escape($activityname);
1325          $xpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][contains(., $activityname)]";
1326  
1327          return $this->find('xpath', $xpath);
1328      }
1329  
1330      /**
1331       * Gets the activity instance name from the activity node.
1332       *
1333       * @throws ElementNotFoundException
1334       * @param NodeElement $activitynode
1335       * @return string
1336       */
1337      protected function get_activity_name($activitynode) {
1338          $instancenamenode = $this->find('xpath', "//span[contains(concat(' ', normalize-space(@class), ' '), ' instancename ')]", false, $activitynode);
1339          return $instancenamenode->getText();
1340      }
1341  
1342      /**
1343       * Returns whether the user can edit the course contents or not.
1344       *
1345       * @return bool
1346       */
1347      protected function is_course_editor(): bool {
1348          try {
1349              $this->find('field', get_string('editmode'), false, false, 0);
1350              return true;
1351          } catch (ElementNotFoundException $e) {
1352              return false;
1353          }
1354      }
1355  
1356      /**
1357       * Returns whether the user can edit the course contents and the editing mode is on.
1358       *
1359       * @return bool
1360       */
1361      protected function is_editing_on() {
1362          $body = $this->find('xpath', "//body", false, false, 0);
1363          return $body->hasClass('editing');
1364      }
1365  
1366      /**
1367       * Returns the category node from within the listing on the management page.
1368       *
1369       * @param string $idnumber
1370       * @return \Behat\Mink\Element\NodeElement
1371       */
1372      protected function get_management_category_listing_node_by_idnumber($idnumber) {
1373          $id = $this->get_category_id($idnumber);
1374          $selector = sprintf('#category-listing .listitem-category[data-id="%d"] > div', $id);
1375          return $this->find('css', $selector);
1376      }
1377  
1378      /**
1379       * Returns a category node from within the management interface.
1380       *
1381       * @param string $name The name of the category.
1382       * @param bool $link If set to true we'll resolve to the link rather than just the node.
1383       * @return \Behat\Mink\Element\NodeElement
1384       */
1385      protected function get_management_category_listing_node_by_name($name, $link = false) {
1386          $selector = "//div[@id='category-listing']//li[contains(concat(' ', normalize-space(@class), ' '), ' listitem-category ')]//a[text()='{$name}']";
1387          if ($link === false) {
1388              $selector .= "/ancestor::li[@data-id][1]";
1389          }
1390          return $this->find('xpath', $selector);
1391      }
1392  
1393      /**
1394       * Returns a course node from within the management interface.
1395       *
1396       * @param string $name The name of the course.
1397       * @param bool $link If set to true we'll resolve to the link rather than just the node.
1398       * @return \Behat\Mink\Element\NodeElement
1399       */
1400      protected function get_management_course_listing_node_by_name($name, $link = false) {
1401          $selector = "//div[@id='course-listing']//li[contains(concat(' ', @class, ' '), ' listitem-course ')]//a[text()='{$name}']";
1402          if ($link === false) {
1403              $selector .= "/ancestor::li[@data-id]";
1404          }
1405          return $this->find('xpath', $selector);
1406      }
1407  
1408      /**
1409       * Returns the course node from within the listing on the management page.
1410       *
1411       * @param string $idnumber
1412       * @return \Behat\Mink\Element\NodeElement
1413       */
1414      protected function get_management_course_listing_node_by_idnumber($idnumber) {
1415          $id = $this->get_course_id($idnumber);
1416          $selector = sprintf('#course-listing .listitem-course[data-id="%d"] > div', $id);
1417          return $this->find('css', $selector);
1418      }
1419  
1420      /**
1421       * Clicks on a category in the management interface.
1422       *
1423       * @Given /^I click on category "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1424       * @param string $name
1425       */
1426      public function i_click_on_category_in_the_management_interface($name) {
1427          $node = $this->get_management_category_listing_node_by_name($name, true);
1428          $node->click();
1429      }
1430  
1431      /**
1432       * Clicks on a course in the management interface.
1433       *
1434       * @Given /^I click on course "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1435       * @param string $name
1436       */
1437      public function i_click_on_course_in_the_management_interface($name) {
1438          $node = $this->get_management_course_listing_node_by_name($name, true);
1439          $node->click();
1440      }
1441  
1442      /**
1443       * Clicks on a category checkbox in the management interface, if not checked.
1444       *
1445       * @Given /^I select category "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1446       * @param string $name
1447       */
1448      public function i_select_category_in_the_management_interface($name) {
1449          $node = $this->get_management_category_listing_node_by_name($name);
1450          $node = $node->findField('bcat[]');
1451          if (!$node->isChecked()) {
1452              $node->click();
1453          }
1454      }
1455  
1456      /**
1457       * Clicks on a category checkbox in the management interface, if checked.
1458       *
1459       * @Given /^I unselect category "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1460       * @param string $name
1461       */
1462      public function i_unselect_category_in_the_management_interface($name) {
1463          $node = $this->get_management_category_listing_node_by_name($name);
1464          $node = $node->findField('bcat[]');
1465          if ($node->isChecked()) {
1466              $node->click();
1467          }
1468      }
1469  
1470      /**
1471       * Clicks course checkbox in the management interface, if not checked.
1472       *
1473       * @Given /^I select course "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1474       * @param string $name
1475       */
1476      public function i_select_course_in_the_management_interface($name) {
1477          $node = $this->get_management_course_listing_node_by_name($name);
1478          $node = $node->findField('bc[]');
1479          if (!$node->isChecked()) {
1480              $node->click();
1481          }
1482      }
1483  
1484      /**
1485       * Clicks course checkbox in the management interface, if checked.
1486       *
1487       * @Given /^I unselect course "(?P<name_string>(?:[^"]|\\")*)" in the management interface$/
1488       * @param string $name
1489       */
1490      public function i_unselect_course_in_the_management_interface($name) {
1491          $node = $this->get_management_course_listing_node_by_name($name);
1492          $node = $node->findField('bc[]');
1493          if ($node->isChecked()) {
1494              $node->click();
1495          }
1496      }
1497  
1498      /**
1499       * Move selected categories to top level in the management interface.
1500       *
1501       * @Given /^I move category "(?P<name_string>(?:[^"]|\\")*)" to top level in the management interface$/
1502       * @param string $name
1503       */
1504      public function i_move_category_to_top_level_in_the_management_interface($name) {
1505          $this->i_select_category_in_the_management_interface($name);
1506  
1507          $this->execute('behat_forms::i_set_the_field_to',
1508              array('menumovecategoriesto', core_course_category::get(0)->get_formatted_name())
1509          );
1510  
1511          // Save event.
1512          $this->execute("behat_forms::press_button", "bulkmovecategories");
1513      }
1514  
1515      /**
1516       * Checks that a category is a subcategory of specific category.
1517       *
1518       * @Given /^I should see category "(?P<subcatidnumber_string>(?:[^"]|\\")*)" as subcategory of "(?P<catidnumber_string>(?:[^"]|\\")*)" in the management interface$/
1519       * @throws ExpectationException
1520       * @param string $subcatidnumber
1521       * @param string $catidnumber
1522       */
1523      public function i_should_see_category_as_subcategory_of_in_the_management_interface($subcatidnumber, $catidnumber) {
1524          $categorynodeid = $this->get_category_id($catidnumber);
1525          $subcategoryid = $this->get_category_id($subcatidnumber);
1526          $exception = new ExpectationException('The category '.$subcatidnumber.' is not a subcategory of '.$catidnumber, $this->getSession());
1527          $selector = sprintf('#category-listing .listitem-category[data-id="%d"] .listitem-category[data-id="%d"]', $categorynodeid, $subcategoryid);
1528          $this->find('css', $selector, $exception);
1529      }
1530  
1531      /**
1532       * Checks that a category is not a subcategory of specific category.
1533       *
1534       * @Given /^I should not see category "(?P<subcatidnumber_string>(?:[^"]|\\")*)" as subcategory of "(?P<catidnumber_string>(?:[^"]|\\")*)" in the management interface$/
1535       * @throws ExpectationException
1536       * @param string $subcatidnumber
1537       * @param string $catidnumber
1538       */
1539      public function i_should_not_see_category_as_subcategory_of_in_the_management_interface($subcatidnumber, $catidnumber) {
1540          try {
1541              $this->i_should_see_category_as_subcategory_of_in_the_management_interface($subcatidnumber, $catidnumber);
1542          } catch (ExpectationException $e) {
1543              // ExpectedException means that it is not highlighted.
1544              return;
1545          }
1546          throw new ExpectationException('The category '.$subcatidnumber.' is a subcategory of '.$catidnumber, $this->getSession());
1547      }
1548  
1549      /**
1550       * Click to expand a category revealing its sub categories within the management UI.
1551       *
1552       * @Given /^I click to expand category "(?P<idnumber_string>(?:[^"]|\\")*)" in the management interface$/
1553       * @param string $idnumber
1554       */
1555      public function i_click_to_expand_category_in_the_management_interface($idnumber) {
1556          $categorynode = $this->get_management_category_listing_node_by_idnumber($idnumber);
1557          $exception = new ExpectationException('Category "' . $idnumber . '" does not contain an expand or collapse toggle.', $this->getSession());
1558          $togglenode = $this->find('css', 'a[data-action=collapse],a[data-action=expand]', $exception, $categorynode);
1559          $togglenode->click();
1560      }
1561  
1562      /**
1563       * Checks that a category within the management interface is visible.
1564       *
1565       * @Given /^category in management listing should be visible "(?P<idnumber_string>(?:[^"]|\\")*)"$/
1566       * @param string $idnumber
1567       */
1568      public function category_in_management_listing_should_be_visible($idnumber) {
1569          $id = $this->get_category_id($idnumber);
1570          $exception = new ExpectationException('The category '.$idnumber.' is not visible.', $this->getSession());
1571          $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible="1"]', $id);
1572          $this->find('css', $selector, $exception);
1573      }
1574  
1575      /**
1576       * Checks that a category within the management interface is dimmed.
1577       *
1578       * @Given /^category in management listing should be dimmed "(?P<idnumber_string>(?:[^"]|\\")*)"$/
1579       * @param string $idnumber
1580       */
1581      public function category_in_management_listing_should_be_dimmed($idnumber) {
1582          $id = $this->get_category_id($idnumber);
1583          $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible="0"]', $id);
1584          $exception = new ExpectationException('The category '.$idnumber.' is visible.', $this->getSession());
1585          $this->find('css', $selector, $exception);
1586      }
1587  
1588      /**
1589       * Checks that a course within the management interface is visible.
1590       *
1591       * @Given /^course in management listing should be visible "(?P<idnumber_string>(?:[^"]|\\")*)"$/
1592       * @param string $idnumber
1593       */
1594      public function course_in_management_listing_should_be_visible($idnumber) {
1595          $id = $this->get_course_id($idnumber);
1596          $exception = new ExpectationException('The course '.$idnumber.' is not visible.', $this->getSession());
1597          $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible="1"]', $id);
1598          $this->find('css', $selector, $exception);
1599      }
1600  
1601      /**
1602       * Checks that a course within the management interface is dimmed.
1603       *
1604       * @Given /^course in management listing should be dimmed "(?P<idnumber_string>(?:[^"]|\\")*)"$/
1605       * @param string $idnumber
1606       */
1607      public function course_in_management_listing_should_be_dimmed($idnumber) {
1608          $id = $this->get_course_id($idnumber);
1609          $exception = new ExpectationException('The course '.$idnumber.' is visible.', $this->getSession());
1610          $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible="0"]', $id);
1611          $this->find('css', $selector, $exception);
1612      }
1613  
1614      /**
1615       * Toggles the visibility of a course in the management UI.
1616       *
1617       * If it was visible it will be hidden. If it is hidden it will be made visible.
1618       *
1619       * @Given /^I toggle visibility of course "(?P<idnumber_string>(?:[^"]|\\")*)" in management listing$/
1620       * @param string $idnumber
1621       */
1622      public function i_toggle_visibility_of_course_in_management_listing($idnumber) {
1623          $id = $this->get_course_id($idnumber);
1624          $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible]', $id);
1625          $node = $this->find('css', $selector);
1626          $exception = new ExpectationException('Course listing "' . $idnumber . '" does not contain a show or hide toggle.', $this->getSession());
1627          if ($node->getAttribute('data-visible') === '1') {
1628              $toggle = $this->find('css', '.action-hide', $exception, $node);
1629          } else {
1630              $toggle = $this->find('css', '.action-show', $exception, $node);
1631          }
1632          $toggle->click();
1633      }
1634  
1635      /**
1636       * Toggles the visibility of a category in the management UI.
1637       *
1638       * If it was visible it will be hidden. If it is hidden it will be made visible.
1639       *
1640       * @Given /^I toggle visibility of category "(?P<idnumber_string>(?:[^"]|\\")*)" in management listing$/
1641       */
1642      public function i_toggle_visibility_of_category_in_management_listing($idnumber) {
1643          $id = $this->get_category_id($idnumber);
1644          $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible]', $id);
1645          $node = $this->find('css', $selector);
1646          $exception = new ExpectationException('Category listing "' . $idnumber . '" does not contain a show or hide toggle.', $this->getSession());
1647          if ($node->getAttribute('data-visible') === '1') {
1648              $toggle = $this->find('css', '.action-hide', $exception, $node);
1649          } else {
1650              $toggle = $this->find('css', '.action-show', $exception, $node);
1651          }
1652          $toggle->click();
1653      }
1654  
1655      /**
1656       * Moves a category displayed in the management interface up or down one place.
1657       *
1658       * @Given /^I click to move category "(?P<idnumber_string>(?:[^"]|\\")*)" (?P<direction>up|down) one$/
1659       *
1660       * @param string $idnumber The category idnumber
1661       * @param string $direction The direction to move in, either up or down
1662       */
1663      public function i_click_to_move_category_by_one($idnumber, $direction) {
1664          $node = $this->get_management_category_listing_node_by_idnumber($idnumber);
1665          $this->user_moves_listing_by_one('category', $node, $direction);
1666      }
1667  
1668      /**
1669       * Moves a course displayed in the management interface up or down one place.
1670       *
1671       * @Given /^I click to move course "(?P<idnumber_string>(?:[^"]|\\")*)" (?P<direction>up|down) one$/
1672       *
1673       * @param string $idnumber The course idnumber
1674       * @param string $direction The direction to move in, either up or down
1675       */
1676      public function i_click_to_move_course_by_one($idnumber, $direction) {
1677          $node = $this->get_management_course_listing_node_by_idnumber($idnumber);
1678          $this->user_moves_listing_by_one('course', $node, $direction);
1679      }
1680  
1681      /**
1682       * Moves a course or category listing within the management interface up or down by one.
1683       *
1684       * @param string $listingtype One of course or category
1685       * @param \Behat\Mink\Element\NodeElement $listingnode
1686       * @param string $direction One of up or down.
1687       * @param bool $highlight If set to false we don't check the node has been highlighted.
1688       */
1689      protected function user_moves_listing_by_one($listingtype, $listingnode, $direction, $highlight = true) {
1690          $up = (strtolower($direction) === 'up');
1691          if ($up) {
1692              $exception = new ExpectationException($listingtype.' listing does not contain a moveup button.', $this->getSession());
1693              $button = $this->find('css', 'a.action-moveup', $exception, $listingnode);
1694          } else {
1695              $exception = new ExpectationException($listingtype.' listing does not contain a movedown button.', $this->getSession());
1696              $button = $this->find('css', 'a.action-movedown', $exception, $listingnode);
1697          }
1698          $button->click();
1699          if ($this->running_javascript() && $highlight) {
1700              $listitem = $listingnode->getParent();
1701              $exception = new ExpectationException('Nothing was highlighted, ajax didn\'t occur or didn\'t succeed.', $this->getSession());
1702              $this->spin(array($this, 'listing_is_highlighted'), $listitem->getTagName().'#'.$listitem->getAttribute('id'), 2, $exception, true);
1703          }
1704      }
1705  
1706      /**
1707       * Used by spin to determine the callback has been highlighted.
1708       *
1709       * @param behat_course $self A self reference (default first arg from a spin callback)
1710       * @param \Behat\Mink\Element\NodeElement $selector
1711       * @return bool
1712       */
1713      protected function listing_is_highlighted($self, $selector) {
1714          $listitem = $this->find('css', $selector);
1715          return $listitem->hasClass('highlight');
1716      }
1717  
1718      /**
1719       * Check that one course appears before another in the course category management listings.
1720       *
1721       * @Given /^I should see course listing "(?P<preceedingcourse_string>(?:[^"]|\\")*)" before "(?P<followingcourse_string>(?:[^"]|\\")*)"$/
1722       *
1723       * @param string $preceedingcourse The first course to find
1724       * @param string $followingcourse The second course to find (should be AFTER the first course)
1725       * @throws ExpectationException
1726       */
1727      public function i_should_see_course_listing_before($preceedingcourse, $followingcourse) {
1728          $xpath = "//div[@id='course-listing']//li[contains(concat(' ', @class, ' '), ' listitem-course ')]//a[text()='{$preceedingcourse}']/ancestor::li[@data-id]//following::a[text()='{$followingcourse}']";
1729          $msg = "{$preceedingcourse} course does not appear before {$followingcourse} course";
1730          if (!$this->getSession()->getDriver()->find($xpath)) {
1731              throw new ExpectationException($msg, $this->getSession());
1732          }
1733      }
1734  
1735      /**
1736       * Check that one category appears before another in the course category management listings.
1737       *
1738       * @Given /^I should see category listing "(?P<preceedingcategory_string>(?:[^"]|\\")*)" before "(?P<followingcategory_string>(?:[^"]|\\")*)"$/
1739       *
1740       * @param string $preceedingcategory The first category to find
1741       * @param string $followingcategory The second category to find (should be after the first category)
1742       * @throws ExpectationException
1743       */
1744      public function i_should_see_category_listing_before($preceedingcategory, $followingcategory) {
1745          $xpath = "//div[@id='category-listing']//li[contains(concat(' ', @class, ' '), ' listitem-category ')]//a[text()='{$preceedingcategory}']/ancestor::li[@data-id]//following::a[text()='{$followingcategory}']";
1746          $msg = "{$preceedingcategory} category does not appear before {$followingcategory} category";
1747          if (!$this->getSession()->getDriver()->find($xpath)) {
1748              throw new ExpectationException($msg, $this->getSession());
1749          }
1750      }
1751  
1752      /**
1753       * Checks that we are on the course management page that we expect to be on and that no course has been selected.
1754       *
1755       * @Given /^I should see the "(?P<mode_string>(?:[^"]|\\")*)" management page$/
1756       * @param string $mode The mode to expected. One of 'Courses', 'Course categories' or 'Course categories and courses'
1757       */
1758      public function i_should_see_the_courses_management_page($mode) {
1759          switch ($mode) {
1760              case "Courses":
1761                  $heading = "Manage courses";
1762                  $this->execute("behat_general::should_not_exist", array("#category-listing", "css_element"));
1763                  $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
1764                  break;
1765  
1766              case "Course categories":
1767                  $heading = "Manage course categories";
1768                  $this->execute("behat_general::should_exist", array("#category-listing", "css_element"));
1769                  $this->execute("behat_general::should_not_exist", array("#course-listing", "css_element"));
1770                  break;
1771  
1772              case "Courses categories and courses":
1773              default:
1774                  $heading = "Manage course categories and courses";
1775                  $this->execute("behat_general::should_exist", array("#category-listing", "css_element"));
1776                  $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
1777                  break;
1778          }
1779  
1780          $this->execute("behat_general::assert_element_contains_text",
1781              array($heading, "h2", "css_element")
1782          );
1783  
1784          $this->execute("behat_general::should_not_exist", array("#course-detail", "css_element"));
1785      }
1786  
1787      /**
1788       * Checks that we are on the course management page that we expect to be on and that a course has been selected.
1789       *
1790       * @Given /^I should see the "(?P<mode_string>(?:[^"]|\\")*)" management page with a course selected$/
1791       * @param string $mode The mode to expected. One of 'Courses', 'Course categories' or 'Course categories and courses'
1792       */
1793      public function i_should_see_the_courses_management_page_with_a_course_selected($mode) {
1794          switch ($mode) {
1795              case "Courses":
1796                  $heading = "Manage courses";
1797                  $this->execute("behat_general::should_not_exist", array("#category-listing", "css_element"));
1798                  $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
1799                  break;
1800  
1801              case "Course categories":
1802                  $heading = "Manage course categories";
1803                  $this->execute("behat_general::should_exist", array("#category-listing", "css_element"));
1804                  $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
1805                  break;
1806  
1807              case "Courses categories and courses":
1808              default:
1809                  $heading = "Manage course categories and courses";
1810                  $this->execute("behat_general::should_exist", array("#category-listing", "css_element"));
1811                  $this->execute("behat_general::should_exist", array("#course-listing", "css_element"));
1812                  break;
1813          }
1814  
1815          $this->execute("behat_general::assert_element_contains_text",
1816              array($heading, "h2", "css_element"));
1817  
1818          $this->execute("behat_general::should_exist", array("#course-detail", "css_element"));
1819      }
1820  
1821      /**
1822       * Locates a course in the course category management interface and then triggers an action for it.
1823       *
1824       * @Given /^I click on "(?P<action_string>(?:[^"]|\\")*)" action for "(?P<name_string>(?:[^"]|\\")*)" in management course listing$/
1825       *
1826       * @param string $action The action to take. One of
1827       * @param string $name The name of the course as it is displayed in the management interface.
1828       */
1829      public function i_click_on_action_for_item_in_management_course_listing($action, $name) {
1830          $node = $this->get_management_course_listing_node_by_name($name);
1831          $this->user_clicks_on_management_listing_action('course', $node, $action);
1832      }
1833  
1834      /**
1835       * Locates a category in the course category management interface and then triggers an action for it.
1836       *
1837       * @Given /^I click on "(?P<action_string>(?:[^"]|\\")*)" action for "(?P<name_string>(?:[^"]|\\")*)" in management category listing$/
1838       *
1839       * @param string $action The action to take. One of
1840       * @param string $name The name of the category as it is displayed in the management interface.
1841       */
1842      public function i_click_on_action_for_item_in_management_category_listing($action, $name) {
1843          $node = $this->get_management_category_listing_node_by_name($name);
1844          $this->user_clicks_on_management_listing_action('category', $node, $action);
1845      }
1846  
1847      /**
1848       * Clicks to expand or collapse a category displayed on the frontpage
1849       *
1850       * @Given /^I toggle "(?P<categoryname_string>(?:[^"]|\\")*)" category children visibility in frontpage$/
1851       * @throws ExpectationException
1852       * @param string $categoryname
1853       */
1854      public function i_toggle_category_children_visibility_in_frontpage($categoryname) {
1855  
1856          $headingtags = array();
1857          for ($i = 1; $i <= 6; $i++) {
1858              $headingtags[] = 'self::h' . $i;
1859          }
1860  
1861          $exception = new ExpectationException('"' . $categoryname . '" category can not be found', $this->getSession());
1862          $categoryliteral = behat_context_helper::escape($categoryname);
1863          $xpath = "//div[@class='info']/descendant::*[" . implode(' or ', $headingtags) .
1864              "][contains(@class,'categoryname')][./descendant::a[.=$categoryliteral]]";
1865          $node = $this->find('xpath', $xpath, $exception);
1866          $node->click();
1867  
1868          // Smooth expansion.
1869          $this->getSession()->wait(1000);
1870      }
1871  
1872      /**
1873       * Finds the node to use for a management listitem action and clicks it.
1874       *
1875       * @param string $listingtype Either course or category.
1876       * @param \Behat\Mink\Element\NodeElement $listingnode
1877       * @param string $action The action being taken
1878       * @throws Behat\Mink\Exception\ExpectationException
1879       */
1880      protected function user_clicks_on_management_listing_action($listingtype, $listingnode, $action) {
1881          $actionsnode = $listingnode->find('xpath', "//*" .
1882                  "[contains(concat(' ', normalize-space(@class), ' '), '{$listingtype}-item-actions')]");
1883          if (!$actionsnode) {
1884              throw new ExpectationException("Could not find the actions for $listingtype", $this->getSession());
1885          }
1886          $actionnode = $actionsnode->find('css', '.action-'.$action);
1887          if (!$actionnode) {
1888              throw new ExpectationException("Expected action was not available or not found ($action)", $this->getSession());
1889          }
1890          if ($this->running_javascript() && !$actionnode->isVisible()) {
1891              $actionsnode->find('css', 'a[data-toggle=dropdown]')->click();
1892              $actionnode = $actionsnode->find('css', '.action-'.$action);
1893          }
1894          $actionnode->click();
1895      }
1896  
1897      /**
1898       * Clicks on a category in the management interface.
1899       *
1900       * @Given /^I click on "(?P<categoryname_string>(?:[^"]|\\")*)" category in the management category listing$/
1901       * @param string $name The name of the category to click.
1902       */
1903      public function i_click_on_category_in_the_management_category_listing($name) {
1904          $node = $this->get_management_category_listing_node_by_name($name);
1905          $node->find('css', 'a.categoryname')->click();
1906      }
1907  
1908      /**
1909       * Locates a category in the course category management interface and then opens action menu for it.
1910       *
1911       * @Given /^I open the action menu for "(?P<name_string>(?:[^"]|\\")*)" in management category listing$/
1912       *
1913       * @param string $name The name of the category as it is displayed in the management interface.
1914       */
1915      public function i_open_the_action_menu_for_item_in_management_category_listing($name) {
1916          $node = $this->get_management_category_listing_node_by_name($name);
1917          $node->find('xpath', "//*[contains(@class, 'category-item-actions')]//a[@data-toggle='dropdown']")->click();
1918      }
1919  
1920      /**
1921       * Checks that the specified category actions menu contains an item.
1922       *
1923       * @Then /^"(?P<name_string>(?:[^"]|\\")*)" category actions menu should have "(?P<menu_item_string>(?:[^"]|\\")*)" item$/
1924       *
1925       * @param string $name
1926       * @param string $menuitem
1927       * @throws Behat\Mink\Exception\ExpectationException
1928       */
1929      public function category_actions_menu_should_have_item($name, $menuitem) {
1930          $node = $this->get_management_category_listing_node_by_name($name);
1931  
1932          $notfoundexception = new ExpectationException('"' . $name . '" doesn\'t have a "' .
1933              $menuitem . '" item', $this->getSession());
1934          $this->find('named_partial', ['link', $menuitem], $notfoundexception, $node);
1935      }
1936  
1937      /**
1938       * Checks that the specified category actions menu does not contain an item.
1939       *
1940       * @Then /^"(?P<name_string>(?:[^"]|\\")*)" category actions menu should not have "(?P<menu_item_string>(?:[^"]|\\")*)" item$/
1941       *
1942       * @param string $name
1943       * @param string $menuitem
1944       * @throws Behat\Mink\Exception\ExpectationException
1945       */
1946      public function category_actions_menu_should_not_have_item($name, $menuitem) {
1947          $node = $this->get_management_category_listing_node_by_name($name);
1948  
1949          try {
1950              $this->find('named_partial', ['link', $menuitem], false, $node);
1951              throw new ExpectationException('"' . $name . '" has a "' . $menuitem .
1952                  '" item when it should not', $this->getSession());
1953          } catch (ElementNotFoundException $e) {
1954              // This is good, the menu item should not be there.
1955          }
1956      }
1957  
1958      /**
1959       * Go to the course participants
1960       *
1961       * @Given /^I navigate to course participants$/
1962       */
1963      public function i_navigate_to_course_participants() {
1964          $this->execute('behat_navigation::i_select_from_secondary_navigation', get_string('participants'));
1965      }
1966  
1967      /**
1968       * Check that one teacher appears before another in the course contacts.
1969       *
1970       * @Given /^I should see teacher "(?P<pteacher_string>(?:[^"]|\\")*)" before "(?P<fteacher_string>(?:[^"]|\\")*)" in the course contact listing$/
1971       *
1972       * @param string $pteacher The first teacher to find
1973       * @param string $fteacher The second teacher to find (should be after the first teacher)
1974       *
1975       * @throws ExpectationException
1976       */
1977      public function i_should_see_teacher_before($pteacher, $fteacher) {
1978          $xpath = "//ul[contains(@class,'teachers')]//li//a[text()='{$pteacher}']/ancestor::li//following::a[text()='{$fteacher}']";
1979          $msg = "Teacher {$pteacher} does not appear before Teacher {$fteacher}";
1980          if (!$this->getSession()->getDriver()->find($xpath)) {
1981              throw new ExpectationException($msg, $this->getSession());
1982          }
1983      }
1984  
1985      /**
1986       * Check that one teacher oes not appears after another in the course contacts.
1987       *
1988       * @Given /^I should not see teacher "(?P<fteacher_string>(?:[^"]|\\")*)" after "(?P<pteacher_string>(?:[^"]|\\")*)" in the course contact listing$/
1989       *
1990       * @param string $fteacher The teacher that should not be found (after the other teacher)
1991       * @param string $pteacher The teacher after who the other should not be found (this teacher must be found!)
1992       *
1993       * @throws ExpectationException
1994       */
1995      public function i_should_not_see_teacher_after($fteacher, $pteacher) {
1996          $xpathliteral = behat_context_helper::escape($pteacher);
1997          $xpath = "/descendant-or-self::*[contains(., $xpathliteral)]" .
1998                  "[count(descendant::*[contains(., $xpathliteral)]) = 0]";
1999          try {
2000              $nodes = $this->find_all('xpath', $xpath);
2001          } catch (ElementNotFoundException $e) {
2002              throw new ExpectationException('"' . $pteacher . '" text was not found in the page', $this->getSession());
2003          }
2004          $xpath = "//ul[contains(@class,'teachers')]//li//a[text()='{$pteacher}']/ancestor::li//following::a[text()='{$fteacher}']";
2005          $msg = "Teacher {$fteacher} appears after Teacher {$pteacher}";
2006          if ($this->getSession()->getDriver()->find($xpath)) {
2007              throw new ExpectationException($msg, $this->getSession());
2008          }
2009      }
2010  
2011      /**
2012       * Open the activity chooser in a course.
2013       *
2014       * @Given /^I open the activity chooser$/
2015       */
2016      public function i_open_the_activity_chooser() {
2017          $this->execute('behat_general::i_click_on',
2018              array('//button[@data-action="open-chooser"]', 'xpath_element'));
2019  
2020          $node = $this->get_selected_node('xpath_element', '//div[@data-region="modules"]');
2021          $this->ensure_node_is_visible($node);
2022      }
2023  
2024      /**
2025       * Checks the presence of the given text in the activity's displayed dates.
2026       *
2027       * @Given /^the activity date in "(?P<activityname>(?:[^"]|\\")*)" should contain "(?P<text>(?:[^"]|\\")*)"$/
2028       * @param string $activityname The activity name.
2029       * @param string $text The text to be searched in the activity date.
2030       */
2031      public function activity_date_in_activity_should_contain_text(string $activityname, string $text): void {
2032          $containerselector = "//div[@data-activityname='$activityname']";
2033          $containerselector .= "//div[@data-region='activity-dates']";
2034  
2035          $params = [$text, $containerselector, 'xpath_element'];
2036          $this->execute("behat_general::assert_element_contains_text", $params);
2037      }
2038  
2039      /**
2040       * Checks the presence of activity dates information in the activity information output component.
2041       *
2042       * @Given /^the activity date information in "(?P<activityname>(?:[^"]|\\")*)" should exist$/
2043       * @param string $activityname The activity name.
2044       */
2045      public function activity_dates_information_in_activity_should_exist(string $activityname): void {
2046          $containerselector = "//div[@data-activityname='$activityname']";
2047          $elementselector = "//div[@data-region='activity-dates']";
2048          $params = [$elementselector, "xpath_element", $containerselector, "xpath_element"];
2049          $this->execute("behat_general::should_exist_in_the", $params);
2050      }
2051  
2052      /**
2053       * Checks the absence of activity dates information in the activity information output component.
2054       *
2055       * @Given /^the activity date information in "(?P<activityname>(?:[^"]|\\")*)" should not exist$/
2056       * @param string $activityname The activity name.
2057       */
2058      public function activity_dates_information_in_activity_should_not_exist(string $activityname): void {
2059          $containerselector = "//div[@data-region='activity-information'][@data-activityname='$activityname']";
2060          try {
2061              $this->find('xpath_element', $containerselector);
2062          } catch (ElementNotFoundException $e) {
2063              // If activity information container does not exist (activity dates not shown, completion info not shown), all good.
2064              return;
2065          }
2066  
2067          // Otherwise, ensure that the completion information does not exist.
2068          $elementselector = "//div[@data-region='activity-dates']";
2069          $params = [$elementselector, "xpath_element", $containerselector, "xpath_element"];
2070          $this->execute("behat_general::should_not_exist_in_the", $params);
2071      }
2072  }