From c5499edaefbe5cb21f871090aa36ee38af48caa0 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Wed, 26 Sep 2018 18:01:56 +0100 Subject: [PATCH] MDL-63185 mod_quiz: more fixes to new behat code Part of MDL-62610 --- .../responses/tests/behat/basic.feature | 10 +- mod/quiz/tests/behat/behat_mod_quiz.php | 199 +++++++++++++----- mod/quiz/tests/generator/lib.php | 26 ++- question/behaviour/interactive/behaviour.php | 2 +- question/tests/generator/lib.php | 19 +- 5 files changed, 185 insertions(+), 71 deletions(-) diff --git a/mod/quiz/report/responses/tests/behat/basic.feature b/mod/quiz/report/responses/tests/behat/basic.feature index 1980f615bf4..dc1570df05d 100644 --- a/mod/quiz/report/responses/tests/behat/basic.feature +++ b/mod/quiz/report/responses/tests/behat/basic.feature @@ -44,19 +44,17 @@ Feature: Basic use of the Responses report @javascript Scenario: Report works when there are attempts - Given I log in as "student1" - And user "student1" has started an attempt at quiz "Quiz 1" - And user "student1" has submitted answers in their attempt at quiz "Quiz 1": + Given user "student1" has started an attempt at quiz "Quiz 1" + And user "student1" has checked answers in their attempt at quiz "Quiz 1": | slot | response | | 1 | 1.0 | - And user "student1" has submitted answers in their attempt at quiz "Quiz 1": + And user "student1" has checked answers in their attempt at quiz "Quiz 1": | slot | response | | 1 | 3.0 | - And user "student1" has submitted answers in their attempt at quiz "Quiz 1": + And user "student1" has checked answers in their attempt at quiz "Quiz 1": | slot | response | | 1 | 3.14 | And user "student1" has finished an attempt at quiz "Quiz 1" - And I log out When I log in as "teacher" And I am on "Course 1" course homepage And I follow "Quiz 1" diff --git a/mod/quiz/tests/behat/behat_mod_quiz.php b/mod/quiz/tests/behat/behat_mod_quiz.php index 8f6bf5d0422..09e5b2b3b63 100644 --- a/mod/quiz/tests/behat/behat_mod_quiz.php +++ b/mod/quiz/tests/behat/behat_mod_quiz.php @@ -562,6 +562,60 @@ class behat_mod_quiz extends behat_question_base { $this->find('xpath', $xpath); } + /** + * Helper used by user_has_attempted_with_responses, + * user_has_started_an_attempt_at_quiz_with_details, etc. + * + * @param TableNode $attemptinfo data table from the Behat step + * @return array with two elements, $forcedrandomquestions, $forcedvariants, + * that can be passed to $quizgenerator->create_attempt. + */ + protected function extract_forced_randomisation_from_attempt_info(TableNode $attemptinfo) { + global $DB; + + $forcedrandomquestions = []; + $forcedvariants = []; + foreach ($attemptinfo->getHash() as $slotinfo) { + if (empty($slotinfo['slot'])) { + throw new ExpectationException('When simulating a quiz attempt, ' . + 'the slot column is required.', $this->getSession()); + } + + if (!empty($slotinfo['actualquestion'])) { + $forcedrandomquestions[$slotinfo['slot']] = $DB->get_field('question', 'id', + ['name' => $slotinfo['actualquestion']], MUST_EXIST); + } + + if (!empty($slotinfo['variant'])) { + $forcedvariants[$slotinfo['slot']] = (int) $slotinfo['variant']; + } + } + return [$forcedrandomquestions, $forcedvariants]; + } + + /** + * Helper used by user_has_attempted_with_responses, user_has_checked_answers_in_their_attempt_at_quiz, + * user_has_input_answers_in_their_attempt_at_quiz, etc. + * + * @param TableNode $attemptinfo data table from the Behat step + * @return array of responses that can be passed to $quizgenerator->submit_responses. + */ + protected function extract_responses_from_attempt_info(TableNode $attemptinfo) { + $responses = []; + foreach ($attemptinfo->getHash() as $slotinfo) { + if (empty($slotinfo['slot'])) { + throw new ExpectationException('When simulating a quiz attempt, ' . + 'the slot column is required.', $this->getSession()); + } + if (!array_key_exists('response', $slotinfo)) { + throw new ExpectationException('When simulating a quiz attempt, ' . + 'the response column is required.', $this->getSession()); + } + $responses[$slotinfo['slot']] = $slotinfo['response']; + } + return $responses; + } + /** * Attempt a quiz. * @@ -603,36 +657,16 @@ class behat_mod_quiz extends behat_question_base { $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST); $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST); - $forcedrandomquestions = []; - $forcedvariants = []; - $responses = []; - foreach ($attemptinfo->getHash() as $slotinfo) { - if (empty($slotinfo['slot'])) { - throw new ExpectationException('When simulating a quiz attempt, ' . - 'the slot column is required.', $this->getSession()); - } - if (!array_key_exists('response', $slotinfo)) { - throw new ExpectationException('When simulating a quiz attempt, ' . - 'the response column is required.', $this->getSession()); - } - $responses[$slotinfo['slot']] = $slotinfo['response']; - - if (!empty($slotinfo['actualquestion'])) { - $forcedrandomquestions[$slotinfo['slot']] = $DB->get_field('question', 'id', - ['name' => $slotinfo['actualquestion']], MUST_EXIST); - } - - if (!empty($slotinfo['variant'])) { - $forcedvariants[$slotinfo['slot']] = (int) $slotinfo['variant']; - } - } + list($forcedrandomquestions, $forcedvariants) = + $this->extract_forced_randomisation_from_attempt_info($attemptinfo); + $responses = $this->extract_responses_from_attempt_info($attemptinfo); $this->set_user($user); $attempt = $quizgenerator->create_attempt($quizid, $user->id, $forcedrandomquestions, $forcedvariants); - $quizgenerator->submit_responses($attempt->id, $responses, true); + $quizgenerator->submit_responses($attempt->id, $responses, false, true); $this->set_user(); } @@ -662,15 +696,18 @@ class behat_mod_quiz extends behat_question_base { } /** - * Submit answers to an existing quiz attempt. + * Start a quiz attempt without answers. + * + * The supplied data table for have a row for each slot where you want + * to force either which random question was chose, or which random variant + * was used, as for {@link user_has_attempted_with_responses()} above. * - * @param string $username the username of the user that will attempt. * @param string $quizname the name of the quiz the user will attempt. + * @param string $username the username of the user that will attempt. * @param TableNode $attemptinfo information about the questions to add, as above. - * @throws \Behat\Mink\Exception\ExpectationException - * @Given /^user "([^"]*)" has submitted answers in their attempt at quiz "([^"]*)":$/ + * @Given /^user "([^"]*)" has started an attempt at quiz "([^"]*) randomised as follows:"$/ */ - public function user_has_submitted_answers_in_their_attempt_at_quiz($username, $quizname, TableNode $attemptinfo) { + public function user_has_started_an_attempt_at_quiz_with_details($username, $quizname, TableNode $attemptinfo) { global $DB; /** @var mod_quiz_generator $quizgenerator */ @@ -679,30 +716,84 @@ class behat_mod_quiz extends behat_question_base { $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST); $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST); - $forcedvariants = []; - $responses = []; - foreach ($attemptinfo->getHash() as $slotinfo) { - if (empty($slotinfo['slot'])) { - throw new ExpectationException('When simulating a quiz attempt, ' . - 'the slot column is required.', $this->getSession()); - } - if (!array_key_exists('response', $slotinfo)) { - throw new ExpectationException('When simulating a quiz attempt, ' . - 'the response column is required.', $this->getSession()); - } - $responses[$slotinfo['slot']] = $slotinfo['response']; - - if (!empty($slotinfo['variant'])) { - $forcedvariants[$slotinfo['slot']] = (int) $slotinfo['variant']; - } - } + list($forcedrandomquestions, $forcedvariants) = + $this->extract_forced_randomisation_from_attempt_info($attemptinfo); $this->set_user($user); - foreach (quiz_get_user_attempts($quizid, $user->id, 'unfinished', true) as $attemptid => $attemptobj) { - $quizgenerator->submit_responses($attemptid, $responses, false); - break; - } + $quizgenerator->create_attempt($quizid, $user->id, + $forcedrandomquestions, $forcedvariants); + + $this->set_user(); + } + + /** + * Input answers to particular questions an existing quiz attempt, without + * simulating a click of the 'Check' button, if any. + * + * Then there should be a number of rows of data, with two columns slot and response, + * as for {@link user_has_attempted_with_responses()} above. + * There is no need to supply answers to all questions. If so, other questions will be + * left unanswered. + * + * @param string $username the username of the user that will attempt. + * @param string $quizname the name of the quiz the user will attempt. + * @param TableNode $attemptinfo information about the questions to add, as above. + * @throws \Behat\Mink\Exception\ExpectationException + * @Given /^user "([^"]*)" has input answers in their attempt at quiz "([^"]*)":$/ + */ + public function user_has_input_answers_in_their_attempt_at_quiz($username, $quizname, TableNode $attemptinfo) { + global $DB; + + /** @var mod_quiz_generator $quizgenerator */ + $quizgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_quiz'); + + $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST); + $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST); + + $responses = $this->extract_responses_from_attempt_info($attemptinfo); + + $this->set_user($user); + + $attempts = quiz_get_user_attempts($quizid, $user->id, 'unfinished', true); + $quizgenerator->submit_responses(key($attempts), $responses, false, false); + + $this->set_user(); + } + + /** + * Submit answers to questions an existing quiz attempt, with a simulated click on the 'Check' button. + * + * This step should only be used with question behaviours that have have + * a 'Check' button. Those include Interactive with multiple tires, Immediate feedback + * and Immediate feedback with CBM. + * + * Then there should be a number of rows of data, with two columns slot and response, + * as for {@link user_has_attempted_with_responses()} above. + * There is no need to supply answers to all questions. If so, other questions will be + * left unanswered. + * + * @param string $quizname the name of the quiz the user will attempt. + * @param string $username the username of the user that will attempt. + * @param TableNode $attemptinfo information about the questions to add, as above. + * @throws \Behat\Mink\Exception\ExpectationException + * @Given /^user "([^"]*)" has checked answers in their attempt at quiz "([^"]*)":$/ + */ + public function user_has_checked_answers_in_their_attempt_at_quiz($username, $quizname, TableNode $attemptinfo) { + global $DB; + + /** @var mod_quiz_generator $quizgenerator */ + $quizgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_quiz'); + + $quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST); + $user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST); + + $responses = $this->extract_responses_from_attempt_info($attemptinfo); + + $this->set_user($user); + + $attempts = quiz_get_user_attempts($quizid, $user->id, 'unfinished', true); + $quizgenerator->submit_responses(key($attempts), $responses, true, false); $this->set_user(); } @@ -722,11 +813,9 @@ class behat_mod_quiz extends behat_question_base { $this->set_user($user); - foreach (quiz_get_user_attempts($quizid, $user->id, 'unfinished', true) as $attemptid => $attemptobj) { - $attemptobj = quiz_attempt::create($attemptid); - $attemptobj->process_finish(time(), true); - break; - } + $attempts = quiz_get_user_attempts($quizid, $user->id, 'unfinished', true); + $attemptobj = quiz_attempt::create(key($attempts)); + $attemptobj->process_finish(time(), true); $this->set_user(); } diff --git a/mod/quiz/tests/generator/lib.php b/mod/quiz/tests/generator/lib.php index f7d7b32a659..5b18decb333 100644 --- a/mod/quiz/tests/generator/lib.php +++ b/mod/quiz/tests/generator/lib.php @@ -121,8 +121,8 @@ class mod_quiz_generator extends testing_module_generator { 'be able to create one attempt for each user. (This should be fixed.)'); } - return quiz_prepare_and_start_new_attempt($quizobj, 1, null, - $offlineattempt = false, $forcedrandomquestions = [], $forcedvariants); + return quiz_prepare_and_start_new_attempt($quizobj, 1, null, false, + $forcedrandomquestions, $forcedvariants); } /** @@ -134,6 +134,8 @@ class mod_quiz_generator extends testing_module_generator { * @param int $attemptid the id of the attempt which is being * @param array $responses array responses to submit. See description on * {@link core_question_generator::get_simulated_post_data_for_questions_in_usage()}. + * @param bool $checkbutton if simulate a click on the check button for each question, else simulate save. + * This should only be used with behaviours that have a check button. * @param bool $finishattempt if true, the attempt will be submitted. */ public function submit_responses($attemptid, array $responses, $checkbutton, $finishattempt) { @@ -142,10 +144,28 @@ class mod_quiz_generator extends testing_module_generator { $attemptobj = quiz_attempt::create($attemptid); $postdata = $questiongenerator->get_simulated_post_data_for_questions_in_usage( - $attemptobj->get_question_usage(), $responses); + $attemptobj->get_question_usage(), $responses, $checkbutton); $attemptobj->process_submitted_actions(time(), false, $postdata); + // Bit if a hack for interactive behaviour. + // TODO handle this in a more plugin-friendly way. + if ($checkbutton) { + $postdata = []; + foreach ($responses as $slot => $notused) { + $qa = $attemptobj->get_question_attempt($slot); + if ($qa->get_behaviour() instanceof qbehaviour_interactive && $qa->get_behaviour()->is_try_again_state()) { + $postdata[$qa->get_control_field_name('sequencecheck')] = (string)$qa->get_sequence_check_count(); + $postdata[$qa->get_flag_field_name()] = (string)(int)$qa->is_flagged(); + $postdata[$qa->get_behaviour_field_name('tryagain')] = 1; + } + } + + if ($postdata) { + $attemptobj->process_submitted_actions(time(), false, $postdata); + } + } + if ($finishattempt) { $attemptobj->process_finish(time(), false); } diff --git a/question/behaviour/interactive/behaviour.php b/question/behaviour/interactive/behaviour.php index 6f534d6d090..b58f0fce406 100644 --- a/question/behaviour/interactive/behaviour.php +++ b/question/behaviour/interactive/behaviour.php @@ -65,7 +65,7 @@ class qbehaviour_interactive extends question_behaviour_with_multiple_tries { /** * @return bool are we are currently in the try_again state. */ - protected function is_try_again_state() { + public function is_try_again_state() { $laststep = $this->qa->get_last_step(); return $this->qa->get_state()->is_active() && $laststep->has_behaviour_var('submit') && $laststep->has_behaviour_var('_triesleft'); diff --git a/question/tests/generator/lib.php b/question/tests/generator/lib.php index 9deeb8ca339..4d09630e4c6 100644 --- a/question/tests/generator/lib.php +++ b/question/tests/generator/lib.php @@ -167,15 +167,17 @@ class core_question_generator extends component_generator_base { * * @param question_usage_by_activity $quba the question usage. * @param array $responses the resonses to submit, in the format described above. + * @param bool $checkbutton if simulate a click on the check button for each question, else simulate save. + * This should only be used with behaviours that have a check button. * @return array that can be passed to methods like $quba->process_all_actions as simulated POST data. */ public function get_simulated_post_data_for_questions_in_usage( - question_usage_by_activity $quba, array $responses) { + question_usage_by_activity $quba, array $responses, $checkbutton) { $postdata = []; foreach ($responses as $slot => $responsesummary) { $postdata += $this->get_simulated_post_data_for_question_attempt( - $quba->get_question_attempt($slot), $responsesummary); + $quba->get_question_attempt($slot), $responsesummary, $checkbutton); } return $postdata; @@ -190,11 +192,13 @@ class core_question_generator extends component_generator_base { * are passed to the un_summarise_response method of the question to decode. * * @param question_attempt $qa the question attempt for which we are generating POST data. - * @param string $responsesummary a textual summary of the resonse, as described above. - * @return array the sumulated post data that can be passed to $quba->process_all_actions. + * @param string $responsesummary a textual summary of the response, as described above. + * @param bool $checkbutton if simulate a click on the check button, else simulate save. + * This should only be used with behaviours that have a check button. + * @return array the simulated post data that can be passed to $quba->process_all_actions. */ public function get_simulated_post_data_for_question_attempt( - question_attempt $qa, $responsesummary) { + question_attempt $qa, $responsesummary, $checkbutton) { $question = $qa->get_question(); if (!$question instanceof question_with_responses) { @@ -210,7 +214,10 @@ class core_question_generator extends component_generator_base { $postdata[$qa->get_qt_field_name($name)] = (string)$value; } - // TODO handle behaviour variables. + // TODO handle behaviour variables better than this. + if ($checkbutton) { + $postdata[$qa->get_behaviour_field_name('submit')] = 1; + } return $postdata; }