MDL-3782 qtype_multianswer: support 'multiple-answer' subquestions
[moodle.git] / question / type / multianswer / tests / walkthrough_test.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * This file contains overall tests of multianswer questions.
19  *
20  * @package    qtype
21  * @subpackage multianswer
22  * @copyright  2011 The Open University
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
27 defined('MOODLE_INTERNAL') || die();
29 global $CFG;
30 require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
33 /**
34  * Unit tests for the multianswer question type.
35  *
36  * @copyright  2011 The Open University
37  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class qtype_multianswer_walkthrough_test extends qbehaviour_walkthrough_test_base {
41     protected function get_contains_subq_status(question_state $state) {
42         return new question_pattern_expectation('~' .
43                 preg_quote($state->default_string(true), '~') . '<br />~');
44     }
46     public function test_deferred_feedback() {
48         // Create a multianswer question.
49         $q = test_question_maker::make_question('multianswer', 'fourmc');
50         $this->start_attempt_at_question($q, 'deferredfeedback', 4);
52         // Check the initial state.
53         $this->check_current_state(question_state::$todo);
54         $this->check_current_mark(null);
55         $this->check_current_output(
56                 $this->get_contains_marked_out_of_summary(),
57                 $this->get_does_not_contain_feedback_expectation(),
58                 $this->get_does_not_contain_validation_error_expectation());
60         // Save in incomplete answer.
61         $this->process_submission(array('sub1_answer' => '1', 'sub2_answer' => '',
62                 'sub3_answer' => '', 'sub4_answer' => ''));
64         // Verify.
65         $this->check_current_state(question_state::$invalid);
66         $this->check_current_mark(null);
67         $this->check_current_output(
68                 $this->get_contains_marked_out_of_summary(),
69                 $this->get_does_not_contain_feedback_expectation(),
70                 $this->get_contains_validation_error_expectation());
72         // Save a partially correct answer.
73         $this->process_submission(array('sub1_answer' => '1', 'sub2_answer' => '1',
74                 'sub3_answer' => '1', 'sub4_answer' => '1'));
76         // Verify.
77         $this->check_current_state(question_state::$complete);
78         $this->check_current_mark(null);
79         $this->check_current_output(
80                 $this->get_contains_marked_out_of_summary(),
81                 $this->get_does_not_contain_feedback_expectation(),
82                 $this->get_does_not_contain_validation_error_expectation());
84         // Now submit all and finish.
85         $this->finish();
87         // Verify.
88         $this->check_current_state(question_state::$gradedpartial);
89         $this->check_current_mark(2);
90         $this->check_current_output(
91                 $this->get_contains_mark_summary(2),
92                 $this->get_contains_partcorrect_expectation(),
93                 $this->get_does_not_contain_validation_error_expectation());
94     }
96     public function test_deferred_feedback_numericalzero_not_answered() {
97         // Tests the situation found in MDL-35370.
99         // Create a multianswer question with one numerical subquestion, right answer zero.
100         $q = test_question_maker::make_question('multianswer', 'numericalzero');
101         $this->start_attempt_at_question($q, 'deferredfeedback', 1);
103         // Check the initial state.
104         $this->check_current_state(question_state::$todo);
105         $this->check_current_mark(null);
106         $this->check_current_output(
107                 $this->get_contains_marked_out_of_summary(),
108                 $this->get_does_not_contain_feedback_expectation(),
109                 $this->get_does_not_contain_validation_error_expectation());
111         // Now submit all and finish.
112         $this->finish();
114         // Verify.
115         $this->check_current_state(question_state::$gaveup);
116         $this->check_current_mark(null);
117         $this->check_current_output(
118                 $this->get_contains_marked_out_of_summary(),
119                 new question_pattern_expectation('~<input[^>]* class="incorrect" [^>]*/>~'),
120                 $this->get_contains_subq_status(question_state::$gaveup),
121                 $this->get_does_not_contain_validation_error_expectation());
122     }
124     public function test_deferred_feedback_numericalzero_0_answer() {
125         // Tests the situation found in MDL-35370.
127         // Create a multianswer question with one numerical subquestion, right answer zero.
128         $q = test_question_maker::make_question('multianswer', 'numericalzero');
129         $this->start_attempt_at_question($q, 'deferredfeedback', 1);
131         // Check the initial state.
132         $this->check_current_state(question_state::$todo);
133         $this->check_current_mark(null);
134         $this->check_current_output(
135                 $this->get_contains_marked_out_of_summary(),
136                 $this->get_does_not_contain_feedback_expectation(),
137                 $this->get_does_not_contain_validation_error_expectation());
139         // Save a the correct answer.
140         $this->process_submission(array('sub1_answer' => '0'));
142         // Verify.
143         $this->check_current_state(question_state::$complete);
144         $this->check_current_mark(null);
145         $this->check_current_output(
146                 $this->get_contains_marked_out_of_summary(),
147                 $this->get_does_not_contain_feedback_expectation(),
148                 $this->get_does_not_contain_validation_error_expectation());
150         // Now submit all and finish.
151         $this->finish();
153         // Verify.
154         $this->check_current_state(question_state::$gradedright);
155         $this->check_current_mark(1);
156         $this->check_current_output(
157                 $this->get_contains_mark_summary(1),
158                 $this->get_contains_correct_expectation(),
159                 $this->get_contains_subq_status(question_state::$gradedright),
160                 $this->get_does_not_contain_validation_error_expectation());
161     }
163     public function test_deferred_feedback_numericalzero_0_wrong() {
164         // Tests the situation found in MDL-35370.
166         // Create a multianswer question with one numerical subquestion, right answer zero.
167         $q = test_question_maker::make_question('multianswer', 'numericalzero');
168         $this->start_attempt_at_question($q, 'deferredfeedback', 1);
170         // Check the initial state.
171         $this->check_current_state(question_state::$todo);
172         $this->check_current_mark(null);
173         $this->check_current_output(
174                 $this->get_contains_marked_out_of_summary(),
175                 $this->get_does_not_contain_feedback_expectation(),
176                 $this->get_does_not_contain_validation_error_expectation());
178         // Save a the correct answer.
179         $this->process_submission(array('sub1_answer' => '42'));
181         // Verify.
182         $this->check_current_state(question_state::$complete);
183         $this->check_current_mark(null);
184         $this->check_current_output(
185                 $this->get_contains_marked_out_of_summary(),
186                 $this->get_does_not_contain_feedback_expectation(),
187                 $this->get_does_not_contain_validation_error_expectation());
189         // Now submit all and finish.
190         $this->finish();
192         // Verify.
193         $this->check_current_state(question_state::$gradedwrong);
194         $this->check_current_mark(0);
195         $this->check_current_output(
196                 $this->get_contains_mark_summary(0),
197                 $this->get_contains_incorrect_expectation(),
198                 $this->get_contains_subq_status(question_state::$gradedwrong),
199                 $this->get_does_not_contain_validation_error_expectation());
200     }
202     public function test_interactive_feedback() {
204         // Create a multianswer question.
205         $q = test_question_maker::make_question('multianswer', 'fourmc');
206         $q->hints = array(
207             new question_hint_with_parts(11, 'This is the first hint.', FORMAT_HTML, false, true),
208             new question_hint_with_parts(12, 'This is the second hint.', FORMAT_HTML, true, true),
209         );
210         $choices = array('' => '', '0' => 'California', '1' => 'Arizona');
212         $this->start_attempt_at_question($q, 'interactive', 4);
214         // Check the initial state.
215         $this->check_current_state(question_state::$todo);
216         $this->check_current_mark(null);
217         $this->assertEquals('interactivecountback',
218                 $this->quba->get_question_attempt($this->slot)->get_behaviour_name());
219         $this->check_current_output(
220                 $this->get_contains_marked_out_of_summary(),
221                 $this->get_contains_select_expectation('sub1_answer', $choices, null, true),
222                 $this->get_contains_select_expectation('sub2_answer', $choices, null, true),
223                 $this->get_contains_select_expectation('sub3_answer', $choices, null, true),
224                 $this->get_contains_select_expectation('sub4_answer', $choices, null, true),
225                 $this->get_contains_submit_button_expectation(true),
226                 $this->get_does_not_contain_validation_error_expectation(),
227                 $this->get_does_not_contain_feedback_expectation(),
228                 $this->get_tries_remaining_expectation(3),
229                 $this->get_does_not_contain_num_parts_correct(),
230                 $this->get_no_hint_visible_expectation());
232         // Submit a completely wrong response.
233         $this->process_submission(array('sub1_answer' => '1', 'sub2_answer' => '0',
234                 'sub3_answer' => '1', 'sub4_answer' => '0', '-submit' => 1));
236         // Verify.
237         $this->check_current_state(question_state::$todo);
238         $this->check_current_mark(null);
239         $this->check_current_output(
240                 $this->get_contains_select_expectation('sub1_answer', $choices, 1, false),
241                 $this->get_contains_select_expectation('sub2_answer', $choices, 0, false),
242                 $this->get_contains_select_expectation('sub3_answer', $choices, 1, false),
243                 $this->get_contains_select_expectation('sub4_answer', $choices, 0, false),
244                 $this->get_does_not_contain_num_parts_correct(),
245                 $this->get_contains_hidden_expectation(
246                         $this->quba->get_field_prefix($this->slot) . 'sub1_answer', ''),
247                 $this->get_contains_hidden_expectation(
248                         $this->quba->get_field_prefix($this->slot) . 'sub2_answer', ''),
249                 $this->get_contains_hidden_expectation(
250                         $this->quba->get_field_prefix($this->slot) . 'sub3_answer', ''),
251                 $this->get_contains_hidden_expectation(
252                         $this->quba->get_field_prefix($this->slot) . 'sub4_answer', ''),
253                 $this->get_does_not_contain_submit_button_expectation(),
254                 $this->get_contains_try_again_button_expectation(true),
255                 $this->get_does_not_contain_correctness_expectation(),
256                 $this->get_contains_hint_expectation('This is the first hint.'));
258         // Check that, if we review in this state, the try again button is disabled.
259         $displayoptions = new question_display_options();
260         $displayoptions->readonly = true;
261         $html = $this->quba->render_question($this->slot, $displayoptions);
262         $this->assert($this->get_contains_try_again_button_expectation(false), $html);
264         // Try again.
265         $this->process_submission(array('sub1_answer' => '',
266                 'sub2_answer' => '', 'sub3_answer' => '',
267                 'sub4_answer' => '', '-tryagain' => 1));
269         // Verify.
270         $this->check_current_state(question_state::$todo);
271         $this->check_current_mark(null);
272         $this->check_current_output(
273                 $this->get_contains_select_expectation('sub1_answer', $choices, null, true),
274                 $this->get_contains_select_expectation('sub2_answer', $choices, null, true),
275                 $this->get_contains_select_expectation('sub3_answer', $choices, null, true),
276                 $this->get_contains_select_expectation('sub4_answer', $choices, null, true),
277                 $this->get_contains_submit_button_expectation(true),
278                 $this->get_does_not_contain_feedback_expectation(),
279                 $this->get_tries_remaining_expectation(2),
280                 $this->get_no_hint_visible_expectation());
282         // Submit a partially wrong response.
283         $this->process_submission(array('sub1_answer' => '1', 'sub2_answer' => '1',
284                 'sub3_answer' => '1', 'sub4_answer' => '1', '-submit' => 1));
286         // Verify.
287         $this->check_current_state(question_state::$todo);
288         $this->check_current_mark(null);
289         $this->check_current_output(
290                 $this->get_contains_select_expectation('sub1_answer', $choices, 1, false),
291                 $this->get_contains_select_expectation('sub2_answer', $choices, 1, false),
292                 $this->get_contains_select_expectation('sub3_answer', $choices, 1, false),
293                 $this->get_contains_select_expectation('sub4_answer', $choices, 1, false),
294                 $this->get_contains_num_parts_correct(2),
295                 $this->get_contains_hidden_expectation(
296                         $this->quba->get_field_prefix($this->slot) . 'sub1_answer', ''),
297                 $this->get_contains_hidden_expectation(
298                         $this->quba->get_field_prefix($this->slot) . 'sub2_answer', '1'),
299                 $this->get_contains_hidden_expectation(
300                         $this->quba->get_field_prefix($this->slot) . 'sub3_answer', ''),
301                 $this->get_contains_hidden_expectation(
302                         $this->quba->get_field_prefix($this->slot) . 'sub4_answer', '1'),
303                 $this->get_does_not_contain_submit_button_expectation(),
304                 $this->get_contains_hint_expectation('This is the second hint.'));
306         // Try again.
307         $this->process_submission(array('sub1_answer' => '',
308                 'sub2_answer' => '1', 'sub3_answer' => '',
309                 'sub4_answer' => '1', '-tryagain' => 1));
311         // Verify.
312         $this->check_current_state(question_state::$todo);
313         $this->check_current_mark(null);
314         $this->check_current_output(
315                 $this->get_contains_select_expectation('sub1_answer', $choices, '', true),
316                 $this->get_contains_select_expectation('sub2_answer', $choices, '1', true),
317                 $this->get_contains_select_expectation('sub3_answer', $choices, '', true),
318                 $this->get_contains_select_expectation('sub4_answer', $choices, '1', true),
319                 $this->get_contains_submit_button_expectation(true),
320                 $this->get_does_not_contain_feedback_expectation(),
321                 $this->get_tries_remaining_expectation(1),
322                 $this->get_no_hint_visible_expectation());
323     }
325     public function test_interactive_partial_response_does_not_reveal_answer() {
327         // Create a multianswer question.
328         $q = test_question_maker::make_question('multianswer', 'fourmc');
329         $q->hints = array(
330                 new question_hint_with_parts(11, 'This is the first hint.', FORMAT_HTML, false, true),
331                 new question_hint_with_parts(12, 'This is the second hint.', FORMAT_HTML, true, true),
332         );
333         $choices = array('' => '', '0' => 'California', '1' => 'Arizona');
335         $this->start_attempt_at_question($q, 'interactive', 4);
337         // Check the initial state.
338         $this->check_current_state(question_state::$todo);
339         $this->check_current_mark(null);
340         $this->assertEquals('interactivecountback',
341                 $this->quba->get_question_attempt($this->slot)->get_behaviour_name());
342         $this->check_current_output(
343                 $this->get_contains_marked_out_of_summary(),
344                 $this->get_contains_select_expectation('sub1_answer', $choices, null, true),
345                 $this->get_contains_select_expectation('sub2_answer', $choices, null, true),
346                 $this->get_contains_select_expectation('sub3_answer', $choices, null, true),
347                 $this->get_contains_select_expectation('sub4_answer', $choices, null, true),
348                 $this->get_contains_submit_button_expectation(true),
349                 $this->get_does_not_contain_validation_error_expectation(),
350                 $this->get_does_not_contain_feedback_expectation(),
351                 $this->get_tries_remaining_expectation(3),
352                 $this->get_does_not_contain_num_parts_correct(),
353                 $this->get_no_hint_visible_expectation());
355         // Submit an incomplete response response.
356         $this->process_submission(array('sub1_answer' => '1', 'sub2_answer' => '1', '-submit' => 1));
358         // Verify.
359         $this->check_current_state(question_state::$invalid);
360         $this->check_current_mark(null);
361         $this->check_current_output(
362                 $this->get_contains_select_expectation('sub1_answer', $choices, 1, true),
363                 $this->get_contains_select_expectation('sub2_answer', $choices, 1, true),
364                 $this->get_contains_select_expectation('sub3_answer', $choices, null, true),
365                 $this->get_contains_select_expectation('sub4_answer', $choices, null, true),
366                 $this->get_does_not_contain_num_parts_correct(),
367                 $this->get_contains_validation_error_expectation(),
368                 $this->get_contains_submit_button_expectation(true),
369                 $this->get_does_not_contain_try_again_button_expectation(),
370                 $this->get_does_not_contain_correctness_expectation(),
371                 $this->get_no_hint_visible_expectation());
372         $this->render();
373         $a = array('mark' => '0.00', 'max' => '1.00');
374         $this->assertNotRegExp('~' . preg_quote(get_string('markoutofmax', 'question', $a), '~') . '~',
375                 $this->currentoutput);
376         $a['mark'] = '1.00';
377         $this->assertNotRegExp('~' . preg_quote(get_string('markoutofmax', 'question', $a), '~') . '~',
378                 $this->currentoutput);
379     }
381     public function test_interactivecountback_feedback() {
383         // Create a multianswer question.
384         $q = test_question_maker::make_question('multianswer', 'fourmc');
385         $q->hints = array(
386             new question_hint_with_parts(11, 'This is the first hint.', FORMAT_HTML, true, true),
387             new question_hint_with_parts(12, 'This is the second hint.', FORMAT_HTML, true, true),
388         );
389         $choices = array('' => '', '0' => 'California', '1' => 'Arizona');
391         $this->start_attempt_at_question($q, 'interactive', 12);
393         // Check the initial state.
394         $this->check_current_state(question_state::$todo);
395         $this->check_current_mark(null);
396         $this->assertEquals('interactivecountback',
397                 $this->quba->get_question_attempt($this->slot)->get_behaviour_name());
398         $this->check_current_output(
399                 $this->get_contains_marked_out_of_summary(),
400                 $this->get_contains_select_expectation('sub1_answer', $choices, null, true),
401                 $this->get_contains_select_expectation('sub2_answer', $choices, null, true),
402                 $this->get_contains_select_expectation('sub3_answer', $choices, null, true),
403                 $this->get_contains_select_expectation('sub4_answer', $choices, null, true),
404                 $this->get_contains_submit_button_expectation(true),
405                 $this->get_does_not_contain_validation_error_expectation(),
406                 $this->get_does_not_contain_feedback_expectation(),
407                 $this->get_tries_remaining_expectation(3),
408                 $this->get_no_hint_visible_expectation());
410         // Submit an answer with two right, and two wrong.
411         $this->process_submission(array('sub1_answer' => '1', 'sub2_answer' => '1',
412                 'sub3_answer' => '1', 'sub4_answer' => '1', '-submit' => 1));
414         // Verify.
415         $this->check_current_state(question_state::$todo);
416         $this->check_current_mark(null);
417         $this->check_current_output(
418                 $this->get_contains_select_expectation('sub1_answer', $choices, 1, false),
419                 $this->get_contains_select_expectation('sub2_answer', $choices, 1, false),
420                 $this->get_contains_select_expectation('sub3_answer', $choices, 1, false),
421                 $this->get_contains_select_expectation('sub4_answer', $choices, 1, false),
422                 $this->get_does_not_contain_submit_button_expectation(),
423                 $this->get_contains_try_again_button_expectation(true),
424                 $this->get_does_not_contain_correctness_expectation(),
425                 new question_pattern_expectation('/Tries remaining: 2/'),
426                 $this->get_contains_hint_expectation('This is the first hint.'));
428         // Check that extract responses will return the reset data.
429         $prefix = $this->quba->get_field_prefix($this->slot);
430         $this->assertEquals(array('sub1_answer' => 1),
431                 $this->quba->extract_responses($this->slot, array($prefix . 'sub1_answer' => 1)));
433         // Do try again.
434         $this->process_submission(array('sub1_answer' => '',
435                 'sub2_answer' => '1', 'sub3_answer' => '',
436                 'sub4_answer' => '1', '-tryagain' => 1));
438         // Verify.
439         $this->check_current_state(question_state::$todo);
440         $this->check_current_mark(null);
441         $this->check_current_output(
442                 $this->get_contains_select_expectation('sub1_answer', $choices, '', true),
443                 $this->get_contains_select_expectation('sub2_answer', $choices, '1', true),
444                 $this->get_contains_select_expectation('sub3_answer', $choices, '', true),
445                 $this->get_contains_select_expectation('sub4_answer', $choices, '1', true),
446                 $this->get_contains_submit_button_expectation(true),
447                 $this->get_does_not_contain_feedback_expectation(),
448                 $this->get_tries_remaining_expectation(2),
449                 $this->get_no_hint_visible_expectation());
451         // Submit the right answer.
452         $this->process_submission(array('sub1_answer' => '0', 'sub2_answer' => '1',
453                 'sub3_answer' => '0', 'sub4_answer' => '1', '-submit' => 1));
455         // Verify.
456         $this->check_current_state(question_state::$gradedright);
457         $this->check_current_mark(10);
458         $this->check_current_output(
459                 $this->get_contains_select_expectation('sub1_answer', $choices, '0', false),
460                 $this->get_contains_select_expectation('sub2_answer', $choices, '1', false),
461                 $this->get_contains_select_expectation('sub3_answer', $choices, '0', false),
462                 $this->get_contains_select_expectation('sub4_answer', $choices, '1', false),
463                 $this->get_does_not_contain_submit_button_expectation(),
464                 $this->get_does_not_contain_try_again_button_expectation(),
465                 $this->get_contains_correct_expectation(),
466                 new question_no_pattern_expectation('/class="control\b[^"]*\bpartiallycorrect"/'));
467     }
469     public function test_deferred_feedback_multiple() {
471         // Create a multianswer question.
472         $q = test_question_maker::make_question('multianswer', 'multiple');
473         $this->start_attempt_at_question($q, 'deferredfeedback', 2);
475         // Check the initial state.
476         $this->check_current_state(question_state::$todo);
477         $this->check_current_mark(null);
478         $this->check_current_output(
479             $this->get_contains_marked_out_of_summary(),
480             $this->get_does_not_contain_feedback_expectation(),
481             $this->get_does_not_contain_validation_error_expectation());
483         // Save in incomplete answer.
484         $this->process_submission(array('sub1_choice0' => '1', 'sub1_choice1' => '1',
485                                         'sub1_choice2' => '', 'sub1_choice3' => '',
486                                         'sub1_choice4' => '', 'sub1_choice5' => '1',
487                                         ));
489         // Verify.
490         $this->check_current_state(question_state::$invalid);
491         $this->check_current_mark(null);
492         $this->check_current_output(
493             $this->get_contains_marked_out_of_summary(),
494             $this->get_does_not_contain_feedback_expectation(),
495             $this->get_contains_validation_error_expectation());
497         // Save a partially correct answer.
498         $this->process_submission(array('sub1_choice0' => '1', 'sub1_choice1' => '',
499                                         'sub1_choice2' => '', 'sub1_choice3' => '',
500                                         'sub1_choice4' => '1', 'sub1_choice5' => '1',
501                                         'sub2_choice0' => '', 'sub2_choice1' => '',
502                                         'sub2_choice2' => '', 'sub2_choice3' => '',
503                                         'sub2_choice4' => '1',
504                                   ));
506         // Verify.
507         $this->check_current_state(question_state::$complete);
508         $this->check_current_mark(null);
509         $this->check_current_output(
510             $this->get_contains_marked_out_of_summary(),
511             $this->get_does_not_contain_feedback_expectation(),
512             $this->get_does_not_contain_validation_error_expectation());
514         // Now submit all and finish.
515         $this->finish();
517         // Verify.
518         $this->check_current_state(question_state::$gradedpartial);
519         $this->check_current_mark(1.5);
520         $this->check_current_output(
521             $this->get_contains_mark_summary(1.5),
522             $this->get_contains_partcorrect_expectation(),
523             $this->get_does_not_contain_validation_error_expectation());
524     }