Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 39 and 401]

   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  namespace qtype_essay;
  18  
  19  use question_bank;
  20  use question_engine;
  21  use question_state;
  22  
  23  defined('MOODLE_INTERNAL') || die();
  24  
  25  global $CFG;
  26  require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
  27  
  28  /**
  29   * Unit tests for the essay question type.
  30   *
  31   * @package   qtype_essay
  32   * @copyright 2013 The Open University
  33   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  class walkthrough_test extends \qbehaviour_walkthrough_test_base {
  36  
  37      protected function check_contains_textarea($name, $content = '', $height = 10) {
  38          $fieldname = $this->quba->get_field_prefix($this->slot) . $name;
  39  
  40          $this->assertTag(array('tag' => 'textarea',
  41                  'attributes' => array('cols' => '60', 'rows' => $height,
  42                          'name' => $fieldname)),
  43                  $this->currentoutput);
  44  
  45          if ($content) {
  46              $this->assertMatchesRegularExpression('/' . preg_quote(s($content), '/') . '/', $this->currentoutput);
  47          }
  48      }
  49  
  50      /**
  51       * Helper method: Store a test file with a given name and contents in a
  52       * draft file area.
  53       *
  54       * @param int $usercontextid user context id.
  55       * @param int $draftitemid draft item id.
  56       * @param string $filename filename.
  57       * @param string $contents file contents.
  58       */
  59      protected function save_file_to_draft_area($usercontextid, $draftitemid, $filename, $contents) {
  60          $fs = get_file_storage();
  61  
  62          $filerecord = new \stdClass();
  63          $filerecord->contextid = $usercontextid;
  64          $filerecord->component = 'user';
  65          $filerecord->filearea = 'draft';
  66          $filerecord->itemid = $draftitemid;
  67          $filerecord->filepath = '/';
  68          $filerecord->filename = $filename;
  69          $fs->create_file_from_string($filerecord, $contents);
  70      }
  71  
  72      public function test_deferred_feedback_html_editor() {
  73          global $PAGE;
  74  
  75          // The current text editor depends on the users profile setting - so it needs a valid user.
  76          $this->setAdminUser();
  77          // Required to init a text editor.
  78          $PAGE->set_url('/');
  79  
  80          // Create an essay question.
  81          $q = \test_question_maker::make_question('essay', 'editor');
  82          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
  83  
  84          $prefix = $this->quba->get_field_prefix($this->slot);
  85          $fieldname = $prefix . 'answer';
  86          $response = '<p>The <b>cat</b> sat on the mat. Then it ate a <b>frog</b>.</p>';
  87  
  88          // Check the initial state.
  89          $this->check_current_state(question_state::$todo);
  90          $this->check_current_mark(null);
  91          $this->render();
  92          $this->check_contains_textarea('answer', '');
  93          $this->check_current_output(
  94                  $this->get_contains_question_text_expectation($q),
  95                  $this->get_does_not_contain_feedback_expectation());
  96          $this->check_step_count(1);
  97  
  98          // Save a response.
  99          $this->quba->process_all_actions(null, array(
 100              'slots'                    => $this->slot,
 101              $fieldname                 => $response,
 102              $fieldname . 'format'      => FORMAT_HTML,
 103              $prefix . ':sequencecheck' => '1',
 104          ));
 105  
 106          // Verify.
 107          $this->check_current_state(question_state::$complete);
 108          $this->check_current_mark(null);
 109          $this->check_step_count(2);
 110          $this->render();
 111          $this->check_contains_textarea('answer', $response);
 112          $this->check_current_output(
 113                  $this->get_contains_question_text_expectation($q),
 114                  $this->get_does_not_contain_feedback_expectation());
 115          $this->check_step_count(2);
 116  
 117          // Finish the attempt.
 118          $this->quba->finish_all_questions();
 119  
 120          // Verify.
 121          $this->check_current_state(question_state::$needsgrading);
 122          $this->check_current_mark(null);
 123          $this->render();
 124          $this->assertMatchesRegularExpression('/' . preg_quote($response, '/') . '/', $this->currentoutput);
 125          $this->check_current_output(
 126                  $this->get_contains_question_text_expectation($q),
 127                  $this->get_contains_general_feedback_expectation($q));
 128      }
 129  
 130      public function test_deferred_feedback_plain_text() {
 131  
 132          // Create an essay question.
 133          $q = \test_question_maker::make_question('essay', 'plain');
 134          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 135  
 136          $prefix = $this->quba->get_field_prefix($this->slot);
 137          $fieldname = $prefix . 'answer';
 138          $response = "x < 1\nx > 0\nFrog & Toad were friends.";
 139  
 140          // Check the initial state.
 141          $this->check_current_state(question_state::$todo);
 142          $this->check_current_mark(null);
 143          $this->render();
 144          $this->check_contains_textarea('answer', '');
 145          $this->check_current_output(
 146                  $this->get_contains_question_text_expectation($q),
 147                  $this->get_does_not_contain_feedback_expectation());
 148          $this->check_step_count(1);
 149  
 150          // Save a response.
 151          $this->quba->process_all_actions(null, array(
 152              'slots'                    => $this->slot,
 153              $fieldname                 => $response,
 154              $fieldname . 'format'      => FORMAT_HTML,
 155              $prefix . ':sequencecheck' => '1',
 156          ));
 157  
 158          // Verify.
 159          $this->check_current_state(question_state::$complete);
 160          $this->check_current_mark(null);
 161          $this->check_step_count(2);
 162          $this->render();
 163          $this->check_contains_textarea('answer', $response);
 164          $this->check_current_output(
 165                  $this->get_contains_question_text_expectation($q),
 166                  $this->get_does_not_contain_feedback_expectation());
 167          $this->check_step_count(2);
 168  
 169          // Finish the attempt.
 170          $this->quba->finish_all_questions();
 171  
 172          // Verify.
 173          $this->check_current_state(question_state::$needsgrading);
 174          $this->check_current_mark(null);
 175          $this->render();
 176          $this->assertMatchesRegularExpression('/' . preg_quote(s($response), '/') . '/', $this->currentoutput);
 177          $this->check_current_output(
 178                  $this->get_contains_question_text_expectation($q),
 179                  $this->get_contains_general_feedback_expectation($q));
 180      }
 181  
 182      public function test_responsetemplate() {
 183          global $PAGE;
 184  
 185          // The current text editor depends on the users profile setting - so it needs a valid user.
 186          $this->setAdminUser();
 187          // Required to init a text editor.
 188          $PAGE->set_url('/');
 189  
 190          // Create an essay question.
 191          $q = \test_question_maker::make_question('essay', 'responsetemplate');
 192          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 193  
 194          $prefix = $this->quba->get_field_prefix($this->slot);
 195          $fieldname = $prefix . 'answer';
 196  
 197          // Check the initial state.
 198          $this->check_current_state(question_state::$todo);
 199          $this->check_current_mark(null);
 200          $this->render();
 201          $this->check_contains_textarea('answer', 'Once upon a time');
 202          $this->check_current_output(
 203                  $this->get_contains_question_text_expectation($q),
 204                  $this->get_does_not_contain_feedback_expectation());
 205          $this->check_step_count(1);
 206  
 207          // Save.
 208          $this->quba->process_all_actions(null, array(
 209              'slots'                    => $this->slot,
 210              $fieldname                 => 'Once upon a time there was a little green frog.',
 211              $fieldname . 'format'      => FORMAT_HTML,
 212              $prefix . ':sequencecheck' => '1',
 213          ));
 214  
 215          // Verify.
 216          $this->check_current_state(question_state::$complete);
 217          $this->check_current_mark(null);
 218          $this->check_step_count(2);
 219          $this->render();
 220          $this->check_contains_textarea('answer', 'Once upon a time there was a little green frog.');
 221          $this->check_current_output(
 222                  $this->get_contains_question_text_expectation($q),
 223                  $this->get_does_not_contain_feedback_expectation());
 224          $this->check_step_count(2);
 225  
 226          // Finish the attempt.
 227          $this->quba->finish_all_questions();
 228  
 229          // Verify.
 230          $this->check_current_state(question_state::$needsgrading);
 231          $this->check_current_mark(null);
 232          $this->render();
 233          $this->assertMatchesRegularExpression(
 234              '/' . preg_quote(s('Once upon a time there was a little green frog.'), '/') . '/', $this->currentoutput);
 235          $this->check_current_output(
 236                  $this->get_contains_question_text_expectation($q),
 237                  $this->get_contains_general_feedback_expectation($q));
 238      }
 239  
 240      public function test_deferred_feedback_html_editor_with_files_attempt_on_last() {
 241          global $CFG, $USER, $PAGE;
 242  
 243          $this->resetAfterTest(true);
 244          $this->setAdminUser();
 245          // Required to init a text editor.
 246          $PAGE->set_url('/');
 247          $usercontextid = \context_user::instance($USER->id)->id;
 248          $fs = get_file_storage();
 249  
 250          // Create an essay question in the DB.
 251          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 252          $cat = $generator->create_question_category();
 253          $question = $generator->create_question('essay', 'editorfilepicker', array('category' => $cat->id));
 254  
 255          // Start attempt at the question.
 256          $q = question_bank::load_question($question->id);
 257          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 258  
 259          $this->check_current_state(question_state::$todo);
 260          $this->check_current_mark(null);
 261          $this->check_step_count(1);
 262  
 263          // Process a response and check the expected result.
 264          // First we need to get the draft item ids.
 265          $this->render();
 266          if (!preg_match('/env=editor&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
 267              throw new \coding_exception('Editor draft item id not found.');
 268          }
 269          $editordraftid = $matches[1];
 270          if (!preg_match('/env=filemanager&amp;action=browse&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
 271              throw new \coding_exception('File manager draft item id not found.');
 272          }
 273          $attachementsdraftid = $matches[1];
 274  
 275          $this->save_file_to_draft_area($usercontextid, $editordraftid, 'smile.txt', ':-)');
 276          $this->save_file_to_draft_area($usercontextid, $attachementsdraftid, 'greeting.txt', 'Hello world!');
 277          $this->process_submission(array(
 278                  'answer' => 'Here is a picture: <img src="' . $CFG->wwwroot .
 279                                  "/draftfile.php/{$usercontextid}/user/draft/{$editordraftid}/smile.txt" .
 280                                  '" alt="smile">.',
 281                  'answerformat' => FORMAT_HTML,
 282                  'answer:itemid' => $editordraftid,
 283                  'attachments' => $attachementsdraftid));
 284  
 285          $this->check_current_state(question_state::$complete);
 286          $this->check_current_mark(null);
 287          $this->check_step_count(2);
 288          $this->save_quba();
 289  
 290          // Save the same response again, and verify no new step is created.
 291          $this->load_quba();
 292  
 293          $this->render();
 294          if (!preg_match('/env=editor&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
 295              throw new \coding_exception('Editor draft item id not found.');
 296          }
 297          $editordraftid = $matches[1];
 298          if (!preg_match('/env=filemanager&amp;action=browse&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
 299              throw new \coding_exception('File manager draft item id not found.');
 300          }
 301          $attachementsdraftid = $matches[1];
 302  
 303          $this->process_submission(array(
 304                  'answer' => 'Here is a picture: <img src="' . $CFG->wwwroot .
 305                                  "/draftfile.php/{$usercontextid}/user/draft/{$editordraftid}/smile.txt" .
 306                                  '" alt="smile">.',
 307                  'answerformat' => FORMAT_HTML,
 308                  'answer:itemid' => $editordraftid,
 309                  'attachments' => $attachementsdraftid));
 310  
 311          $this->check_current_state(question_state::$complete);
 312          $this->check_current_mark(null);
 313          $this->check_step_count(2);
 314  
 315          // Now submit all and finish.
 316          $this->finish();
 317          $this->check_current_state(question_state::$needsgrading);
 318          $this->check_current_mark(null);
 319          $this->check_step_count(3);
 320          $this->save_quba();
 321  
 322          // Now start a new attempt based on the old one.
 323          $this->load_quba();
 324          $oldqa = $this->get_question_attempt();
 325  
 326          $q = question_bank::load_question($question->id);
 327          $this->quba = question_engine::make_questions_usage_by_activity('unit_test',
 328                  \context_system::instance());
 329          $this->quba->set_preferred_behaviour('deferredfeedback');
 330          $this->slot = $this->quba->add_question($q, 1);
 331          $this->quba->start_question_based_on($this->slot, $oldqa);
 332  
 333          $this->check_current_state(question_state::$complete);
 334          $this->check_current_mark(null);
 335          $this->check_step_count(1);
 336          $this->save_quba();
 337  
 338          // Now save the same response again, and ensure that a new step is not created.
 339          $this->load_quba();
 340  
 341          $this->render();
 342          if (!preg_match('/env=editor&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
 343              throw new \coding_exception('Editor draft item id not found.');
 344          }
 345          $editordraftid = $matches[1];
 346          if (!preg_match('/env=filemanager&amp;action=browse&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
 347              throw new \coding_exception('File manager draft item id not found.');
 348          }
 349          $attachementsdraftid = $matches[1];
 350  
 351          $this->process_submission(array(
 352                  'answer' => 'Here is a picture: <img src="' . $CFG->wwwroot .
 353                                  "/draftfile.php/{$usercontextid}/user/draft/{$editordraftid}/smile.txt" .
 354                                  '" alt="smile">.',
 355                  'answerformat' => FORMAT_HTML,
 356                  'answer:itemid' => $editordraftid,
 357                  'attachments' => $attachementsdraftid));
 358  
 359          $this->check_current_state(question_state::$complete);
 360          $this->check_current_mark(null);
 361          $this->check_step_count(1);
 362      }
 363  
 364      public function test_deferred_feedback_html_editor_with_files_attempt_on_last_no_files_uploaded() {
 365          global $CFG, $USER, $PAGE;
 366  
 367          $this->resetAfterTest(true);
 368          $this->setAdminUser();
 369          // Required to init a text editor.
 370          $PAGE->set_url('/');
 371          $usercontextid = \context_user::instance($USER->id)->id;
 372          $fs = get_file_storage();
 373  
 374          // Create an essay question in the DB.
 375          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 376          $cat = $generator->create_question_category();
 377          $question = $generator->create_question('essay', 'editorfilepicker', array('category' => $cat->id));
 378  
 379          // Start attempt at the question.
 380          $q = question_bank::load_question($question->id);
 381          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 382  
 383          $this->check_current_state(question_state::$todo);
 384          $this->check_current_mark(null);
 385          $this->check_step_count(1);
 386  
 387          // Process a response and check the expected result.
 388          // First we need to get the draft item ids.
 389          $this->render();
 390          if (!preg_match('/env=editor&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
 391              throw new \coding_exception('Editor draft item id not found.');
 392          }
 393          $editordraftid = $matches[1];
 394          if (!preg_match('/env=filemanager&amp;action=browse&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
 395              throw new \coding_exception('File manager draft item id not found.');
 396          }
 397          $attachementsdraftid = $matches[1];
 398  
 399          $this->process_submission(array(
 400                  'answer' => 'I refuse to draw you a picture, so there!',
 401                  'answerformat' => FORMAT_HTML,
 402                  'answer:itemid' => $editordraftid,
 403                  'attachments' => $attachementsdraftid));
 404  
 405          $this->check_current_state(question_state::$complete);
 406          $this->check_current_mark(null);
 407          $this->check_step_count(2);
 408          $this->save_quba();
 409  
 410          // Now submit all and finish.
 411          $this->finish();
 412          $this->check_current_state(question_state::$needsgrading);
 413          $this->check_current_mark(null);
 414          $this->check_step_count(3);
 415          $this->save_quba();
 416  
 417          // Now start a new attempt based on the old one.
 418          $this->load_quba();
 419          $oldqa = $this->get_question_attempt();
 420  
 421          $q = question_bank::load_question($question->id);
 422          $this->quba = question_engine::make_questions_usage_by_activity('unit_test',
 423                  \context_system::instance());
 424          $this->quba->set_preferred_behaviour('deferredfeedback');
 425          $this->slot = $this->quba->add_question($q, 1);
 426          $this->quba->start_question_based_on($this->slot, $oldqa);
 427  
 428          $this->check_current_state(question_state::$complete);
 429          $this->check_current_mark(null);
 430          $this->check_step_count(1);
 431          $this->save_quba();
 432  
 433          // Check the display.
 434          $this->load_quba();
 435          $this->render();
 436          $this->assertMatchesRegularExpression('/I refuse to draw you a picture, so there!/', $this->currentoutput);
 437      }
 438  
 439      public function test_deferred_feedback_plain_attempt_on_last() {
 440          global $CFG, $USER;
 441  
 442          $this->resetAfterTest(true);
 443          $this->setAdminUser();
 444          $usercontextid = \context_user::instance($USER->id)->id;
 445  
 446          // Create an essay question in the DB.
 447          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 448          $cat = $generator->create_question_category();
 449          $question = $generator->create_question('essay', 'plain', array('category' => $cat->id));
 450  
 451          // Start attempt at the question.
 452          $q = question_bank::load_question($question->id);
 453          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 454  
 455          $this->check_current_state(question_state::$todo);
 456          $this->check_current_mark(null);
 457          $this->check_step_count(1);
 458  
 459          // Process a response and check the expected result.
 460  
 461          $this->process_submission(array(
 462              'answer' => 'Once upon a time there was a frog called Freddy. He lived happily ever after.',
 463              'answerformat' => FORMAT_PLAIN,
 464          ));
 465  
 466          $this->check_current_state(question_state::$complete);
 467          $this->check_current_mark(null);
 468          $this->check_step_count(2);
 469          $this->save_quba();
 470  
 471          // Now submit all and finish.
 472          $this->finish();
 473          $this->check_current_state(question_state::$needsgrading);
 474          $this->check_current_mark(null);
 475          $this->check_step_count(3);
 476          $this->save_quba();
 477  
 478          // Now start a new attempt based on the old one.
 479          $this->load_quba();
 480          $oldqa = $this->get_question_attempt();
 481  
 482          $q = question_bank::load_question($question->id);
 483          $this->quba = question_engine::make_questions_usage_by_activity('unit_test',
 484                  \context_system::instance());
 485          $this->quba->set_preferred_behaviour('deferredfeedback');
 486          $this->slot = $this->quba->add_question($q, 1);
 487          $this->quba->start_question_based_on($this->slot, $oldqa);
 488  
 489          $this->check_current_state(question_state::$complete);
 490          $this->check_current_mark(null);
 491          $this->check_step_count(1);
 492          $this->save_quba();
 493  
 494          // Check the display.
 495          $this->load_quba();
 496          $this->render();
 497          // Test taht no HTML comment has been added to the response.
 498          $this->assertMatchesRegularExpression(
 499              '/Once upon a time there was a frog called Freddy. He lived happily ever after.(?!&lt;!--)/', $this->currentoutput);
 500          // Test for the hash of an empty file area.
 501          $this->assertStringNotContainsString('d41d8cd98f00b204e9800998ecf8427e', $this->currentoutput);
 502      }
 503  
 504      public function test_deferred_feedback_html_editor_with_files_attempt_wrong_filetypes() {
 505          global $CFG, $USER, $PAGE;
 506  
 507          $this->resetAfterTest(true);
 508          $this->setAdminUser();
 509          // Required to init a text editor.
 510          $PAGE->set_url('/');
 511          $usercontextid = \context_user::instance($USER->id)->id;
 512          $fs = get_file_storage();
 513  
 514          // Create an essay question in the DB.
 515          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 516          $cat = $generator->create_question_category();
 517          $question = $generator->create_question('essay', 'editorfilepicker', array('category' => $cat->id));
 518  
 519          // Start attempt at the question.
 520          $q = question_bank::load_question($question->id);
 521          $q->filetypeslist = '.pdf,.docx';
 522          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 523  
 524          $this->check_current_state(question_state::$todo);
 525          $this->check_current_mark(null);
 526          $this->check_step_count(1);
 527  
 528          // Process a response and check the expected result.
 529          // First we need to get the draft item ids.
 530          $this->render();
 531          if (!preg_match('/env=editor&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
 532              throw new \coding_exception('Editor draft item id not found.');
 533          }
 534          $editordraftid = $matches[1];
 535          if (!preg_match('/env=filemanager&amp;action=browse&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
 536              throw new \coding_exception('File manager draft item id not found.');
 537          }
 538          $attachementsdraftid = $matches[1];
 539  
 540          $this->save_file_to_draft_area($usercontextid, $editordraftid, 'smile.txt', ':-)');
 541          $this->save_file_to_draft_area($usercontextid, $attachementsdraftid, 'greeting.txt', 'Hello world!');
 542          $this->process_submission(array(
 543              'answer' => 'Here is a picture: <img src="' . $CFG->wwwroot .
 544                  "/draftfile.php/{$usercontextid}/user/draft/{$editordraftid}/smile.txt" .
 545                  '" alt="smile">.',
 546              'answerformat' => FORMAT_HTML,
 547              'answer:itemid' => $editordraftid,
 548              'attachments' => $attachementsdraftid));
 549  
 550          $this->check_current_state(question_state::$invalid);
 551          $this->check_current_mark(null);
 552          $this->check_step_count(2);
 553          $this->save_quba();
 554  
 555          // Now submit all and finish.
 556          $this->finish();
 557          $this->check_current_state(question_state::$needsgrading);
 558          $this->check_current_mark(null);
 559          $this->check_step_count(3);
 560          $this->save_quba();
 561      }
 562  
 563      public function test_deferred_feedback_html_editor_with_files_attempt_correct_filetypes() {
 564          global $CFG, $USER, $PAGE;
 565  
 566          $this->resetAfterTest(true);
 567          $this->setAdminUser();
 568          // Required to init a text editor.
 569          $PAGE->set_url('/');
 570          $usercontextid = \context_user::instance($USER->id)->id;
 571          $fs = get_file_storage();
 572  
 573          // Create an essay question in the DB.
 574          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 575          $cat = $generator->create_question_category();
 576          $question = $generator->create_question('essay', 'editorfilepicker', array('category' => $cat->id));
 577  
 578          // Start attempt at the question.
 579          $q = question_bank::load_question($question->id);
 580          $q->filetypeslist = '.txt,.docx';
 581          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 582  
 583          $this->check_current_state(question_state::$todo);
 584          $this->check_current_mark(null);
 585          $this->check_step_count(1);
 586  
 587          // Process a response and check the expected result.
 588          // First we need to get the draft item ids.
 589          $this->render();
 590          if (!preg_match('/env=editor&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
 591              throw new \coding_exception('Editor draft item id not found.');
 592          }
 593          $editordraftid = $matches[1];
 594          if (!preg_match('/env=filemanager&amp;action=browse&amp;.*?itemid=(\d+)&amp;/', $this->currentoutput, $matches)) {
 595              throw new \coding_exception('File manager draft item id not found.');
 596          }
 597          $attachementsdraftid = $matches[1];
 598  
 599          $this->save_file_to_draft_area($usercontextid, $editordraftid, 'smile.txt', ':-)');
 600          $this->save_file_to_draft_area($usercontextid, $attachementsdraftid, 'greeting.txt', 'Hello world!');
 601          $this->process_submission(array(
 602              'answer' => 'Here is a picture: <img src="' . $CFG->wwwroot .
 603                  "/draftfile.php/{$usercontextid}/user/draft/{$editordraftid}/smile.txt" .
 604                  '" alt="smile">.',
 605              'answerformat' => FORMAT_HTML,
 606              'answer:itemid' => $editordraftid,
 607              'attachments' => $attachementsdraftid));
 608  
 609          $this->check_current_state(question_state::$complete);
 610          $this->check_current_mark(null);
 611          $this->check_step_count(2);
 612          $this->save_quba();
 613  
 614          // Now submit all and finish.
 615          $this->finish();
 616          $this->check_current_state(question_state::$needsgrading);
 617          $this->check_current_mark(null);
 618          $this->check_step_count(3);
 619          $this->save_quba();
 620      }
 621  
 622      public function test_deferred_feedback_word_limits() {
 623          global $PAGE;
 624  
 625          // The current text editor depends on the users profile setting - so it needs a valid user.
 626          $this->setAdminUser();
 627          // Required to init a text editor.
 628          $PAGE->set_url('/');
 629  
 630          // Create an essay question.
 631          /** @var qtype_essay_question $q */
 632          $q = \test_question_maker::make_question('essay', 'editor');
 633          $q->minwordlimit = 3;
 634          $q->maxwordlimit = 7;
 635          $this->start_attempt_at_question($q, 'deferredfeedback', 1);
 636  
 637          // Check the initial state.
 638          $this->check_current_state(question_state::$todo);
 639          $this->check_current_mark(null);
 640          $this->render();
 641          $this->check_contains_textarea('answer', '');
 642          $this->check_current_output(
 643                  $this->get_contains_question_text_expectation($q),
 644                  $this->get_does_not_contain_validation_error_expectation(),
 645                  $this->get_does_not_contain_feedback_expectation());
 646  
 647          // Save a response that is too short (and give the word-count code a tricky case).
 648          $response = '<div class="card">
 649                          <div class="card-body">
 650                              <h3 class="card-title">One</h3>
 651                              <div class="card-text">
 652                                  <ul>
 653                                      <li>Two</li>
 654                                  </ul>
 655                              </div>
 656                          </div>
 657                      </div>';
 658          $this->process_submission(['answer' => $response, 'answerformat' => FORMAT_HTML]);
 659  
 660          // Verify.
 661          $this->check_current_state(question_state::$invalid);
 662          $this->check_current_mark(null);
 663          $this->render();
 664          $this->check_contains_textarea('answer', $response);
 665          $this->check_current_output(
 666                  $this->get_contains_question_text_expectation($q),
 667                  $this->get_contains_validation_error_expectation(),
 668                  $this->get_does_not_contain_feedback_expectation());
 669          $this->assertStringContainsString('This question requires a response of at least 3 words and you are ' .
 670                  'attempting to submit 2 words. Please expand your response and try again.',
 671                  $this->currentoutput);
 672  
 673          // Save a response that is just long enough.
 674          $this->process_submission(['answer' => '<p>One two three.</p>', 'answerformat' => FORMAT_HTML]);
 675  
 676          // Verify.
 677          $this->check_current_state(question_state::$complete);
 678          $this->check_current_mark(null);
 679          $this->render();
 680          $this->check_contains_textarea('answer', '<p>One two three.</p>');
 681          $this->check_current_output(
 682                  $this->get_contains_question_text_expectation($q),
 683                  $this->get_does_not_contain_validation_error_expectation(),
 684                  $this->get_does_not_contain_feedback_expectation());
 685  
 686          // Save a response that is as long as possible short.
 687          $this->process_submission(['answer' => '<p>One two three four five six seven.</p>',
 688                  'answerformat' => FORMAT_HTML]);
 689  
 690          // Verify.
 691          $this->check_current_state(question_state::$complete);
 692          $this->check_current_mark(null);
 693          $this->render();
 694          $this->check_contains_textarea('answer', '<p>One two three four five six seven.</p>');
 695          $this->check_current_output(
 696                  $this->get_contains_question_text_expectation($q),
 697                  $this->get_does_not_contain_validation_error_expectation(),
 698                  $this->get_does_not_contain_feedback_expectation());
 699  
 700          // Save a response that is just too long.
 701          $this->process_submission(['answer' => '<p>One two three four five six seven eight.</p>',
 702                  'answerformat' => FORMAT_HTML]);
 703  
 704          // Verify.
 705          $this->check_current_state(question_state::$invalid);
 706          $this->check_current_mark(null);
 707          $this->render();
 708          $this->check_contains_textarea('answer', '<p>One two three four five six seven eight.</p>');
 709          $this->check_current_output(
 710                  $this->get_contains_question_text_expectation($q),
 711                  $this->get_contains_validation_error_expectation(),
 712                  $this->get_does_not_contain_feedback_expectation());
 713          $this->assertStringContainsString('The word limit for this question is 7 words and you are ' .
 714                  'attempting to submit 8 words. Please shorten your response and try again.',
 715                  $this->currentoutput);
 716  
 717          // Now submit all and finish.
 718          $this->finish();
 719  
 720          // Verify.
 721          $this->check_current_state(question_state::$needsgrading);
 722          $this->check_current_mark(null);
 723          $this->render();
 724          $this->check_current_output(
 725                  $this->get_contains_question_text_expectation($q),
 726                  $this->get_contains_general_feedback_expectation($q));
 727          $this->assertStringContainsString('Word count: 8, more than the limit of 7 words.',
 728                  $this->currentoutput);
 729      }
 730  }