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