From 35738b348b311f745b84c9a5a29c391fccef3e53 Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Wed, 7 Oct 2026 16:14:57 -0400 Subject: [PATCH 1/3] Bug 2079453 - Upgrade devbridge-autocomplete to 2.0.1 and fix autocomplete dropdown reopening after the field loses focus --- .../ProdCompSearch/web/js/prod_comp_search.js | 9 +- js/field.js | 9 +- package-lock.json | 19 +-- package.json | 2 +- qa/t/2_test_autocomplete.t | 109 ++++++++++++++++++ 5 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 qa/t/2_test_autocomplete.t diff --git a/extensions/ProdCompSearch/web/js/prod_comp_search.js b/extensions/ProdCompSearch/web/js/prod_comp_search.js index 9f40745cef..94af3e21d8 100644 --- a/extensions/ProdCompSearch/web/js/prod_comp_search.js +++ b/extensions/ProdCompSearch/web/js/prod_comp_search.js @@ -26,8 +26,13 @@ $(function() { that.data('counter', that.data('counter') - 1); hideNotifications(this); - if (document.activeElement != this) - that.devbridgeAutocomplete('hide'); + // devbridge-autocomplete 2.x calls onSearchComplete before rendering + // suggestions, so defer the hide until after they have been shown + var input = this; + setTimeout(function() { + if (document.activeElement != input) + that.devbridgeAutocomplete('hide'); + }, 0); if (that.data('error')) { searchError.call(that[0], null, null, null, that.data('error')); that.data('error', ''); diff --git a/js/field.js b/js/field.js index de4880d748..8899453233 100644 --- a/js/field.js +++ b/js/field.js @@ -477,8 +477,13 @@ $(function() { that.data('counter', that.data('counter') - 1); if (that.data('counter') === 0) that.removeClass('autocomplete-running'); - if (document.activeElement != this) - that.devbridgeAutocomplete('hide'); + // devbridge-autocomplete 2.x calls onSearchComplete before rendering + // suggestions, so defer the hide until after they have been shown + var input = this; + setTimeout(function() { + if (document.activeElement != input) + that.devbridgeAutocomplete('hide'); + }, 0); } var options_user = { diff --git a/package-lock.json b/package-lock.json index b58bb991aa..dc2742c4b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MPL-2.0", "dependencies": { "blueimp-md5": "2.19.0", - "devbridge-autocomplete": "1.5.0", + "devbridge-autocomplete": "2.0.1", "jquery": "3.7.1", "jquery-contextmenu": "2.10.2", "jquery-datetimepicker": "2.5.21", @@ -644,12 +644,15 @@ } }, "node_modules/devbridge-autocomplete": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/devbridge-autocomplete/-/devbridge-autocomplete-1.5.0.tgz", - "integrity": "sha512-yz6W76PyXS9DtuyYbQtVcpxtkI8pQ02EQwZeNWnqUJXmM5jSmsPFKmpFEBneImcHwYJrMf3+6m86Hj2MRPELGQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/devbridge-autocomplete/-/devbridge-autocomplete-2.0.1.tgz", + "integrity": "sha512-Y4ftxTnLVjfDfGXbhaIh3l6jrYQJuTQDLH1WoPkzd3XMKJo5eYFv83I53wXRxeuO3L2WWAYZA+oa0t5eiSMx0A==", "license": "MIT", + "engines": { + "node": ">=20" + }, "peerDependencies": { - "jquery": ">=1.9" + "jquery": ">=3.0" } }, "node_modules/diff": { @@ -662,9 +665,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.13", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", - "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", + "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" diff --git a/package.json b/package.json index a6b15eb8c1..705f43308a 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "//": "The generated files are not committed; the Docker assets stage builds them from this lockfile. Dependabot proposes upgrades and CI tests them with no regeneration step needed; treat majors (jquery 4.x, mermaid 11.x, devbridge 2.x) as manual, tested reviews. A bump now moves a library's stylesheet along with its script, so review upgrades for visual changes too. bpopup is not published to npm and remains vendored under js/jquery/plugins/bPopup/.", "dependencies": { "blueimp-md5": "2.19.0", - "devbridge-autocomplete": "1.5.0", + "devbridge-autocomplete": "2.0.1", "jquery": "3.7.1", "jquery-contextmenu": "2.10.2", "jquery-datetimepicker": "2.5.21", diff --git a/qa/t/2_test_autocomplete.t b/qa/t/2_test_autocomplete.t new file mode 100644 index 0000000000..0eb4ec39d2 --- /dev/null +++ b/qa/t/2_test_autocomplete.t @@ -0,0 +1,109 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +# The user and product/component autocomplete fields must not show their +# suggestion list if the input has lost focus by the time the lookup returns. +# devbridge-autocomplete 2.x calls onSearchComplete before rendering the +# suggestions, so the "hide if unfocused" check in searchComplete has to run +# after the render (bug 2079453). + +use strict; +use warnings; +use lib qw(lib ../../lib ../../local/lib/perl5); + +use Test::More "no_plan"; + +use QA::Util; + +# Replace Bugzilla.API.get with a stub that holds each lookup open until the +# test calls window.__bzLookup.release(), so the field can be blurred while a +# request is still pending. +use constant STUB_LOOKUP_JS => <<'END'; +window.__bzLookup = { calls: 0, release: null }; +Bugzilla.API.get = (endpoint) => { + window.__bzLookup.calls++; + const body = endpoint.startsWith('user/suggest') + ? { users: [{ name: 'selenium@example.com', real_name: 'Selenium', requests: {}, gravatar: '' }] } + : { products: [{ product: 'Selenium', component: '' }] }; + return new Promise(resolve => { window.__bzLookup.release = () => resolve(body); }); +}; +END + +use constant SUGGESTIONS_VISIBLE_JS => + 'return jQuery(".autocomplete-suggestions:visible").length'; + +my ($sel, $config) = get_selenium(); + +log_in($sel, $config, 'unprivileged'); + +# User autocomplete (js/field.js) on the advanced search page. +go_to_home($sel); +open_advanced_search_page($sel); +check_autocomplete($sel, 'email1', 'sel', 'user autocomplete', 1); + +go_to_home($sel); +open_advanced_search_page($sel); +check_autocomplete($sel, 'email1', 'sel', 'user autocomplete', 0); + +# Product/component search (extensions/ProdCompSearch). +$sel->open_ok('/page.cgi?id=prodcompsearch.html'); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +check_autocomplete($sel, 'pcs', 'sel', + 'product/component search', 1); + +$sel->open_ok('/page.cgi?id=prodcompsearch.html'); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +check_autocomplete($sel, 'pcs', 'sel', + 'product/component search', 0); + +logout($sel); + +# Type into $field and let the stubbed lookup return. With $focused the +# suggestions must be shown; otherwise the field is blurred while the lookup +# is pending and the suggestions must stay hidden. +sub check_autocomplete { + my ($sel, $field, $text, $desc, $focused) = @_; + my $driver = $sel->driver; + + $driver->execute_script(STUB_LOOKUP_JS); + $sel->type_ok($field, $text, "Type into the $desc field"); + + ok(wait_for_js($sel, 'return window.__bzLookup.calls'), + "$desc lookup started"); + + if (!$focused) { + $driver->execute_script("document.getElementById('$field').blur()"); + + # Let the widget's own 200ms blur timeout run first, as it would for a + # slow request in the real app. + select(undef, undef, undef, 0.5); + } + + $driver->execute_script('window.__bzLookup.release?.()'); + + if ($focused) { + ok(wait_for_js($sel, SUGGESTIONS_VISIBLE_JS), + "$desc suggestions are shown while the field has focus"); + } + else { + select(undef, undef, undef, 0.5); + ok(!$driver->execute_script(SUGGESTIONS_VISIBLE_JS), + "$desc suggestions stay hidden after the field loses focus"); + } +} + +# Poll a script returning a true value for up to 5 seconds. +sub wait_for_js { + my ($sel, $script) = @_; + my $result; + for (0 .. 50) { + $result = $sel->driver->execute_script($script); + last if $result; + select(undef, undef, undef, 0.1); + } + return $result; +} From dbe904efea21dd6e766097f289a29bb721cf03db Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Thu, 8 Oct 2026 12:57:20 -0400 Subject: [PATCH 2/3] Update extensions/ProdCompSearch/web/js/prod_comp_search.js Co-authored-by: Kohei Yoshino --- extensions/ProdCompSearch/web/js/prod_comp_search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ProdCompSearch/web/js/prod_comp_search.js b/extensions/ProdCompSearch/web/js/prod_comp_search.js index 94af3e21d8..83ed9d74a2 100644 --- a/extensions/ProdCompSearch/web/js/prod_comp_search.js +++ b/extensions/ProdCompSearch/web/js/prod_comp_search.js @@ -30,7 +30,7 @@ $(function() { // suggestions, so defer the hide until after they have been shown var input = this; setTimeout(function() { - if (document.activeElement != input) + if (document.activeElement !== input) that.devbridgeAutocomplete('hide'); }, 0); if (that.data('error')) { From 7864ef8960ba844eaf70c40cabe0bdc85739c53b Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Thu, 8 Oct 2026 12:57:29 -0400 Subject: [PATCH 3/3] Update js/field.js Co-authored-by: Kohei Yoshino --- js/field.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/field.js b/js/field.js index 8899453233..2b2eabbe7c 100644 --- a/js/field.js +++ b/js/field.js @@ -481,7 +481,7 @@ $(function() { // suggestions, so defer the hide until after they have been shown var input = this; setTimeout(function() { - if (document.activeElement != input) + if (document.activeElement !== input) that.devbridgeAutocomplete('hide'); }, 0); }