Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

   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   * Steps definitions for rubrics.
  19   *
  20   * @package   gradingform_rubric
  21   * @category  test
  22   * @copyright 2013 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\ElementNotFoundException as ElementNotFoundException,
  32      Behat\Mink\Exception\ExpectationException as ExpectationException;
  33  
  34  /**
  35   * Steps definitions to help with rubrics.
  36   *
  37   * @package   gradingform_rubric
  38   * @category  test
  39   * @copyright 2013 David MonllaĆ³
  40   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   */
  42  class behat_gradingform_rubric extends behat_base {
  43  
  44      /**
  45       * @var The number of levels added by default when a rubric is created.
  46       */
  47      const DEFAULT_RUBRIC_LEVELS = 3;
  48  
  49      /**
  50       * Defines the rubric with the provided data, following rubric's definition grid cells.
  51       *
  52       * This method fills the rubric of the rubric definition
  53       * form; the provided TableNode should contain one row for
  54       * each criterion and each cell of the row should contain:
  55       * # Criterion description
  56       * # Criterion level 1 name
  57       * # Criterion level 1 points
  58       * # Criterion level 2 name
  59       * # Criterion level 2 points
  60       * # Criterion level 3 .....
  61       *
  62       * Works with both JS and non-JS.
  63       *
  64       * @When /^I define the following rubric:$/
  65       * @throws ExpectationException
  66       * @param TableNode $rubric
  67       */
  68      public function i_define_the_following_rubric(TableNode $rubric) {
  69  
  70          // Being a smart method is nothing good when we talk about step definitions, in
  71          // this case we didn't have any other options as there are no labels no elements
  72          // id we can point to without having to "calculate" them.
  73  
  74          $steptableinfo = '| criterion description | level1 name  | level1 points | level2 name | level2 points | ...';
  75  
  76          $criteria = $rubric->getRows();
  77  
  78          $addcriterionbutton = $this->find_button(get_string('addcriterion', 'gradingform_rubric'));
  79  
  80          // Cleaning the current ones.
  81          $deletebuttons = $this->find_all('css', "input[value='" . get_string('criteriondelete', 'gradingform_rubric') . "']");
  82          if ($deletebuttons) {
  83  
  84              // We should reverse the deletebuttons because otherwise once we delete
  85              // the first one the DOM will change and the [X] one will not exist anymore.
  86              $deletebuttons = array_reverse($deletebuttons, true);
  87              foreach ($deletebuttons as $button) {
  88                  $this->click_and_confirm($button);
  89              }
  90          }
  91  
  92          // The level number (NEWID$N) is not reset after each criterion.
  93          $levelnumber = 1;
  94  
  95          // The next criterion is created with the same number of levels than the last criterion.
  96          $defaultnumberoflevels = self::DEFAULT_RUBRIC_LEVELS;
  97  
  98          if ($criteria) {
  99              foreach ($criteria as $criterionit => $criterion) {
 100                  // Unset empty levels in criterion.
 101                  foreach ($criterion as $i => $value) {
 102                      if (empty($value)) {
 103                          unset($criterion[$i]);
 104                      }
 105                  }
 106  
 107                  // Remove empty criterion, as TableNode might contain them to make table rows equal size.
 108                  $newcriterion = array();
 109                  foreach ($criterion as $k => $c) {
 110                      if (!empty($c)) {
 111                          $newcriterion[$k] = $c;
 112                      }
 113                  }
 114                  $criterion = $newcriterion;
 115  
 116                  // Checking the number of cells.
 117                  if (count($criterion) % 2 === 0) {
 118                      throw new ExpectationException(
 119                          'The criterion levels should contain both definition and points, follow this format:' . $steptableinfo,
 120                          $this->getSession()
 121                      );
 122                  }
 123  
 124                  // Minimum 2 levels per criterion.
 125                  // description + definition1 + score1 + definition2 + score2 = 5.
 126                  if (count($criterion) < 5) {
 127                      throw new ExpectationException(
 128                          get_string('err_mintwolevels', 'gradingform_rubric'),
 129                          $this->getSession()
 130                      );
 131  
 132                  }
 133  
 134                  // Add new criterion.
 135                  $addcriterionbutton->click();
 136  
 137                  $criterionroot = 'rubric[criteria][NEWID' . ($criterionit + 1) . ']';
 138  
 139                  // Getting the criterion description, this one is visible by default.
 140                  $this->set_rubric_field_value($criterionroot . '[description]', array_shift($criterion), true);
 141  
 142                  // When JS is disabled each criterion's levels name numbers starts from 0.
 143                  if (!$this->running_javascript()) {
 144                      $levelnumber = 0;
 145                  }
 146  
 147                  // Setting the correct number of levels.
 148                  $nlevels = count($criterion) / 2;
 149                  if ($nlevels < $defaultnumberoflevels) {
 150  
 151                      // Removing levels if there are too much levels.
 152                      // When we add a new level the NEWID$N is increased from the last criterion.
 153                      $lastcriteriondefaultlevel = $defaultnumberoflevels + $levelnumber - 1;
 154                      $lastcriterionlevel = $nlevels + $levelnumber - 1;
 155                      for ($i = $lastcriteriondefaultlevel; $i > $lastcriterionlevel; $i--) {
 156  
 157                          // If JS is disabled seems that new levels are not added.
 158                          if ($this->running_javascript()) {
 159                              $deletelevel = $this->find_button($criterionroot . '[levels][NEWID' . $i . '][delete]');
 160                              $this->click_and_confirm($deletelevel);
 161  
 162                          } else {
 163                              // Only if the level exists.
 164                              $buttonname = $criterionroot . '[levels][NEWID' . $i . '][delete]';
 165                              if ($deletelevel = $this->getSession()->getPage()->findButton($buttonname)) {
 166                                  $deletelevel->click();
 167                              }
 168                          }
 169                      }
 170                  } else if ($nlevels > $defaultnumberoflevels) {
 171                      // Adding levels if we don't have enough.
 172                      $addlevel = $this->find_button($criterionroot . '[levels][addlevel]');
 173                      for ($i = ($defaultnumberoflevels + 1); $i <= $nlevels; $i++) {
 174                          $addlevel->click();
 175                      }
 176                  }
 177  
 178                  // Updating it.
 179                  if ($nlevels > self::DEFAULT_RUBRIC_LEVELS) {
 180                      $defaultnumberoflevels = $nlevels;
 181                  } else {
 182                      // If it is less than the default value it sets it to
 183                      // the default value.
 184                      $defaultnumberoflevels = self::DEFAULT_RUBRIC_LEVELS;
 185                  }
 186  
 187                  foreach ($criterion as $i => $value) {
 188  
 189                      $levelroot = $criterionroot . '[levels][NEWID' . $levelnumber . ']';
 190  
 191                      if ($i % 2 === 0) {
 192                          // Pairs are the definitions.
 193                          $fieldname = $levelroot . '[definition]';
 194                          $this->set_rubric_field_value($fieldname, $value);
 195  
 196                      } else {
 197                          // Odds are the points.
 198  
 199                          // Checking it now, we would need to remove it if we are testing the form validations...
 200                          if (!is_numeric($value)) {
 201                              throw new ExpectationException(
 202                                  'The points cells should contain numeric values, follow this format: ' . $steptableinfo,
 203                                  $this->getSession()
 204                              );
 205                          }
 206  
 207                          $fieldname = $levelroot . '[score]';
 208                          $this->set_rubric_field_value($fieldname, $value, true);
 209  
 210                          // Increase the level by one every 2 cells.
 211                          $levelnumber++;
 212                      }
 213  
 214                  }
 215              }
 216          }
 217      }
 218  
 219      /**
 220       * Replaces a value from the specified criterion. You can use it when editing rubrics, to set both name or points.
 221       *
 222       * @When /^I replace "(?P<current_value_string>(?:[^"]|\\")*)" rubric level with "(?P<value_string>(?:[^"]|\\")*)" in "(?P<criterion_string>(?:[^"]|\\")*)" criterion$/
 223       * @throws ElementNotFoundException
 224       * @param string $currentvalue
 225       * @param string $value
 226       * @param string $criterionname
 227       */
 228      public function i_replace_rubric_level_with($currentvalue, $value, $criterionname) {
 229  
 230          $currentvalueliteral = behat_context_helper::escape($currentvalue);
 231          $criterionliteral = behat_context_helper::escape($criterionname);
 232  
 233          $criterionxpath = "//div[@id='rubric-rubric']" .
 234              "/descendant::td[contains(concat(' ', normalize-space(@class), ' '), ' description ')]";
 235          // It differs between JS on/off.
 236          if ($this->running_javascript()) {
 237              $criterionxpath .= "/descendant::span[@class='textvalue'][text()=$criterionliteral]" .
 238                  "/ancestor::tr[contains(concat(' ', normalize-space(@class), ' '), ' criterion ')]";
 239          } else {
 240              $criterionxpath .= "/descendant::textarea[text()=$criterionliteral]" .
 241                  "/ancestor::tr[contains(concat(' ', normalize-space(@class), ' '), ' criterion ')]";
 242          }
 243  
 244          $inputxpath = $criterionxpath .
 245              "/descendant::input[@type='text'][@value=$currentvalueliteral]";
 246          $textareaxpath = $criterionxpath .
 247              "/descendant::textarea[text()=$currentvalueliteral]";
 248  
 249          if ($this->running_javascript()) {
 250  
 251              $spansufix = "/ancestor::div[@class='level-wrapper']" .
 252                  "/descendant::div[@class='definition']" .
 253                  "/descendant::span[@class='textvalue']";
 254  
 255              // Expanding the level input boxes.
 256              $spannode = $this->find('xpath', $inputxpath . $spansufix . '|' . $textareaxpath . $spansufix);
 257              $spannode->click();
 258  
 259              $inputfield = $this->find('xpath', $inputxpath . '|' . $textareaxpath);
 260              $inputfield->setValue($value);
 261  
 262          } else {
 263              $fieldnode = $this->find('xpath', $inputxpath . '|' . $textareaxpath);
 264              $this->set_rubric_field_value($fieldnode->getAttribute('name'), $value);
 265          }
 266  
 267      }
 268  
 269      /**
 270       * Grades filling the current page rubric. Set one line per criterion and for each criterion set "| Criterion name | Points | Remark |".
 271       *
 272       * @When /^I grade by filling the rubric with:$/
 273       *
 274       * @throws ExpectationException
 275       * @param TableNode $rubric
 276       */
 277      public function i_grade_by_filling_the_rubric_with(TableNode $rubric) {
 278  
 279          $criteria = $rubric->getRowsHash();
 280  
 281          $stepusage = '"I grade by filling the rubric with:" step needs you to provide a table where each row is a criterion' .
 282              ' and each criterion has 3 different values: | Criterion name | Number of points | Remark text |';
 283  
 284          // If running Javascript, ensure we zoom in before filling the grades.
 285          if ($this->running_javascript()) {
 286              $this->execute('behat_general::click_link', get_string('togglezoom', 'mod_assign'));
 287          }
 288  
 289          // First element -> name, second -> points, third -> Remark.
 290          foreach ($criteria as $name => $criterion) {
 291  
 292              // We only expect the points and the remark, as the criterion name is $name.
 293              if (count($criterion) !== 2) {
 294                  throw new ExpectationException($stepusage, $this->getSession());
 295              }
 296  
 297              // Numeric value here.
 298              $points = $criterion[0];
 299              if (!is_numeric($points)) {
 300                  throw new ExpectationException($stepusage, $this->getSession());
 301              }
 302  
 303              // Selecting a value.
 304              // When JS is disabled there are radio options, with JS enabled divs.
 305              $selectedlevelxpath = $this->get_level_xpath($points);
 306              if ($this->running_javascript()) {
 307  
 308                  // Only clicking on the selected level if it was not already selected.
 309                  $levelnode = $this->find('xpath', $selectedlevelxpath);
 310  
 311                  // Using in_array() as there are only a few elements.
 312                  if (!$levelnode->hasClass('checked')) {
 313                      $levelnodexpath = $selectedlevelxpath . "//div[contains(concat(' ', normalize-space(@class), ' '), ' score ')]";
 314                      $this->execute('behat_general::i_click_on_in_the',
 315                          array($levelnodexpath, "xpath_element", $this->escape($name), "table_row")
 316                      );
 317                  }
 318  
 319              } else {
 320  
 321                  // Getting the name of the field.
 322                  $radioxpath = $this->get_criterion_xpath($name) .
 323                      $selectedlevelxpath . "/descendant::input[@type='radio']";
 324                  $radionode = $this->find('xpath', $radioxpath);
 325                  // which will delegate the process to the field type.
 326                  $radionode->setValue($radionode->getAttribute('value'));
 327              }
 328  
 329              // Setting the remark.
 330  
 331              // First we need to get the textarea name, then we can set the value.
 332              $textarea = $this->get_node_in_container('css_element', 'textarea', 'table_row', $name);
 333              $this->execute('behat_forms::i_set_the_field_to', array($textarea->getAttribute('name'), $criterion[1]));
 334          }
 335  
 336          // If running Javascript, then ensure to close zoomed rubric.
 337          if ($this->running_javascript()) {
 338              $this->execute('behat_general::click_link', get_string('togglezoom', 'mod_assign'));
 339          }
 340      }
 341  
 342      /**
 343       * Checks that the level was previously selected and the user changed to another level.
 344       *
 345       * @Then /^the level with "(?P<points_number>\d+)" points was previously selected for the rubric criterion "(?P<criterion_name_string>(?:[^"]|\\")*)"$/
 346       * @throws ExpectationException
 347       * @param string $criterionname
 348       * @param int $points
 349       * @return void
 350       */
 351      public function the_level_with_points_was_previously_selected_for_the_rubric_criterion($points, $criterionname) {
 352  
 353          $levelxpath = $this->get_criterion_xpath($criterionname) .
 354              $this->get_level_xpath($points) .
 355              "[contains(concat(' ', normalize-space(@class), ' '), ' currentchecked ')]";
 356  
 357          // Works both for JS and non-JS.
 358          // - JS: Class -> checked is there when is marked as green.
 359          // - Non-JS: When editing a rubric definition, there are radio inputs and when viewing a
 360          //   grade @class contains checked.
 361          $levelxpath .= "[not(contains(concat(' ', normalize-space(@class), ' '), ' checked '))]" .
 362              "[not(/descendant::input[@type='radio'][@checked!='checked'])]";
 363  
 364          try {
 365              $this->find('xpath', $levelxpath);
 366          } catch (ElementNotFoundException $e) {
 367              throw new ExpectationException('"' . $points . '" points level was not previously selected', $this->getSession());
 368          }
 369      }
 370  
 371      /**
 372       * Checks that the level is currently selected. Works both when grading rubrics and viewing graded rubrics.
 373       *
 374       * @Then /^the level with "(?P<points_number>\d+)" points is selected for the rubric criterion "(?P<criterion_name_string>(?:[^"]|\\")*)"$/
 375       * @throws ExpectationException
 376       * @param string $criterionname
 377       * @param int $points
 378       * @return void
 379       */
 380      public function the_level_with_points_is_selected_for_the_rubric_criterion($points, $criterionname) {
 381  
 382          $levelxpath = $this->get_criterion_xpath($criterionname) .
 383              $this->get_level_xpath($points);
 384  
 385          // Works both for JS and non-JS.
 386          // - JS: Class -> checked is there when is marked as green.
 387          // - Non-JS: When editing a rubric definition, there are radio inputs and when viewing a
 388          //   grade @class contains checked.
 389          $levelxpath .= "[" .
 390              "contains(concat(' ', normalize-space(@class), ' '), ' checked ')" .
 391              " or " .
 392              "/descendant::input[@type='radio'][@checked='checked']" .
 393              "]";
 394  
 395          try {
 396              $this->find('xpath', $levelxpath);
 397          } catch (ElementNotFoundException $e) {
 398              throw new ExpectationException('"' . $points . '" points level is not selected', $this->getSession());
 399          }
 400      }
 401  
 402      /**
 403       * Checks that the level is not currently selected. Works both when grading rubrics and viewing graded rubrics.
 404       *
 405       * @Then /^the level with "(?P<points_number>\d+)" points is not selected for the rubric criterion "(?P<criterion_name_string>(?:[^"]|\\")*)"$/
 406       * @throws ExpectationException
 407       * @param string $criterionname
 408       * @param int $points
 409       * @return void
 410       */
 411      public function the_level_with_points_is_not_selected_for_the_rubric_criterion($points, $criterionname) {
 412  
 413          $levelxpath = $this->get_criterion_xpath($criterionname) .
 414              $this->get_level_xpath($points);
 415  
 416          // Works both for JS and non-JS.
 417          // - JS: Class -> checked is there when is marked as green.
 418          // - Non-JS: When editing a rubric definition, there are radio inputs and when viewing a
 419          //   grade @class contains checked.
 420          $levelxpath .= "[not(contains(concat(' ', normalize-space(@class), ' '), ' checked '))]" .
 421              "[./descendant::input[@type='radio'][@checked!='checked'] or not(./descendant::input[@type='radio'])]";
 422  
 423          try {
 424              $this->find('xpath', $levelxpath);
 425          } catch (ElementNotFoundException $e) {
 426              throw new ExpectationException('"' . $points . '" points level is selected', $this->getSession());
 427          }
 428      }
 429  
 430  
 431      /**
 432       * Makes a hidden rubric field visible (if necessary) and sets a value on it.
 433       *
 434       * @param string $name The name of the field
 435       * @param string $value The value to set
 436       * @param bool $visible
 437       * @return void
 438       */
 439      protected function set_rubric_field_value($name, $value, $visible = false) {
 440  
 441          // Fields are hidden by default.
 442          if ($this->running_javascript() == true && $visible === false) {
 443              $xpath = "//*[@name='$name']/following-sibling::*[contains(concat(' ', normalize-space(@class), ' '), ' plainvalue ')]";
 444              $textnode = $this->find('xpath', $xpath);
 445              $textnode->click();
 446          }
 447  
 448          // Set the value now.
 449          $description = $this->find_field($name);
 450          $description->setValue($value);
 451      }
 452  
 453      /**
 454       * Performs click confirming the action.
 455       *
 456       * @param NodeElement $node
 457       * @return void
 458       */
 459      protected function click_and_confirm($node) {
 460  
 461          // Clicks to perform the action.
 462          $node->click();
 463  
 464          // Confirms the delete.
 465          if ($this->running_javascript()) {
 466              $confirmbutton = $this->get_node_in_container(
 467                  'button',
 468                  get_string('yes'),
 469                  'dialogue',
 470                  get_string('confirmation', 'admin')
 471              );
 472              $confirmbutton->click();
 473          }
 474      }
 475  
 476      /**
 477       * Returns the xpath representing a selected level.
 478       *
 479       * It is not including the path to the criterion.
 480       *
 481       * It is the xpath when grading a rubric or viewing a rubric,
 482       * it is not the same xpath when editing a rubric.
 483       *
 484       * @param int $points
 485       * @return string
 486       */
 487      protected function get_level_xpath($points) {
 488          return "//td[contains(concat(' ', normalize-space(@class), ' '), ' level ')]" .
 489              "[./descendant::span[@class='scorevalue'][text()='$points']]";
 490      }
 491  
 492      /**
 493       * Returns the xpath representing the selected criterion.
 494       *
 495       * It is the xpath when grading a rubric or viewing a rubric,
 496       * it is not the same xpath when editing a rubric.
 497       *
 498       * @param string $criterionname Literal including the criterion name.
 499       * @return string
 500       */
 501      protected function get_criterion_xpath($criterionname) {
 502          $literal = behat_context_helper::escape($criterionname);
 503          return "//tr[contains(concat(' ', normalize-space(@class), ' '), ' criterion ')]" .
 504              "[./descendant::td[@class='description'][text()=$literal]]";
 505      }
 506  }