Search moodle.org's
Developer Documentation

See Release Notes

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

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

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