Search moodle.org's
Developer Documentation

See Release Notes

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

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