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