From 7bfb575f633d5a828afd4be41525d7c16cbf60cb Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 22 May 2018 07:48:54 +0800 Subject: [PATCH 1/5] MDL-62514 behat: Add a wait_for_pending_js to form field --- lib/behat/behat_base.php | 36 ++++++++++++++--------- lib/behat/form_field/behat_form_field.php | 14 +++++++++ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/lib/behat/behat_base.php b/lib/behat/behat_base.php index f1daac04d05c8..a6cca8ad2f14e 100644 --- a/lib/behat/behat_base.php +++ b/lib/behat/behat_base.php @@ -28,10 +28,11 @@ // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. -use Behat\Mink\Exception\DriverException, - Behat\Mink\Exception\ExpectationException as ExpectationException, - Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException, - Behat\Mink\Element\NodeElement as NodeElement; +use Behat\Mink\Exception\DriverException; +use Behat\Mink\Exception\ExpectationException; +use Behat\Mink\Exception\ElementNotFoundException; +use Behat\Mink\Element\NodeElement; +use Behat\Mink\Session; /** * Steps definitions base class. @@ -709,23 +710,30 @@ protected function resize_window($windowsize, $viewport = false) { /** * Waits for all the JS to be loaded. * - * @throws \Exception - * @throws NoSuchWindow - * @throws UnknownError - * @return bool True or false depending whether all the JS is loaded or not. + * @return bool Whether any JS is still pending completion. */ public function wait_for_pending_js() { - // Waiting for JS is only valid for JS scenarios. if (!$this->running_javascript()) { - return; + // JS is not available therefore there is nothing to wait for. + return false; } + return static::wait_for_pending_js_in_session($this->getSession()); + } + + /** + * Waits for all the JS to be loaded. + * + * @param Session $session The Mink Session where JS can be run + * @return bool Whether any JS is still pending completion. + */ + public static function wait_for_pending_js_in_session(Session $session) { // We don't use behat_base::spin() here as we don't want to end up with an exception // if the page & JSs don't finish loading properly. for ($i = 0; $i < self::EXTENDED_TIMEOUT * 10; $i++) { $pending = ''; try { - $jscode = ' + $jscode = trim(preg_replace('/\s+/', ' ', ' return (function() { if (typeof M === "undefined") { if (document.readyState === "complete") { @@ -740,8 +748,8 @@ public function wait_for_pending_js() { } else { return "incomplete" } - }());'; - $pending = $this->getSession()->evaluateScript($jscode); + }());')); + $pending = $session->evaluateScript($jscode); } catch (NoSuchWindow $nsw) { // We catch an exception here, in case we just closed the window we were interacting with. // No javascript is running if there is no window right? @@ -762,7 +770,7 @@ public function wait_for_pending_js() { usleep(100000); } - // Timeout waiting for JS to complete. It will be catched and forwarded to behat_hooks::i_look_for_exceptions(). + // Timeout waiting for JS to complete. It will be caught and forwarded to behat_hooks::i_look_for_exceptions(). // It is unlikely that Javascript code of a page or an AJAX request needs more than self::EXTENDED_TIMEOUT seconds // to be loaded, although when pages contains Javascript errors M.util.js_complete() can not be executed, so the // number of JS pending code and JS completed code will not match and we will reach this point. diff --git a/lib/behat/form_field/behat_form_field.php b/lib/behat/form_field/behat_form_field.php index 56d7124f117bf..d6da75c63c444 100644 --- a/lib/behat/form_field/behat_form_field.php +++ b/lib/behat/form_field/behat_form_field.php @@ -172,6 +172,20 @@ protected function running_javascript() { return get_class($this->session->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver'; } + /** + * Waits for all the JS activity to be completed. + * + * @return bool Whether any JS is still pending completion. + */ + protected function wait_for_pending_js() { + if (!$this->running_javascript()) { + // JS is not available therefore there is nothing to wait for. + return false; + } + + return behat_base::wait_for_pending_js_in_session($this->session); + } + /** * Gets the field internal id used by selenium wire protocol. * From e994dea0b3b583056d7738c184b0ede242afdcba Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 22 May 2018 07:49:14 +0800 Subject: [PATCH 2/5] MDL-62514 behat: Rewrite handling of autocomplete This includes a minor restructure of the autocomplete JS to make use of promises and improve tracking of pending JS. In particular it improves the way in which throttled text input is handled to ensure that the behat does not continue until: - typing is fully complete; and - all possible ajax requests have been sent; and - all possible ajax requests complete; and - the suggestions are updated. A number of conditions existed where behat would move on to the next step too early in a race condition effect between Behat and Autocomplete. --- lib/amd/build/form-autocomplete.min.js | 2 +- lib/amd/src/form-autocomplete.js | 396 ++++++++++++------ .../form_field/behat_form_autocomplete.php | 48 ++- .../form_autocomplete_input.mustache | 2 +- .../tests/behat/coursemapping.feature | 4 - .../core/form_autocomplete_input.mustache | 2 +- theme/upgrade.txt | 3 + 7 files changed, 307 insertions(+), 150 deletions(-) diff --git a/lib/amd/build/form-autocomplete.min.js b/lib/amd/build/form-autocomplete.min.js index e66e5ff27cdf8..830d6fbc36299 100644 --- a/lib/amd/build/form-autocomplete.min.js +++ b/lib/amd/build/form-autocomplete.min.js @@ -1 +1 @@ -define(["jquery","core/log","core/str","core/templates","core/notification"],function(a,b,c,d,e){var f={DOWN:40,ENTER:13,SPACE:32,ESCAPE:27,COMMA:44,UP:38},g=a.now(),h=function(b,c){var d=a(document.getElementById(c.selectionId)),e=d.children("[aria-selected=true]").length;for(b%=e;b<0;)b+=e;var f=a(d.children("[aria-selected=true]").get(b)),g=c.selectionId+"-"+b;d.children().attr("data-active-selection",!1).attr("id",""),f.attr("data-active-selection",!0).attr("id",g),d.attr("aria-activedescendant",g)},i=function(b,c,f){var g=[],i=a(document.getElementById(c.selectionId)),j=i.attr("aria-activedescendant"),k=!1;j&&(k=a(document.getElementById(j)).attr("data-value")),f.children("option").each(function(b,c){if(a(c).prop("selected")){var d;d=a(c).data("html")?a(c).data("html"):a(c).html(),g.push({label:d,value:a(c).attr("value")})}});var l=a.extend({items:g},b,c);d.render("core/form_autocomplete_selection",l).done(function(b){i.empty().append(a(b).html()),k!==!1&&i.children("[aria-selected=true]").each(function(b,d){a(d).attr("data-value")===k&&h(b,c)})}).fail(e.exception)},j=function(a){"undefined"!=typeof M.core_formchangechecker&&M.core_formchangechecker.set_form_changed(),a.change()},k=function(b,c,d,e){var f=a(d).attr("data-value");b.multiple&&e.children("option").each(function(b,c){a(c).attr("value")==f&&(a(c).prop("selected",!1),a(c).attr("data-iscustom")&&a(c).remove())}),i(b,c,e),j(e)},l=function(b,c){var d=a(document.getElementById(c.inputId)),e=a(document.getElementById(c.suggestionsId)),f=e.children("[aria-hidden=false]").length;for(b%=f;b<0;)b+=f;var g=a(e.children("[aria-hidden=false]").get(b)),h=a(e.children("[role=option]")).index(g),i=c.suggestionsId+"-"+h;e.children().attr("aria-selected",!1).attr("id",""),g.attr("aria-selected",!0).attr("id",i),d.attr("aria-activedescendant",i);var j=g.offset().top-e.offset().top+e.scrollTop()-e.height()/2;e.animate({scrollTop:j},100)},m=function(b){var c=a(document.getElementById(b.suggestionsId)),d=c.children("[aria-selected=true]"),e=c.children("[aria-hidden=false]").index(d);l(e+1,b)},n=function(b){var c=a(document.getElementById(b.selectionId)),d=c.children("[data-active-selection=true]");if(!d)return void h(0,b);var e=c.children("[aria-selected=true]").index(d);h(e-1,b)},o=function(b){var c=a(document.getElementById(b.selectionId)),d=c.children("[data-active-selection=true]");if(!d)return void h(0,b);var e=c.children("[aria-selected=true]").index(d);h(e+1,b)},p=function(b){var c=a(document.getElementById(b.suggestionsId)),d=c.children("[aria-selected=true]"),e=c.children("[aria-hidden=false]").index(d);l(e-1,b)},q=function(b){var c=a(document.getElementById(b.inputId)),d=a(document.getElementById(b.suggestionsId));c.attr("aria-expanded",!1).attr("aria-activedescendant",b.selectionId),d.hide().attr("aria-hidden",!0)},r=function(b,f,g,h){var i=a(document.getElementById(f.inputId)),j=a(document.getElementById(f.suggestionsId)),k=!1,m=[];h.children("option").each(function(b,c){a(c).prop("selected")!==!0&&(m[m.length]={label:c.innerHTML,value:a(c).attr("value")})});var n=f.caseSensitive?g:g.toLocaleLowerCase(),o=a.extend({options:m},b,f);d.render("core/form_autocomplete_suggestions",o).done(function(d){j.replaceWith(d),j=a(document.getElementById(f.suggestionsId)),j.show().attr("aria-hidden",!1),j.children().each(function(c,d){d=a(d),b.caseSensitive&&d.text().indexOf(n)>-1||!b.caseSensitive&&d.text().toLocaleLowerCase().indexOf(n)>-1?(d.show().attr("aria-hidden",!1),k=!0):d.hide().attr("aria-hidden",!0)}),i.attr("aria-expanded",!0),h.attr("data-notice")?j.html(h.attr("data-notice")):k?b.tags||l(0,f):c.get_string("nosuggestions","form").done(function(a){j.html(a)})}).fail(e.exception)},s=function(b,c,d){var e=a(document.getElementById(c.inputId)),f=e.val(),g=f.split(","),h=!1;a.each(g,function(c,e){if(e=e.trim(),""!==e&&(b.multiple||d.children("option").prop("selected",!1),d.children("option").each(function(b,c){a(c).attr("value")==e&&(h=!0,a(c).prop("selected",!0))}),!h)){var f=a("