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] [Versions 401 and 403]

   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       * @param string|null $actualvalue The actual value. If not specified, this will be fetched from $this->get_value().
 248       * @return bool
 249       */
 250      protected function text_matches($expectedvalue, ?string $actualvalue = null): bool {
 251          $actualvalue = $actualvalue ?? $this->get_value();
 252  
 253          // Non strict string comparison.
 254          if (trim($expectedvalue) == trim($actualvalue)) {
 255              return true;
 256          }
 257  
 258          // Do one more matching attempt for floats that are valid with current decsep in use
 259          // (let's continue non strict comparing them as strings, but once unformatted).
 260          $expectedfloat = unformat_float(trim($expectedvalue), true);
 261          $actualfloat = unformat_float(trim($actualvalue), true);
 262          // If they aren't null or false, then we are good to be compared (basically is_numeric()).
 263          $goodfloats = !is_null($expectedfloat) && ($expectedfloat !== false) &&
 264              !is_null($actualfloat) && ($actualfloat !== false);
 265          if ($goodfloats && ((string)$expectedfloat == (string)$actualfloat)) {
 266              return true;
 267          }
 268  
 269          return false;
 270      }
 271  
 272      /**
 273       * Gets the field locator.
 274       *
 275       * Defaults to the field label but you can
 276       * specify other locators if you are interested.
 277       *
 278       * Public visibility as in most cases will be hard to
 279       * use this method in a generic way, as fields can
 280       * be selected using multiple ways (label, id, name...).
 281       *
 282       * @throws coding_exception
 283       * @param string $locatortype
 284       * @return string
 285       */
 286      protected function get_field_locator($locatortype = false) {
 287  
 288          if (!empty($this->fieldlocator)) {
 289              return $this->fieldlocator;
 290          }
 291  
 292          $fieldid = $this->field->getAttribute('id');
 293  
 294          // Defaults to label.
 295          if ($locatortype == 'label' || $locatortype == false) {
 296  
 297              $labelnode = $this->session->getPage()->find('xpath', "//label[@for='$fieldid']|//p[@id='{$fieldid}_label']");
 298  
 299              // Exception only if $locatortype was specified.
 300              if (!$labelnode && $locatortype == 'label') {
 301                  throw new coding_exception('Field with "' . $fieldid . '" id does not have a label.');
 302              }
 303  
 304              $this->fieldlocator = $labelnode->getText();
 305          }
 306  
 307          // Let's look for the name as a second option (more popular than
 308          // id's when pointing to fields).
 309          if (($locatortype == 'name' || $locatortype == false) &&
 310                  empty($this->fieldlocator)) {
 311  
 312              $name = $this->field->getAttribute('name');
 313  
 314              // Exception only if $locatortype was specified.
 315              if (!$name && $locatortype == 'name') {
 316                  throw new coding_exception('Field with "' . $fieldid . '" id does not have a name attribute.');
 317              }
 318  
 319              $this->fieldlocator = $name;
 320          }
 321  
 322          // Otherwise returns the id if no specific locator type was provided.
 323          if (empty($this->fieldlocator)) {
 324              $this->fieldlocator = $fieldid;
 325          }
 326  
 327          return $this->fieldlocator;
 328      }
 329  }