| 1 | efrain | 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 |  * Form fields helper.
 | 
        
           |  |  | 19 |  *
 | 
        
           |  |  | 20 |  * @package    core
 | 
        
           |  |  | 21 |  * @category   test
 | 
        
           |  |  | 22 |  * @copyright  2013 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\Session as Session,
 | 
        
           |  |  | 29 |     Behat\Mink\Element\NodeElement as NodeElement,
 | 
        
           |  |  | 30 |     Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
 | 
        
           |  |  | 31 |     Behat\MinkExtension\Context\RawMinkContext as RawMinkContext;
 | 
        
           |  |  | 32 |   | 
        
           |  |  | 33 | /**
 | 
        
           |  |  | 34 |  * Helper to interact with form fields.
 | 
        
           |  |  | 35 |  *
 | 
        
           |  |  | 36 |  * @package    core
 | 
        
           |  |  | 37 |  * @category   test
 | 
        
           |  |  | 38 |  * @copyright  2013 David Monllaó
 | 
        
           |  |  | 39 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 40 |  */
 | 
        
           |  |  | 41 | class behat_field_manager {
 | 
        
           |  |  | 42 |   | 
        
           |  |  | 43 |     /**
 | 
        
           |  |  | 44 |      * Gets an instance of the form field from it's label
 | 
        
           |  |  | 45 |      *
 | 
        
           |  |  | 46 |      * @param string $label
 | 
        
           |  |  | 47 |      * @param RawMinkContext $context
 | 
        
           |  |  | 48 |      * @return behat_form_field
 | 
        
           |  |  | 49 |      */
 | 
        
           |  |  | 50 |     public static function get_form_field_from_label($label, RawMinkContext $context) {
 | 
        
           |  |  | 51 |         // There are moodle form elements that are not directly related with
 | 
        
           |  |  | 52 |         // a basic HTML form field, we should also take care of them.
 | 
        
           |  |  | 53 |         // The DOM node.
 | 
        
           |  |  | 54 |         $fieldnode = $context->find_field($label);
 | 
        
           |  |  | 55 |   | 
        
           |  |  | 56 |         // The behat field manager.
 | 
        
           |  |  | 57 |         $field = self::get_form_field($fieldnode, $context->getSession());
 | 
        
           |  |  | 58 |         return $field;
 | 
        
           |  |  | 59 |     }
 | 
        
           |  |  | 60 |   | 
        
           |  |  | 61 |     /**
 | 
        
           |  |  | 62 |      * Gets an instance of the form field.
 | 
        
           |  |  | 63 |      *
 | 
        
           |  |  | 64 |      * Not all the fields are part of a moodle form, in this
 | 
        
           |  |  | 65 |      * cases it fallsback to the generic form field. Also note
 | 
        
           |  |  | 66 |      * that this generic field type is using a generic setValue()
 | 
        
           |  |  | 67 |      * method from the Behat API, which is not always good to set
 | 
        
           |  |  | 68 |      * the value of form elements.
 | 
        
           |  |  | 69 |      *
 | 
        
           |  |  | 70 |      * @param NodeElement $fieldnode
 | 
        
           |  |  | 71 |      * @param Session $session The behat browser session
 | 
        
           |  |  | 72 |      * @return behat_form_field
 | 
        
           |  |  | 73 |      */
 | 
        
           |  |  | 74 |     public static function get_form_field(NodeElement $fieldnode, Session $session) {
 | 
        
           |  |  | 75 |         // Get the field type if is part of a moodleform.
 | 
        
           |  |  | 76 |         if (self::is_moodleform_field($fieldnode)) {
 | 
        
           |  |  | 77 |             $type = self::get_field_node_type($fieldnode, $session);
 | 
        
           |  |  | 78 |         }
 | 
        
           |  |  | 79 |   | 
        
           |  |  | 80 |         // If is not a moodleforms field use the base field type.
 | 
        
           |  |  | 81 |         if (empty($type)) {
 | 
        
           |  |  | 82 |             $type = 'field';
 | 
        
           |  |  | 83 |         }
 | 
        
           |  |  | 84 |   | 
        
           |  |  | 85 |         return self::get_field_instance($type, $fieldnode, $session);
 | 
        
           |  |  | 86 |     }
 | 
        
           |  |  | 87 |   | 
        
           |  |  | 88 |     /**
 | 
        
           |  |  | 89 |      * Returns the appropiate behat_form_field according to the provided type.
 | 
        
           |  |  | 90 |      *
 | 
        
           |  |  | 91 |      * It defaults to behat_form_field.
 | 
        
           |  |  | 92 |      *
 | 
        
           |  |  | 93 |      * @param string $type The field type (checkbox, date_selector, text...)
 | 
        
           |  |  | 94 |      * @param NodeElement $fieldnode
 | 
        
           |  |  | 95 |      * @param Session $session The behat session
 | 
        
           |  |  | 96 |      * @return behat_form_field
 | 
        
           |  |  | 97 |      */
 | 
        
           |  |  | 98 |     public static function get_field_instance($type, NodeElement $fieldnode, Session $session) {
 | 
        
           |  |  | 99 |         global $CFG;
 | 
        
           |  |  | 100 |   | 
        
           |  |  | 101 |         // If the field is not part of a moodleform, we should still try to find out
 | 
        
           |  |  | 102 |         // which field type are we dealing with.
 | 
        
           |  |  | 103 |         if ($type == 'field' && $guessedtype = self::guess_field_type($fieldnode, $session)) {
 | 
        
           |  |  | 104 |             $type = $guessedtype;
 | 
        
           |  |  | 105 |         }
 | 
        
           |  |  | 106 |   | 
        
           |  |  | 107 |         $classname = 'behat_form_' . $type;
 | 
        
           |  |  | 108 |   | 
        
           |  |  | 109 |         // Fallsback on the type guesser if nothing specific exists.
 | 
        
           |  |  | 110 |         $classpath = $CFG->libdir . '/behat/form_field/' . $classname . '.php';
 | 
        
           |  |  | 111 |         if (!file_exists($classpath)) {
 | 
        
           |  |  | 112 |             $classname = 'behat_form_field';
 | 
        
           |  |  | 113 |             $classpath = $CFG->libdir . '/behat/form_field/' . $classname . '.php';
 | 
        
           |  |  | 114 |         }
 | 
        
           |  |  | 115 |   | 
        
           |  |  | 116 |         // Returns the instance.
 | 
        
           |  |  | 117 |         require_once($classpath);
 | 
        
           |  |  | 118 |         return new $classname($session, $fieldnode);
 | 
        
           |  |  | 119 |     }
 | 
        
           |  |  | 120 |   | 
        
           |  |  | 121 |     /**
 | 
        
           |  |  | 122 |      * Guesses a basic field type and returns it.
 | 
        
           |  |  | 123 |      *
 | 
        
           |  |  | 124 |      * This method is intended to detect HTML form fields when no
 | 
        
           |  |  | 125 |      * moodleform-specific elements have been detected.
 | 
        
           |  |  | 126 |      *
 | 
        
           |  |  | 127 |      * @param NodeElement $fieldnode
 | 
        
           |  |  | 128 |      * @param Session $session
 | 
        
           |  |  | 129 |      * @return string|bool The field type or false.
 | 
        
           |  |  | 130 |      */
 | 
        
           |  |  | 131 |     public static function guess_field_type(NodeElement $fieldnode, Session $session) {
 | 
        
           |  |  | 132 |         [
 | 
        
           |  |  | 133 |             'document' => $document,
 | 
        
           |  |  | 134 |             'node' => $node,
 | 
        
           |  |  | 135 |         ] = self::get_dom_elements_for_node($fieldnode, $session);
 | 
        
           |  |  | 136 |   | 
        
           |  |  | 137 |         // If the type is explicitly set on the element pointed to by the label - use it.
 | 
        
           |  |  | 138 |         if ($fieldtype = $node->getAttribute('data-fieldtype')) {
 | 
        
           |  |  | 139 |             return self::normalise_fieldtype($fieldtype);
 | 
        
           |  |  | 140 |         }
 | 
        
           |  |  | 141 |   | 
        
           |  |  | 142 |         // Textareas are considered text based elements.
 | 
        
           |  |  | 143 |         $tagname = strtolower($node->nodeName);
 | 
        
           |  |  | 144 |         if ($tagname == 'textarea') {
 | 
        
           |  |  | 145 |             $xpath = new \DOMXPath($document);
 | 
        
           |  |  | 146 |   | 
        
           |  |  | 147 |             // If there is an iframe with $id + _ifr there a TinyMCE editor loaded.
 | 
        
           |  |  | 148 |             if ($xpath->query('//div[@id="' . $node->getAttribute('id') . 'editable"]')->count() !== 0) {
 | 
        
           |  |  | 149 |                 return 'editor';
 | 
        
           |  |  | 150 |             }
 | 
        
           |  |  | 151 |             return 'textarea';
 | 
        
           |  |  | 152 |   | 
        
           |  |  | 153 |         }
 | 
        
           |  |  | 154 |   | 
        
           |  |  | 155 |         if ($tagname == 'input') {
 | 
        
           |  |  | 156 |             switch ($node->getAttribute('type')) {
 | 
        
           |  |  | 157 |                 case 'text':
 | 
        
           |  |  | 158 |                 case 'password':
 | 
        
           |  |  | 159 |                 case 'email':
 | 
        
           |  |  | 160 |                 case 'file':
 | 
        
           |  |  | 161 |                     return 'text';
 | 
        
           |  |  | 162 |                 case 'checkbox':
 | 
        
           |  |  | 163 |                     return 'checkbox';
 | 
        
           |  |  | 164 |                     break;
 | 
        
           |  |  | 165 |                 case 'radio':
 | 
        
           |  |  | 166 |                     return 'radio';
 | 
        
           |  |  | 167 |                     break;
 | 
        
           | 1441 | ariadna | 168 |                 case 'datetime-local':
 | 
        
           |  |  | 169 |                     return 'datetime_local';
 | 
        
           | 1 | efrain | 170 |                 default:
 | 
        
           |  |  | 171 |                     // Here we return false because all text-based
 | 
        
           |  |  | 172 |                     // fields should be included in the first switch case.
 | 
        
           |  |  | 173 |                     return false;
 | 
        
           |  |  | 174 |             }
 | 
        
           |  |  | 175 |   | 
        
           |  |  | 176 |         }
 | 
        
           |  |  | 177 |   | 
        
           |  |  | 178 |         if ($tagname == 'select') {
 | 
        
           |  |  | 179 |             // Select tag.
 | 
        
           |  |  | 180 |             return 'select';
 | 
        
           |  |  | 181 |         }
 | 
        
           |  |  | 182 |   | 
        
           |  |  | 183 |         if ($tagname == 'span') {
 | 
        
           |  |  | 184 |             if ($node->hasAttribute('data-inplaceeditable') && $node->getAttribute('data-inplaceeditable')) {
 | 
        
           |  |  | 185 |                 // Determine appropriate editable type of this field (text or select).
 | 
        
           |  |  | 186 |                 if ($node->getAttribute('data-type') == 'select') {
 | 
        
           |  |  | 187 |                     return 'inplaceeditable_select';
 | 
        
           |  |  | 188 |                 } else {
 | 
        
           |  |  | 189 |                     return 'inplaceeditable';
 | 
        
           |  |  | 190 |                 }
 | 
        
           |  |  | 191 |             }
 | 
        
           |  |  | 192 |         }
 | 
        
           |  |  | 193 |   | 
        
           |  |  | 194 |         if ($tagname == 'div') {
 | 
        
           |  |  | 195 |             if ($node->getAttribute('role') == 'combobox') {
 | 
        
           |  |  | 196 |                 return 'select_menu';
 | 
        
           |  |  | 197 |             }
 | 
        
           |  |  | 198 |         }
 | 
        
           |  |  | 199 |   | 
        
           |  |  | 200 |         // We can not provide a closer field type.
 | 
        
           |  |  | 201 |         return false;
 | 
        
           |  |  | 202 |     }
 | 
        
           |  |  | 203 |   | 
        
           |  |  | 204 |     /**
 | 
        
           |  |  | 205 |      * Detects when the field is a moodleform field type.
 | 
        
           |  |  | 206 |      *
 | 
        
           |  |  | 207 |      * Note that there are fields inside moodleforms that are not
 | 
        
           |  |  | 208 |      * moodleform element; this method can not detect this, this will
 | 
        
           |  |  | 209 |      * be managed by get_field_node_type, after failing to find the form
 | 
        
           |  |  | 210 |      * element element type.
 | 
        
           |  |  | 211 |      *
 | 
        
           |  |  | 212 |      * @param NodeElement $fieldnode
 | 
        
           |  |  | 213 |      * @return bool
 | 
        
           |  |  | 214 |      */
 | 
        
           |  |  | 215 |     protected static function is_moodleform_field(NodeElement $fieldnode) {
 | 
        
           |  |  | 216 |   | 
        
           |  |  | 217 |         // We already waited when getting the NodeElement and we don't want an exception if it's not part of a moodleform.
 | 
        
           |  |  | 218 |         $parentformfound = $fieldnode->find('xpath',
 | 
        
           |  |  | 219 |             "/ancestor::form[contains(concat(' ', normalize-space(@class), ' '), ' mform ')]"
 | 
        
           |  |  | 220 |         );
 | 
        
           |  |  | 221 |   | 
        
           |  |  | 222 |         return ($parentformfound != false);
 | 
        
           |  |  | 223 |     }
 | 
        
           |  |  | 224 |   | 
        
           |  |  | 225 |     /**
 | 
        
           |  |  | 226 |      * Get the DOMDocument and DOMElement for a NodeElement.
 | 
        
           |  |  | 227 |      *
 | 
        
           |  |  | 228 |      * @param NodeElement $fieldnode
 | 
        
           |  |  | 229 |      * @param Session $session
 | 
        
           |  |  | 230 |      * @return array
 | 
        
           |  |  | 231 |      */
 | 
        
           |  |  | 232 |     protected static function get_dom_elements_for_node(NodeElement $fieldnode, Session $session): array {
 | 
        
           |  |  | 233 |         $html = $session->getPage()->getContent();
 | 
        
           |  |  | 234 |   | 
        
           |  |  | 235 |         $document = new \DOMDocument();
 | 
        
           |  |  | 236 |   | 
        
           |  |  | 237 |         $previousinternalerrors = libxml_use_internal_errors(true);
 | 
        
           |  |  | 238 |         $document->loadHTML($html, LIBXML_HTML_NODEFDTD | LIBXML_BIGLINES);
 | 
        
           |  |  | 239 |         libxml_clear_errors();
 | 
        
           |  |  | 240 |         libxml_use_internal_errors($previousinternalerrors);
 | 
        
           |  |  | 241 |   | 
        
           |  |  | 242 |         $xpath = new \DOMXPath($document);
 | 
        
           |  |  | 243 |         $node = $xpath->query($fieldnode->getXpath())->item(0);
 | 
        
           |  |  | 244 |   | 
        
           |  |  | 245 |         return [
 | 
        
           |  |  | 246 |             'document' => $document,
 | 
        
           |  |  | 247 |             'node' => $node,
 | 
        
           |  |  | 248 |         ];
 | 
        
           |  |  | 249 |     }
 | 
        
           |  |  | 250 |   | 
        
           |  |  | 251 |     /**
 | 
        
           |  |  | 252 |      * Recursive method to find the field type.
 | 
        
           |  |  | 253 |      *
 | 
        
           |  |  | 254 |      * Depending on the field the felement class node is in a level or in another. We
 | 
        
           |  |  | 255 |      * look recursively for a parent node with a 'felement' class to find the field type.
 | 
        
           |  |  | 256 |      *
 | 
        
           |  |  | 257 |      * @param NodeElement $fieldnode The current node.
 | 
        
           |  |  | 258 |      * @param Session $session The behat browser session
 | 
        
           |  |  | 259 |      * @return null|string A text description of the node type, or null if one could not be accurately determined
 | 
        
           |  |  | 260 |      */
 | 
        
           |  |  | 261 |     protected static function get_field_node_type(NodeElement $fieldnode, Session $session): ?string {
 | 
        
           |  |  | 262 |         [
 | 
        
           |  |  | 263 |             'document' => $document,
 | 
        
           |  |  | 264 |             'node' => $node,
 | 
        
           |  |  | 265 |         ] = self::get_dom_elements_for_node($fieldnode, $session);
 | 
        
           |  |  | 266 |   | 
        
           |  |  | 267 |         return self::get_field_type($document, $node, $session);
 | 
        
           |  |  | 268 |     }
 | 
        
           |  |  | 269 |   | 
        
           |  |  | 270 |     /**
 | 
        
           |  |  | 271 |      * Get the field type from the specified DOMElement.
 | 
        
           |  |  | 272 |      *
 | 
        
           |  |  | 273 |      * @param \DOMDocument $document
 | 
        
           |  |  | 274 |      * @param \DOMElement $node
 | 
        
           |  |  | 275 |      * @param Session $session
 | 
        
           |  |  | 276 |      * @return null|string
 | 
        
           |  |  | 277 |      */
 | 
        
           |  |  | 278 |     protected static function get_field_type(\DOMDocument $document, \DOMElement $node, Session $session): ?string {
 | 
        
           |  |  | 279 |         $xpath = new \DOMXPath($document);
 | 
        
           |  |  | 280 |   | 
        
           |  |  | 281 |         if ($node->getAttribute('name') === 'availabilityconditionsjson') {
 | 
        
           |  |  | 282 |             // Special handling for availability field which requires custom JavaScript.
 | 
        
           |  |  | 283 |             return 'availability';
 | 
        
           |  |  | 284 |         }
 | 
        
           |  |  | 285 |   | 
        
           |  |  | 286 |         if ($node->nodeName == 'html') {
 | 
        
           |  |  | 287 |             // The top of the document has been reached.
 | 
        
           |  |  | 288 |             return null;
 | 
        
           |  |  | 289 |         }
 | 
        
           |  |  | 290 |   | 
        
           |  |  | 291 |         // If the type is explictly set on the element pointed to by the label - use it.
 | 
        
           |  |  | 292 |         $fieldtype = $node->getAttribute('data-fieldtype');
 | 
        
           |  |  | 293 |         if ($fieldtype) {
 | 
        
           |  |  | 294 |             return self::normalise_fieldtype($fieldtype);
 | 
        
           |  |  | 295 |         }
 | 
        
           |  |  | 296 |   | 
        
           |  |  | 297 |         if ($xpath->query('/ancestor::*[@data-passwordunmaskid]', $node)->count() !== 0) {
 | 
        
           |  |  | 298 |             // This element has a passwordunmaskid as a parent.
 | 
        
           |  |  | 299 |             return 'passwordunmask';
 | 
        
           |  |  | 300 |         }
 | 
        
           |  |  | 301 |   | 
        
           |  |  | 302 |         // Fetch the parentnode only once.
 | 
        
           |  |  | 303 |         $parentnode = $node->parentNode;
 | 
        
           |  |  | 304 |         if ($parentnode instanceof \DOMDocument) {
 | 
        
           |  |  | 305 |             return null;
 | 
        
           |  |  | 306 |         }
 | 
        
           |  |  | 307 |   | 
        
           |  |  | 308 |         // Check the parent fieldtype before we check classes.
 | 
        
           |  |  | 309 |         $fieldtype = $parentnode->getAttribute('data-fieldtype');
 | 
        
           |  |  | 310 |         if ($fieldtype) {
 | 
        
           |  |  | 311 |             return self::normalise_fieldtype($fieldtype);
 | 
        
           |  |  | 312 |         }
 | 
        
           |  |  | 313 |   | 
        
           |  |  | 314 |         // We look for a parent node with 'felement' class.
 | 
        
           |  |  | 315 |         if ($class = $parentnode->getAttribute('class')) {
 | 
        
           |  |  | 316 |             if (strstr($class, 'felement') != false) {
 | 
        
           |  |  | 317 |                 // Remove 'felement f' from class value.
 | 
        
           |  |  | 318 |                 return substr($class, 10);
 | 
        
           |  |  | 319 |             }
 | 
        
           |  |  | 320 |   | 
        
           |  |  | 321 |             // Stop propagation through the DOM, if it does not have a felement is not part of a moodle form.
 | 
        
           |  |  | 322 |             if (strstr($class, 'fcontainer') != false) {
 | 
        
           |  |  | 323 |                 return null;
 | 
        
           |  |  | 324 |             }
 | 
        
           |  |  | 325 |         }
 | 
        
           |  |  | 326 |   | 
        
           |  |  | 327 |         // Move up the tree.
 | 
        
           |  |  | 328 |         return self::get_field_type($document, $parentnode, $session);
 | 
        
           |  |  | 329 |     }
 | 
        
           |  |  | 330 |   | 
        
           |  |  | 331 |     /**
 | 
        
           |  |  | 332 |      * Normalise the field type.
 | 
        
           |  |  | 333 |      *
 | 
        
           |  |  | 334 |      * @param string $fieldtype
 | 
        
           |  |  | 335 |      * @return string
 | 
        
           |  |  | 336 |      */
 | 
        
           |  |  | 337 |     protected static function normalise_fieldtype(string $fieldtype): string {
 | 
        
           |  |  | 338 |         if ($fieldtype === 'tags') {
 | 
        
           |  |  | 339 |             return 'autocomplete';
 | 
        
           |  |  | 340 |         }
 | 
        
           | 1441 | ariadna | 341 |         if ($fieldtype === 'date_time_selector') {
 | 
        
           |  |  | 342 |             return 'date_time';
 | 
        
           |  |  | 343 |         }
 | 
        
           |  |  | 344 |         if ($fieldtype === 'date_selector') {
 | 
        
           |  |  | 345 |             return 'date';
 | 
        
           |  |  | 346 |         }
 | 
        
           | 1 | efrain | 347 |   | 
        
           |  |  | 348 |         return $fieldtype;
 | 
        
           |  |  | 349 |     }
 | 
        
           |  |  | 350 | }
 |