Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 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 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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 false;
 256          }
 257          return true;
 258      }
 259  
 260      /**
 261       * Gets the field locator.
 262       *
 263       * Defaults to the field label but you can
 264       * specify other locators if you are interested.
 265       *
 266       * Public visibility as in most cases will be hard to
 267       * use this method in a generic way, as fields can
 268       * be selected using multiple ways (label, id, name...).
 269       *
 270       * @throws coding_exception
 271       * @param string $locatortype
 272       * @return string
 273       */
 274      protected function get_field_locator($locatortype = false) {
 275  
 276          if (!empty($this->fieldlocator)) {
 277              return $this->fieldlocator;
 278          }
 279  
 280          $fieldid = $this->field->getAttribute('id');
 281  
 282          // Defaults to label.
 283          if ($locatortype == 'label' || $locatortype == false) {
 284  
 285              $labelnode = $this->session->getPage()->find('xpath', "//label[@for='$fieldid']|//p[@id='{$fieldid}_label']");
 286  
 287              // Exception only if $locatortype was specified.
 288              if (!$labelnode && $locatortype == 'label') {
 289                  throw new coding_exception('Field with "' . $fieldid . '" id does not have a label.');
 290              }
 291  
 292              $this->fieldlocator = $labelnode->getText();
 293          }
 294  
 295          // Let's look for the name as a second option (more popular than
 296          // id's when pointing to fields).
 297          if (($locatortype == 'name' || $locatortype == false) &&
 298                  empty($this->fieldlocator)) {
 299  
 300              $name = $this->field->getAttribute('name');
 301  
 302              // Exception only if $locatortype was specified.
 303              if (!$name && $locatortype == 'name') {
 304                  throw new coding_exception('Field with "' . $fieldid . '" id does not have a name attribute.');
 305              }
 306  
 307              $this->fieldlocator = $name;
 308          }
 309  
 310          // Otherwise returns the id if no specific locator type was provided.
 311          if (empty($this->fieldlocator)) {
 312              $this->fieldlocator = $fieldid;
 313          }
 314  
 315          return $this->fieldlocator;
 316      }
 317  }