Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | Ultima modificación | Ver Log |

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