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