MDL-58428 theme: Shift templates ready for Bootstrapbase removal
[moodle.git] / mod / quiz / tests / behat / behat_mod_quiz.php
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/>.
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  */
26 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
28 require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
29 require_once(__DIR__ . '/../../../../question/tests/behat/behat_question_base.php');
31 use Behat\Gherkin\Node\TableNode as TableNode;
33 use Behat\Mink\Exception\ExpectationException as ExpectationException;
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 {
43     /**
44      * Put the specified questions on the specified pages of a given quiz.
45      *
46      * The first row should be column names:
47      * | question | page | maxmark | requireprevious |
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.
54      * requireprevious The question can only be attempted after the previous one was completed.
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 |.
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;
70         $quiz = $DB->get_record('quiz', array('name' => $quizname), '*', MUST_EXIST);
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 {
80                 throw new ExpectationException('When adding questions to a quiz, you should give 2 or three 3 things: ' .
81                         ' the question name, the page number, and optionally the maximum mark. ' .
82                         count($firstrow) . ' values passed.', $this->getSession());
83             }
84             $rows = $data->getRows();
85             array_unshift($rows, $headings);
86             $data = new TableNode($rows);
87         }
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             }
101             // Question id, category and type.
102             $question = $DB->get_record('question', array('name' => $questiondata['question']), 'id, category, qtype', MUST_EXIST);
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());
116             }
117             $lastpage = $page;
119             // Max mark.
120             if (!array_key_exists('maxmark', $questiondata) || $questiondata['maxmark'] === '') {
121                 $maxmark = null;
122             } else {
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.',
127                             $this->getSession());
128                 }
129             }
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             }
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             }
155         }
157         quiz_update_sumgrades($quiz);
158     }
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;
180         $quiz = $DB->get_record('quiz', array('name' => $quizname), '*', MUST_EXIST);
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             }
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             }
205             // Heading.
206             $section->heading = $sectiondata['heading'];
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             }
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             }
229             if ($rownumber == 0) {
230                 $DB->update_record('quiz_sections', $section);
231             } else {
232                 $DB->insert_record('quiz_sections', $section);
233             }
234         }
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     }
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
250      * @param TableNode $questiondata with data for filling the add question form
251      */
252     public function i_add_question_to_the_quiz_with($questiontype, $quizname, TableNode $questiondata) {
253         $quizname = $this->escape($quizname);
254         $editquiz = $this->escape(get_string('editquiz', 'quiz'));
255         $quizadmin = $this->escape(get_string('pluginadministration', 'quiz'));
256         $addaquestion = $this->escape(get_string('addaquestion', 'quiz'));
258         $this->execute('behat_general::click_link', $quizname);
260         $this->execute("behat_navigation::i_navigate_to_in_current_page_administration",
261                 $quizadmin . ' > ' . $editquiz);
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         }
270         $this->finish_adding_question($questiontype, $questiondata);
271     }
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) {
281         $this->execute('behat_general::click_link', $this->escape(get_string('editmaxmark', 'quiz')));
283         $this->execute('behat_general::wait_until_exists', array("li input[name=maxmark]", "css_element"));
285         $this->execute('behat_general::assert_page_contains_text', $this->escape(get_string('edittitleinstructions')));
287         $this->execute('behat_forms::i_set_the_field_to', array('maxmark', $this->escape($newmark) . chr(10)));
288     }
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) {
297         if (!$this->running_javascript()) {
298             throw new DriverException('Activities actions menu not available when Javascript is disabled');
299         }
301         if ($pageorlast == 'last') {
302             $xpath = "//div[@class = 'last-add-menu']//a[contains(@data-toggle, 'dropdown') and contains(., 'Add')]";
303         } else if (preg_match('~Page (\d+)~', $pageorlast, $matches)) {
304             $xpath = "//li[@id = 'page-{$matches[1]}']//a[contains(@data-toggle, 'dropdown') and contains(., 'Add')]";
305         } else {
306             throw new ExpectationException("The I open the add to quiz menu step must specify either 'Page N' or 'last'.");
307         }
308         $this->find('xpath', $xpath)->click();
309     }
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.
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 . "')]]";
322         $this->execute('behat_general::should_exist', array($xpath, 'xpath_element'));
323     }
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.
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 . "')]]";
336         $this->execute('behat_general::should_not_exist', array($xpath, 'xpath_element'));
337     }
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.
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) . "')]";
351         $this->execute('behat_general::should_exist', array($xpath, 'xpath_element'));
352     }
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.
359      */
360     public function should_have_number_on_the_edit_quiz_page($questionname, $number) {
361         $xpath = "//li[contains(@class, 'slot') and contains(., '" . $this->escape($questionname) .
362                 "')]//span[contains(@class, 'slotnumber') and normalize-space(text()) = '" . $this->escape($number) . "']";
364         $this->execute('behat_general::should_exist', array($xpath, 'xpath_element'));
365     }
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     }
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.
383      */
384     public function i_click_on_the_page_break_icon_after_question($addorremoves, $questionname) {
385         $xpath = $this->get_xpath_page_break_icon_after_question($addorremoves, $questionname);
387         $this->execute("behat_general::i_click_on", array($xpath, "xpath_element"));
388     }
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);
400         $this->execute('behat_general::should_exist', array($xpath, 'xpath_element'));
401     }
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);
413         $this->execute('behat_general::should_not_exist', array($xpath, 'xpath_element'));
414     }
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);
427         $this->execute("behat_general::i_click_on", array($xpath, "xpath_element"));
428     }
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     }
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     }
469     /**
470      * Return the xpath for shuffle checkbox in section heading
471      * @param string $heading
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     }
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".
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')]";
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"));
493     }
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"
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) . "')]";
507         $this->execute('behat_general::i_drag_and_i_drop_it_in',
508             array($iconxpath, 'xpath_element', $destinationxpath, 'xpath_element')
509         );
510     }
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')]";
524         $this->execute("behat_general::i_click_on", array($slotxpath . $deletexpath, "xpath_element"));
526         $this->execute('behat_general::i_click_on_in_the',
527             array('Yes', "button", "Confirm", "dialogue")
528         );
529     }
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) {
539         $this->execute('behat_general::click_link', $this->escape("Edit heading '{$sectionname}'"));
541         $this->execute('behat_general::assert_page_contains_text', $this->escape(get_string('edittitleinstructions')));
543         $this->execute('behat_forms::i_set_the_field_to', array('section', $this->escape($sectionheading) . chr(10)));
544     }
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) {
556         // Using xpath literal to avoid quotes problems.
557         $questionnumberliteral = behat_context_helper::escape('Question ' . $questionnumber);
558         $headingliteral = behat_context_helper::escape($sectionheading);
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());
563         $xpath = "//*[@id = 'mod_quiz_navblock']//*[contains(concat(' ', normalize-space(@class), ' '), ' qnbutton ') and " .
564                 "contains(., {$questionnumberliteral}) and contains(preceding-sibling::h3[1], {$headingliteral})]";
565         $this->find('xpath', $xpath);
566     }
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;
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             }
587             if (!empty($slotinfo['actualquestion'])) {
588                 $forcedrandomquestions[$slotinfo['slot']] = $DB->get_field('question', 'id',
589                         ['name' => $slotinfo['actualquestion']], MUST_EXIST);
590             }
592             if (!empty($slotinfo['variant'])) {
593                 $forcedvariants[$slotinfo['slot']] = (int) $slotinfo['variant'];
594             }
595         }
596         return [$forcedrandomquestions, $forcedvariants];
597     }
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     }
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
631      *                random questions. If so, this will let you control which actual
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
642      *                {@link core_question_generator::get_simulated_post_data_for_question_attempt()}
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      *
649      * @param string $username the username of the user that will attempt.
650      * @param string $quizname the name of the quiz the user will attempt.
651      * @param TableNode $attemptinfo information about the questions to add, as above.
652      * @Given /^user "([^"]*)" has attempted "([^"]*)" with responses:$/
653      */
654     public function user_has_attempted_with_responses($username, $quizname, TableNode $attemptinfo) {
655         global $DB;
657         /** @var mod_quiz_generator $quizgenerator */
658         $quizgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_quiz');
660         $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
661         $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
663         list($forcedrandomquestions, $forcedvariants) =
664                 $this->extract_forced_randomisation_from_attempt_info($attemptinfo);
665         $responses = $this->extract_responses_from_attempt_info($attemptinfo);
667         $this->set_user($user);
669         $attempt = $quizgenerator->create_attempt($quizid, $user->id,
670                 $forcedrandomquestions, $forcedvariants);
672         $quizgenerator->submit_responses($attempt->id, $responses, false, true);
674         $this->set_user();
675     }
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      *
684      * @param string $username the username of the user that will attempt.
685      * @param string $quizname the name of the quiz the user will attempt.
686      * @Given /^user "([^"]*)" has started an attempt at quiz "([^"]*)"$/
687      */
688     public function user_has_started_an_attempt_at_quiz($username, $quizname) {
689         global $DB;
691         /** @var mod_quiz_generator $quizgenerator */
692         $quizgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_quiz');
694         $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
695         $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
696         $this->set_user($user);
697         $quizgenerator->create_attempt($quizid, $user->id);
698         $this->set_user();
699     }
701     /**
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      *
708      * @param string $username the username of the user that will attempt.
709      * @param string $quizname the name of the quiz the user will attempt.
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;
716         /** @var mod_quiz_generator $quizgenerator */
717         $quizgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_quiz');
719         $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
720         $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
722         list($forcedrandomquestions, $forcedvariants) =
723                 $this->extract_forced_randomisation_from_attempt_info($attemptinfo);
725         $this->set_user($user);
727         $quizgenerator->create_attempt($quizid, $user->id,
728                 $forcedrandomquestions, $forcedvariants);
730         $this->set_user();
731     }
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.
741      *
742      * @param string $username the username of the user that will attempt.
743      * @param string $quizname the name of the quiz the user will attempt.
744      * @param TableNode $attemptinfo information about the questions to add, as above.
745      * @throws \Behat\Mink\Exception\ExpectationException
746      * @Given /^user "([^"]*)" has input answers in their attempt at quiz "([^"]*)":$/
747      */
748     public function user_has_input_answers_in_their_attempt_at_quiz($username, $quizname, TableNode $attemptinfo) {
749         global $DB;
751         /** @var mod_quiz_generator $quizgenerator */
752         $quizgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_quiz');
754         $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
755         $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
757         $responses = $this->extract_responses_from_attempt_info($attemptinfo);
759         $this->set_user($user);
761         $attempts = quiz_get_user_attempts($quizid, $user->id, 'unfinished', true);
762         $quizgenerator->submit_responses(key($attempts), $responses, false, false);
764         $this->set_user();
765     }
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      *
779      * @param string $username the username of the user that will attempt.
780      * @param string $quizname the name of the quiz the user will attempt.
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;
788         /** @var mod_quiz_generator $quizgenerator */
789         $quizgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_quiz');
791         $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
792         $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
794         $responses = $this->extract_responses_from_attempt_info($attemptinfo);
796         $this->set_user($user);
798         $attempts = quiz_get_user_attempts($quizid, $user->id, 'unfinished', true);
799         $quizgenerator->submit_responses(key($attempts), $responses, true, false);
801         $this->set_user();
802     }
804     /**
805      * Finish an existing quiz attempt.
806      *
807      * @param string $username the username of the user that will attempt.
808      * @param string $quizname the name of the quiz the user will attempt.
809      * @Given /^user "([^"]*)" has finished an attempt at quiz "([^"]*)"$/
810      */
811     public function user_has_finished_an_attempt_at_quiz($username, $quizname) {
812         global $DB;
814         $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
815         $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
817         $this->set_user($user);
819         $attempts = quiz_get_user_attempts($quizid, $user->id, 'unfinished', true);
820         $attemptobj = quiz_attempt::create(key($attempts));
821         $attemptobj->process_finish(time(), true);
823         $this->set_user();
824     }