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.
   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  use Behat\Mink\Element\NodeElement;
  18  use Behat\Mink\Exception\DriverException;
  19  use Behat\Mink\Exception\ExpectationException;
  20  
  21  /**
  22   * Behat helpers for TinyMCE Plugins.
  23   *
  24   * @package    editor_tiny
  25   * @category   test
  26   * @copyright  2022 Andrew Lyons <andrew@nicols.co.uk>
  27   */
  28  trait editor_tiny_helpers {
  29      /**
  30       * Execute some JavaScript for a particular Editor instance.
  31       *
  32       * The editor instance is available on the 'instnace' variable.
  33       *
  34       * @param string $editorid The ID of the editor
  35       * @param string $code The code to execute
  36       */
  37      protected function execute_javascript_for_editor(string $editorid, string $code): void {
  38          $js = <<<EOF
  39          require(['editor_tiny/editor'], (editor) => {
  40              const instance = editor.getInstanceForElementId('{$editorid}');
  41              {$code}
  42          });
  43          EOF;
  44  
  45          $this->execute_script($js);
  46      }
  47  
  48      /**
  49       * Resolve some JavaScript for a particular Editor instance.
  50       *
  51       * The editor instance is available on the 'instnace' variable.
  52       * The code should return a value by passing it to the `resolve` function.
  53       *
  54       * @param string $editorid The ID of the editor
  55       * @param string $code The code to evaluate
  56       * @return string|null|array
  57       */
  58      protected function evaluate_javascript_for_editor(string $editorid, string $code) {
  59          $js = <<<EOF
  60          return new Promise((resolve, reject) => {
  61              require(['editor_tiny/editor'], (editor) => {
  62                  const instance = editor.getInstanceForElementId('{$editorid}');
  63                  if (!instance) {
  64                      reject("Instance '{$editorid}' not found");
  65                  }
  66  
  67                  {$code}
  68              });
  69          });
  70          EOF;
  71  
  72          return $this->evaluate_script($js);
  73      }
  74  
  75      /**
  76       * Set the value for the editor.
  77       *
  78       * Note: This function is called by the behat_form_editor class.
  79       * It is called regardless of the current default editor as editor selection is a user preference.
  80       * Therefore it must fail gracefully and only set a value if the editor instance was found on the page.
  81       *
  82       * @param string $editorid
  83       * @param string $value
  84       */
  85      public function set_editor_value(string $editorid, string $value): void {
  86          if (!$this->running_javascript()) {
  87              return;
  88          }
  89  
  90          $this->execute_javascript_for_editor($editorid, <<<EOF
  91              instance.setContent('{$value}');
  92              instance.undoManager.add();
  93              EOF);
  94      }
  95  
  96      /**
  97       * Store the current value of the editor, if it is a Tiny editor, to the textarea.
  98       *
  99       * @param string $editorid The ID of the editor.
 100       */
 101      public function store_current_value(string $editorid): void {
 102          $this->execute_javascript_for_editor($editorid, "instance?.save();");
 103      }
 104  
 105      /**
 106       * Ensure that the editor_tiny tag is in use.
 107       *
 108       * This function should be used for any step defined in this file.
 109       *
 110       * @throws DriverException Thrown if the editor_tiny tag is not specified for this file
 111       */
 112      protected function require_tiny_tags(): void {
 113          // Ensure that this step only runs in TinyMCE tags.
 114          if (!$this->has_tag('editor_tiny')) {
 115              throw new DriverException(
 116                  'TinyMCE tests using this step must have the @editor_tiny tag on either the scenario or feature.'
 117              );
 118          }
 119      }
 120  
 121      /**
 122       * Get the Mink NodeElement of the <textarea> for the specified locator.
 123       *
 124       * Moodle mostly referes to the textarea, rather than the editor itself and interactions are translated to the
 125       * Editor using the TinyMCE API.
 126       *
 127       * @param string $locator A Moodle field locator
 128       * @return NodeElement The element found by the find_field function
 129       */
 130      protected function get_textarea_for_locator(string $locator): NodeElement {
 131          return $this->find_field($locator);
 132      }
 133  
 134      /**
 135       * Get the Mink NodeElement of the container for the specified locator.
 136       *
 137       * This is the top-most HTML element for the editor found by TinyMCE.getContainer().
 138       *
 139       * @param string $locator A Moodle field locator
 140       * @return NodeElement The Mink NodeElement representing the container.
 141       */
 142      protected function get_editor_container_for_locator(string $locator): NodeElement {
 143          $textarea = $this->get_textarea_for_locator($locator);
 144          $editorid = $textarea->getAttribute('id');
 145  
 146          $targetid = uniqid();
 147          $js = <<<EOF
 148              const container = instance.getContainer();
 149              if (!container.id) {
 150                  container.id = '{$targetid}';
 151              }
 152              resolve(container.id);
 153          EOF;
 154          $containerid = $this->evaluate_javascript_for_editor($editorid, $js);
 155  
 156          return $this->find('css', "#{$containerid}");
 157      }
 158  
 159      /**
 160       * Get the name of the iframe relating to the editor.
 161       *
 162       * If no name is found, then add one.
 163       *
 164       * If the editor it not found, then throw an exception.
 165       *
 166       * @param string $locator The name of the editor
 167       * @return string The name of the iframe
 168       */
 169      protected function get_editor_iframe_name(string $locator): string {
 170          return $this->get_editor_iframe_name_for_element($this->get_textarea_for_locator($locator));
 171      }
 172  
 173      /**
 174       * Get the name of the iframe relating to the editor.
 175       *
 176       * If no name is found, then add one.
 177       *
 178       * If the editor it not found, then throw an exception.
 179  
 180       * @param NodeElement $editor The editor element
 181       * @return string The name of the iframe
 182       */
 183      protected function get_editor_iframe_name_for_element(NodeElement $editor): string {
 184          $editorid = $editor->getAttribute('id');
 185  
 186          // Ensure that a name is set on the iframe relating to the editorid.
 187          $js = <<<EOF
 188              if (!instance.iframeElement.name) {
 189                  instance.iframeElement.name = '{$editorid}';
 190              }
 191              resolve(instance.iframeElement.name);
 192          EOF;
 193  
 194          return $this->evaluate_javascript_for_editor($editorid, $js);
 195      }
 196  
 197      /**
 198       * Normalise the fixture file path relative to the dirroot.
 199       *
 200       * @param string $filepath
 201       * @return string
 202       */
 203      protected function normalise_fixture_filepath(string $filepath): string {
 204          global $CFG;
 205  
 206          $filepath = str_replace('/', DIRECTORY_SEPARATOR, $filepath);
 207          if (!is_readable($filepath)) {
 208              $filepath = $CFG->dirroot . DIRECTORY_SEPARATOR . $filepath;
 209              if (!is_readable($filepath)) {
 210                  throw new ExpectationException('The file to be uploaded does not exist.', $this->getSession());
 211              }
 212          }
 213  
 214          return $filepath;
 215      }
 216  }