Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   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   * Generic moodleforms 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;
  29  use Behat\Mink\Session;
  30  
  31  /**
  32   * Representation of a form field.
  33   *
  34   * Basically an interface with Mink session.
  35   *
  36   * @package    core_form
  37   * @category   test
  38   * @copyright  2012 David MonllaĆ³
  39   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  40   */
  41  class behat_form_field implements behat_session_interface {
  42  
  43      // All of the functionality of behat_base is shared with form fields via the behat_session_trait trait.
  44      use behat_session_trait;
  45  
  46      /**
  47       * @var Session Behat session.
  48       */
  49      protected $session;
  50  
  51      /**
  52       * @var NodeElement The field DOM node to interact with.
  53       */
  54      protected $field;
  55  
  56      /**
  57       * @var string The field's locator.
  58       */
  59      protected $fieldlocator = false;
  60  
  61      /**
  62       * Returns the Mink session.
  63       *
  64       * @param   string|null $name name of the session OR active session will be used
  65       * @return  \Behat\Mink\Session
  66       */
  67      public function getSession($name = null) {
  68          return $this->session;
  69      }
  70  
  71  
  72      /**
  73       * General constructor with the node and the session to interact with.
  74       *
  75       * @param Session $session Reference to Mink session to traverse/modify the page DOM.
  76       * @param NodeElement $fieldnode The field DOM node
  77       * @return void
  78       */
  79      public function __construct(Session $session, NodeElement $fieldnode) {
  80          $this->session = $session;
  81          $this->field = $fieldnode;
  82      }
  83  
  84      /**
  85       * Sets the value to a field.
  86       *
  87       * @param string $value
  88       * @return void
  89       */
  90      public function set_value($value) {
  91          // We delegate to the best guess, if we arrived here
  92          // using the generic behat_form_field is because we are
  93          // dealing with a fgroup element.
  94          $instance = $this->guess_type();
  95          return $instance->set_value($value);
  96      }
  97  
  98      /**
  99       * Returns the current value of the select element.
 100       *
 101       * @return string
 102       */
 103      public function get_value() {
 104          // We delegate to the best guess, if we arrived here
 105          // using the generic behat_form_field is because we are
 106          // dealing with a fgroup element.
 107          $instance = $this->guess_type();
 108          return $instance->get_value();
 109      }
 110  
 111      /**
 112       * Presses specific keyboard key.
 113       *
 114       * @param mixed  $char     could be either char ('b') or char-code (98)
 115       * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta')
 116       */
 117      public function key_press($char, $modifier = null) {
 118          // We delegate to the best guess, if we arrived here
 119          // using the generic behat_form_field is because we are
 120          // dealing with a fgroup element.
 121          $instance = $this->guess_type();
 122          $instance->field->keyDown($char, $modifier);
 123          try {
 124              $instance->field->keyPress($char, $modifier);
 125              $instance->field->keyUp($char, $modifier);
 126          } catch (\Facebook\WebDriver\Exception\WebDriverException $e) {
 127              // If the JS handler attached to keydown or keypress destroys the element
 128              // the later events may trigger errors because form element no longer exist
 129              // or is not visible. Ignore such exceptions here.
 130          } catch (\Behat\Mink\Exception\ElementNotFoundException $e) {
 131              // Other Mink drivers can throw this for the same reason as above.
 132          }
 133      }
 134  
 135      /**
 136       * Generic match implementation
 137       *
 138       * Will work well with text-based fields, extension required
 139       * for most of the other cases.
 140       *
 141       * @param string $expectedvalue
 142       * @return bool The provided value matches the field value?
 143       */
 144      public function matches($expectedvalue) {
 145          // We delegate to the best guess, if we arrived here
 146          // using the generic behat_form_field is because we are
 147          // dealing with a fgroup element.
 148          $instance = $this->guess_type();
 149          return $instance->matches($expectedvalue);
 150      }
 151  
 152      /**
 153       * Get the value of an attribute set on this field.
 154       *
 155       * @param string $name The attribute name
 156       * @return string The attribute value
 157       */
 158      public function get_attribute($name) {
 159          return $this->field->getAttribute($name);
 160      }
 161  
 162      /**
 163       * Guesses the element type we are dealing with in case is not a text-based element.
 164       *
 165       * This class is the generic field type, behat_field_manager::get_form_field()
 166       * should be able to find the appropiate class for the field type, but
 167       * in cases like moodle form group elements we can not find the type of
 168       * the field through the DOM so we also need to take care of the
 169       * different field types from here. If we need to deal with more complex
 170       * moodle form elements we will need to refactor this simple HTML elements
 171       * guess method.
 172       *
 173       * @return behat_form_field
 174       */
 175      private function guess_type() {
 176          return $this->get_field_instance_for_element($this->field);
 177      }
 178  
 179      /**
 180       * Returns the appropriate form field object for a given node element.
 181       *
 182       * @param NodeElement $element The node element
 183       * @return behat_form_field
 184       */
 185      protected function get_field_instance_for_element(NodeElement $element): behat_form_field {
 186          global $CFG;
 187  
 188          // We default to the text-based field if nothing was detected.
 189          if (!$type = behat_field_manager::guess_field_type($element, $this->session)) {
 190              $type = 'text';
 191          }
 192  
 193          $classname = 'behat_form_' . $type;
 194          $classpath = $CFG->dirroot . '/lib/behat/form_field/' . $classname . '.php';
 195          require_once($classpath);
 196  
 197          return new $classname($this->session, $element);
 198      }
 199  
 200      /**
 201       * Returns whether the scenario is running in a browser that can run Javascript or not.
 202       *
 203       * @return bool
 204       */
 205      protected function running_javascript() {
 206          return get_class($this->session->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver';
 207      }
 208  
 209      /**
 210       * Waits for all the JS activity to be completed.
 211       *
 212       * @return bool Whether any JS is still pending completion.
 213       */
 214      protected function wait_for_pending_js() {
 215          if (!$this->running_javascript()) {
 216              // JS is not available therefore there is nothing to wait for.
 217              return false;
 218          }
 219  
 220          return behat_base::wait_for_pending_js_in_session($this->session);
 221      }
 222  
 223      /**
 224       * Gets the field internal id used by selenium wire protocol.
 225       *
 226       * Only available when running_javascript().
 227       *
 228       * @throws coding_exception
 229       * @return int
 230       */
 231      protected function get_internal_field_id() {
 232          if (!$this->running_javascript()) {
 233              throw new coding_exception('You can only get an internal ID using the selenium driver.');
 234          }
 235  
 236          return $this->getSession()
 237              ->getDriver()
 238              ->getWebDriver()
 239              ->findElement(WebDriverBy::xpath($node->getXpath()))
 240              ->getID();
 241      }
 242  
 243      /**
 244       * Checks if the provided text matches the field value.
 245       *
 246       * @param string $expectedvalue
 247       * @return bool
 248       */
 249      protected function text_matches($expectedvalue) {
 250          if (trim($expectedvalue) != trim($this->get_value())) {
 251              return false;
 252          }
 253          return true;
 254      }
 255  
 256      /**
 257       * Gets the field locator.
 258       *
 259       * Defaults to the field label but you can
 260       * specify other locators if you are interested.
 261       *
 262       * Public visibility as in most cases will be hard to
 263       * use this method in a generic way, as fields can
 264       * be selected using multiple ways (label, id, name...).
 265       *
 266       * @throws coding_exception
 267       * @param string $locatortype
 268       * @return string
 269       */
 270      protected function get_field_locator($locatortype = false) {
 271  
 272          if (!empty($this->fieldlocator)) {
 273              return $this->fieldlocator;
 274          }
 275  
 276          $fieldid = $this->field->getAttribute('id');
 277  
 278          // Defaults to label.
 279          if ($locatortype == 'label' || $locatortype == false) {
 280  
 281              $labelnode = $this->session->getPage()->find('xpath', "//label[@for='$fieldid']|//p[@id='{$fieldid}_label']");
 282  
 283              // Exception only if $locatortype was specified.
 284              if (!$labelnode && $locatortype == 'label') {
 285                  throw new coding_exception('Field with "' . $fieldid . '" id does not have a label.');
 286              }
 287  
 288              $this->fieldlocator = $labelnode->getText();
 289          }
 290  
 291          // Let's look for the name as a second option (more popular than
 292          // id's when pointing to fields).
 293          if (($locatortype == 'name' || $locatortype == false) &&
 294                  empty($this->fieldlocator)) {
 295  
 296              $name = $this->field->getAttribute('name');
 297  
 298              // Exception only if $locatortype was specified.
 299              if (!$name && $locatortype == 'name') {
 300                  throw new coding_exception('Field with "' . $fieldid . '" id does not have a name attribute.');
 301              }
 302  
 303              $this->fieldlocator = $name;
 304          }
 305  
 306          // Otherwise returns the id if no specific locator type was provided.
 307          if (empty($this->fieldlocator)) {
 308              $this->fieldlocator = $fieldid;
 309          }
 310  
 311          return $this->fieldlocator;
 312      }
 313  }