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