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   * Steps definitions related to mod_quiz.
  19   *
  20   * @package   mod_quiz
  21   * @category  test
  22   * @copyright 2014 Marina Glancy
  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  require_once (__DIR__ . '/../../../../question/tests/behat/behat_question_base.php');
  30  
  31  use Behat\Gherkin\Node\TableNode as TableNode;
  32  
  33  use Behat\Mink\Exception\ExpectationException as ExpectationException;
  34  use mod_quiz\quiz_attempt;
  35  use mod_quiz\quiz_settings;
  36  
  37  /**
  38   * Steps definitions related to mod_quiz.
  39   *
  40   * @copyright 2014 Marina Glancy
  41   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  42   */
  43  class behat_mod_quiz extends behat_question_base {
  44  
  45      /**
  46       * Convert page names to URLs for steps like 'When I am on the "[page name]" page'.
  47       *
  48       * Recognised page names are:
  49       * | None so far!      |                                                              |
  50       *
  51       * @param string $page name of the page, with the component name removed e.g. 'Admin notification'.
  52       * @return moodle_url the corresponding URL.
  53       * @throws Exception with a meaningful error message if the specified page cannot be found.
  54       */
  55      protected function resolve_page_url(string $page): moodle_url {
  56          switch (strtolower($page)) {
  57              default:
  58                  throw new Exception('Unrecognised quiz page type "' . $page . '."');
  59          }
  60      }
  61  
  62      /**
  63       * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'.
  64       *
  65       * Recognised page names are:
  66       * | pagetype          | name meaning                                | description                                  |
  67       * | View              | Quiz name                                   | The quiz info page (view.php)                |
  68       * | Edit              | Quiz name                                   | The edit quiz page (edit.php)                |
  69       * | Group overrides   | Quiz name                                   | The manage group overrides page              |
  70       * | User overrides    | Quiz name                                   | The manage user overrides page               |
  71       * | Grades report     | Quiz name                                   | The overview report for a quiz               |
  72       * | Responses report  | Quiz name                                   | The responses report for a quiz              |
  73       * | Manual grading report | Quiz name                               | The manual grading report for a quiz         |
  74       * | Statistics report | Quiz name                                   | The statistics report for a quiz             |
  75       * | Attempt review    | Quiz name > username > [Attempt] attempt no | Review page for a given attempt (review.php) |
  76       * | Question bank     | Quiz name                                   | The question bank page for a quiz            |
  77       *
  78       * @param string $type identifies which type of page this is, e.g. 'Attempt review'.
  79       * @param string $identifier identifies the particular page, e.g. 'Test quiz > student > Attempt 1'.
  80       * @return moodle_url the corresponding URL.
  81       * @throws Exception with a meaningful error message if the specified page cannot be found.
  82       */
  83      protected function resolve_page_instance_url(string $type, string $identifier): moodle_url {
  84          global $DB;
  85  
  86          switch (strtolower($type)) {
  87              case 'view':
  88                  return new moodle_url('/mod/quiz/view.php',
  89                          ['id' => $this->get_cm_by_quiz_name($identifier)->id]);
  90  
  91              case 'edit':
  92                  return new moodle_url('/mod/quiz/edit.php',
  93                          ['cmid' => $this->get_cm_by_quiz_name($identifier)->id]);
  94  
  95              case 'group overrides':
  96                  return new moodle_url('/mod/quiz/overrides.php',
  97                      ['cmid' => $this->get_cm_by_quiz_name($identifier)->id, 'mode' => 'group']);
  98  
  99              case 'user overrides':
 100                  return new moodle_url('/mod/quiz/overrides.php',
 101                      ['cmid' => $this->get_cm_by_quiz_name($identifier)->id, 'mode' => 'user']);
 102  
 103              case 'grades report':
 104                  return new moodle_url('/mod/quiz/report.php',
 105                      ['id' => $this->get_cm_by_quiz_name($identifier)->id, 'mode' => 'overview']);
 106  
 107              case 'responses report':
 108                  return new moodle_url('/mod/quiz/report.php',
 109                      ['id' => $this->get_cm_by_quiz_name($identifier)->id, 'mode' => 'responses']);
 110  
 111              case 'statistics report':
 112                  return new moodle_url('/mod/quiz/report.php',
 113                      ['id' => $this->get_cm_by_quiz_name($identifier)->id, 'mode' => 'statistics']);
 114  
 115              case 'manual grading report':
 116                  return new moodle_url('/mod/quiz/report.php',
 117                          ['id' => $this->get_cm_by_quiz_name($identifier)->id, 'mode' => 'grading']);
 118              case 'attempt view':
 119                  list($quizname, $username, $attemptno, $pageno) = explode(' > ', $identifier);
 120                  $pageno = intval($pageno);
 121                  $pageno = $pageno > 0 ? $pageno - 1 : 0;
 122                  $attemptno = (int) trim(str_replace ('Attempt', '', $attemptno));
 123                  $quiz = $this->get_quiz_by_name($quizname);
 124                  $quizcm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
 125                  $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
 126                  $attempt = $DB->get_record('quiz_attempts',
 127                      ['quiz' => $quiz->id, 'userid' => $user->id, 'attempt' => $attemptno], '*', MUST_EXIST);
 128                  return new moodle_url('/mod/quiz/attempt.php', [
 129                      'attempt' => $attempt->id,
 130                      'cmid' => $quizcm->id,
 131                      'page' => $pageno
 132                  ]);
 133              case 'attempt review':
 134                  if (substr_count($identifier, ' > ') !== 2) {
 135                      throw new coding_exception('For "attempt review", name must be ' .
 136                              '"{Quiz name} > {username} > Attempt {attemptnumber}", ' .
 137                              'for example "Quiz 1 > student > Attempt 1".');
 138                  }
 139                  list($quizname, $username, $attemptno) = explode(' > ', $identifier);
 140                  $attemptno = (int) trim(str_replace ('Attempt', '', $attemptno));
 141                  $quiz = $this->get_quiz_by_name($quizname);
 142                  $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
 143                  $attempt = $DB->get_record('quiz_attempts',
 144                          ['quiz' => $quiz->id, 'userid' => $user->id, 'attempt' => $attemptno], '*', MUST_EXIST);
 145                  return new moodle_url('/mod/quiz/review.php', ['attempt' => $attempt->id]);
 146  
 147              case 'question bank':
 148                  return new moodle_url('/question/edit.php', [
 149                      'cmid' => $this->get_cm_by_quiz_name($identifier)->id,
 150                  ]);
 151  
 152  
 153              default:
 154                  throw new Exception('Unrecognised quiz page type "' . $type . '."');
 155          }
 156      }
 157  
 158      /**
 159       * Get a quiz by name.
 160       *
 161       * @param string $name quiz name.
 162       * @return stdClass the corresponding DB row.
 163       */
 164      protected function get_quiz_by_name(string $name): stdClass {
 165          global $DB;
 166          return $DB->get_record('quiz', ['name' => $name], '*', MUST_EXIST);
 167      }
 168  
 169      /**
 170       * Get a quiz cmid from the quiz name.
 171       *
 172       * @param string $name quiz name.
 173       * @return stdClass cm from get_coursemodule_from_instance.
 174       */
 175      protected function get_cm_by_quiz_name(string $name): stdClass {
 176          $quiz = $this->get_quiz_by_name($name);
 177          return get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
 178      }
 179  
 180      /**
 181       * Put the specified questions on the specified pages of a given quiz.
 182       *
 183       * The first row should be column names:
 184       * | question | page | maxmark | requireprevious |
 185       * The first two of those are required. The others are optional.
 186       *
 187       * question        needs to uniquely match a question name.
 188       * page            is a page number. Must start at 1, and on each following
 189       *                 row should be the same as the previous, or one more.
 190       * maxmark         What the question is marked out of. Defaults to question.defaultmark.
 191       * requireprevious The question can only be attempted after the previous one was completed.
 192       *
 193       * Then there should be a number of rows of data, one for each question you want to add.
 194       *
 195       * For backwards-compatibility reasons, specifying the column names is optional
 196       * (but strongly encouraged). If not specified, the columns are asseumed to be
 197       * | question | page | maxmark |.
 198       *
 199       * @param string $quizname the name of the quiz to add questions to.
 200       * @param TableNode $data information about the questions to add.
 201       *
 202       * @Given /^quiz "([^"]*)" contains the following questions:$/
 203       */
 204      public function quiz_contains_the_following_questions($quizname, TableNode $data) {
 205          global $DB;
 206  
 207          $quiz = $this->get_quiz_by_name($quizname);
 208  
 209          // Deal with backwards-compatibility, optional first row.
 210          $firstrow = $data->getRow(0);
 211          if (!in_array('question', $firstrow) && !in_array('page', $firstrow)) {
 212              if (count($firstrow) == 2) {
 213                  $headings = ['question', 'page'];
 214              } else if (count($firstrow) == 3) {
 215                  $headings = ['question', 'page', 'maxmark'];
 216              } else {
 217                  throw new ExpectationException('When adding questions to a quiz, you should give 2 or three 3 things: ' .
 218                          ' the question name, the page number, and optionally the maximum mark. ' .
 219                          count($firstrow) . ' values passed.', $this->getSession());
 220              }
 221              $rows = $data->getRows();
 222              array_unshift($rows, $headings);
 223              $data = new TableNode($rows);
 224          }
 225  
 226          // Add the questions.
 227          $lastpage = 0;
 228          foreach ($data->getHash() as $questiondata) {
 229              if (!array_key_exists('question', $questiondata)) {
 230                  throw new ExpectationException('When adding questions to a quiz, ' .
 231                          'the question name column is required.', $this->getSession());
 232              }
 233              if (!array_key_exists('page', $questiondata)) {
 234                  throw new ExpectationException('When adding questions to a quiz, ' .
 235                          'the page number column is required.', $this->getSession());
 236              }
 237  
 238              // Question id, category and type.
 239              $sql = 'SELECT q.id AS id, qbe.questioncategoryid AS category, q.qtype AS qtype
 240                        FROM {question} q
 241                        JOIN {question_versions} qv ON qv.questionid = q.id
 242                        JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 243                       WHERE q.name = :name';
 244              $question = $DB->get_record_sql($sql, ['name' => $questiondata['question']], MUST_EXIST);
 245  
 246              // Page number.
 247              $page = clean_param($questiondata['page'], PARAM_INT);
 248              if ($page <= 0 || (string) $page !== $questiondata['page']) {
 249                  throw new ExpectationException('The page number for question "' .
 250                           $questiondata['question'] . '" must be a positive integer.',
 251                          $this->getSession());
 252              }
 253              if ($page < $lastpage || $page > $lastpage + 1) {
 254                  throw new ExpectationException('When adding questions to a quiz, ' .
 255                          'the page number for each question must either be the same, ' .
 256                          'or one more, then the page number for the previous question.',
 257                          $this->getSession());
 258              }
 259              $lastpage = $page;
 260  
 261              // Max mark.
 262              if (!array_key_exists('maxmark', $questiondata) || $questiondata['maxmark'] === '') {
 263                  $maxmark = null;
 264              } else {
 265                  $maxmark = clean_param($questiondata['maxmark'], PARAM_LOCALISEDFLOAT);
 266                  if (!is_numeric($maxmark) || $maxmark < 0) {
 267                      throw new ExpectationException('The max mark for question "' .
 268                              $questiondata['question'] . '" must be a positive number.',
 269                              $this->getSession());
 270                  }
 271              }
 272  
 273              if ($question->qtype == 'random') {
 274                  if (!array_key_exists('includingsubcategories', $questiondata) || $questiondata['includingsubcategories'] === '') {
 275                      $includingsubcategories = false;
 276                  } else {
 277                      $includingsubcategories = clean_param($questiondata['includingsubcategories'], PARAM_BOOL);
 278                  }
 279                  quiz_add_random_questions($quiz, $page, $question->category, 1, $includingsubcategories);
 280              } else {
 281                  // Add the question.
 282                  quiz_add_quiz_question($question->id, $quiz, $page, $maxmark);
 283              }
 284  
 285              // Display number (allowing editable customised question number).
 286              if (array_key_exists('displaynumber', $questiondata)) {
 287                  $slot = $DB->get_field('quiz_slots', 'MAX(slot)', ['quizid' => $quiz->id]);
 288                  $DB->set_field('quiz_slots', 'displaynumber', $questiondata['displaynumber'],
 289                          ['quizid' => $quiz->id, 'slot' => $slot]);
 290                  if (!is_number($questiondata['displaynumber']) && !is_string($questiondata['displaynumber'])) {
 291                      throw new ExpectationException('Displayed question number for "' . $questiondata['question'] .
 292                              '" should either be \'i\', automatically numbered (eg. 1, 2, 3),
 293                              or customised (eg. A.1, A.2, 1.1, 1.2)', $this->getSession());
 294                  }
 295              }
 296  
 297              // Require previous.
 298              if (array_key_exists('requireprevious', $questiondata)) {
 299                  if ($questiondata['requireprevious'] === '1') {
 300                      $slot = $DB->get_field('quiz_slots', 'MAX(slot)', ['quizid' => $quiz->id]);
 301                      $DB->set_field('quiz_slots', 'requireprevious', 1,
 302                              ['quizid' => $quiz->id, 'slot' => $slot]);
 303                  } else if ($questiondata['requireprevious'] !== '' && $questiondata['requireprevious'] !== '0') {
 304                      throw new ExpectationException('Require previous for question "' .
 305                              $questiondata['question'] . '" should be 0, 1 or blank.',
 306                              $this->getSession());
 307                  }
 308              }
 309          }
 310  
 311          $quizobj = quiz_settings::create($quiz->id);
 312          $quizobj->get_grade_calculator()->recompute_quiz_sumgrades();
 313      }
 314  
 315      /**
 316       * Put the specified section headings to start at specified pages of a given quiz.
 317       *
 318       * The first row should be column names:
 319       * | heading | firstslot | shufflequestions |
 320       *
 321       * heading   is the section heading text
 322       * firstslot is the slot number where the section starts
 323       * shuffle   whether this section is shuffled (0 or 1)
 324       *
 325       * Then there should be a number of rows of data, one for each section you want to add.
 326       *
 327       * @param string $quizname the name of the quiz to add sections to.
 328       * @param TableNode $data information about the sections to add.
 329       *
 330       * @Given /^quiz "([^"]*)" contains the following sections:$/
 331       */
 332      public function quiz_contains_the_following_sections($quizname, TableNode $data) {
 333          global $DB;
 334  
 335          $quiz = $DB->get_record('quiz', ['name' => $quizname], '*', MUST_EXIST);
 336  
 337          // Add the sections.
 338          $previousfirstslot = 0;
 339          foreach ($data->getHash() as $rownumber => $sectiondata) {
 340              if (!array_key_exists('heading', $sectiondata)) {
 341                  throw new ExpectationException('When adding sections to a quiz, ' .
 342                          'the heading name column is required.', $this->getSession());
 343              }
 344              if (!array_key_exists('firstslot', $sectiondata)) {
 345                  throw new ExpectationException('When adding sections to a quiz, ' .
 346                          'the firstslot name column is required.', $this->getSession());
 347              }
 348              if (!array_key_exists('shuffle', $sectiondata)) {
 349                  throw new ExpectationException('When adding sections to a quiz, ' .
 350                          'the shuffle name column is required.', $this->getSession());
 351              }
 352  
 353              if ($rownumber == 0) {
 354                  $section = $DB->get_record('quiz_sections', ['quizid' => $quiz->id], '*', MUST_EXIST);
 355              } else {
 356                  $section = new stdClass();
 357                  $section->quizid = $quiz->id;
 358              }
 359  
 360              // Heading.
 361              $section->heading = $sectiondata['heading'];
 362  
 363              // First slot.
 364              $section->firstslot = clean_param($sectiondata['firstslot'], PARAM_INT);
 365              if ($section->firstslot <= $previousfirstslot ||
 366                      (string) $section->firstslot !== $sectiondata['firstslot']) {
 367                  throw new ExpectationException('The firstslot number for section "' .
 368                          $sectiondata['heading'] . '" must an integer greater than the previous section firstslot.',
 369                          $this->getSession());
 370              }
 371              if ($rownumber == 0 && $section->firstslot != 1) {
 372                  throw new ExpectationException('The first section must have firstslot set to 1.',
 373                          $this->getSession());
 374              }
 375  
 376              // Shuffle.
 377              $section->shufflequestions = clean_param($sectiondata['shuffle'], PARAM_INT);
 378              if ((string) $section->shufflequestions !== $sectiondata['shuffle']) {
 379                  throw new ExpectationException('The shuffle value for section "' .
 380                          $sectiondata['heading'] . '" must be 0 or 1.',
 381                          $this->getSession());
 382              }
 383  
 384              if ($rownumber == 0) {
 385                  $DB->update_record('quiz_sections', $section);
 386              } else {
 387                  $DB->insert_record('quiz_sections', $section);
 388              }
 389          }
 390  
 391          if ($section->firstslot > $DB->count_records('quiz_slots', ['quizid' => $quiz->id])) {
 392              throw new ExpectationException('The section firstslot must be less than the total number of slots in the quiz.',
 393                      $this->getSession());
 394          }
 395      }
 396  
 397      /**
 398       * Adds a question to the existing quiz with filling the form.
 399       *
 400       * The form for creating a question should be on one page.
 401       *
 402       * @When /^I add a "(?P<question_type_string>(?:[^"]|\\")*)" question to the "(?P<quiz_name_string>(?:[^"]|\\")*)" quiz with:$/
 403       * @param string $questiontype
 404       * @param string $quizname
 405       * @param TableNode $questiondata with data for filling the add question form
 406       */
 407      public function i_add_question_to_the_quiz_with($questiontype, $quizname, TableNode $questiondata) {
 408          $quizname = $this->escape($quizname);
 409          $addaquestion = $this->escape(get_string('addaquestion', 'quiz'));
 410  
 411          $this->execute('behat_navigation::i_am_on_page_instance', [
 412              $quizname,
 413              'mod_quiz > Edit',
 414          ]);
 415  
 416          if ($this->running_javascript()) {
 417              $this->execute("behat_action_menu::i_open_the_action_menu_in", ['.slots', "css_element"]);
 418              $this->execute("behat_action_menu::i_choose_in_the_open_action_menu", [$addaquestion]);
 419          } else {
 420              $this->execute('behat_general::click_link', $addaquestion);
 421          }
 422  
 423          $this->finish_adding_question($questiontype, $questiondata);
 424      }
 425  
 426      /**
 427       * Set the max mark for a question on the Edit quiz page.
 428       *
 429       * @When /^I set the max mark for question "(?P<question_name_string>(?:[^"]|\\")*)" to "(?P<new_mark_string>(?:[^"]|\\")*)"$/
 430       * @param string $questionname the name of the question to set the max mark for.
 431       * @param string $newmark the mark to set
 432       */
 433      public function i_set_the_max_mark_for_quiz_question($questionname, $newmark) {
 434          $this->execute('behat_general::click_link', $this->escape(get_string('editmaxmark', 'quiz')));
 435  
 436          $this->execute('behat_general::wait_until_exists', ["li input[name=maxmark]", "css_element"]);
 437  
 438          $this->execute('behat_general::assert_page_contains_text', $this->escape(get_string('edittitleinstructions')));
 439  
 440          $this->execute('behat_general::i_type', [$newmark]);
 441          $this->execute('behat_general::i_press_named_key', ['', 'enter']);
 442      }
 443  
 444      /**
 445       * Open the add menu on a given page, or at the end of the Edit quiz page.
 446       * @Given /^I open the "(?P<page_n_or_last_string>(?:[^"]|\\")*)" add to quiz menu$/
 447       * @param string $pageorlast either "Page n" or "last".
 448       */
 449      public function i_open_the_add_to_quiz_menu_for($pageorlast) {
 450  
 451          if (!$this->running_javascript()) {
 452              throw new DriverException('Activities actions menu not available when Javascript is disabled');
 453          }
 454  
 455          if ($pageorlast == 'last') {
 456              $xpath = "//div[@class = 'last-add-menu']//a[contains(@data-toggle, 'dropdown') and contains(., 'Add')]";
 457          } else if (preg_match('~Page (\d+)~', $pageorlast, $matches)) {
 458              $xpath = "//li[@id = 'page-{$matches[1]}']//a[contains(@data-toggle, 'dropdown') and contains(., 'Add')]";
 459          } else {
 460              throw new ExpectationException("The I open the add to quiz menu step must specify either 'Page N' or 'last'.",
 461                  $this->getSession());
 462          }
 463          $this->find('xpath', $xpath)->click();
 464      }
 465  
 466      /**
 467       * Check whether a particular question is on a particular page of the quiz on the Edit quiz page.
 468       * @Given /^I should see "(?P<question_name>(?:[^"]|\\")*)" on quiz page "(?P<page_number>\d+)"$/
 469       * @param string $questionname the name of the question we are looking for.
 470       * @param number $pagenumber the page it should be found on.
 471       */
 472      public function i_should_see_on_quiz_page($questionname, $pagenumber) {
 473          $xpath = "//li[contains(., '" . $this->escape($questionname) .
 474              "')][./preceding-sibling::li[contains(@class, 'pagenumber')][1][contains(., 'Page " .
 475              $pagenumber . "')]]";
 476  
 477          $this->execute('behat_general::should_exist', [$xpath, 'xpath_element']);
 478      }
 479  
 480      /**
 481       * Check whether a particular question is not on a particular page of the quiz on the Edit quiz page.
 482       * @Given /^I should not see "(?P<question_name>(?:[^"]|\\")*)" on quiz page "(?P<page_number>\d+)"$/
 483       * @param string $questionname the name of the question we are looking for.
 484       * @param number $pagenumber the page it should be found on.
 485       */
 486      public function i_should_not_see_on_quiz_page($questionname, $pagenumber) {
 487          $xpath = "//li[contains(., '" . $this->escape($questionname) .
 488                  "')][./preceding-sibling::li[contains(@class, 'pagenumber')][1][contains(., 'Page " .
 489                  $pagenumber . "')]]";
 490  
 491          $this->execute('behat_general::should_not_exist', [$xpath, 'xpath_element']);
 492      }
 493  
 494      /**
 495       * Check whether one question comes before another on the Edit quiz page.
 496       * The two questions must be on the same page.
 497       * @Given /^I should see "(?P<first_q_name>(?:[^"]|\\")*)" before "(?P<second_q_name>(?:[^"]|\\")*)" on the edit quiz page$/
 498       * @param string $firstquestionname the name of the question that should come first in order.
 499       * @param string $secondquestionname the name of the question that should come immediately after it in order.
 500       */
 501      public function i_should_see_before_on_the_edit_quiz_page($firstquestionname, $secondquestionname) {
 502          $xpath = "//li[contains(., '" . $this->escape($firstquestionname) .
 503                  "')]/following-sibling::li" .
 504                  "[contains(., '" . $this->escape($secondquestionname) . "')]";
 505  
 506          $this->execute('behat_general::should_exist', [$xpath, 'xpath_element']);
 507      }
 508  
 509      /**
 510       * Check the number displayed alongside a question on the Edit quiz page.
 511       * @Given /^"(?P<question_name>(?:[^"]|\\")*)" should have number "(?P<number>(?:[^"]|\\")*)" on the edit quiz page$/
 512       * @param string $questionname the name of the question we are looking for.
 513       * @param number $number the number (or 'i') that should be displayed beside that question.
 514       */
 515      public function should_have_number_on_the_edit_quiz_page($questionname, $number) {
 516          if ($number !== get_string('infoshort', 'quiz')) {
 517              // Logic here copied from edit_renderer, which is not ideal, but necessary.
 518              $number = get_string('question') . ' ' . $number;
 519          }
 520          $xpath = "//li[contains(@class, 'slot') and contains(., '" . $this->escape($questionname) .
 521                  "')]//span[contains(@class, 'slotnumber') and normalize-space(.) = '" . $this->escape($number) . "']";
 522          $this->execute('behat_general::should_exist', [$xpath, 'xpath_element']);
 523      }
 524  
 525      /**
 526       * Get the xpath for a partcular add/remove page-break icon.
 527       * @param string $addorremoves 'Add' or 'Remove'.
 528       * @param string $questionname the name of the question before the icon.
 529       * @return string the requried xpath.
 530       */
 531      protected function get_xpath_page_break_icon_after_question($addorremoves, $questionname) {
 532          return "//li[contains(@class, 'slot') and contains(., '" . $this->escape($questionname) .
 533                  "')]//a[contains(@class, 'page_split_join') and @title = '" . $addorremoves . " page break']";
 534      }
 535  
 536      /**
 537       * Click the add or remove page-break icon after a particular question.
 538       * @When /^I click on the "(Add|Remove)" page break icon after question "(?P<question_name>(?:[^"]|\\")*)"$/
 539       * @param string $addorremoves 'Add' or 'Remove'.
 540       * @param string $questionname the name of the question before the icon to click.
 541       */
 542      public function i_click_on_the_page_break_icon_after_question($addorremoves, $questionname) {
 543          $xpath = $this->get_xpath_page_break_icon_after_question($addorremoves, $questionname);
 544  
 545          $this->execute("behat_general::i_click_on", [$xpath, "xpath_element"]);
 546      }
 547  
 548      /**
 549       * Assert the add or remove page-break icon after a particular question exists.
 550       * @When /^the "(Add|Remove)" page break icon after question "(?P<question_name>(?:[^"]|\\")*)" should exist$/
 551       * @param string $addorremoves 'Add' or 'Remove'.
 552       * @param string $questionname the name of the question before the icon to click.
 553       * @return array of steps.
 554       */
 555      public function the_page_break_icon_after_question_should_exist($addorremoves, $questionname) {
 556          $xpath = $this->get_xpath_page_break_icon_after_question($addorremoves, $questionname);
 557  
 558          $this->execute('behat_general::should_exist', [$xpath, 'xpath_element']);
 559      }
 560  
 561      /**
 562       * Assert the add or remove page-break icon after a particular question does not exist.
 563       * @When /^the "(Add|Remove)" page break icon after question "(?P<question_name>(?:[^"]|\\")*)" should not exist$/
 564       * @param string $addorremoves 'Add' or 'Remove'.
 565       * @param string $questionname the name of the question before the icon to click.
 566       * @return array of steps.
 567       */
 568      public function the_page_break_icon_after_question_should_not_exist($addorremoves, $questionname) {
 569          $xpath = $this->get_xpath_page_break_icon_after_question($addorremoves, $questionname);
 570  
 571          $this->execute('behat_general::should_not_exist', [$xpath, 'xpath_element']);
 572      }
 573  
 574      /**
 575       * Check the add or remove page-break link after a particular question contains the given parameters in its url.
 576       *
 577       * @When /^the "(Add|Remove)" page break link after question "(?P<question_name>(?:[^"]|\\")*) should contain:$/
 578       * @When /^the "(Add|Remove)" page break link after question "(?P<question_name>(?:[^"]|\\")*) should contain:"$/
 579       * @param string $addorremoves 'Add' or 'Remove'.
 580       * @param string $questionname the name of the question before the icon to click.
 581       * @param TableNode $paramdata with data for checking the page break url
 582       * @return array of steps.
 583       */
 584      public function the_page_break_link_after_question_should_contain($addorremoves, $questionname, $paramdata) {
 585          $xpath = $this->get_xpath_page_break_icon_after_question($addorremoves, $questionname);
 586  
 587          $this->execute("behat_general::i_click_on", [$xpath, "xpath_element"]);
 588      }
 589  
 590      /**
 591       * Set Shuffle for shuffling questions within sections
 592       *
 593       * @param string $heading the heading of the section to change shuffle for.
 594       *
 595       * @Given /^I click on shuffle for section "([^"]*)" on the quiz edit page$/
 596       */
 597      public function i_click_on_shuffle_for_section($heading) {
 598          $xpath = $this->get_xpath_for_shuffle_checkbox($heading);
 599          $checkbox = $this->find('xpath', $xpath);
 600          $this->ensure_node_is_visible($checkbox);
 601          $checkbox->click();
 602      }
 603  
 604      /**
 605       * Check the shuffle checkbox for a particular section.
 606       *
 607       * @param string $heading the heading of the section to check shuffle for
 608       * @param int $value whether the shuffle checkbox should be on or off.
 609       *
 610       * @Given /^shuffle for section "([^"]*)" should be "(On|Off)" on the quiz edit page$/
 611       */
 612      public function shuffle_for_section_should_be($heading, $value) {
 613          $xpath = $this->get_xpath_for_shuffle_checkbox($heading);
 614          $checkbox = $this->find('xpath', $xpath);
 615          $this->ensure_node_is_visible($checkbox);
 616          if ($value == 'On' && !$checkbox->isChecked()) {
 617              $msg = "Shuffle for section '$heading' is not checked, but you are expecting it to be checked ($value). " .
 618                      "Check the line with: \nshuffle for section \"$heading\" should be \"$value\" on the quiz edit page" .
 619                      "\nin your behat script";
 620              throw new ExpectationException($msg, $this->getSession());
 621          } else if ($value == 'Off' && $checkbox->isChecked()) {
 622              $msg = "Shuffle for section '$heading' is checked, but you are expecting it not to be ($value). " .
 623                      "Check the line with: \nshuffle for section \"$heading\" should be \"$value\" on the quiz edit page" .
 624                      "\nin your behat script";
 625              throw new ExpectationException($msg, $this->getSession());
 626          }
 627      }
 628  
 629      /**
 630       * Return the xpath for shuffle checkbox in section heading
 631       * @param string $heading
 632       * @return string
 633       */
 634      protected function get_xpath_for_shuffle_checkbox($heading) {
 635           return "//div[contains(@class, 'section-heading') and contains(., '" . $this->escape($heading) .
 636                  "')]//input[@type = 'checkbox']";
 637      }
 638  
 639      /**
 640       * Move a question on the Edit quiz page by first clicking on the Move icon,
 641       * then clicking one of the "After ..." links.
 642       * @When /^I move "(?P<question_name>(?:[^"]|\\")*)" to "(?P<target>(?:[^"]|\\")*)" in the quiz by clicking the move icon$/
 643       * @param string $questionname the name of the question we are looking for.
 644       * @param string $target the target place to move to. One of the links in the pop-up like
 645       *      "After Page 1" or "After Question N".
 646       */
 647      public function i_move_question_after_item_by_clicking_the_move_icon($questionname, $target) {
 648          $iconxpath = "//li[contains(@class, ' slot ') and contains(., '" . $this->escape($questionname) .
 649                  "')]//span[contains(@class, 'editing_move')]";
 650  
 651          $this->execute("behat_general::i_click_on", [$iconxpath, "xpath_element"]);
 652          $this->execute("behat_general::i_click_on", [$this->escape($target), "button"]);
 653      }
 654  
 655      /**
 656       * Move a question on the Edit quiz page by dragging a given question on top of another item.
 657       * @When /^I move "(?P<question_name>(?:[^"]|\\")*)" to "(?P<target>(?:[^"]|\\")*)" in the quiz by dragging$/
 658       * @param string $questionname the name of the question we are looking for.
 659       * @param string $target the target place to move to. Ether a question name, or "Page N"
 660       */
 661      public function i_move_question_after_item_by_dragging($questionname, $target) {
 662          $iconxpath = "//li[contains(@class, ' slot ') and contains(., '" . $this->escape($questionname) .
 663                  "')]//span[contains(@class, 'editing_move')]//img";
 664          $destinationxpath = "//li[contains(@class, ' slot ') or contains(@class, 'pagenumber ')]" .
 665                  "[contains(., '" . $this->escape($target) . "')]";
 666  
 667          $this->execute('behat_general::i_drag_and_i_drop_it_in',
 668              [$iconxpath, 'xpath_element', $destinationxpath, 'xpath_element']
 669          );
 670      }
 671  
 672      /**
 673       * Delete a question on the Edit quiz page by first clicking on the Delete icon,
 674       * then clicking one of the "After ..." links.
 675       * @When /^I delete "(?P<question_name>(?:[^"]|\\")*)" in the quiz by clicking the delete icon$/
 676       * @param string $questionname the name of the question we are looking for.
 677       * @return array of steps.
 678       */
 679      public function i_delete_question_by_clicking_the_delete_icon($questionname) {
 680          $slotxpath = "//li[contains(@class, ' slot ') and contains(., '" . $this->escape($questionname) .
 681                  "')]";
 682          $deletexpath = "//a[contains(@class, 'editing_delete')]";
 683  
 684          $this->execute("behat_general::i_click_on", [$slotxpath . $deletexpath, "xpath_element"]);
 685  
 686          $this->execute('behat_general::i_click_on_in_the',
 687              ['Yes', "button", "Confirm", "dialogue"]
 688          );
 689      }
 690  
 691      /**
 692       * Set the section heading for a given section on the Edit quiz page
 693       *
 694       * @When /^I change quiz section heading "(?P<section_name_string>(?:[^"]|\\")*)" to "(?P<new_section_heading_string>(?:[^"]|\\")*)"$/
 695       * @param string $sectionname the heading to change.
 696       * @param string $sectionheading the new heading to set.
 697       */
 698      public function i_set_the_section_heading_for($sectionname, $sectionheading) {
 699          // Empty section headings will have a default names of "Untitled heading".
 700          if (empty($sectionname)) {
 701              $sectionname = get_string('sectionnoname', 'quiz');
 702          }
 703          $this->execute('behat_general::click_link', $this->escape("Edit heading '{$sectionname}'"));
 704  
 705          $this->execute('behat_general::assert_page_contains_text', $this->escape(get_string('edittitleinstructions')));
 706  
 707          $this->execute('behat_general::i_press_named_key', ['', 'backspace']);
 708          $this->execute('behat_general::i_type', [$sectionheading]);
 709          $this->execute('behat_general::i_press_named_key', ['', 'enter']);
 710      }
 711  
 712      /**
 713       * Check that a given question comes after a given section heading in the
 714       * quiz navigation block.
 715       *
 716       * @Then /^I should see question "(?P<questionnumber>(?:[^"]|\\")*)" in section "(?P<section_heading_string>(?:[^"]|\\")*)" in the quiz navigation$/
 717       * @param string $questionnumber the number of the question to check.
 718       * @param string $sectionheading which section heading it should appear after.
 719       */
 720      public function i_should_see_question_in_section_in_the_quiz_navigation($questionnumber, $sectionheading) {
 721  
 722          // Using xpath literal to avoid quotes problems.
 723          $questionnumberliteral = behat_context_helper::escape($questionnumber);
 724          $headingliteral = behat_context_helper::escape($sectionheading);
 725  
 726          // Split in two checkings to give more feedback in case of exception.
 727          $exception = new ExpectationException('Question "' . $questionnumber . '" is not in section "' .
 728                  $sectionheading . '" in the quiz navigation.', $this->getSession());
 729          $xpath = "//*[@id = 'mod_quiz_navblock']//*[contains(concat(' ', normalize-space(@class), ' '), ' qnbutton ') and " .
 730                  "contains(., {$questionnumberliteral}) and contains(preceding-sibling::h3[1], {$headingliteral})]";
 731          $this->find('xpath', $xpath, $exception);
 732      }
 733  
 734      /**
 735       * Helper used by user_has_attempted_with_responses,
 736       * user_has_started_an_attempt_at_quiz_with_details, etc.
 737       *
 738       * @param TableNode $attemptinfo data table from the Behat step
 739       * @return array with two elements, $forcedrandomquestions, $forcedvariants,
 740       *      that can be passed to $quizgenerator->create_attempt.
 741       */
 742      protected function extract_forced_randomisation_from_attempt_info(TableNode $attemptinfo) {
 743          global $DB;
 744  
 745          $forcedrandomquestions = [];
 746          $forcedvariants = [];
 747          foreach ($attemptinfo->getHash() as $slotinfo) {
 748              if (empty($slotinfo['slot'])) {
 749                  throw new ExpectationException('When simulating a quiz attempt, ' .
 750                          'the slot column is required.', $this->getSession());
 751              }
 752  
 753              if (!empty($slotinfo['actualquestion'])) {
 754                  $forcedrandomquestions[$slotinfo['slot']] = $DB->get_field('question', 'id',
 755                          ['name' => $slotinfo['actualquestion']], MUST_EXIST);
 756              }
 757  
 758              if (!empty($slotinfo['variant'])) {
 759                  $forcedvariants[$slotinfo['slot']] = (int) $slotinfo['variant'];
 760              }
 761          }
 762          return [$forcedrandomquestions, $forcedvariants];
 763      }
 764  
 765      /**
 766       * Helper used by user_has_attempted_with_responses, user_has_checked_answers_in_their_attempt_at_quiz,
 767       * user_has_input_answers_in_their_attempt_at_quiz, etc.
 768       *
 769       * @param TableNode $attemptinfo data table from the Behat step
 770       * @return array of responses that can be passed to $quizgenerator->submit_responses.
 771       */
 772      protected function extract_responses_from_attempt_info(TableNode $attemptinfo) {
 773          $responses = [];
 774          foreach ($attemptinfo->getHash() as $slotinfo) {
 775              if (empty($slotinfo['slot'])) {
 776                  throw new ExpectationException('When simulating a quiz attempt, ' .
 777                          'the slot column is required.', $this->getSession());
 778              }
 779              if (!array_key_exists('response', $slotinfo)) {
 780                  throw new ExpectationException('When simulating a quiz attempt, ' .
 781                          'the response column is required.', $this->getSession());
 782              }
 783              $responses[$slotinfo['slot']] = $slotinfo['response'];
 784          }
 785          return $responses;
 786      }
 787  
 788      /**
 789       * Attempt a quiz.
 790       *
 791       * The first row should be column names:
 792       * | slot | actualquestion | variant | response |
 793       * The first two of those are required. The others are optional.
 794       *
 795       * slot           The slot
 796       * actualquestion This column is optional, and is only needed if the quiz contains
 797       *                random questions. If so, this will let you control which actual
 798       *                question gets picked when this slot is 'randomised' at the
 799       *                start of the attempt. If you don't specify, then one will be picked
 800       *                at random (which might make the response meaningless).
 801       *                Give the question name.
 802       * variant        This column is similar, and also options. It is only needed if
 803       *                the question that ends up in this slot returns something greater
 804       *                than 1 for $question->get_num_variants(). Like with actualquestion,
 805       *                if you specify a value here it is used the fix the 'random' choice
 806       *                made when the quiz is started.
 807       * response       The response that was submitted. How this is interpreted depends on
 808       *                the question type. It gets passed to
 809       *                {@link core_question_generator::get_simulated_post_data_for_question_attempt()}
 810       *                and therefore to the un_summarise_response method of the question to decode.
 811       *
 812       * Then there should be a number of rows of data, one for each question you want to add.
 813       * There is no need to supply answers to all questions. If so, other qusetions will be
 814       * left unanswered.
 815       *
 816       * @param string $username the username of the user that will attempt.
 817       * @param string $quizname the name of the quiz the user will attempt.
 818       * @param TableNode $attemptinfo information about the questions to add, as above.
 819       * @Given /^user "([^"]*)" has attempted "([^"]*)" with responses:$/
 820       */
 821      public function user_has_attempted_with_responses($username, $quizname, TableNode $attemptinfo) {
 822          global $DB;
 823  
 824          /** @var mod_quiz_generator $quizgenerator */
 825          $quizgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_quiz');
 826  
 827          $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
 828          $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
 829  
 830          list($forcedrandomquestions, $forcedvariants) =
 831                  $this->extract_forced_randomisation_from_attempt_info($attemptinfo);
 832          $responses = $this->extract_responses_from_attempt_info($attemptinfo);
 833  
 834          $this->set_user($user);
 835  
 836          $attempt = $quizgenerator->create_attempt($quizid, $user->id,
 837                  $forcedrandomquestions, $forcedvariants);
 838  
 839          $quizgenerator->submit_responses($attempt->id, $responses, false, true);
 840  
 841          $this->set_user();
 842      }
 843  
 844      /**
 845       * Start a quiz attempt without answers.
 846       *
 847       * @param string $username the username of the user that will attempt.
 848       * @param string $quizname the name of the quiz the user will attempt.
 849       * @Given /^user "([^"]*)" has started an attempt at quiz "([^"]*)"$/
 850       */
 851      public function user_has_started_an_attempt_at_quiz($username, $quizname) {
 852          global $DB;
 853  
 854          /** @var mod_quiz_generator $quizgenerator */
 855          $quizgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_quiz');
 856  
 857          $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
 858          $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
 859          $this->set_user($user);
 860          $quizgenerator->create_attempt($quizid, $user->id);
 861          $this->set_user();
 862      }
 863  
 864      /**
 865       * Start a quiz attempt without answers.
 866       *
 867       * The supplied data table for have a row for each slot where you want
 868       * to force either which random question was chose, or which random variant
 869       * was used, as for {@link user_has_attempted_with_responses()} above.
 870       *
 871       * @param string $username the username of the user that will attempt.
 872       * @param string $quizname the name of the quiz the user will attempt.
 873       * @param TableNode $attemptinfo information about the questions to add, as above.
 874       * @Given /^user "([^"]*)" has started an attempt at quiz "([^"]*)" randomised as follows:$/
 875       */
 876      public function user_has_started_an_attempt_at_quiz_with_details($username, $quizname, TableNode $attemptinfo) {
 877          global $DB;
 878  
 879          /** @var mod_quiz_generator $quizgenerator */
 880          $quizgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_quiz');
 881  
 882          $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
 883          $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
 884  
 885          list($forcedrandomquestions, $forcedvariants) =
 886                  $this->extract_forced_randomisation_from_attempt_info($attemptinfo);
 887  
 888          $this->set_user($user);
 889  
 890          $quizgenerator->create_attempt($quizid, $user->id,
 891                  $forcedrandomquestions, $forcedvariants);
 892  
 893          $this->set_user();
 894      }
 895  
 896      /**
 897       * Input answers to particular questions an existing quiz attempt, without
 898       * simulating a click of the 'Check' button, if any.
 899       *
 900       * Then there should be a number of rows of data, with two columns slot and response,
 901       * as for {@link user_has_attempted_with_responses()} above.
 902       * There is no need to supply answers to all questions. If so, other questions will be
 903       * left unanswered.
 904       *
 905       * @param string $username the username of the user that will attempt.
 906       * @param string $quizname the name of the quiz the user will attempt.
 907       * @param TableNode $attemptinfo information about the questions to add, as above.
 908       * @throws \Behat\Mink\Exception\ExpectationException
 909       * @Given /^user "([^"]*)" has input answers in their attempt at quiz "([^"]*)":$/
 910       */
 911      public function user_has_input_answers_in_their_attempt_at_quiz($username, $quizname, TableNode $attemptinfo) {
 912          global $DB;
 913  
 914          /** @var mod_quiz_generator $quizgenerator */
 915          $quizgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_quiz');
 916  
 917          $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
 918          $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
 919  
 920          $responses = $this->extract_responses_from_attempt_info($attemptinfo);
 921  
 922          $this->set_user($user);
 923  
 924          $attempts = quiz_get_user_attempts($quizid, $user->id, 'unfinished', true);
 925          $quizgenerator->submit_responses(key($attempts), $responses, false, false);
 926  
 927          $this->set_user();
 928      }
 929  
 930      /**
 931       * Submit answers to questions an existing quiz attempt, with a simulated click on the 'Check' button.
 932       *
 933       * This step should only be used with question behaviours that have have
 934       * a 'Check' button. Those include Interactive with multiple tires, Immediate feedback
 935       * and Immediate feedback with CBM.
 936       *
 937       * Then there should be a number of rows of data, with two columns slot and response,
 938       * as for {@link user_has_attempted_with_responses()} above.
 939       * There is no need to supply answers to all questions. If so, other questions will be
 940       * left unanswered.
 941       *
 942       * @param string $username the username of the user that will attempt.
 943       * @param string $quizname the name of the quiz the user will attempt.
 944       * @param TableNode $attemptinfo information about the questions to add, as above.
 945       * @throws \Behat\Mink\Exception\ExpectationException
 946       * @Given /^user "([^"]*)" has checked answers in their attempt at quiz "([^"]*)":$/
 947       */
 948      public function user_has_checked_answers_in_their_attempt_at_quiz($username, $quizname, TableNode $attemptinfo) {
 949          global $DB;
 950  
 951          /** @var mod_quiz_generator $quizgenerator */
 952          $quizgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_quiz');
 953  
 954          $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
 955          $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
 956  
 957          $responses = $this->extract_responses_from_attempt_info($attemptinfo);
 958  
 959          $this->set_user($user);
 960  
 961          $attempts = quiz_get_user_attempts($quizid, $user->id, 'unfinished', true);
 962          $quizgenerator->submit_responses(key($attempts), $responses, true, false);
 963  
 964          $this->set_user();
 965      }
 966  
 967      /**
 968       * Finish an existing quiz attempt.
 969       *
 970       * @param string $username the username of the user that will attempt.
 971       * @param string $quizname the name of the quiz the user will attempt.
 972       * @Given /^user "([^"]*)" has finished an attempt at quiz "([^"]*)"$/
 973       */
 974      public function user_has_finished_an_attempt_at_quiz($username, $quizname) {
 975          global $DB;
 976  
 977          $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
 978          $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
 979  
 980          $this->set_user($user);
 981  
 982          $attempts = quiz_get_user_attempts($quizid, $user->id, 'unfinished', true);
 983          $attemptobj = quiz_attempt::create(key($attempts));
 984          $attemptobj->process_finish(time(), true);
 985  
 986          $this->set_user();
 987      }
 988  
 989      /**
 990       * Finish an existing quiz attempt.
 991       *
 992       * @param string $quizname the name of the quiz the user will attempt.
 993       * @param string $username the username of the user that will attempt.
 994       * @Given the attempt at :quizname by :username was never submitted
 995       */
 996      public function attempt_was_abandoned($quizname, $username) {
 997          global $DB;
 998  
 999          $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
1000          $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
1001  
1002          $this->set_user($user);
1003  
1004          $attempt = quiz_get_user_attempt_unfinished($quizid, $user->id);
1005          if (!$attempt) {
1006              throw new coding_exception("No in-progress attempt found for $username and quiz $quizname.");
1007          }
1008          $attemptobj = quiz_attempt::create($attempt->id);
1009          $attemptobj->process_abandon(time(), false);
1010  
1011          $this->set_user();
1012      }
1013  
1014      /**
1015       * Return a list of the exact named selectors for the component.
1016       *
1017       * @return behat_component_named_selector[]
1018       */
1019      public static function get_exact_named_selectors(): array {
1020          return [
1021              new behat_component_named_selector('Edit slot',
1022              ["//li[contains(@class,'qtype')]//span[@class='slotnumber' and contains(., %locator%)]/.."])
1023          ];
1024      }
1025  }