MDL-69107 form_autocomplete: Rewrite item selection
authorAndrew Nicols <andrew@nicols.co.uk>
Mon, 15 Jun 2020 01:44:39 +0000 (09:44 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Tue, 15 Dec 2020 01:08:05 +0000 (09:08 +0800)
The form_autocomplete is essentially a custom element. Unfortunately the
`setValue()` function in Mink has undesired actions so it is necessary
to write our own handling for it.

The standard Mink `setValue()` function focuses the element, sets a
value, and then blurs the element. In the case of the autocomplete this
can cause the autocomplete suggestions list to be closed in some
situations. Instead of using the setValue we click, and type the value,
but do not immediately blur.

lib/amd/build/form-autocomplete.min.js
lib/amd/build/form-autocomplete.min.js.map
lib/amd/src/form-autocomplete.js
lib/behat/behat_field_manager.php
lib/behat/form_field/behat_form_autocomplete.php
lib/templates/form_autocomplete_input.mustache
lib/tests/behat/behat_forms.php

index 23e42cb..71d7b14 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index 5c1fe60..89cf819 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js.map and b/lib/amd/build/form-autocomplete.min.js.map differ
index f7f0f21..ffe8b07 100644 (file)
@@ -1173,6 +1173,9 @@ function($, log, str, templates, notification, LoadingIcon, Aria) {
                 originalSelect.hide();
                 var container = originalSelect.parent();
 
                 originalSelect.hide();
                 var container = originalSelect.parent();
 
+                // Ensure that the data-fieldtype is set for behat.
+                input.find('input').attr('data-fieldtype', 'autocomplete');
+
                 container.append(layout);
                 container.find('[data-region="form_autocomplete-input"]').replaceWith(input);
                 container.find('[data-region="form_autocomplete-suggestions"]').replaceWith(suggestions);
                 container.append(layout);
                 container.find('[data-region="form_autocomplete-input"]').replaceWith(input);
                 container.find('[data-region="form_autocomplete-suggestions"]').replaceWith(suggestions);
index 1a807a2..193d2af 100644 (file)
@@ -101,7 +101,6 @@ class behat_field_manager {
      * @return behat_form_field
      */
     public static function get_field_instance($type, NodeElement $fieldnode, Session $session) {
      * @return behat_form_field
      */
     public static function get_field_instance($type, NodeElement $fieldnode, Session $session) {
-
         global $CFG;
 
         // If the field is not part of a moodleform, we should still try to find out
         global $CFG;
 
         // If the field is not part of a moodleform, we should still try to find out
@@ -152,6 +151,10 @@ class behat_field_manager {
             $type = $fieldnode->getAttribute('type');
             switch ($type) {
                 case 'text':
             $type = $fieldnode->getAttribute('type');
             switch ($type) {
                 case 'text':
+                    if ($fieldtype = $fieldnode->getAttribute('data-fieldtype')) {
+                        return self::normalise_fieldtype($fieldtype);
+                    }
+                    return 'text';
                 case 'password':
                 case 'email':
                 case 'file':
                 case 'password':
                 case 'email':
                 case 'file':
index 238bbee..6c4b59c 100644 (file)
@@ -48,43 +48,75 @@ class behat_form_autocomplete extends behat_form_text {
             throw new coding_exception('Setting the value of an autocomplete field requires javascript.');
         }
 
             throw new coding_exception('Setting the value of an autocomplete field requires javascript.');
         }
 
-        // Set the value of the autocomplete's input.
-        // If this autocomplete offers suggestions then these should be fetched by setting the value and waiting for the
-        // JS to finish fetching those suggestions.
+        // Clear all current selections.
+        $rootnode = $this->field->getParent()->getParent();
+        $selections = $rootnode->findAll('css', '.form-autocomplete-selection [role=option]');
+        foreach ($selections as $selection) {
+            $selection->click();
+            $this->wait_for_pending_js();
+        }
 
 
-        $istagelement = $this->field->hasAttribute('data-tags') && $this->field->getAttribute('data-tags');
+        $allowscreation = $this->field->hasAttribute('data-tags') && !empty($this->field->getAttribute('data-tags'));
+        $hasmultiple = $this->field->hasAttribute('data-multiple') && !empty($this->field->getAttribute('data-multiple'));
 
 
-        if ($istagelement && false !== strpos($value, ',')) {
-            // Commas have a special meaning as a value separator in 'tag' autocomplete elements.
+        if ($hasmultiple && false !== strpos($value, ',')) {
+            // Commas have a special meaning as a value separator in 'multiple' autocomplete elements.
             // To handle this we break the value up by comma, and enter it in chunks.
             $values = explode(',', $value);
 
             while ($value = array_shift($values)) {
             // To handle this we break the value up by comma, and enter it in chunks.
             $values = explode(',', $value);
 
             while ($value = array_shift($values)) {
-                $this->set_value($value);
+                $this->add_value(trim($value), $allowscreation);
             }
         } else {
             }
         } else {
-            $this->field->setValue($value);
-            $this->wait_for_pending_js();
+            $this->add_value(trim($value), $allowscreation);
+        }
+    }
 
 
-            // If the autocomplete found suggestions, then it will have:
-            // 1) marked itself as expanded; and
-            // 2) have an aria-selected suggestion in the list.
-            $expanded = $this->field->getAttribute('aria-expanded');
-            $suggestion = $this->field->getParent()->find('css', '.form-autocomplete-suggestions > [aria-selected="true"]');
-
-            if ($expanded && null !== $suggestion) {
-                // A suggestion was found.
-                // Click on the first item in the list.
-                $suggestion->click();
-            } else {
-                // Press the return key to create a new tag.
-                behat_base::type_keys($this->session, [behat_keys::ENTER]);
-            }
-            $this->wait_for_pending_js();
+    /**
+     * Add a value to the autocomplete.
+     *
+     * @param   string $value
+     * @param   bool $allowscreation
+     */
+    protected function add_value(string $value, bool $allowscreation): void {
+        $value = trim($value);
 
 
-            // Press the escape to close the autocomplete suggestions list.
-            behat_base::type_keys($this->session, [behat_keys::ESCAPE]);
-            $this->wait_for_pending_js();
+        // Click into the field.
+        $this->field->click();
+
+        // Remove any existing text.
+        do {
+            behat_base::type_keys($this->session, [behat_keys::BACKSPACE, behat_keys::DELETE]);
+        } while (strlen($this->field->getValue()) > 0);
+        $this->wait_for_pending_js();
+
+        // Type in the new value.
+        behat_base::type_keys($this->session, str_split($value));
+        $this->wait_for_pending_js();
+
+        // If the autocomplete found suggestions, then it will have:
+        // 1) marked itself as expanded; and
+        // 2) have an aria-selected suggestion in the list.
+        $expanded = $this->field->getAttribute('aria-expanded');
+        $suggestion = $this->field->getParent()->getParent()->find('css', '.form-autocomplete-suggestions > [aria-selected="true"]');
+
+        if ($expanded && null !== $suggestion) {
+            // A suggestion was found.
+            // Click on the first item in the list.
+            $suggestion->click();
+        } else if ($allowscreation) {
+            // Press the return key to create a new entry.
+            behat_base::type_keys($this->session, [behat_keys::ENTER]);
+        } else {
+            throw new \InvalidArgumentException(
+                "Unable to find '{$value}' in the list of options, and unable to create a new option"
+            );
         }
         }
+
+        $this->wait_for_pending_js();
+
+        // Press the escape to close the autocomplete suggestions list.
+        behat_base::type_keys($this->session, [behat_keys::ESCAPE]);
+        $this->wait_for_pending_js();
     }
 }
     }
 }
index ac76ed5..ae7d194 100644 (file)
 }}
 {{#showSuggestions}}
 <div class="d-md-inline-block mr-md-2 position-relative">
 }}
 {{#showSuggestions}}
 <div class="d-md-inline-block mr-md-2 position-relative">
-    <input type="text" id="{{inputId}}" class="form-control" list="{{suggestionsId}}" placeholder="{{placeholder}}" role="combobox" aria-expanded="false" autocomplete="off" autocorrect="off" autocapitalize="off" aria-autocomplete="list" aria-owns="{{suggestionsId}} {{selectionId}}" {{#tags}}data-tags="1"{{/tags}}/>
+    <input type="text"{{!
+    }} id="{{inputId}}"{{!
+    }} class="form-control"{{!
+    }} list="{{suggestionsId}}"{{!
+    }} placeholder="{{placeholder}}"{{!
+    }} role="combobox"{{!
+    }} aria-expanded="false"{{!
+    }} autocomplete="off"{{!
+    }} autocorrect="off"{{!
+    }} autocapitalize="off"{{!
+    }} aria-autocomplete="list"{{!
+    }} aria-owns="{{suggestionsId}} {{selectionId}}"{{!
+    }} {{#tags}}data-tags="1"{{/tags}}{{!
+    }} {{#multiple}}data-multiple="multiple"{{/multiple}}{{!
+    }}>
     <span class="form-autocomplete-downarrow position-absolute p-1" id="{{downArrowId}}">&#x25BC;</span>
 </div>
 {{/showSuggestions}}
 {{^showSuggestions}}
 <div class="d-md-inline-block mr-md-2">
     <span class="form-autocomplete-downarrow position-absolute p-1" id="{{downArrowId}}">&#x25BC;</span>
 </div>
 {{/showSuggestions}}
 {{^showSuggestions}}
 <div class="d-md-inline-block mr-md-2">
-    <input type="text" id="{{inputId}}" class="form-control" placeholder="{{placeholder}}" role="textbox" aria-owns="{{selectionId}}" {{#tags}}data-tags="1"{{/tags}}/>
+    <input type="text"{{!
+    }} id="{{inputId}}"{{!
+    }} class="form-control"{{!
+    }} placeholder="{{placeholder}}"{{!
+    }} role="textbox"{{!
+    }} aria-owns="{{selectionId}}"{{!
+    }} {{#tags}}data-tags="1"{{/tags}}{{!
+    }} {{#multiple}}data-multiple="multiple"{{/multiple}}{{!
+    }}>
 </div>
 {{/showSuggestions}}
 
 </div>
 {{/showSuggestions}}
 
index 2532f94..1d2a3e4 100644 (file)
@@ -693,8 +693,6 @@ class behat_forms extends behat_base {
         $xpathtarget = "//ul[@class='form-autocomplete-suggestions']//*[contains(concat('|', string(.), '|'),'|" . $item . "|')]";
 
         $this->execute('behat_general::i_click_on', [$xpathtarget, 'xpath_element']);
         $xpathtarget = "//ul[@class='form-autocomplete-suggestions']//*[contains(concat('|', string(.), '|'),'|" . $item . "|')]";
 
         $this->execute('behat_general::i_click_on', [$xpathtarget, 'xpath_element']);
-
-        $this->execute('behat_general::i_press_key_in_element', ['13', 'body', 'xpath_element']);
     }
 
     /**
     }
 
     /**