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]

   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   * Moodle editor field.
  19   *
  20   * @package    core_form
  21   * @category   test
  22   * @copyright  2012 David MonllaĆ³
  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  use Behat\Mink\Element\NodeElement as NodeElement;
  29  
  30  require_once (__DIR__ . '/behat_form_textarea.php');
  31  
  32  /**
  33   * Moodle editor field.
  34   *
  35   * @package   core_form
  36   * @category  test
  37   * @copyright 2012 David MonllaĆ³
  38   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class behat_form_editor extends behat_form_textarea {
  41  
  42      /**
  43       * Sets the value to a field.
  44       *
  45       * @param string $value
  46       */
  47      public function set_value($value): void {
  48          $editorid = $this->field->getAttribute('id');
  49          if ($this->running_javascript()) {
  50              $value = addslashes($value);
  51              // This will be transported in JSON, which doesn't allow newlines in strings, so we must escape them.
  52              $value = str_replace("\n", "\\n", $value);
  53              behat_base::execute_in_matching_contexts('editor', 'set_editor_value', [
  54                  $editorid,
  55                  $value,
  56              ]);
  57  
  58          } else {
  59              parent::set_value($value);
  60          }
  61      }
  62  
  63      /**
  64       * Returns the current value of the select element.
  65       *
  66       * @return string
  67       */
  68      public function get_value(): string {
  69          if ($this->running_javascript()) {
  70              // Give any listening editors a chance to persist the value to the textarea.
  71              // Some editors only do this on form submission or similar events.
  72              behat_base::execute_in_matching_contexts('editor', 'store_current_value', [
  73                  $this->field->getAttribute('id'),
  74              ]);
  75          }
  76  
  77          return parent::get_value();
  78      }
  79  
  80      /**
  81       * Select all the text in the form field.
  82       *
  83       */
  84      public function select_text() {
  85          // NodeElement.keyPress simply doesn't work.
  86          if (!$this->running_javascript()) {
  87              throw new coding_exception('Selecting text requires javascript.');
  88          }
  89  
  90          $editorid = $this->field->getAttribute('id');
  91          $js = ' (function() {
  92      var e = document.getElementById("'.$editorid.'editable"),
  93          r = rangy.createRange(),
  94          s = rangy.getSelection();
  95  
  96      while ((e.firstChild !== null) && (e.firstChild.nodeType != document.TEXT_NODE)) {
  97          e = e.firstChild;
  98      }
  99      e.focus();
 100      r.selectNodeContents(e);
 101      s.setSingleRange(r);
 102  }()); ';
 103          behat_base::execute_script_in_session($this->session, $js);
 104      }
 105  
 106      /**
 107       * Matches the provided value against the current field value.
 108       *
 109       * @param string $expectedvalue
 110       * @return bool The provided value matches the field value?
 111       */
 112      public function matches($expectedvalue) {
 113          // Fetch the actual value to save fetching it multiple times.
 114          $actualvalue = $this->get_value();
 115  
 116          if ($this->text_matches($expectedvalue, $actualvalue)) {
 117              // The text is an exact match already.
 118              return true;
 119          }
 120  
 121          if ($this->text_matches("<p>{$expectedvalue}</p>", $actualvalue)) {
 122              // A text editor may silently wrap the content in p tags.
 123              return true;
 124          }
 125  
 126          // Standardise both the expected value and the actual field value.
 127          // We are likely dealing with HTML content, given this is an editor.
 128          $expectedvalue = $this->standardise_html($expectedvalue);
 129          $actualvalue = $this->standardise_html($actualvalue);
 130  
 131          // Note: We don't need to worry about the floats here that we care about in text_matches.
 132          // That condition isn't relevant to the content of an editor.
 133          if ($expectedvalue === $actualvalue) {
 134              return true;
 135          }
 136  
 137          return false;
 138      }
 139  
 140      /**
 141       * Standardises the HTML content for comparison.
 142       *
 143       * @param string $html The HTML content to standardise
 144       * @return string The standardised HTML content
 145       */
 146      protected function standardise_html(string $html): string {
 147          $document = new DOMDocument();
 148          $errorstate = libxml_use_internal_errors(true);
 149  
 150          // Format the whitespace nicely.
 151          $document->preserveWhiteSpace = false;
 152          $document->formatOutput = true;
 153  
 154          // Wrap the content in a DIV element so that it is not parsed weirdly.
 155          // Note: We must remove newlines too because DOMDocument does not do so, despite preserveWhiteSpace being false.
 156          // Unfortunately this is slightly limited in that it will also remove newlines from <pre> content and similar.
 157          $document->loadHTML(str_replace("\n", "", "<div>{$html}</div>"), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
 158          $document->normalizeDocument();
 159          libxml_clear_errors();
 160          libxml_use_internal_errors($errorstate);
 161  
 162          // Save the content of the 'div' element, removing the <div> and </div> tags at the start and end.
 163          return trim(substr(
 164              $document->saveHTML($document->getElementsByTagName('div')->item(0)),
 165              5,
 166              -6
 167          ));
 168      }
 169  }