diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /browser/components/urlbar/tests/browser | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/urlbar/tests/browser')
263 files changed, 47848 insertions, 0 deletions
diff --git a/browser/components/urlbar/tests/browser/POSTSearchEngine.xml b/browser/components/urlbar/tests/browser/POSTSearchEngine.xml new file mode 100644 index 0000000000..8b387ea9ae --- /dev/null +++ b/browser/components/urlbar/tests/browser/POSTSearchEngine.xml @@ -0,0 +1,6 @@ +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>POST Search</ShortName> + <Url type="text/html" method="POST" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/print_postdata.sjs"> + <Param name="searchterms" value="{searchTerms}"/> + </Url> +</OpenSearchDescription> diff --git a/browser/components/urlbar/tests/browser/add_search_engine_0.xml b/browser/components/urlbar/tests/browser/add_search_engine_0.xml new file mode 100644 index 0000000000..a24348deb7 --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_0.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>add_search_engine_0</ShortName> +<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform"> + <Param name="terms" value="{searchTerms}"/> +</Url> +</SearchPlugin> diff --git a/browser/components/urlbar/tests/browser/add_search_engine_1.xml b/browser/components/urlbar/tests/browser/add_search_engine_1.xml new file mode 100644 index 0000000000..61092247a9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_1.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>add_search_engine_1</ShortName> +<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform"> + <Param name="terms" value="{searchTerms}"/> +</Url> +</SearchPlugin> diff --git a/browser/components/urlbar/tests/browser/add_search_engine_2.xml b/browser/components/urlbar/tests/browser/add_search_engine_2.xml new file mode 100644 index 0000000000..3f5c2f0037 --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_2.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>add_search_engine_2</ShortName> +<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform"> + <Param name="terms" value="{searchTerms}"/> +</Url> +</SearchPlugin> diff --git a/browser/components/urlbar/tests/browser/add_search_engine_3.xml b/browser/components/urlbar/tests/browser/add_search_engine_3.xml new file mode 100644 index 0000000000..bacfffa3e2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_3.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>add_search_engine_3</ShortName> +<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform"> + <Param name="terms" value="{searchTerms}"/> +</Url> +</SearchPlugin> diff --git a/browser/components/urlbar/tests/browser/add_search_engine_invalid.html b/browser/components/urlbar/tests/browser/add_search_engine_invalid.html new file mode 100644 index 0000000000..ea5baa93d5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_invalid.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<link rel="search" + type="application/opensearchdescription+xml" + title="add_search_engine_404" + href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_404.xml"> +</head> +<body></body> +</html> diff --git a/browser/components/urlbar/tests/browser/add_search_engine_many.html b/browser/components/urlbar/tests/browser/add_search_engine_many.html new file mode 100644 index 0000000000..d75bccc890 --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_many.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="/> +<link rel="search" + type="application/opensearchdescription+xml" + title="add_search_engine_0" + href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_0.xml"> +<link rel="search" + type="application/opensearchdescription+xml" + title="add_search_engine_1" + href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_1.xml"> +<link rel="search" + type="application/opensearchdescription+xml" + title="add_search_engine_2" + href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_2.xml"> +<link rel="search" + type="application/opensearchdescription+xml" + title="add_search_engine_3" + href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_3.xml"> +</head> +<body></body> +</html> diff --git a/browser/components/urlbar/tests/browser/add_search_engine_one.html b/browser/components/urlbar/tests/browser/add_search_engine_one.html new file mode 100644 index 0000000000..c11409604f --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_one.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="/> +<link rel="search" + type="application/opensearchdescription+xml" + title="add_search_engine_0" + href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_0.xml"> +</head> +<body></body> +</html> diff --git a/browser/components/urlbar/tests/browser/add_search_engine_same_names.html b/browser/components/urlbar/tests/browser/add_search_engine_same_names.html new file mode 100644 index 0000000000..7112905a75 --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_same_names.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<link rel="search" + type="application/opensearchdescription+xml" + title="add_search_engine_0" + href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_0.xml"> +<link rel="search" + type="application/opensearchdescription+xml" + title="add_search_engine_0" + href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_0.xml"> +</head> +<body></body> +</html> diff --git a/browser/components/urlbar/tests/browser/add_search_engine_two.html b/browser/components/urlbar/tests/browser/add_search_engine_two.html new file mode 100644 index 0000000000..c1d33860a4 --- /dev/null +++ b/browser/components/urlbar/tests/browser/add_search_engine_two.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="/> +<link rel="search" + type="application/opensearchdescription+xml" + title="add_search_engine_0" + href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_0.xml"> + <link rel="search" + type="application/opensearchdescription+xml" + title="add_search_engine_1" + href="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/add_search_engine_2.xml"> +</head> +<body></body> +</html> diff --git a/browser/components/urlbar/tests/browser/authenticate.sjs b/browser/components/urlbar/tests/browser/authenticate.sjs new file mode 100644 index 0000000000..8218a46eb6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/authenticate.sjs @@ -0,0 +1,218 @@ +"use strict"; + +function handleRequest(request, response) { + try { + reallyHandleRequest(request, response); + } catch (e) { + response.setStatusLine("1.0", 200, "AlmostOK"); + response.write("Error handling request: " + e); + } +} + +function reallyHandleRequest(request, response) { + let match; + let requestAuth = true, + requestProxyAuth = true; + + // Allow the caller to drive how authentication is processed via the query. + // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar + // The extra ? allows the user/pass/realm checks to succeed if the name is + // at the beginning of the query string. + let query = "?" + request.queryString; + + let expected_user = "", + expected_pass = "", + realm = "mochitest"; + let proxy_expected_user = "", + proxy_expected_pass = "", + proxy_realm = "mochi-proxy"; + let huge = false, + plugin = false, + anonymous = false; + let authHeaderCount = 1; + // user=xxx + match = /[^_]user=([^&]*)/.exec(query); + if (match) { + expected_user = match[1]; + } + + // pass=xxx + match = /[^_]pass=([^&]*)/.exec(query); + if (match) { + expected_pass = match[1]; + } + + // realm=xxx + match = /[^_]realm=([^&]*)/.exec(query); + if (match) { + realm = match[1]; + } + + // proxy_user=xxx + match = /proxy_user=([^&]*)/.exec(query); + if (match) { + proxy_expected_user = match[1]; + } + + // proxy_pass=xxx + match = /proxy_pass=([^&]*)/.exec(query); + if (match) { + proxy_expected_pass = match[1]; + } + + // proxy_realm=xxx + match = /proxy_realm=([^&]*)/.exec(query); + if (match) { + proxy_realm = match[1]; + } + + // huge=1 + match = /huge=1/.exec(query); + if (match) { + huge = true; + } + + // plugin=1 + match = /plugin=1/.exec(query); + if (match) { + plugin = true; + } + + // multiple=1 + match = /multiple=([^&]*)/.exec(query); + if (match) { + authHeaderCount = match[1] + 0; + } + + // anonymous=1 + match = /anonymous=1/.exec(query); + if (match) { + anonymous = true; + } + + // Look for an authentication header, if any, in the request. + // + // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== + // + // This test only supports Basic auth. The value sent by the client is + // "username:password", obscured with base64 encoding. + + let actual_user = "", + actual_pass = "", + authHeader, + authPresent = false; + if (request.hasHeader("Authorization")) { + authPresent = true; + authHeader = request.getHeader("Authorization"); + match = /Basic (.+)/.exec(authHeader); + if (match.length != 2) { + throw Error("Couldn't parse auth header: " + authHeader); + } + + let userpass = atob(match[1]); + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) { + throw Error("Couldn't decode auth header: " + userpass); + } + actual_user = match[1]; + actual_pass = match[2]; + } + + let proxy_actual_user = "", + proxy_actual_pass = ""; + if (request.hasHeader("Proxy-Authorization")) { + authHeader = request.getHeader("Proxy-Authorization"); + match = /Basic (.+)/.exec(authHeader); + if (match.length != 2) { + throw Error("Couldn't parse auth header: " + authHeader); + } + + let userpass = atob(match[1]); + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) { + throw Error("Couldn't decode auth header: " + userpass); + } + proxy_actual_user = match[1]; + proxy_actual_pass = match[2]; + } + + // Don't request authentication if the credentials we got were what we + // expected. + if (expected_user == actual_user && expected_pass == actual_pass) { + requestAuth = false; + } + if ( + proxy_expected_user == proxy_actual_user && + proxy_expected_pass == proxy_actual_pass + ) { + requestProxyAuth = false; + } + + if (anonymous) { + if (authPresent) { + response.setStatusLine( + "1.0", + 400, + "Unexpected authorization header found" + ); + } else { + response.setStatusLine("1.0", 200, "Authorization header not found"); + } + } else if (requestProxyAuth) { + response.setStatusLine("1.0", 407, "Proxy authentication required"); + for (let i = 0; i < authHeaderCount; ++i) { + response.setHeader( + "Proxy-Authenticate", + 'basic realm="' + proxy_realm + '"', + true + ); + } + } else if (requestAuth) { + response.setStatusLine("1.0", 401, "Authentication required"); + for (let i = 0; i < authHeaderCount; ++i) { + response.setHeader( + "WWW-Authenticate", + 'basic realm="' + realm + '"', + true + ); + } + } else { + response.setStatusLine("1.0", 200, "OK"); + } + + response.setHeader("Content-Type", "application/xhtml+xml", false); + response.write("<html xmlns='http://www.w3.org/1999/xhtml'>"); + response.write( + "<p>Login: <span id='ok'>" + + (requestAuth ? "FAIL" : "PASS") + + "</span></p>\n" + ); + response.write( + "<p>Proxy: <span id='proxy'>" + + (requestProxyAuth ? "FAIL" : "PASS") + + "</span></p>\n" + ); + response.write("<p>Auth: <span id='auth'>" + authHeader + "</span></p>\n"); + response.write("<p>User: <span id='user'>" + actual_user + "</span></p>\n"); + response.write("<p>Pass: <span id='pass'>" + actual_pass + "</span></p>\n"); + + if (huge) { + response.write("<div style='display: none'>"); + for (let i = 0; i < 100000; i++) { + response.write("123456789\n"); + } + response.write("</div>"); + response.write( + "<span id='footnote'>This is a footnote after the huge content fill</span>" + ); + } + + if (plugin) { + response.write( + "<embed id='embedtest' style='width: 400px; height: 100px;' " + + "type='application/x-test'></embed>\n" + ); + } + + response.write("</html>"); +} diff --git a/browser/components/urlbar/tests/browser/browser.toml b/browser/components/urlbar/tests/browser/browser.toml new file mode 100644 index 0000000000..a77a831fab --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser.toml @@ -0,0 +1,692 @@ +[DEFAULT] +support-files = [ + "dummy_page.html", + "head.js", + "head-common.js", +] + +prefs = [ + "browser.bookmarks.testing.skipDefaultBookmarksImport=true", + "browser.urlbar.trending.featureGate=false", + "extensions.screenshots.disabled=false", + "screenshots.browser.component.enabled=true", +] + +["browser_UrlbarInput_formatValue.js"] + +["browser_UrlbarInput_formatValue_detachedTab.js"] +skip-if = [ + "apple_catalina", # Bug 1756585 + "os == 'win'", # Bug 1756585 +] + +["browser_UrlbarInput_formatValue_strikeout.js"] +support-files = ["mixed_active.html"] + +["browser_UrlbarInput_hiddenFocus.js"] + +["browser_UrlbarInput_overflow.js"] + +["browser_UrlbarInput_overflow_resize.js"] + +["browser_UrlbarInput_privateFeature.js"] + +["browser_UrlbarInput_searchTerms.js"] + +["browser_UrlbarInput_searchTerms_backgroundTabs.js"] + +["browser_UrlbarInput_searchTerms_modifiedUrl.js"] + +["browser_UrlbarInput_searchTerms_moveTab.js"] + +["browser_UrlbarInput_searchTerms_popup.js"] + +["browser_UrlbarInput_searchTerms_revert.js"] + +["browser_UrlbarInput_searchTerms_searchBar.js"] + +["browser_UrlbarInput_searchTerms_searchMode.js"] + +["browser_UrlbarInput_searchTerms_strings.js"] + +["browser_UrlbarInput_searchTerms_stringsUnsafe.js"] + +["browser_UrlbarInput_searchTerms_switch_tab.js"] + +["browser_UrlbarInput_searchTerms_telemetry.js"] + +["browser_UrlbarInput_setURI.js"] +https_first_disabled = true +skip-if = ["apple_catalina && debug"] # Bug 1773790 + +["browser_UrlbarInput_tooltip.js"] + +["browser_UrlbarInput_trimURLs.js"] +https_first_disabled = true + +["browser_aboutHomeLoading.js"] +skip-if = [ + "tsan", # Intermittently times out, see 1622698 (frequent on TSan). + "os == 'linux' && bits == 64 && !debug", # Bug 1622698 +] + +["browser_acknowledgeFeedbackAndDismissal.js"] + +["browser_action_searchengine.js"] +skip-if = [ + "os == 'linux' && asan", # Bug 1834810 + "os == 'linux' && debug", # Bug 1834810 + "os == 'win' && asan", # Bug 1834810 + "os == 'win' && debug", # Bug 1834810 +] + +["browser_action_searchengine_alias.js"] + +["browser_add_search_engine.js"] +support-files = [ + "add_search_engine_0.xml", + "add_search_engine_1.xml", + "add_search_engine_2.xml", + "add_search_engine_3.xml", + "add_search_engine_invalid.html", + "add_search_engine_one.html", + "add_search_engine_many.html", + "add_search_engine_same_names.html", + "add_search_engine_two.html", +] + +["browser_autoFill_backspaced.js"] + +["browser_autoFill_canonize.js"] +https_first_disabled = true + +["browser_autoFill_caretNotAtEnd.js"] + +["browser_autoFill_clear_properly_on_accent_char.js"] + +["browser_autoFill_firstResult.js"] + +["browser_autoFill_paste.js"] + +["browser_autoFill_placeholder.js"] + +["browser_autoFill_preserve.js"] + +["browser_autoFill_trimURLs.js"] + +["browser_autoFill_typed.js"] + +["browser_autoFill_undo.js"] + +["browser_autoOpen.js"] + +["browser_autocomplete_a11y_label.js"] +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", +] +skip-if = ["a11y_checks"] # Test times out (bug 1854660) + +["browser_autocomplete_autoselect.js"] + +["browser_autocomplete_cursor.js"] + +["browser_autocomplete_edit_completed.js"] + +["browser_autocomplete_enter_race.js"] +https_first_disabled = true + +["browser_autocomplete_no_title.js"] + +["browser_autocomplete_readline_navigation.js"] +skip-if = ["os != 'mac'"] # Mac only feature + +["browser_autocomplete_tag_star_visibility.js"] + +["browser_bestMatch.js"] + +["browser_blanking.js"] +support-files = ["file_blank_but_not_blank.html"] + +["browser_blobIcons.js"] + +["browser_bufferer_onQueryResults.js"] + +["browser_calculator.js"] + +["browser_canonizeURL.js"] +https_first_disabled = true +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", +] + +["browser_caret_position.js"] + +["browser_click_row_border.js"] + +["browser_clipboard.js"] + +["browser_closePanelOnClick.js"] + +["browser_content_opener.js"] + +["browser_contextualsearch.js"] + +["browser_copy_and_paste_first_result.js"] + +["browser_copy_during_load.js"] +support-files = ["slow-page.sjs"] + +["browser_copying.js"] +https_first_disabled = true +support-files = [ + "authenticate.sjs", + "file_copying_home.html", + "wait-a-bit.sjs", +] + +["browser_customizeMode.js"] + +["browser_cutting.js"] + +["browser_decode.js"] + +["browser_delete.js"] + +["browser_deleteAllText.js"] + +["browser_display_selectedAction_Extensions.js"] + +["browser_dns_first_for_single_words.js"] +skip-if = ["verify && os == 'linux'"] # Bug 1581635 + +["browser_downArrowKeySearch.js"] +https_first_disabled = true + +["browser_dragdropURL.js"] + +["browser_dynamicResults.js"] +https_first_disabled = true +support-files = [ + "dynamicResult0.css", + "dynamicResult1.css", +] + +["browser_editAndEnterWithSlowQuery.js"] + +["browser_edit_invalid_url.js"] + +["browser_engagement.js"] + +["browser_enter.js"] + +["browser_enterAfterMouseOver.js"] + +["browser_focusedCmdK.js"] + +["browser_groupLabels.js"] +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", +] + +["browser_handleCommand_fallback.js"] + +["browser_hashChangeProxyState.js"] + +["browser_heuristicNotAddedFirst.js"] + +["browser_hideHeuristic.js"] + +["browser_ime_composition.js"] + +["browser_inputHistory.js"] +https_first_disabled = true +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", +] + +["browser_inputHistory_autofill.js"] + +["browser_inputHistory_emptystring.js"] + +["browser_keepStateAcrossTabSwitches.js"] +https_first_disabled = true + +["browser_keyword.js"] +support-files = [ + "print_postdata.sjs", + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", +] + +["browser_keywordBookmarklets.js"] + +["browser_keywordSearch.js"] +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", +] + +["browser_keywordSearch_postData.js"] +support-files = [ + "POSTSearchEngine.xml", + "print_postdata.sjs", +] + +["browser_keyword_override.js"] + +["browser_keyword_select_and_type.js"] + +["browser_loadRace.js"] + +["browser_locationBarCommand.js"] +https_first_disabled = true + +["browser_locationBarExternalLoad.js"] + +["browser_locationchange_urlbar_edit_dos.js"] +support-files = ["file_urlbar_edit_dos.html"] + +["browser_middleClick.js"] +fail-if = ["a11y_checks"] # Bug 1854660 clicked element may not be focusable and/or labeled + +["browser_move_tab_to_new_window.js"] + +["browser_new_tab_urlbar_reset.js"] + +["browser_observers_for_strip_on_share.js"] + +["browser_oneOffs.js"] +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", +] + +["browser_oneOffs_contextMenu.js"] +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", +] + +["browser_oneOffs_heuristicRestyle.js"] +skip-if = [ + "os == 'linux' && bits == 64 && !debug", # Bug 1775811 + "a11y_checks", # Bugs 1858041 and 1854661 to investigate intermittent a11y_checks results +] + +["browser_oneOffs_keyModifiers.js"] +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", +] + +["browser_oneOffs_searchSuggestions.js"] +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", + "searchSuggestionEngine2.xml", +] + +["browser_oneOffs_settings.js"] + +["browser_pasteAndGo.js"] +https_first_disabled = true + +["browser_paste_multi_lines.js"] + +["browser_paste_then_focus.js"] + +["browser_paste_then_switch_tab.js"] + +["browser_percent_encoded.js"] + +["browser_placeholder.js"] +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine2.xml", + "searchSuggestionEngine.sjs", +] + +["browser_populateAfterPushState.js"] + +["browser_primary_selection_safe_on_new_tab.js"] + +["browser_privateBrowsingWindowChange.js"] + +["browser_queryContextCache.js"] + +["browser_quickactions.js"] + +["browser_quickactions_devtools.js"] + +["browser_quickactions_screenshot.js"] + +["browser_quickactions_tab_refocus.js"] + +["browser_raceWithTabs.js"] + +["browser_recentsearches.js"] +support-files = ["search-engines"] + +["browser_redirect_error.js"] +support-files = ["redirect_error.sjs"] + +["browser_remoteness_switch.js"] +https_first_disabled = true + +["browser_remotetab.js"] + +["browser_removeUnsafeProtocolsFromURLBarPaste.js"] + +["browser_remove_match.js"] + +["browser_restoreEmptyInput.js"] + +["browser_resultSpan.js"] + +["browser_result_menu.js"] + +["browser_result_menu_general.js"] + +["browser_result_onSelection.js"] + +["browser_results_format_displayValue.js"] + +["browser_retainedResultsOnFocus.js"] + +["browser_revert.js"] + +["browser_search_continuation.js"] +support-files = ["search-engines", "../../../search/test/browser/trendingSuggestionEngine.sjs"] + +["browser_searchFunction.js"] + +["browser_searchHistoryLimit.js"] + +["browser_searchMode_alias_replacement.js"] +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", +] + +["browser_searchMode_autofill.js"] + +["browser_searchMode_clickLink.js"] +https_first_disabled = true +support-files = ["dummy_page.html"] + +["browser_searchMode_engineRemoval.js"] + +["browser_searchMode_excludeResults.js"] + +["browser_searchMode_heuristic.js"] +https_first_disabled = true + +["browser_searchMode_indicator.js"] +https_first_disabled = true +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", +] + +["browser_searchMode_indicator_clickthrough.js"] + +["browser_searchMode_localOneOffs_actionText.js"] +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", +] + +["browser_searchMode_newWindow.js"] + +["browser_searchMode_no_results.js"] + +["browser_searchMode_oneOffButton.js"] + +["browser_searchMode_pickResult.js"] +https_first_disabled = true + +["browser_searchMode_preview.js"] + +["browser_searchMode_sessionStore.js"] +https_first_disabled = true +skip-if = [ + "os == 'mac' && debug", # Bug 1671045, Bug 1849098 + "os == 'linux' && (debug || tsan || asan)", # Bug 1849098 + "os == 'win' && debug", # Bug 1849098 +] + +["browser_searchMode_setURI.js"] +https_first_disabled = true + +["browser_searchMode_suggestions.js"] +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", + "searchSuggestionEngineMany.xml", +] + +["browser_searchMode_switchTabs.js"] + +["browser_searchSettings.js"] + +["browser_searchSingleWordNotification.js"] +https_first_disabled = true +skip-if = ["os == 'linux' && bits == 64"] # Bug 1773830 + +["browser_searchSuggestions.js"] +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", +] + +["browser_searchTelemetry.js"] +support-files = [ + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", +] + +["browser_search_bookmarks_from_bookmarks_menu.js"] + +["browser_search_history_from_history_panel.js"] + +["browser_selectStaleResults.js"] +support-files = [ + "searchSuggestionEngineSlow.xml", + "searchSuggestionEngine.sjs", +] + +["browser_selectionKeyNavigation.js"] + +["browser_separatePrivateDefault.js"] +support-files = [ + "POSTSearchEngine.xml", + "print_postdata.sjs", + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", + "searchSuggestionEngine2.xml", +] + +["browser_separatePrivateDefault_differentEngine.js"] +support-files = [ + "POSTSearchEngine.xml", + "print_postdata.sjs", + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", + "searchSuggestionEngine2.xml", +] + +["browser_shortcuts_add_search_engine.js"] +support-files = [ + "add_search_engine_many.html", + "add_search_engine_two.html", + "add_search_engine_0.xml", + "add_search_engine_1.xml", +] + +["browser_slow_heuristic.js"] + +["browser_speculative_connect.js"] +support-files = [ + "searchSuggestionEngine2.xml", + "searchSuggestionEngine.sjs", +] + +["browser_speculative_connect_not_with_client_cert.js"] + +["browser_stop.js"] + +["browser_stopSearchOnSelection.js"] +support-files = [ + "searchSuggestionEngineSlow.xml", + "searchSuggestionEngine.sjs", +] + +["browser_stop_pending.js"] +https_first_disabled = true +support-files = ["slow-page.sjs"] + +["browser_strip_on_share.js"] + +["browser_strip_on_share_telemetry.js"] + +["browser_suggestedIndex.js"] + +["browser_suppressFocusBorder.js"] + +["browser_switchTab_closesUrlbarPopup.js"] + +["browser_switchTab_currentTab.js"] + +["browser_switchTab_decodeuri.js"] + +["browser_switchTab_inputHistory.js"] + +["browser_switchTab_override.js"] + +["browser_switchToTabHavingURI_aOpenParams.js"] + +["browser_switchToTab_chiclet.js"] + +["browser_switchToTab_closed_tab.js"] + +["browser_switchToTab_closes_newtab.js"] + +["browser_switchToTab_fullUrl_repeatedKeydown.js"] + +["browser_tabKeyBehavior.js"] + +["browser_tabMatchesInAwesomebar.js"] +support-files = ["moz.png"] + +["browser_tabMatchesInAwesomebar_perwindowpb.js"] + +["browser_tabToSearch.js"] + +["browser_textruns.js"] + +["browser_tokenAlias.js"] + +["browser_top_sites.js"] +https_first_disabled = true + +["browser_top_sites_private.js"] +https_first_disabled = true + +["browser_typed_value.js"] + +["browser_unitConversion.js"] + +["browser_updateForDomainCompletion.js"] +https_first_disabled = true + +["browser_url_formatted_correctly_on_load.js"] + +["browser_urlbar_annotation.js"] +support-files = ["redirect_to.sjs"] + +["browser_urlbar_selection.js"] +skip-if = ["(os == 'mac')"] # bug 1570474 + +["browser_urlbar_telemetry.js"] +tags = "search-telemetry" +support-files = [ + "urlbarTelemetrySearchSuggestions.sjs", + "urlbarTelemetrySearchSuggestions.xml", +] + +["browser_urlbar_telemetry_autofill.js"] +tags = "search-telemetry" + +["browser_urlbar_telemetry_dynamic.js"] +tags = "search-telemetry" +support-files = ["urlbarTelemetryUrlbarDynamic.css"] + +["browser_urlbar_telemetry_extension.js"] +tags = "search-telemetry" + +["browser_urlbar_telemetry_handoff.js"] +tags = "search-telemetry" + +["browser_urlbar_telemetry_persisted.js"] +tags = "search-telemetry" + +["browser_urlbar_telemetry_places.js"] +https_first_disabled = true +tags = "search-telemetry" + +["browser_urlbar_telemetry_quickactions.js"] +tags = "search-telemetry" + +["browser_urlbar_telemetry_remotetab.js"] +tags = "search-telemetry" + +["browser_urlbar_telemetry_searchmode.js"] +tags = "search-telemetry" +support-files = [ + "urlbarTelemetrySearchSuggestions.sjs", + "urlbarTelemetrySearchSuggestions.xml", +] + +["browser_urlbar_telemetry_tabtosearch.js"] +tags = "search-telemetry" + +["browser_urlbar_telemetry_tip.js"] +tags = "search-telemetry" + +["browser_urlbar_telemetry_topsite.js"] +tags = "search-telemetry" + +["browser_urlbar_telemetry_zeroPrefix.js"] +tags = "search-telemetry" + +["browser_userTypedValue.js"] +support-files = ["file_userTypedValue.html"] + +["browser_valueOnTabSwitch.js"] + +["browser_view_emptyResultSet.js"] + +["browser_view_removedSelectedElement.js"] + +["browser_view_resultDisplay.js"] + +["browser_view_resultTypes_display.js"] +support-files = [ + "print_postdata.sjs", + "searchSuggestionEngine.xml", + "searchSuggestionEngine.sjs", +] + +["browser_view_selectionByMouse.js"] +skip-if = [ + "os == 'linux' && asan", # Bug 1789051 +] + +["browser_waitForLoadStartOrTimeout.js"] +https_first_disabled = true + +["browser_whereToOpen.js"] diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue.js new file mode 100644 index 0000000000..307767fa96 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue.js @@ -0,0 +1,187 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +// Checks that the url formatter properly recognizes the host and de-emphasizes +// the rest of the url. + +/** + * Tests a given url. + * The de-emphasized parts must be wrapped in "<" and ">" chars. + * + * @param {string} urlFormatString The URL to test. + * @param {string} [clobberedURLString] Normally the URL is de-emphasized + * in-place, thus it's enough to pass aExpected. Though, in some cases + * the formatter may decide to replace the URL with a fixed one, because + * it can't properly guess a host. In that case clobberedURLString is + * the expected de-emphasized value. + */ +async function testVal(urlFormatString, clobberedURLString = null) { + let str = urlFormatString.replace(/[<>]/g, ""); + + info("Setting the value property directly"); + gURLBar.value = str; + gBrowser.selectedBrowser.focus(); + UrlbarTestUtils.checkFormatting(window, urlFormatString, { + clobberedURLString, + }); + + info("Simulating user input"); + await UrlbarTestUtils.inputIntoURLBar(window, str); + Assert.equal( + gURLBar.editor.rootElement.textContent, + str, + "URL is not highlighted" + ); + gBrowser.selectedBrowser.focus(); + UrlbarTestUtils.checkFormatting(window, urlFormatString, { + clobberedURLString, + additionalMsg: "with input simulation", + }); +} + +add_task(async function () { + const PREF_FORMATTING = "browser.urlbar.formatting.enabled"; + const PREF_TRIM_HTTPS = "browser.urlbar.trimHttps"; + + registerCleanupFunction(function () { + Services.prefs.clearUserPref(PREF_FORMATTING); + Services.prefs.clearUserPref(PREF_TRIM_HTTPS); + gURLBar.setURI(); + }); + + Services.prefs.setBoolPref(PREF_TRIM_HTTPS, false); + + gBrowser.selectedBrowser.focus(); + + await testVal("<https://>mozilla.org"); + await testVal("<https://>mözilla.org"); + await testVal("<https://>mozilla.imaginatory"); + + await testVal("<https://www.>mozilla.org"); + await testVal("<https://sub.>mozilla.org"); + await testVal("<https://sub1.sub2.sub3.>mozilla.org"); + await testVal("<www.>mozilla.org"); + await testVal("<sub.>mozilla.org"); + await testVal("<sub1.sub2.sub3.>mozilla.org"); + await testVal("<mozilla.com.>mozilla.com"); + await testVal("<https://mozilla.com:mozilla.com@>mozilla.com"); + await testVal("<mozilla.com:mozilla.com@>mozilla.com"); + + await testVal("<ftp.>mozilla.org"); + await testVal("<ftp://ftp.>mozilla.org"); + + await testVal("<https://sub.>mozilla.org"); + await testVal("<https://sub1.sub2.sub3.>mozilla.org"); + await testVal("<https://user:pass@sub1.sub2.sub3.>mozilla.org"); + await testVal("<https://user:pass@>mozilla.org"); + await testVal("<user:pass@sub1.sub2.sub3.>mozilla.org"); + await testVal("<user:pass@>mozilla.org"); + + await testVal("<https://>mozilla.org< >"); + await testVal("mozilla.org< >"); + // RTL characters in domain change order of domain and suffix. Domain should + // be highlighted correctly. + await testVal("<http://>اختبار.اختبار</www.mozilla.org/index.html>"); + + await testVal("<https://>mozilla.org</file.ext>"); + await testVal("<https://>mozilla.org</sub/file.ext>"); + await testVal("<https://>mozilla.org</sub/file.ext?foo>"); + await testVal("<https://>mozilla.org</sub/file.ext?foo&bar>"); + await testVal("<https://>mozilla.org</sub/file.ext?foo&bar#top>"); + await testVal("<https://>mozilla.org</sub/file.ext?foo&bar#top>"); + await testVal("foo.bar<?q=test>"); + await testVal("foo.bar<#mozilla.org>"); + await testVal("foo.bar<?somewhere.mozilla.org>"); + await testVal("foo.bar<?@mozilla.org>"); + await testVal("foo.bar<#x@mozilla.org>"); + await testVal("foo.bar<#@x@mozilla.org>"); + await testVal("foo.bar<?x@mozilla.org>"); + await testVal("foo.bar<?@x@mozilla.org>"); + await testVal("<foo.bar@x@>mozilla.org"); + await testVal("<foo.bar@:baz@>mozilla.org"); + await testVal("<foo.bar:@baz@>mozilla.org"); + await testVal("<foo.bar@:ba:z@>mozilla.org"); + await testVal("<foo.:bar:@baz@>mozilla.org"); + await testVal( + "foopy:\\blah@somewhere.com//whatever/", + "foopy</blah@somewhere.com//whatever/>" + ); + + await testVal("<https://sub.>mozilla.org<:666/file.ext>"); + await testVal("<sub.>mozilla.org<:666/file.ext>"); + await testVal("localhost<:666/file.ext>"); + + let IPs = [ + "192.168.1.1", + "[::]", + "[::1]", + "[1::]", + "[::]", + "[::1]", + "[1::]", + "[1:2:3:4:5:6:7::]", + "[::1:2:3:4:5:6:7]", + "[1:2:a:B:c:D:e:F]", + "[1::8]", + "[1:2::8]", + "[fe80::222:19ff:fe11:8c76]", + "[0000:0123:4567:89AB:CDEF:abcd:ef00:0000]", + "[::192.168.1.1]", + "[1::0.0.0.0]", + "[1:2::255.255.255.255]", + "[1:2:3::255.255.255.255]", + "[1:2:3:4::255.255.255.255]", + "[1:2:3:4:5::255.255.255.255]", + "[1:2:3:4:5:6:255.255.255.255]", + ]; + for (let IP of IPs) { + await testVal(IP); + await testVal(IP + "</file.ext>"); + await testVal(IP + "<:666/file.ext>"); + await testVal("<https://>" + IP); + await testVal(`<https://>${IP}</file.ext>`); + await testVal(`<https://user:pass@>${IP}<:666/file.ext>`); + await testVal(`<user:pass@>${IP}<:666/file.ext>`); + await testVal(`user:\\pass@${IP}/`, `user</pass@${IP}/>`); + } + + await testVal("mailto:admin@mozilla.org"); + await testVal("gopher://mozilla.org/"); + await testVal("about:config"); + await testVal("jar:http://mozilla.org/example.jar!/"); + await testVal("view-source:http://mozilla.org/"); + await testVal("foo9://mozilla.org/"); + await testVal("foo+://mozilla.org/"); + await testVal("foo.://mozilla.org/"); + await testVal("foo-://mozilla.org/"); + + // Disable formatting. + Services.prefs.setBoolPref(PREF_FORMATTING, false); + + await testVal("https://mozilla.org"); +}); + +add_task(async function test_url_formatting_after_visiting_bookmarks() { + SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.trimURLs", true], + ["browser.urlbar.trimHttps", true], + ["browser.urlbar.formatting.enabled", true], + ], + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "https://something.example.com/test", + }); + await search({ + searchString: "something", + valueBefore: "something", + valueAfter: "something.example.com/", + placeholderAfter: "something.example.com/", + }); + EventUtils.sendKey("DOWN"); + EventUtils.sendKey("RETURN"); + await BrowserTestUtils.browserLoaded(gBrowser, false, null, true); + + UrlbarTestUtils.checkFormatting(window, "<something.>example.com</test>"); + SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue_detachedTab.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue_detachedTab.js new file mode 100644 index 0000000000..fcb1357095 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue_detachedTab.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +async function detachTab(tab) { + let winPromise = BrowserTestUtils.waitForNewWindow(); + info("Detaching tab"); + let win = gBrowser.replaceTabWithWindow(tab, {}); + info("Waiting for new window"); + await winPromise; + + // Wait an extra tick for good measure since the code itself also waits for + // `delayedStartupPromise`. + info("Waiting for delayed startup in new window"); + await win.delayedStartupPromise; + info("Waiting for tick"); + await TestUtils.waitForTick(); + + return win; +} + +add_task(async function detach() { + // After detaching a tab into a new window, the input value in the new window + // should be formatted. + + // Sometimes the value isn't formatted on Mac when running in verify chaos + // mode. The usual, proper front-end code path is hit, and the path that + // removes formatting is not hit, so it seems like some kind of race in the + // editor or selection code in Gecko. Since this has only been observed on Mac + // in chaos mode and doesn't seem to be a problem in urlbar code, skip the + // test in that case. + if (AppConstants.platform == "macosx" && Services.env.get("MOZ_CHAOSMODE")) { + Assert.ok(true, "Skipping test in chaos mode on Mac"); + return; + } + + let originalTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "https://example.com/original-tab", + }); + + let tabToDetach = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "https://example.com/detach", + }); + + let win = await detachTab(tabToDetach); + + UrlbarTestUtils.checkFormatting( + win, + UrlbarTestUtils.trimURL("<https://>example.com</detach>") + ); + await BrowserTestUtils.closeWindow(win); + + UrlbarTestUtils.checkFormatting( + window, + UrlbarTestUtils.trimURL("<https://>example.com</original-tab>") + ); + gBrowser.removeTab(originalTab); +}); + +add_task(async function detach_emptyTab() { + // After detaching an empty tab into a new window, the input value in the + // original window should be formatted. + + let originalTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "https://example.com/original-tab", + }); + + let emptyTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "about:blank", + }); + gURLBar.focus(); + ok(gURLBar.focused, "urlbar is focused"); + is(gURLBar.value, "", "urlbar is empty"); + + let focusPromise = BrowserTestUtils.waitForEvent( + originalTab.linkedBrowser, + "focus" + ); + let win = await detachTab(emptyTab); + await BrowserTestUtils.closeWindow(win); + await focusPromise; + + ok(!gURLBar.focused, "urlbar is not focused"); + UrlbarTestUtils.checkFormatting( + window, + UrlbarTestUtils.trimURL("<https://>example.com</original-tab>") + ); + gBrowser.removeTab(originalTab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue_strikeout.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue_strikeout.js new file mode 100644 index 0000000000..2dd236525e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue_strikeout.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "mixed_active.html"; + +/** + * Tests a given url. + * The de-emphasized parts must be wrapped in "<" and ">" chars. + * + * @param {string} urlFormatString The URL to test. + * @param {string} [clobberedURLString] Normally the URL is de-emphasized + * in-place, thus it's enough to pass aExpected. Though, in some cases + * the formatter may decide to replace the URL with a fixed one, because + * it can't properly guess a host. In that case clobberedURLString is + * the expected de-emphasized value. + */ +async function testVal(urlFormatString, clobberedURLString = null) { + let str = urlFormatString.replace(/[<>]/g, ""); + + info("Setting the value property directly"); + gURLBar.value = str; + gBrowser.selectedBrowser.focus(); + UrlbarTestUtils.checkFormatting(window, urlFormatString, { + clobberedURLString, + selectionType: Ci.nsISelectionController.SELECTION_URLSTRIKEOUT, + }); +} + +add_task(async function test_strikeout_on_no_https_trimming() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.trimHttps", false], + ["security.mixed_content.block_active_content", false], + ], + }); + await BrowserTestUtils.withNewTab(TEST_URL, function () { + testVal("<https>://example.com/mixed_active.html"); + }); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_no_strikeout_on_https_trimming() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.trimHttps", true], + ["security.mixed_content.block_active_content", false], + ], + }); + await BrowserTestUtils.withNewTab(TEST_URL, function () { + testVal( + "https://example.com/mixed_active.html", + "example.com/mixed_active.html" + ); + }); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_hiddenFocus.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_hiddenFocus.js new file mode 100644 index 0000000000..08e5ae97d3 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_hiddenFocus.js @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(tab); + gURLBar.setURI(); + }); + + gURLBar.blur(); + ok(!gURLBar.focused, "url bar is not focused"); + ok(!gURLBar.hasAttribute("focused"), "url bar is not visibly focused"); + gURLBar.setHiddenFocus(); + ok(gURLBar.focused, "url bar is focused"); + ok(!gURLBar.hasAttribute("focused"), "url bar is not visibly focused"); + gURLBar.removeHiddenFocus(); + ok(gURLBar.focused, "url bar is focused"); + ok(gURLBar.hasAttribute("focused"), "url bar is visibly focused"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js new file mode 100644 index 0000000000..f191cae321 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js @@ -0,0 +1,159 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function testVal(aExpected, overflowSide = "") { + info(`Testing ${aExpected}`); + try { + gURLBar.setURI(makeURI(aExpected)); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_MALFORMED_URI) { + throw ex; + } + // For values without a protocol fallback to setting the raw value. + gURLBar.value = aExpected; + } + + Assert.equal( + gURLBar.selectionStart, + gURLBar.selectionEnd, + "Selection sanity check" + ); + + gURLBar.focus(); + Assert.equal( + document.activeElement, + gURLBar.inputField, + "URL Bar should be focused" + ); + Assert.equal( + gURLBar.valueFormatter.scheme.value, + "", + "Check the scheme value" + ); + Assert.equal( + getComputedStyle(gURLBar.valueFormatter.scheme).visibility, + "hidden", + "Check the scheme box visibility" + ); + + gURLBar.blur(); + await window.promiseDocumentFlushed(() => {}); + // The attribute doesn't always change, so we can't use waitForAttribute. + await TestUtils.waitForCondition( + () => gURLBar.getAttribute("textoverflow") === overflowSide + ); + + let scheme = aExpected.match(/^([a-z]+:\/{0,2})/)?.[1] || ""; + // We strip http, so we should not show the scheme for it. + if ( + scheme == "http://" && + Services.prefs.getBoolPref("browser.urlbar.trimURLs", true) + ) { + scheme = ""; + } + + Assert.equal( + gURLBar.valueFormatter.scheme.value, + scheme, + "Check the scheme value" + ); + let isOverflowed = + gURLBar.inputField.scrollWidth > gURLBar.inputField.clientWidth; + Assert.equal(isOverflowed, !!overflowSide, "Check The input field overflow"); + Assert.equal( + gURLBar.getAttribute("textoverflow"), + overflowSide, + "Check the textoverflow attribute" + ); + if (overflowSide) { + let side = gURLBar.getAttribute("domaindir") == "ltr" ? "right" : "left"; + Assert.equal(side, overflowSide, "Check the overflow side"); + Assert.equal( + getComputedStyle(gURLBar.valueFormatter.scheme).visibility, + scheme && isOverflowed && overflowSide == "left" ? "visible" : "hidden", + "Check the scheme box visibility" + ); + + info("Focus, change scroll position and blur, to ensure proper restore"); + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_End"); + gURLBar.blur(); + await window.promiseDocumentFlushed(() => {}); + // The attribute doesn't always change, so we can't use waitForAttribute. + await TestUtils.waitForCondition( + () => gURLBar.getAttribute("textoverflow") === overflowSide + ); + + Assert.equal(side, overflowSide, "Check the overflow side"); + Assert.equal( + getComputedStyle(gURLBar.valueFormatter.scheme).visibility, + scheme && isOverflowed && overflowSide == "left" ? "visible" : "hidden", + "Check the scheme box visibility" + ); + } +} + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.trimHttps", false]], + }); + // We use a new tab for the test to be sure all the tab switching and loading + // is complete before starting, otherwise onLocationChange for this tab could + // override the value we set with an empty value. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + registerCleanupFunction(function () { + gURLBar.setURI(); + BrowserTestUtils.removeTab(tab); + }); + + let lotsOfSpaces = "%20".repeat(200); + + // اسماء.شبكة + let rtlDomain = + "\u0627\u0633\u0645\u0627\u0621\u002e\u0634\u0628\u0643\u0629"; + let rtlChar = "\u0627"; + + // Mix the direction of the tests to cover more cases, and to ensure the + // textoverflow attribute changes every time, because tewtVal waits for that. + await testVal(`https://mozilla.org/${lotsOfSpaces}/test/`, "right"); + await testVal(`https://mozilla.org/`); + await testVal(`https://${rtlDomain}/${lotsOfSpaces}/test/`, "left"); + await testVal(`https://mozilla.org:8888/${lotsOfSpaces}/test/`, "right"); + await testVal(`https://${rtlDomain}:8888/${lotsOfSpaces}/test/`, "left"); + + await testVal(`ftp://mozilla.org/${lotsOfSpaces}/test/`, "right"); + await testVal(`ftp://${rtlDomain}/${lotsOfSpaces}/test/`, "left"); + await testVal(`ftp://mozilla.org/`); + + await testVal(`http://${rtlDomain}/${lotsOfSpaces}/test/`, "left"); + await testVal(`http://mozilla.org/`); + await testVal(`http://mozilla.org/${lotsOfSpaces}/test/`, "right"); + await testVal(`http://${rtlDomain}:8888/${lotsOfSpaces}/test/`, "left"); + await testVal(`http://[::1]/${rtlChar}/${lotsOfSpaces}/test/`, "right"); + + info("Test with formatting disabled"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.formatting.enabled", false], + ["browser.urlbar.trimURLs", false], + ], + }); + + await testVal(`https://mozilla.org/`); + await testVal(`https://${rtlDomain}/${lotsOfSpaces}/test/`, "left"); + await testVal(`https://mozilla.org/${lotsOfSpaces}/test/`, "right"); + + info("Test with trimURLs disabled"); + await testVal(`http://${rtlDomain}/${lotsOfSpaces}/test/`, "left"); + + await SpecialPowers.popPrefEnv(); + + info("Tests without protocol"); + await testVal(`mozilla.org/${lotsOfSpaces}/test/`, "right"); + await testVal(`mozilla.org/`); + await testVal(`${rtlDomain}/${lotsOfSpaces}/test/`, "left"); + await testVal(`mozilla.org:8888/${lotsOfSpaces}/test/`, "right"); + await testVal(`${rtlDomain}:8888/${lotsOfSpaces}/test/`, "left"); + await testVal(`[::1]/${rtlChar}/${lotsOfSpaces}/test/`, "right"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow_resize.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow_resize.js new file mode 100644 index 0000000000..879911d703 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow_resize.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +async function testVal(win, url) { + info(`Testing ${url}`); + win.gURLBar.setURI(makeURI(url)); + + let urlbar = win.gURLBar; + urlbar.blur(); + + for (let width of [1000, 800]) { + win.resizeTo(width, 500); + await win.promiseDocumentFlushed(() => {}); + Assert.greater( + urlbar.inputField.scrollWidth, + urlbar.inputField.clientWidth, + "Check The input field overflows" + ); + // Resize is handled on a timer, so we must wait for it. + await TestUtils.waitForCondition( + () => urlbar.inputField.scrollLeft == urlbar.inputField.scrollLeftMax, + "The urlbar input field is completely scrolled to the end" + ); + await TestUtils.waitForCondition( + () => urlbar.getAttribute("textoverflow") == "left", + "Wait for the textoverflow attribute" + ); + } +} + +add_task(async function () { + // We use a new tab for the test to be sure all the tab switching and loading + // is complete before starting, otherwise onLocationChange for this tab could + // override the value we set with an empty value. + let win = await BrowserTestUtils.openNewBrowserWindow(); + registerCleanupFunction(() => BrowserTestUtils.closeWindow(win)); + + let lotsOfSpaces = "%20".repeat(200); + + // اسماء.شبكة + let rtlDomain = + "\u0627\u0633\u0645\u0627\u0621\u002e\u0634\u0628\u0643\u0629"; + + // Mix the direction of the tests to cover more cases, and to ensure the + // textoverflow attribute changes every time, because tewtVal waits for that. + await testVal(win, `https://${rtlDomain}/${lotsOfSpaces}/test/`); + + info("Test with formatting and trimurl disabled"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.formatting.enabled", false], + ["browser.urlbar.trimURLs", false], + ], + }); + + await testVal(win, `https://${rtlDomain}/${lotsOfSpaces}/test/`); + await testVal(win, `http://${rtlDomain}/${lotsOfSpaces}/test/`); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_privateFeature.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_privateFeature.js new file mode 100644 index 0000000000..fb81e9f536 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_privateFeature.js @@ -0,0 +1,74 @@ +/* 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/. */ + +// Tests that _loadURL correctly sets and passes on the `private` window +// attribute (or not) with various arguments. + +add_task(async function privateFeatureSetOnNewWindowImplicitly() { + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + let newWinOpened = BrowserTestUtils.waitForNewWindow(); + + privateWin.gURLBar._loadURL("about:blank", null, "window", {}); + + let newWin = await newWinOpened; + Assert.equal( + newWin.docShell.treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow).chromeFlags & + Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, + Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, + "New window opened from existing private window should be marked as private" + ); + await BrowserTestUtils.closeWindow(newWin); + await BrowserTestUtils.closeWindow(privateWin); +}); + +add_task(async function privateFeatureSetOnNewWindowExplicitly() { + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + let newWinOpened = BrowserTestUtils.waitForNewWindow(); + + privateWin.gURLBar._loadURL("about:blank", null, "window", { private: true }); + + let newWin = await newWinOpened; + Assert.equal( + newWin.docShell.treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow).chromeFlags & + Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, + Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, + "New window opened from existing private window should be marked as private" + ); + await BrowserTestUtils.closeWindow(newWin); + await BrowserTestUtils.closeWindow(privateWin); +}); + +add_task(async function privateFeatureNotSetOnNewWindowExplicitly() { + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + let newWinOpened = BrowserTestUtils.waitForNewWindow(); + + privateWin.gURLBar._loadURL("about:blank", null, "window", { + private: false, + }); + + let newWin = await newWinOpened; + Assert.notEqual( + newWin.docShell.treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow).chromeFlags & + Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, + Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, + "New window opened from existing private window should be marked as private" + ); + await BrowserTestUtils.closeWindow(newWin); + await BrowserTestUtils.closeWindow(privateWin); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms.js new file mode 100644 index 0000000000..46afdc5856 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms.js @@ -0,0 +1,275 @@ +/* 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/. */ + +// These tests check the behavior of the Urlbar when loading a page +// whose url matches that of the default search engine. + +let defaultTestEngine; + +// The main search string used in tests +const SEARCH_STRING = "chocolate cake"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +// Starts a search with a tab and asserts that +// the state of the Urlbar contains the search term +async function searchWithTab( + searchString, + tab = null, + engine = defaultTestEngine +) { + if (!tab) { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + } + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: searchString, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + + assertSearchStringIsInUrlbar(searchString); + + return { tab, expectedSearchUrl }; +} + +// If a user does a search, goes to another page, and then +// goes back to the SERP, the search term should show. +add_task(async function go_back() { + let { tab } = await searchWithTab(SEARCH_STRING); + + let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + BrowserTestUtils.startLoadingURIString( + tab.linkedBrowser, + "http://www.example.com/some_url" + ); + await browserLoadedPromise; + + let pageShowPromise = BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "pageshow" + ); + tab.linkedBrowser.goBack(); + await pageShowPromise; + + assertSearchStringIsInUrlbar(SEARCH_STRING); + + BrowserTestUtils.removeTab(tab); +}); + +// Manually loading a url that matches a search query url +// should show the search term in the Urlbar. +add_task(async function load_url() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + SEARCH_STRING + ); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, expectedSearchUrl); + await browserLoadedPromise; + assertSearchStringIsInUrlbar(SEARCH_STRING); + + BrowserTestUtils.removeTab(tab); +}); + +// Focusing and blurring the urlbar while the search terms +// persist should change the pageproxystate. +add_task(async function focus_and_unfocus() { + let { tab } = await searchWithTab(SEARCH_STRING); + + gURLBar.focus(); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "invalid", + "Should have matching pageproxystate." + ); + + gURLBar.blur(); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Should have matching pageproxystate." + ); + + BrowserTestUtils.removeTab(tab); +}); + +// If the user modifies the search term, blurring the +// urlbar should keep the urlbar in an invalid pageproxystate. +add_task(async function focus_and_unfocus_modified() { + let { tab } = await searchWithTab(SEARCH_STRING); + + gURLBar.focus(); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "invalid", + "Should have matching pageproxystate." + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "another search term", + fireInputEvent: true, + }); + + gURLBar.blur(); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "invalid", + "Should have matching pageproxystate." + ); + + BrowserTestUtils.removeTab(tab); +}); + +// If Top Sites is cached in the UrlbarView, don't show it if the search terms +// persist in the Urlbar. +add_task(async function focus_after_top_sites() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Prevent the persist tip from interrupting clicking the Urlbar + // after the the SERP has been loaded. + [ + `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}`, + 10000, + ], + ["browser.newtabpage.activity-stream.feeds.topsites", true], + ], + }); + + // Populate Top Sites on a clean version of Places. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesTestUtils.promiseAsyncUpdates(); + await TestUtils.waitForTick(); + + const urls = []; + const N_TOP_SITES = 5; + const N_VISITS = 5; + + for (let i = 0; i < N_TOP_SITES; i++) { + let url = `https://${i}.example.com/hello_world${i}`; + urls.unshift(url); + // Each URL needs to be added several times to boost its frecency enough to + // qualify as a top site. + for (let j = 0; j < N_VISITS; j++) { + await PlacesTestUtils.addVisits(url); + } + } + + let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed").then( + () => info("Observed newtab-top-sites-changed") + ); + await updateTopSites(sites => sites?.length == N_TOP_SITES); + await changedPromise; + + // Ensure Top Sites is cached. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + N_TOP_SITES, + `The number of results should be the same as the number of Top Sites ${N_TOP_SITES}.` + ); + for (let i = 0; i < urls.length; i++) { + let { url } = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.equal(url, urls[i], "The result url should be a Top Site."); + } + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + SEARCH_STRING + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: SEARCH_STRING, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + expectedSearchUrl + ); + Assert.equal( + gBrowser.selectedBrowser.searchTerms, + SEARCH_STRING, + "The search term should be in the Urlbar." + ); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.notEqual( + details.url, + urls[0], + "The first result should not be a Top Site." + ); + Assert.equal( + details.heuristic, + true, + "The first result should be the heuristic result." + ); + Assert.equal( + details.url, + expectedSearchUrl, + "The first result url should be the same as the SERP." + ); + Assert.equal( + details.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "The first result be a search result." + ); + Assert.equal( + details.searchParams?.query, + SEARCH_STRING, + "The first result should have a matching query." + ); + + // Clean up. + SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_backgroundTabs.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_backgroundTabs.js new file mode 100644 index 0000000000..8ed29a9c5b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_backgroundTabs.js @@ -0,0 +1,63 @@ +/* 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/. */ + +// These tests check the behavior of the Urlbar when search terms are +// expected to be shown and tabs are opened in the background. + +let defaultTestEngine; + +// The main search string used in tests +const SEARCH_STRING = "chocolate cake"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +// If a user opens background tab search from the Urlbar, +// the search term should show when the tab is focused. +add_task(async function ctrl_open() { + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + SEARCH_STRING + ); + // Search for the term in a new background tab. + let newTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + expectedSearchUrl + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: SEARCH_STRING, + fireInputEvent: true, + }); + gURLBar.focus(); + + EventUtils.synthesizeKey("KEY_Enter", { + altKey: true, + shiftKey: true, + }); + + // Find the background tab that was created, and switch to it. + let backgroundTab = await newTabPromise; + await BrowserTestUtils.switchTab(gBrowser, backgroundTab); + assertSearchStringIsInUrlbar(SEARCH_STRING); + + BrowserTestUtils.removeTab(backgroundTab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_modifiedUrl.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_modifiedUrl.js new file mode 100644 index 0000000000..4182e3bf3d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_modifiedUrl.js @@ -0,0 +1,104 @@ +/* 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/. */ + +// These tests check the behavior of the Urlbar when search terms are +// expected to be shown but the url is modified from what the browser expects. + +let defaultTestEngine; + +// The main search string used in tests +const SEARCH_STRING = "chocolate cake"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +// If a SERP uses the History API to modify the URI, +// the search term should still show in the URL bar. +add_task(async function history_push_state() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + SEARCH_STRING + ); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, expectedSearchUrl); + await browserLoadedPromise; + + let locationChangePromise = BrowserTestUtils.waitForLocationChange(gBrowser); + await SpecialPowers.spawn(tab.linkedBrowser, [], function () { + let url = new URL(content.window.location); + url.searchParams.set("pc", "fake_code_2"); + content.history.pushState({}, "", url); + }); + + await locationChangePromise; + // Check URI to make sure that it's actually been changed + Assert.equal( + gBrowser.currentURI.spec, + `https://www.example.com/?q=chocolate+cake&pc=fake_code_2`, + "URI of Urlbar should have changed" + ); + + Assert.equal( + gURLBar.value, + SEARCH_STRING, + `Search string ${SEARCH_STRING} should be in the url bar` + ); + + BrowserTestUtils.removeTab(tab); +}); + +// Loading a url that looks like a search query url but has additional +// query params should not show the search term in the Urlbar. +add_task(async function url_with_additional_query_params() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + SEARCH_STRING + ); + // Add a query param + expectedSearchUrl += "&another_code=something_else"; + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, expectedSearchUrl); + await browserLoadedPromise; + + Assert.equal( + gURLBar.value, + UrlbarTestUtils.trimURL(expectedSearchUrl), + `URL should be in URL bar` + ); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Pageproxystate should be valid" + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_moveTab.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_moveTab.js new file mode 100644 index 0000000000..790e950c65 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_moveTab.js @@ -0,0 +1,136 @@ +/* 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/. */ + +/* + These tests check the behavior of the Urlbar when search terms are shown + and the tab with the default SERP moves from one window to another. + + Unlike other searchTerm tests, these modify the currentURI to ensure + that the currentURI has a different spec than the default SERP so that + the search terms won't show if the originalURI wasn't properly copied + during the tab swap. +*/ + +let originalEngine, defaultTestEngine; + +// The main search keyword used in tests +const SEARCH_STRING = "chocolate cake"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.showSearchTerms.featureGate", true], + ["browser.urlbar.tipShownCount.searchTip_persist", 999], + ], + }); + + await SearchTestUtils.installSearchExtension({ + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + originalEngine = await Services.search.getDefault(); + await Services.search.setDefault( + defaultTestEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + registerCleanupFunction(async function () { + await Services.search.setDefault( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await PlacesUtils.history.clear(); + }); +}); + +async function searchWithTab( + searchString, + tab = null, + engine = defaultTestEngine +) { + if (!tab) { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + } + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: searchString, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + + return { tab, expectedSearchUrl }; +} + +// Move a tab showing the search term into its own window. +add_task(async function move_tab_into_new_window() { + let { tab, expectedSearchUrl } = await searchWithTab(SEARCH_STRING); + + // Mock the default SERP modifying the existing url + // so that the originalURI and currentURI differ. + await SpecialPowers.spawn( + tab.linkedBrowser, + [expectedSearchUrl], + async url => { + content.history.pushState({}, "", url + "&pc2=firefox"); + } + ); + + // Move the tab into its own window. + let newWindow = gBrowser.replaceTabWithWindow(tab); + await BrowserTestUtils.waitForEvent(tab.linkedBrowser, "SwapDocShells"); + + assertSearchStringIsInUrlbar(SEARCH_STRING, { win: newWindow }); + + // Clean up. + await BrowserTestUtils.closeWindow(newWindow); +}); + +// Move a tab from its own window into an existing window. +add_task(async function move_tab_into_existing_window() { + // Load a second window with the default SERP. + let win = await BrowserTestUtils.openNewBrowserWindow({ remote: true }); + let browser = win.gBrowser.selectedBrowser; + let tab = win.gBrowser.tabs[0]; + + // Load the default SERP into the second window. + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + SEARCH_STRING + ); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + expectedSearchUrl + ); + BrowserTestUtils.startLoadingURIString(browser, expectedSearchUrl); + await browserLoadedPromise; + + // Mock the default SERP modifying the existing url + // so that the originalURI and currentURI differ. + await SpecialPowers.spawn(browser, [expectedSearchUrl], async url => { + content.history.pushState({}, "", url + "&pc2=firefox"); + }); + + // Make the first window adopt and switch to that tab. + tab = gBrowser.adoptTab(tab); + await BrowserTestUtils.switchTab(gBrowser, tab); + assertSearchStringIsInUrlbar(SEARCH_STRING); + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_popup.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_popup.js new file mode 100644 index 0000000000..ee5bfb7dfc --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_popup.js @@ -0,0 +1,145 @@ +/* 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/. */ + +// These tests check the behavior of the Urlbar when persist search terms +// are either enabled or disabled, and a popup notification is shown. + +function waitForPopupNotification() { + let promisePopupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + PopupNotifications.show( + gBrowser.selectedBrowser, + "test-notification", + "This is a sample popup." + ); + return promisePopupShown; +} + +// The main search string used in tests. +const SEARCH_TERM = "chocolate"; +const PREF_FEATUREGATE = "browser.urlbar.showSearchTerms.featureGate"; +let defaultTestEngine; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_FEATUREGATE, true]], + }); + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +async function searchWithTab( + searchString, + tab = null, + engine = defaultTestEngine, + expectedPersistedSearchTerms = true +) { + if (!tab) { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + } + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: searchString, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + + if (expectedPersistedSearchTerms) { + assertSearchStringIsInUrlbar(searchString); + } + + return { tab, expectedSearchUrl }; +} + +// A notification should cause the urlbar to revert while +// the search term persists. +add_task(async function generic_popup_when_persist_is_enabled() { + let { tab, expectedSearchUrl } = await searchWithTab(SEARCH_TERM); + + await waitForPopupNotification(); + + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Urlbar should have a valid pageproxystate." + ); + + Assert.equal( + gURLBar.value, + UrlbarTestUtils.trimURL(expectedSearchUrl), + "Search url should be in the urlbar." + ); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// Ensure the urlbar is not being reverted when a prompt is shown +// and the persist feature is disabled. +add_task(async function generic_popup_no_revert_when_persist_is_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_FEATUREGATE, false]], + }); + + let { tab } = await searchWithTab( + SEARCH_TERM, + null, + defaultTestEngine, + false + ); + + // Have a user typed value in the urlbar to make + // pageproxystate invalid. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: SEARCH_TERM, + }); + gURLBar.blur(); + + await waitForPopupNotification(); + + // Wait a brief amount of time between when the popup is shown + // and when the event handler should fire if it's enabled. + await TestUtils.waitForTick(); + + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "invalid", + "Urlbar should not be reverted." + ); + + Assert.equal( + gURLBar.value, + SEARCH_TERM, + "User typed value should remain in urlbar." + ); + + BrowserTestUtils.removeTab(tab); + SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_revert.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_revert.js new file mode 100644 index 0000000000..0fb9f2e7fb --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_revert.js @@ -0,0 +1,170 @@ +/* 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/. */ + +// These tests check the behavior of the Urlbar when search terms are shown +// and the user reverts the Urlbar. + +let defaultTestEngine; + +// The main search keyword used in tests +const SEARCH_STRING = "chocolate cake"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +async function searchWithTab( + searchString, + tab = null, + engine = defaultTestEngine +) { + if (!tab) { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + } + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: searchString, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + + assertSearchStringIsInUrlbar(searchString); + + return { tab, expectedSearchUrl }; +} + +function synthesizeRevert() { + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Escape", { repeat: 2 }); +} + +// Users should be able to revert the URL bar +add_task(async function revert() { + let { tab, expectedSearchUrl } = await searchWithTab(SEARCH_STRING); + synthesizeRevert(); + + Assert.equal( + gURLBar.value, + UrlbarTestUtils.trimURL(expectedSearchUrl), + `Urlbar should have the reverted URI ${expectedSearchUrl} as its value.` + ); + + BrowserTestUtils.removeTab(tab); +}); + +// Users should be able to revert the URL bar, +// and go to the same page. +add_task(async function revert_and_press_enter() { + let { tab, expectedSearchUrl } = await searchWithTab(SEARCH_STRING); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + + synthesizeRevert(); + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + + BrowserTestUtils.removeTab(tab); +}); + +// Users should be able to revert the URL, and then if they navigate +// to another tab, the tab that was reverted will show the search term again. +add_task(async function revert_and_change_tab() { + let { tab, expectedSearchUrl } = await searchWithTab(SEARCH_STRING); + + synthesizeRevert(); + + Assert.notEqual( + gURLBar.value, + SEARCH_STRING, + `Search string ${SEARCH_STRING} should not be in the url bar` + ); + Assert.equal( + gURLBar.value, + UrlbarTestUtils.trimURL(expectedSearchUrl), + `Urlbar should have ${expectedSearchUrl} as value.` + ); + + // Open another tab + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // Switch back to the original tab. + await BrowserTestUtils.switchTab(gBrowser, tab); + + // Because the urlbar is focused, the pageproxystate should be invalid. + assertSearchStringIsInUrlbar(SEARCH_STRING, { pageProxyState: "invalid" }); + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); + +// If a user reverts a tab, and then does another search, +// they should be able to see the search term again. +add_task(async function revert_and_search_again() { + let { tab } = await searchWithTab(SEARCH_STRING); + synthesizeRevert(); + await searchWithTab("another search string", tab); + BrowserTestUtils.removeTab(tab); +}); + +// If a user reverts the Urlbar while on a default SERP, +// and they navigate away from the page by visiting another +// link or using the back/forward buttons, the Urlbar should +// show the search term again when returning back to the default SERP. +add_task(async function revert_when_using_content() { + let { tab } = await searchWithTab(SEARCH_STRING); + synthesizeRevert(); + await searchWithTab("another search string", tab); + + // Revert the page, and then go back and forth in history. + // The search terms should show up. + synthesizeRevert(); + let pageShowPromise = BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "pageshow" + ); + tab.linkedBrowser.goBack(); + await pageShowPromise; + assertSearchStringIsInUrlbar(SEARCH_STRING); + + pageShowPromise = BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "pageshow" + ); + tab.linkedBrowser.goForward(); + await pageShowPromise; + assertSearchStringIsInUrlbar("another search string"); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchBar.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchBar.js new file mode 100644 index 0000000000..784d8932ac --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchBar.js @@ -0,0 +1,104 @@ +/* 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/. */ + +// These tests check the behavior of the Urlbar when a user enables +// the search bar and showSearchTerms is true. + +const { CustomizableUITestUtils } = ChromeUtils.importESModule( + "resource://testing-common/CustomizableUITestUtils.sys.mjs" +); + +const gCUITestUtils = new CustomizableUITestUtils(window); +const SEARCH_STRING = "example_string"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.widget.inNavBar", true], + ["browser.urlbar.showSearchTerms.featureGate", true], + ], + }); + + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + gCUITestUtils.removeSearchBar(); + }); +}); + +function assertSearchStringIsNotInUrlbar(searchString) { + Assert.notEqual( + gURLBar.value, + searchString, + `Search string ${searchString} should not be in the url bar.` + ); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Pageproxystate should be valid." + ); + Assert.equal( + gBrowser.selectedBrowser.searchTerms, + "", + "searchTerms should be blank." + ); +} + +// When a user enables the search bar, and does a search in the search bar, +// the search term should not show in the URL bar. +add_task(async function search_bar_on() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await gCUITestUtils.addSearchBar(); + + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + `https://www.example.com/?q=${SEARCH_STRING}&pc=fake_code` + ); + + let searchBar = BrowserSearch.searchBar; + searchBar.value = SEARCH_STRING; + searchBar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + + await browserLoadedPromise; + assertSearchStringIsNotInUrlbar(SEARCH_STRING); + + BrowserTestUtils.removeTab(tab); +}); + +// When a user enables the search bar, and does a search in the URL bar, +// the search term should still not show in the URL bar. +add_task(async function search_bar_on_with_url_bar_search() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await gCUITestUtils.addSearchBar(); + + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + `https://www.example.com/?q=${SEARCH_STRING}&pc=fake_code` + ); + + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: SEARCH_STRING, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + + await browserLoadedPromise; + assertSearchStringIsNotInUrlbar(SEARCH_STRING); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchMode.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchMode.js new file mode 100644 index 0000000000..d6793f4d0f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_searchMode.js @@ -0,0 +1,85 @@ +/* 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/. */ + +// These tests check the behavior of the Urlbar when using search mode + +let defaultTestEngine; + +// The main search string used in tests +const SEARCH_STRING = "chocolate cake"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + await SearchTestUtils.installSearchExtension({ + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + await SearchTestUtils.installSearchExtension( + { + name: "MochiSearch", + search_url: "https://mochi.test:8888/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +// When a user does a search with search mode, they should +// not see the search term in the URL bar for that engine. +add_task(async function non_default_search() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + SEARCH_STRING + ); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: SEARCH_STRING, + }); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: defaultTestEngine.name, + }); + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + + Assert.equal( + gURLBar.value, + UrlbarTestUtils.trimURL(expectedSearchUrl), + `URL should be in URL bar` + ); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Pageproxystate should be valid" + ); + Assert.equal( + gBrowser.userTypedValue, + null, + "There should not be a userTypedValue for a search on a non-default search engine" + ); + Assert.equal( + gBrowser.selectedBrowser.searchTerms, + "", + "searchTerms should be empty." + ); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_strings.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_strings.js new file mode 100644 index 0000000000..866cb38760 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_strings.js @@ -0,0 +1,79 @@ +/* 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 test checks whether certain patterns of search terms will show +// in the Urlbar as a search term. + +ChromeUtils.defineESModuleGetters(this, { + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +let defaultTestEngine; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}", + }, + { setAsDefault: true } + ); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +// Search terms should show up in the url bar if the pref is on +// and the SERP url matches the one constructed in Firefox +add_task(async function search_strings() { + const searches = [ + // Single word + "chocolate", + // Word with space + "chocolate cake", + // Allowable special characters. + "chocolate;,?@&=+$-_!~*'()#cake", + // Period used after the first word. + "what is 255.255.255.255", + // Protocol used after the first word. + "what is https://", + // Search with special characters + '"chocolate cake" -recipes', + "window.location how to use", + "http", + "https", + // Long string within threshold. + "h".repeat(UrlbarUtils.MAX_TEXT_LENGTH), + ]; + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + for (let searchString of searches) { + info("Search for term:", searchString); + let [searchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + searchString + ); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + searchUrl + ); + BrowserTestUtils.startLoadingURIString(gBrowser, searchUrl); + await browserLoadedPromise; + assertSearchStringIsInUrlbar(searchString); + + info("Check that no formatting is applied."); + UrlbarTestUtils.checkFormatting(window, searchString); + } + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_stringsUnsafe.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_stringsUnsafe.js new file mode 100644 index 0000000000..09743b3ec2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_stringsUnsafe.js @@ -0,0 +1,133 @@ +/* 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 test checks whether certain patterns of search terms won't show +// in the Urlbar as a search term. + +// Can regularly cause a timeout error on Mac verify mode. +requestLongerTimeout(5); + +ChromeUtils.defineESModuleGetters(this, { + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +let defaultTestEngine, tab; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}", + }, + { setAsDefault: true } + ); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +async function checkSearchString(searchString, isIpv6) { + info("Search for term:", searchString); + let [searchUrl] = UrlbarUtils.getSearchQueryUrl( + defaultTestEngine, + searchString + ); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + searchUrl + ); + BrowserTestUtils.startLoadingURIString(gBrowser, searchUrl); + await browserLoadedPromise; + + // decodeURI is necessary for matching square brackets in IPV6. + let expectedUrl = isIpv6 ? decodeURI(searchUrl) : searchUrl; + + if (UrlbarPrefs.get("trimHttps") && expectedUrl.startsWith("https://")) { + expectedUrl = expectedUrl.slice("https://".length); + } + + Assert.equal(gURLBar.value, expectedUrl, "The full URL should be in URL bar"); + Assert.equal( + gBrowser.userTypedValue, + null, + `There should not be a userTypedValue for ${searchString}` + ); + Assert.equal( + gBrowser.selectedBrowser.searchTerms, + "", + "searchTerms should be empty." + ); +} + +add_task(async function unsafe_search_strings() { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + const searches = [ + "example.org", + "www.example.org", + " www.example.org ", + "www.example.org/path", + "https://", + "https://example", + "https://example.org", + "https://example.org/path", + "https:// example.org/", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://", + "http://example", + "http://example.org", + "http://example.org/path", + "http:// example.org/path", + "file://example", + // Some protocols can be fixed up. + "ttp://example", + "htp://example", + "ttps://example", + "tps://example", + "ps://example", + "htps://example", + // Protocol fixup with a space and path. + "ttp:// example.org/path", + "htp:// example.org/path", + "ttps:// example.org/path", + "tps:// example.org/path", + "ps:// example.org/path", + "htps:// example.org/path", + // Variations of spaces. + "https ://example.org", + "https: //example.org", + "https:/ /example.org", + "https://\texample.org", + "https://\r\nexample.org", + // URL without protocols. + "www.example.org", + "www.example.org/path", + "www.example.org/path path", + "www. example.org/path", + // Long string exceeding threshold. + "h".repeat(UrlbarUtils.MAX_TEXT_LENGTH + 1), + ]; + for (let searchString of searches) { + await checkSearchString(searchString, false); + } + + const ipV6Searches = [ + "[2001:db8:85a3:8d3:1319:8a2e:370:7348]/example", + // Includes a space. + "[2001:db8:85a3:8d3:1319:8a2e:370:7348]/path path", + ]; + for (let searchString of ipV6Searches) { + await checkSearchString(searchString, true); + } + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_switch_tab.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_switch_tab.js new file mode 100644 index 0000000000..77ee3e19d7 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_switch_tab.js @@ -0,0 +1,139 @@ +/* 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/. */ + +// These tests check the behavior of the Urlbar when search terms are shown +// and the user switches between tabs. + +let defaultTestEngine; + +// The main search keyword used in tests +const SEARCH_STRING = "chocolate cake"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +async function searchWithTab( + searchString, + tab = null, + engine = defaultTestEngine +) { + if (!tab) { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + } + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: searchString, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + + assertSearchStringIsInUrlbar(searchString); + + return { tab, expectedSearchUrl }; +} + +// Users should be able to search, change the tab, and come +// back to the original tab to see the search term again +add_task(async function change_tab() { + let { tab: tab1 } = await searchWithTab(SEARCH_STRING); + let { tab: tab2 } = await searchWithTab("another keyword"); + let { tab: tab3 } = await searchWithTab("yet another keyword"); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + assertSearchStringIsInUrlbar(SEARCH_STRING); + + await BrowserTestUtils.switchTab(gBrowser, tab2); + assertSearchStringIsInUrlbar("another keyword"); + + await BrowserTestUtils.switchTab(gBrowser, tab3); + assertSearchStringIsInUrlbar("yet another keyword"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); +}); + +// If a user types in the URL bar, and the user goes to a +// different tab, the original tab should still contain the +// text written by the user. +add_task(async function user_overwrites_search_term() { + let { tab: tab1 } = await searchWithTab(SEARCH_STRING); + + gURLBar.focus(); + gURLBar.select(); + EventUtils.sendString("another_word"); + + Assert.notEqual( + gURLBar.value, + SEARCH_STRING, + `Search string ${SEARCH_STRING} should not be in the url bar` + ); + + // Open a new tab, switch back to the first and + // check that the user typed value is still there. + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await BrowserTestUtils.switchTab(gBrowser, tab1); + + Assert.equal( + gURLBar.value, + "another_word", + "another_word should be in the url bar" + ); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// If a user clears the URL bar, and goes to a different tab, +// and returns to the initial tab, it should show the search term again. +add_task(async function user_overwrites_search_term() { + let { tab: tab1 } = await searchWithTab(SEARCH_STRING); + + gURLBar.focus(); + gURLBar.select(); + EventUtils.sendKey("delete"); + + Assert.equal(gURLBar.value, "", "Empty string should be in url bar."); + + // Open a new tab, switch back to the first and check + // the blank string is replaced with the search string. + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await BrowserTestUtils.switchTab(gBrowser, tab1); + + assertSearchStringIsInUrlbar(SEARCH_STRING, { + pageProxyState: "invalid", + userTypedValue: "", + }); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_telemetry.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_telemetry.js new file mode 100644 index 0000000000..4bc4a3935d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_searchTerms_telemetry.js @@ -0,0 +1,378 @@ +/* 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/. */ + +/** + * These tests check that we record the number of times search terms + * persist in the Urlbar, and when search terms are cleared due to a + * PopupNotification. + * + * This is different from existing telemetry that tracks whether users + * interacted with the Urlbar or made another search while the search + * terms were peristed. + */ + +let defaultTestEngine; + +// The main search string used in tests +const SEARCH_STRING = "chocolate cake"; + +// Telemetry keys. +const PERSISTED_VIEWED = "urlbar.persistedsearchterms.view_count"; +const PERSISTED_REVERTED = "urlbar.persistedsearchterms.revert_by_popup_count"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + defaultTestEngine = Services.search.getEngineByName("MozSearch"); + Services.telemetry.clearScalars(); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + Services.telemetry.clearScalars(); + }); +}); + +// Starts a search with a tab and asserts that +// the state of the Urlbar contains the search term. +async function searchWithTab( + searchString, + tab = null, + engine = defaultTestEngine, + assertSearchString = true +) { + if (!tab) { + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + } + + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, searchString); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: searchString, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + + if (assertSearchString) { + assertSearchStringIsInUrlbar(searchString); + } + + return { tab, expectedSearchUrl }; +} + +add_task(async function load_page_with_persisted_search() { + let { tab } = await searchWithTab(SEARCH_STRING); + const scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function load_page_without_persisted_search() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", false]], + }); + + let { tab } = await searchWithTab( + SEARCH_STRING, + null, + defaultTestEngine, + false + ); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, undefined); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// Multiple searches should result in the correct number of recorded views. +add_task(async function load_page_n_times() { + let N = 5; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + for (let index = 0; index < N; ++index) { + await searchWithTab(SEARCH_STRING, tab); + } + + const scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, N); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +// A persisted search view event should not be recorded when unfocusing the urlbar. +add_task(async function focus_and_unfocus() { + let { tab } = await searchWithTab(SEARCH_STRING); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + gURLBar.focus(); + gURLBar.select(); + gURLBar.blur(); + + // Focusing and unfocusing the urlbar shouldn't change the persisted view count. + scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +// A persisted search view event should not be recorded by a +// pushState event after a page has been loaded. +add_task(async function history_api() { + let { tab } = await searchWithTab(SEARCH_STRING); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + await SpecialPowers.spawn(tab.linkedBrowser, [], function () { + let url = new URL(content.window.location); + let someState = { value: true }; + url.searchParams.set("pc", "fake_code_2"); + content.history.pushState(someState, "", url); + someState.value = false; + content.history.replaceState(someState, "", url); + }); + + scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +// A persisted search view event should be recorded when switching back to a tab +// that contains search terms. +add_task(async function switch_tabs() { + let { tab } = await searchWithTab(SEARCH_STRING); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await BrowserTestUtils.switchTab(gBrowser, tab); + + scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 2); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); + +// A telemetry event should be recorded when returning to a persisted SERP via tabhistory. +add_task(async function tabhistory() { + let { tab } = await searchWithTab(SEARCH_STRING); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + BrowserTestUtils.startLoadingURIString( + tab.linkedBrowser, + "https://www.example.com/some_url" + ); + await browserLoadedPromise; + + let pageShowPromise = BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "pageshow" + ); + tab.linkedBrowser.goBack(); + await pageShowPromise; + + scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 2); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +// PopupNotification's that rely on an anchor element in the urlbar should trigger +// an increment of the revert counter. +// This assumes the anchor element is present in the Urlbar during a valid pageproxystate. +add_task(async function popup_in_urlbar() { + let { tab } = await searchWithTab(SEARCH_STRING); + let promisePopupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + PopupNotifications.show( + gBrowser.selectedBrowser, + "test-notification", + "This is a sample popup." + ); + await promisePopupShown; + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, 1); + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +// Non-persistent PopupNotifications won't re-appear if a user switches +// tabs and returns to the tab that had the Popup. +add_task(async function non_persistent_popup_in_urlbar_switch_tab() { + let { tab } = await searchWithTab(SEARCH_STRING); + let promisePopupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + PopupNotifications.show( + gBrowser.selectedBrowser, + "test-notification", + "This is a sample popup.", + "geo-notification-icon" + ); + await promisePopupShown; + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, 1); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await BrowserTestUtils.switchTab(gBrowser, tab); + + scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 2); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, 1); + + // Clean up. + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); + +// Persistent PopupNotifications will constantly appear to users +// even if they switch to another tab and switch back. +add_task(async function persistent_popup_in_urlbar_switch_tab() { + let { tab } = await searchWithTab(SEARCH_STRING); + let promisePopupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + PopupNotifications.show( + gBrowser.selectedBrowser, + "test-notification", + "This is a sample popup.", + "geo-notification-icon", + null, + null, + { persistent: true } + ); + await promisePopupShown; + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, 1); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + promisePopupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + await BrowserTestUtils.switchTab(gBrowser, tab); + await promisePopupShown; + + scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 2); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, 2); + + // Clean up. + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); + +// If the persist feature is not enabled, a telemetry event should not be recorded +// if a PopupNotification uses an anchor in the Urlbar. +add_task(async function popup_in_urlbar_without_feature() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", false]], + }); + + let { tab } = await searchWithTab( + SEARCH_STRING, + null, + defaultTestEngine, + false + ); + let promisePopupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + PopupNotifications.show( + gBrowser.selectedBrowser, + "test-notification", + "This is a sample popup." + ); + await promisePopupShown; + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, undefined); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// If the anchor element for the PopupNotification is not located in the Urlbar, +// a telemetry event should not be recorded. +add_task(async function popup_not_in_urlbar() { + let { tab } = await searchWithTab(SEARCH_STRING); + + let promisePopupShown = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + PopupNotifications.show( + gBrowser.selectedBrowser, + "test-notification", + "This is a sample popup that uses the unified extensions button.", + gUnifiedExtensions.getPopupAnchorID(gBrowser.selectedBrowser, window) + ); + await promisePopupShown; + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_VIEWED, 1); + TelemetryTestUtils.assertScalar(scalars, PERSISTED_REVERTED, undefined); + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_setURI.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_setURI.js new file mode 100644 index 0000000000..f4afeded40 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_setURI.js @@ -0,0 +1,128 @@ +/* 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/. */ + +function test() { + waitForExplicitFinish(); + + // avoid prompting about phishing + Services.prefs.setIntPref(phishyUserPassPref, 32); + registerCleanupFunction(function () { + Services.prefs.clearUserPref(phishyUserPassPref); + }); + + nextTest(); +} + +const phishyUserPassPref = "network.http.phishy-userpass-length"; + +function nextTest() { + let testCase = tests.shift(); + if (testCase) { + testCase(function () { + executeSoon(nextTest); + }); + } else { + executeSoon(finish); + } +} + +var tests = [ + function revert(next) { + loadTabInWindow(window, function (tab) { + gURLBar.handleRevert(); + is( + gURLBar.value, + UrlbarTestUtils.trimURL("http://example.com"), + "URL bar had user/pass stripped after reverting" + ); + gBrowser.removeTab(tab); + next(); + }); + }, + function customize(next) { + // Need to wait for delayedStartup for the customization part of the test, + // since that's where BrowserToolboxCustomizeDone is set. + BrowserTestUtils.openNewBrowserWindow().then(function (win) { + loadTabInWindow(win, function () { + openToolbarCustomizationUI(function () { + closeToolbarCustomizationUI(function () { + is( + win.gURLBar.value, + UrlbarTestUtils.trimURL("http://example.com"), + "URL bar had user/pass stripped after customize" + ); + win.close(); + next(); + }, win); + }, win); + }); + }); + }, + function pageloaderror(next) { + loadTabInWindow(window, function (tab) { + // Load a new URL and then immediately stop it, to simulate a page load + // error. + BrowserTestUtils.startLoadingURIString( + tab.linkedBrowser, + "http://test1.example.com" + ); + tab.linkedBrowser.stop(); + is( + gURLBar.value, + UrlbarTestUtils.trimURL("http://example.com"), + "URL bar had user/pass stripped after load error" + ); + gBrowser.removeTab(tab); + next(); + }); + }, +]; + +function loadTabInWindow(win, callback) { + info("Loading tab"); + let url = "http://user:pass@example.com/"; + let tab = (win.gBrowser.selectedTab = BrowserTestUtils.addTab( + win.gBrowser, + url + )); + BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url).then(() => { + info("Tab loaded"); + is( + win.gURLBar.value, + UrlbarTestUtils.trimURL("http://example.com"), + "URL bar had user/pass stripped initially" + ); + callback(tab); + }, true); +} + +function openToolbarCustomizationUI(aCallback, aBrowserWin) { + if (!aBrowserWin) { + aBrowserWin = window; + } + + aBrowserWin.gCustomizeMode.enter(); + + aBrowserWin.gNavToolbox.addEventListener( + "customizationready", + function () { + executeSoon(function () { + aCallback(aBrowserWin); + }); + }, + { once: true } + ); +} + +function closeToolbarCustomizationUI(aCallback, aBrowserWin) { + aBrowserWin.gNavToolbox.addEventListener( + "aftercustomization", + function () { + executeSoon(aCallback); + }, + { once: true } + ); + + aBrowserWin.gCustomizeMode.exit(); +} diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_tooltip.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_tooltip.js new file mode 100644 index 0000000000..484ac22007 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_tooltip.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function synthesizeMouseOver(element) { + info("synthesize mouseover"); + let promise = BrowserTestUtils.waitForEvent(element, "mouseover"); + EventUtils.synthesizeMouseAtCenter(document.documentElement, { + type: "mouseout", + }); + EventUtils.synthesizeMouseAtCenter(element, { type: "mouseover" }); + EventUtils.synthesizeMouseAtCenter(element, { type: "mousemove" }); + return promise; +} + +function synthesizeMouseOut(element) { + info("synthesize mouseout"); + let promise = BrowserTestUtils.waitForEvent(element, "mouseout"); + EventUtils.synthesizeMouseAtCenter(element, { type: "mouseover" }); + EventUtils.synthesizeMouseAtCenter(element, { type: "mouseout" }); + EventUtils.synthesizeMouseAtCenter(document.documentElement, { + type: "mousemove", + }); + return promise; +} + +async function expectTooltip(text) { + if (!gURLBar._overflowing && !gURLBar._inOverflow) { + info("waiting for overflow event"); + await BrowserTestUtils.waitForEvent(gURLBar.inputField, "overflow"); + } + + let tooltip = document.getElementById("aHTMLTooltip"); + let element = gURLBar.inputField; + + let popupShownPromise = BrowserTestUtils.waitForEvent(tooltip, "popupshown"); + await synthesizeMouseOver(element); + info("awaiting for tooltip popup"); + await popupShownPromise; + + is(element.getAttribute("title"), text, "title attribute has expected text"); + is(tooltip.textContent, text, "tooltip shows expected text"); + + await synthesizeMouseOut(element); +} + +async function expectNoTooltip() { + if (gURLBar._overflowing || gURLBar._inOverflow) { + info("waiting for underflow event"); + await BrowserTestUtils.waitForEvent(gURLBar.inputField, "underflow"); + } + + let element = gURLBar.inputField; + await synthesizeMouseOver(element); + + is(element.getAttribute("title"), null, "title attribute shouldn't be set"); + + await synthesizeMouseOut(element); +} + +add_task(async function () { + window.windowUtils.disableNonTestMouseEvents(true); + registerCleanupFunction(() => { + window.windowUtils.disableNonTestMouseEvents(false); + }); + + // Ensure the URL bar is neither focused nor hovered before we start. + gBrowser.selectedBrowser.focus(); + await synthesizeMouseOut(gURLBar.inputField); + + gURLBar.value = "short string"; + await expectNoTooltip(); + + let longURL = "http://longurl.com/" + "foobar/".repeat(30); + gURLBar.value = longURL; + is( + gURLBar.value, + UrlbarTestUtils.trimURL(longURL), + "Urlbar value has http:// stripped" + ); + await expectTooltip(longURL); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js new file mode 100644 index 0000000000..b96017435e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js @@ -0,0 +1,150 @@ +function testValues(trimmedProtocol, notTrimmedProtocol) { + testVal(trimmedProtocol + "mozilla.org/", "mozilla.org"); + testVal( + notTrimmedProtocol + "mozilla.org/", + notTrimmedProtocol + "mozilla.org" + ); + testVal(trimmedProtocol + "mözilla.org/", "mözilla.org"); + // This isn't a valid public suffix, thus we should untrim it or it would + // end up doing a search. + testVal(trimmedProtocol + "mozilla.imaginatory/"); + testVal(trimmedProtocol + "www.mozilla.org/", "www.mozilla.org"); + testVal(trimmedProtocol + "sub.mozilla.org/", "sub.mozilla.org"); + testVal( + trimmedProtocol + "sub1.sub2.sub3.mozilla.org/", + "sub1.sub2.sub3.mozilla.org" + ); + testVal(trimmedProtocol + "mozilla.org/file.ext", "mozilla.org/file.ext"); + testVal(trimmedProtocol + "mozilla.org/sub/", "mozilla.org/sub/"); + + testVal(trimmedProtocol + "ftp.mozilla.org/", "ftp.mozilla.org"); + testVal(trimmedProtocol + "ftp1.mozilla.org/", "ftp1.mozilla.org"); + testVal(trimmedProtocol + "ftp42.mozilla.org/", "ftp42.mozilla.org"); + testVal(trimmedProtocol + "ftpx.mozilla.org/", "ftpx.mozilla.org"); + testVal("ftp://ftp.mozilla.org/", "ftp://ftp.mozilla.org"); + testVal("ftp://ftp1.mozilla.org/", "ftp://ftp1.mozilla.org"); + testVal("ftp://ftp42.mozilla.org/", "ftp://ftp42.mozilla.org"); + testVal("ftp://ftpx.mozilla.org/", "ftp://ftpx.mozilla.org"); + + testVal( + notTrimmedProtocol + "user:pass@mozilla.org/", + notTrimmedProtocol + "user:pass@mozilla.org" + ); + testVal( + notTrimmedProtocol + "user@mozilla.org/", + notTrimmedProtocol + "user@mozilla.org" + ); + + testVal("mailto:admin@mozilla.org"); + testVal("gopher://mozilla.org/"); + testVal("about:config"); + testVal("jar:http://mozilla.org/example.jar!/"); + testVal("view-source:http://mozilla.org/"); +} + +add_task(async function () { + const PREF_TRIM_URLS = "browser.urlbar.trimURLs"; + const PREF_TRIM_HTTPS = "browser.urlbar.trimHttps"; + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(tab); + Services.prefs.clearUserPref(PREF_TRIM_URLS); + Services.prefs.clearUserPref(PREF_TRIM_HTTPS); + gURLBar.setURI(); + }); + + Services.prefs.setBoolPref(PREF_TRIM_HTTPS, false); + + // Avoid search service sync init warnings due to URIFixup, when running the + // test alone. + await Services.search.init(); + + Services.prefs.setBoolPref(PREF_TRIM_URLS, true); + + testValues("http://", "https://"); + Services.prefs.setBoolPref(PREF_TRIM_HTTPS, true); + testValues("https://", "http://"); + Services.prefs.setBoolPref(PREF_TRIM_HTTPS, false); + + // Behaviour for hosts with no dots depends on the whitelist: + let fixupWhitelistPref = "browser.fixup.domainwhitelist.localhost"; + Services.prefs.setBoolPref(fixupWhitelistPref, false); + testVal("http://localhost"); + Services.prefs.setBoolPref(fixupWhitelistPref, true); + testVal("http://localhost", "localhost"); + Services.prefs.clearUserPref(fixupWhitelistPref); + + testVal("http:// invalid url"); + + testVal("http://someotherhostwithnodots"); + + // This host is whitelisted, it can be trimmed. + testVal("http://localhost/ foo bar baz", "localhost/ foo bar baz"); + + testVal("http://user:pass@mozilla.org/", "user:pass@mozilla.org"); + testVal("http://user@mozilla.org/", "user@mozilla.org"); + testVal("http://sub.mozilla.org:666/", "sub.mozilla.org:666"); + + testVal("https://[fe80::222:19ff:fe11:8c76]/file.ext"); + testVal("http://[fe80::222:19ff:fe11:8c76]/", "[fe80::222:19ff:fe11:8c76]"); + testVal("https://user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext"); + testVal( + "http://user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext", + "user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext" + ); + + // This is not trimmed because it's not in the domain whitelist. + testVal( + "http://localhost.localdomain/ foo bar baz", + "http://localhost.localdomain/ foo bar baz" + ); + Services.prefs.setBoolPref(PREF_TRIM_URLS, false); + + testVal("http://mozilla.org/"); + + Services.prefs.setBoolPref(PREF_TRIM_URLS, true); + + let promiseLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "http://example.com/" + ); + BrowserTestUtils.startLoadingURIString(gBrowser, "http://example.com/"); + await promiseLoaded; + + await testCopy("example.com", "http://example.com/"); + + gURLBar.setPageProxyState("invalid"); + gURLBar.valueIsTyped = true; + await testCopy("example.com", "example.com"); +}); + +function testVal(originalValue, targetValue) { + gURLBar.value = originalValue; + gURLBar.valueIsTyped = false; + let trimmedValue = UrlbarPrefs.get("trimURLs") + ? BrowserUIUtils.trimURL(originalValue) + : originalValue; + Assert.equal(gURLBar.value, trimmedValue, "url bar value set"); + // Now focus the urlbar and check the inputField value is properly set. + gURLBar.focus(); + Assert.equal( + gURLBar.value, + targetValue || originalValue, + "Check urlbar value on focus" + ); + // On blur we should trim again. + gURLBar.blur(); + Assert.equal(gURLBar.value, trimmedValue, "Check urlbar value on blur"); +} + +function testCopy(originalValue, targetValue) { + return SimpleTest.promiseClipboardChange(targetValue, () => { + Assert.equal(gURLBar.value, originalValue, "url bar copy value set"); + gURLBar.focus(); + gURLBar.select(); + goDoCommand("cmd_copy"); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js b/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js new file mode 100644 index 0000000000..427a7419c8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js @@ -0,0 +1,228 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests ensures the urlbar is cleared properly when about:home is visited. + */ + +"use strict"; + +const { SessionSaver } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionSaver.sys.mjs" +); +const { TabStateFlusher } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/TabStateFlusher.sys.mjs" +); + +add_setup(function addHomeButton() { + CustomizableUI.addWidgetToArea("home-button", "nav-bar"); + registerCleanupFunction(() => + CustomizableUI.removeWidgetFromArea("home-button") + ); +}); + +/** + * Test what happens if loading a URL that should clear the + * location bar after a parent process URL. + */ +add_task(async function clearURLBarAfterParentProcessURL() { + let tab = await new Promise(resolve => { + gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:preferences" + ); + let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab); + newTabBrowser.addEventListener( + "Initialized", + async function () { + resolve(gBrowser.selectedTab); + }, + { capture: true, once: true } + ); + }); + document.getElementById("home-button").click(); + await BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + HomePage.get() + ); + is(gURLBar.value, "", "URL bar should be empty"); + is( + tab.linkedBrowser.userTypedValue, + null, + "The browser should have no recorded userTypedValue" + ); + BrowserTestUtils.removeTab(tab); +}); + +/** + * Same as above, but open the tab without passing the URL immediately + * which changes behaviour in tabbrowser.xml. + */ +add_task(async function clearURLBarAfterParentProcessURLInExistingTab() { + let tab = await new Promise(resolve => { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab); + newTabBrowser.addEventListener( + "Initialized", + async function () { + resolve(gBrowser.selectedTab); + }, + { capture: true, once: true } + ); + BrowserTestUtils.startLoadingURIString(newTabBrowser, "about:preferences"); + }); + document.getElementById("home-button").click(); + await BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + HomePage.get() + ); + is(gURLBar.value, "", "URL bar should be empty"); + is( + tab.linkedBrowser.userTypedValue, + null, + "The browser should have no recorded userTypedValue" + ); + BrowserTestUtils.removeTab(tab); +}); + +/** + * Load about:home directly from an about:newtab page. Because it is an + * 'initial' page, we need to treat this specially if the user actually + * loads a page like this from the URL bar. + */ +add_task(async function clearURLBarAfterManuallyLoadingAboutHome() { + let promiseTabOpenedAndSwitchedTo = BrowserTestUtils.switchTab( + gBrowser, + () => {} + ); + // This opens about:newtab: + BrowserOpenTab(); + let tab = await promiseTabOpenedAndSwitchedTo; + is(gURLBar.value, "", "URL bar should be empty"); + is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null"); + + gURLBar.value = "about:home"; + gURLBar.select(); + let aboutHomeLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "about:home" + ); + EventUtils.sendKey("return"); + await aboutHomeLoaded; + + is(gURLBar.value, "", "URL bar should be empty"); + is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null"); + BrowserTestUtils.removeTab(tab); +}); + +/** + * Ensure we don't show 'about:home' in the URL bar temporarily in new tabs + * while we're switching remoteness (when the URL we're loading and the + * default content principal are different). + */ +add_task(async function dontTemporarilyShowAboutHome() { + requestLongerTimeout(2); + let currentBrowser; + + await SpecialPowers.pushPrefEnv({ set: [["browser.startup.page", 1]] }); + let windowOpenedPromise = BrowserTestUtils.waitForNewWindow(); + let win = OpenBrowserWindow(); + await windowOpenedPromise; + let promiseTabSwitch = BrowserTestUtils.switchTab(win.gBrowser, () => {}); + win.BrowserOpenTab(); + await promiseTabSwitch; + currentBrowser = win.gBrowser.selectedBrowser; + is(win.gBrowser.visibleTabs.length, 2, "2 tabs opened"); + + // We need to load *something* here otherwise SessionStore will refuse to save this + // window when it closes as there is no user interaction, no tab history, and all the + // tab URIs are in the ignore list. + let loadPromise = BrowserTestUtils.browserLoaded( + currentBrowser, + false, + "about:logo" + ); + BrowserTestUtils.startLoadingURIString(currentBrowser, "about:logo"); + await loadPromise; + + let homeButton = win.document.getElementById("home-button"); + ok(BrowserTestUtils.isVisible(homeButton), "home-button is visible"); + + let changeListener; + let locationChangePromise = new Promise(resolve => { + changeListener = { + onLocationChange() { + is(win.gURLBar.value, "", "URL bar value should stay empty."); + resolve(); + }, + }; + win.gBrowser.addProgressListener(changeListener); + }); + homeButton.click(); + info("Waiting for location change to about:home"); + await locationChangePromise; + win.gBrowser.removeProgressListener(changeListener); + + await TabStateFlusher.flush(win.gBrowser.selectedBrowser); + await BrowserTestUtils.closeWindow(win); + ok(SessionStore.getClosedWindowCount(), "Should have a closed window"); + + await SessionSaver.run(); + + windowOpenedPromise = BrowserTestUtils.waitForNewWindow(); + win = SessionStore.undoCloseWindow(0); + await windowOpenedPromise; + let wpl = { + onLocationChange() { + is(win.gURLBar.value, "", "URL bar value should stay empty."); + }, + }; + win.gBrowser.addProgressListener(wpl); + + if (win.gBrowser.visibleTabs.length < 2) { + await BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen"); + } + let otherTab = win.gBrowser.selectedTab.previousElementSibling; + let tabLoaded = BrowserTestUtils.browserLoaded( + otherTab.linkedBrowser, + false, + "about:home" + ); + await BrowserTestUtils.switchTab(win.gBrowser, otherTab); + await tabLoaded; + win.gBrowser.removeProgressListener(wpl); + is(win.gURLBar.value, "", "URL bar value should be empty."); + + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Test that if the Home Button is clicked after a user has typed + * some value into the URL bar, that the URL bar is cleared if + * the homepage is one of the initial pages set. + */ +add_task(async function () { + await BrowserTestUtils.withNewTab( + { + url: "http://example.com", + gBrowser, + }, + async browser => { + const TYPED_VALUE = "This string should get cleared"; + gURLBar.value = TYPED_VALUE; + browser.userTypedValue = TYPED_VALUE; + + document.getElementById("home-button").click(); + await BrowserTestUtils.browserLoaded(browser, false, HomePage.get()); + is(gURLBar.value, "", "URL bar should be empty"); + is( + browser.userTypedValue, + null, + "The browser should have no recorded userTypedValue" + ); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js b/browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js new file mode 100644 index 0000000000..5ad8dfc75d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js @@ -0,0 +1,361 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests feedback and dismissal acknowledgments in the view. + */ + +"use strict"; + +// The command that dismisses a single result. +const DISMISS_ONE_COMMAND = "dismiss-one"; + +// The command that dismisses all results of a particular type. +const DISMISS_ALL_COMMAND = "dismiss-all"; + +// The name of this command must be one that's recognized as not ending the +// urlbar session. See `isSessionOngoing` comments for details. +const FEEDBACK_COMMAND = "show_less_frequently"; + +let gTestProvider; + +add_setup(async function () { + gTestProvider = new TestProvider({ + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url: "https://example.com/", + isBlockable: true, + blockL10n: { + id: "urlbar-result-menu-dismiss-firefox-suggest", + }, + } + ), + ], + }); + + gTestProvider.commandCount = {}; + UrlbarProvidersManager.registerProvider(gTestProvider); + + // Add a visit so that there's one result above the test result (the + // heuristic) and one below (the visit) just to make sure removing the test + // result doesn't mess up adjacent results. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await UrlbarTestUtils.formHistory.clear(); + await PlacesTestUtils.addVisits("https://example.com/aaa"); + + registerCleanupFunction(() => { + UrlbarProvidersManager.unregisterProvider(gTestProvider); + }); +}); + +// Tests dismissal acknowledgment when the dismissed row is not selected. +add_task(async function acknowledgeDismissal_rowNotSelected() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await doDismissTest({ + command: DISMISS_ONE_COMMAND, + shouldBeSelected: false, + }); +}); + +// Tests dismissal acknowledgment when the dismissed row is selected. +add_task(async function acknowledgeDismissal_rowSelected() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + + // Select the row. + let resultIndex = await getTestResultIndex(); + while (gURLBar.view.selectedRowIndex != resultIndex) { + this.EventUtils.synthesizeKey("KEY_ArrowDown"); + } + + await doDismissTest({ + resultIndex, + command: DISMISS_ONE_COMMAND, + shouldBeSelected: true, + }); +}); + +// Tests a feedback acknowledgment command immediately followed by a dismissal +// acknowledgment command. This makes sure that both feedback acknowledgment +// works and a subsequent dismissal command works while the urlbar session +// remains ongoing. +add_task(async function acknowledgeFeedbackAndDismissal() { + // Trigger the suggestion. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + + let resultIndex = await getTestResultIndex(); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + + // Click the feedback command. + await UrlbarTestUtils.openResultMenuAndClickItem(window, FEEDBACK_COMMAND, { + resultIndex, + }); + + Assert.equal( + gTestProvider.commandCount[FEEDBACK_COMMAND], + 1, + "One feedback command should have happened" + ); + gTestProvider.commandCount[FEEDBACK_COMMAND] = 0; + + Assert.ok( + gURLBar.view.isOpen, + "The view should remain open clicking the command" + ); + Assert.ok( + details.element.row.hasAttribute("feedback-acknowledgment"), + "Row should have feedback acknowledgment after clicking command" + ); + + info("Doing dismissal"); + await doDismissTest({ + resultIndex, + command: DISMISS_ONE_COMMAND, + shouldBeSelected: true, + }); +}); + +// Tests dismissal of all results of a particular type. +add_task(async function acknowledgeDismissal_all() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await doDismissTest({ + command: DISMISS_ALL_COMMAND, + shouldBeSelected: false, + }); +}); + +/** + * Does a dismissal test: + * + * 1. Clicks a dismiss command in the test result + * 2. Verifies a dismissal acknowledgment tip replaces the result + * 3. Clicks the "Got it" button in the tip + * 4. Verifies the tip is dismissed + * + * @param {object} options + * Options object + * @param {string} options.command + * One of: DISMISS_ONE_COMMAND, DISMISS_ALL_COMMAND + * @param {boolean} options.shouldBeSelected + * True if the test result is expected to be selected initially. If true, this + * function verifies the "Got it" button in the dismissal acknowledgment tip + * also becomes selected. + * @param {number} options.resultIndex + * The index of the test result, if known beforehand. Leave -1 to find it + * automatically. + */ +async function doDismissTest({ command, shouldBeSelected, resultIndex = -1 }) { + if (resultIndex < 0) { + resultIndex = await getTestResultIndex(); + } + + let selectedElement = gURLBar.view.selectedElement; + Assert.ok(selectedElement, "There should be an initially selected element"); + + if (shouldBeSelected) { + Assert.equal( + gURLBar.view.selectedRowIndex, + resultIndex, + "The test result should be selected" + ); + } else { + Assert.notEqual( + gURLBar.view.selectedRowIndex, + resultIndex, + "The test result should not be selected" + ); + } + + let resultCount = UrlbarTestUtils.getResultCount(window); + + // Click the command. + await UrlbarTestUtils.openResultMenuAndClickItem(window, command, { + resultIndex, + openByMouse: true, + }); + + Assert.equal( + gTestProvider.commandCount[command], + 1, + "One dismissal should have happened" + ); + gTestProvider.commandCount[command] = 0; + + // The row should be a tip now. + Assert.ok(gURLBar.view.isOpen, "The view should remain open after dismissal"); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + resultCount, + "The result count should not haved changed after dismissal" + ); + + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + Assert.equal( + details.type, + UrlbarUtils.RESULT_TYPE.TIP, + "Row should be a tip after dismissal" + ); + Assert.equal( + details.result.payload.type, + "dismissalAcknowledgment", + "Tip type should be dismissalAcknowledgment" + ); + Assert.equal( + details.displayed.title, + command == DISMISS_ONE_COMMAND + ? "Thanks for your feedback. You won’t see this suggestion again." + : "Thanks for your feedback. You won’t see these suggestions anymore.", + "Tip text should be correct for the dismiss type" + ); + Assert.ok( + !details.element.row.hasAttribute("selected"), + "Row should not have 'selected' attribute" + ); + Assert.ok( + !details.element.row._content.hasAttribute("selected"), + "Row-inner should not have 'selected' attribute" + ); + Assert.ok( + !details.element.row.hasAttribute("feedback-acknowledgment"), + "Row should not have feedback acknowledgment after dismissal" + ); + + // Get the dismissal acknowledgment's "Got it" button. + let gotItButton = UrlbarTestUtils.getButtonForResultIndex( + window, + "0", + resultIndex + ); + Assert.ok(gotItButton, "Row should have a 'Got it' button"); + + if (shouldBeSelected) { + Assert.equal( + gURLBar.view.selectedElement, + gotItButton, + "The 'Got it' button should be selected" + ); + } else { + Assert.notEqual( + gURLBar.view.selectedElement, + gotItButton, + "The 'Got it' button should not be selected" + ); + Assert.equal( + gURLBar.view.selectedElement, + selectedElement, + "The initially selected element should remain selected" + ); + } + + // Click it. + EventUtils.synthesizeMouseAtCenter(gotItButton, {}, window); + + // The view should remain open and the tip row should be gone. + Assert.ok( + gURLBar.view.isOpen, + "The view should remain open clicking the 'Got it' button" + ); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + resultCount - 1, + "The result count should be one less after clicking 'Got it' button" + ); + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.ok( + details.type != UrlbarUtils.RESULT_TYPE.TIP && + details.result.providerName != gTestProvider.name, + "Tip result and test result should not be present" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); +} + +/** + * A provider that acknowledges feedback and dismissals. + */ +class TestProvider extends UrlbarTestUtils.TestProvider { + getResultCommands(result) { + // The l10n values aren't important. + return [ + { + name: FEEDBACK_COMMAND, + l10n: { + id: "firefox-suggest-weather-command-inaccurate-location", + }, + }, + { + name: DISMISS_ONE_COMMAND, + l10n: { + id: "firefox-suggest-weather-command-not-interested", + }, + }, + { + name: DISMISS_ALL_COMMAND, + l10n: { + id: "firefox-suggest-weather-command-not-interested", + }, + }, + ]; + } + + onEngagement(state, queryContext, details, controller) { + if (details.result?.providerName == this.name) { + let { selType } = details; + + info(`onEngagement called, selType=` + selType); + + if (!this.commandCount.hasOwnProperty(selType)) { + this.commandCount[selType] = 0; + } + this.commandCount[selType]++; + + switch (selType) { + case FEEDBACK_COMMAND: + controller.view.acknowledgeFeedback(details.result); + break; + case DISMISS_ONE_COMMAND: + details.result.acknowledgeDismissalL10n = { + id: "firefox-suggest-dismissal-acknowledgment-one", + }; + controller.removeResult(details.result); + break; + case DISMISS_ALL_COMMAND: + details.result.acknowledgeDismissalL10n = { + id: "firefox-suggest-dismissal-acknowledgment-all", + }; + controller.removeResult(details.result); + break; + } + } + } +} + +async function getTestResultIndex() { + let index = 0; + let resultCount = UrlbarTestUtils.getResultCount(window); + for (; index < resultCount; index++) { + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + if (details.result.providerName == gTestProvider.name) { + break; + } + } + Assert.less(index, resultCount, "The test result should be present"); + return index; +} diff --git a/browser/components/urlbar/tests/browser/browser_action_searchengine.js b/browser/components/urlbar/tests/browser/browser_action_searchengine.js new file mode 100644 index 0000000000..2520315fa2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_action_searchengine.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that a search result has the correct attributes and visits the + * expected URL for the engine. + */ + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault", false], + ], + }); + + await SearchTestUtils.installSearchExtension( + { name: "MozSearch" }, + { setAsDefault: true } + ); + await SearchTestUtils.installSearchExtension({ + name: "MozSearchPrivate", + search_url: "https://example.com/private", + }); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +async function testSearch(win, expectedName, expectedBaseUrl) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "open a search", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Should have type search" + ); + Assert.deepEqual( + result.searchParams, + { + engine: expectedName, + keyword: undefined, + query: "open a search", + suggestion: undefined, + inPrivateWindow: undefined, + isPrivateEngine: undefined, + }, + "Should have the correct result parameters." + ); + + Assert.equal( + result.image, + UrlbarUtils.ICON.SEARCH_GLASS, + "Should have the search icon image" + ); + + let tabPromise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 0); + EventUtils.synthesizeMouseAtCenter(element, {}, win); + await tabPromise; + + Assert.equal( + win.gBrowser.selectedBrowser.currentURI.spec, + expectedBaseUrl + "?q=open+a+search", + "Should have loaded the correct page" + ); +} + +add_task(async function test_search_normal_window() { + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + + registerCleanupFunction(async function () { + try { + BrowserTestUtils.removeTab(tab); + } catch (ex) { + /* tab may have already been closed in case of failure */ + } + }); + + await testSearch(window, "MozSearch", "https://example.com/"); +}); + +add_task(async function test_search_private_window_no_separate_default() { + const win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + registerCleanupFunction(async function () { + await BrowserTestUtils.closeWindow(win); + }); + + await testSearch(win, "MozSearch", "https://example.com/"); +}); + +add_task(async function test_search_private_window() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.separatePrivateDefault", true]], + }); + + let engine = Services.search.getEngineByName("MozSearchPrivate"); + let originalEngine = await Services.search.getDefaultPrivate(); + await Services.search.setDefaultPrivate( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + registerCleanupFunction(async () => { + await BrowserTestUtils.closeWindow(win); + await Services.search.setDefaultPrivate( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + }); + + const win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + await testSearch(win, "MozSearchPrivate", "https://example.com/private"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js b/browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js new file mode 100644 index 0000000000..b79c324a04 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that search result obtained using a search keyword gives an entry with + * the correct attributes and visits the expected URL for the engine. + */ + +add_task(async function () { + await SearchTestUtils.installSearchExtension( + { keyword: "moz" }, + { setAsDefault: true } + ); + let engine = Services.search.getEngineByName("Example"); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + + // Disable autofill so mozilla.org isn't autofilled below. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", false]], + }); + + registerCleanupFunction(async function () { + try { + BrowserTestUtils.removeTab(tab); + } catch (ex) { + /* tab may have already been closed in case of failure */ + } + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "moz", + }); + Assert.equal(gURLBar.value, "moz", "Value should be unchanged"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "moz open a search", + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: engine.name, + entry: "typed", + }); + Assert.equal(gURLBar.value, "open a search", "value should be query"); + + let tabPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await tabPromise; + + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + "https://example.com/?q=open+a+search", + "Should have loaded the correct page" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_add_search_engine.js b/browser/components/urlbar/tests/browser/browser_add_search_engine.js new file mode 100644 index 0000000000..cfcaccfdd5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_add_search_engine.js @@ -0,0 +1,325 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test adding engines through the Address Bar context menu. + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); +const BASE_URL = + "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/"; + +add_task(async function context_none() { + info("Checks the context menu with a page that doesn't offer any engines."); + let url = "http://mochi.test:8888/"; + await BrowserTestUtils.withNewTab(url, async () => { + await UrlbarTestUtils.withContextMenu(window, popup => { + info("The separator and the add engine item should not be present."); + let elt = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(!!elt); + Assert.ok(!BrowserTestUtils.isVisible(elt)); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-0")); + }); + }); +}); + +add_task(async function context_one() { + info("Checks the context menu with a page that offers one engine."); + let url = getRootDirectory(gTestPath) + "add_search_engine_one.html"; + await BrowserTestUtils.withNewTab(url, async () => { + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine item should be present."); + let elt = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(BrowserTestUtils.isVisible(elt)); + + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-1")); + + elt = popup.parentNode.getMenuItem("add-engine-0"); + Assert.ok(BrowserTestUtils.isVisible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes("add_search_engine_0")); + Assert.ok(elt.hasAttribute("image")); + Assert.equal( + elt.getAttribute("uri"), + BASE_URL + "add_search_engine_0.xml" + ); + + info("Click on the menuitem"); + let enginePromise = promiseEngine("engine-added", "add_search_engine_0"); + popup.activateItem(elt); + await enginePromise; + Assert.equal(popup.state, "closed"); + }); + + await UrlbarTestUtils.withContextMenu(window, popup => { + info("The separator and the add engine item should not be present."); + let elt = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(!BrowserTestUtils.isVisible(elt)); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-0")); + }); + + info("Remove the engine."); + let engine = await Services.search.getEngineByName("add_search_engine_0"); + await Services.search.removeEngine(engine); + + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine item should be present again."); + let elt = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(BrowserTestUtils.isVisible(elt)); + + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-1")); + + elt = popup.parentNode.getMenuItem("add-engine-0"); + Assert.ok(BrowserTestUtils.isVisible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes("add_search_engine_0")); + }); + }); +}); + +add_task(async function context_invalid() { + info("Checks the context menu with a page that offers an invalid engine."); + await SpecialPowers.pushPrefEnv({ + set: [["prompts.contentPromptSubDialog", false]], + }); + + let url = getRootDirectory(gTestPath) + "add_search_engine_invalid.html"; + await BrowserTestUtils.withNewTab(url, async tab => { + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine item should be present."); + Assert.ok(popup.parentNode.getMenuItem("add-engine-separator")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-1")); + + let elt = popup.parentNode.getMenuItem("add-engine-0"); + Assert.ok(BrowserTestUtils.isVisible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes("add_search_engine_404")); + Assert.equal( + elt.getAttribute("uri"), + BASE_URL + "add_search_engine_404.xml" + ); + + info("Click on the menuitem"); + let promptPromise = PromptTestUtils.waitForPrompt(tab.linkedBrowser, { + modalType: Ci.nsIPromptService.MODAL_TYPE_CONTENT, + promptType: "alert", + }); + + popup.activateItem(elt); + + let prompt = await promptPromise; + Assert.ok( + prompt.ui.infoBody.textContent.includes( + BASE_URL + "add_search_engine_404.xml" + ), + "Should have included the url in the prompt body" + ); + await PromptTestUtils.handlePrompt(prompt); + Assert.equal(popup.state, "closed"); + }); + }); +}); + +add_task(async function context_same_name() { + info("Checks the context menu with a page that offers same named engines."); + let url = getRootDirectory(gTestPath) + "add_search_engine_same_names.html"; + await BrowserTestUtils.withNewTab(url, async () => { + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine item should be present."); + let elt = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(BrowserTestUtils.isVisible(elt)); + + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-1")); + + elt = popup.parentNode.getMenuItem("add-engine-0"); + Assert.ok(BrowserTestUtils.isVisible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes("add_search_engine_0")); + }); + }); +}); + +add_task(async function context_two() { + info("Checks the context menu with a page that offers two engines."); + let url = getRootDirectory(gTestPath) + "add_search_engine_two.html"; + await BrowserTestUtils.withNewTab(url, async () => { + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine item should be present."); + let elt = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(BrowserTestUtils.isVisible(elt)); + + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + + elt = popup.parentNode.getMenuItem("add-engine-0"); + Assert.ok(BrowserTestUtils.isVisible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes("add_search_engine_0")); + elt = popup.parentNode.getMenuItem("add-engine-1"); + Assert.ok(BrowserTestUtils.isVisible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes("add_search_engine_1")); + }); + }); +}); + +add_task(async function context_many() { + info("Checks the context menu with a page that offers many engines."); + let url = getRootDirectory(gTestPath) + "add_search_engine_many.html"; + await BrowserTestUtils.withNewTab(url, async () => { + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine menu should be present."); + let separator = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(BrowserTestUtils.isVisible(separator)); + + info("Engines should appear in sub menu"); + let menu = popup.parentNode.getMenuItem("add-engine-menu"); + Assert.ok(BrowserTestUtils.isVisible(menu)); + Assert.ok( + !menu.nextElementSibling + ?.getAttribute("anonid") + .startsWith("add-engine") + ); + Assert.ok(menu.hasAttribute("image"), "Menu should have an icon"); + Assert.ok( + !menu.label.includes("add-engine"), + "Menu should not contain an engine name" + ); + + info("Open the submenu"); + let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown"); + menu.openMenu(true); + await popupShown; + for (let i = 0; i < 4; ++i) { + let elt = popup.parentNode.getMenuItem(`add-engine-${i}`); + Assert.equal(elt.parentNode, menu.menupopup); + Assert.ok(BrowserTestUtils.isVisible(elt)); + } + + info("Click on the first engine to install it"); + let enginePromise = promiseEngine("engine-added", "add_search_engine_0"); + let elt = popup.parentNode.getMenuItem("add-engine-0"); + + elt.closest("menupopup").activateItem(elt); + await enginePromise; + Assert.equal(popup.state, "closed"); + }); + + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("Check the installed engine has been removed"); + // We're below the limit of engines for the menu now. + Assert.ok(!!popup.parentNode.getMenuItem("add-engine-separator")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + + for (let i = 0; i < 3; ++i) { + let elt = popup.parentNode.getMenuItem(`add-engine-${i}`); + Assert.equal(elt.parentNode, popup); + Assert.ok(BrowserTestUtils.isVisible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes(`add_search_engine_${i + 1}`)); + } + }); + + info("Remove the engine."); + let engine = await Services.search.getEngineByName("add_search_engine_0"); + await Services.search.removeEngine(engine); + + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine menu should be present."); + let separator = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(BrowserTestUtils.isVisible(separator)); + + info("Engines should appear in sub menu"); + let menu = popup.parentNode.getMenuItem("add-engine-menu"); + Assert.ok(BrowserTestUtils.isVisible(menu)); + Assert.ok( + !menu.nextElementSibling + ?.getAttribute("anonid") + .startsWith("add-engine") + ); + + info("Open the submenu"); + let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown"); + menu.openMenu(true); + await popupShown; + for (let i = 0; i < 4; ++i) { + let elt = popup.parentNode.getMenuItem(`add-engine-${i}`); + Assert.equal(elt.parentNode, menu.menupopup); + if ( + AppConstants.platform != "macosx" || + !Services.prefs.getBoolPref( + "widget.macos.native-context-menus", + false + ) + ) { + Assert.ok(BrowserTestUtils.isVisible(elt)); + } + } + }); + }); +}); + +add_task(async function context_after_customize() { + info("Checks the context menu after customization."); + let url = getRootDirectory(gTestPath) + "add_search_engine_one.html"; + await BrowserTestUtils.withNewTab(url, async () => { + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine item should be present."); + let elt = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(BrowserTestUtils.isVisible(elt)); + + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-1")); + + elt = popup.parentNode.getMenuItem("add-engine-0"); + Assert.ok(BrowserTestUtils.isVisible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes("add_search_engine_0")); + }); + + let promise = BrowserTestUtils.waitForEvent( + gNavToolbox, + "customizationready" + ); + gCustomizeMode.enter(); + await promise; + promise = BrowserTestUtils.waitForEvent(gNavToolbox, "aftercustomization"); + gCustomizeMode.exit(); + await promise; + + await UrlbarTestUtils.withContextMenu(window, async popup => { + info("The separator and the add engine item should be present."); + let elt = popup.parentNode.getMenuItem("add-engine-separator"); + Assert.ok(BrowserTestUtils.isVisible(elt)); + + Assert.ok(!popup.parentNode.getMenuItem("add-engine-menu")); + Assert.ok(!popup.parentNode.getMenuItem("add-engine-1")); + + elt = popup.parentNode.getMenuItem("add-engine-0"); + Assert.ok(BrowserTestUtils.isVisible(elt)); + await document.l10n.translateElements([elt]); + Assert.ok(elt.label.includes("add_search_engine_0")); + }); + }); +}); + +function promiseEngine(expectedData, expectedEngineName) { + info(`Waiting for engine ${expectedData}`); + return TestUtils.topicObserved( + "browser-search-engine-modified", + (engine, data) => { + info(`Got engine ${engine.wrappedJSObject.name} ${data}`); + return ( + expectedData == data && + expectedEngineName == engine.wrappedJSObject.name + ); + } + ).then(([engine, data]) => engine); +} diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_backspaced.js b/browser/components/urlbar/tests/browser/browser_autoFill_backspaced.js new file mode 100644 index 0000000000..6df60f6941 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_backspaced.js @@ -0,0 +1,272 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* This test ensures that backspacing autoFilled values still allows to + * confirm the remaining value. + */ + +"use strict"; + +async function test_autocomplete(data) { + let { desc, typed, autofilled, modified, keys, type, onAutoFill } = data; + info(desc); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typed, + fireInputEvent: true, + }); + Assert.equal(gURLBar.value, autofilled, "autofilled value is as expected"); + if (onAutoFill) { + onAutoFill(); + } + + info("Synthesizing keys"); + for (let key of keys) { + let args = Array.isArray(key) ? key : [key]; + EventUtils.synthesizeKey(...args); + } + await UrlbarTestUtils.promiseSearchComplete(window); + + Assert.equal( + gURLBar.value, + UrlbarTestUtils.trimURL(modified), + "backspaced value is as expected" + ); + + Assert.greater( + UrlbarTestUtils.getResultCount(window), + 0, + "Should get at least 1 result" + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + Assert.equal(result.type, type, "Should have the correct result type"); + + await UrlbarTestUtils.promisePopupClose(window); + gURLBar.blur(); +} + +add_task(async function () { + registerCleanupFunction(async function () { + Services.prefs.clearUserPref("browser.urlbar.autoFill"); + Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions"); + gURLBar.handleRevert(); + await PlacesUtils.history.clear(); + }); + Services.prefs.setBoolPref("browser.urlbar.autoFill", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false); + + await PlacesTestUtils.addVisits([ + "http://example.com/", + "http://example.com/foo", + ]); + // Bookmark the page so it ignores autofill threshold and doesn't risk to + // not be autofilled. + let bm = await PlacesUtils.bookmarks.insert({ + url: "http://example.com/", + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + }); + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.remove(bm); + }); + + await test_autocomplete({ + desc: "DELETE the autofilled part should search", + typed: "exam", + autofilled: "example.com/", + modified: "exam", + keys: ["KEY_Delete"], + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }); + await test_autocomplete({ + desc: "DELETE the final slash should visit", + typed: "example.com", + autofilled: "example.com/", + modified: "example.com", + keys: ["KEY_Delete"], + type: UrlbarUtils.RESULT_TYPE.URL, + }); + + await test_autocomplete({ + desc: "BACK_SPACE the autofilled part should search", + typed: "exam", + autofilled: "example.com/", + modified: "exam", + keys: ["KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }); + await test_autocomplete({ + desc: "BACK_SPACE the final slash should visit", + typed: "example.com", + autofilled: "example.com/", + modified: "example.com", + keys: ["KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.URL, + }); + + await test_autocomplete({ + desc: "DELETE the autofilled part, then BACK_SPACE, should search", + typed: "exam", + autofilled: "example.com/", + modified: "exa", + keys: ["KEY_Delete", "KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }); + await test_autocomplete({ + desc: "DELETE the final slash, then BACK_SPACE, should search", + typed: "example.com", + autofilled: "example.com/", + modified: "example.co", + keys: ["KEY_Delete", "KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.URL, + }); + + await test_autocomplete({ + desc: "BACK_SPACE the autofilled part, then BACK_SPACE, should search", + typed: "exam", + autofilled: "example.com/", + modified: "exa", + keys: ["KEY_Backspace", "KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }); + await test_autocomplete({ + desc: "BACK_SPACE the final slash, then BACK_SPACE, should search", + typed: "example.com", + autofilled: "example.com/", + modified: "example.co", + keys: ["KEY_Backspace", "KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.URL, + }); + + await test_autocomplete({ + desc: "BACK_SPACE after blur should search", + typed: "ex", + autofilled: "example.com/", + modified: "e", + keys: ["KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.SEARCH, + onAutoFill: () => { + gURLBar.blur(); + gURLBar.focus(); + Assert.equal( + gURLBar.selectionStart, + gURLBar.value.length, + "blur/focus should not change selection" + ); + Assert.equal( + gURLBar.selectionEnd, + gURLBar.value.length, + "blur/focus should not change selection" + ); + }, + }); + await test_autocomplete({ + desc: "DELETE after blur should search", + typed: "ex", + autofilled: "example.com/", + modified: "e", + keys: ["KEY_ArrowLeft", "KEY_Delete"], + type: UrlbarUtils.RESULT_TYPE.SEARCH, + onAutoFill: () => { + gURLBar.blur(); + gURLBar.focus(); + Assert.equal( + gURLBar.selectionStart, + gURLBar.value.length, + "blur/focus should not change selection" + ); + Assert.equal( + gURLBar.selectionEnd, + gURLBar.value.length, + "blur/focus should not change selection" + ); + }, + }); + await test_autocomplete({ + desc: "double BACK_SPACE after blur should search", + typed: "exa", + autofilled: "example.com/", + modified: "e", + keys: ["KEY_Backspace", "KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.SEARCH, + onAutoFill: () => { + gURLBar.blur(); + gURLBar.focus(); + Assert.equal( + gURLBar.selectionStart, + gURLBar.value.length, + "blur/focus should not change selection" + ); + Assert.equal( + gURLBar.selectionEnd, + gURLBar.value.length, + "blur/focus should not change selection" + ); + }, + }); + + await test_autocomplete({ + desc: "Right arrow key and then backspace should delete the backslash and not re-trigger autofill", + typed: "ex", + autofilled: "example.com/", + modified: "example.com", + keys: ["KEY_ArrowRight", "KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.URL, + }); + + await test_autocomplete({ + desc: "Right arrow key, selecting the last few characters using the keyboard, and then backspace should delete the characters and not re-trigger autofill", + typed: "ex", + autofilled: "example.com/", + modified: "example.c", + keys: [ + "KEY_ArrowRight", + ["KEY_ArrowLeft", { shiftKey: true }], + ["KEY_ArrowLeft", { shiftKey: true }], + ["KEY_ArrowLeft", { shiftKey: true }], + "KEY_Backspace", + ], + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }); + + await test_autocomplete({ + desc: "End and then backspace should delete the backslash and not re-trigger autofill", + typed: "ex", + autofilled: "example.com/", + modified: "example.com", + keys: [ + AppConstants.platform == "macosx" + ? ["KEY_ArrowRight", { metaKey: true }] + : "KEY_End", + "KEY_Backspace", + ], + type: UrlbarUtils.RESULT_TYPE.URL, + }); + + await test_autocomplete({ + desc: "Clicking in the input after the text and then backspace should delete the backslash and not re-trigger autofill", + typed: "ex", + autofilled: "example.com/", + modified: "example.com", + keys: ["KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.URL, + onAutoFill: () => { + // This assumes that the center of the input is to the right of the end + // of the text, so the caret is placed at the end of the text on click. + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }, + }); + + await test_autocomplete({ + desc: "Selecting the next result and then backspace should delete the last character and not re-trigger autofill", + typed: "ex", + autofilled: "example.com/", + modified: "http://example.com/fo", + keys: ["KEY_ArrowDown", "KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.URL, + }); + + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_canonize.js b/browser/components/urlbar/tests/browser/browser_autoFill_canonize.js new file mode 100644 index 0000000000..fec11a9c8f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_canonize.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* This test ensures that pressing ctrl+enter bypasses the autoFilled + * value, and only considers what the user typed (but not just enter). + */ + +async function test_autocomplete(data) { + let { desc, typed, autofilled, modified, waitForUrl, keys } = data; + info(desc); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typed, + }); + Assert.equal(gURLBar.value, autofilled, "autofilled value is as expected"); + + let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt( + waitForUrl, + gBrowser.selectedBrowser + ); + + keys.forEach(([key, mods]) => EventUtils.synthesizeKey(key, mods)); + + Assert.equal(gURLBar.value, modified, "value is as expected"); + + await promiseLoad; + gURLBar.blur(); +} + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", true]], + }); + registerCleanupFunction(async function () { + gURLBar.handleRevert(); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + }); + + // Add a typed visit, so it will be autofilled. + await PlacesTestUtils.addVisits({ + uri: "https://example.com/", + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + await test_autocomplete({ + desc: "ENTER on the autofilled part should use autofill", + typed: "exam", + autofilled: "example.com/", + modified: UrlbarTestUtils.trimURL("https://example.com"), + waitForUrl: "https://example.com/", + keys: [["KEY_Enter"]], + }); + + await test_autocomplete({ + desc: "CTRL+ENTER on the autofilled part should bypass autofill", + typed: "exam", + autofilled: "example.com/", + modified: UrlbarTestUtils.trimURL("https://www.exam.com"), + waitForUrl: "https://www.exam.com/", + keys: [["KEY_Enter", { ctrlKey: true }]], + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_caretNotAtEnd.js b/browser/components/urlbar/tests/browser/browser_autoFill_caretNotAtEnd.js new file mode 100644 index 0000000000..23382a70da --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_caretNotAtEnd.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function noAutofillWhenCaretNotAtEnd() { + gURLBar.focus(); + + // Add a visit that can be autofilled. + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/", + }, + ]); + + // Fill the input with xample. + gURLBar.value = "xample"; + + // Move the caret to the beginning and type e. + gURLBar.selectionStart = 0; + gURLBar.selectionEnd = 0; + EventUtils.sendString("e"); + + // Check the first result and input. + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(!result.autofill, "The first result should not be autofill"); + + Assert.equal(gURLBar.value, "example"); + Assert.equal(gURLBar.selectionStart, 1); + Assert.equal(gURLBar.selectionEnd, 1); + + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_clear_properly_on_accent_char.js b/browser/components/urlbar/tests/browser/browser_autoFill_clear_properly_on_accent_char.js new file mode 100644 index 0000000000..a65da338a8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_clear_properly_on_accent_char.js @@ -0,0 +1,185 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + await cleanUp(); +}); + +add_task(async function test_autoFill_clear_properly_on_accent_char() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "", + url: "https://example.com", + }); + + await search({ + searchString: "e", + valueBefore: "e", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + // Simulate macos accent character insertion. First the character is selected and + // then replaced by the accentuated character. + gURLBar.selectionStart = 0; + gURLBar.selectionEnd = 1; + EventUtils.sendChar("è", window); + + await UrlbarTestUtils.promiseSearchComplete(window); + + is(gURLBar.value, "è", "No auto complete for accent char."); + + await cleanUp(); +}); + +add_task(async function dont_clear_placeholder_if_autofill_accepted() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "", + url: "https://abc.yz", + }); + + let selectionChangedPromise = waitForSelectionChange({ times: 2 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "abc", + }); + await UrlbarTestUtils.promiseSearchComplete(window); + + // PromiseAutoCompleteResultPopup fires one input event and two + // selectionchange events. If we don't wait for them to be fired before + // entering navigation keys, the selection gets messed up. + await selectionChangedPromise; + + Assert.equal(gURLBar.value, "abc.yz/", "autofilled value is as expected"); + info("Synthesizing keys"); + await sendNavigationKey("KEY_ArrowRight"); + await sendNavigationKey("KEY_ArrowLeft"); + await sendNavigationKey("KEY_ArrowLeft"); + await sendNavigationKey("KEY_ArrowLeft"); + + EventUtils.sendChar("x"); + is(gURLBar.value, "abc.xyz/", "No auto complete for accent char."); + + await cleanUp(); +}); + +add_task(async function dont_clear_placeholder_after_selection_change() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "", + url: "https://mozilla.org/", + }); + + let userTypedValue = "mo"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: userTypedValue, + }); + + Assert.equal( + gURLBar.value, + "mozilla.org/", + "autofilled value is as expected" + ); + + info("Simulate mouse click to change caret position."); + let selectionChangedPromise = waitForSelectionChange(); + is( + gURLBar.selectionStart, + userTypedValue.length, + " SelectionStart at the beginning of the placeholder" + ); + is( + gURLBar.selectionEnd, + gURLBar.value.length, + " Selection at the end of the placeholder" + ); + gURLBar.selectionStart = 1; + gURLBar.selectionEnd = 1; + + await selectionChangedPromise; + await UrlbarTestUtils.promiseSearchComplete(window); + + EventUtils.sendChar("o", window); + + await UrlbarTestUtils.promiseSearchComplete(window); + + is( + gURLBar.value, + "moozilla.org/", + "Autofill was not cleared and new character was inserted." + ); + + await cleanUp(); +}); + +add_task(async function modify_autofilled_selection() { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "", + url: "https://developer.mozilla.org/en-US/", + }); + + let userTypedValue = "d"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: userTypedValue, + }); + + Assert.equal( + gURLBar.value, + "developer.mozilla.org/", + "autofilled value is as expected" + ); + await sendNavigationKey("KEY_ArrowDown"); + + let selectionChangedPromise = waitForSelectionChange(); + gURLBar.selectionStart = gURLBar.value.length - 6; + gURLBar.selectionEnd = gURLBar.value.length - 1; + + await selectionChangedPromise; + await UrlbarTestUtils.promiseSearchComplete(window); + + EventUtils.sendChar("j", window); + + await UrlbarTestUtils.promiseSearchComplete(window); + is( + gURLBar.value, + UrlbarTestUtils.trimURL("https://developer.mozilla.org/j/"), + "gURLBar contains correct modified autofilled value" + ); +}); + +async function cleanUp() { + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +} + +async function sendNavigationKey(key) { + let selectionChangePromise = waitForSelectionChange(); + EventUtils.synthesizeKey(key); + await selectionChangePromise; +} + +async function waitForSelectionChange(options = { times: 1 }) { + let observedSelectionChanges = 0; + + function handler(event, resolve) { + observedSelectionChanges += 1; + if (observedSelectionChanges == options.times) { + resolve(); + } + } + + await new Promise(resolve => { + gURLBar.addEventListener("selectionchange", event => + handler(event, resolve) + ); + }); + + gURLBar.removeEventListener("selectionchange", handler); +} diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_firstResult.js b/browser/components/urlbar/tests/browser/browser_autoFill_firstResult.js new file mode 100644 index 0000000000..ba7ef20df6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_firstResult.js @@ -0,0 +1,201 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test makes sure that autofilling the first result of a new search works +// correctly: autofill happens when it should and doesn't when it shouldn't. + +"use strict"; + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits(["http://example.com/"]); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + // Disable placeholder completion. The point of this test is to make sure the + // first result is autofilled (or not) correctly. Autofilling the placeholder + // before the search starts interferes with that. + gURLBar._enableAutofillPlaceholder = false; + registerCleanupFunction(async () => { + gURLBar._enableAutofillPlaceholder = true; + }); +}); + +// The first result should be autofilled when all conditions are met. This also +// does a sanity check to make sure that placeholder autofill is correctly +// disabled, which is helpful for all tasks here and is why this one is first. +add_task(async function successfulAutofill() { + // Do a simple search that should autofill. This will also set up the + // autofill placeholder string, which next we make sure is *not* autofilled. + await doInitialAutofillSearch(); + + // As a sanity check, do another search to make sure the placeholder is *not* + // autofilled. Make sure it's not autofilled by checking the input value and + // selection *before* the search completes. If placeholder autofill was not + // correctly disabled, then these assertions will fail. + + gURLBar.value = "exa"; + UrlbarTestUtils.fireInputEvent(window); + + // before the search completes: no autofill + Assert.equal(gURLBar.value, "exa"); + Assert.equal(gURLBar.selectionStart, "exa".length); + Assert.equal(gURLBar.selectionEnd, "exa".length); + + await UrlbarTestUtils.promiseSearchComplete(window); + + // after the search completes: successful autofill + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "exa".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); +}); + +// The first result should not be autofilled when it's not an autofill result. +add_task(async function firstResultNotAutofill() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(!details.autofill); + Assert.equal(gURLBar.value, "foo"); + Assert.equal(gURLBar.selectionStart, "foo".length); + Assert.equal(gURLBar.selectionEnd, "foo".length); +}); + +// The first result should *not* be autofilled when the placeholder is not +// selected, the selection is empty, and the caret is *not* at the end of the +// search string. +add_task(async function caretNotAtEndOfSearchString() { + // To set up the placeholder, do an initial search that triggers autofill. + await doInitialAutofillSearch(); + + // Now do another search but set the caret to somewhere else besides the end + // of the new search string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + selectionStart: "exa".length, + selectionEnd: "exa".length, + fireInputEvent: false, + }); + + // The first result should be an autofill result, but it should not have been + // autofilled. + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "exam"); + Assert.equal(gURLBar.selectionStart, "exa".length); + Assert.equal(gURLBar.selectionEnd, "exa".length); + + await cleanUp(); +}); + +// The first result should *not* be autofilled when the placeholder is not +// selected, the selection is *not* empty, and the caret is at the end of the +// search string. +add_task(async function selectionNotEmpty() { + // To set up the placeholder, do an initial search that triggers autofill. + await doInitialAutofillSearch(); + + // Now do another search. Set the selection end at the end of the search + // string, but make the selection non-empty. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + selectionStart: "exa".length, + selectionEnd: "exam".length, + }); + + // The first result should be an autofill result, but it should not have been + // autofilled. + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "exam"); + Assert.equal(gURLBar.selectionStart, "exa".length); + Assert.equal(gURLBar.selectionEnd, "exam".length); + + await cleanUp(); +}); + +// The first result should be autofilled when the placeholder is not selected, +// the selection is empty, and the caret is at the end of the search string. +add_task(async function successfulAutofillAfterSettingPlaceholder() { + // To set up the placeholder, do an initial search that triggers autofill. + await doInitialAutofillSearch(); + + // Now do another search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + selectionStart: "exam".length, + selectionEnd: "exam".length, + }); + + // It should be autofilled. + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "exam".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + await cleanUp(); +}); + +// The first result should be autofilled when the placeholder *is* selected -- +// more precisely, when the portion of the placeholder after the new search +// string is selected. +add_task(async function successfulAutofillPlaceholderSelected() { + // To set up the placeholder, do an initial search that triggers autofill. + await doInitialAutofillSearch(); + + // Now do another search and select the portion of the placeholder after the + // new search string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + selectionStart: "exam".length, + selectionEnd: "example.com/".length, + }); + + // It should be autofilled. + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "exam".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + await cleanUp(); +}); + +async function doInitialAutofillSearch() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ex", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "ex".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); +} + +async function cleanUp() { + // In some cases above, a test task searches for "exam" at the end, and then + // the next task searches for "ex". Autofill results will not be allowed in + // the next task in that case since the old search string starts with the new + // search string. To prevent one task from interfering with the next, do a + // search that changes the search string. Also close the popup while we're + // here, although that's not really necessary. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "reset last search string", + }); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_paste.js b/browser/components/urlbar/tests/browser/browser_autoFill_paste.js new file mode 100644 index 0000000000..7e0d76c8cc --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_paste.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test checks we don't autofill on paste. + +"use strict"; + +add_task(async function test() { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits(["http://example.com/"]); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); + + // Search for "e". It should autofill to example.com/. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "e", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "e".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + // Now paste. + await selectAndPaste("ex"); + + // Nothing should have been autofilled. + await UrlbarTestUtils.promiseSearchComplete(window); + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(!details.autofill); + Assert.equal(gURLBar.value, "ex"); + Assert.equal(gURLBar.selectionStart, "ex".length); + Assert.equal(gURLBar.selectionEnd, "ex".length); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_placeholder.js b/browser/components/urlbar/tests/browser/browser_autoFill_placeholder.js new file mode 100644 index 0000000000..fd475f31c6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_placeholder.js @@ -0,0 +1,894 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test makes sure that the autofill placeholder value is autofilled +// correctly. The placeholder is a string that we immediately autofill when a +// search starts and before its first result arrives in order to prevent text +// flicker in the input. +// +// Because this test specifically checks autofill *before* searches complete, we +// can't use promiseAutocompleteResultPopup() or other helpers that wait for +// searches to complete. Instead the test uses fireInputEvent() to trigger +// placeholder autofill and then immediately checks autofill status. + +"use strict"; + +// Allow more time for verify mode. +requestLongerTimeout(5); + +add_setup(async function () { + await cleanUp(); +}); + +// Basic origin autofill test. +add_task(async function origin() { + await addVisits("http://example.com/"); + + await search({ + searchString: "ex", + valueBefore: "ex", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + await search({ + searchString: "exa", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + await search({ + searchString: "EXAM", + valueBefore: "EXAMple.com/", + valueAfter: "EXAMple.com/", + placeholderAfter: "EXAMple.com/", + }); + await search({ + searchString: "eXaMp", + valueBefore: "eXaMple.com/", + valueAfter: "eXaMple.com/", + placeholderAfter: "eXaMple.com/", + }); + await search({ + searchString: "exampL", + valueBefore: "exampLe.com/", + valueAfter: "exampLe.com/", + placeholderAfter: "exampLe.com/", + }); + await search({ + searchString: "example.com", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + await search({ + searchString: "example.com/", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + await cleanUp(); +}); + +// Basic URL autofill test. +add_task(async function url() { + await addVisits("http://example.com/aaa/bbb/ccc"); + + await search({ + searchString: "example.com/a", + valueBefore: "example.com/a", + valueAfter: "example.com/aaa/", + placeholderAfter: "example.com/aaa/", + }); + await search({ + searchString: "EXAmple.com/aA", + valueBefore: "EXAmple.com/aAa/", + valueAfter: "EXAmple.com/aAa/", + placeholderAfter: "EXAmple.com/aAa/", + }); + await search({ + searchString: "example.com/aAa", + valueBefore: "example.com/aAa/", + valueAfter: "example.com/aAa/", + placeholderAfter: "example.com/aAa/", + }); + await search({ + searchString: "example.com/aaa/", + valueBefore: "example.com/aaa/", + valueAfter: "example.com/aaa/", + placeholderAfter: "example.com/aaa/", + }); + await search({ + searchString: "example.com/aaa/b", + valueBefore: "example.com/aaa/b", + valueAfter: "example.com/aaa/bbb/", + placeholderAfter: "example.com/aaa/bbb/", + }); + await search({ + searchString: "example.com/aAa/bB", + valueBefore: "example.com/aAa/bBb/", + valueAfter: "example.com/aAa/bBb/", + placeholderAfter: "example.com/aAa/bBb/", + }); + await search({ + searchString: "example.com/aAa/bBb", + valueBefore: "example.com/aAa/bBb/", + valueAfter: "example.com/aAa/bBb/", + placeholderAfter: "example.com/aAa/bBb/", + }); + await search({ + searchString: "example.com/aaa/bbb/", + valueBefore: "example.com/aaa/bbb/", + valueAfter: "example.com/aaa/bbb/", + placeholderAfter: "example.com/aaa/bbb/", + }); + await search({ + searchString: "example.com/aaa/bbb/c", + valueBefore: "example.com/aaa/bbb/c", + valueAfter: "example.com/aaa/bbb/ccc", + placeholderAfter: "example.com/aaa/bbb/ccc", + }); + await search({ + searchString: "example.com/aAa/bBb/cC", + valueBefore: "example.com/aAa/bBb/cCc", + valueAfter: "example.com/aAa/bBb/cCc", + placeholderAfter: "example.com/aAa/bBb/cCc", + }); + await search({ + searchString: "example.com/aaa/bbb/ccc", + valueBefore: "example.com/aaa/bbb/ccc", + valueAfter: "example.com/aaa/bbb/ccc", + placeholderAfter: "example.com/aaa/bbb/ccc", + }); + + await cleanUp(); +}); + +// Basic adaptive history autofill test. +add_task(async function adaptiveHistory() { + UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", true); + + await addVisits("http://example.com/test"); + await UrlbarUtils.addToInputHistory("http://example.com/test", "exa"); + + await search({ + searchString: "exa", + valueBefore: "exa", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + await search({ + searchString: "EXAM", + valueBefore: "EXAMple.com/test", + valueAfter: "EXAMple.com/test", + placeholderAfter: "EXAMple.com/test", + }); + await search({ + searchString: "eXaMpLe", + valueBefore: "eXaMpLe.com/test", + valueAfter: "eXaMpLe.com/test", + placeholderAfter: "eXaMpLe.com/test", + }); + await search({ + searchString: "example.", + valueBefore: "example.com/test", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + await search({ + searchString: "example.c", + valueBefore: "example.com/test", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + await search({ + searchString: "example.com", + valueBefore: "example.com/test", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + await search({ + searchString: "example.com/", + valueBefore: "example.com/test", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + await search({ + searchString: "example.com/T", + valueBefore: "example.com/Test", + valueAfter: "example.com/Test", + placeholderAfter: "example.com/Test", + }); + await search({ + searchString: "eXaMple.com/tE", + valueBefore: "eXaMple.com/tEst", + valueAfter: "eXaMple.com/tEst", + placeholderAfter: "eXaMple.com/tEst", + }); + await search({ + searchString: "example.com/tes", + valueBefore: "example.com/test", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + await search({ + searchString: "example.com/test", + valueBefore: "example.com/test", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + + UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled"); + await cleanUp(); +}); + +// Search engine token alias test (aliases that start with "@"). +add_task(async function tokenAlias() { + // We have built-in engine aliases that may conflict with the one we choose + // here in terms of autofill, so be careful and choose a weird alias. + await SearchTestUtils.installSearchExtension({ keyword: "@__example" }); + + await search({ + searchString: "@__ex", + valueBefore: "@__ex", + valueAfter: "@__example ", + placeholderAfter: "@__example ", + }); + await search({ + searchString: "@__exa", + valueBefore: "@__example ", + valueAfter: "@__example ", + placeholderAfter: "@__example ", + }); + await search({ + searchString: "@__EXAM", + valueBefore: "@__EXAMple ", + valueAfter: "@__EXAMple ", + placeholderAfter: "@__EXAMple ", + }); + await search({ + searchString: "@__eXaMp", + valueBefore: "@__eXaMple ", + valueAfter: "@__eXaMple ", + placeholderAfter: "@__eXaMple ", + }); + await search({ + searchString: "@__exampl", + valueBefore: "@__example ", + valueAfter: "@__example ", + placeholderAfter: "@__example ", + }); + + await cleanUp(); +}); + +// The placeholder should not be used for a search that does not autofill, and +// it should be cleared after the search completes. +add_task(async function noAutofill() { + await addVisits("http://example.com/"); + + // Do an initial search that triggers autofill so that the placeholder has an + // initial value. + await search({ + searchString: "ex", + valueBefore: "ex", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + // Search with a string that does not match the placeholder. Placeholder + // autofill shouldn't happen. + await search({ + searchString: "moz", + valueBefore: "moz", + valueAfter: "moz", + placeholderAfter: "", + }); + + // Search for "ex" again. It should be autofilled when the search completes + // but the placeholder will not be autofilled. + await search({ + searchString: "ex", + valueBefore: "ex", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + // Continue with a series of searches that should all use the placeholder. + await search({ + searchString: "exa", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + await search({ + searchString: "EXAM", + valueBefore: "EXAMple.com/", + valueAfter: "EXAMple.com/", + placeholderAfter: "EXAMple.com/", + }); + await search({ + searchString: "eXaMp", + valueBefore: "eXaMple.com/", + valueAfter: "eXaMple.com/", + placeholderAfter: "eXaMple.com/", + }); + await search({ + searchString: "exampl", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + await cleanUp(); +}); + +// The placeholder should not be used for a search that autofills a different +// value. +add_task(async function differentAutofill() { + await addVisits("http://mozilla.org/", "http://example.com/"); + + // Do an initial search that triggers autofill so that the placeholder has an + // initial value. + await search({ + searchString: "moz", + valueBefore: "moz", + valueAfter: "mozilla.org/", + placeholderAfter: "mozilla.org/", + }); + + // Search with a string that does not match the placeholder but does trigger + // autofill. Placeholder autofill shouldn't happen. + await search({ + searchString: "ex", + valueBefore: "ex", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + // Continue with a series of searches that should all use the placeholder. + await search({ + searchString: "exa", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + await search({ + searchString: "EXAm", + valueBefore: "EXAmple.com/", + valueAfter: "EXAmple.com/", + placeholderAfter: "EXAmple.com/", + }); + + // Search for "moz" again. It should be autofilled. Placeholder autofill + // shouldn't happen. + await search({ + searchString: "moz", + valueBefore: "moz", + valueAfter: "mozilla.org/", + placeholderAfter: "mozilla.org/", + }); + + // Continue with a series of searches that should all use the placeholder. + await search({ + searchString: "mozi", + valueBefore: "mozilla.org/", + valueAfter: "mozilla.org/", + placeholderAfter: "mozilla.org/", + }); + await search({ + searchString: "MOZil", + valueBefore: "MOZilla.org/", + valueAfter: "MOZilla.org/", + placeholderAfter: "MOZilla.org/", + }); + + await cleanUp(); +}); + +// The placeholder should not be used for a search that uses a bookmark keyword +// even when the keyword matches the placeholder, and the placeholder should be +// cleared after the search completes. +add_task(async function bookmarkKeyword() { + // Add a visit to example.com. + await addVisits("https://example.com/"); + + // Add a bookmark keyword that is a prefix of example.com. + await PlacesUtils.keywords.insert({ + keyword: "ex", + url: "https://somekeyword.com/", + }); + + // Do an initial search that triggers autofill for the visit so that the + // placeholder has an initial value of "example.com/". + await search({ + searchString: "e", + valueBefore: "e", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + // Do a search that matches the bookmark keyword. The placeholder from the + // search above should be autofilled since the autofill placeholder + // ("example.com/") starts with the keyword ("ex"), but then when the bookmark + // result arrives, the autofilled value and placeholder should be cleared. + await search({ + searchString: "ex", + valueBefore: "example.com/", + valueAfter: "ex", + placeholderAfter: "", + }); + + // Do another search that simulates the user continuing to type "example". No + // placeholder should be autofilled, but once the autofill result arrives for + // the visit, "example.com/" should be autofilled. + await search({ + searchString: "exa", + valueBefore: "exa", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + await PlacesUtils.keywords.remove("ex"); + await cleanUp(); +}); + +// The placeholder should not be used for a search that doesn't match its URI +// fragment. This task uses a URL whose path is "/". +add_task(async function noURIFragmentMatch1() { + await addVisits("https://example.com/#TEST"); + + const testData = [ + { + desc: "Autofill example.com/#TEST then search for example.com/#Te", + searches: [ + { + searchString: "example.com/#T", + valueBefore: "example.com/#T", + valueAfter: "example.com/#TEST", + placeholderAfter: "example.com/#TEST", + }, + { + searchString: "example.com/#Te", + valueBefore: "example.com/#Te", + valueAfter: "example.com/#Te", + placeholderAfter: "", + }, + ], + }, + { + desc: "Autofill https://example.com/#TEST then search for https://example.com/#Te", + searches: [ + { + searchString: "https://example.com/#T", + valueBefore: "https://example.com/#T", + valueAfter: "https://example.com/#TEST", + placeholderAfter: "https://example.com/#TEST", + }, + { + searchString: "https://example.com/#Te", + valueBefore: "https://example.com/#Te", + valueAfter: "https://example.com/#Te", + placeholderAfter: "", + }, + ], + }, + { + desc: "Autofill example.com/#TEST then search for example.com/", + searches: [ + { + searchString: "example.com/#T", + valueBefore: "example.com/#T", + valueAfter: "example.com/#TEST", + placeholderAfter: "example.com/#TEST", + }, + { + searchString: "example.com/", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }, + ], + }, + ]; + + for (const { desc, searches } of testData) { + info("Running subtest: " + desc); + + for (let i = 0; i < searches.length; i++) { + info("Doing search at index " + i); + await search(searches[i]); + } + + // Clear the placeholder for the next subtest. + info("Doing extra search to clear placeholder"); + await search({ + searchString: "no match", + valueBefore: "no match", + valueAfter: "no match", + placeholderAfter: "", + }); + } + + await cleanUp(); +}); + +// The placeholder should not be used for a search that doesn't match its URI +// fragment. This task uses a URL whose path is "/foo". +add_task(async function noURIFragmentMatch2() { + await addVisits("https://example.com/foo#TEST"); + + const testData = [ + { + desc: "Autofill example.com/foo#TEST then search for example.com/foo#Te", + searches: [ + { + searchString: "example.com/foo#T", + valueBefore: "example.com/foo#T", + valueAfter: "example.com/foo#TEST", + placeholderAfter: "example.com/foo#TEST", + }, + { + searchString: "example.com/foo#Te", + valueBefore: "example.com/foo#Te", + valueAfter: "example.com/foo#Te", + placeholderAfter: "", + }, + ], + }, + { + desc: "Autofill https://example.com/foo#TEST then search for https://example.com/foo#Te", + searches: [ + { + searchString: "https://example.com/foo#T", + valueBefore: "https://example.com/foo#T", + valueAfter: "https://example.com/foo#TEST", + placeholderAfter: "https://example.com/foo#TEST", + }, + { + searchString: "https://example.com/foo#Te", + valueBefore: "https://example.com/foo#Te", + valueAfter: "https://example.com/foo#Te", + placeholderAfter: "", + }, + ], + }, + { + desc: "Autofill example.com/foo#TEST then search for example.com/", + searches: [ + { + searchString: "example.com/foo#T", + valueBefore: "example.com/foo#T", + valueAfter: "example.com/foo#TEST", + placeholderAfter: "example.com/foo#TEST", + }, + { + searchString: "example.com/", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }, + ], + }, + ]; + + for (const { desc, searches } of testData) { + info("Running subtest: " + desc); + + for (let i = 0; i < searches.length; i++) { + info("Doing search at index " + i); + await search(searches[i]); + } + + // Clear the placeholder for the next subtest. + info("Doing extra search to clear placeholder"); + await search({ + searchString: "no match", + valueBefore: "no match", + valueAfter: "no match", + placeholderAfter: "", + }); + } + + await cleanUp(); +}); + +// The placeholder should not be used for a search that does not autofill its +// URL path. +add_task(async function noPathMatch() { + await addVisits("http://example.com/shallow/deep/file"); + + const testData = [ + { + desc: "Autofill example.com/shallow/ then search for exam", + searches: [ + { + searchString: "example.com/s", + valueBefore: "example.com/s", + valueAfter: "example.com/shallow/", + placeholderAfter: "example.com/shallow/", + }, + { + searchString: "exam", + valueBefore: "exam", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/ then search for example.com/", + searches: [ + { + searchString: "example.com/s", + valueBefore: "example.com/s", + valueAfter: "example.com/shallow/", + placeholderAfter: "example.com/shallow/", + }, + { + searchString: "example.com/", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/ then search for exam", + searches: [ + { + searchString: "example.com/shallow/d", + valueBefore: "example.com/shallow/d", + valueAfter: "example.com/shallow/deep/", + placeholderAfter: "example.com/shallow/deep/", + }, + { + searchString: "exam", + valueBefore: "exam", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/ then search for example.com/", + searches: [ + { + searchString: "example.com/shallow/d", + valueBefore: "example.com/shallow/d", + valueAfter: "example.com/shallow/deep/", + placeholderAfter: "example.com/shallow/deep/", + }, + { + searchString: "example.com/", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/ then search for example.com/s", + searches: [ + { + searchString: "example.com/shallow/d", + valueBefore: "example.com/shallow/d", + valueAfter: "example.com/shallow/deep/", + placeholderAfter: "example.com/shallow/deep/", + }, + { + searchString: "example.com/s", + valueBefore: "example.com/s", + valueAfter: "example.com/shallow/", + placeholderAfter: "example.com/shallow/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/ then search for example.com/shallow/", + searches: [ + { + searchString: "example.com/shallow/d", + valueBefore: "example.com/shallow/d", + valueAfter: "example.com/shallow/deep/", + placeholderAfter: "example.com/shallow/deep/", + }, + { + searchString: "example.com/shallow/", + valueBefore: "example.com/shallow/", + valueAfter: "example.com/shallow/", + placeholderAfter: "example.com/shallow/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/file then search for exam", + searches: [ + { + searchString: "example.com/shallow/deep/f", + valueBefore: "example.com/shallow/deep/f", + valueAfter: "example.com/shallow/deep/file", + placeholderAfter: "example.com/shallow/deep/file", + }, + { + searchString: "exam", + valueBefore: "exam", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/file then search for example.com/", + searches: [ + { + searchString: "example.com/shallow/deep/f", + valueBefore: "example.com/shallow/deep/f", + valueAfter: "example.com/shallow/deep/file", + placeholderAfter: "example.com/shallow/deep/file", + }, + { + searchString: "example.com/", + valueBefore: "example.com/", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/file then search for example.com/s", + searches: [ + { + searchString: "example.com/shallow/deep/f", + valueBefore: "example.com/shallow/deep/f", + valueAfter: "example.com/shallow/deep/file", + placeholderAfter: "example.com/shallow/deep/file", + }, + { + searchString: "example.com/s", + valueBefore: "example.com/s", + valueAfter: "example.com/shallow/", + placeholderAfter: "example.com/shallow/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/file then search for example.com/shallow/", + searches: [ + { + searchString: "example.com/shallow/deep/f", + valueBefore: "example.com/shallow/deep/f", + valueAfter: "example.com/shallow/deep/file", + placeholderAfter: "example.com/shallow/deep/file", + }, + { + searchString: "example.com/shallow/", + valueBefore: "example.com/shallow/", + valueAfter: "example.com/shallow/", + placeholderAfter: "example.com/shallow/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/file then search for example.com/shallow/d", + searches: [ + { + searchString: "example.com/shallow/deep/f", + valueBefore: "example.com/shallow/deep/f", + valueAfter: "example.com/shallow/deep/file", + placeholderAfter: "example.com/shallow/deep/file", + }, + { + searchString: "example.com/shallow/d", + valueBefore: "example.com/shallow/d", + valueAfter: "example.com/shallow/deep/", + placeholderAfter: "example.com/shallow/deep/", + }, + ], + }, + { + desc: "Autofill example.com/shallow/deep/file then search for example.com/shallow/deep/", + searches: [ + { + searchString: "example.com/shallow/deep/f", + valueBefore: "example.com/shallow/deep/f", + valueAfter: "example.com/shallow/deep/file", + placeholderAfter: "example.com/shallow/deep/file", + }, + { + searchString: "example.com/shallow/deep/fi", + valueBefore: "example.com/shallow/deep/file", + valueAfter: "example.com/shallow/deep/file", + placeholderAfter: "example.com/shallow/deep/file", + }, + { + searchString: "example.com/shallow/deep/", + valueBefore: "example.com/shallow/deep/", + valueAfter: "example.com/shallow/deep/", + placeholderAfter: "example.com/shallow/deep/", + }, + ], + }, + ]; + + for (const { desc, searches } of testData) { + info("Running subtest: " + desc); + + for (let i = 0; i < searches.length; i++) { + info("Doing search at index " + i); + await search(searches[i]); + } + + // Clear the placeholder for the next subtest. + info("Doing extra search to clear placeholder"); + await search({ + searchString: "no match", + valueBefore: "no match", + valueAfter: "no match", + placeholderAfter: "", + }); + } + + await cleanUp(); +}); + +// An adaptive history placeholder should not be used for a search that does not +// autofill it. +add_task(async function noAdaptiveHistoryMatch() { + UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", true); + + await addVisits("http://example.com/test"); + await UrlbarUtils.addToInputHistory("http://example.com/test", "exam"); + + // Search for a longer string than the adaptive history input. Adaptive + // history autofill should be triggered. + await search({ + searchString: "example", + valueBefore: "example", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + + // Search for the same string as the adaptive history input. The placeholder + // from the previous search should be used and adaptive history autofill + // should be triggered. + await search({ + searchString: "exam", + valueBefore: "example.com/test", + valueAfter: "example.com/test", + placeholderAfter: "example.com/test", + }); + + // Search for a shorter string than the adaptive history input. The + // placeholder from the previous search should not be used since the search + // string is shorter than the adaptive history input. + await search({ + searchString: "ex", + valueBefore: "ex", + valueAfter: "example.com/", + placeholderAfter: "example.com/", + }); + + UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled"); + await cleanUp(); +}); + +/** + * Adds enough visits to URLs so their origins start autofilling. + * + * @param {...string} urls The URLs to add visits to. + */ +async function addVisits(...urls) { + for (let url of urls) { + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits(url); + } + } + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); +} + +async function cleanUp() { + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +} diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_preserve.js b/browser/components/urlbar/tests/browser/browser_autoFill_preserve.js new file mode 100644 index 0000000000..a197be8bf1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_preserve.js @@ -0,0 +1,257 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test makes sure that a few of aspects of autofill are correctly +// preserved: +// +// * Autofill should preserve the user's case. If you type ExA, it should be +// autofilled to ExAmple.com/, not example.com/. +// * When you key down and then back up to the autofill result, autofill should +// be restored, with the text selection and the user's case both preserved. +// * When you key down/up so that no result is selected, the value that the +// user typed to trigger autofill should be restored in the input. + +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + // The example.com engine can interfere with this test. + set: [["browser.urlbar.suggest.engines", false]], + }); + await cleanUp(); +}); + +add_task(async function origin() { + await PlacesTestUtils.addVisits([ + "http://example.com/", + "http://mozilla.org/example", + ]); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ExA", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "ExAmple.com/"); + Assert.equal(gURLBar.selectionStart, "ExA".length); + Assert.equal(gURLBar.selectionEnd, "ExAmple.com/".length); + checkKeys([ + ["KEY_ArrowDown", "http://mozilla.org/example", 1], + ["KEY_ArrowDown", "ExA", -1], + ["KEY_ArrowUp", "http://mozilla.org/example", 1], + ["KEY_ArrowUp", "ExAmple.com/", 0], + ["KEY_ArrowUp", "ExA", -1], + ["KEY_ArrowDown", "ExAmple.com/", 0], + ]); + await cleanUp(); +}); + +add_task(async function originPort() { + await PlacesTestUtils.addVisits([ + "http://example.com:8888/", + "http://mozilla.org/example", + ]); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ExA", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "ExAmple.com:8888/"); + Assert.equal(gURLBar.selectionStart, "ExA".length); + Assert.equal(gURLBar.selectionEnd, "ExAmple.com:8888/".length); + checkKeys([ + ["KEY_ArrowDown", "http://mozilla.org/example", 1], + ["KEY_ArrowDown", "ExA", -1], + ["KEY_ArrowUp", "http://mozilla.org/example", 1], + ["KEY_ArrowUp", "ExAmple.com:8888/", 0], + ["KEY_ArrowUp", "ExA", -1], + ["KEY_ArrowDown", "ExAmple.com:8888/", 0], + ]); + await cleanUp(); +}); + +add_task(async function originScheme() { + await PlacesTestUtils.addVisits([ + "http://example.com/", + "http://mozilla.org/example", + ]); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "http://ExA", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "http://ExAmple.com/"); + Assert.equal(gURLBar.selectionStart, "http://ExA".length); + Assert.equal(gURLBar.selectionEnd, "http://ExAmple.com/".length); + checkKeys([ + ["KEY_ArrowDown", "http://mozilla.org/example", 1], + ["KEY_ArrowDown", "http://ExA", -1], + ["KEY_ArrowUp", "http://mozilla.org/example", 1], + ["KEY_ArrowUp", "http://ExAmple.com/", 0], + ["KEY_ArrowUp", "http://ExA", -1], + ["KEY_ArrowDown", "http://ExAmple.com/", 0], + ]); + await cleanUp(); +}); + +add_task(async function originPortScheme() { + await PlacesTestUtils.addVisits([ + "http://example.com:8888/", + "http://mozilla.org/example", + ]); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "http://ExA", + fireInputEvents: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "http://ExAmple.com:8888/"); + Assert.equal(gURLBar.selectionStart, "http://ExA".length); + Assert.equal(gURLBar.selectionEnd, "http://ExAmple.com:8888/".length); + checkKeys([ + ["KEY_ArrowDown", "http://mozilla.org/example", 1], + ["KEY_ArrowDown", "http://ExA", -1], + ["KEY_ArrowUp", "http://mozilla.org/example", 1], + ["KEY_ArrowUp", "http://ExAmple.com:8888/", 0], + ["KEY_ArrowUp", "http://ExA", -1], + ["KEY_ArrowDown", "http://ExAmple.com:8888/", 0], + ]); + await cleanUp(); +}); + +add_task(async function url() { + await PlacesTestUtils.addVisits([ + "http://example.com/foo", + "http://example.com/foo", + "http://example.com/fff", + ]); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ExAmple.com/f", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "ExAmple.com/foo"); + Assert.equal(gURLBar.selectionStart, "ExAmple.com/f".length); + Assert.equal(gURLBar.selectionEnd, "ExAmple.com/foo".length); + checkKeys([ + ["KEY_ArrowDown", "http://example.com/fff", 1], + ["KEY_ArrowDown", "ExAmple.com/f", -1], + ["KEY_ArrowUp", "http://example.com/fff", 1], + ["KEY_ArrowUp", "ExAmple.com/foo", 0], + ["KEY_ArrowUp", "ExAmple.com/f", -1], + ["KEY_ArrowDown", "ExAmple.com/foo", 0], + ]); + await cleanUp(); +}); + +add_task(async function urlPort() { + await PlacesTestUtils.addVisits([ + "http://example.com:8888/foo", + "http://example.com:8888/foo", + "http://example.com:8888/fff", + ]); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ExAmple.com:8888/f", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "ExAmple.com:8888/foo"); + Assert.equal(gURLBar.selectionStart, "ExAmple.com:8888/f".length); + Assert.equal(gURLBar.selectionEnd, "ExAmple.com:8888/foo".length); + checkKeys([ + ["KEY_ArrowDown", "http://example.com:8888/fff", 1], + ["KEY_ArrowDown", "ExAmple.com:8888/f", -1], + ["KEY_ArrowUp", "http://example.com:8888/fff", 1], + ["KEY_ArrowUp", "ExAmple.com:8888/foo", 0], + ["KEY_ArrowUp", "ExAmple.com:8888/f", -1], + ["KEY_ArrowDown", "ExAmple.com:8888/foo", 0], + ]); + await cleanUp(); +}); + +add_task(async function tokenAlias() { + await SearchTestUtils.installSearchExtension({ keyword: "@example" }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@ExA", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "@ExAmple "); + Assert.equal(gURLBar.selectionStart, "@ExA".length); + Assert.equal(gURLBar.selectionEnd, "@ExAmple ".length); + // Token aliases (1) hide the one-off buttons and (2) show only a single + // result, the "Search with" result for the alias's engine, so there's no way + // to key up/down to change the selection, so this task doesn't check key + // presses like the others do. + await cleanUp(); +}); + +// This test is a little different from the others. It backspaces over the +// autofilled substring and checks that autofill is *not* preserved. +add_task(async function backspaceNoAutofill() { + await PlacesTestUtils.addVisits([ + "http://example.com/", + "http://example.com/", + "http://mozilla.org/example", + ]); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ExA", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "ExAmple.com/"); + Assert.equal(gURLBar.selectionStart, "ExA".length); + Assert.equal(gURLBar.selectionEnd, "ExAmple.com/".length); + + EventUtils.synthesizeKey("KEY_Backspace"); + await UrlbarTestUtils.promiseSearchComplete(window); + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(!details.autofill); + Assert.equal(gURLBar.value, "ExA"); + Assert.equal(gURLBar.selectionStart, "ExA".length); + Assert.equal(gURLBar.selectionEnd, "ExA".length); + + let heuristicValue = "ExA"; + + checkKeys([ + ["KEY_ArrowDown", "http://example.com/", 1], + ["KEY_ArrowDown", "http://mozilla.org/example", 2], + ["KEY_ArrowDown", "ExA", -1], + ["KEY_ArrowUp", "http://mozilla.org/example", 2], + ["KEY_ArrowUp", "http://example.com/", 1], + ["KEY_ArrowUp", heuristicValue, 0], + ["KEY_ArrowUp", "ExA", -1], + ["KEY_ArrowDown", heuristicValue, 0], + ]); + + await cleanUp(); +}); + +function checkKeys(testTuples) { + for (let [key, value, selectedIndex] of testTuples) { + EventUtils.synthesizeKey(key); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), selectedIndex); + Assert.equal(gURLBar.untrimmedValue, value); + } +} + +async function cleanUp() { + EventUtils.synthesizeKey("KEY_Escape"); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +} diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_trimURLs.js b/browser/components/urlbar/tests/browser/browser_autoFill_trimURLs.js new file mode 100644 index 0000000000..5e941e9ede --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_trimURLs.js @@ -0,0 +1,181 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test ensures that autoFilled values are not trimmed, unless the user +// selects from the autocomplete popup. + +"use strict"; + +add_setup(async function () { + SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.trimURLs", true], + ["browser.urlbar.trimHttps", false], + ["browser.urlbar.autoFill", true], + ], + }); + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + gURLBar.handleRevert(); + }); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + + // Adding a tab would hit switch-to-tab, so it's safer to just add a visit. + await PlacesTestUtils.addVisits([ + { + uri: "http://www.autofilltrimurl.com/whatever", + }, + { + uri: "https://www.secureautofillurl.com/whatever", + }, + ]); +}); + +async function promiseSearch(searchtext) { + await UrlbarTestUtils.inputIntoURLBar(window, searchtext); + await UrlbarTestUtils.promiseSearchComplete(window); +} + +async function promiseTestResult(test) { + info(`Searching for '${test.search}'`); + + await promiseSearch(test.search); + + Assert.equal( + gURLBar.value, + test.autofilledValue, + `Autofilled value is as expected for search '${test.search}'` + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + Assert.equal( + result.displayed.title, + test.resultListDisplayTitle, + `Autocomplete result should have displayed title as expected for search '${test.search}'` + ); + + Assert.equal( + result.displayed.action, + test.resultListActionText, + `Autocomplete action text should be as expected for search '${test.search}'` + ); + + Assert.equal( + result.type, + test.resultListType, + `Autocomplete result should have searchengine for the type for search '${test.search}'` + ); + + Assert.equal( + !!result.searchParams, + !!test.searchParams, + "Should have search params if expected" + ); + if (test.searchParams) { + let definedParams = {}; + for (let [k, v] of Object.entries(result.searchParams)) { + if (v !== undefined) { + definedParams[k] = v; + } + } + Assert.deepEqual( + definedParams, + test.searchParams, + "Shoud have the correct search params" + ); + } else { + Assert.equal( + result.url, + test.finalCompleteValue, + "Should have the correct URL/finalCompleteValue" + ); + } +} + +const tests = [ + { + search: "http://", + autofilledValue: "http://", + resultListDisplayTitle: "http://", + resultListActionText: "Search with Google", + resultListType: UrlbarUtils.RESULT_TYPE.SEARCH, + searchParams: { + engine: "Google", + query: "http://", + }, + }, + { + search: "https://", + autofilledValue: "https://", + resultListDisplayTitle: "https://", + resultListActionText: "Search with Google", + resultListType: UrlbarUtils.RESULT_TYPE.SEARCH, + searchParams: { + engine: "Google", + query: "https://", + }, + }, + { + search: "au", + autofilledValue: "autofilltrimurl.com/", + resultListDisplayTitle: "www.autofilltrimurl.com", + resultListActionText: "Visit", + resultListType: UrlbarUtils.RESULT_TYPE.URL, + finalCompleteValue: "http://www.autofilltrimurl.com/", + }, + { + search: "http://au", + autofilledValue: "http://autofilltrimurl.com/", + resultListDisplayTitle: "www.autofilltrimurl.com", + resultListActionText: "Visit", + resultListType: UrlbarUtils.RESULT_TYPE.URL, + finalCompleteValue: "http://www.autofilltrimurl.com/", + }, + { + search: "sec", + autofilledValue: "secureautofillurl.com/", + resultListDisplayTitle: "https://www.secureautofillurl.com", + resultListActionText: "Visit", + resultListType: UrlbarUtils.RESULT_TYPE.URL, + finalCompleteValue: "https://www.secureautofillurl.com/", + }, + { + search: "https://sec", + autofilledValue: "https://secureautofillurl.com/", + resultListDisplayTitle: "https://www.secureautofillurl.com", + resultListActionText: "Visit", + resultListType: UrlbarUtils.RESULT_TYPE.URL, + finalCompleteValue: "https://www.secureautofillurl.com/", + }, +]; + +add_task(async function autofill_tests() { + for (let test of tests) { + await promiseTestResult(test); + } +}); + +add_task(async function autofill_complete_domain() { + await promiseSearch("http://www.autofilltrimurl.com"); + Assert.equal( + gURLBar.value, + "http://www.autofilltrimurl.com/", + "Should have the correct autofill value" + ); + + // Now ensure selecting from the popup correctly trims. + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Should have the correct matches" + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + gURLBar.value, + "www.autofilltrimurl.com/whatever", + "Should have applied trim correctly" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_typed.js b/browser/components/urlbar/tests/browser/browser_autoFill_typed.js new file mode 100644 index 0000000000..6f6ac57648 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_typed.js @@ -0,0 +1,174 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test makes sure that autofill works as expected when typing, character +// by character. + +"use strict"; + +add_setup(async function () { + await cleanUp(); +}); + +add_task(async function origin() { + await PlacesTestUtils.addVisits(["http://example.com/"]); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + // all lowercase + await typeAndCheck([ + ["e", "example.com/"], + ["x", "example.com/"], + ["a", "example.com/"], + ["m", "example.com/"], + ["p", "example.com/"], + ["l", "example.com/"], + ["e", "example.com/"], + [".", "example.com/"], + ["c", "example.com/"], + ["o", "example.com/"], + ["m", "example.com/"], + ["/", "example.com/"], + ]); + gURLBar.value = ""; + // mixed case + await typeAndCheck([ + ["E", "Example.com/"], + ["x", "Example.com/"], + ["A", "ExAmple.com/"], + ["m", "ExAmple.com/"], + ["P", "ExAmPle.com/"], + ["L", "ExAmPLe.com/"], + ["e", "ExAmPLe.com/"], + [".", "ExAmPLe.com/"], + ["C", "ExAmPLe.Com/"], + ["o", "ExAmPLe.Com/"], + ["M", "ExAmPLe.CoM/"], + ["/", "ExAmPLe.CoM/"], + ]); + await cleanUp(); +}); + +add_task(async function url() { + await PlacesTestUtils.addVisits(["http://example.com/foo/bar"]); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + // all lowercase + await typeAndCheck([ + ["e", "example.com/"], + ["x", "example.com/"], + ["a", "example.com/"], + ["m", "example.com/"], + ["p", "example.com/"], + ["l", "example.com/"], + ["e", "example.com/"], + [".", "example.com/"], + ["c", "example.com/"], + ["o", "example.com/"], + ["m", "example.com/"], + ["/", "example.com/"], + ["f", "example.com/foo/"], + ["o", "example.com/foo/"], + ["o", "example.com/foo/"], + ["/", "example.com/foo/"], + ["b", "example.com/foo/bar"], + ["a", "example.com/foo/bar"], + ["r", "example.com/foo/bar"], + ]); + gURLBar.value = ""; + // mixed case + await typeAndCheck([ + ["E", "Example.com/"], + ["x", "Example.com/"], + ["A", "ExAmple.com/"], + ["m", "ExAmple.com/"], + ["P", "ExAmPle.com/"], + ["L", "ExAmPLe.com/"], + ["e", "ExAmPLe.com/"], + [".", "ExAmPLe.com/"], + ["C", "ExAmPLe.Com/"], + ["o", "ExAmPLe.Com/"], + ["M", "ExAmPLe.CoM/"], + ["/", "ExAmPLe.CoM/"], + ["f", "ExAmPLe.CoM/foo/"], + ["o", "ExAmPLe.CoM/foo/"], + ["o", "ExAmPLe.CoM/foo/"], + ["/", "ExAmPLe.CoM/foo/"], + ["b", "ExAmPLe.CoM/foo/bar"], + ["a", "ExAmPLe.CoM/foo/bar"], + ["r", "ExAmPLe.CoM/foo/bar"], + ]); + await cleanUp(); +}); + +add_task(async function tokenAlias() { + // We have built-in engine aliases that may conflict with the one we choose + // here in terms of autofill, so be careful and choose a weird alias. + await SearchTestUtils.installSearchExtension({ keyword: "@__example" }); + // all lowercase + await typeAndCheck([ + ["@", "@"], + ["_", "@__example "], + ["_", "@__example "], + ["e", "@__example "], + ["x", "@__example "], + ["a", "@__example "], + ["m", "@__example "], + ["p", "@__example "], + ["l", "@__example "], + ["e", "@__example "], + ]); + gURLBar.value = ""; + // mixed case + await typeAndCheck([ + ["@", "@"], + ["_", "@__example "], + ["_", "@__example "], + ["E", "@__Example "], + ["x", "@__Example "], + ["A", "@__ExAmple "], + ["m", "@__ExAmple "], + ["P", "@__ExAmPle "], + ["L", "@__ExAmPLe "], + ["e", "@__ExAmPLe "], + ]); + await cleanUp(); +}); + +async function typeAndCheck(values) { + gURLBar.focus(); + for (let i = 0; i < values.length; i++) { + let [char, expectedInputValue] = values[i]; + info( + `Typing: i=${i} char=${char} ` + + `substring="${expectedInputValue.substring(0, i + 1)}"` + ); + EventUtils.synthesizeKey(char); + if (i == 0 && char == "@") { + // A single "@" doesn't trigger autofill, so skip the checks below. (It + // shows all the @ aliases.) + continue; + } + await UrlbarTestUtils.promiseSearchComplete(window); + let restIsSpaces = !expectedInputValue.substring(i + 1).trim(); + Assert.equal(gURLBar.value, expectedInputValue); + Assert.equal(gURLBar.selectionStart, i + 1); + Assert.equal(gURLBar.selectionEnd, expectedInputValue.length); + if (restIsSpaces) { + // Autofilled @ aliases have a trailing space. We should check that the + // space is autofilled when each preceding character is typed, but once + // the final non-space char is typed, autofill actually stops and the + // trailing space is not autofilled. (Which is maybe not the way it + // should work...) Skip the check below. + continue; + } + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + } +} + +async function cleanUp() { + gURLBar.value = ""; + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +} diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_undo.js b/browser/components/urlbar/tests/browser/browser_autoFill_undo.js new file mode 100644 index 0000000000..8abe846754 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_undo.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test checks the behavior of text undo (Ctrl-Z, cmd_undo) in regard to +// autofill. + +"use strict"; + +add_task(async function test() { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits(["http://example.com/"]); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + // Search for "ex". It should autofill to example.com/. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ex", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "ex".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + // Type an x. + EventUtils.synthesizeKey("x"); + + // Nothing should have been autofilled. + await UrlbarTestUtils.promiseSearchComplete(window); + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(!details.autofill); + Assert.equal(gURLBar.value, "exx"); + Assert.equal(gURLBar.selectionStart, "exx".length); + Assert.equal(gURLBar.selectionEnd, "exx".length); + + // Undo the typed x. + goDoCommand("cmd_undo"); + + // The text should be restored to ex[ample.com/] (with the part in brackets + // autofilled and selected). + await UrlbarTestUtils.promiseSearchComplete(window); + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(gURLBar.value, "example.com/"); + Assert.ok(!details.autofill, "Autofill should not be set."); + Assert.equal(gURLBar.selectionStart, "ex".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autoOpen.js b/browser/components/urlbar/tests/browser/browser_autoOpen.js new file mode 100644 index 0000000000..bfe491fc61 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoOpen.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function checkOpensOnFocus(win = window) { + // The view should not open when the input is focused programmatically. + win.gURLBar.blur(); + win.gURLBar.focus(); + Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open"); + win.gURLBar.blur(); + + // Check the keyboard shortcut. + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + + // Focus with the mouse. + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win); + }); + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); +} + +add_setup(async function () { + // Add some history for the empty panel. + await PlacesTestUtils.addVisits([ + { + uri: "http://mochi.test:8888/", + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }, + ]); + registerCleanupFunction(() => PlacesUtils.history.clear()); +}); + +add_task(async function test() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async browser => { + await checkOpensOnFocus(); + } + ); +}); + +add_task(async function newtabAndHome() { + for (let url of ["about:newtab", "about:home"]) { + // withNewTab randomly hangs on these pages when waitForLoad = true (the + // default), so pass false. + await BrowserTestUtils.withNewTab( + { gBrowser, url, waitForLoad: false }, + async browser => { + // We don't wait for load, but we must ensure to be on the expected url. + await TestUtils.waitForCondition( + () => gBrowser.currentURI.spec == url, + "Ensure we're on the expected page" + ); + await checkOpensOnFocus(); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "http://example.com/" }, + async otherBrowser => { + await checkOpensOnFocus(); + // Switch back to about:newtab/home. + await BrowserTestUtils.switchTab( + gBrowser, + gBrowser.getTabForBrowser(browser) + ); + await checkOpensOnFocus(); + // Switch back to example.com. + await BrowserTestUtils.switchTab( + gBrowser, + gBrowser.getTabForBrowser(otherBrowser) + ); + await checkOpensOnFocus(); + } + ); + // After example.com closes, about:newtab/home is selected again. + await checkOpensOnFocus(); + // Load example.com in the same tab. + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + "http://example.com/" + ); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await checkOpensOnFocus(); + } + ); + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js b/browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js new file mode 100644 index 0000000000..ead026244e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js @@ -0,0 +1,185 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test ensures that we produce good labels for a11y purposes. + */ + +const { CommonUtils } = ChromeUtils.importESModule( + "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs" +); + +const SUGGEST_ALL_PREF = "browser.search.suggest.enabled"; +const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches"; +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +let accService; + +async function getResultText(element, expectedValue, description = "") { + await BrowserTestUtils.waitForCondition( + () => { + let accessible = accService.getAccessibleFor(element); + return accessible !== null && accessible.name === expectedValue; + }, + description, + 200 + ); +} + +/** + * Initializes the accessibility service and registers a cleanup function to + * shut it down. If it's not shut down properly, it can crash the current tab + * and cause the test to fail, especially in verify mode. + * + * This function is adapted from from tests in accessible/tests/browser and its + * helper functions are adapted or copied from functions of the same names in + * the same directory. + */ +async function initAccessibilityService() { + const [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + await a11yInit; + + registerCleanupFunction(async () => { + const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService(); + await a11yShutdownObserver; + accService = null; + forceGC(); + await a11yShutdownPromise; + }); +} + +// Adapted from `initAccService()` in accessible/tests/browser/head.js +function initAccService() { + return [ + CommonUtils.addAccServiceInitializedObserver(), + CommonUtils.observeAccServiceInitialized(), + ]; +} + +// Adapted from `shutdownAccService()` in accessible/tests/browser/head.js +function shutdownAccService() { + return [ + CommonUtils.addAccServiceShutdownObserver(), + CommonUtils.observeAccServiceShutdown(), + ]; +} + +// Copied from accessible/tests/browser/shared-head.js +function forceGC() { + SpecialPowers.gc(); + SpecialPowers.forceShrinkingGC(); + SpecialPowers.forceCC(); + SpecialPowers.gc(); + SpecialPowers.forceShrinkingGC(); + SpecialPowers.forceCC(); +} + +add_setup(async function () { + await initAccessibilityService(); +}); + +add_task(async function switchToTab() { + let tab = BrowserTestUtils.addTab(gBrowser, "about:robots"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "% robots", + }); + + let index = 0; + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + "Should have a switch tab result" + ); + + let element = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + index + ); + // The a11y text will include the "Firefox Suggest" pseudo-element label shown + // before the result. + await getResultText( + element._content, + "Firefox Suggest about:robots — Switch to Tab", + "Result a11y text is correct" + ); + + await UrlbarTestUtils.promisePopupClose(window); + gURLBar.handleRevert(); + gBrowser.removeTab(tab); +}); + +add_task(async function searchSuggestions() { + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true); + let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF); + Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true); + registerCleanupFunction(async function () { + Services.prefs.clearUserPref(SUGGEST_ALL_PREF); + Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let length = await UrlbarTestUtils.getResultCount(window); + // Don't assume that the search doesn't match history or bookmarks left around + // by earlier tests. + Assert.greaterOrEqual( + length, + 3, + "Should get at least heuristic result + two search suggestions" + ); + // The first expected search is the search term itself since the heuristic + // result will come before the search suggestions. + let searchTerm = "foo"; + let expectedSearches = [searchTerm, "foofoo", "foobar"]; + for (let i = 0; i < length; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (result.type === UrlbarUtils.RESULT_TYPE.SEARCH) { + Assert.greaterOrEqual( + expectedSearches.length, + 0, + "Should still have expected searches remaining" + ); + + let element = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + i + ); + + // Select the row so we see the expanded text. + gURLBar.view.selectedRowIndex = i; + + if (result.searchParams.inPrivateWindow) { + await getResultText( + element._content, + searchTerm + " — Search in a Private Window", + "Check result label for search in private window" + ); + } else { + let suggestion = expectedSearches.shift(); + await getResultText( + element._content, + suggestion + + " — Search with browser_searchSuggestionEngine searchSuggestionEngine.xml", + "Check result label for non-private search" + ); + } + } + } + Assert.ok(!expectedSearches.length); + + await UrlbarTestUtils.promisePopupClose(window); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_autoselect.js b/browser/components/urlbar/tests/browser/browser_autocomplete_autoselect.js new file mode 100644 index 0000000000..ef3da56ef0 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_autoselect.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the first item is correctly autoselected and some navigation + * around the results list. + */ + +function repeat(limit, func) { + for (let i = 0; i < limit; i++) { + func(i); + } +} + +function assertSelected(index) { + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + index, + "Should have selected the correct item" + ); + + // This is true because although both the listbox and the one-offs can have + // selections, the test doesn't check that. + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton, + null, + "A result is selected, so the one-offs should not have a selection" + ); +} + +function assertSelected_one_off(index) { + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtons(window).selectedButtonIndex, + index, + "Expected one-off button should be selected" + ); + + // This is true because although both the listbox and the one-offs can have + // selections, the test doesn't check that. + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + -1, + "A one-off is selected, so the listbox should not have a selection" + ); +} + +add_task(async function () { + let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults"); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + BrowserTestUtils.removeTab(tab); + }); + + let visits = []; + repeat(maxResults, i => { + visits.push({ + uri: makeURI("http://example.com/autocomplete/?" + i), + }); + }); + await PlacesTestUtils.addVisits(visits); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example.com/autocomplete", + fireInputEvent: true, + }); + + let resultCount = await UrlbarTestUtils.getResultCount(window); + + Assert.equal( + resultCount, + maxResults, + "Should get the expected amount of results" + ); + assertSelected(0); + + info("Key Down to select the next item"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertSelected(1); + + info("Key Down maxResults-1 times should select the first one-off"); + repeat(maxResults - 1, () => EventUtils.synthesizeKey("KEY_ArrowDown")); + assertSelected_one_off(0); + + info("Key Down numButtons-1 should select the last one-off"); + let numButtons = + UrlbarTestUtils.getOneOffSearchButtons(window).getSelectableButtons( + true + ).length; + repeat(numButtons - 1, () => EventUtils.synthesizeKey("KEY_ArrowDown")); + assertSelected_one_off(numButtons - 1); + + info("Key Down twice more should select the second result"); + repeat(2, () => EventUtils.synthesizeKey("KEY_ArrowDown")); + assertSelected(1); + + info("Key Down maxResults + numButtons times should wrap around"); + repeat(maxResults + numButtons, () => + EventUtils.synthesizeKey("KEY_ArrowDown") + ); + assertSelected(1); + + info("Key Up maxResults + numButtons times should wrap around the other way"); + repeat(maxResults + numButtons, () => + EventUtils.synthesizeKey("KEY_ArrowUp") + ); + assertSelected(1); + + info("Page Up will go up the list, but not wrap"); + EventUtils.synthesizeKey("KEY_PageUp"); + assertSelected(0); + + info("Page Up again will wrap around to the end of the list"); + EventUtils.synthesizeKey("KEY_PageUp"); + assertSelected(maxResults - 1); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_cursor.js b/browser/components/urlbar/tests/browser/browser_autocomplete_cursor.js new file mode 100644 index 0000000000..5e0081a92c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_cursor.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests the cursor remains in the right place when a new window is opened. + */ + +add_task(async function test_windowSwitch() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "www.mozilla.org", + }); + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + + gURLBar.focus(); + gURLBar.inputField.setSelectionRange(4, 4); + + let newWindow = await BrowserTestUtils.openNewBrowserWindow(); + + await BrowserTestUtils.closeWindow(newWindow); + + Assert.equal( + document.activeElement, + gURLBar.inputField, + "URL Bar should be focused" + ); + Assert.equal(gURLBar.selectionStart, 4, "Should not have moved the cursor"); + Assert.equal(gURLBar.selectionEnd, 4, "Should not have selected anything"); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js b/browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js new file mode 100644 index 0000000000..4fa60f6bf3 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests selecting a result, and editing the value of that autocompleted result. + */ + +add_task(async function () { + SpecialPowers.pushPrefEnv({ set: [["browser.urlbar.trimHttps", false]] }); + await PlacesUtils.history.clear(); + + await PlacesTestUtils.addVisits([ + { uri: makeURI("http://example.com/foo") }, + { uri: makeURI("http://example.com/foo/bar") }, + ]); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(tab); + await PlacesUtils.history.clear(); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "http://example.com", + }); + + const initialIndex = UrlbarTestUtils.getSelectedRowIndex(window); + + info("Key Down to select the next item."); + EventUtils.synthesizeKey("KEY_ArrowDown"); + + let nextIndex = initialIndex + 1; + let nextResult = await UrlbarTestUtils.getDetailsOfResultAt( + window, + nextIndex + ); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + nextIndex, + "Should have selected the next item" + ); + Assert.equal( + gURLBar.untrimmedValue, + nextResult.url, + "Should have completed the URL" + ); + + info("Press backspace"); + EventUtils.synthesizeKey("KEY_Backspace"); + await UrlbarTestUtils.promiseSearchComplete(window); + + let editedValue = gURLBar.value; + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + initialIndex, + "Should have selected the initialIndex again" + ); + Assert.notEqual(editedValue, nextResult.url, "The URL has changed."); + + let docLoad = BrowserTestUtils.waitForDocLoadAndStopIt( + "http://" + editedValue, + gBrowser.selectedBrowser + ); + + info("Press return to load edited URL."); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Enter"); + }); + + await docLoad; +}); diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_enter_race.js b/browser/components/urlbar/tests/browser/browser_autocomplete_enter_race.js new file mode 100644 index 0000000000..63a0958e0f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_enter_race.js @@ -0,0 +1,198 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests what happens when the enter key is pressed quickly after entering text. + */ + +// The order of these tests matters! +const IS_UPGRADING_SCHEMELESS = SpecialPowers.getBoolPref( + "dom.security.https_first_schemeless" +); +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const DEFAULT_URL_SCHEME = IS_UPGRADING_SCHEMELESS ? "https://" : "http://"; + +add_setup(async function () { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: DEFAULT_URL_SCHEME + "/example.com/?q=%s", + title: "test", + }); + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.remove(bm); + await PlacesUtils.history.clear(); + }); + // Needs at least one success. + ok(true, "Setup complete"); +}); + +add_task( + taskWithNewTab(async function test_loadSite() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autofill", false]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example.co", + }); + gURLBar.focus(); + EventUtils.sendString("m"); + EventUtils.synthesizeKey("KEY_Enter"); + info("wait for the page to load"); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedTab.linkedBrowser, + false, + DEFAULT_URL_SCHEME + "example.com/" + ); + await SpecialPowers.popPrefEnv(); + }) +); + +add_task( + taskWithNewTab(async function test_sametext() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example.com", + fireInputEvent: true, + }); + + // Simulate re-entering the same text searched the last time. This may happen + // through a copy paste, but clipboard handling is not much reliable, so just + // fire an input event. + info("synthesize input event"); + let event = document.createEvent("Events"); + event.initEvent("input", true, true); + gURLBar.inputField.dispatchEvent(event); + EventUtils.synthesizeKey("KEY_Enter"); + + info("wait for the page to load"); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedTab.linkedBrowser, + false, + DEFAULT_URL_SCHEME + "example.com/" + ); + }) +); + +add_task( + taskWithNewTab(async function test_after_empty_search() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + gURLBar.focus(); + gURLBar.value = "e"; + EventUtils.synthesizeKey("x"); + EventUtils.synthesizeKey("KEY_Enter"); + + info("wait for the page to load"); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedTab.linkedBrowser, + false, + DEFAULT_URL_SCHEME + "example.com/" + ); + }) +); + +add_task( + taskWithNewTab(async function test_disabled_ac() { + // Disable autocomplete. + let suggestHistory = Preferences.get("browser.urlbar.suggest.history"); + Preferences.set("browser.urlbar.suggest.history", false); + let suggestBookmarks = Preferences.get("browser.urlbar.suggest.bookmark"); + Preferences.set("browser.urlbar.suggest.bookmark", false); + let suggestOpenPages = Preferences.get("browser.urlbar.suggest.openpage"); + Preferences.set("browser.urlbar.suggest.openpage", false); + + await SearchTestUtils.installSearchExtension(); + + let engine = Services.search.getEngineByName("Example"); + let originalEngine = await Services.search.getDefault(); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + async function cleanup() { + Preferences.set("browser.urlbar.suggest.history", suggestHistory); + Preferences.set("browser.urlbar.suggest.bookmark", suggestBookmarks); + Preferences.set("browser.urlbar.suggest.openpage", suggestOpenPages); + + await Services.search.setDefault( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + } + registerCleanupFunction(cleanup); + + gURLBar.focus(); + gURLBar.value = "e"; + EventUtils.sendString("x"); + EventUtils.synthesizeKey("KEY_Enter"); + + info("wait for the page to load"); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedTab.linkedBrowser, + false, + "https://example.com/?q=ex" + ); + await cleanup(); + }) +); + +// Tests that setting a high value for browser.urlbar.delay does not delay the +// fetching of heuristic results. +add_task( + taskWithNewTab(async function test_delay() { + // This is needed to clear the current value, otherwise autocomplete may think + // the user removed text from the end. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.promisePopupClose(window); + + // Set a large delay. + const TIMEOUT = 3000; + let delay = UrlbarPrefs.get("delay"); + UrlbarPrefs.set("delay", TIMEOUT); + registerCleanupFunction(function () { + UrlbarPrefs.set("delay", delay); + }); + + gURLBar.focus(); + gURLBar.value = "e"; + let recievedResult = new Promise(resolve => { + gURLBar.controller.addQueryListener({ + onQueryResults(queryContext) { + gURLBar.controller.removeQueryListener(this); + Assert.ok( + queryContext.heuristicResult, + "Recieved a heuristic result." + ); + Assert.equal( + queryContext.searchString, + "ex", + "The heuristic result is based on the correct search string." + ); + resolve(); + }, + }); + }); + let start = Cu.now(); + EventUtils.sendString("x"); + EventUtils.synthesizeKey("KEY_Enter"); + await recievedResult; + Assert.ok(Cu.now() - start < TIMEOUT); + }) +); + +// The main reason for running each test task in a new tab that's closed when +// the task finishes is to avoid switch-to-tab results. +function taskWithNewTab(fn) { + return async function () { + await BrowserTestUtils.withNewTab("about:blank", fn); + }; +} diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_no_title.js b/browser/components/urlbar/tests/browser/browser_autocomplete_no_title.js new file mode 100644 index 0000000000..fa30a7608a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_no_title.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test ensures that we display just the domain name when a URL result doesn't + * have a title. + */ + +add_task(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + await PlacesUtils.history.clear(); + const uri = "http://bug1060642.example.com/beards/are/pretty/great"; + await PlacesTestUtils.addVisits([{ uri, title: "" }]); + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + BrowserTestUtils.removeTab(tab); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "bug1060642", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + result.displayed.title, + "bug1060642.example.com", + "Result title should be as expected" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_readline_navigation.js b/browser/components/urlbar/tests/browser/browser_autocomplete_readline_navigation.js new file mode 100644 index 0000000000..36f990503e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_readline_navigation.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests navigation between results using ctrl-n/p. + */ + +function repeat(limit, func) { + for (let i = 0; i < limit; i++) { + func(i); + } +} + +function assertSelected(index) { + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + index, + "Should have the correct item selected" + ); + + // This is true because although both the listbox and the one-offs can have + // selections, the test doesn't check that. + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton, + null, + "A result is selected, so the one-offs should not have a selection" + ); +} + +add_task(async function () { + let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults"); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + BrowserTestUtils.removeTab(tab); + }); + + let visits = []; + repeat(maxResults, i => { + visits.push({ + uri: makeURI("http://example.com/autocomplete/?" + i), + }); + }); + await PlacesTestUtils.addVisits(visits); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example.com/autocomplete", + }); + await UrlbarTestUtils.waitForAutocompleteResultAt(window, maxResults - 1); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + maxResults, + "Should get maxResults=" + maxResults + " results" + ); + assertSelected(0); + + info("Ctrl-n to select the next item"); + EventUtils.synthesizeKey("n", { ctrlKey: true }); + assertSelected(1); + + info("Ctrl-p to select the previous item"); + EventUtils.synthesizeKey("p", { ctrlKey: true }); + assertSelected(0); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js b/browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js new file mode 100644 index 0000000000..10e26f6f71 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js @@ -0,0 +1,167 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests for the bookmark star being correct displayed for results matching + * tags. + */ + +add_task(async function () { + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + }); + + async function addTagItem(tagName) { + let url = `http://example.com/this/is/tagged/${tagName}`; + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url, + title: `test ${tagName}`, + }); + PlacesUtils.tagging.tagURI(Services.io.newURI(url), [tagName]); + await PlacesTestUtils.addVisits({ + uri: url, + title: `Test page with tag ${tagName}`, + }); + } + + // We use different tags for each part of the test, as otherwise the + // autocomplete code tries to be smart by using the previously cached element + // without updating it (since all parameters it knows about are the same). + + let testcases = [ + { + description: "Test with suggest.bookmark=true", + tagName: "tagtest1", + prefs: { + "suggest.bookmark": true, + }, + input: "tagtest1", + expected: { + typeImageVisible: true, + }, + }, + { + description: "Test with suggest.bookmark=false", + tagName: "tagtest2", + prefs: { + "suggest.bookmark": false, + }, + input: "tagtest2", + expected: { + typeImageVisible: false, + }, + }, + { + description: "Test with suggest.bookmark=true (again)", + tagName: "tagtest3", + prefs: { + "suggest.bookmark": true, + }, + input: "tagtest3", + expected: { + typeImageVisible: true, + }, + }, + { + description: "Test with bookmark restriction token", + tagName: "tagtest4", + prefs: { + "suggest.bookmark": true, + }, + input: "* tagtest4", + expected: { + typeImageVisible: true, + }, + }, + { + description: "Test with history restriction token", + tagName: "tagtest5", + prefs: { + "suggest.bookmark": true, + }, + input: "^ tagtest5", + expected: { + typeImageVisible: false, + }, + }, + { + description: "Test partial tag and casing", + tagName: "tagtest6", + prefs: { + "suggest.bookmark": true, + }, + input: "TeSt6", + expected: { + typeImageVisible: true, + }, + }, + ]; + + for (let testcase of testcases) { + info(`Test case: ${testcase.description}`); + + await addTagItem(testcase.tagName); + for (let prefName of Object.keys(testcase.prefs)) { + Services.prefs.setBoolPref( + `browser.urlbar.${prefName}`, + testcase.prefs[prefName] + ); + } + + let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: testcase.input, + }); + + // If testcase.input triggers local search mode, there won't be a heuristic. + let resultIndex = + context.searchMode && !context.searchMode.engineName ? 0 : 1; + + Assert.greaterOrEqual( + UrlbarTestUtils.getResultCount(window), + resultIndex + 1, + `Should be at least ${resultIndex + 1} results` + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + resultIndex + ); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.URL, + "Should have a URL result type" + ); + // The Quantum Bar differs from the legacy urlbar in the fact that, if + // bookmarks are filtered out, it won't show tags for history results. + let expected_tags = !testcase.expected.typeImageVisible + ? [] + : [testcase.tagName]; + Assert.deepEqual( + result.tags, + expected_tags, + "Should have the expected tag" + ); + + if (testcase.expected.typeImageVisible) { + Assert.equal( + result.displayed.typeIcon, + 'url("chrome://browser/skin/bookmark-12.svg")', + "Should have the star image displayed or not as expected" + ); + } else { + Assert.equal( + result.displayed.typeIcon, + "none", + "Should have the star image displayed or not as expected" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); + gURLBar.handleRevert(); + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_bestMatch.js b/browser/components/urlbar/tests/browser/browser_bestMatch.js new file mode 100644 index 0000000000..21c97405a6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_bestMatch.js @@ -0,0 +1,193 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests best match rows in the view. + +"use strict"; + +// Tests a non-sponsored best match row. +add_task(async function nonsponsored() { + let result = makeBestMatchResult(); + await withProvider(result, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkBestMatchRow({ result }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Tests a non-sponsored best match row with a help button. +add_task(async function nonsponsoredHelpButton() { + let result = makeBestMatchResult({ helpUrl: "https://example.com/help" }); + await withProvider(result, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkBestMatchRow({ result, hasHelpUrl: true }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Tests a sponsored best match row. +add_task(async function sponsored() { + let result = makeBestMatchResult({ isSponsored: true }); + await withProvider(result, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkBestMatchRow({ result, isSponsored: true }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Tests a sponsored best match row with a help button. +add_task(async function sponsoredHelpButton() { + let result = makeBestMatchResult({ + isSponsored: true, + helpUrl: "https://example.com/help", + }); + await withProvider(result, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkBestMatchRow({ result, isSponsored: true, hasHelpUrl: true }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Tests keyboard selection. +add_task(async function keySelection() { + let result = makeBestMatchResult({ + isSponsored: true, + helpUrl: "https://example.com/help", + }); + + await withProvider(result, async () => { + // Ordered list of class names of the elements that should be selected. + let expectedClassNames = ["urlbarView-row-inner", "urlbarView-button-menu"]; + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkBestMatchRow({ + result, + isSponsored: true, + hasHelpUrl: true, + }); + + // Test with the tab key in order vs. reverse order. + for (let reverse of [false, true]) { + info("Doing TAB key selection: " + JSON.stringify({ reverse })); + + let classNames = [...expectedClassNames]; + if (reverse) { + classNames.reverse(); + } + + let sendKey = () => { + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: reverse }); + }; + + // Move selection through each expected element. + for (let className of classNames) { + info("Expecting selection: " + className); + sendKey(); + Assert.ok(gURLBar.view.isOpen, "View remains open"); + let { selectedElement } = gURLBar.view; + Assert.ok(selectedElement, "Selected element exists"); + Assert.ok( + selectedElement.classList.contains(className), + "Expected element is selected" + ); + } + sendKey(); + Assert.ok( + gURLBar.view.isOpen, + "View remains open after keying through best match row" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +async function checkBestMatchRow({ + result, + isSponsored = false, + hasHelpUrl = false, +}) { + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "One result is present" + ); + + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + let { row } = details.element; + + let favicon = row._elements.get("favicon"); + Assert.ok(favicon, "Row has a favicon"); + + let title = row._elements.get("title"); + Assert.ok(title, "Row has a title"); + Assert.ok(title.textContent, "Row title has non-empty textContext"); + Assert.equal(title.textContent, result.payload.title, "Row title is correct"); + + let url = row._elements.get("url"); + Assert.ok(url, "Row has a URL"); + Assert.ok(url.textContent, "Row URL has non-empty textContext"); + Assert.equal( + url.textContent, + result.payload.displayUrl, + "Row URL is correct" + ); + + let button = row._buttons.get("menu"); + Assert.equal( + !!result.payload.helpUrl, + hasHelpUrl, + "Sanity check: Row's expected hasHelpUrl matches result" + ); + if (hasHelpUrl) { + Assert.ok(button, "Row with helpUrl has a help or menu button"); + } else { + Assert.ok( + !button, + "Row without helpUrl does not have a help or menu button" + ); + } +} + +async function withProvider(result, callback) { + let provider = new UrlbarTestUtils.TestProvider({ + results: [result], + priority: Infinity, + }); + UrlbarProvidersManager.registerProvider(provider); + try { + await callback(); + } finally { + UrlbarProvidersManager.unregisterProvider(provider); + } +} + +function makeBestMatchResult(payloadExtra = {}) { + return Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.SEARCH, + ...UrlbarResult.payloadAndSimpleHighlights([], { + title: "Test best match", + url: "https://example.com/best-match", + ...payloadExtra, + }) + ), + { isBestMatch: true } + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_blanking.js b/browser/components/urlbar/tests/browser/browser_blanking.js new file mode 100644 index 0000000000..f68c4d894a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_blanking.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = `${TEST_BASE_URL}file_blank_but_not_blank.html`; + +add_task(async function () { + for (let page of gInitialPages) { + if (page == "about:newtab") { + // New tab preloading makes this a pain to test, so skip + continue; + } + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, page); + ok( + !gURLBar.value, + "The URL bar should be empty if we load a plain " + page + " page." + ); + BrowserTestUtils.removeTab(tab); + } +}); + +add_task(async function () { + // The test was originally to check that reloading of a javascript: URL could + // throw an error and empty the URL bar. This situation can no longer happen + // as in bug 836567 we set document.URL to active document's URL on navigation + // to a javascript: URL; reloading after that will simply reload the original + // active document rather than the javascript: URL itself. But we can still + // verify that the URL bar's value is correct. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + is( + gURLBar.value, + UrlbarTestUtils.trimURL(TEST_URL), + "The URL bar should match the URI" + ); + let browserLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + SpecialPowers.spawn(tab.linkedBrowser, [], function () { + content.document.querySelector("a").click(); + }); + await browserLoaded; + is( + gURLBar.value, + UrlbarTestUtils.trimURL(TEST_URL), + "The URL bar should be the previous active document's URI." + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + // This is sync, so by the time we return we should have changed the URL bar. + content.location.reload(); + }).catch(e => { + // Ignore expected exception. + }); + is( + gURLBar.value, + UrlbarTestUtils.trimURL(TEST_URL), + "The URL bar should still be the previous active document's URI." + ); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_blobIcons.js b/browser/components/urlbar/tests/browser/browser_blobIcons.js new file mode 100644 index 0000000000..701519c97f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_blobIcons.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests `Blob` icon management in the view. + +"use strict"; + +// `URL.createObjectURL()` should be called the first time a blob icon is shown +// while the view is open, and `revokeObjectURL()` should be called when the +// view is closed. +add_task(async function test() { + let sandbox = sinon.createSandbox(); + registerCleanupFunction(() => sandbox.restore()); + + // Spy on `URL.createObjectURL()` and `revokeObjectURL()`. + let spies = ["createObjectURL", "revokeObjectURL"].reduce((memo, name) => { + memo[name] = sandbox.spy(Cu.getGlobalForObject(gURLBar.view).URL, name); + return memo; + }, {}); + + // Do a search and close the view. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await UrlbarTestUtils.promisePopupClose(window); + + // No blob URLs should have been created or revoked since no results that have + // blob icons were matched. + checkCallCounts(spies, { + createObjectURL: 0, + revokeObjectURL: 0, + }); + + // Create a test provider that returns a result with a blob icon. + let provider = new UrlbarTestUtils.TestProvider({ + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url: "https://example.com/", + iconBlob: new Blob([new Uint8Array([])]), + } + ), + ], + }); + UrlbarProvidersManager.registerProvider(provider); + + // Do some searches. + await doSearches(provider, spies, { + createObjectURL: 1, + revokeObjectURL: 0, + }); + + // Closing the view should cause `revokeObjectURL()` to be called. + await UrlbarTestUtils.promisePopupClose(window); + checkCallCounts(spies, { + createObjectURL: 1, + revokeObjectURL: 1, + }); + + // Do some more searches. + await doSearches(provider, spies, { + createObjectURL: 2, + revokeObjectURL: 1, + }); + + // Close the view. + await UrlbarTestUtils.promisePopupClose(window); + checkCallCounts(spies, { + createObjectURL: 2, + revokeObjectURL: 2, + }); + + // Remove the provider, do another search, and close the view. Since no + // results with blob icons are matched, the call counts should not change. + UrlbarProvidersManager.unregisterProvider(provider); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await UrlbarTestUtils.promisePopupClose(window); + checkCallCounts(spies, { + createObjectURL: 2, + revokeObjectURL: 2, + }); + + sandbox.restore(); +}); + +async function doSearches(provider, spies, expectedCountsByName) { + let previousImage; + for (let i = 0; i < 3; i++) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test " + i, + }); + + let result = await getTestResult(provider); + Assert.ok(result, "Test result should be present"); + Assert.ok(result.image, "Row has an icon with a src"); + Assert.ok(result.image.startsWith("blob:"), "Row icon src is a blob URL"); + if (i > 0) { + Assert.equal( + result.image, + previousImage, + "Blob URL should be the same as in previous searches" + ); + } + previousImage = result.image; + + // `createObjectURL()` should be called only once across all searches since + // the view remains open the whole time. + checkCallCounts(spies, expectedCountsByName); + } +} + +async function getTestResult(provider) { + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (result.result.providerName == provider.name) { + return result; + } + } + return null; +} + +function checkCallCounts(spies, expectedCountsByName) { + for (let [name, count] of Object.entries(expectedCountsByName)) { + Assert.strictEqual(spies[name].callCount, count, "Spy call count: " + name); + } +} diff --git a/browser/components/urlbar/tests/browser/browser_bufferer_onQueryResults.js b/browser/components/urlbar/tests/browser/browser_bufferer_onQueryResults.js new file mode 100644 index 0000000000..7447f44ffd --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_bufferer_onQueryResults.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This test covers a race condition of input events followed by Enter. +// The test is putting the event bufferer in a situation where a new query has +// already results in the context object, but onQueryResults has not been +// invoked yet. The EventBufferer should wait for onQueryResults to proceed, +// otherwise the view cannot yet contain the updated query string and we may +// end up searching for a partial string. + +add_setup(async function () { + sandbox = sinon.createSandbox(); + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + setAsDefault: true, + }); + // To reproduce the race condition it's important to disable any provider + // having `deferUserSelection` == true; + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.engines", false]], + }); + await PlacesUtils.history.clear(); + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + sandbox.restore(); + }); +}); + +add_task(async function test() { + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:robots", + }); + + let defer = Promise.withResolvers(); + let waitFirstSearchResults = Promise.withResolvers(); + let count = 0; + let original = gURLBar.controller.notify; + sandbox.stub(gURLBar.controller, "notify").callsFake(async (msg, context) => { + if (context?.deferUserSelectionProviders.size) { + Assert.ok(false, "Any provider deferring selection should be disabled"); + } + if (msg == "onQueryResults") { + waitFirstSearchResults.resolve(); + count++; + } + // Delay any events after the second onQueryResults call. + if (count >= 2) { + await defer.promise; + } + return original.call(gURLBar.controller, msg, context); + }); + + gURLBar.focus(); + gURLBar.select(); + EventUtils.synthesizeKey("t", {}); + await waitFirstSearchResults.promise; + EventUtils.synthesizeKey("e", {}); + + let promiseLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter", {}); + + let context = await UrlbarTestUtils.promiseSearchComplete(window); + await TestUtils.waitForCondition( + () => context.results.length, + "Waiting for any result in the QueryContext" + ); + info("Simulate a request to replay deferred events at this point"); + gURLBar.eventBufferer.replayDeferredEvents(true); + + defer.resolve(); + await promiseLoaded; + + let expectedURL = UrlbarPrefs.isPersistedSearchTermsEnabled() + ? "http://mochi.test:8888/?terms=" + gURLBar.value + : gURLBar.untrimmedValue; + Assert.equal(gBrowser.selectedBrowser.currentURI.spec, expectedURL); + + BrowserTestUtils.removeTab(tab); + sandbox.restore(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_calculator.js b/browser/components/urlbar/tests/browser/browser_calculator.js new file mode 100644 index 0000000000..899cbc6d5b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_calculator.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const FORMULA = "8 * 8"; +const RESULT = "64"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.calculator", true]], + }); +}); + +add_task(async function test_calculator() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: FORMULA, + }); + + let result = (await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)) + .result; + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC); + Assert.equal(result.payload.input, FORMULA); + Assert.equal(result.payload.value, RESULT); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + + // Ensure the RESULT get written to the clipboard when selected. + await SimpleTest.promiseClipboardChange(RESULT, () => { + EventUtils.synthesizeKey("KEY_Enter"); + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_canonizeURL.js b/browser/components/urlbar/tests/browser/browser_canonizeURL.js new file mode 100644 index 0000000000..fbbb7c01d1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_canonizeURL.js @@ -0,0 +1,284 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests turning non-url-looking values typed in the input field into proper URLs. + */ + +requestLongerTimeout(2); + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +add_task(async function checkCtrlWorks() { + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); + + // We do not want schemeless HTTPS-First interfering with this test, + // that interaction is already tested in dom/security/test/https-first/browser_schemeless.js + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first_schemeless", false]], + }); + + let defaultEngine = await Services.search.getDefault(); + let testcases = [ + ["example", "https://www.example.com/", { ctrlKey: true }], + // Check that a direct load is not overwritten by a previous canonization. + ["http://example.com/test/", "http://example.com/test/", {}], + ["ex-ample", "https://www.ex-ample.com/", { ctrlKey: true }], + [" example ", "https://www.example.com/", { ctrlKey: true }], + [" example/foo ", "https://www.example.com/foo", { ctrlKey: true }], + [ + " example/foo bar ", + "https://www.example.com/foo%20bar", + { ctrlKey: true }, + ], + ["example.net", "http://example.net/", { ctrlKey: true }], + ["http://example", "http://example/", { ctrlKey: true }], + ["example:8080", "http://example:8080/", { ctrlKey: true }], + ["ex-ample.foo", "http://ex-ample.foo/", { ctrlKey: true }], + ["example.foo/bar ", "http://example.foo/bar", { ctrlKey: true }], + ["1.1.1.1", "http://1.1.1.1/", { ctrlKey: true }], + ["ftp.example.bar", "http://ftp.example.bar/", { ctrlKey: true }], + [ + "ex ample", + defaultEngine.getSubmission("ex ample", null, "keyword").uri.spec, + { ctrlKey: true }, + ], + ]; + + // Disable autoFill for this test, since it could mess up the results. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.autoFill", false], + ["browser.urlbar.ctrlCanonizesURLs", true], + ], + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + for (let [inputValue, expectedURL, options] of testcases) { + info(`Testing input string: "${inputValue}" - expected: "${expectedURL}"`); + let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt( + expectedURL, + win.gBrowser.selectedBrowser + ); + let promiseStopped = BrowserTestUtils.browserStopped( + win.gBrowser.selectedBrowser, + undefined, + true + ); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await UrlbarTestUtils.inputIntoURLBar(win, inputValue); + EventUtils.synthesizeKey("KEY_Enter", options, win); + await Promise.all([promiseLoad, promiseStopped]); + } + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function checkPrefTurnsOffCanonize() { + // Add a dummy search engine to avoid hitting the network. + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + // Ensure we don't end up loading something in the current tab becuase it's empty: + let initialTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser: win.gBrowser, + opening: "about:mozilla", + }); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.ctrlCanonizesURLs", false]], + }); + + let newURL = "http://mochi.test:8888/?terms=example"; + // On MacOS CTRL+Enter is not supposed to open in a new tab, because it uses + // CMD+Enter for that. + let promiseLoaded = + AppConstants.platform == "macosx" + ? BrowserTestUtils.browserLoaded( + win.gBrowser.selectedBrowser, + false, + newURL + ) + : BrowserTestUtils.waitForNewTab(win.gBrowser); + + win.gURLBar.focus(); + win.gURLBar.selectionStart = win.gURLBar.selectionEnd = + win.gURLBar.value.length; + win.gURLBar.value = "exampl"; + EventUtils.sendString("e", win); + EventUtils.synthesizeKey("KEY_Enter", { ctrlKey: true }, win); + + await promiseLoaded; + if (AppConstants.platform == "macosx") { + Assert.equal( + initialTab.linkedBrowser.currentURI.spec, + newURL, + "Original tab should have navigated" + ); + } else { + Assert.equal( + initialTab.linkedBrowser.currentURI.spec, + "about:mozilla", + "Original tab shouldn't have navigated" + ); + Assert.equal( + win.gBrowser.selectedBrowser.currentURI.spec, + newURL, + "New tab should have navigated" + ); + } + while (win.gBrowser.tabs.length > 1) { + win.gBrowser.removeTab(win.gBrowser.selectedTab, { animate: false }); + } + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function autofill() { + // Re-enable autofill and canonization. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.autoFill", true], + ["browser.urlbar.ctrlCanonizesURLs", true], + ], + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + // Quantumbar automatically disables autofill when the old search string + // starts with the new search string, so to make sure that doesn't happen and + // that earlier tests don't conflict with this one, start a new search for + // some other string. + win.gURLBar.select(); + EventUtils.sendString("blah", win); + + // Add a visit that will be autofilled. + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits([ + { + uri: "https://example.com/", + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }, + ]); + + let testcases = [ + ["ex", "https://www.ex.com/", { ctrlKey: true }], + // Check that a direct load is not overwritten by a previous canonization. + ["ex", "https://example.com/", {}], + // search alias + ["@goo", "https://www.goo.com/", { ctrlKey: true }], + ]; + + for (let [inputValue, expectedURL, options] of testcases) { + let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt( + expectedURL, + win.gBrowser.selectedBrowser + ); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + win.gURLBar.select(); + let autofillPromise = BrowserTestUtils.waitForEvent( + win.gURLBar.inputField, + "select" + ); + EventUtils.sendString(inputValue, win); + await autofillPromise; + EventUtils.synthesizeKey("KEY_Enter", options, win); + await promiseLoad; + + // Here again, make sure autofill isn't disabled for the next search. See + // the comment above. + win.gURLBar.select(); + EventUtils.sendString("blah", win); + } + + await PlacesUtils.history.clear(); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function () { + info( + "Test whether canonization is disabled until the ctrl key is releasing if the key was used to paste text into urlbar" + ); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.ctrlCanonizesURLs", true]], + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + info("Paste the word to the urlbar"); + const testWord = "example"; + simulatePastingToUrlbar(testWord, win); + is(win.gURLBar.value, testWord, "Paste the test word correctly"); + + info("Send enter key while pressing the ctrl key"); + EventUtils.synthesizeKey("VK_RETURN", { ctrlKey: true }, win); + await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + is( + win.gBrowser.selectedBrowser.documentURI.spec, + `http://mochi.test:8888/?terms=${testWord}`, + "The loaded url is not canonized" + ); + EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" }, win); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function () { + info("Test whether canonization is enabled again after releasing the ctrl"); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.ctrlCanonizesURLs", true]], + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + info("Paste the word to the urlbar"); + const testWord = "example"; + simulatePastingToUrlbar(testWord, win); + is(win.gURLBar.value, testWord, "Paste the test word correctly"); + + info("Release the ctrl key befoer typing Enter key"); + EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" }, win); + + info("Send enter key with the ctrl"); + const onLoad = BrowserTestUtils.waitForDocLoadAndStopIt( + `https://www.${testWord}.com/`, + win.gBrowser.selectedBrowser + ); + const onStop = BrowserTestUtils.browserStopped( + win.gBrowser.selectedBrowser, + undefined, + true + ); + EventUtils.synthesizeKey("VK_RETURN", { ctrlKey: true }, win); + await Promise.all([onLoad, onStop]); + info("The loaded url is canonized"); + + await BrowserTestUtils.closeWindow(win); +}); + +function simulatePastingToUrlbar(text, win) { + win.gURLBar.focus(); + + const keyForPaste = win.document + .getElementById("key_paste") + .getAttribute("key") + .toLowerCase(); + EventUtils.synthesizeKey( + keyForPaste, + { type: "keydown", ctrlKey: true }, + win + ); + + win.gURLBar.select(); + EventUtils.sendString(text, win); +} diff --git a/browser/components/urlbar/tests/browser/browser_caret_position.js b/browser/components/urlbar/tests/browser/browser_caret_position.js new file mode 100644 index 0000000000..6a8a8b18f8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_caret_position.js @@ -0,0 +1,362 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const LARGE_DATA_URL = + "data:text/plain," + [...Array(1000)].map(() => "0123456789").join(""); + +// Tests for the caret position after gURLBar.setURI(). +add_task(async function setURI() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.trimHttps", false]], + }); + const testData = [ + { + firstURL: "https://example.com/test", + secondURL: "https://example.com/test", + initialSelectionStart: 0, + initialSelectionEnd: 0, + expectedSelectionStart: 0, + expectedSelectionEnd: 0, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.com/test", + initialSelectionStart: 20, + initialSelectionEnd: 20, + expectedSelectionStart: 20, + expectedSelectionEnd: 20, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.com/test", + initialSelectionStart: 1, + initialSelectionEnd: 20, + expectedSelectionStart: 1, + expectedSelectionEnd: 20, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.com/test", + initialSelectionStart: "https://example.com/test".length, + initialSelectionEnd: "https://example.com/test".length, + expectedSelectionStart: "https://example.com/test".length, + expectedSelectionEnd: "https://example.com/test".length, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.com/test", + initialSelectionStart: 0, + initialSelectionEnd: "https://example.com/test".length, + expectedSelectionStart: 0, + expectedSelectionEnd: "https://example.com/test".length, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.org/test", + initialSelectionStart: 0, + initialSelectionEnd: 0, + expectedSelectionStart: 0, + expectedSelectionEnd: 0, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.org/test", + initialSelectionStart: 20, + initialSelectionEnd: 20, + expectedSelectionStart: 20, + expectedSelectionEnd: 20, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.org/test", + initialSelectionStart: 1, + initialSelectionEnd: 10, + expectedSelectionStart: 1, + expectedSelectionEnd: 10, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.org/test", + initialSelectionStart: "https://example.".length, + initialSelectionEnd: "https://example.c".length, + expectedSelectionStart: "https://example.c".length, + expectedSelectionEnd: "https://example.c".length, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.org/test", + initialSelectionStart: "https://example.com/test".length, + initialSelectionEnd: "https://example.com/test".length, + expectedSelectionStart: "https://example.org/test".length, + expectedSelectionEnd: "https://example.org/test".length, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.org/test", + initialSelectionStart: 0, + initialSelectionEnd: "https://example.com/test".length, + expectedSelectionStart: "https://example.org/test".length, + expectedSelectionEnd: "https://example.org/test".length, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.com/longer", + initialSelectionStart: "https://example.com/test".length, + initialSelectionEnd: "https://example.com/test".length, + expectedSelectionStart: "https://example.com/longer".length, + expectedSelectionEnd: "https://example.com/longer".length, + }, + { + firstURL: "https://example.com/test", + secondURL: "https://example.com/longer", + initialSelectionStart: 20, + initialSelectionEnd: 20, + expectedSelectionStart: 20, + expectedSelectionEnd: 20, + }, + { + firstURL: "https://example.com/longer", + secondURL: "https://example.com/test", + initialSelectionStart: 0, + initialSelectionEnd: "https://example.com/longer".length, + expectedSelectionStart: "https://example.com/test".length, + expectedSelectionEnd: "https://example.com/test".length, + }, + { + firstURL: "https://example.com/longer", + secondURL: "https://example.com/test", + initialSelectionStart: "https://example.com/longer".length, + initialSelectionEnd: "https://example.com/longer".length, + expectedSelectionStart: "https://example.com/test".length, + expectedSelectionEnd: "https://example.com/test".length, + }, + { + firstURL: "https://example.com/longer", + secondURL: "https://example.com/test", + initialSelectionStart: "https://example.com/longer".length - 1, + initialSelectionEnd: "https://example.com/longer".length - 1, + expectedSelectionStart: "https://example.com/test".length, + expectedSelectionEnd: "https://example.com/test".length, + }, + { + firstURL: "https://example.com/longer", + secondURL: "https://example.com/test", + initialSelectionStart: 0, + initialSelectionEnd: "https://example.com/longer".length - 1, + expectedSelectionStart: "https://example.com/test".length, + expectedSelectionEnd: "https://example.com/test".length, + }, + { + firstURL: "https://example.com/test", + secondURL: "about:blank", + initialSelectionStart: 0, + initialSelectionEnd: 0, + expectedSelectionStart: 0, + expectedSelectionEnd: 0, + }, + { + firstURL: "https://example.com/test", + secondURL: "about:blank", + initialSelectionStart: 0, + initialSelectionEnd: "https://example.com/test".length, + expectedSelectionStart: 0, + expectedSelectionEnd: 0, + }, + { + firstURL: "https://example.com/test", + secondURL: "about:blank", + initialSelectionStart: 3, + initialSelectionEnd: 4, + expectedSelectionStart: 0, + expectedSelectionEnd: 0, + }, + { + firstURL: "https://example.com/test", + secondURL: "about:blank", + initialSelectionStart: "https://example.com/test".length, + initialSelectionEnd: "https://example.com/test".length, + expectedSelectionStart: 0, + expectedSelectionEnd: 0, + }, + { + firstURL: "about:blank", + secondURL: "https://example.com/test", + initialSelectionStart: 0, + initialSelectionEnd: 0, + expectedSelectionStart: 0, + expectedSelectionEnd: 0, + }, + { + firstURL: "about:blank", + secondURL: LARGE_DATA_URL, + initialSelectionStart: 0, + initialSelectionEnd: 0, + expectedSelectionStart: 0, + expectedSelectionEnd: 0, + }, + { + firstURL: "about:telemetry", + secondURL: LARGE_DATA_URL, + initialSelectionStart: "about:telemetry".length, + initialSelectionEnd: "about:telemetry".length, + expectedSelectionStart: LARGE_DATA_URL.length, + expectedSelectionEnd: LARGE_DATA_URL.length, + }, + ]; + + for (const data of testData) { + info( + `Test for ${data.firstURL} -> ${data.secondURL} with initial selection: ${data.initialSelectionStart}, ${data.initialSelectionEnd}` + ); + info("Check the caret position after setting second URL"); + gURLBar.setURI(makeURI(data.firstURL)); + gURLBar.selectionStart = data.initialSelectionStart; + gURLBar.selectionEnd = data.initialSelectionEnd; + + // The change of the scroll amount dependent on the selection change will be + // ignored if the previous processing is unfinished yet. Therefore, make the + // processing finalize explicitly here. + await flushScrollStyle(); + + gURLBar.focus(); + gURLBar.setURI(makeURI(data.secondURL)); + await flushScrollStyle(); + + Assert.equal(gURLBar.selectionStart, data.expectedSelectionStart); + Assert.equal(gURLBar.selectionEnd, data.expectedSelectionEnd); + if (data.secondURL.length === data.expectedSelectionStart) { + // If the caret is at the end of url, the input field shows the end of + // text. + Assert.equal( + gURLBar.inputField.scrollLeft, + gURLBar.inputField.scrollLeftMax + ); + } + + info("Check the caret position while the input is not focused"); + gURLBar.setURI(makeURI(data.firstURL)); + gURLBar.selectionStart = data.initialSelectionStart; + gURLBar.selectionEnd = data.initialSelectionEnd; + + await flushScrollStyle(); + + gURLBar.blur(); + gURLBar.setURI(makeURI(data.secondURL)); + await flushScrollStyle(); + + if (data.firstURL === data.secondURL) { + Assert.equal(gURLBar.selectionStart, data.initialSelectionStart); + Assert.equal(gURLBar.selectionEnd, data.initialSelectionEnd); + } else { + Assert.equal(gURLBar.selectionStart, gURLBar.value.length); + Assert.equal(gURLBar.selectionEnd, gURLBar.value.length); + } + Assert.equal(gURLBar.inputField.scrollLeft, 0); + } +}); + +// Tests that up and down keys move the caret on certain platforms, and that +// opening the popup doesn't change the caret position. +add_task(async function navigation() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "This is a generic sentence", + }); + await UrlbarTestUtils.promisePopupClose(window); + + const INITIAL_SELECTION_START = 3; + const INITIAL_SELECTION_END = 10; + gURLBar.selectionStart = INITIAL_SELECTION_START; + gURLBar.selectionEnd = INITIAL_SELECTION_END; + + if (AppConstants.platform == "macosx" || AppConstants.platform == "linux") { + await checkCaretMoves( + "KEY_ArrowDown", + gURLBar.value.length, + "Caret should have moved to the end", + window + ); + await checkPopupOpens("KEY_ArrowDown", window); + + await checkCaretMoves( + "KEY_ArrowUp", + 0, + "Caret should have moved to the start", + window + ); + await checkPopupOpens("KEY_ArrowUp", window); + } else { + await checkPopupOpens("KEY_ArrowDown", window); + await checkPopupOpens("KEY_ArrowUp", window); + } +}); + +async function checkCaretMoves(key, pos, msg, win) { + checkIfKeyStartsQuery(key, false, win); + Assert.equal( + UrlbarTestUtils.isPopupOpen(win), + false, + `${key}: Popup shouldn't be open` + ); + Assert.equal( + win.gURLBar.selectionStart, + win.gURLBar.selectionEnd, + `${key}: Input selection should be empty` + ); + Assert.equal(win.gURLBar.selectionStart, pos, `${key}: ${msg}`); +} + +async function checkPopupOpens(key, win) { + // Store current selection and check it doesn't change. + let selectionStart = win.gURLBar.selectionStart; + let selectionEnd = win.gURLBar.selectionEnd; + await UrlbarTestUtils.promisePopupOpen(win, () => { + checkIfKeyStartsQuery(key, true, win); + }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(win), + 0, + `${key}: Heuristic result should be selected` + ); + Assert.equal( + win.gURLBar.selectionStart, + selectionStart, + `${key}: Input selection start should not change` + ); + Assert.equal( + win.gURLBar.selectionEnd, + selectionEnd, + `${key}: Input selection end should not change` + ); + await UrlbarTestUtils.promisePopupClose(win); +} + +function checkIfKeyStartsQuery(key, shouldStartQuery, win) { + let queryStarted = false; + let queryListener = { + onQueryStarted() { + queryStarted = true; + }, + }; + win.gURLBar.controller.addQueryListener(queryListener); + EventUtils.synthesizeKey(key, {}, win); + win.gURLBar.eventBufferer.replayDeferredEvents(false); + win.gURLBar.controller.removeQueryListener(queryListener); + Assert.equal( + queryStarted, + shouldStartQuery, + `${key}: Should${shouldStartQuery ? "" : "n't"} have started a query` + ); +} + +async function flushScrollStyle() { + // Flush pending notifications for the style. + /* eslint-disable no-unused-expressions */ + gURLBar.inputField.scrollLeft; + // Ensure to apply the style. + await new Promise(resolve => + gURLBar.inputField.ownerGlobal.requestAnimationFrame(resolve) + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_click_row_border.js b/browser/components/urlbar/tests/browser/browser_click_row_border.js new file mode 100644 index 0000000000..59915ed3b1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_click_row_border.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = "https://example.com/autocomplete"; + +add_setup(async function () { + await PlacesTestUtils.addVisits(TEST_URL); + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function test_click_row_border() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example.com/autocomplete", + }); + let resultRow = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + let loaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + TEST_URL + ); + info("Clicking on the result's top pixel row"); + EventUtils.synthesizeMouse( + resultRow, + parseInt(getComputedStyle(resultRow).borderTopLeftRadius) * 2, + 1, + {} + ); + info("Waiting for page to load"); + await loaded; + ok(true, "Page loaded"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_clipboard.js b/browser/components/urlbar/tests/browser/browser_clipboard.js new file mode 100644 index 0000000000..f6127ef8d9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_clipboard.js @@ -0,0 +1,349 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Browser test for clipboard suggestion. + */ + +"use strict"; + +const { UrlbarProviderClipboard, CLIPBOARD_IMPRESSION_LIMIT } = + ChromeUtils.importESModule( + "resource:///modules/UrlbarProviderClipboard.sys.mjs" + ); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.clipboard.featureGate", true], + ["browser.urlbar.suggest.clipboard", true], + ], + }); + registerCleanupFunction(() => { + SpecialPowers.clipboardCopyString(""); + }); +}); + +async function searchEmptyStringAndGetFirstRow() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + return UrlbarTestUtils.getRowAt(window, 0); +} + +async function checkClipboardSuggestionAbsent(startIdx) { + for (let i = startIdx; i < UrlbarTestUtils.getResultCount(window); i++) { + const row = await UrlbarTestUtils.getRowAt(window, i); + Assert.notEqual( + row.result.providerName, + UrlbarProviderClipboard.name, + `Clipboard suggestion should be absent (checking index ${i})` + ); + } +} + +add_task(async function testFormattingOfClipboardSuggestion() { + let unicodeURL = "https://пример.com/"; + let punycodeURL = "https://xn--e1afmkfd.com/"; + + SpecialPowers.clipboardCopyString(unicodeURL); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:home" }, + async browser => { + let { result } = await searchEmptyStringAndGetFirstRow(); + + Assert.equal( + result.providerName, + UrlbarProviderClipboard.name, + "The first result is a clipboard valid url suggestion." + ); + Assert.equal( + result.payload.url, + punycodeURL, + "The Clipboard suggestion URL should not be decoded." + ); + Assert.equal( + result.payload.fallbackTitle, + unicodeURL, + "The Clipboard suggestion fallback title should be decoded." + ); + } + ); +}); +// Verifies that a valid URL copied to the clipboard results in the +// display of a corresponding suggestion in the URL bar as the first +// suggestion with accurate URL and icon. Also ensures that engaging +// with a clipboard suggestion leads to navigation to the copied URL +// and subsequent absence of the suggestion upon refocusing the URL bar. +add_task(async function testUserEngagementWithClipboardSuggestion() { + const validURL = "https://example.com/"; + SpecialPowers.clipboardCopyString(validURL); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:home" }, + async browser => { + let { result } = await searchEmptyStringAndGetFirstRow(); + let onLoad = BrowserTestUtils.browserLoaded(browser, false); + + Assert.equal( + result.providerName, + UrlbarProviderClipboard.name, + "The first result is a clipboard valid url suggestion." + ); + Assert.equal( + result.payload.url, + validURL, + "The Clipboard suggestion URL and the valid URL should match." + ); + Assert.equal( + result.icon, + "chrome://global/skin/icons/clipboard.svg", + "Clipboard suggestion icon" + ); + await checkClipboardSuggestionAbsent(1); + + // Focus and select the clipbaord result. + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + validURL, + "Navigated to the validURL webpage after selecting the clipboard result." + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await checkClipboardSuggestionAbsent(0); + } + ); +}); + +// This test confirms that dismissing the result from the result menu +// button after copying a valid URL dismisses the clipboard suggestion, +// and the suggestion does not reappear upon refocusing the URL bar. +add_task(async function testDismissClipboardSuggestion() { + SpecialPowers.clipboardCopyString("https://example.com/2"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:home" }, + async () => { + const resultIndex = 0; + const command = "dismiss"; + let row = await searchEmptyStringAndGetFirstRow(); + + Assert.equal( + row.result.providerName, + UrlbarProviderClipboard.name, + "Clipboard suggestion should be present" + ); + await checkClipboardSuggestionAbsent(1); + await UrlbarTestUtils.openResultMenuAndClickItem(window, command, { + resultIndex, + }); + Assert.ok( + gURLBar.view.isOpen, + "The view should remain open after clicking the command" + ); + Assert.ok( + !row.hasAttribute("feedback-acknowledgement"), + "Row should not have feedback acknowledgement after clicking command" + ); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + + // Do the same search again. The suggestion should not appear. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await checkClipboardSuggestionAbsent(0); + } + ); +}); + +// The test validates that the clipboard suggestion is displayed for +// the first two URL bar openings after copying a valid URL, but is +// suppressed on the third opening of URL bar. +add_task(async function testClipboardSuggestionLimit() { + SpecialPowers.clipboardCopyString("https://example.com/3"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:home" }, + async () => { + for (let i = 0; i < CLIPBOARD_IMPRESSION_LIMIT; i++) { + const { result } = await searchEmptyStringAndGetFirstRow(); + Assert.equal( + result.providerName, + UrlbarProviderClipboard.name, + "Clipboard suggestion should be present as the first suggestion." + ); + await checkClipboardSuggestionAbsent(1); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + } + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await checkClipboardSuggestionAbsent(0); + } + ); +}); + +// This test ensures that copying non-URL content to the clipboard +// results in the absence of a clipboard suggestion when opening +// the URL bar. +add_task(async function testNonUrlClipboardSuggestion() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:home" }, + async () => { + const malformedURLs = [ + "plain text", + "ftp://example.com", + "https://example.com[invalid]", + // Testing http because it is considered as a valid URL. + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://", + "https://example.com some text", + "https://example.com/ some text", + ]; + for (let i = 0; i < malformedURLs.length; i++) { + SpecialPowers.clipboardCopyString(malformedURLs[i]); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await checkClipboardSuggestionAbsent(0); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + } + } + ); +}); + +// This test verifies that clipboard suggestions are displayed +// based on the toggled state of the 'clipboard.featureGate' preference. +add_task(async function testClipboardFeatureGateToggle() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.clipboard.featureGate", false], + ["browser.urlbar.suggest.clipboard", true], + ], + }); + SpecialPowers.clipboardCopyString("https://example.com/4"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:home" }, + async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await checkClipboardSuggestionAbsent(0); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.clipboard.featureGate", true]], + }); + const { result } = await searchEmptyStringAndGetFirstRow(); + Assert.equal( + result.providerName, + UrlbarProviderClipboard.name, + "Clipboard suggestion should be present as the first suggestion." + ); + await checkClipboardSuggestionAbsent(1); + } + ); +}); + +// This test confirms that clipboard suggestions are presented based on +// the state of the 'suggest.clipboard' preference toggle. +add_task(async function testClipboardSuggestToggle() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.clipboard.featureGate", true], + ["browser.urlbar.suggest.clipboard", false], + ], + }); + SpecialPowers.clipboardCopyString("https://example.com/5"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:home" }, + async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await checkClipboardSuggestionAbsent(0); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.clipboard", true]], + }); + const { result } = await searchEmptyStringAndGetFirstRow(); + Assert.equal( + result.providerName, + UrlbarProviderClipboard.name, + "Clipboard suggestion should be present as the first suggestion." + ); + await checkClipboardSuggestionAbsent(1); + } + ); +}); + +add_task(async function testScalarAndStopWatchTelemetry() { + SpecialPowers.clipboardCopyString("https://example.com/6"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:home" }, + async () => { + Services.telemetry.clearScalars(); + let histogram = Services.telemetry.getHistogramById( + "FX_URLBAR_PROVIDER_CLIPBOARD_READ_TIME_MS" + ); + histogram.clear(); + Assert.equal( + Object.values(histogram.snapshot().values).length, + 0, + "histogram is empty before search" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + waitForFocus, + fireInputEvent: true, + }); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + }); + + const scalars = TelemetryTestUtils.getProcessScalars( + "parent", + true, + true + ); + + TelemetryTestUtils.assertKeyedScalar( + scalars, + `urlbar.picked.clipboard`, + 0, + 1 + ); + + Assert.greater( + Object.values(histogram.snapshot().values).length, + 0, + "histogram updated after search" + ); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_closePanelOnClick.js b/browser/components/urlbar/tests/browser/browser_closePanelOnClick.js new file mode 100644 index 0000000000..c61bb35bb6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_closePanelOnClick.js @@ -0,0 +1,51 @@ +/* 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 tests that the urlbar panel closes when clicking certain ui elements. + */ + +"use strict"; + +add_setup(function () { + // We intentionally turn off this a11y check, because the following + // clicks is purposefully targeting non-interactive elements to dismiss + // the opened URL Bar with a mouse which can be done by assistive + // technology and keyboard by pressing `Esc` key, this rule check shall + // be ignored by a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + + registerCleanupFunction(async () => { + // Usually, the AccessibilityUtils environment should be reset right after + // the click, but in this case there are no other testable interactions + // between iterations of the use case task besides those clicks that we are + // setting the environment with. + AccessibilityUtils.resetEnv(); + }); +}); + +add_task(async function () { + await BrowserTestUtils.withNewTab("about:robots", async () => { + for (let elt of [ + gBrowser.selectedBrowser, + gBrowser.tabContainer, + document.querySelector("#nav-bar toolbarspring"), + ]) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "dummy", + }); + // Must have at least one test. + Assert.ok(!!elt, "Found a valid element: " + (elt.id || elt.localName)); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeNativeMouseEvent({ + type: "click", + target: elt, + atCenter: true, + }) + ); + } + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_content_opener.js b/browser/components/urlbar/tests/browser/browser_content_opener.js new file mode 100644 index 0000000000..0cf4865ad7 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_content_opener.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + await BrowserTestUtils.withNewTab( + TEST_BASE_URL + "dummy_page.html", + async function (browser) { + let windowOpenedPromise = BrowserTestUtils.waitForNewWindow(); + await SpecialPowers.spawn(browser, [], function () { + content.window.open("", "_BLANK", "toolbar=no,height=300,width=500"); + }); + let newWin = await windowOpenedPromise; + is( + newWin.gURLBar.value, + "about:blank", + "Should be displaying about:blank for the opened window." + ); + await BrowserTestUtils.closeWindow(newWin); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_contextualsearch.js b/browser/components/urlbar/tests/browser/browser_contextualsearch.js new file mode 100644 index 0000000000..60e489a542 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_contextualsearch.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { UrlbarProviderContextualSearch } = ChromeUtils.importESModule( + "resource:///modules/UrlbarProviderContextualSearch.sys.mjs" +); + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.contextualSearch.enabled", true]], + }); +}); + +add_task(async function test_selectContextualSearchResult_already_installed() { + await SearchTestUtils.installSearchExtension({ + name: "Contextual", + search_url: "https://example.com/browser", + }); + + const ENGINE_TEST_URL = "https://example.com/"; + let onLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + ENGINE_TEST_URL + ); + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + ENGINE_TEST_URL + ); + await onLoaded; + + const query = "search"; + let engine = Services.search.getEngineByName("Contextual"); + const [expectedUrl] = UrlbarUtils.getSearchQueryUrl(engine, query); + + Assert.ok( + expectedUrl.includes(`?q=${query}`), + "Expected URL should be a search URL" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + const resultIndex = UrlbarTestUtils.getResultCount(window) - 1; + const result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + resultIndex + ); + + is( + result.dynamicType, + "contextualSearch", + "Second last result is a contextual search result" + ); + + info("Focus and select the contextual search result"); + UrlbarTestUtils.setSelectedRowIndex(window, resultIndex); + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + expectedUrl + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + expectedUrl, + "Selecting the contextual search result opens the search URL" + ); +}); + +add_task(async function test_selectContextualSearchResult_not_installed() { + const ENGINE_TEST_URL = + "http://mochi.test:8888/browser/browser/components/search/test/browser/opensearch.html"; + const EXPECTED_URL = + "http://mochi.test:8888/browser/browser/components/search/test/browser/?search&test=search"; + let onLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + ENGINE_TEST_URL + ); + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + ENGINE_TEST_URL + ); + await onLoaded; + + const query = "search"; + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + const resultIndex = UrlbarTestUtils.getResultCount(window) - 1; + const result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + resultIndex + ); + + Assert.equal( + result.dynamicType, + "contextualSearch", + "Second last result is a contextual search result" + ); + + info("Focus and select the contextual search result"); + UrlbarTestUtils.setSelectedRowIndex(window, resultIndex); + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + EXPECTED_URL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + EXPECTED_URL, + "Selecting the contextual search result opens the search URL" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_copy_and_paste_first_result.js b/browser/components/urlbar/tests/browser/browser_copy_and_paste_first_result.js new file mode 100644 index 0000000000..236ad49671 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_copy_and_paste_first_result.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + registerCleanupFunction(async function () { + gURLBar.handleRevert(); + await PlacesUtils.history.clear(); + }); + SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.autoFill", true], + ["browser.urlbar.suggest.quickactions", false], + ], + }); + + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits([ + "https://example.com/", + "https://example.com/foo", + ]); +}); + +add_task(async function () { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + }); + Assert.equal( + gURLBar.value, + "example.com/", + "autofilled value is as expected" + ); + + await UrlbarTestUtils.promisePopupClose(window); + + goDoCommand("cmd_selectAll"); + goDoCommand("cmd_copy"); + goDoCommand("cmd_paste"); + Assert.equal( + gURLBar.inputField.value, + "https://example.com/", + "pasted value contains scheme" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_copy_during_load.js b/browser/components/urlbar/tests/browser/browser_copy_during_load.js new file mode 100644 index 0000000000..4a81ff08be --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_copy_during_load.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that copying from the urlbar page works correctly after a result is +// confirmed but takes a while to load. + +add_task(async function () { + const SLOW_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://www.example.com" + ) + "slow-page.sjs"; + + await BrowserTestUtils.withNewTab(gBrowser, async tab => { + gURLBar.focus(); + gURLBar.value = SLOW_PAGE; + let promise = TestUtils.waitForCondition( + () => gURLBar.getAttribute("pageproxystate") == "invalid" + ); + EventUtils.synthesizeKey("KEY_Enter"); + info("wait for the initial conditions"); + await promise; + + info("Copy the whole url"); + await SimpleTest.promiseClipboardChange(SLOW_PAGE, () => { + gURLBar.select(); + goDoCommand("cmd_copy"); + }); + + info("Copy the initial part of the url, as a different valid url"); + await SimpleTest.promiseClipboardChange( + SLOW_PAGE.substring(0, SLOW_PAGE.indexOf("slow-page.sjs")), + () => { + gURLBar.selectionStart = 0; + gURLBar.selectionEnd = gURLBar.value.indexOf("slow-page.sjs"); + goDoCommand("cmd_copy"); + } + ); + + // This is apparently necessary to avoid a timeout on mochitest shutdown(!?) + let browserStoppedPromise = BrowserTestUtils.browserStopped( + gBrowser, + null, + true + ); + BrowserStop(); + await browserStoppedPromise; + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_copying.js b/browser/components/urlbar/tests/browser/browser_copying.js new file mode 100644 index 0000000000..111df58fd1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_copying.js @@ -0,0 +1,738 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function getUrl(hostname, file) { + return ( + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + hostname + ) + file + ); +} + +add_task(async function () { + await test_copy_values(trimHttpTests, false); + await test_copy_values(trimHttpsTests, true); +}); + +async function test_copy_values(testValues, trimHttps) { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + registerCleanupFunction(function () { + gBrowser.removeTab(tab); + gURLBar.setURI(); + }); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.trimURLs", true], + ["browser.urlbar.trimHttps", trimHttps], + // avoid prompting about phishing + ["network.http.phishy-userpass-length", 32], + ], + }); + + for (let testCase of testValues) { + if (testCase.setup) { + await testCase.setup(); + } + + if (testCase.loadURL) { + info(`Loading : ${testCase.loadURL}`); + let expectedLoad = testCase.expectedLoad || testCase.loadURL; + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + testCase.loadURL + ); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + expectedLoad + ); + } else if (testCase.setURL) { + gURLBar.value = testCase.setURL; + } + if (testCase.setURL || testCase.loadURL) { + gURLBar.valueIsTyped = !!testCase.setURL; + is( + gURLBar.value, + testCase.expectedURL, + "url bar value set to " + gURLBar.value + ); + } + + gURLBar.focus(); + if (testCase.expectedValueOnFocus) { + Assert.equal( + gURLBar.value, + testCase.expectedValueOnFocus, + "Check value on focus" + ); + } + await testCopy(testCase.copyVal, testCase.copyExpected); + gURLBar.blur(); + + if (testCase.cleanup) { + await testCase.cleanup(); + } + } +} + +var trimHttpTests = [ + // pageproxystate="invalid" + { + setURL: "http://example.com/", + expectedURL: "example.com", + copyExpected: "example.com", + }, + { + copyVal: "<e>xample.com", + copyExpected: "e", + }, + { + copyVal: "<e>x<a>mple.com", + copyExpected: "ea", + }, + { + copyVal: "<e><xa>mple.com", + copyExpected: "exa", + }, + { + copyVal: "<e><xa>mple.co<m>", + copyExpected: "exam", + }, + { + copyVal: "<e><xample.co><m>", + copyExpected: "example.com", + }, + + // pageproxystate="valid" from this point on (due to the load) + { + loadURL: "http://example.com/", + expectedURL: "example.com", + copyExpected: "http://example.com/", + }, + { + copyVal: "<example.co>m", + copyExpected: "example.co", + }, + { + copyVal: "e<x>ample.com", + copyExpected: "x", + }, + { + copyVal: "<e>xample.com", + copyExpected: "e", + }, + { + copyVal: "<e>xample.co<m>", + copyExpected: "em", + }, + { + copyVal: "<exam><ple.com>", + copyExpected: "example.com", + }, + + { + loadURL: "http://example.com/foo", + expectedURL: "example.com/foo", + copyExpected: "http://example.com/foo", + }, + { + copyVal: "<example.com>/foo", + copyExpected: "http://example.com", + }, + { + copyVal: "<example>.com/foo", + copyExpected: "example", + }, + // Test that partially selected URL is copied with encoded spaces + { + loadURL: "http://example.com/%20space/test", + expectedURL: "example.com/ space/test", + copyExpected: "http://example.com/%20space/test", + }, + { + copyVal: "<example.com/ space>/test", + copyExpected: "http://example.com/%20space", + }, + { + copyVal: "<example.com/ space/test>", + copyExpected: "http://example.com/%20space/test", + }, + { + loadURL: "http://example.com/%20foo%20bar%20baz/", + expectedURL: "example.com/ foo bar baz/", + copyExpected: "http://example.com/%20foo%20bar%20baz/", + }, + { + copyVal: "<example.com/ foo bar> baz/", + copyExpected: "http://example.com/%20foo%20bar", + }, + { + copyVal: "example.<com/ foo bar> baz/", + copyExpected: "com/ foo bar", + }, + + // Test that userPass is stripped out + { + loadURL: getUrl( + "http://user:pass@mochi.test:8888", + "authenticate.sjs?user=user&pass=pass" + ), + expectedURL: getUrl( + "mochi.test:8888", + "authenticate.sjs?user=user&pass=pass" + ), + copyExpected: getUrl( + "http://mochi.test:8888", + "authenticate.sjs?user=user&pass=pass" + ), + }, + + // Test escaping + { + loadURL: "http://example.com/()%28%29%C3%A9", + expectedURL: "example.com/()()\xe9", + copyExpected: "http://example.com/()%28%29%C3%A9", + }, + { + copyVal: "<example.com/(>)()\xe9", + copyExpected: "http://example.com/(", + }, + { + copyVal: "e<xample.com/(>)()\xe9", + copyExpected: "xample.com/(", + }, + + { + loadURL: "http://example.com/%C3%A9%C3%A9", + expectedURL: "example.com/\xe9\xe9", + copyExpected: "http://example.com/%C3%A9%C3%A9", + }, + { + copyVal: "e<xample.com/\xe9>\xe9", + copyExpected: "xample.com/\xe9", + }, + { + copyVal: "<example.com/\xe9>\xe9", + copyExpected: "http://example.com/%C3%A9", + }, + { + // Note: it seems BrowserTestUtils.loadURI fails for unicode domains + loadURL: "http://sub2.xn--lt-uia.mochi.test:8888/foo", + expectedURL: "sub2.ält.mochi.test:8888/foo", + copyExpected: "http://sub2.ält.mochi.test:8888/foo", + }, + { + copyVal: "s<ub2.ält.mochi.test:8888/f>oo", + copyExpected: "ub2.ält.mochi.test:8888/f", + }, + { + copyVal: "<sub2.ält.mochi.test:8888/f>oo", + copyExpected: "http://sub2.%C3%A4lt.mochi.test:8888/f", + }, + + { + loadURL: "http://example.com/?%C3%B7%C3%B7", + expectedURL: "example.com/?\xf7\xf7", + copyExpected: "http://example.com/?%C3%B7%C3%B7", + }, + { + copyVal: "e<xample.com/?\xf7>\xf7", + copyExpected: "xample.com/?\xf7", + }, + { + copyVal: "<example.com/?\xf7>\xf7", + copyExpected: "http://example.com/?%C3%B7", + }, + { + loadURL: "http://example.com/a%20test", + expectedURL: "example.com/a test", + copyExpected: "http://example.com/a%20test", + }, + { + loadURL: "http://example.com/a%E3%80%80test", + expectedURL: "example.com/a%E3%80%80test", + copyExpected: "http://example.com/a%E3%80%80test", + }, + { + loadURL: "http://example.com/a%20%C2%A0test", + expectedURL: "example.com/a %C2%A0test", + copyExpected: "http://example.com/a%20%C2%A0test", + }, + { + loadURL: "http://example.com/%20%20%20", + expectedURL: "example.com/%20%20%20", + copyExpected: "http://example.com/%20%20%20", + }, + { + loadURL: "http://example.com/%E3%80%80%E3%80%80", + expectedURL: "example.com/%E3%80%80%E3%80%80", + copyExpected: "http://example.com/%E3%80%80%E3%80%80", + }, + + // Loading of javascript: URI results in previous URI, so if the previous + // entry changes, change this one too! + { + loadURL: "javascript:('%C3%A9%20%25%50')", + expectedLoad: "http://example.com/%E3%80%80%E3%80%80", + expectedURL: "example.com/%E3%80%80%E3%80%80", + copyExpected: "http://example.com/%E3%80%80%E3%80%80", + }, + + // data: URIs shouldn't be encoded + { + loadURL: "data:text/html,(%C3%A9%20%25%50)", + expectedURL: "data:text/html,(%C3%A9 %25P)", + copyExpected: "data:text/html,(%C3%A9 %25P)", + }, + { + copyVal: "<data:text/html,(>%C3%A9 %25P)", + copyExpected: "data:text/html,(", + }, + { + copyVal: "<data:text/html,(%C3%A9 %25P>)", + copyExpected: "data:text/html,(%C3%A9 %25P", + }, + + { + async setup() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.decodeURLsOnCopy", true]], + }); + }, + async cleanup() { + await SpecialPowers.popPrefEnv(); + }, + loadURL: + "http://example.com/%D0%B1%D0%B8%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1%8F", + expectedURL: "example.com/биография", + copyExpected: "http://example.com/биография", + }, + { + copyVal: "<example.com/би>ография", + copyExpected: "http://example.com/%D0%B1%D0%B8", + }, + + { + async setup() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.decodeURLsOnCopy", true]], + }); + // Setup a valid intranet url that resolves but is not yet known. + const proxyService = Cc[ + "@mozilla.org/network/protocol-proxy-service;1" + ].getService(Ci.nsIProtocolProxyService); + let proxyInfo = proxyService.newProxyInfo( + "http", + "localhost", + 8888, + "", + "", + 0, + 4096, + null + ); + this._proxyFilter = { + applyFilter(channel, defaultProxyInfo, callback) { + callback.onProxyFilterResult( + channel.URI.host === "mytest" ? proxyInfo : defaultProxyInfo + ); + }, + }; + proxyService.registerChannelFilter(this._proxyFilter, 0); + registerCleanupFunction(() => { + if (this._proxyFilter) { + proxyService.unregisterChannelFilter(this._proxyFilter); + } + }); + }, + async cleanup() { + await SpecialPowers.popPrefEnv(); + const proxyService = Cc[ + "@mozilla.org/network/protocol-proxy-service;1" + ].getService(Ci.nsIProtocolProxyService); + proxyService.unregisterChannelFilter(this._proxyFilter); + this._proxyFilter = null; + }, + loadURL: "http://mytest/", + expectedURL: "mytest", + expectedValueOnFocus: "http://mytest/", + copyExpected: "http://mytest/", + }, + + { + async setup() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.decodeURLsOnCopy", true]], + }); + }, + async cleanup() { + await SpecialPowers.popPrefEnv(); + }, + loadURL: "https://example.com/", + expectedURL: "https://example.com", + copyExpected: "https://example.com", + }, +]; + +var trimHttpsTests = [ + // pageproxystate="invalid" + { + setURL: "https://example.com/", + expectedURL: "example.com", + copyExpected: "example.com", + }, + { + copyVal: "<e>xample.com", + copyExpected: "e", + }, + { + copyVal: "<e>x<a>mple.com", + copyExpected: "ea", + }, + { + copyVal: "<e><xa>mple.com", + copyExpected: "exa", + }, + { + copyVal: "<e><xa>mple.co<m>", + copyExpected: "exam", + }, + { + copyVal: "<e><xample.co><m>", + copyExpected: "example.com", + }, + + // pageproxystate="valid" from this point on (due to the load) + { + loadURL: "https://example.com/", + expectedURL: "example.com", + copyExpected: "https://example.com/", + }, + { + copyVal: "<example.co>m", + copyExpected: "example.co", + }, + { + copyVal: "e<x>ample.com", + copyExpected: "x", + }, + { + copyVal: "<e>xample.com", + copyExpected: "e", + }, + { + copyVal: "<e>xample.co<m>", + copyExpected: "em", + }, + { + copyVal: "<exam><ple.com>", + copyExpected: "example.com", + }, + + { + loadURL: "https://example.com/foo", + expectedURL: "example.com/foo", + copyExpected: "https://example.com/foo", + }, + { + copyVal: "<example.com>/foo", + copyExpected: "https://example.com", + }, + { + copyVal: "<example>.com/foo", + copyExpected: "example", + }, + // Test that partially selected URL is copied with encoded spaces + { + loadURL: "https://example.com/%20space/test", + expectedURL: "example.com/ space/test", + copyExpected: "https://example.com/%20space/test", + }, + { + copyVal: "<example.com/ space>/test", + copyExpected: "https://example.com/%20space", + }, + { + copyVal: "<example.com/ space/test>", + copyExpected: "https://example.com/%20space/test", + }, + { + loadURL: "https://example.com/%20foo%20bar%20baz/", + expectedURL: "example.com/ foo bar baz/", + copyExpected: "https://example.com/%20foo%20bar%20baz/", + }, + { + copyVal: "<example.com/ foo bar> baz/", + copyExpected: "https://example.com/%20foo%20bar", + }, + { + copyVal: "example.<com/ foo bar> baz/", + copyExpected: "com/ foo bar", + }, + // Test escaping + { + loadURL: "https://example.com/()%28%29%C3%A9", + expectedURL: "example.com/()()\xe9", + copyExpected: "https://example.com/()%28%29%C3%A9", + }, + { + copyVal: "<example.com/(>)()\xe9", + copyExpected: "https://example.com/(", + }, + { + copyVal: "e<xample.com/(>)()\xe9", + copyExpected: "xample.com/(", + }, + + { + loadURL: "https://example.com/%C3%A9%C3%A9", + expectedURL: "example.com/\xe9\xe9", + copyExpected: "https://example.com/%C3%A9%C3%A9", + }, + { + copyVal: "e<xample.com/\xe9>\xe9", + copyExpected: "xample.com/\xe9", + }, + { + copyVal: "<example.com/\xe9>\xe9", + copyExpected: "https://example.com/%C3%A9", + } /* + { + // Note: it seems BrowserTestUtils.loadURI fails for unicode domains + loadURL: "https://sub2.xn--lt-uia.mochi.test:8888/foo", + expectedURL: "sub2.ält.mochi.test:8888/foo", + copyExpected: "https://sub2.ält.mochi.test:8888/foo", + }, + { + copyVal: "s<ub2.ält.mochi.test:8888/f>oo", + copyExpected: "ub2.ält.mochi.test:8888/f", + }, + { + copyVal: "<sub2.ält.mochi.test:8888/f>oo", + copyExpected: "https://sub2.%C3%A4lt.mochi.test:8888/f", + },*/, + + { + loadURL: "https://example.com/?%C3%B7%C3%B7", + expectedURL: "example.com/?\xf7\xf7", + copyExpected: "https://example.com/?%C3%B7%C3%B7", + }, + { + copyVal: "e<xample.com/?\xf7>\xf7", + copyExpected: "xample.com/?\xf7", + }, + { + copyVal: "<example.com/?\xf7>\xf7", + copyExpected: "https://example.com/?%C3%B7", + }, + { + loadURL: "https://example.com/a%20test", + expectedURL: "example.com/a test", + copyExpected: "https://example.com/a%20test", + }, + { + loadURL: "https://example.com/a%E3%80%80test", + expectedURL: "example.com/a%E3%80%80test", + copyExpected: "https://example.com/a%E3%80%80test", + }, + { + loadURL: "https://example.com/a%20%C2%A0test", + expectedURL: "example.com/a %C2%A0test", + copyExpected: "https://example.com/a%20%C2%A0test", + }, + { + loadURL: "https://example.com/%20%20%20", + expectedURL: "example.com/%20%20%20", + copyExpected: "https://example.com/%20%20%20", + }, + { + loadURL: "https://example.com/%E3%80%80%E3%80%80", + expectedURL: "example.com/%E3%80%80%E3%80%80", + copyExpected: "https://example.com/%E3%80%80%E3%80%80", + }, + + // Loading of javascript: URI results in previous URI, so if the previous + // entry changes, change this one too! + { + loadURL: "javascript:('%C3%A9%20%25%50')", + expectedLoad: "https://example.com/%E3%80%80%E3%80%80", + expectedURL: "example.com/%E3%80%80%E3%80%80", + copyExpected: "https://example.com/%E3%80%80%E3%80%80", + }, + + // data: URIs shouldn't be encoded + { + loadURL: "data:text/html,(%C3%A9%20%25%50)", + expectedURL: "data:text/html,(%C3%A9 %25P)", + copyExpected: "data:text/html,(%C3%A9 %25P)", + }, + { + copyVal: "<data:text/html,(>%C3%A9 %25P)", + copyExpected: "data:text/html,(", + }, + { + copyVal: "<data:text/html,(%C3%A9 %25P>)", + copyExpected: "data:text/html,(%C3%A9 %25P", + }, + + { + async setup() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.decodeURLsOnCopy", true]], + }); + }, + async cleanup() { + await SpecialPowers.popPrefEnv(); + }, + loadURL: + "https://example.com/%D0%B1%D0%B8%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1%8F", + expectedURL: "example.com/биография", + copyExpected: "https://example.com/биография", + }, + { + copyVal: "<example.com/би>ография", + copyExpected: "https://example.com/%D0%B1%D0%B8", + }, + { + async setup() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.decodeURLsOnCopy", true]], + }); + }, + async cleanup() { + await SpecialPowers.popPrefEnv(); + }, + loadURL: "http://example.com/", + expectedURL: "http://example.com", + copyExpected: "http://example.com", + }, +]; + +function testCopy(copyVal, targetValue) { + info("Expecting copy of: " + targetValue); + + if (copyVal) { + let offsets = []; + while (true) { + let startBracket = copyVal.indexOf("<"); + let endBracket = copyVal.indexOf(">"); + if (startBracket == -1 && endBracket == -1) { + break; + } + if (startBracket > endBracket || startBracket == -1) { + offsets = []; + break; + } + offsets.push([startBracket, endBracket - 1]); + copyVal = copyVal.replace("<", "").replace(">", ""); + } + if (!offsets.length || copyVal != gURLBar.value) { + ok(false, "invalid copyVal: " + copyVal); + } + gURLBar.selectionStart = offsets[0][0]; + gURLBar.selectionEnd = offsets[0][1]; + if (offsets.length > 1) { + let sel = gURLBar.editor.selection; + let r0 = sel.getRangeAt(0); + let node0 = r0.startContainer; + sel.removeAllRanges(); + offsets.map(function (startEnd) { + let range = r0.cloneRange(); + range.setStart(node0, startEnd[0]); + range.setEnd(node0, startEnd[1]); + sel.addRange(range); + }); + } + } else { + gURLBar.select(); + } + info(`Target Value ${targetValue}`); + return SimpleTest.promiseClipboardChange(targetValue, () => + goDoCommand("cmd_copy") + ); +} + +add_task(async function includingProtocol() { + await PlacesUtils.history.clear(); + await PlacesTestUtils.clearInputHistory(); + SpecialPowers.pushPrefEnv({ set: [["browser.urlbar.trimHttps", true]] }); + + await PlacesTestUtils.addVisits(["https://example.com/"]); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + // If the url is autofilled, the protocol should be included in the copied + // value. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example", + fireInputEvent: true, + }); + Assert.ok( + (await UrlbarTestUtils.getDetailsOfResultAt(window, 0)).autofill, + "The first result should be aufotill suggestion" + ); + + window.goDoCommand("cmd_selectAll"); + await SimpleTest.promiseClipboardChange("https://example.com/", () => + goDoCommand("cmd_copy") + ); + Assert.ok(true, "Expected value is copied"); + + // Then, when adding some more characters, should not be included. + gURLBar.selectionStart = gURLBar.value.length; + gURLBar.selectionEnd = gURLBar.value.length; + EventUtils.synthesizeKey("x"); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.ok( + !(await UrlbarTestUtils.getDetailsOfResultAt(window, 0)).autofill, + "The first result should not be aufotill suggestion" + ); + + window.goDoCommand("cmd_selectAll"); + await SimpleTest.promiseClipboardChange("example.com/x", () => + goDoCommand("cmd_copy") + ); + Assert.ok(true, "Expected value is copied"); + + await PlacesUtils.history.clear(); + await PlacesTestUtils.clearInputHistory(); +}); + +add_task(async function loadingPageInBlank() { + const home = `${TEST_BASE_URL}file_copying_home.html`; + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, home); + const onNewTabCreated = waitForNewTabWithLoadRequest(); + SpecialPowers.spawn(tab.linkedBrowser, [], function () { + content.document.querySelector("a").click(); + }); + const newtab = await onNewTabCreated; + await BrowserTestUtils.waitForCondition( + () => + newtab.linkedBrowser.browsingContext.mostRecentLoadingSessionHistoryEntry + ); + gURLBar.focus(); + window.goDoCommand("cmd_selectAll"); + await SimpleTest.promiseClipboardChange( + "https://example.com/browser/browser/components/urlbar/tests/browser/wait-a-bit.sjs", + () => goDoCommand("cmd_copy") + ); + Assert.ok(true, "Expected value is copied"); + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(newtab); +}); + +async function waitForNewTabWithLoadRequest() { + return new Promise(resolve => + gBrowser.addTabsProgressListener({ + onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) { + gBrowser.removeTabsProgressListener(this); + resolve(gBrowser.getTabForBrowser(aBrowser)); + } + }, + }) + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_customizeMode.js b/browser/components/urlbar/tests/browser/browser_customizeMode.js new file mode 100644 index 0000000000..0ed26644cc --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_customizeMode.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test checks that the left/right arrow keys and home/end keys work in +// the urlbar after customize mode starts and ends. + +"use strict"; + +add_task(async function test() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await startCustomizing(win); + await endCustomizing(win); + + let urlbar = win.gURLBar; + + let value = "example"; + urlbar.value = value; + urlbar.focus(); + urlbar.selectionEnd = value.length; + urlbar.selectionStart = value.length; + + // left + EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win); + Assert.equal(urlbar.selectionStart, value.length - 1); + Assert.equal(urlbar.selectionEnd, value.length - 1); + + // home + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey("KEY_ArrowLeft", { metaKey: true }, win); + } else { + EventUtils.synthesizeKey("KEY_Home", {}, win); + } + Assert.equal(urlbar.selectionStart, 0); + Assert.equal(urlbar.selectionEnd, 0); + + // right + EventUtils.synthesizeKey("KEY_ArrowRight", {}, win); + Assert.equal(urlbar.selectionStart, 1); + Assert.equal(urlbar.selectionEnd, 1); + + // end + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey("KEY_ArrowRight", { metaKey: true }, win); + } else { + EventUtils.synthesizeKey("KEY_End", {}, win); + } + Assert.equal(urlbar.selectionStart, value.length); + Assert.equal(urlbar.selectionEnd, value.length); + + await BrowserTestUtils.closeWindow(win); +}); + +async function startCustomizing(win = window) { + if (win.document.documentElement.getAttribute("customizing") != "true") { + let eventPromise = BrowserTestUtils.waitForEvent( + win.gNavToolbox, + "customizationready" + ); + win.gCustomizeMode.enter(); + await eventPromise; + } +} + +async function endCustomizing(win = window) { + if (win.document.documentElement.getAttribute("customizing") == "true") { + let eventPromise = BrowserTestUtils.waitForEvent( + win.gNavToolbox, + "aftercustomization" + ); + win.gCustomizeMode.exit(); + await eventPromise; + } +} diff --git a/browser/components/urlbar/tests/browser/browser_cutting.js b/browser/components/urlbar/tests/browser/browser_cutting.js new file mode 100644 index 0000000000..87e1b01695 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_cutting.js @@ -0,0 +1,16 @@ +/* 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/. */ + +add_task(async function test() { + await UrlbarTestUtils.inputIntoURLBar(window, "https://example.com/"); + gURLBar.selectionStart = 4; + gURLBar.selectionEnd = 5; + goDoCommand("cmd_cut"); + is( + gURLBar.value, + "http://example.com/", + "location bar value after cutting 's' from https" + ); + gURLBar.handleRevert(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_decode.js b/browser/components/urlbar/tests/browser/browser_decode.js new file mode 100644 index 0000000000..577d39b587 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_decode.js @@ -0,0 +1,144 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This test makes sure (1) you can't break the urlbar by typing particular JSON +// or JS fragments into it, (2) urlbar.textValue shows URLs unescaped, and (3) +// the urlbar also shows the URLs embedded in action URIs unescaped. See bug +// 1233672. + +add_task(async function injectJSON() { + let inputStrs = [ + 'http://example.com/ ", "url": "bar', + "http://example.com/\\", + 'http://example.com/"', + 'http://example.com/","url":"evil.com', + "http://mozilla.org/\\u0020", + 'http://www.mozilla.org/","url":1e6,"some-key":"foo', + 'http://www.mozilla.org/","url":null,"some-key":"foo', + 'http://www.mozilla.org/","url":["foo","bar"],"some-key":"foo', + ]; + for (let inputStr of inputStrs) { + await checkInput(inputStr); + } + gURLBar.value = ""; + gURLBar.handleRevert(); + gURLBar.blur(); +}); + +add_task(function losslessDecode() { + let urlNoScheme = "example.com/\u30a2\u30a4\u30a6\u30a8\u30aa"; + let url = UrlbarTestUtils.getTrimmedProtocolWithSlashes() + urlNoScheme; + const result = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url } + ); + gURLBar.setValueFromResult({ result }); + // Since this is directly setting textValue, it is expected to be trimmed. + Assert.equal( + gURLBar.value, + urlNoScheme, + "The string displayed in the textbox should not be escaped" + ); + gURLBar.value = ""; + gURLBar.handleRevert(); + gURLBar.blur(); +}); + +add_task(async function actionURILosslessDecode() { + let urlNoScheme = "example.com/\u30a2\u30a4\u30a6\u30a8\u30aa"; + let url = UrlbarTestUtils.getTrimmedProtocolWithSlashes() + urlNoScheme; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: url, + }); + + // At this point the heuristic result is selected but the urlbar's value is + // simply `url`. Key down and back around until the heuristic result is + // selected again. + do { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } while (UrlbarTestUtils.getSelectedRowIndex(window) != 0); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.URL, + "Should have selected a result of URL type" + ); + + Assert.equal( + gURLBar.value, + UrlbarTestUtils.trimURL(urlNoScheme), + "The string displayed in the textbox should not be escaped" + ); + + gURLBar.value = ""; + gURLBar.handleRevert(); + gURLBar.blur(); +}); + +add_task(async function test_resultsDisplayDecoded() { + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + + await PlacesTestUtils.addVisits("http://example.com/%E9%A1%B5"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example", + }); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + result.displayed.url, + "http://example.com/\u9875", + "Should be displayed the correctly unescaped URL" + ); +}); + +async function checkInput(inputStr) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: inputStr, + }); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + // URL matches have their param.urls fixed up. + let fixupInfo = Services.uriFixup.getFixupURIInfo( + inputStr, + Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | + Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP + ); + let expectedVisitURL = fixupInfo.fixedURI.spec; + + Assert.equal(result.url, expectedVisitURL, "Should have the correct URL"); + Assert.equal( + result.title, + inputStr.replace("\\", "/"), + "Should have the correct title" + ); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.URL, + "Should have be a result of type URL" + ); + + Assert.equal( + result.displayed.title, + inputStr.replace("\\", "/"), + "Should be displaying the correct text" + ); + let [action] = await document.l10n.formatValues([ + { id: "urlbar-result-action-visit" }, + ]); + Assert.equal( + result.displayed.action, + action, + "Should be displaying the correct action text" + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_delete.js b/browser/components/urlbar/tests/browser/browser_delete.js new file mode 100644 index 0000000000..f1f85c4cd0 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_delete.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test deleting the start of urls works correctly. + */ + +add_task(async function () { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://bug1105244.example.com/", + title: "test", + }); + + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.remove(bm); + }); + + await BrowserTestUtils.withNewTab("about:blank", testDelete); +}); + +function sendHome() { + // unclear why VK_HOME doesn't work on Mac, but it doesn't... + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey("KEY_ArrowLeft", { metaKey: true }); + } else { + EventUtils.synthesizeKey("KEY_Home"); + } +} + +function sendDelete() { + EventUtils.synthesizeKey("KEY_Delete"); +} + +async function testDelete() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "bug1105244", + }); + + // move to the start. + sendHome(); + + // delete the first few chars - each delete should operate on the input field. + await UrlbarTestUtils.promisePopupOpen(window, sendDelete); + Assert.equal(gURLBar.value, "ug1105244.example.com/"); + sendDelete(); + Assert.equal(gURLBar.value, "g1105244.example.com/"); +} diff --git a/browser/components/urlbar/tests/browser/browser_deleteAllText.js b/browser/components/urlbar/tests/browser/browser_deleteAllText.js new file mode 100644 index 0000000000..5b355fa477 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_deleteAllText.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test makes sure that deleting all text in the input doesn't mess up +// subsequent searches. + +"use strict"; + +add_task(async function test() { + await runTest(); + // Setting suggest.topsites to false disables the view's autoOpen behavior, + // which changes this test's outcomes. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.topsites", false]], + }); + info("Running the test with autoOpen disabled."); + await runTest(); + await SpecialPowers.popPrefEnv(); +}); + +async function runTest() { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits([ + "http://example.com/", + "http://mozilla.org/", + ]); + + // Do an initial search for "x". + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "x", + fireInputEvent: true, + }); + await checkResults(); + + await deleteInput(); + + // Type "x". A new search should start. Don't use + // promiseAutocompleteResultPopup, which has some logic that starts the search + // manually in certain conditions. We want to specifically check that the + // input event causes UrlbarInput to start a new search on its own. If it + // doesn't, then the test will hang here on promiseSearchComplete. + EventUtils.synthesizeKey("x"); + await UrlbarTestUtils.promiseSearchComplete(window); + await checkResults(); + + // Now repeat the backspace + x two more times. Same thing should happen. + for (let i = 0; i < 2; i++) { + await deleteInput(); + EventUtils.synthesizeKey("x"); + await UrlbarTestUtils.promiseSearchComplete(window); + await checkResults(); + } + + await deleteInput(); + // autoOpen opened the panel, so we need to close it. + gURLBar.view.close(); +} + +async function checkResults() { + Assert.equal(await UrlbarTestUtils.getResultCount(window), 2); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(details.type, UrlbarUtils.RESULT_TYPE.SEARCH); + Assert.equal(details.searchParams.query, "x"); + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(details.type, UrlbarUtils.RESULT_TYPE.URL); + Assert.equal(details.url, "http://example.com/"); +} + +async function deleteInput() { + if (UrlbarPrefs.get("suggest.topsites")) { + // The popup should remain open and show top sites. + while (gURLBar.value.length) { + EventUtils.synthesizeKey("KEY_Backspace"); + } + Assert.ok( + gURLBar.view.isOpen, + "View should remain open when deleting all input text" + ); + let queryContext = await UrlbarTestUtils.promiseSearchComplete(window); + Assert.notEqual( + queryContext.results.length, + 0, + "View should show results when deleting all input text" + ); + Assert.equal( + queryContext.searchString, + "", + "Results should be for the empty search string (i.e. top sites) when deleting all input text" + ); + } else { + // Deleting all text should close the view. + await UrlbarTestUtils.promisePopupClose(window, () => { + while (gURLBar.value.length) { + EventUtils.synthesizeKey("KEY_Backspace"); + } + }); + } +} diff --git a/browser/components/urlbar/tests/browser/browser_display_selectedAction_Extensions.js b/browser/components/urlbar/tests/browser/browser_display_selectedAction_Extensions.js new file mode 100644 index 0000000000..64a086b0cb --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_display_selectedAction_Extensions.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests for the presence of selected action text "Extensions:" in the URL bar. + */ + +add_task(async function testSwitchToTabTextDisplay() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + omnibox: { + keyword: "omniboxtest", + }, + + background() { + /* global browser */ + browser.omnibox.setDefaultSuggestion({ + description: "doit", + }); + // Just do nothing for this test. + }, + }, + }); + + await extension.startup(); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "omniboxtest ", + fireInputEvent: true, + }); + + // The "Extension:" label appears after a key down followed by a key up + // back to the extension result. + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + + // Checks to see if "Extension:" text in URL bar is visible + const extensionText = document.getElementById("urlbar-label-extension"); + Assert.ok(BrowserTestUtils.isVisible(extensionText)); + Assert.equal(extensionText.value, "Extension:"); + + // Check to see if all other labels are hidden + const allLabels = document.getElementById("urlbar-label-box").children; + for (let label of allLabels) { + if (label != extensionText) { + Assert.ok(BrowserTestUtils.isHidden(label)); + } + } + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + await extension.unload(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js b/browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js new file mode 100644 index 0000000000..a0aacc83d2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +// Checks that if browser.fixup.dns_first_for_single_words pref is set, we pass +// the original search string to the docshell and not a search url. + +add_task(async function test() { + const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" + ); + const sandbox = sinon.createSandbox(); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.fixup.dns_first_for_single_words", true]], + }); + + registerCleanupFunction(sandbox.restore); + + /** + * Tests the given search string. + * + * @param {string} str The search string + * @param {boolean} passthrough whether the value should be passed unchanged + * to the docshell that will first execute a DNS request. + */ + async function testVal(str, passthrough) { + sandbox.stub(gURLBar, "_loadURL").callsFake(url => { + if (passthrough) { + Assert.equal(url, str, "Should pass the unmodified search string"); + } else { + Assert.ok(url.startsWith("http"), "Should pass an url"); + } + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: str, + }); + EventUtils.synthesizeKey("KEY_Enter"); + sandbox.restore(); + } + + await testVal("test", true); + await testVal("te-st", true); + await testVal("test ", true); + await testVal(" test", true); + await testVal(" test", true); + await testVal("test.test", true); + await testVal("test test", false); + // This is not a single word host, though it contains one. At a certain point + // we may evaluate to increase coverage of the feature to also ask for this. + await testVal("test/test", false); +}); diff --git a/browser/components/urlbar/tests/browser/browser_downArrowKeySearch.js b/browser/components/urlbar/tests/browser/browser_downArrowKeySearch.js new file mode 100644 index 0000000000..215f21bd3f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_downArrowKeySearch.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Checks that pressing the down arrow key starts the proper searches, depending +// on the input value/state. + +"use strict"; + +add_setup(async function () { + await PlacesUtils.history.clear(); + // Enough vists to get this site into Top Sites. + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits("http://example.com/"); + } + + await updateTopSites( + sites => sites && sites[0] && sites[0].url == "http://example.com/" + ); + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function url() { + await BrowserTestUtils.withNewTab("http://example.com/", async () => { + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + gURLBar.focus(); + gURLBar.selectionEnd = gURLBar.untrimmedValue.length; + gURLBar.selectionStart = gURLBar.untrimmedValue.length; + EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(details.url, "http://example.com/"); + Assert.equal( + gURLBar.value, + UrlbarTestUtils.trimURL("http://example.com/", { + removeSingleTrailingSlash: false, + }) + ); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +add_task(async function userTyping() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(details.type, UrlbarUtils.RESULT_TYPE.SEARCH); + Assert.ok(details.searchParams); + Assert.equal(details.searchParams.query, "foo"); + Assert.equal(gURLBar.value, "foo"); + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function empty() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), -1); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(details.url, "http://example.com/"); + Assert.equal(gURLBar.value, ""); +}); + +add_task(async function new_window() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + win.gURLBar.focus(); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + await UrlbarTestUtils.promiseSearchComplete(win); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(win), -1); + let details = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + Assert.equal(details.url, "http://example.com/"); + Assert.equal(win.gURLBar.value, ""); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/urlbar/tests/browser/browser_dragdropURL.js b/browser/components/urlbar/tests/browser/browser_dragdropURL.js new file mode 100644 index 0000000000..52c19e8965 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_dragdropURL.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests for draging and dropping to the Urlbar. + */ + +const TEST_URL = "data:text/html,a test page"; + +add_task(async function test_setup() { + // Stop search-engine loads from hitting the network. + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + registerCleanupFunction(async function cleanup() { + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]); + } + }); + + CustomizableUI.addWidgetToArea("home-button", "nav-bar"); + registerCleanupFunction(() => + CustomizableUI.removeWidgetFromArea("home-button") + ); +}); + +/** + * Simulates a drop on the URL bar input field. + * The drag source must be something different from the URL bar, so we pick the + * home button somewhat arbitrarily. + * + * @param {object} content a {type, data} object representing the DND content. + */ +function simulateURLBarDrop(content) { + EventUtils.synthesizeDrop( + document.getElementById("home-button"), // Dragstart element. + gURLBar.inputField, // Drop element. + [[content]], // Drag data. + "copy", + window + ); +} + +add_task(async function checkDragURL() { + await BrowserTestUtils.withNewTab(TEST_URL, function (browser) { + info("Check dragging a normal url to the urlbar"); + const DRAG_URL = "http://www.example.com/"; + simulateURLBarDrop({ type: "text/plain", data: DRAG_URL }); + Assert.equal( + gURLBar.value, + TEST_URL, + "URL bar value should not have changed" + ); + Assert.equal( + gBrowser.selectedBrowser.userTypedValue, + null, + "Stored URL bar value should not have changed" + ); + }); +}); + +add_task(async function checkDragForbiddenURL() { + await BrowserTestUtils.withNewTab(TEST_URL, function (browser) { + // See also browser_removeUnsafeProtocolsFromURLBarPaste.js for other + // examples. In general we trust that function, we pick some testcases to + // ensure we disallow dropping trimmed text. + for (let url of [ + "chrome://browser/content/aboutDialog.xhtml", + "file:///", + "javascript:", + "javascript:void(0)", + "java\r\ns\ncript:void(0)", + " javascript:void(0)", + "\u00A0java\nscript:void(0)", + "javascript:document.domain", + "javascript:javascript:alert('hi!')", + ]) { + info(`Check dragging "{$url}" to the URL bar`); + simulateURLBarDrop({ type: "text/plain", data: url }); + Assert.notEqual( + gURLBar.value, + url, + `Shouldn't be allowed to drop ${url} on URL bar` + ); + } + }); +}); + +add_task(async function checkDragText() { + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + info("Check dragging multi word text to the urlbar"); + const TEXT = "Firefox is awesome"; + const TEXT_URL = "https://example.com/?q=Firefox+is+awesome"; + let promiseLoad = BrowserTestUtils.browserLoaded(browser, false, TEXT_URL); + simulateURLBarDrop({ type: "text/plain", data: TEXT }); + await promiseLoad; + + info("Check dragging single word text to the urlbar"); + const WORD = "Firefox"; + const WORD_URL = "https://example.com/?q=Firefox"; + promiseLoad = BrowserTestUtils.browserLoaded(browser, false, WORD_URL); + simulateURLBarDrop({ type: "text/plain", data: WORD }); + await promiseLoad; + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_dynamicResults.js b/browser/components/urlbar/tests/browser/browser_dynamicResults.js new file mode 100644 index 0000000000..976ae3b9cb --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_dynamicResults.js @@ -0,0 +1,998 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests dynamic results. + */ + +"use strict"; + +const DYNAMIC_TYPE_NAME = "test"; + +const DYNAMIC_TYPE_VIEW_TEMPLATE = { + stylesheet: getRootDirectory(gTestPath) + "dynamicResult0.css", + children: [ + { + name: "selectable", + tag: "span", + attributes: { + selectable: "true", + }, + }, + { + name: "text", + tag: "span", + }, + { + name: "buttonBox", + tag: "span", + children: [ + { + name: "button1", + tag: "span", + attributes: { + role: "button", + attribute_to_remove: "value", + }, + }, + { + name: "button2", + tag: "span", + attributes: { + role: "button", + }, + }, + ], + }, + ], +}; + +const IS_UPGRADING_SCHEMELESS = SpecialPowers.getBoolPref( + "dom.security.https_first_schemeless" +); +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const DEFAULT_URL_SCHEME = IS_UPGRADING_SCHEMELESS ? "https://" : "http://"; +const DUMMY_PAGE = + DEFAULT_URL_SCHEME + + "example.com/browser/browser/base/content/test/general/dummy_page.html"; + +// Tests the dynamic type registration functions and stylesheet loading. +add_task(async function registration() { + // Get our test stylesheet URIs. + let stylesheetURIs = []; + for (let i = 0; i < 2; i++) { + stylesheetURIs.push( + Services.io.newURI(getRootDirectory(gTestPath) + `dynamicResult${i}.css`) + ); + } + + // Maps from dynamic type names to their type. + let viewTemplatesByName = { + foo: { + stylesheet: stylesheetURIs[0].spec, + children: [ + { + name: "text", + tag: "span", + }, + ], + }, + bar: { + stylesheet: stylesheetURIs[1].spec, + children: [ + { + name: "icon", + tag: "span", + }, + { + name: "button", + tag: "span", + attributes: { + role: "button", + }, + }, + ], + }, + }; + + // First, open another window so that multiple windows are open when we add + // the types so we can verify below that the stylesheets are added to all open + // windows. + let newWindows = []; + newWindows.push(await BrowserTestUtils.openNewBrowserWindow()); + + // Add the test dynamic types. + for (let [name, viewTemplate] of Object.entries(viewTemplatesByName)) { + UrlbarResult.addDynamicResultType(name); + UrlbarView.addDynamicViewTemplate(name, viewTemplate); + } + + // Get them back to make sure they were added. + for (let name of Object.keys(viewTemplatesByName)) { + let actualType = UrlbarResult.getDynamicResultType(name); + // Types are currently just empty objects. + Assert.deepEqual(actualType, {}, "Types should match"); + } + + // Their stylesheets should have been applied to all open windows. There's no + // good way to check this because: + // + // * nsIStyleSheetService has a function that returns whether a stylesheet has + // been loaded, but it's global and not per window. + // * nsIDOMWindowUtils has functions to load stylesheets but not one to check + // whether a stylesheet has been loaded. + // * document.stylesheets only contains stylesheets in the DOM. + // + // So instead we set a CSS variable on #urlbar in each of our stylesheets and + // check that it's present. + function getCSSVariables(windows) { + let valuesByWindow = new Map(); + for (let win of windows) { + let values = []; + valuesByWindow.set(window, values); + for (let i = 0; i < stylesheetURIs.length; i++) { + let value = win + .getComputedStyle(gURLBar.panel) + .getPropertyValue(`--testDynamicResult${i}`); + values.push((value || "").trim()); + } + } + return valuesByWindow; + } + function checkCSSVariables(windows) { + for (let values of getCSSVariables(windows).values()) { + for (let i = 0; i < stylesheetURIs.length; i++) { + if (values[i].trim() !== `ok${i}`) { + return false; + } + } + } + return true; + } + + // The stylesheets are loaded asyncly, so we need to poll for it. + await TestUtils.waitForCondition(() => + checkCSSVariables(BrowserWindowTracker.orderedWindows) + ); + Assert.ok(true, "Stylesheets loaded in all open windows"); + + // Open another window to make sure the stylesheets are loaded in it after we + // added the new dynamic types. + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + newWindows.push(newWin); + await TestUtils.waitForCondition(() => checkCSSVariables([newWin])); + Assert.ok(true, "Stylesheets loaded in new window"); + + // Remove the dynamic types. + for (let name of Object.keys(viewTemplatesByName)) { + UrlbarView.removeDynamicViewTemplate(name); + UrlbarResult.removeDynamicResultType(name); + let actualType = UrlbarResult.getDynamicResultType(name); + Assert.equal(actualType, null, "Type should be unregistered"); + } + + // The stylesheets should be removed from all windows. + let valuesByWindow = getCSSVariables(BrowserWindowTracker.orderedWindows); + for (let values of valuesByWindow.values()) { + for (let i = 0; i < stylesheetURIs.length; i++) { + Assert.ok(!values[i], "Stylesheet should be removed"); + } + } + + // Close the new windows. + for (let win of newWindows) { + await BrowserTestUtils.closeWindow(win); + } +}); + +// Tests that the view is created correctly from the view template. +add_task(async function viewCreated() { + await withDynamicTypeProvider(async () => { + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + waitForFocus: SimpleTest.waitForFocus, + }); + + // Get the row. + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + Assert.equal( + row.result.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "row.result.type" + ); + Assert.equal( + row.getAttribute("dynamicType"), + DYNAMIC_TYPE_NAME, + "row[dynamicType]" + ); + Assert.ok( + !row.hasAttribute("has-url"), + "Row should not have has-url since view template does not contain .urlbarView-url" + ); + let inner = row.querySelector(".urlbarView-row-inner"); + Assert.ok(inner, ".urlbarView-row-inner should exist"); + + // Check the DOM. + checkDOM(inner, DYNAMIC_TYPE_VIEW_TEMPLATE.children); + + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Tests that the view is updated correctly. +async function checkViewUpdated(provider) { + await withDynamicTypeProvider(async () => { + // Test a few different search strings. The dynamic result view will be + // updated to reflect the current string. + for (let searchString of ["test", "some other string", "and another"]) { + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + waitForFocus: SimpleTest.waitForFocus, + }); + + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + let text = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text` + ); + + // The view's call to provider.getViewUpdate is async, so we need to make + // sure the update has been applied before continuing to avoid + // intermittent failures. + await TestUtils.waitForCondition( + () => text.getAttribute("searchString") == searchString + ); + + // The "searchString" attribute of these elements should be updated. + let elementNames = ["selectable", "text", "button1", "button2"]; + for (let name of elementNames) { + let element = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${name}` + ); + Assert.equal( + element.getAttribute("searchString"), + searchString, + 'element.getAttribute("searchString")' + ); + } + + let button1 = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-button1` + ); + + Assert.equal( + button1.hasAttribute("attribute_to_remove"), + false, + "Attribute should be removed" + ); + + // text.textContent should be updated. + Assert.equal( + text.textContent, + `result.payload.searchString is: ${searchString}`, + "text.textContent" + ); + + await UrlbarTestUtils.promisePopupClose(window); + } + }, provider); +} + +add_task(async function checkViewUpdatedPlain() { + await checkViewUpdated(new TestProvider()); +}); + +add_task(async function checkViewUpdatedWDynamicViewTemplate() { + /** + * A dummy provider that provides the viewTemplate dynamically. + */ + class TestShouldCallGetViewTemplateProvider extends TestProvider { + getViewTemplateWasCalled = false; + + getViewTemplate() { + this.getViewTemplateWasCalled = true; + return DYNAMIC_TYPE_VIEW_TEMPLATE; + } + } + + let provider = new TestShouldCallGetViewTemplateProvider(); + Assert.ok( + !provider.getViewTemplateWasCalled, + "getViewTemplate has not yet been called for the provider" + ); + Assert.ok( + !UrlbarView.dynamicViewTemplatesByName.get(DYNAMIC_TYPE_NAME), + "No template has been registered" + ); + await checkViewUpdated(provider); + Assert.ok( + provider.getViewTemplateWasCalled, + "getViewTemplate was called for the provider" + ); +}); + +// Tests that selection correctly moves through buttons and selectables in a +// dynamic result. +add_task(async function selection() { + await withDynamicTypeProvider(async () => { + // Add a visit so we have at least one result after the dynamic result. + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits("http://example.com/test"); + + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + waitForFocus: SimpleTest.waitForFocus, + }); + + // Sanity check that the dynamic result is at index 1. + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + Assert.equal( + row.result.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "row.result.type" + ); + + // The heuristic result will be selected. TAB from the heuristic through + // all the selectable elements in the dynamic result. + let selectables = ["selectable", "button1", "button2"]; + for (let name of selectables) { + let element = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${name}` + ); + Assert.ok(element, "Sanity check element"); + EventUtils.synthesizeKey("KEY_Tab"); + Assert.equal( + UrlbarTestUtils.getSelectedElement(window), + element, + `Selected element: ${name}` + ); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "Row at index 1 selected" + ); + Assert.equal(UrlbarTestUtils.getSelectedRow(window), row, "Row selected"); + } + + // TAB again to select the result after the dynamic result. + EventUtils.synthesizeKey("KEY_Tab"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 2, + "Row at index 2 selected" + ); + Assert.notEqual( + UrlbarTestUtils.getSelectedRow(window), + row, + "Row is not selected" + ); + + // SHIFT+TAB back through the dynamic result. + for (let name of selectables.reverse()) { + let element = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${name}` + ); + Assert.ok(element, "Sanity check element"); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + Assert.equal( + UrlbarTestUtils.getSelectedElement(window), + element, + `Selected element: ${name}` + ); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "Row at index 1 selected" + ); + Assert.equal(UrlbarTestUtils.getSelectedRow(window), row, "Row selected"); + } + + // SHIFT+TAB again to select the heuristic result. + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "Row at index 0 selected" + ); + Assert.notEqual( + UrlbarTestUtils.getSelectedRow(window), + row, + "Row is not selected" + ); + + await UrlbarTestUtils.promisePopupClose(window); + await PlacesUtils.history.clear(); + }); +}); + +// Tests picking elements in a dynamic result. +add_task(async function pick() { + await withDynamicTypeProvider(async provider => { + let selectables = ["selectable", "button1", "button2"]; + for (let i = 0; i < selectables.length; i++) { + let selectable = selectables[i]; + + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + waitForFocus: SimpleTest.waitForFocus, + }); + + // Sanity check that the dynamic result is at index 1. + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + Assert.equal( + row.result.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "row.result.type" + ); + + // The heuristic result will be selected. TAB from the heuristic + // to the selectable element. + let element = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${selectable}` + ); + Assert.ok(element, "Sanity check element"); + EventUtils.synthesizeKey("KEY_Tab", { repeat: i + 1 }); + Assert.equal( + UrlbarTestUtils.getSelectedElement(window), + element, + `Selected element: ${name}` + ); + + // Pick the element. + let pickPromise = provider.promisePick(); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Enter") + ); + let [result, pickedElement] = await pickPromise; + Assert.equal(result, row.result, "Picked result"); + Assert.equal(pickedElement, element, "Picked element"); + } + }); +}); + +// Tests picking elements in a dynamic result. +add_task(async function shouldNavigate() { + /** + * A dummy provider that providers results with a `shouldNavigate` property. + */ + class TestShouldNavigateProvider extends TestProvider { + /** + * @param {object} context - Data regarding the context of the query. + * @param {Function} addCallback - Function to add a result to the query. + */ + async startQuery(context, addCallback) { + for (let result of this.results) { + result.payload.searchString = context.searchString; + result.payload.shouldNavigate = true; + result.payload.url = DUMMY_PAGE; + addCallback(this, result); + } + } + } + + await withDynamicTypeProvider(async provider => { + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + waitForFocus: SimpleTest.waitForFocus, + }); + + // Sanity check that the dynamic result is at index 1. + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + Assert.equal( + row.result.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "row.result.type" + ); + + // The heuristic result will be selected. TAB from the heuristic + // to the selectable element. + let element = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-selectable` + ); + Assert.ok(element, "Sanity check element"); + EventUtils.synthesizeKey("KEY_Tab", { repeat: 1 }); + Assert.equal( + UrlbarTestUtils.getSelectedElement(window), + element, + `Selected element: ${name}` + ); + + // Pick the element. + let pickPromise = provider.promisePick(); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Enter") + ); + // Verify that onEngagement was still called. + let [result, pickedElement] = await pickPromise; + Assert.equal(result, row.result, "Picked result"); + Assert.equal(pickedElement, element, "Picked element"); + + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + is( + gBrowser.currentURI.spec, + DUMMY_PAGE, + "We navigated to payload.url when result selected" + ); + + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + "about:home" + ); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "about:home" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + waitForFocus: SimpleTest.waitForFocus, + }); + + row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + element = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-selectable` + ); + + pickPromise = provider.promisePick(); + EventUtils.synthesizeMouseAtCenter(element, {}); + [result, pickedElement] = await pickPromise; + Assert.equal(result, row.result, "Picked result"); + Assert.equal(pickedElement, element, "Picked element"); + + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + is( + gBrowser.currentURI.spec, + DUMMY_PAGE, + "We navigated to payload.url when result is clicked" + ); + }, new TestShouldNavigateProvider()); +}); + +// Tests applying highlighting to a dynamic result. +add_task(async function highlighting() { + /** + * Provides a dynamic result with highlighted text. + */ + class TestHighlightProvider extends TestProvider { + startQuery(context, addCallback) { + let result = Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.DYNAMIC, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + ...UrlbarResult.payloadAndSimpleHighlights(context.tokens, { + dynamicType: DYNAMIC_TYPE_NAME, + text: ["Test title", UrlbarUtils.HIGHLIGHT.SUGGESTED], + }) + ), + { suggestedIndex: 1 } + ); + addCallback(this, result); + } + + getViewUpdate(result, idsByName) { + return {}; + } + } + + // Test that highlighting is applied. + await withDynamicTypeProvider(async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + waitForFocus: SimpleTest.waitForFocus, + }); + + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + Assert.equal( + row.result.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "row.result.type" + ); + let parentTextNode = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text` + ); + let highlightedTextNode = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text > strong` + ); + Assert.equal(parentTextNode.firstChild.textContent, "Test"); + Assert.equal( + highlightedTextNode.textContent, + " title", + "The highlighting was applied successfully." + ); + }, new TestHighlightProvider()); + + /** + * Provides a dynamic result with highlighted text that is then overridden. + */ + class TestHighlightProviderOveridden extends TestHighlightProvider { + getViewUpdate(result, idsByName) { + return { + text: { + textContent: "Test title", + }, + }; + } + } + + // Test that highlighting is not applied when overridden from getViewUpdate. + await withDynamicTypeProvider(async () => { + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + waitForFocus: SimpleTest.waitForFocus, + }); + + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + Assert.equal( + row.result.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "row.result.type" + ); + let parentTextNode = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text` + ); + let highlightedTextNode = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text > strong` + ); + Assert.equal( + parentTextNode.firstChild.textContent, + "Test title", + "No highlighting was applied" + ); + Assert.ok(!highlightedTextNode, "The <strong> child node was deleted."); + }, new TestHighlightProviderOveridden()); +}); + +// View templates that contain a top-level `.urlbarView-url` element should +// cause `has-url` to be set on `.urlbarView-row`. +add_task(async function hasUrlTopLevel() { + await doAttributesTest({ + viewTemplate: { + name: "url", + tag: "span", + classList: ["urlbarView-url"], + }, + viewUpdate: { + url: { + textContent: "https://example.com/", + }, + }, + expectedAttributes: { + "has-url": true, + }, + }); +}); + +// View templates that contain a descendant `.urlbarView-url` element should +// cause `has-url` to be set on `.urlbarView-row`. +add_task(async function hasUrlDescendant() { + await doAttributesTest({ + viewTemplate: { + children: [ + { + children: [ + { + children: [ + { + name: "url", + tag: "span", + classList: ["urlbarView-url"], + }, + ], + }, + ], + }, + ], + }, + viewUpdate: { + url: { + textContent: "https://example.com/", + }, + }, + expectedAttributes: { + "has-url": true, + }, + }); +}); + +// View templates that contain a top-level `.urlbarView-action` element should +// cause `has-action` to be set on `.urlbarView-row`. +add_task(async function hasActionTopLevel() { + await doAttributesTest({ + viewTemplate: { + name: "action", + tag: "span", + classList: ["urlbarView-action"], + }, + viewUpdate: { + action: { + textContent: "Some action text", + }, + }, + expectedAttributes: { + "has-action": true, + }, + }); +}); + +// View templates that contain a descendant `.urlbarView-action` element should +// cause `has-action` to be set on `.urlbarView-row`. +add_task(async function hasActionDescendant() { + await doAttributesTest({ + viewTemplate: { + children: [ + { + children: [ + { + children: [ + { + name: "action", + tag: "span", + classList: ["urlbarView-action"], + }, + ], + }, + ], + }, + ], + }, + viewUpdate: { + action: { + textContent: "Some action text", + }, + }, + expectedAttributes: { + "has-action": true, + }, + }); +}); + +// View templates that contain descendant `.urlbarView-url` and +// `.urlbarView-action` elements should cause `has-url` and `has-action` to be +// set on `.urlbarView-row`. +add_task(async function hasUrlAndActionDescendant() { + await doAttributesTest({ + viewTemplate: { + children: [ + { + children: [ + { + children: [ + { + name: "url", + tag: "span", + classList: ["urlbarView-url"], + }, + ], + }, + { + name: "action", + tag: "span", + classList: ["urlbarView-action"], + }, + ], + }, + ], + }, + viewUpdate: { + url: { + textContent: "https://example.com/", + }, + action: { + textContent: "Some action text", + }, + }, + expectedAttributes: { + "has-url": true, + "has-action": true, + }, + }); +}); + +async function doAttributesTest({ + viewTemplate, + viewUpdate, + expectedAttributes, +}) { + expectedAttributes = { + "has-url": false, + "has-action": false, + ...expectedAttributes, + }; + + let provider = new TestProvider(); + provider.getViewTemplate = () => viewTemplate; + provider.getViewUpdate = () => viewUpdate; + + await withDynamicTypeProvider(async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + waitForFocus: SimpleTest.waitForFocus, + }); + + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + Assert.equal( + row.result.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "Sanity check: The expected row is present" + ); + for (let [name, expected] of Object.entries(expectedAttributes)) { + Assert.equal( + row.hasAttribute(name), + expected, + "Row should have attribute as expected: " + name + ); + } + + await UrlbarTestUtils.promisePopupClose(window); + }, provider); +} + +/** + * Provides a dynamic result. + */ +class TestProvider extends UrlbarTestUtils.TestProvider { + constructor() { + super({ + results: [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.DYNAMIC, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + dynamicType: DYNAMIC_TYPE_NAME, + } + ), + { suggestedIndex: 1 } + ), + ], + }); + } + + async startQuery(context, addCallback) { + for (let result of this.results) { + result.payload.searchString = context.searchString; + addCallback(this, result); + } + } + + getViewUpdate(result, idsByName) { + for (let child of DYNAMIC_TYPE_VIEW_TEMPLATE.children) { + Assert.ok(idsByName.get(child.name), `idsByName contains ${child.name}`); + } + + return { + selectable: { + textContent: "Selectable", + attributes: { + searchString: result.payload.searchString, + }, + }, + text: { + textContent: `result.payload.searchString is: ${result.payload.searchString}`, + attributes: { + searchString: result.payload.searchString, + }, + }, + button1: { + textContent: "Button 1", + attributes: { + searchString: result.payload.searchString, + attribute_to_remove: null, + }, + }, + button2: { + textContent: "Button 2", + attributes: { + searchString: result.payload.searchString, + }, + }, + }; + } + + onEngagement(state, queryContext, details, controller) { + if (this._pickPromiseResolve) { + let { result, element } = details; + this._pickPromiseResolve([result, element]); + delete this._pickPromiseResolve; + delete this._pickPromise; + } + } + + promisePick() { + this._pickPromise = new Promise(resolve => { + this._pickPromiseResolve = resolve; + }); + return this._pickPromise; + } +} + +/** + * Provides a dynamic result. + * + * @param {object} callback - Function that runs the body of the test. + * @param {object} provider - The dummy provider to use. + */ +async function withDynamicTypeProvider( + callback, + provider = new TestProvider() +) { + // Add a dynamic result type. + UrlbarResult.addDynamicResultType(DYNAMIC_TYPE_NAME); + if (!provider.getViewTemplate) { + UrlbarView.addDynamicViewTemplate( + DYNAMIC_TYPE_NAME, + DYNAMIC_TYPE_VIEW_TEMPLATE + ); + } + + // Add a provider of the dynamic type. + UrlbarProvidersManager.registerProvider(provider); + + await callback(provider); + + // Clean up. + UrlbarProvidersManager.unregisterProvider(provider); + if (!provider.getViewTemplate) { + UrlbarView.removeDynamicViewTemplate(DYNAMIC_TYPE_NAME); + } + UrlbarResult.removeDynamicResultType(DYNAMIC_TYPE_NAME); +} + +function checkDOM(parentNode, expectedChildren) { + info( + `checkDOM: Checking parentNode id=${parentNode.id} className=${parentNode.className}` + ); + for (let i = 0; i < expectedChildren.length; i++) { + let child = expectedChildren[i]; + let actualChild = parentNode.children[i]; + info(`checkDOM: Checking expected child: ${JSON.stringify(child)}`); + Assert.ok(actualChild, "actualChild should exist"); + Assert.equal(actualChild.tagName, child.tag, "child.tag"); + Assert.equal(actualChild.getAttribute("name"), child.name, "child.name"); + Assert.ok( + actualChild.classList.contains( + `urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${child.name}` + ), + "child.name should be in classList" + ); + // We have to use startsWith/endsWith since the middle of the ID is a random + // number. + Assert.ok(actualChild.id.startsWith("urlbarView-row-")); + Assert.ok( + actualChild.id.endsWith(child.name), + "The child was assigned the correct ID." + ); + for (let [name, value] of Object.entries(child.attributes || {})) { + if (name == "attribute_to_remove") { + Assert.equal( + actualChild.hasAttribute(name), + false, + `attribute: ${name}` + ); + continue; + } + Assert.equal(actualChild.getAttribute(name), value, `attribute: ${name}`); + } + for (let name of child.classList || []) { + Assert.ok(actualChild.classList.contains(name), `classList: ${name}`); + } + if (child.children) { + checkDOM(actualChild, child.children); + } + } +} diff --git a/browser/components/urlbar/tests/browser/browser_editAndEnterWithSlowQuery.js b/browser/components/urlbar/tests/browser/browser_editAndEnterWithSlowQuery.js new file mode 100644 index 0000000000..63e799e178 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_editAndEnterWithSlowQuery.js @@ -0,0 +1,476 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test when a user enters a different URL than the result being selected. + +"use strict"; + +const ORIGINAL_CHUNK_RESULTS_DELAY = + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS; + +add_setup(async function setup() { + let suggestionsEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + }); + await SearchTestUtils.installSearchExtension( + { + name: "Test", + keyword: "@test", + }, + { setAsDefault: true } + ); + await Services.search.moveEngine(suggestionsEngine, 0); + + registerCleanupFunction(async () => { + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = + ORIGINAL_CHUNK_RESULTS_DELAY; + UrlbarPrefs.clear("delay"); + }); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quickactions", false], + ["browser.urlbar.trimHttps", false], + [ + "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", + false, + ], + ], + }); +}); + +add_task(async function test_url_type() { + const testCases = [ + { + testURL: "https://example.com/123", + displayedURL: "https://example.com/123", + trimURLs: true, + }, + { + testURL: "https://example.com/123", + displayedURL: "https://example.com/123", + trimURLs: false, + }, + { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + testURL: "http://example.com/123", + displayedURL: "example.com/123", + trimURLs: true, + }, + { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + testURL: "http://example.com/123", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + displayedURL: "http://example.com/123", + trimURLs: false, + }, + ]; + + for (const { testURL, displayedURL, trimURLs } of testCases) { + info("Setup: " + JSON.stringify({ testURL, displayedURL, trimURLs })); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.trimURLs", trimURLs]], + }); + await PlacesTestUtils.addVisits([testURL]); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + info("Show results"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exa", + fireInputEvent: true, + }); + + info("Find target result"); + let targetRowIndex = await findTargetRowIndex( + result => + result.type == UrlbarUtils.RESULT_TYPE.URL && result.url == testURL + ); + + info("Select a visit suggestion"); + UrlbarTestUtils.setSelectedRowIndex(window, targetRowIndex); + Assert.equal(window.gURLBar.value, displayedURL); + + info("Change the delay time to avoid updating results"); + const DELAY = 10000; + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = DELAY; + UrlbarPrefs.set("delay", DELAY); + + info("Edit text on the URL bar"); + window.gURLBar.setSelectionRange( + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER + ); + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.ok(gURLBar.valueIsTyped); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), targetRowIndex); + let selectedResult = UrlbarTestUtils.getSelectedRow(window).result; + Assert.equal(selectedResult.type, UrlbarUtils.RESULT_TYPE.URL); + + info("Enter before updating"); + let loadingURL = testURL.substring(0, testURL.length - 1); + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + loadingURL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + Assert.equal(gBrowser.currentURI.spec, loadingURL); + + info("Clean up"); + await PlacesUtils.history.clear(); + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = + ORIGINAL_CHUNK_RESULTS_DELAY; + UrlbarPrefs.clear("delay"); + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function test_search_type() { + info("Show results"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "123", + fireInputEvent: true, + }); + await UrlbarTestUtils.enterSearchMode(window); + + info("Find target result"); + let targetRowIndex = await findTargetRowIndex( + result => + result.type == UrlbarUtils.RESULT_TYPE.SEARCH && + result.url == "http://mochi.test:8888/?terms=123foo" + ); + + info("Select a search suggestion"); + UrlbarTestUtils.setSelectedRowIndex(window, targetRowIndex); + Assert.equal(window.gURLBar.value, "123foo"); + + info("Change the delay time to avoid updating results"); + const DELAY = 10000; + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = DELAY; + UrlbarPrefs.set("delay", DELAY); + + info("Edit text on the URL bar"); + window.gURLBar.setSelectionRange( + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER + ); + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.ok(gURLBar.valueIsTyped); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), targetRowIndex); + let selectedResult = UrlbarTestUtils.getSelectedRow(window).result; + Assert.equal(selectedResult.type, UrlbarUtils.RESULT_TYPE.SEARCH); + + info("Enter before updating"); + let loadingURL = "http://mochi.test:8888/?terms=123fo"; + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + loadingURL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + Assert.equal(gBrowser.currentURI.spec, loadingURL); + + info("Clean up"); + await PlacesUtils.history.clear(); + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = ORIGINAL_CHUNK_RESULTS_DELAY; + UrlbarPrefs.clear("delay"); +}); + +add_task(async function test_keyword_type() { + info("Setup"); + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "https://example.com/?q=%s", + }); + + info("Show results"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "keyword 123", + fireInputEvent: true, + }); + + info("Find target result"); + let targetRowIndex = await findTargetRowIndex( + result => + result.type == UrlbarUtils.RESULT_TYPE.KEYWORD && + result.url == "https://example.com/?q=123" + ); + + info("Select a search suggestion"); + UrlbarTestUtils.setSelectedRowIndex(window, targetRowIndex); + Assert.equal(window.gURLBar.value, "keyword 123"); + + info("Change the delay time to avoid updating results"); + const DELAY = 10000; + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = DELAY; + UrlbarPrefs.set("delay", DELAY); + + info("Edit text on the URL bar"); + window.gURLBar.setSelectionRange( + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER + ); + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.ok(gURLBar.valueIsTyped); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), targetRowIndex); + let selectedResult = UrlbarTestUtils.getSelectedRow(window).result; + Assert.equal(selectedResult.type, UrlbarUtils.RESULT_TYPE.KEYWORD); + + info("Enter before updating"); + let loadingURL = "https://example.com/?q=12"; + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + loadingURL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + Assert.equal(gBrowser.currentURI.spec, loadingURL); + + info("Clean up"); + await PlacesUtils.history.clear(); + await PlacesUtils.keywords.remove("keyword"); + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = ORIGINAL_CHUNK_RESULTS_DELAY; + UrlbarPrefs.clear("delay"); +}); + +add_task(async function test_dynamic_type() { + info("Setup"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.unitConversion.enabled", true]], + }); + + info("Show results"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "12 cm to mm", + fireInputEvent: true, + }); + + info("Find target result"); + let targetRowIndex = await findTargetRowIndex( + result => result.type == UrlbarUtils.RESULT_TYPE.DYNAMIC + ); + + info("Select a dynamic suggestion"); + UrlbarTestUtils.setSelectedRowIndex(window, targetRowIndex); + Assert.equal(window.gURLBar.value, "12 cm to mm"); + + info("Change the delay time to avoid updating results"); + const DELAY = 10000; + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = DELAY; + UrlbarPrefs.set("delay", DELAY); + + info("Edit text on the URL bar"); + window.gURLBar.setSelectionRange( + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER + ); + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.ok(gURLBar.valueIsTyped); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), targetRowIndex); + let selectedResult = UrlbarTestUtils.getSelectedRow(window).result; + Assert.equal(selectedResult.type, UrlbarUtils.RESULT_TYPE.DYNAMIC); + + info("Enter before updating"); + // TODO: We need to show the dynamic result with different word here. + let loadingURL = "https://example.com/?q=12+cm+to+m"; + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + loadingURL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + Assert.equal(gBrowser.currentURI.spec, loadingURL); + + info("Clean up"); + await PlacesUtils.history.clear(); + await SpecialPowers.popPrefEnv(); + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = ORIGINAL_CHUNK_RESULTS_DELAY; + UrlbarPrefs.clear("delay"); +}); + +add_task(async function test_omnibox_type() { + info("Setup"); + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["tabs"], + omnibox: { + keyword: "omnibox", + }, + }, + background() { + /* global browser */ + browser.omnibox.setDefaultSuggestion({ + description: "doit", + }); + browser.omnibox.onInputEntered.addListener(text => { + browser.tabs.update({ url: `https://example.com/${text}` }); + }); + browser.omnibox.onInputChanged.addListener((text, suggest) => { + suggest([]); + }); + }, + }); + await extension.startup(); + + info("Show results"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "omnibox 123", + fireInputEvent: true, + }); + + info("Find target result"); + let targetRowIndex = await findTargetRowIndex( + result => result.type == UrlbarUtils.RESULT_TYPE.OMNIBOX + ); + + info("Select an omnibox suggestion"); + UrlbarTestUtils.setSelectedRowIndex(window, targetRowIndex); + Assert.equal(window.gURLBar.value, "omnibox 123"); + + info("Change the delay time to avoid updating results"); + const DELAY = 10000; + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = DELAY; + UrlbarPrefs.set("delay", DELAY); + + info("Edit text on the URL bar"); + window.gURLBar.setSelectionRange( + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER + ); + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.ok(gURLBar.valueIsTyped); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), targetRowIndex); + let selectedResult = UrlbarTestUtils.getSelectedRow(window).result; + Assert.equal(selectedResult.type, UrlbarUtils.RESULT_TYPE.OMNIBOX); + Assert.ok(selectedResult.heuristic); + + info("Enter before updating"); + // As this result is heuristic, should pick as it is. + let loadingURL = "https://example.com/123"; + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + loadingURL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + Assert.equal(gBrowser.currentURI.spec, loadingURL); + + info("Clean up"); + await PlacesUtils.history.clear(); + await extension.unload(); + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = ORIGINAL_CHUNK_RESULTS_DELAY; + UrlbarPrefs.clear("delay"); +}); + +add_task(async function test_heuristic() { + const testCases = [ + { + testResult: new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "https://example.com/123" } + ), + loadingURL: "https://example.com/123", + displayedValue: "https://example.com/123", + }, + { + testResult: new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + engine: Services.search.defaultEngine.name, + query: "heuristic_search", + } + ), + loadingURL: "https://example.com/?q=heuristic_search", + displayedValue: "heuristic_search", + }, + ]; + + for (const { testResult, loadingURL, displayedValue } of testCases) { + info("Setup: " + JSON.stringify(testResult)); + testResult.heuristic = true; + let provider = new UrlbarTestUtils.TestProvider({ + results: [testResult], + name: "TestProviderHeuristic", + priority: Infinity, + }); + UrlbarProvidersManager.registerProvider(provider); + + info("Show results"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "any query", + fireInputEvent: true, + }); + + info("Select a visit suggestion"); + const targetRowIndex = 0; + UrlbarTestUtils.setSelectedRowIndex(window, targetRowIndex); + Assert.equal(window.gURLBar.value, displayedValue); + + info("Change the delay time to avoid updating results"); + const DELAY = 10000; + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = DELAY; + UrlbarPrefs.set("delay", DELAY); + + info("Edit text on the URL bar"); + window.gURLBar.setSelectionRange( + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER + ); + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.ok(gURLBar.valueIsTyped); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), targetRowIndex); + let selectedResult = UrlbarTestUtils.getSelectedRow(window).result; + Assert.equal(selectedResult, testResult); + Assert.equal( + window.gURLBar.value, + displayedValue.substring(0, displayedValue.length - 1) + ); + + info("Enter before updating"); + let spy = sinon.spy(UrlbarUtils, "getHeuristicResultFor"); + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + loadingURL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + Assert.equal(gBrowser.currentURI.spec, loadingURL); + spy.restore(); + Assert.ok(!spy.called, "getHeuristicResultFor should not be called"); + + info("Clean up"); + UrlbarProvidersManager.unregisterProvider(provider); + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = + ORIGINAL_CHUNK_RESULTS_DELAY; + UrlbarPrefs.clear("delay"); + } +}); + +async function findTargetRowIndex(finder) { + for ( + let i = 0, count = UrlbarTestUtils.getResultCount(window); + i < count; + i++ + ) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (finder(result)) { + return i; + } + } + + throw new Error("Target not found"); +} diff --git a/browser/components/urlbar/tests/browser/browser_edit_invalid_url.js b/browser/components/urlbar/tests/browser/browser_edit_invalid_url.js new file mode 100644 index 0000000000..5a710c1285 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_edit_invalid_url.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +// Checks that we trim invalid urls when they are selected, so that if the user +// modifies the selected url, or just closes the results pane, we do a visit +// rather than searching for the trimmed string. + +const url = BrowserUIUtils.trimURLProtocol + "invalid.somehost/mytest"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.trimURLs", true]], + }); + await PlacesTestUtils.addVisits(url); + registerCleanupFunction(PlacesUtils.history.clear); +}); + +add_task(async function test_escape() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "invalid", + }); + // Look for our result. + let resultCount = UrlbarTestUtils.getResultCount(window); + Assert.greater(resultCount, 1, "There should be at least two results"); + for (let i = 0; i < resultCount; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + info(`Result at ${i} has url ${result.url}`); + if (result.url.startsWith(url)) { + break; + } + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + Assert.equal( + gURLBar.value, + url, + "The string displayed in the textbox should be the untrimmed url" + ); + // Close the results pane by ESC. + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); + // Confirm the result and check the loaded page. + let promise = waitforLoadURL(); + EventUtils.synthesizeKey("KEY_Enter"); + let loadedUrl = await promise; + Assert.equal(loadedUrl, url, "Should try to load a url"); +}); + +add_task(async function test_edit_url() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "invalid", + }); + // Look for our result. + let resultCount = UrlbarTestUtils.getResultCount(window); + Assert.greater(resultCount, 1, "There should be at least two results"); + for (let i = 1; i < resultCount; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + info(`Result at ${i} has url ${result.url}`); + if (result.url.startsWith(url)) { + break; + } + } + Assert.equal( + gURLBar.value, + url, + "The string displayed in the textbox should be the untrimmed url" + ); + // Modify the url. + EventUtils.synthesizeKey("2"); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL, "Should visit a url"); + Assert.equal(result.url, url + "2", "Should visit the modified url"); + + // Confirm the result and check the loaded page. + let promise = waitforLoadURL(); + EventUtils.synthesizeKey("KEY_Enter"); + let loadedUrl = await promise; + Assert.equal(loadedUrl, url + "2", "Should try to load the modified url"); +}); + +async function waitforLoadURL() { + let sandbox = sinon.createSandbox(); + let loadedUrl = await new Promise(resolve => + sandbox.stub(gURLBar, "_loadURL").callsFake(resolve) + ); + sandbox.restore(); + return loadedUrl; +} diff --git a/browser/components/urlbar/tests/browser/browser_engagement.js b/browser/components/urlbar/tests/browser/browser_engagement.js new file mode 100644 index 0000000000..b1998b6f55 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_engagement.js @@ -0,0 +1,210 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the UrlbarProvider.onEngagement() method. + +"use strict"; + +add_task(async function abandonment() { + await doTest({ + expectedEndState: "abandonment", + endEngagement: async () => { + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + }, + }); +}); + +add_task(async function engagement() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await doTest({ + expectedEndState: "engagement", + endEngagement: async () => { + let result, element; + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_ArrowDown"); + result = gURLBar.view.selectedResult; + element = gURLBar.view.selectedElement; + EventUtils.synthesizeKey("KEY_Enter"); + }); + return { result, element }; + }, + expectedEndDetails: { + selIndex: 0, + selType: "history", + provider: "", + searchSource: "urlbar", + isSessionOngoing: false, + }, + }); + }); +}); + +add_task(async function privateWindow_abandonment() { + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + await doTest({ + win, + expectedEndState: "abandonment", + expectedIsPrivate: true, + endEngagement: async () => { + await UrlbarTestUtils.promisePopupClose(win, () => win.gURLBar.blur()); + }, + }); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function privateWindow_engagement() { + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + await doTest({ + win, + expectedEndState: "engagement", + expectedIsPrivate: true, + endEngagement: async () => { + let result, element; + await UrlbarTestUtils.promisePopupClose(win, () => { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + result = win.gURLBar.view.selectedResult; + element = win.gURLBar.view.selectedElement; + EventUtils.synthesizeKey("KEY_Enter", {}, win); + }); + return { result, element }; + }, + expectedEndDetails: { + selIndex: 0, + selType: "history", + provider: "", + searchSource: "urlbar", + isSessionOngoing: false, + }, + }); + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Performs an engagement test. + * + * @param {object} options + * Options object. + * @param {string} options.expectedEndState + * The expected state at the end of the engagement. + * @param {Function} options.endEngagement + * A function that should end the engagement. If the expected end state is + * "engagement", the function should return `{ result, element }` with the + * expected engaged result and element. + * @param {window} [options.win] + * The window to perform the test in. + * @param {boolean} [options.expectedIsPrivate] + * Whether the engagement and query context are expected to be private. + * @param {object} [options.expectedEndDetails] + * The expected `details` at the end of the engagement. `searchString` is + * automatically included since it's always present. If `provider` is + * expected, then include it and set it to any value; this function will + * replace it with the name of the test provider. + */ +async function doTest({ + expectedEndState, + endEngagement, + win = window, + expectedIsPrivate = false, + expectedEndDetails = {}, +}) { + let provider = new TestProvider(); + UrlbarProvidersManager.registerProvider(provider); + + let startPromise = provider.promiseEngagement(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "test", + fireInputEvent: true, + }); + + let [state, queryContext, details, controller] = await startPromise; + Assert.equal( + controller.input.isPrivate, + expectedIsPrivate, + "Start isPrivate" + ); + Assert.equal(state, "start", "Start state"); + + // `queryContext` isn't always defined for `start`, and `onEngagement` + // shouldn't rely on it being defined on start, but there's no good reason to + // assert that it's not defined here. + + // Similarly, `details` is never defined for `start`, but there's no good + // reason to assert that it's not defined. + + let endPromise = provider.promiseEngagement(); + let { result, element } = (await endEngagement()) ?? {}; + + [state, queryContext, details, controller] = await endPromise; + Assert.equal(controller.input.isPrivate, expectedIsPrivate, "End isPrivate"); + Assert.equal(state, expectedEndState, "End state"); + Assert.ok(queryContext, "End queryContext"); + Assert.equal( + queryContext.isPrivate, + expectedIsPrivate, + "End queryContext.isPrivate" + ); + + let detailsDefaults = { + searchString: "test", + searchSource: "urlbar", + provider: undefined, + selIndex: -1, + }; + if ("provider" in expectedEndDetails) { + detailsDefaults.provider = provider.name; + delete expectedEndDetails.provider; + } + + if (expectedEndState == "engagement") { + Assert.ok( + result, + "endEngagement() should have returned the expected engaged result" + ); + Assert.ok( + element, + "endEngagement() should have returned the expected engaged element" + ); + expectedEndDetails.result = result; + expectedEndDetails.element = element; + } + + Assert.deepEqual( + details, + Object.assign(detailsDefaults, expectedEndDetails), + "End details" + ); + + UrlbarProvidersManager.unregisterProvider(provider); +} + +/** + * Test provider that resolves promises when onEngagement is called. + */ +class TestProvider extends UrlbarTestUtils.TestProvider { + _resolves = []; + + constructor() { + super({ + priority: Infinity, + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://example.com/" } + ), + ], + }); + } + + onEngagement(...args) { + let resolve = this._resolves.shift(); + if (resolve) { + resolve(args); + } + } + + promiseEngagement() { + return new Promise(resolve => this._resolves.push(resolve)); + } +} diff --git a/browser/components/urlbar/tests/browser/browser_enter.js b/browser/components/urlbar/tests/browser/browser_enter.js new file mode 100644 index 0000000000..5fa301c027 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_enter.js @@ -0,0 +1,331 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_VALUE = "http://example.com/\xF7?\xF7"; +const START_VALUE = "http://example.com/%C3%B7?%C3%B7"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + const engine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + setAsDefault: true, + }); + engine.alias = "@default"; +}); + +add_task(async function returnKeypress() { + info("Simple return keypress"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, START_VALUE); + + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + // Check url bar and selected tab. + is( + gURLBar.value, + UrlbarTestUtils.trimURL(TEST_VALUE), + "Urlbar should preserve the value on return keypress" + ); + is(gBrowser.selectedTab, tab, "New URL was loaded in the current tab"); + + // Cleanup. + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function altReturnKeypress() { + info("Alt+Return keypress"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, START_VALUE); + + let tabOpenPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter", { altKey: true }); + + // wait for the new tab to appear. + await tabOpenPromise; + + // Check url bar and selected tab. + is( + gURLBar.value, + UrlbarTestUtils.trimURL(TEST_VALUE), + "Urlbar should preserve the value on return keypress" + ); + isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab"); + + // Cleanup. + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function altGrReturnKeypress() { + info("AltGr+Return keypress"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, START_VALUE); + + let tabOpenPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter", { altGraphKey: true }); + + // wait for the new tab to appear. + await tabOpenPromise; + + // Check url bar and selected tab. + is( + gURLBar.value, + UrlbarTestUtils.trimURL(TEST_VALUE), + "Urlbar should preserve the value on return keypress" + ); + isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab"); + + // Cleanup. + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function searchOnEnterNoPick() { + info("Search on Enter without picking a urlbar result"); + await SpecialPowers.pushPrefEnv({ + // The test checks that the untrimmed value is equal to the spec. + // When using showSearchTerms, the untrimmed value becomes + // the search terms. + set: [["browser.urlbar.showSearchTerms.featureGate", false]], + }); + + // Why is BrowserTestUtils.openNewForegroundTab not causing the bug? + let promiseTabOpened = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + EventUtils.synthesizeMouseAtCenter(gBrowser.tabContainer.newTabButton, {}); + let openEvent = await promiseTabOpened; + let tab = openEvent.target; + + let loadPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + null, + true + ); + gURLBar.focus(); + gURLBar.value = "test test"; + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + + Assert.ok( + gBrowser.selectedBrowser.currentURI.spec.endsWith("test+test"), + "Should have loaded the correct page" + ); + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + gURLBar.untrimmedValue, + "The location should have changed" + ); + + // Cleanup. + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function searchOnEnterSoon() { + info("Search on Enter as soon as typing a char"); + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + START_VALUE + ); + + const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + const onPageHide = SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + return new Promise(resolve => { + content.window.addEventListener("pagehide", () => { + resolve(); + }); + }); + }); + const onResult = SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + return new Promise(resolve => { + content.window.addEventListener("keyup", () => { + resolve("keyup"); + }); + content.window.addEventListener("unload", () => { + resolve("unload"); + }); + }); + }); + + // Focus on the input field in urlbar. + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + const ownerDocument = gBrowser.selectedBrowser.ownerDocument; + is( + ownerDocument.activeElement, + gURLBar.inputField, + "The input field in urlbar has focus" + ); + + info("Keydown a char and Enter"); + EventUtils.synthesizeKey("x", { type: "keydown" }); + EventUtils.synthesizeKey("KEY_Enter", { type: "keydown" }); + + // Wait for pagehide event in the content. + await onPageHide; + is( + ownerDocument.activeElement, + gURLBar.inputField, + "The input field in urlbar still has focus" + ); + + // Check the caret position. + Assert.equal( + gURLBar.selectionStart, + gURLBar.value.length, + "The selectionStart indicates at ending of the value" + ); + Assert.equal( + gURLBar.selectionEnd, + gURLBar.value.length, + "The selectionEnd indicates at ending of the value" + ); + + // Keyup both key as soon as pagehide event happens. + EventUtils.synthesizeKey("x", { type: "keyup" }); + EventUtils.synthesizeKey("KEY_Enter", { type: "keyup" }); + + // Wait for moving the focus. + await TestUtils.waitForCondition( + () => ownerDocument.activeElement === gBrowser.selectedBrowser + ); + info("The focus is moved to the browser"); + + // Check whether keyup event is not captured before unload event happens. + const result = await onResult; + is(result, "unload", "Keyup event is not captured."); + + // Check the caret position again. + Assert.equal( + gURLBar.selectionStart, + 0, + "The selectionStart indicates at beginning of the value" + ); + Assert.equal( + gURLBar.selectionEnd, + 0, + "The selectionEnd indicates at beginning of the value" + ); + + // Cleanup. + await onLoad; + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function searchByMultipleEnters() { + info("Search on Enter after selecting the search engine by Enter"); + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + START_VALUE + ); + + info("Select a search engine by Enter key"); + gURLBar.focus(); + gURLBar.select(); + EventUtils.sendString("@default"); + EventUtils.synthesizeKey("KEY_Enter"); + await TestUtils.waitForCondition( + () => gURLBar.searchMode, + "Wait until entering search mode" + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: "browser_searchSuggestionEngine searchSuggestionEngine.xml", + entry: "keywordoffer", + }); + const ownerDocument = gBrowser.selectedBrowser.ownerDocument; + is( + ownerDocument.activeElement, + gURLBar.inputField, + "The input field in urlbar has focus" + ); + + info("Search by Enter key"); + const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.sendString("mozilla"); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + is( + ownerDocument.activeElement, + gBrowser.selectedBrowser, + "The focus is moved to the browser" + ); + + // Cleanup. + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function typeCharWhileProcessingEnter() { + info("Typing a char while processing enter key"); + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + START_VALUE + ); + + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + START_VALUE + ); + gURLBar.focus(); + + info("Keydown Enter"); + EventUtils.synthesizeKey("KEY_Enter", { type: "keydown" }); + await TestUtils.waitForCondition( + () => gURLBar._keyDownEnterDeferred, + "Wait for starting process for the enter key" + ); + + info("Keydown a char"); + EventUtils.synthesizeKey("x", { type: "keydown" }); + + info("Keyup both"); + EventUtils.synthesizeKey("x", { type: "keyup" }); + EventUtils.synthesizeKey("KEY_Enter", { type: "keyup" }); + + Assert.equal( + gURLBar.value, + UrlbarTestUtils.trimURL(TEST_VALUE), + "The value of urlbar is correct" + ); + + await onLoad; + Assert.ok("Browser loaded the correct url"); + + // Cleanup. + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function keyupEnterWhilePressingMeta() { + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + info("Keydown Meta+Enter"); + gURLBar.focus(); + gURLBar.value = ""; + EventUtils.synthesizeKey("KEY_Enter", { type: "keydown", metaKey: true }); + + // Pressing Enter key while pressing Meta key, and next, even when releasing + // Enter key before releasing Meta key, the keyup event is not fired. + // Therefor, we fire Meta keyup event only. + info("Keyup Meta"); + EventUtils.synthesizeKey("KEY_Meta", { type: "keyup" }); + + // Check whether we can input on URL bar. + EventUtils.synthesizeKey("a"); + is(gURLBar.value, "a", "Can input a char"); + + // Cleanup. + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_enterAfterMouseOver.js b/browser/components/urlbar/tests/browser/browser_enterAfterMouseOver.js new file mode 100644 index 0000000000..e102fda09c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_enterAfterMouseOver.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that enter works correctly after a mouse over. + */ + +function repeat(limit, func) { + for (let i = 0; i < limit; i++) { + func(i); + } +} + +async function promiseAutoComplete(inputText) { + gURLBar.focus(); + gURLBar.value = inputText.slice(0, -1); + EventUtils.sendString(inputText.slice(-1)); + await UrlbarTestUtils.promiseSearchComplete(window); +} + +function assertSelected(index) { + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + index, + "Should have the correct index selected" + ); +} + +let gMaxResults; + +add_task(async function () { + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); + + await PlacesUtils.history.clear(); + + gMaxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults"); + + let visits = []; + repeat(gMaxResults, i => { + visits.push({ + uri: makeURI("http://example.com/autocomplete/?" + i), + }); + }); + await PlacesTestUtils.addVisits(visits); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + await promiseAutoComplete("http://example.com/autocomplete/"); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + gMaxResults, + "Should have got the correct amount of results" + ); + + let initiallySelected = UrlbarTestUtils.getSelectedRowIndex(window); + + info("Key Down to select the next item"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertSelected(initiallySelected + 1); + + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + initiallySelected + 1 + ); + let expectedURL = result.url; + + Assert.equal( + gURLBar.untrimmedValue, + expectedURL, + "Value in the URL bar should be updated by keyboard selection" + ); + + // Verify that what we're about to do changes the selectedIndex: + Assert.notEqual( + initiallySelected + 1, + 3, + "Shouldn't be changing the selectedIndex to the same index we keyboard-selected." + ); + + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 3); + EventUtils.synthesizeMouseAtCenter(element, { type: "mousemove" }); + + await UrlbarTestUtils.promisePopupClose(window, async () => { + let openedExpectedPage = BrowserTestUtils.waitForDocLoadAndStopIt( + expectedURL, + gBrowser.selectedBrowser + ); + EventUtils.synthesizeKey("KEY_Enter"); + await openedExpectedPage; + }); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_focusedCmdK.js b/browser/components/urlbar/tests/browser/browser_focusedCmdK.js new file mode 100644 index 0000000000..fc32c2c13c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_focusedCmdK.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + // Test that Ctrl/Cmd + K will focus the url bar + let focusPromise = BrowserTestUtils.waitForEvent(gURLBar, "focus"); + document.documentElement.focus(); + EventUtils.synthesizeKey("k", { accelKey: true }); + await focusPromise; + Assert.equal( + document.activeElement, + gURLBar.inputField, + "URL Bar should be focused" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_groupLabels.js b/browser/components/urlbar/tests/browser/browser_groupLabels.js new file mode 100644 index 0000000000..2b43990b77 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_groupLabels.js @@ -0,0 +1,629 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests group labels in the view. + +"use strict"; + +const SUGGESTIONS_FIRST_PREF = "browser.urlbar.showSearchSuggestionsFirst"; +const SUGGESTIONS_PREF = "browser.urlbar.suggest.searches"; + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; +const TEST_ENGINE_2_BASENAME = "searchSuggestionEngine2.xml"; +const MAX_RESULTS = UrlbarPrefs.get("maxRichResults"); + +const TOP_SITES = [ + "http://example-1.com/", + "http://example-2.com/", + "http://example-3.com/", +]; + +const FIREFOX_SUGGEST_LABEL = "Firefox Suggest"; + +// %s is replaced with the engine name. +const ENGINE_SUGGESTIONS_LABEL = "%s suggestions"; + +// Allow more time for Mac machines so they don't time out in verify mode. +if (AppConstants.platform == "macosx") { + requestLongerTimeout(3); +} + +add_setup(async function () { + Assert.ok( + UrlbarPrefs.get("showSearchSuggestionsFirst"), + "Precondition: Search suggestions shown first by default" + ); + + // Add some history. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await UrlbarTestUtils.formHistory.clear(); + await addHistory(); + + // Make sure we have some top sites. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.topsites", true], + ["browser.newtabpage.activity-stream.default.sites", TOP_SITES.join(",")], + ], + }); + // Waiting for all top sites to be added intermittently times out, so just + // wait for any to be added. We're not testing top sites here; we only need + // the view to open in top-sites mode. + await updateTopSites(sites => sites && sites.length); + + // Add a mock engine so we don't hit the network. + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +// The Firefox Suggest label should not appear when the labels pref is disabled. +add_task(async function prefDisabled() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.groupLabels.enabled", false]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(MAX_RESULTS, {}); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); +}); + +// The Firefox Suggest label should not appear when the view shows top sites. +add_task(async function topSites() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await checkLabels(-1, {}); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// The Firefox Suggest label should appear when the search string is non-empty +// and there are only general results. +add_task(async function general() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(MAX_RESULTS, { + 1: FIREFOX_SUGGEST_LABEL, + }); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// The Firefox Suggest label should appear when the search string is non-empty +// and there are suggestions followed by general results. +add_task(async function suggestionsBeforeGeneral() { + await withSuggestions(async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(MAX_RESULTS, { + 3: FIREFOX_SUGGEST_LABEL, + }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Both the Firefox Suggest and Suggestions labels should appear when the search +// string is non-empty, general results are shown before suggestions, and there +// are general and suggestion results. +add_task(async function generalBeforeSuggestions() { + await withSuggestions(async engine => { + Assert.ok(engine.name, "Engine name is non-empty"); + await SpecialPowers.pushPrefEnv({ + set: [[SUGGESTIONS_FIRST_PREF, false]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(MAX_RESULTS, { + 1: FIREFOX_SUGGEST_LABEL, + [MAX_RESULTS - 2]: engineSuggestionsLabel(engine.name), + }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Neither the Firefox Suggest nor Suggestions label should appear when the +// search string is non-empty, general results are shown before suggestions, and +// there are only suggestion results. +add_task(async function generalBeforeSuggestions_suggestionsOnly() { + await PlacesUtils.history.clear(); + + await withSuggestions(async engine => { + await SpecialPowers.pushPrefEnv({ + set: [[SUGGESTIONS_FIRST_PREF, false]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(3, {}); + await UrlbarTestUtils.promisePopupClose(window); + }); + + // Add back history so subsequent tasks run with this test's initial state. + await addHistory(); +}); + +// The Suggestions label should be updated when the default engine changes. +add_task(async function generalBeforeSuggestions_defaultChanged() { + // Install both test engines, one after the other. Engine 2 will be the final + // default engine. + await withSuggestions(async engine1 => { + await withSuggestions(async engine2 => { + Assert.ok(engine2.name, "Engine 2 name is non-empty"); + Assert.notEqual(engine1.name, engine2.name, "Engine names are different"); + Assert.equal( + Services.search.defaultEngine.name, + engine2.name, + "Engine 2 is default" + ); + await SpecialPowers.pushPrefEnv({ + set: [[SUGGESTIONS_FIRST_PREF, false]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(MAX_RESULTS, { + 1: FIREFOX_SUGGEST_LABEL, + [MAX_RESULTS - 2]: engineSuggestionsLabel(engine2.name), + }); + await UrlbarTestUtils.promisePopupClose(window); + }, TEST_ENGINE_2_BASENAME); + }); +}); + +// The Firefox Suggest label should appear above a suggested-index result when +// the result is the only result with that label. +add_task(async function suggestedIndex_only() { + // Clear history, add a provider that returns a result with suggestedIndex = + // -1, set up an engine with suggestions, and start a query. The suggested- + // index result will be the only result with a label. + await PlacesUtils.history.clear(); + + let index = -1; + let provider = new SuggestedIndexProvider(index); + UrlbarProvidersManager.registerProvider(provider); + + await withSuggestions(async engine => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 3); + Assert.equal( + result.element.row.result.suggestedIndex, + index, + "Sanity check: Our suggested-index result is present" + ); + await checkLabels(4, { + 3: FIREFOX_SUGGEST_LABEL, + }); + await UrlbarTestUtils.promisePopupClose(window); + }); + + UrlbarProvidersManager.unregisterProvider(provider); + + // Add back history so subsequent tasks run with this test's initial state. + await addHistory(); +}); + +// The Firefox Suggest label should appear above a suggested-index result when +// the result is the first but not the only result with that label. +add_task(async function suggestedIndex_first() { + let index = 1; + let provider = new SuggestedIndexProvider(index); + UrlbarProvidersManager.registerProvider(provider); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + Assert.equal( + result.element.row.result.suggestedIndex, + index, + "Sanity check: Our suggested-index result is present" + ); + await checkLabels(MAX_RESULTS, { + [index]: FIREFOX_SUGGEST_LABEL, + }); + await UrlbarTestUtils.promisePopupClose(window); + + UrlbarProvidersManager.unregisterProvider(provider); +}); + +// The Firefox Suggest label should not appear above a suggested-index result +// when the result is not the first with that label. +add_task(async function suggestedIndex_notFirst() { + let index = -1; + let provider = new SuggestedIndexProvider(index); + UrlbarProvidersManager.registerProvider(provider); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + MAX_RESULTS + index + ); + Assert.equal( + result.element.row.result.suggestedIndex, + index, + "Sanity check: Our suggested-index result is present" + ); + await checkLabels(MAX_RESULTS, { + 1: FIREFOX_SUGGEST_LABEL, + }); + await UrlbarTestUtils.promisePopupClose(window); + + UrlbarProvidersManager.unregisterProvider(provider); +}); + +// Labels that appear multiple times but not consecutively should be shown. +add_task(async function repeatLabels() { + let engineName = Services.search.defaultEngine.name; + let results = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "http://example.com/1" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { suggestion: "test1", engine: engineName } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "http://example.com/2" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { suggestion: "test2", engine: engineName } + ), + ]; + + for (let i = 0; i < results.length; i++) { + results[i].suggestedIndex = i; + } + + let provider = new UrlbarTestUtils.TestProvider({ + results, + priority: Infinity, + }); + UrlbarProvidersManager.registerProvider(provider); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(results.length, { + 0: FIREFOX_SUGGEST_LABEL, + 1: engineSuggestionsLabel(engineName), + 2: FIREFOX_SUGGEST_LABEL, + 3: engineSuggestionsLabel(engineName), + }); + await UrlbarTestUtils.promisePopupClose(window); + + UrlbarProvidersManager.unregisterProvider(provider); +}); + +// Clicking a row label shouldn't do anything. +add_task(async function clickLabel() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + // Do a search. The mock history added in init() should appear with the + // Firefox Suggest label at index 1. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(MAX_RESULTS, { + 1: FIREFOX_SUGGEST_LABEL, + }); + + // Check the result at index 2. + let result2 = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.ok(result2.url, "Result at index 2 has a URL"); + let url2 = result2.url; + Assert.ok( + url2.startsWith("http://example.com/"), + "Result at index 2 is one of our mock history results" + ); + + // Get the row at index 3 and click above it. The click should hit the row + // at index 2 and load its URL. We do this to make sure our click code + // here in the test works properly and that performing a similar click + // relative to index 1 (see below) would hit the row at index 0 if not for + // the label at index 1. + let result3 = await UrlbarTestUtils.getDetailsOfResultAt(window, 3); + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + info("Performing click relative to index 3"); + await UrlbarTestUtils.promisePopupClose(window, () => + click(result3.element.row, { y: -2 }) + ); + info("Waiting for load after performing click relative to index 3"); + await loadPromise; + Assert.equal(gBrowser.currentURI.spec, url2, "Loaded URL at index 2"); + // Now do the search again. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + + await checkLabels(MAX_RESULTS, { + 1: FIREFOX_SUGGEST_LABEL, + }); + + // Check the result at index 1, the one with the label. + let result1 = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.ok(result1.url, "Result at index 1 has a URL"); + let url1 = result1.url; + Assert.ok( + url1.startsWith("http://example.com/"), + "Result at index 1 is one of our mock history results" + ); + Assert.notEqual(url1, url2, "URLs at indexes 1 and 2 are different"); + + // Do a click on the row at index 1 in the same way as before. This time + // nothing should happen because the click should hit the label, not the + // row at index 0. + info("Clicking row label at index 1"); + click(result1.element.row, { y: -2 }); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 500)); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "View remains open"); + Assert.equal( + gBrowser.currentURI.spec, + url2, + "Current URL is still URL from index 2" + ); + + // Now click the main part of the row at index 1. Its URL should load. + loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + let { height } = result1.element.row.getBoundingClientRect(); + info(`Clicking main part of the row at index 1, height=${height}`); + await UrlbarTestUtils.promisePopupClose(window, () => + click(result1.element.row) + ); + info("Waiting for load after clicking row at index 1"); + await loadPromise; + Assert.equal(gBrowser.currentURI.spec, url1, "Loaded URL at index 1"); + }); +}); + +add_task(async function ariaLabel() { + const helpUrl = "http://example.com/help"; + const results = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "http://example.com/1", helpUrl } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "http://example.com/2", helpUrl } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "http://example.com/3" } + ), + ]; + + for (let i = 0; i < results.length; i++) { + results[i].suggestedIndex = i; + } + + const provider = new UrlbarTestUtils.TestProvider({ + results, + priority: Infinity, + }); + UrlbarProvidersManager.registerProvider(provider); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await checkLabels(results.length, { + 0: FIREFOX_SUGGEST_LABEL, + }); + + const expectedRows = [ + { hasGroupAriaLabel: true, ariaLabel: FIREFOX_SUGGEST_LABEL }, + { hasGroupAriaLabel: false }, + { hasGroupAriaLabel: false }, + ]; + await checkGroupAriaLabels(expectedRows); + + await UrlbarTestUtils.promisePopupClose(window); + + UrlbarProvidersManager.unregisterProvider(provider); +}); + +/** + * Provider that returns a suggested-index result. + */ +class SuggestedIndexProvider extends UrlbarTestUtils.TestProvider { + constructor(suggestedIndex) { + super({ + results: [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "http://example.com/" } + ), + { suggestedIndex } + ), + ], + }); + } +} + +async function addHistory() { + for (let i = 0; i < MAX_RESULTS; i++) { + await PlacesTestUtils.addVisits("http://example.com/" + i); + } +} + +/** + * Asserts that each result in the view does or doesn't have a label, depending + * on `labelsByIndex`. + * + * @param {number} resultCount + * The expected number of results. Pass -1 to use the max index in + * `labelsByIndex` or the actual result count if `labelsByIndex` is empty. + * @param {object} labelsByIndex + * A mapping from indexes to expected labels. + */ +async function checkLabels(resultCount, labelsByIndex) { + if (resultCount >= 0) { + Assert.equal( + UrlbarTestUtils.getResultCount(window), + resultCount, + "Expected result count" + ); + } else { + // This `else` branch is only necessary because waiting for all top sites to + // be added intermittently times out. Don't let the test fail for such a + // dumb reason. + let indexes = Object.keys(labelsByIndex); + if (indexes.length) { + resultCount = indexes.sort((a, b) => b - a)[0] + 1; + } else { + resultCount = UrlbarTestUtils.getResultCount(window); + Assert.greater(resultCount, 0, "Actual result count is > 0"); + } + } + for (let i = 0; i < resultCount; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + let { row } = result.element; + let before = getComputedStyle(row, "::before"); + if (labelsByIndex.hasOwnProperty(i)) { + Assert.equal( + before.content, + "attr(label)", + `::before.content is correct at index ${i}` + ); + Assert.equal( + row.getAttribute("label"), + labelsByIndex[i], + `Row has correct label at index ${i}` + ); + } else { + Assert.equal( + before.content, + "none", + `::before.content is 'none' at index ${i}` + ); + Assert.ok( + !row.hasAttribute("label"), + `Row does not have label attribute at index ${i}` + ); + } + } +} + +/** + * Asserts that an element for group aria label. + * + * @param {Array} expectedRows The expected rows. + */ +async function checkGroupAriaLabels(expectedRows) { + Assert.equal( + UrlbarTestUtils.getResultCount(window), + expectedRows.length, + "Expected result count" + ); + + for (let i = 0; i < expectedRows.length; i++) { + const result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + const { row } = result.element; + const groupAriaLabel = row.querySelector(".urlbarView-group-aria-label"); + + const expected = expectedRows[i]; + + Assert.equal( + !!groupAriaLabel, + expected.hasGroupAriaLabel, + `Group aria label exists as expected in the results[${i}]` + ); + + if (expected.hasGroupAriaLabel) { + Assert.equal( + groupAriaLabel.getAttribute("aria-label"), + expected.ariaLabel, + `Content of aria-label attribute in the element for group aria label in the results[${i}] is correct` + ); + } + } +} + +function engineSuggestionsLabel(engineName) { + return ENGINE_SUGGESTIONS_LABEL.replace("%s", engineName); +} + +/** + * Adds a search engine that provides suggestions, calls your callback, and then + * remove the engine. + * + * @param {Function} callback + * Your callback function. + * @param {string} [engineBasename] + * The basename of the engine file. + */ +async function withSuggestions( + callback, + engineBasename = TEST_ENGINE_BASENAME +) { + await SpecialPowers.pushPrefEnv({ + set: [[SUGGESTIONS_PREF, true]], + }); + let engine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + engineBasename, + }); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + try { + await callback(engine); + } finally { + await Services.search.setDefault( + oldDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.removeEngine(engine); + await SpecialPowers.popPrefEnv(); + } +} + +function click(element, { x = undefined, y = undefined } = {}) { + let { width, height } = element.getBoundingClientRect(); + if (typeof x != "number") { + x = width / 2; + } + if (typeof y != "number") { + y = height / 2; + } + EventUtils.synthesizeMouse(element, x, y, { type: "mousedown" }); + EventUtils.synthesizeMouse(element, x, y, { type: "mouseup" }); +} diff --git a/browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js b/browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js new file mode 100644 index 0000000000..9d8ac8754c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js @@ -0,0 +1,142 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the fallback paths of handleCommand (no view and no previous + * result) work consistently against the normal case of picking the heuristic + * result. + */ + +const TEST_STRINGS = [ + "test", + "test/", + "test.com", + "test.invalid", + "moz", + "moz test", + "@moz test", + "keyword", + "keyword test", + "test/test/", + "test /test/", +]; + +add_task(async function () { + // Disable autofill so mozilla.org isn't autofilled below. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", false]], + }); + + sandbox = sinon.createSandbox(); + await SearchTestUtils.installSearchExtension(); + await SearchTestUtils.installSearchExtension({ name: "Example2" }); + + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "https://example.com/?q=%s", + title: "test", + }); + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "https://example.com/?q=%s", + }); + registerCleanupFunction(async () => { + sandbox.restore(); + await PlacesUtils.bookmarks.remove(bm); + await UrlbarTestUtils.formHistory.clear(); + }); + + async function promiseLoadURL() { + return new Promise(resolve => { + sandbox.stub(gURLBar, "_loadURL").callsFake(function () { + sandbox.restore(); + // The last arguments are optional and apply only to some cases, so we + // could not use deepEqual with them. + resolve(Array.from(arguments).slice(0, 3)); + }); + }); + } + + // Run the string through a normal search where the user types the string + // and confirms the heuristic result, store the arguments to _loadURL, then + // confirm the same string without a view and without an input event, and + // compare the arguments. + for (let value of TEST_STRINGS) { + info(`Input the value normally and Enter. Value: ${value}`); + let promise = promiseLoadURL(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + }); + EventUtils.synthesizeKey("KEY_Enter"); + let args = await promise; + Assert.ok(args.length, "Sanity check"); + info("Close the panel and confirm again."); + promise = promiseLoadURL(); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Enter"); + Assert.deepEqual(await promise, args, "Check arguments are coherent"); + + info("Set the value directly and Enter."); + // To properly testing the original value we must be out of search mode. + if (gURLBar.searchMode) { + await UrlbarTestUtils.exitSearchMode(window); + // Exiting search mode may reopen the panel. + await UrlbarTestUtils.promisePopupClose(window); + } + promise = promiseLoadURL(); + gURLBar.value = value; + let spy = sinon.spy(UrlbarUtils, "getHeuristicResultFor"); + EventUtils.synthesizeKey("KEY_Enter"); + spy.restore(); + Assert.ok(spy.called, "invoked getHeuristicResultFor"); + Assert.deepEqual(await promise, args, "Check arguments are coherent"); + gURLBar.handleRevert(); + } +}); + +// This is testing the final fallback case that may happen when we can't +// get a heuristic result, maybe because the Places database is corrupt. +add_task(async function no_heuristic_test() { + sandbox = sinon.createSandbox(); + + let stub = sandbox + .stub(UrlbarUtils, "getHeuristicResultFor") + .callsFake(async function () { + throw new Error("I failed!"); + }); + + registerCleanupFunction(async () => { + sandbox.restore(); + await UrlbarTestUtils.formHistory.clear(); + }); + + async function promiseLoadURL() { + return new Promise(resolve => { + sandbox.stub(gURLBar, "_loadURL").callsFake(function () { + sandbox.restore(); + // The last arguments are optional and apply only to some cases, so we + // could not use deepEqual with them. + resolve(Array.from(arguments).slice(0, 3)); + }); + }); + } + + // Run the string through a normal search where the user types the string + // and confirms the heuristic result, store the arguments to _loadURL, then + // confirm the same string without a view and without an input event, and + // compare the arguments. + for (let value of TEST_STRINGS) { + // To properly testing the original value we must be out of search mode. + if (gURLBar.searchMode) { + await UrlbarTestUtils.exitSearchMode(window); + } + let promise = promiseLoadURL(); + gURLBar.value = value; + EventUtils.synthesizeKey("KEY_Enter"); + Assert.ok(stub.called, "invoked getHeuristicResultFor"); + // The first argument to _loadURL should always be a valid url, so this + // should never throw. + new URL((await promise)[0]); + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_hashChangeProxyState.js b/browser/components/urlbar/tests/browser/browser_hashChangeProxyState.js new file mode 100644 index 0000000000..b750080d41 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_hashChangeProxyState.js @@ -0,0 +1,151 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that navigating through both the URL bar and using in-page hash- or ref- + * based links and back or forward navigation updates the URL bar and identity block correctly. + */ +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.navigation.requireUserInteraction", false]], + }); + let baseURL = `${TEST_BASE_URL}dummy_page.html`; + let url = baseURL + "#foo"; + await BrowserTestUtils.withNewTab( + { gBrowser, url }, + async function (browser) { + let identityBox = document.getElementById("identity-box"); + let expectedURL = url; + + let verifyURLBarState = testType => { + is( + gURLBar.value, + UrlbarTestUtils.trimURL(expectedURL), + "URL bar visible value should be correct " + testType + ); + is( + gURLBar.untrimmedValue, + expectedURL, + "URL bar value should be correct " + testType + ); + ok( + identityBox.classList.contains("verifiedDomain"), + "Identity box should know we're doing SSL " + testType + ); + is( + gURLBar.getAttribute("pageproxystate"), + "valid", + "URL bar is in valid page proxy state" + ); + }; + + verifyURLBarState("at the beginning"); + + let locationChangePromise; + let resolveLocationChangePromise; + let expectURL = urlTemp => { + expectedURL = urlTemp; + locationChangePromise = new Promise( + r => (resolveLocationChangePromise = r) + ); + }; + let wpl = { + onLocationChange(unused, unused2, location) { + is(location.spec, expectedURL, "Got the expected URL"); + resolveLocationChangePromise(); + }, + }; + gBrowser.addProgressListener(wpl); + + expectURL(baseURL + "#foo"); + gURLBar.select(); + EventUtils.sendKey("return"); + + await locationChangePromise; + verifyURLBarState("after hitting enter on the same URL a second time"); + + expectURL(baseURL + "#bar"); + gURLBar.value = expectedURL; + gURLBar.select(); + EventUtils.sendKey("return"); + + await locationChangePromise; + verifyURLBarState("after a URL bar hash navigation"); + + expectURL(baseURL + "#foo"); + await SpecialPowers.spawn(browser, [], function () { + let a = content.document.createElement("a"); + a.href = "#foo"; + a.textContent = "Foo Link"; + content.document.body.appendChild(a); + a.click(); + }); + + await locationChangePromise; + verifyURLBarState("after a page link hash navigation"); + + expectURL(baseURL + "#bar"); + gBrowser.goBack(); + + await locationChangePromise; + verifyURLBarState("after going back"); + + expectURL(baseURL + "#foo"); + gBrowser.goForward(); + + await locationChangePromise; + verifyURLBarState("after going forward"); + + expectURL(baseURL + "#foo"); + gURLBar.select(); + EventUtils.sendKey("return"); + + await locationChangePromise; + verifyURLBarState("after hitting enter on the same URL"); + + gBrowser.removeProgressListener(wpl); + } + ); +}); + +/** + * Check that initial secure loads that swap remoteness + * get the correct page icon when finished. + */ +add_task(async function () { + // Ensure there's no preloaded newtab browser, since that'll not fire a load event. + NewTabPagePreloading.removePreloadedBrowser(window); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:newtab" + ); + let url = `${TEST_BASE_URL}dummy_page.html#foo`; + gURLBar.value = url; + gURLBar.select(); + EventUtils.sendKey("return"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + is( + gURLBar.value, + UrlbarTestUtils.trimURL(url), + "URL bar visible value should be correct when the page loads from about:newtab" + ); + is( + gURLBar.untrimmedValue, + url, + "URL bar value should be correct when the page loads from about:newtab" + ); + let identityBox = document.getElementById("identity-box"); + ok( + identityBox.classList.contains("verifiedDomain"), + "Identity box should know we're doing SSL when the page loads from about:newtab" + ); + is( + gURLBar.getAttribute("pageproxystate"), + "valid", + "URL bar is in valid page proxy state when SSL page with hash loads from about:newtab" + ); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js b/browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js new file mode 100644 index 0000000000..fa7c65b378 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js @@ -0,0 +1,159 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// When the heuristic result is not the first result added, it should still be +// selected. + +"use strict"; + +// When the heuristic result is not the first result added, it should still be +// selected. +add_task(async function slowHeuristicSelected() { + // First, add a provider that adds a heuristic result on a delay. Both this + // provider and the one below have a high priority so that only they are used + // during the test. + let engine = await Services.search.getDefault(); + let heuristicResult = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + suggestion: "test", + engine: engine.name, + } + ); + heuristicResult.heuristic = true; + let heuristicProvider = new UrlbarTestUtils.TestProvider({ + results: [heuristicResult], + name: "heuristicProvider", + priority: Infinity, + addTimeout: 500, + }); + UrlbarProvidersManager.registerProvider(heuristicProvider); + + // Second, add another provider that adds a non-heuristic result immediately + // with suggestedIndex = 1. + let nonHeuristicResult = makeTipResult(); + nonHeuristicResult.suggestedIndex = 1; + let nonHeuristicProvider = new UrlbarTestUtils.TestProvider({ + results: [nonHeuristicResult], + name: "nonHeuristicProvider", + priority: Infinity, + }); + UrlbarProvidersManager.registerProvider(nonHeuristicProvider); + + // Do a search. + const win = await BrowserTestUtils.openNewBrowserWindow(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window: win, + }); + + // The first result should be the heuristic and it should be selected. + let actualHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + Assert.equal(actualHeuristic.type, UrlbarUtils.RESULT_TYPE.SEARCH); + Assert.equal(UrlbarTestUtils.getSelectedElementIndex(win), 0); + + // Check the second result for good measure. + let actualNonHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(win, 1); + Assert.equal(actualNonHeuristic.type, UrlbarUtils.RESULT_TYPE.TIP); + + await UrlbarTestUtils.promisePopupClose(win); + UrlbarProvidersManager.unregisterProvider(heuristicProvider); + UrlbarProvidersManager.unregisterProvider(nonHeuristicProvider); + await BrowserTestUtils.closeWindow(win); +}); + +// When the heuristic result is not the first result added but a one-off search +// button is already selected, the heuristic result should not steal the +// selection from the one-off button. +add_task(async function oneOffRemainsSelected() { + // First, add a provider that adds a heuristic result on a delay. Both this + // provider and the one below have a high priority so that only they are used + // during the test. + let engine = await Services.search.getDefault(); + let heuristicResult = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + suggestion: "test", + engine: engine.name, + } + ); + heuristicResult.heuristic = true; + let heuristicProvider = new UrlbarTestUtils.TestProvider({ + results: [heuristicResult], + name: "heuristicProvider", + priority: Infinity, + addTimeout: 500, + }); + UrlbarProvidersManager.registerProvider(heuristicProvider); + + // Second, add another provider that adds a non-heuristic result immediately + // with suggestedIndex = 1. + let nonHeuristicResult = makeTipResult(); + nonHeuristicResult.suggestedIndex = 1; + let nonHeuristicProvider = new UrlbarTestUtils.TestProvider({ + results: [nonHeuristicResult], + name: "nonHeuristicProvider", + priority: Infinity, + }); + UrlbarProvidersManager.registerProvider(nonHeuristicProvider); + + // Do a search but don't wait for it to finish. + const win = await BrowserTestUtils.openNewBrowserWindow(); + let searchPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window: win, + }); + + // When the view opens, press the up arrow key to select the one-off search + // settings button. There's no point in selecting instead the non-heuristic + // result because once we do that, the search is canceled, and the heuristic + // result will never be added. + await UrlbarTestUtils.promisePopupOpen(win, () => {}); + EventUtils.synthesizeKey("KEY_ArrowUp", {}, win); + + // Wait for the search to finish. + await searchPromise; + + // The first result should be the heuristic. + let actualHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + Assert.equal(actualHeuristic.type, UrlbarUtils.RESULT_TYPE.SEARCH); + + // Check the second result for good measure. + let actualNonHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(win, 1); + Assert.equal(actualNonHeuristic.type, UrlbarUtils.RESULT_TYPE.TIP); + + // No result should be selected. + Assert.equal(UrlbarTestUtils.getSelectedElement(win), null); + Assert.equal(UrlbarTestUtils.getSelectedElementIndex(win), -1); + + // The one-off settings button should be selected. + Assert.equal( + win.gURLBar.view.oneOffSearchButtons.selectedButton, + win.gURLBar.view.oneOffSearchButtons.settingsButton + ); + + await UrlbarTestUtils.promisePopupClose(win); + UrlbarProvidersManager.unregisterProvider(heuristicProvider); + UrlbarProvidersManager.unregisterProvider(nonHeuristicProvider); + await BrowserTestUtils.closeWindow(win); +}); + +function makeTipResult() { + return new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + helpUrl: "http://example.com/", + type: "test", + titleL10n: { id: "urlbar-search-tips-confirm" }, + buttons: [ + { + url: "http://example.com/", + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + } + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_hideHeuristic.js b/browser/components/urlbar/tests/browser/browser_hideHeuristic.js new file mode 100644 index 0000000000..5f76157f9d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_hideHeuristic.js @@ -0,0 +1,514 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Basic smoke tests for the `browser.urlbar.experimental.hideHeuristic` pref, +// which hides the heuristic result. Each task performs a search that triggers a +// specific heuristic, verifies that it's hidden or shown as appropriate, and +// verifies that it's picked when enter is pressed. +// +// If/when it becomes the default, we should update existing tests as necessary +// and remove this one. + +"use strict"; + +// Allow more time for Mac machines so they don't time out in verify mode. +if (AppConstants.platform == "macosx") { + requestLongerTimeout(3); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.experimental.hideHeuristic", true], + ["browser.urlbar.suggest.quickactions", false], + ["dom.security.https_first_schemeless", false], + ], + }); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION should be hidden. +add_task(async function extension() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withVisits(async visitURLs => { + // Add an extension provider that returns a heuristic. + let url = "http://example.com/extension-test"; + let provider = new UrlbarTestUtils.TestProvider({ + name: "ExtensionTest", + type: UrlbarUtils.PROVIDER_TYPE.EXTENSION, + results: [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url, + title: "Test", + } + ), + { heuristic: true } + ), + ], + }); + UrlbarProvidersManager.registerProvider(provider); + + // Do a search that fetches the provider's result and check it. + let heuristic = await search({ + value: "test", + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION, + }); + Assert.equal(heuristic.payload.url, url, "Heuristic URL is correct"); + + // Check the other visit results. + await checkVisitResults(visitURLs); + + // Press enter to verify the heuristic result is loaded. + await synthesizeEnterAndAwaitLoad(url); + + UrlbarProvidersManager.unregisterProvider(provider); + }); + }); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX should be hidden. +add_task(async function omnibox() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + // Load an extension. + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + omnibox: { + keyword: "omniboxtest", + }, + }, + background() { + /* global browser */ + browser.omnibox.onInputEntered.addListener(() => { + browser.test.sendMessage("onInputEntered"); + }); + }, + }); + await extension.startup(); + + // Do a search using the omnibox keyword and check the hidden heuristic + // result. + let heuristic = await search({ + value: "omniboxtest foo", + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX, + }); + Assert.equal( + heuristic.payload.keyword, + "omniboxtest", + "Heuristic keyword is correct" + ); + + // Press enter to verify the heuristic result is picked. + let messagePromise = extension.awaitMessage("onInputEntered"); + EventUtils.synthesizeKey("KEY_Enter"); + await messagePromise; + + await extension.unload(); + }); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP should be shown. +add_task(async function searchTip() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.searchTips.test.ignoreShowLimits", true]], + }); + await BrowserTestUtils.withNewTab( + { + gBrowser: window.gBrowser, + url: "about:newtab", + // `withNewTab` hangs waiting for about:newtab to load without this. + waitForLoad: false, + }, + async () => { + await UrlbarTestUtils.promisePopupOpen(window, () => {}); + Assert.ok(true, "View opened"); + Assert.equal(UrlbarTestUtils.getResultCount(window), 1); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TIP); + Assert.ok(result.heuristic); + Assert.ok(UrlbarTestUtils.getSelectedElement(window), "Selection exists"); + } + ); + await SpecialPowers.popPrefEnv(); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_ENGINE_ALIAS should be hidden. +add_task(async function engineAlias() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withVisits(async visitURLs => { + // Add an engine with an alias. + await withEngine({ keyword: "test" }, async () => { + // Do a search using the alias and check the hidden heuristic result. + // The heuristic will be HEURISTIC_FALLBACK, not HEURISTIC_ENGINE_ALIAS, + // because two searches are performed and + // `UrlbarTestUtils.promiseAutocompleteResultPopup` waits for both. The + // first returns a HEURISTIC_ENGINE_ALIAS that triggers search mode and + // then an immediate second search, which returns HEURISTIC_FALLBACK. + let heuristic = await search({ + value: "test foo", + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK, + }); + Assert.equal( + heuristic.payload.engine, + "Example", + "Heuristic engine is correct" + ); + Assert.equal( + heuristic.payload.query, + "foo", + "Heuristic query is correct" + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: "Example", + entry: "typed", + }); + + // Check the other visit results. + await checkVisitResults(visitURLs); + + // Press enter to verify the heuristic result is loaded. + await synthesizeEnterAndAwaitLoad("https://example.com/?q=foo"); + }); + }); + }); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD should be hidden. +add_task(async function bookmarkKeyword() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withVisits(async visitURLs => { + // Add a bookmark with a keyword. + let keyword = "bm"; + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/?q=%s", + title: "test", + }); + await PlacesUtils.keywords.insert({ keyword, url: bm.url }); + + // Do a search using the keyword and check the hidden heuristic result. + let heuristic = await search({ + value: "bm foo", + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD, + }); + Assert.equal( + heuristic.payload.keyword, + keyword, + "Heuristic keyword is correct" + ); + let heuristicURL = "http://example.com/?q=foo"; + Assert.equal( + heuristic.payload.url, + heuristicURL, + "Heuristic URL is correct" + ); + + // Check the other visit results. + await checkVisitResults(visitURLs); + + // Press enter to verify the heuristic result is loaded. + await synthesizeEnterAndAwaitLoad(heuristicURL); + + await PlacesUtils.bookmarks.eraseEverything(); + await UrlbarTestUtils.formHistory.clear(window); + }); + }); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL should be hidden. +add_task(async function autofill() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withVisits(async visitURLs => { + // Do a search that triggers autofill and check the hidden heuristic + // result. + let heuristic = await search({ + value: "ex", + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL, + }); + Assert.ok(heuristic.autofill, "Heuristic is autofill"); + let heuristicURL = "http://example.com/"; + Assert.equal( + heuristic.payload.url, + heuristicURL, + "Heuristic URL is correct" + ); + Assert.equal(gURLBar.value, "example.com/", "Input has been autofilled"); + + // Check the other visit results. + await checkVisitResults(visitURLs); + + // Press enter to verify the heuristic result is loaded. + await synthesizeEnterAndAwaitLoad(heuristicURL); + }); + }); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK with an unknown URL should be +// hidden. +add_task(async function fallback_unknownURL() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + // Do a search for an unknown URL and check the hidden heuristic result. + let url = "http://example.com/unknown-url"; + let heuristic = await search({ + value: url, + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK, + }); + Assert.equal(heuristic.payload.url, url, "Heuristic URL is correct"); + + // Press enter to verify the heuristic result is loaded. + await synthesizeEnterAndAwaitLoad(url); + }); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK with the search restriction token +// should be hidden. +add_task(async function fallback_searchRestrictionToken() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withVisits(async visitURLs => { + // Add a mock default engine so we don't hit the network. + await withEngine({ makeDefault: true }, async () => { + // Do a search with `?` and check the hidden heuristic result. + let heuristic = await search({ + value: UrlbarTokenizer.RESTRICT.SEARCH + " foo", + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK, + }); + Assert.equal( + heuristic.payload.engine, + "Example", + "Heuristic engine is correct" + ); + Assert.equal( + heuristic.payload.query, + "foo", + "Heuristic query is correct" + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: "Example", + entry: "typed", + }); + + // Check the other visit results. + await checkVisitResults(visitURLs); + + // Press enter to verify the heuristic result is loaded. + await synthesizeEnterAndAwaitLoad("https://example.com/?q=foo"); + + await UrlbarTestUtils.formHistory.clear(window); + }); + }); + }); +}); + +// UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK with a search string that falls +// back to a search result should be hidden. +add_task(async function fallback_search() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withVisits(async visitURLs => { + // Add a mock default engine so we don't hit the network. + await withEngine({ makeDefault: true }, async () => { + // Do a search and check the hidden heuristic result. + let heuristic = await search({ + value: "foo", + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK, + }); + Assert.equal( + heuristic.payload.engine, + "Example", + "Heuristic engine is correct" + ); + Assert.equal( + heuristic.payload.query, + "foo", + "Heuristic query is correct" + ); + + // Check the other visit results. + await checkVisitResults(visitURLs); + + // Press enter to verify the heuristic result is loaded. + await synthesizeEnterAndAwaitLoad("https://example.com/?q=foo"); + + await UrlbarTestUtils.formHistory.clear(window); + }); + }); + }); +}); + +// Picking a non-heuristic result should work correctly (and not pick the +// heuristic). +add_task(async function pickNonHeuristic() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withVisits(async visitURLs => { + // Do a search that triggers autofill and check the hidden heuristic + // result. + let heuristic = await search({ + value: "ex", + expectedGroup: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL, + }); + Assert.ok(heuristic.autofill, "Heuristic is autofill"); + Assert.equal( + heuristic.payload.url, + "http://example.com/", + "Heuristic URL is correct" + ); + + // Pick the first visit result. + Assert.notEqual( + heuristic.payload.url, + visitURLs[0], + "Sanity check: Heuristic and first results have different URLs" + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await synthesizeEnterAndAwaitLoad(visitURLs[0]); + }); + }); +}); + +/** + * Adds `maxRichResults` visits, calls your callback, and clears history. We add + * `maxRichResults` visits to verify that the view correctly contains the + * maximum number of results when the heuristic is hidden. + * + * @param {Function} callback + * The callback to call after adding visits. Can be async + */ +async function withVisits(callback) { + let urls = []; + for (let i = 0; i < UrlbarPrefs.get("maxRichResults"); i++) { + urls.push("http://example.com/foo/" + i); + } + await PlacesTestUtils.addVisits(urls); + + // The URLs will appear in the view in reverse order so that newer visits are + // first. Reverse the array now so callers to `checkVisitResults` or + // `checkVisitResults` itself doesn't need to do it. + urls.reverse(); + + await callback(urls); + await PlacesUtils.history.clear(); +} + +/** + * Adds a search engine, calls your callback, and removes the engine. + * + * @param {object} options + * Options object + * @param {string} [options.keyword] + * The keyword/alias for the engine. + * @param {boolean} [options.makeDefault] + * Whether to make the engine default. + * @param {Function} callback + * The callback to call after changing the default search engine. Can be async + */ +async function withEngine( + { keyword = undefined, makeDefault = false }, + callback +) { + await SearchTestUtils.installSearchExtension({ keyword }); + let engine = Services.search.getEngineByName("Example"); + let originalEngine; + if (makeDefault) { + originalEngine = await Services.search.getDefault(); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + } + await callback(); + if (originalEngine) { + await Services.search.setDefault( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + } + await Services.search.removeEngine(engine); +} + +/** + * Asserts the view contains visit results with the given URLs. + * + * @param {Array} expectedURLs + * The expected urls. + */ +async function checkVisitResults(expectedURLs) { + Assert.equal( + UrlbarTestUtils.getResultCount(window), + expectedURLs.length, + "The view has other results" + ); + for (let i = 0; i < expectedURLs.length; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.URL, + "Other result type is correct at index " + i + ); + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.HISTORY, + "Other result source is correct at index " + i + ); + Assert.equal( + result.url, + expectedURLs[i], + "Other result URL is correct at index " + i + ); + } +} + +/** + * Performs a search and makes some basic assertions under the assumption that + * the heuristic should be hidden. + * + * @param {object} options + * Options object + * @param {string} options.value + * The search string. + * @param {UrlbarUtils.RESULT_GROUP} options.expectedGroup + * The expected result group of the hidden heuristic. + * @returns {UrlbarResult} + * The hidden heuristic result. + */ +async function search({ value, expectedGroup }) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value, + fireInputEvent: true, + }); + + // _resultForCurrentValue should be the hidden heuristic result. + let { _resultForCurrentValue: result } = gURLBar; + Assert.ok(result, "_resultForCurrentValue is defined"); + Assert.ok(result.heuristic, "_resultForCurrentValue.heuristic is true"); + Assert.equal( + UrlbarUtils.getResultGroup(result), + expectedGroup, + "_resultForCurrentValue has expected group" + ); + + Assert.ok(!UrlbarTestUtils.getSelectedElement(window), "No selection exists"); + + return result; +} + +/** + * Synthesizes the enter key and waits for a load in the current tab. + * + * @param {string} expectedURL + * The URL that should load. + */ +async function synthesizeEnterAndAwaitLoad(expectedURL) { + let loadPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + expectedURL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + await PlacesUtils.history.clear(); +} diff --git a/browser/components/urlbar/tests/browser/browser_ime_composition.js b/browser/components/urlbar/tests/browser/browser_ime_composition.js new file mode 100644 index 0000000000..5d04f51411 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_ime_composition.js @@ -0,0 +1,328 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests ime composition handling. + +function composeAndCheckPanel(string, isPopupOpen) { + EventUtils.synthesizeCompositionChange({ + composition: { + string, + clauses: [ + { + length: string.length, + attr: Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE, + }, + ], + }, + caret: { start: string.length, length: 0 }, + key: { key: string ? string[string.length - 1] : "KEY_Backspace" }, + }); + Assert.equal( + UrlbarTestUtils.isPopupOpen(window), + isPopupOpen, + "Check panel open state" + ); +} + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0], + ["browser.urlbar.suggest.quickactions", false], + ], + }); + + await PlacesUtils.history.clear(); + // Add at least one typed entry for the empty results set. Also clear history + // so that this can be over the autofill threshold. + await PlacesTestUtils.addVisits({ + uri: "http://mozilla.org/", + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }); + // Add a bookmark to ensure we autofill the engine domain for tab-to-search. + let bm = await PlacesUtils.bookmarks.insert({ + url: "http://example.com/", + parentGuid: PlacesUtils.bookmarks.menuGuid, + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + await SearchTestUtils.installSearchExtension( + { + name: "Test", + keyword: "@test", + }, + { setAsDefault: true } + ); + + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.remove(bm); + await PlacesUtils.history.clear(); + }); + + // Test both pref values. + for (let val of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.keepPanelOpenDuringImeComposition", val]], + }); + await test_composition(val); + await test_composition_searchMode_preview(val); + await test_composition_tabToSearch(val); + await test_composition_autofill(val); + } + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); +}); + +async function test_composition(keepPanelOpenDuringImeComposition) { + gURLBar.focus(); + await UrlbarTestUtils.promisePopupClose(window); + + info("Check the panel state during composition"); + composeAndCheckPanel("I", false); + Assert.equal(gURLBar.value, "I", "Check urlbar value"); + composeAndCheckPanel("In", false); + Assert.equal(gURLBar.value, "In", "Check urlbar value"); + + info("Committing composition should open the panel."); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeComposition({ + type: "compositioncommitasis", + key: { key: "KEY_Enter" }, + }); + }); + Assert.equal(gURLBar.value, "In", "Check urlbar value"); + + info("Check the panel state starting from an open panel."); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open"); + composeAndCheckPanel("t", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "Int", "Check urlbar value"); + composeAndCheckPanel("te", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "Inte", "Check urlbar value"); + + // Committing composition should open the popup. + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeComposition({ + type: "compositioncommitasis", + key: { key: "KEY_Enter" }, + }); + }); + Assert.equal(gURLBar.value, "Inte", "Check urlbar value"); + + info("If composition is cancelled, the value shouldn't be changed."); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open"); + composeAndCheckPanel("r", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "Inter", "Check urlbar value"); + composeAndCheckPanel("", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "Inte", "Check urlbar value"); + // Canceled compositionend should reopen the popup. + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeComposition({ + type: "compositioncommit", + data: "", + key: { key: "KEY_Escape" }, + }); + }); + Assert.equal(gURLBar.value, "Inte", "Check urlbar value"); + + info( + "If composition replaces some characters and canceled, the search string should be the latest value." + ); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open"); + EventUtils.synthesizeKey("VK_LEFT", { shiftKey: true }); + EventUtils.synthesizeKey("VK_LEFT", { shiftKey: true }); + composeAndCheckPanel("t", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "Int", "Check urlbar value"); + composeAndCheckPanel("te", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "Inte", "Check urlbar value"); + composeAndCheckPanel("", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "In", "Check urlbar value"); + + // Canceled compositionend should search the result with the latest value. + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeComposition({ + type: "compositioncommitasis", + key: { key: "KEY_Escape" }, + }); + }); + Assert.equal(gURLBar.value, "In", "Check urlbar value"); + + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open"); + info( + "Removing all characters should leave the popup open, Esc should then close it." + ); + EventUtils.synthesizeKey("KEY_Backspace", {}); + EventUtils.synthesizeKey("KEY_Backspace", {}); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open"); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape", {}); + }); + Assert.equal(gURLBar.value, "", "Check urlbar value"); + + info("Composition which is canceled shouldn't cause opening the popup."); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Popup should be closed"); + composeAndCheckPanel("I", false); + Assert.equal(gURLBar.value, "I", "Check urlbar value"); + composeAndCheckPanel("In", false); + Assert.equal(gURLBar.value, "In", "Check urlbar value"); + composeAndCheckPanel("", false); + Assert.equal(gURLBar.value, "", "Check urlbar value"); + + info("Canceled compositionend shouldn't open the popup if it was closed."); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeComposition({ + type: "compositioncommitasis", + key: { key: "KEY_Escape" }, + }); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Popup should be closed"); + Assert.equal(gURLBar.value, "", "Check urlbar value"); + + info("Down key should open the popup even if the editor is empty."); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeKey("KEY_ArrowDown", {}); + }); + Assert.equal(gURLBar.value, "", "Check urlbar value"); + + info( + "If popup is open at starting composition, the popup should be reopened after composition anyway." + ); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open"); + composeAndCheckPanel("I", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "I", "Check urlbar value"); + composeAndCheckPanel("In", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "In", "Check urlbar value"); + composeAndCheckPanel("", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "", "Check urlbar value"); + // A canceled compositionend should open the popup if it was open. + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeComposition({ + type: "compositioncommitasis", + key: { key: "KEY_Escape" }, + }); + }); + Assert.equal(gURLBar.value, "", "Check urlbar value"); + + info("Type normally, and hit escape, the popup should be closed."); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open"); + EventUtils.synthesizeKey("I", {}); + EventUtils.synthesizeKey("n", {}); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape", {}); + }); + Assert.equal(gURLBar.value, "In", "Check urlbar value"); + // Clear typed chars. + EventUtils.synthesizeKey("KEY_Backspace", {}); + EventUtils.synthesizeKey("KEY_Backspace", {}); + Assert.equal(gURLBar.value, "", "Check urlbar value"); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape", {}); + }); + + info("With autofill, compositionstart shouldn't open the popup"); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Popup should be closed"); + composeAndCheckPanel("M", false); + Assert.equal(gURLBar.value, "M", "Check urlbar value"); + composeAndCheckPanel("Mo", false); + Assert.equal(gURLBar.value, "Mo", "Check urlbar value"); + // Committing composition should open the popup. + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeComposition({ + type: "compositioncommitasis", + key: { key: "KEY_Enter" }, + }); + }); + Assert.equal(gURLBar.value, "Mozilla.org/", "Check urlbar value"); +} + +async function test_composition_searchMode_preview( + keepPanelOpenDuringImeComposition +) { + info("Check Search Mode preview is retained by composition"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + + while (gURLBar.searchMode?.engineName != "Test") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + } + let expectedSearchMode = { + engineName: "Test", + isPreview: true, + entry: "keywordoffer", + }; + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + composeAndCheckPanel("I", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "I", "Check urlbar value"); + if (keepPanelOpenDuringImeComposition) { + await UrlbarTestUtils.promiseSearchComplete(window); + } + // Test that we are in confirmed search mode. + await UrlbarTestUtils.assertSearchMode(window, { + engineName: "Test", + entry: "keywordoffer", + }); + await UrlbarTestUtils.exitSearchMode(window); +} + +async function test_composition_tabToSearch(keepPanelOpenDuringImeComposition) { + info("Check Tab-to-Search is retained by composition"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exa", + fireInputEvent: true, + }); + + while (gURLBar.searchMode?.engineName != "Test") { + EventUtils.synthesizeKey("KEY_Tab", {}, window); + } + let expectedSearchMode = { + engineName: "Test", + isPreview: true, + entry: "tabtosearch", + }; + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + composeAndCheckPanel("I", keepPanelOpenDuringImeComposition); + Assert.equal(gURLBar.value, "I", "Check urlbar value"); + if (keepPanelOpenDuringImeComposition) { + await UrlbarTestUtils.promiseSearchComplete(window); + } + // Test that we are in confirmed search mode. + await UrlbarTestUtils.assertSearchMode(window, { + engineName: "Test", + entry: "tabtosearch", + }); + await UrlbarTestUtils.exitSearchMode(window); +} + +async function test_composition_autofill(keepPanelOpenDuringImeComposition) { + info("Check whether autofills or not"); + await UrlbarTestUtils.promisePopupClose(window); + + info("Check the urlbar value during composition"); + composeAndCheckPanel("m", false); + + if (keepPanelOpenDuringImeComposition) { + info("Wait for search suggestions"); + await UrlbarTestUtils.promiseSearchComplete(window); + } + + Assert.equal( + gURLBar.value, + "m", + "The urlbar value is not autofilled while turning IME on" + ); + + info("Check the urlbar value after committing composition"); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeComposition({ + type: "compositioncommitasis", + key: { key: "KEY_Enter" }, + }); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal(gURLBar.value, "mozilla.org/", "The urlbar value is autofilled"); + + // Clean-up. + gURLBar.value = ""; +} diff --git a/browser/components/urlbar/tests/browser/browser_inputHistory.js b/browser/components/urlbar/tests/browser/browser_inputHistory.js new file mode 100644 index 0000000000..0791f9da20 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_inputHistory.js @@ -0,0 +1,676 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests the urlbar adaptive behavior powered by input history. + */ + +"use strict"; + +async function bumpScore( + uri, + searchString, + counts, + useMouseClick = false, + needToLoad = false +) { + if (counts.visits) { + let visits = new Array(counts.visits).fill(uri); + await PlacesTestUtils.addVisits(visits); + } + if (counts.picks) { + for (let i = 0; i < counts.picks; ++i) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + }); + let promise = needToLoad + ? BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser) + : BrowserTestUtils.waitForDocLoadAndStopIt( + uri, + gBrowser.selectedBrowser + ); + // Look for the expected uri. + while (gURLBar.untrimmedValue != uri) { + EventUtils.synthesizeKey("KEY_ArrowDown", {}); + } + if (useMouseClick) { + let element = UrlbarTestUtils.getSelectedRow(window); + EventUtils.synthesizeMouseAtCenter(element, {}); + } else { + EventUtils.synthesizeKey("KEY_Enter", {}); + } + await promise; + } + } + await PlacesTestUtils.promiseAsyncUpdates(); +} + +async function decayInputHistory() { + await Cc["@mozilla.org/places/frecency-recalculator;1"] + .getService(Ci.nsIObserver) + .wrappedJSObject.decay(); +} + +async function isPageInInputHistory(url) { + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.executeCached( + `SELECT 1 + FROM moz_inputhistory i + JOIN moz_places h ON h.id = i.place_id + WHERE h.url = :url`, + { url } + ); + return rows?.length > 0; +} + +async function isInputHistoryUrlInResults(url) { + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); ++i) { + const result = await UrlbarTestUtils.getRowAt(window, i).result; + if (result.providerName == "InputHistory") { + if (result.payload.url == url) { + return true; + } + } + } + return false; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // We don't want autofill to influence this test. + ["browser.urlbar.autoFill", false], + ], + }); + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function test_adaptive_with_search_terms() { + let url1 = "http://site.tld/1"; + let url2 = "http://site.tld/2"; + + info("Same visit count, same picks, one partial match, one exact match"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "si", { visits: 3, picks: 3 }); + await bumpScore(url2, "site", { visits: 3, picks: 3 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url1, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url2, "Check second result"); + + info( + "Same visit count, same picks, one partial match, one exact match, invert" + ); + await PlacesUtils.history.clear(); + await bumpScore(url1, "site", { visits: 3, picks: 3 }); + await bumpScore(url2, "si", { visits: 3, picks: 3 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url2, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url1, "Check second result"); + + info("Same visit count, different picks, both exact match"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "si", { visits: 3, picks: 3 }); + await bumpScore(url2, "si", { visits: 3, picks: 1 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url1, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url2, "Check second result"); + + info("Same visit count, different picks, both exact match, invert"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "si", { visits: 3, picks: 1 }); + await bumpScore(url2, "si", { visits: 3, picks: 3 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url2, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url1, "Check second result"); + + info("Same visit count, different picks, both partial match"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "site", { visits: 3, picks: 3 }); + await bumpScore(url2, "site", { visits: 3, picks: 1 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url1, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url2, "Check second result"); + + info("Same visit count, different picks, both partial match, invert"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "site", { visits: 3, picks: 1 }); + await bumpScore(url2, "site", { visits: 3, picks: 3 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url2, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url1, "Check second result"); +}); + +add_task(async function test_adaptive_with_decay() { + let url1 = "http://site.tld/1"; + let url2 = "http://site.tld/2"; + + info("Same visit count, same picks, both exact match, decay first"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "si", { visits: 3, picks: 3 }); + await decayInputHistory(); + await bumpScore(url2, "si", { visits: 3, picks: 3 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url2, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url1, "Check second result"); + + info("Same visit count, same picks, both exact match, decay second"); + await PlacesUtils.history.clear(); + await bumpScore(url2, "si", { visits: 3, picks: 3 }); + await decayInputHistory(); + await bumpScore(url1, "si", { visits: 3, picks: 3 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url1, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url2, "Check second result"); +}); + +add_task(async function test_adaptive_limited() { + let url1 = "http://site.tld/1"; + let url2 = "http://site.tld/2"; + + info("Same visit count, same picks, both exact match, decay first"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "si", { visits: 3, picks: 3 }); + await decayInputHistory(); + await bumpScore(url2, "si", { visits: 3, picks: 3 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url2, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url1, "Check second result"); + + info("Same visit count, same picks, both exact match, decay second"); + await PlacesUtils.history.clear(); + await bumpScore(url2, "si", { visits: 3, picks: 3 }); + await decayInputHistory(); + await bumpScore(url1, "si", { visits: 3, picks: 3 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url1, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url2, "Check second result"); +}); + +add_task(async function test_adaptive_limited() { + info("Up to 3 adaptive results should be added at the top, then enqueued"); + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + // Add as many adaptive results as maxRichResults. + let n = UrlbarPrefs.get("maxRichResults"); + let urls = Array(n) + .fill(0) + .map((e, i) => "http://site.tld/" + i); + for (let url of urls) { + await bumpScore(url, "site", { visits: 1, picks: 1 }); + } + + // Add a matching bookmark with an higher frecency. + let url = "http://site.bookmark.tld/"; + await PlacesTestUtils.addVisits(url); + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "test_site_book", + url, + }); + + // After 1 heuristic and 3 input history results. + let expectedBookmarkIndex = 4; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "site", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + expectedBookmarkIndex + ); + Assert.equal(result.url, url, "Check bookmarked result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, n - 1); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + n, + "Check all the results are filled" + ); + Assert.ok( + result.url.startsWith("http://site.tld"), + "Check last adaptive result" + ); + + await PlacesUtils.bookmarks.remove(bm); +}); + +add_task(async function test_adaptive_behaviors() { + info( + "Check adaptive results are not provided regardless of the requested behavior" + ); + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + // Add an adaptive entry. + let historyUrl = "http://site.tld/1"; + await bumpScore(historyUrl, "site", { visits: 1, picks: 1 }); + + let bookmarkURL = "http://bookmarked.site.tld/1"; + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "test_book", + url: bookmarkURL, + }); + + await SpecialPowers.pushPrefEnv({ + set: [ + // Search only bookmarks. + ["browser.urlbar.suggest.bookmark", true], + ["browser.urlbar.suggest.history", false], + ], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "site", + }); + let result = (await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)) + .result; + Assert.equal(result.payload.url, bookmarkURL, "Check bookmarked result"); + Assert.notEqual( + result.providerName, + "InputHistory", + "The bookmarked result is not from InputHistory." + ); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Check there are no unexpected results" + ); + await PlacesUtils.bookmarks.remove(bm); + + // Repeat the previous case but now the bookmark has the same URL as the + // history result. We expect the returned result comes from InputHistory. + bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "test_book", + url: historyUrl, + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "sit", + }); + result = (await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)) + .result; + Assert.equal(result.payload.url, historyUrl, "Check bookmarked result"); + Assert.equal( + result.providerName, + "InputHistory", + "The bookmarked result is from InputHistory." + ); + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + "The input history result is a bookmark." + ); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Check there are no unexpected results" + ); + + await SpecialPowers.popPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + // Search only open pages. We don't provide an open page, but we want to + // enable at least one of these prefs so that UrlbarProviderInputHistory + // is active. + ["browser.urlbar.suggest.bookmark", false], + ["browser.urlbar.suggest.history", false], + ["browser.urlbar.suggest.openpage", true], + ], + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "site", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "There is no adaptive history result because it is not an open page." + ); + await SpecialPowers.popPrefEnv(); + + // Clearing history but not deleting the bookmark. This simulates the case + // where the user has cleared their history or is using permanent private + // browsing mode. + await PlacesUtils.history.clear(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.bookmark", true], + ["browser.urlbar.suggest.history", false], + ["browser.urlbar.suggest.openpage", false], + ], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "sit", + }); + result = (await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)) + .result; + Assert.equal(result.payload.url, historyUrl, "Check bookmarked result"); + Assert.equal( + result.providerName, + "InputHistory", + "The bookmarked result is from InputHistory." + ); + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + "The input history result is a bookmark." + ); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Check there are no unexpected results" + ); + + await PlacesUtils.bookmarks.remove(bm); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_adaptive_mouse() { + info("Check adaptive results are updated on mouse picks"); + let url1 = "http://site.tld/1"; + let url2 = "http://site.tld/2"; + + info("Same visit count, different picks"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "site", { visits: 3, picks: 3 }, true); + await bumpScore(url2, "site", { visits: 3, picks: 1 }, true); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url1, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url2, "Check second result"); + + info("Same visit count, different picks, invert"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "site", { visits: 3, picks: 1 }, true); + await bumpScore(url2, "site", { visits: 3, picks: 3 }, true); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url2, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url1, "Check second result"); +}); + +add_task(async function test_adaptive_searchmode() { + info("Check adaptive history is not shown in search mode."); + + let suggestionsEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + }); + + let url1 = "http://site.tld/1"; + let url2 = "http://site.tld/2"; + + info("Sanity check: adaptive history is shown for a normal search."); + await PlacesUtils.history.clear(); + await bumpScore(url1, "site", { visits: 3, picks: 3 }, true); + await bumpScore(url2, "site", { visits: 3, picks: 1 }, true); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url1, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url2, "Check second result"); + + info("Entering search mode."); + // enterSearchMode checks internally that our site.tld URLs are not shown. + await UrlbarTestUtils.enterSearchMode(window, { + engineName: suggestionsEngine.name, + }); + + await Services.search.removeEngine(suggestionsEngine); +}); + +add_task(async function test_ignore_case() { + const url1 = "http://example.com/yes"; + const url2 = "http://example.com/no"; + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits([url1, url2]); + await UrlbarUtils.addToInputHistory(url1, "SampLE"); + await UrlbarUtils.addToInputHistory(url1, "SaMpLE"); + await UrlbarUtils.addToInputHistory(url1, "SAMPLE"); + await UrlbarUtils.addToInputHistory(url1, "sample"); + await UrlbarUtils.addToInputHistory(url2, "sample"); + await UrlbarUtils.addToInputHistory(url2, "sample"); + await UrlbarUtils.addToInputHistory(url2, "sample"); + await UrlbarUtils.addToInputHistory(url2, "sample"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "sAM", + }); + const result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + result.url, + url1, + "Seaching for input history is case-insensitive" + ); +}); + +add_task(async function test_adaptive_history_in_privatewindow() { + info( + "Check adaptive history is not shown in private window as tab switching candidate." + ); + + await PlacesUtils.history.clear(); + + info("Add a test url as an input history."); + const url = "http://example.com/"; + // We need to wait for loading the page in order to register the url into + // moz_openpages_temp table. + await bumpScore(url, "exa", { visits: 1, picks: 1 }, false, true); + + info("Check the url could be registered properly."); + const connection = await PlacesUtils.promiseLargeCacheDBConnection(); + const rows = await connection.executeCached( + "SELECT userContextId FROM moz_openpages_temp WHERE url = :url", + { url } + ); + Assert.equal(rows.length, 1, "Length of rows for the url is 1."); + Assert.greaterOrEqual( + rows[0].getResultByName("userContextId"), + 0, + "The url is registered as non-private-browsing context." + ); + + info("Open popup in private window."); + const privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: privateWindow, + value: "ex", + }); + + info("Check the popup results."); + let hasResult = false; + for (let i = 0; i < UrlbarTestUtils.getResultCount(privateWindow); i++) { + const result = await UrlbarTestUtils.getDetailsOfResultAt(privateWindow, i); + + if (result.url !== url) { + continue; + } + + Assert.notEqual( + result.type, + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + "Result type of the url is not for tab switch." + ); + + hasResult = true; + } + Assert.ok(hasResult, "Popup has a result for the url."); + + await BrowserTestUtils.closeWindow(privateWindow); +}); + +add_task(async function test_adaptive_dismiss() { + info("Check dismissing an adaptive history result"); + + let url1 = "http://site.tld/1"; + let url2 = "http://site.tld/2"; + + info("Sanity check: adaptive history is shown for a normal search."); + await PlacesUtils.history.clear(); + await bumpScore(url1, "site", { visits: 3, picks: 3 }, true); + await bumpScore(url2, "site", { visits: 3, picks: 1 }, true); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + let result = UrlbarTestUtils.getRowAt(window, 1).result; + Assert.equal(result.payload.url, url1, "Check result #1 URL"); + Assert.equal(result.providerName, "InputHistory", "Check result #1 provider"); + result = UrlbarTestUtils.getRowAt(window, 2).result; + Assert.equal(result.payload.url, url2, "Check result #2 URL"); + Assert.equal(result.providerName, "InputHistory", "Check result #2 provider"); + let waitForHistoryRemoval = + PlacesTestUtils.waitForNotification("page-removed"); + await UrlbarTestUtils.openResultMenuAndClickItem(window, "dismiss", { + resultIndex: 1, + }); + await waitForHistoryRemoval; + Assert.ok( + gURLBar.view.isOpen, + "The view should remain open after clicking the command" + ); + + Assert.ok( + !(await isInputHistoryUrlInResults(url1)), + "Check result has been removed" + ); + Assert.strictEqual( + await PlacesUtils.history.fetch(url1), + null, + "The removed page should not be in browsing history" + ); + Assert.ok( + !(await isPageInInputHistory(url1)), + "The removed page should not be in input history" + ); + + Assert.ok( + await isInputHistoryUrlInResults(url2), + "Check result has been retained" + ); + Assert.notStrictEqual( + await PlacesUtils.history.fetch(url2), + null, + "The non removed page should still be in history" + ); + Assert.ok( + await isPageInInputHistory(url2), + "The non removed page should still be in input history" + ); +}); + +add_task(async function test_bookmarked_adaptive_dismiss() { + info("Check dismissing a bookmarked adaptive history result"); + + let url = "http://mysite.tld/"; + + info("Sanity check: adaptive history is shown for a normal search."); + await PlacesUtils.history.clear(); + await bumpScore(url, "site", { visits: 3, picks: 3 }, true); + await PlacesUtils.bookmarks.insert({ + url, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + let result = UrlbarTestUtils.getRowAt(window, 1).result; + Assert.equal(result.payload.url, url, "Check result #1 URL"); + Assert.equal(result.providerName, "InputHistory", "Check result #1 provider"); + + let waitForHistoryRemoval = + PlacesTestUtils.waitForNotification("page-removed"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true }); + await waitForHistoryRemoval; + Assert.ok( + gURLBar.view.isOpen, + "The view should remain open after removing history" + ); + + Assert.ok( + !(await isInputHistoryUrlInResults(url)), + "Check result has been removed" + ); + Assert.ok( + !(await PlacesUtils.history.hasVisits(url)), + "The removed page should not be in browsing history" + ); + Assert.ok( + !(await isPageInInputHistory(url)), + "The removed page should be in input history" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_inputHistory_autofill.js b/browser/components/urlbar/tests/browser/browser_inputHistory_autofill.js new file mode 100644 index 0000000000..5c8ad73491 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_inputHistory_autofill.js @@ -0,0 +1,210 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests for input history related to autofill. + +"use strict"; + +let addToInputHistorySpy; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill.adaptiveHistory.enabled", true]], + }); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + + let sandbox = sinon.createSandbox(); + addToInputHistorySpy = sandbox.spy(UrlbarUtils, "addToInputHistory"); + + registerCleanupFunction(async () => { + sandbox.restore(); + }); +}); + +// Input history use count should be bumped when an adaptive history autofill +// result is triggered and picked. +add_task(async function bumped() { + let input = "exam"; + let tests = [ + // Basic test where the search string = the adaptive history input. + { + url: "http://example.com/test", + searchString: "exam", + }, + // The history with input "exam" should be bumped, not "example", even + // though the search string is "example". + { + url: "http://example.com/test", + searchString: "example", + }, + // The history with URL "http://www.example.com/test" should be bumped, not + // "http://example.com/test", even though the search string starts with + // "example". + { + url: "http://www.example.com/test", + searchString: "exam", + }, + ]; + + for (let { url, searchString } of tests) { + info("Running subtest: " + JSON.stringify({ url, searchString })); + + await PlacesTestUtils.addVisits(url); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await UrlbarUtils.addToInputHistory(url, input); + addToInputHistorySpy.resetHistory(); + + let initialUseCount = await getUseCount({ url, input }); + info("Got initial use count: " + initialUseCount); + + await triggerAutofillAndPickResult(searchString, "example.com/test"); + + let calls = addToInputHistorySpy.getCalls(); + Assert.equal( + calls.length, + 1, + "UrlbarUtils.addToInputHistory() called once" + ); + Assert.deepEqual( + calls[0].args, + [url, input], + "UrlbarUtils.addToInputHistory() called with expected args" + ); + + Assert.greater( + await getUseCount({ url, input }), + initialUseCount, + "New use count > initial use count" + ); + + if (searchString != input) { + Assert.strictEqual( + await getUseCount({ input: searchString }), + undefined, + "Search string not present in input history: " + searchString + ); + } + + await PlacesUtils.history.clear(); + await PlacesTestUtils.clearInputHistory(); + addToInputHistorySpy.resetHistory(); + } +}); + +// Input history use count should not be bumped when an origin autofill result +// is triggered and picked. +add_task(async function notBumped_origin() { + // Add enough visits to trigger origin autofill. + let url = "http://example.com/test"; + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits(url); + } + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + await triggerAutofillAndPickResult("exam", "example.com/"); + + let calls = addToInputHistorySpy.getCalls(); + Assert.equal(calls.length, 0, "UrlbarUtils.addToInputHistory() not called"); + + Assert.strictEqual( + await getUseCount({ url }), + undefined, + "URL not present in input history: " + url + ); + + await PlacesUtils.history.clear(); +}); + +// Input history use count should not be bumped when a URL autofill result is +// triggered and picked. +add_task(async function notBumped_url() { + let url = "http://example.com/test"; + await PlacesTestUtils.addVisits(url); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + await triggerAutofillAndPickResult("example.com/t", "example.com/test"); + + let calls = addToInputHistorySpy.getCalls(); + Assert.equal(calls.length, 0, "UrlbarUtils.addToInputHistory() not called"); + + Assert.strictEqual( + await getUseCount({ url }), + undefined, + "URL not present in input history: " + url + ); + + await PlacesUtils.history.clear(); +}); + +/** + * Performs a search and picks the first result. + * + * @param {string} searchString + * The search string. Assumed to trigger an autofill result. + * @param {string} autofilledValue + * The input's expected value after autofill occurs. + */ +async function triggerAutofillAndPickResult(searchString, autofilledValue) { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill, "Result is autofill"); + Assert.equal(gURLBar.value, autofilledValue, "gURLBar.value"); + Assert.equal(gURLBar.selectionStart, searchString.length, "selectionStart"); + Assert.equal(gURLBar.selectionEnd, autofilledValue.length, "selectionEnd"); + + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + }); +} + +/** + * Gets the use count of an input history record. + * + * @param {object} options + * Options object. + * @param {string} [options.url] + * The URL of the `moz_places` row corresponding to the record. + * @param {string} [options.input] + * The `input` value in the record. + * @returns {number} + * The use count. If no record exists with the URL and/or input, undefined is + * returned. + */ +async function getUseCount({ url = undefined, input = undefined }) { + return PlacesUtils.withConnectionWrapper("test::getUseCount", async db => { + let rows; + if (input && url) { + rows = await db.executeCached( + `SELECT i.use_count + FROM moz_inputhistory i + JOIN moz_places h ON h.id = i.place_id + WHERE h.url = :url AND i.input = :input`, + { url, input } + ); + } else if (url) { + rows = await db.executeCached( + `SELECT i.use_count + FROM moz_inputhistory i + JOIN moz_places h ON h.id = i.place_id + WHERE h.url = :url`, + { url } + ); + } else if (input) { + rows = await db.executeCached( + `SELECT use_count + FROM moz_inputhistory i + WHERE input = :input`, + { input } + ); + } + return rows[0]?.getResultByIndex(0); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_inputHistory_emptystring.js b/browser/components/urlbar/tests/browser/browser_inputHistory_emptystring.js new file mode 100644 index 0000000000..421c01fb69 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_inputHistory_emptystring.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests input history in cases where the search string is empty. + * In the future we may want to not account for these, but for now they are + * stored with an empty input field. + */ + +"use strict"; + +async function checkInputHistory(len = 0) { + await PlacesUtils.withConnectionWrapper( + "test::checkInputHistory", + async db => { + let rows = await db.executeCached(`SELECT input FROM moz_inputhistory`); + Assert.equal(rows.length, len, "There should only be 1 entry"); + if (len) { + Assert.equal(rows[0].getResultByIndex(0), "", "Input should be empty"); + } + } + ); +} + +const TEST_URL = "http://example.com/"; + +async function do_test(openFn, pickMethod) { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:blank", + }, + async function (browser) { + await PlacesTestUtils.clearInputHistory(); + await openFn(); + await UrlbarTestUtils.promiseSearchComplete(window); + let promise = BrowserTestUtils.waitForDocLoadAndStopIt(TEST_URL, browser); + if (pickMethod == "keyboard") { + info(`Test pressing Enter`); + EventUtils.sendKey("down"); + EventUtils.sendKey("return"); + } else { + info("Test with click"); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + EventUtils.synthesizeMouseAtCenter(result.element.row, {}); + } + await promise; + await checkInputHistory(1); + } + ); +} + +add_setup(async function () { + await PlacesUtils.history.clear(); + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits(TEST_URL); + } + + await updateTopSites(sites => sites && sites[0] && sites[0].url == TEST_URL); + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function test_history_no_search_terms() { + for (let pickMethod of ["keyboard", "mouse"]) { + // If a testFn returns false, it will be skipped. + for (let openFn of [ + () => { + info("Test opening panel with down key"); + gURLBar.focus(); + EventUtils.sendKey("down"); + }, + async () => { + info("Test opening panel on focus"); + gURLBar.blur(); + EventUtils.synthesizeMouseAtCenter(gURLBar.textbox, {}); + }, + async () => { + info("Test opening panel on focus on a page"); + let selectedBrowser = gBrowser.selectedBrowser; + // A page other than TEST_URL must be loaded, or the first Top Site + // result will be a switch-to-tab result and page won't be reloaded when + // the result is selected. + BrowserTestUtils.startLoadingURIString( + selectedBrowser, + "http://example.org/" + ); + await BrowserTestUtils.browserLoaded(selectedBrowser); + gURLBar.blur(); + EventUtils.synthesizeMouseAtCenter(gURLBar.textbox, {}); + }, + ]) { + await do_test(openFn, pickMethod); + } + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js b/browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js new file mode 100644 index 0000000000..6ad6ce43e6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js @@ -0,0 +1,235 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify user typed text remains in the URL bar when tab switching, even when + * loads fail. + */ +add_task(async function validURL() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first_schemeless", false]], + }); + let input = "http://i-definitely-dont-exist.example.com"; + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com/" + ); + let browser = tab.linkedBrowser; + // Note: Waiting on content document not being hidden because new tab pages can be preloaded, + // in which case no load events fire. + await SpecialPowers.spawn(browser, [], async () => { + await ContentTaskUtils.waitForCondition(() => { + return content.document && !content.document.hidden; + }); + }); + let errorPageLoaded = BrowserTestUtils.waitForErrorPage(browser); + gURLBar.value = input; + gURLBar.select(); + EventUtils.sendKey("return"); + await errorPageLoaded; + is(gURLBar.value, UrlbarTestUtils.trimURL(input), "Text is still in URL bar"); + await BrowserTestUtils.switchTab(gBrowser, tab.previousElementSibling); + await BrowserTestUtils.switchTab(gBrowser, tab); + is( + gURLBar.value, + UrlbarTestUtils.trimURL(input), + "Text is still in URL bar after tab switch" + ); + BrowserTestUtils.removeTab(tab); +}); + +/** + * Invalid URIs fail differently (that is, immediately, in the loadURI call) + * if keyword searches are turned off. Test that this works, too. + */ +add_task(async function invalidURL() { + let input = "To be or not to be-that is the question"; + await SpecialPowers.pushPrefEnv({ set: [["keyword.enabled", false]] }); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank", + false + ); + let browser = tab.linkedBrowser; + // Note: Waiting on content document not being hidden because new tab pages can be preloaded, + // in which case no load events fire. + await SpecialPowers.spawn(browser, [], async () => { + await ContentTaskUtils.waitForCondition(() => { + return content.document && !content.document.hidden; + }); + }); + let errorPageLoaded = BrowserTestUtils.waitForErrorPage(tab.linkedBrowser); + gURLBar.value = input; + gURLBar.select(); + EventUtils.sendKey("return"); + await errorPageLoaded; + is(gURLBar.value, input, "Text is still in URL bar"); + is(tab.linkedBrowser.userTypedValue, input, "Text still stored on browser"); + await BrowserTestUtils.switchTab(gBrowser, tab.previousElementSibling); + await BrowserTestUtils.switchTab(gBrowser, tab); + is(gURLBar.value, input, "Text is still in URL bar after tab switch"); + is(tab.linkedBrowser.userTypedValue, input, "Text still stored on browser"); + BrowserTestUtils.removeTab(tab); +}); + +/** + * Test the urlbar status of text selection and focusing by tab switching. + */ +add_task(async function selectAndFocus() { + // Create a tab with normal web page. Use a test-url that uses a protocol that + // is not trimmed. + const webpageTabURL = + UrlbarTestUtils.getTrimmedProtocolWithSlashes() == "https://" + ? "http://example.com" + : "https://example.com"; + const webpageTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: webpageTabURL, + }); + + // Create a tab with userTypedValue. + const userTypedTabText = "test"; + const userTypedTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + }); + await UrlbarTestUtils.inputIntoURLBar(window, userTypedTabText); + + // Create an empty tab. + const emptyTab = await BrowserTestUtils.openNewForegroundTab({ gBrowser }); + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + BrowserTestUtils.removeTab(webpageTab); + BrowserTestUtils.removeTab(userTypedTab); + BrowserTestUtils.removeTab(emptyTab); + }); + + await doSelectAndFocusTest({ + targetTab: webpageTab, + targetSelectionStart: 0, + targetSelectionEnd: 0, + anotherTab: userTypedTab, + }); + await doSelectAndFocusTest({ + targetTab: webpageTab, + targetSelectionStart: 2, + targetSelectionEnd: 5, + anotherTab: userTypedTab, + }); + await doSelectAndFocusTest({ + targetTab: webpageTab, + targetSelectionStart: webpageTabURL.length, + targetSelectionEnd: webpageTabURL.length, + anotherTab: userTypedTab, + }); + await doSelectAndFocusTest({ + targetTab: webpageTab, + targetSelectionStart: 0, + targetSelectionEnd: 0, + anotherTab: emptyTab, + }); + await doSelectAndFocusTest({ + targetTab: userTypedTab, + targetSelectionStart: 0, + targetSelectionEnd: 0, + anotherTab: webpageTab, + }); + await doSelectAndFocusTest({ + targetTab: userTypedTab, + targetSelectionStart: 0, + targetSelectionEnd: 0, + anotherTab: emptyTab, + }); + await doSelectAndFocusTest({ + targetTab: userTypedTab, + targetSelectionStart: 1, + targetSelectionEnd: 2, + anotherTab: emptyTab, + }); + await doSelectAndFocusTest({ + targetTab: userTypedTab, + targetSelectionStart: userTypedTabText.length, + targetSelectionEnd: userTypedTabText.length, + anotherTab: emptyTab, + }); + await doSelectAndFocusTest({ + targetTab: emptyTab, + targetSelectionStart: 0, + targetSelectionEnd: 0, + anotherTab: webpageTab, + }); + await doSelectAndFocusTest({ + targetTab: emptyTab, + targetSelectionStart: 0, + targetSelectionEnd: 0, + anotherTab: userTypedTab, + }); +}); + +async function doSelectAndFocusTest({ + targetTab, + targetSelectionStart, + targetSelectionEnd, + anotherTab, +}) { + const testCases = [ + { + targetFocus: false, + anotherFocus: false, + }, + { + targetFocus: true, + anotherFocus: false, + }, + { + targetFocus: true, + anotherFocus: true, + }, + ]; + + for (const { targetFocus, anotherFocus } of testCases) { + // Setup the target tab. + await switchTab(targetTab); + setURLBarFocus(targetFocus); + gURLBar.inputField.setSelectionRange( + targetSelectionStart, + targetSelectionEnd + ); + const targetValue = gURLBar.value; + + // Switch to another tab. + await switchTab(anotherTab); + setURLBarFocus(anotherFocus); + + // Switch back to the target tab. + await switchTab(targetTab); + + // Check whether the value, selection and focusing status are reverted. + Assert.equal(gURLBar.value, targetValue); + Assert.equal(gURLBar.focused, targetFocus); + if (gURLBar.focused) { + Assert.equal(gURLBar.selectionStart, targetSelectionStart); + Assert.equal(gURLBar.selectionEnd, targetSelectionEnd); + } else { + Assert.equal(gURLBar.selectionStart, gURLBar.value.length); + Assert.equal(gURLBar.selectionEnd, gURLBar.value.length); + } + } +} + +function setURLBarFocus(focus) { + if (focus) { + gURLBar.focus(); + } else { + gURLBar.blur(); + } +} + +async function switchTab(tab) { + if (gBrowser.selectedTab !== tab) { + EventUtils.synthesizeMouseAtCenter(tab, {}); + await BrowserTestUtils.waitForCondition(() => gBrowser.selectedTab === tab); + } +} diff --git a/browser/components/urlbar/tests/browser/browser_keyword.js b/browser/components/urlbar/tests/browser/browser_keyword.js new file mode 100644 index 0000000000..04568cc1b5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keyword.js @@ -0,0 +1,234 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This tests that keywords are displayed and handled correctly. + */ + +async function promise_first_result(inputText) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: inputText, + }); + + return UrlbarTestUtils.getDetailsOfResultAt(window, 0); +} + +function assertURL(result, expectedUrl, keyword, input, postData) { + Assert.equal(result.url, expectedUrl, "Should have the correct URL"); + if (postData) { + Assert.equal( + NetUtil.readInputStreamToString( + result.postData, + result.postData.available() + ), + postData, + "Should have the correct postData" + ); + } +} + +const TEST_URL = `${TEST_BASE_URL}print_postdata.sjs`; + +add_setup(async function () { + await PlacesUtils.keywords.insert({ + keyword: "get", + url: TEST_URL + "?q=%s", + }); + await PlacesUtils.keywords.insert({ + keyword: "post", + url: TEST_URL, + postData: "q=%s", + }); + await PlacesUtils.keywords.insert({ + keyword: "question?", + url: TEST_URL + "?q2=%s", + }); + await PlacesUtils.keywords.insert({ + keyword: "?question", + url: TEST_URL + "?q3=%s", + }); + // Avoid fetching search suggestions. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", false]], + }); + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + setAsDefault: true, + }); + + registerCleanupFunction(async function () { + await PlacesUtils.keywords.remove("get"); + await PlacesUtils.keywords.remove("post"); + await PlacesUtils.keywords.remove("question?"); + await PlacesUtils.keywords.remove("?question"); + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + }); +}); + +add_task(async function test_display_keyword_without_query() { + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla"); + + // Test a keyword that also has blank spaces to ensure they are ignored as well. + let result = await promise_first_result("get "); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Should have a keyword result" + ); + Assert.equal( + result.displayed.title, + "example.com/browser/browser/components/urlbar/tests/browser/print_postdata.sjs?q=", + "Node should contain the url of the bookmark" + ); + let [action] = await document.l10n.formatValues([ + { id: "urlbar-result-action-visit" }, + ]); + Assert.equal(result.displayed.action, action, "Should have visit indicated"); +}); + +add_task(async function test_keyword_using_get() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + + let result = await promise_first_result("get something"); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Should have a keyword result" + ); + Assert.equal( + result.displayed.title, + "example.com: something", + "Node should contain the name of the bookmark and query" + ); + Assert.ok(!result.displayed.action, "Should have an empty action"); + + assertURL(result, TEST_URL + "?q=something", "get", "get something"); + + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + + // Click on the result + info("Normal click on result"); + let tabPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeMouseAtCenter(element, {}); + await tabPromise; + Assert.equal( + tab.linkedBrowser.currentURI.spec, + TEST_URL + "?q=something", + "Tab should have loaded from clicking on result" + ); + + // Middle-click on the result + info("Middle-click on result"); + result = await promise_first_result("get somethingmore"); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Should have a keyword result" + ); + + assertURL(result, TEST_URL + "?q=somethingmore", "get", "get somethingmore"); + + tabPromise = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen"); + element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + EventUtils.synthesizeMouseAtCenter(element, { button: 1 }); + let tabOpenEvent = await tabPromise; + let newTab = tabOpenEvent.target; + await BrowserTestUtils.browserLoaded(newTab.linkedBrowser); + Assert.equal( + newTab.linkedBrowser.currentURI.spec, + TEST_URL + "?q=somethingmore", + "Tab should have loaded from middle-clicking on result" + ); +}); + +add_task(async function test_keyword_using_post() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + + let result = await promise_first_result("post something"); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Should have a keyword result" + ); + Assert.equal( + result.displayed.title, + "example.com: something", + "Node should contain the name of the bookmark and query" + ); + Assert.ok(!result.displayed.action, "Should have an empty action"); + + assertURL(result, TEST_URL, "post", "post something", "q=something"); + + // Click on the result + info("Normal click on result"); + let tabPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + EventUtils.synthesizeMouseAtCenter(element, {}); + info("waiting for tab"); + await tabPromise; + Assert.equal( + tab.linkedBrowser.currentURI.spec, + TEST_URL, + "Tab should have loaded from clicking on result" + ); + + let postData = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async function () { + return content.document.body.textContent; + } + ); + Assert.equal(postData, "q=something", "post data was submitted correctly"); +}); + +add_task(async function test_keyword_with_question_mark() { + // TODO Bug 1517140: keywords containing restriction chars should not be + // allowed, or properly supported. + let result = await promise_first_result("question?"); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Result should be a keyword" + ); + Assert.equal(result.keyword, "question?", "Check search query"); + + result = await promise_first_result("question? something"); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Result should be a keyword" + ); + Assert.equal(result.keyword, "question?", "Check search query"); + + result = await promise_first_result("?question"); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Result should be a keyword" + ); + Assert.equal(result.keyword, "?question", "Check search query"); + + result = await promise_first_result("?question something"); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Result should be a keyword" + ); + Assert.equal(result.keyword, "?question", "Check search query"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_keywordBookmarklets.js b/browser/components/urlbar/tests/browser/browser_keywordBookmarklets.js new file mode 100644 index 0000000000..c10fcdd9c3 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keywordBookmarklets.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmarklet", + url: "javascript:'%sx'%20", + }); + await PlacesUtils.keywords.insert({ keyword: "bm", url: bm.url }); + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.remove(bm); + }); + + let testFns = [ + function () { + info("Type keyword and immediately press enter"); + gURLBar.value = "bm"; + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + return "x"; + }, + function () { + info("Type keyword with searchstring and immediately press enter"); + gURLBar.value = "bm a"; + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + return "ax"; + }, + async function () { + info("Search keyword, then press enter"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "bm", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.title, "javascript:'x' ", "Check title"); + EventUtils.synthesizeKey("KEY_Enter"); + return "x"; + }, + async function () { + info("Search keyword with searchstring, then press enter"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "bm a", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.title, "javascript:'ax' ", "Check title"); + EventUtils.synthesizeKey("KEY_Enter"); + return "ax"; + }, + async function () { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "bm", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.title, "javascript:'x' ", "Check title"); + let element = UrlbarTestUtils.getSelectedRow(window); + EventUtils.synthesizeMouseAtCenter(element, {}); + return "x"; + }, + async function () { + info("Search keyword with searchstring, then click"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "bm a", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.title, "javascript:'ax' ", "Check title"); + let element = UrlbarTestUtils.getSelectedRow(window); + EventUtils.synthesizeMouseAtCenter(element, {}); + return "ax"; + }, + ]; + for (let testFn of testFns) { + await do_test(testFn); + } +}); + +async function do_test(loadFn) { + await BrowserTestUtils.withNewTab( + { + gBrowser, + }, + async browser => { + let originalPrincipal = gBrowser.contentPrincipal; + let originalPrincipalURI = await getPrincipalURI(browser); + + let promise = BrowserTestUtils.waitForContentEvent(browser, "pageshow"); + const expectedTextContent = await loadFn(); + info("Awaiting pageshow event"); + await promise; + // URI should not change when we run a javascript: URL. + Assert.equal(gBrowser.currentURI.spec, "about:blank"); + const textContent = await ContentTask.spawn(browser, [], function () { + return content.document.documentElement.textContent; + }); + Assert.equal(textContent, expectedTextContent); + + let newPrincipalURI = await getPrincipalURI(browser); + Assert.equal( + newPrincipalURI, + originalPrincipalURI, + "content has the same principal" + ); + + // In e10s, null principals don't round-trip so the same null principal sent + // from the child will be a new null principal. Verify that this is the + // case. + if (browser.isRemoteBrowser) { + Assert.ok( + originalPrincipal.isNullPrincipal && + gBrowser.contentPrincipal.isNullPrincipal, + "both principals should be null principals in the parent" + ); + } else { + Assert.ok( + gBrowser.contentPrincipal.equals(originalPrincipal), + "javascript bookmarklet should inherit principal" + ); + } + } + ); +} + +function getPrincipalURI(browser) { + return SpecialPowers.spawn(browser, [], function () { + return content.document.nodePrincipal.spec; + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_keywordSearch.js b/browser/components/urlbar/tests/browser/browser_keywordSearch.js new file mode 100644 index 0000000000..b8402a4e90 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keywordSearch.js @@ -0,0 +1,57 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var gTests = [ + { + name: "normal search (search service)", + text: "test search", + expectText: "test+search", + }, + { + name: "?-prefixed search (search service)", + text: "? foo ", + expectText: "foo", + }, +]; + +add_setup(async function () { + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + setAsDefault: true, + }); +}); + +add_task(async function () { + // Test both directly setting a value and pressing enter, or setting the + // value through input events, like the user would do. + const setValueFns = [ + value => { + gURLBar.value = value; + }, + value => { + return UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + }); + }, + ]; + + for (let test of gTests) { + info("Testing: " + test.name); + await BrowserTestUtils.withNewTab({ gBrowser }, async browser => { + for (let setValueFn of setValueFns) { + gURLBar.select(); + await setValueFn(test.text); + EventUtils.synthesizeKey("KEY_Enter"); + + let expectedUrl = "http://mochi.test:8888/?terms=" + test.expectText; + info("Waiting for load: " + expectedUrl); + await BrowserTestUtils.browserLoaded(browser, false, expectedUrl); + // At least one test. + Assert.equal(browser.currentURI.spec, expectedUrl); + } + }); + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_keywordSearch_postData.js b/browser/components/urlbar/tests/browser/browser_keywordSearch_postData.js new file mode 100644 index 0000000000..d2b3aa253a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keywordSearch_postData.js @@ -0,0 +1,74 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var gTests = [ + { + name: "single word search (search service)", + text: "pizza", + expectText: "pizza", + }, + { + name: "multi word search (search service)", + text: "test search", + expectText: "test+search", + }, + { + name: "?-prefixed search (search service)", + text: "? foo ", + expectText: "foo", + }, +]; + +add_setup(async function () { + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "POSTSearchEngine.xml", + setAsDefault: true, + }); +}); + +add_task(async function () { + // Test both directly setting a value and pressing enter, or setting the + // value through input events, like the user would do. + const setValueFns = [ + value => { + gURLBar.value = value; + }, + value => { + return UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + }); + }, + ]; + + for (let test of gTests) { + info("Testing: " + test.name); + await BrowserTestUtils.withNewTab({ gBrowser }, async browser => { + for (let setValueFn of setValueFns) { + gURLBar.select(); + await setValueFn(test.text); + EventUtils.synthesizeKey("KEY_Enter"); + + await BrowserTestUtils.browserLoaded( + browser, + false, + "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/print_postdata.sjs" + ); + + let textContent = await SpecialPowers.spawn(browser, [], async () => { + return content.document.body.textContent; + }); + + Assert.ok(textContent, "search page loaded"); + let needle = "searchterms=" + test.expectText; + Assert.equal( + textContent, + needle, + "The query POST data should be returned in the response" + ); + } + }); + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_keyword_override.js b/browser/components/urlbar/tests/browser/browser_keyword_override.js new file mode 100644 index 0000000000..b358f3a4ac --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keyword_override.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This tests that the display of keyword results are not changed when the user + * presses the override button. + */ + +add_task(async function () { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/?q=%s", + title: "test", + }); + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "http://example.com/?q=%s", + }); + + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.remove(bm); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "keyword search", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + info("Before override"); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Should have a keyword result" + ); + Assert.equal( + result.displayed.title, + "example.com: search", + "Node should contain the name of the bookmark and query" + ); + Assert.ok(!result.displayed.action, "Should have an empty action"); + + info("During override"); + EventUtils.synthesizeKey("VK_SHIFT", { type: "keydown" }); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Should have a keyword result" + ); + Assert.equal( + result.displayed.title, + "example.com: search", + "Node should contain the name of the bookmark and query" + ); + Assert.ok(!result.displayed.action, "Should have an empty action"); + + EventUtils.synthesizeKey("VK_SHIFT", { type: "keyup" }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_keyword_select_and_type.js b/browser/components/urlbar/tests/browser/browser_keyword_select_and_type.js new file mode 100644 index 0000000000..a3222c293f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keyword_select_and_type.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This tests that changing away from a keyword result and back again, still + * operates correctly. + */ + +add_task(async function () { + let bookmarks = []; + bookmarks.push( + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/?q=%s", + title: "test", + }) + ); + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "http://example.com/?q=%s", + }); + + // This item is only needed so we can select the keyword item, select something + // else, then select the keyword item again. + bookmarks.push( + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/keyword", + title: "keyword abc", + }) + ); + + registerCleanupFunction(async function () { + for (let bm of bookmarks) { + await PlacesUtils.bookmarks.remove(bm); + } + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "keyword a", + }); + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + + // First item should already be selected + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "Should have the first item selected" + ); + + // Select next one (important!) + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "Should have the second item selected" + ); + + // Re-select keyword item + EventUtils.synthesizeKey("KEY_ArrowUp"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "Should have the first item selected" + ); + + EventUtils.sendString("b"); + await UrlbarTestUtils.promiseSearchComplete(window); + + Assert.equal( + gURLBar.value, + "keyword ab", + "urlbar should have expected input" + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Should have a result of type keyword" + ); + Assert.equal( + result.url, + "http://example.com/?q=ab", + "Should have the correct url" + ); + + gBrowser.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_loadRace.js b/browser/components/urlbar/tests/browser/browser_loadRace.js new file mode 100644 index 0000000000..cd00646cbd --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_loadRace.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This test is for testing races of loading the Urlbar when loading shortcuts. +// For example, ensuring that if a search query is entered, but something causes +// a page load whilst we're getting the search url, then we don't handle the +// original search query. + +add_setup(async function () { + sandbox = sinon.createSandbox(); + + registerCleanupFunction(async () => { + sandbox.restore(); + }); +}); + +async function checkShortcutLoading(modifierKeys) { + let deferred = Promise.withResolvers(); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:robots", + }); + + // We stub getHeuristicResultFor to guarentee it doesn't resolve until after + // we've loaded a new page. + let original = UrlbarUtils.getHeuristicResultFor; + sandbox + .stub(UrlbarUtils, "getHeuristicResultFor") + .callsFake(async searchString => { + await deferred.promise; + return original.call(this, searchString); + }); + + // This load will be blocked until the deferred is resolved. + // Use a string that will be interepreted as a local URL to avoid hitting the + // network. + gURLBar.focus(); + gURLBar.value = "example.com"; + gURLBar.userTypedValue = true; + EventUtils.synthesizeKey("KEY_Enter", modifierKeys); + + Assert.ok( + UrlbarUtils.getHeuristicResultFor.calledOnce, + "should have called getHeuristicResultFor" + ); + + // Now load a different page. + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, "about:license"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + Assert.equal(gBrowser.visibleTabs.length, 2, "Should have 2 tabs"); + + // Now that the new page has loaded, unblock the previous urlbar load. + deferred.resolve(); + if (modifierKeys) { + let openedTab = await new Promise(resolve => { + window.addEventListener( + "TabOpen", + event => { + resolve(event.target); + }, + { once: true } + ); + }); + await BrowserTestUtils.browserLoaded(openedTab.linkedBrowser); + Assert.ok( + openedTab.linkedBrowser.currentURI.spec.includes("example.com"), + "Should have attempted to open the shortcut page" + ); + BrowserTestUtils.removeTab(openedTab); + } + + Assert.equal( + tab.linkedBrowser.currentURI.spec, + "about:license", + "Tab url should not have changed" + ); + Assert.equal(gBrowser.visibleTabs.length, 2, "Should still have 2 tabs"); + + BrowserTestUtils.removeTab(tab); + sandbox.restore(); +} + +add_task(async function test_location_change_stops_load() { + await checkShortcutLoading(); +}); + +add_task(async function test_opening_different_tab_with_location_change() { + await checkShortcutLoading({ altKey: true }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_locationBarCommand.js b/browser/components/urlbar/tests/browser/browser_locationBarCommand.js new file mode 100644 index 0000000000..670b9741f4 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_locationBarCommand.js @@ -0,0 +1,352 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test is designed to ensure that the correct command/operation happens + * when pressing Enter, or clicking the Go button, with various key + * combinations in the urlbar. + */ + +const TEST_VALUE = "http://example.com"; +const START_VALUE = "example.org"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.altClickSave", true], + ["browser.urlbar.autoFill", false], + ], + }); +}); + +add_task(async function alt_left_click_test() { + info("Running test: Alt left click"); + + // Monkey patch saveURL() to avoid dealing with file save code paths. + let oldSaveURL = saveURL; + let saveURLPromise = new Promise(resolve => { + saveURL = () => { + // Restore old saveURL() value. + saveURL = oldSaveURL; + resolve(); + }; + }); + + await typeAndCommand("click", { altKey: true }); + + await saveURLPromise; + ok(true, "SaveURL was called"); + is(gURLBar.value, "", "Urlbar reverted to original value"); +}); + +add_task(async function shift_left_click_test() { + info("Running test: Shift left click"); + + let destinationURL = TEST_VALUE + "/"; + let newWindowPromise = BrowserTestUtils.waitForNewWindow({ + url: destinationURL, + }); + await typeAndCommand("click", { shiftKey: true }); + let win = await newWindowPromise; + + info("URL should be loaded in a new window"); + is(gURLBar.value, "", "Urlbar reverted to original value"); + await promiseCheckChildNoFocusedElement(gBrowser.selectedBrowser); + is( + document.activeElement, + gBrowser.selectedBrowser, + "Content window should be focused" + ); + is( + win.gURLBar.value, + UrlbarTestUtils.trimURL(TEST_VALUE), + "New URL is loaded in new window" + ); + + // Cleanup. + let ourWindowRefocusedPromise = Promise.all([ + BrowserTestUtils.waitForEvent(window, "activate"), + BrowserTestUtils.waitForEvent(window, "focus", true), + ]); + await BrowserTestUtils.closeWindow(win); + await ourWindowRefocusedPromise; +}); + +add_task(async function right_click_test() { + info("Running test: Right click on go button"); + + // Add a new tab. + await promiseOpenNewTab(); + + await typeAndCommand("click", { button: 2 }); + + // Right click should do nothing (context menu will be shown). + is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered"); + + // Cleanup. + gBrowser.removeCurrentTab(); +}); + +add_task(async function shift_accel_left_click_test() { + info("Running test: Shift+Ctrl/Cmd left click on go button"); + + // Add a new tab. + let tab = await promiseOpenNewTab(); + + let loadStartedPromise = promiseLoadStarted(); + await typeAndCommand("click", { accelKey: true, shiftKey: true }); + await loadStartedPromise; + + // Check the load occurred in a new background tab. + info("URL should be loaded in a new background tab"); + is(gURLBar.value, "", "Urlbar reverted to original value"); + ok(!gURLBar.focused, "Urlbar is no longer focused after urlbar command"); + is(gBrowser.selectedTab, tab, "Focus did not change to the new tab"); + + // Select the new background tab + gBrowser.selectedTab = gBrowser.selectedTab.nextElementSibling; + is( + gURLBar.value, + UrlbarTestUtils.trimURL(TEST_VALUE), + "New URL is loaded in new tab" + ); + + // Cleanup. + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); +}); + +add_task(async function load_in_current_tab_test() { + let tests = [ + { + desc: "Simple return keypress", + type: "keypress", + }, + { + desc: "Left click on go button", + type: "click", + }, + { + desc: "Ctrl/Cmd+Return keypress", + type: "keypress", + details: { accelKey: true }, + }, + { + desc: "Alt+Return keypress in a blank tab", + type: "keypress", + details: { altKey: true }, + }, + { + desc: "AltGr+Return keypress in a blank tab", + type: "keypress", + details: { altGraphKey: true }, + }, + ]; + + for (let { desc, type, details } of tests) { + info(`Running test: ${desc}`); + + // Add a new tab. + let tab = await promiseOpenNewTab(); + + // Trigger a load and check it occurs in the current tab. + let loadStartedPromise = promiseLoadStarted(); + await typeAndCommand(type, details); + await loadStartedPromise; + + info("URL should be loaded in the current tab"); + is( + gURLBar.value, + UrlbarTestUtils.trimURL(TEST_VALUE), + "Urlbar still has the value we entered" + ); + await promiseCheckChildNoFocusedElement(gBrowser.selectedBrowser); + is( + document.activeElement, + gBrowser.selectedBrowser, + "Content window should be focused" + ); + is(gBrowser.selectedTab, tab, "New URL was loaded in the current tab"); + + // Cleanup. + gBrowser.removeCurrentTab(); + } +}); + +add_task(async function load_in_new_tab_test() { + let tests = [ + { + desc: "Ctrl/Cmd left click on go button", + type: "click", + details: { accelKey: true }, + url: "about:blank", + }, + { + desc: "Alt+Return keypress in a dirty tab", + type: "keypress", + details: { altKey: true }, + url: START_VALUE, + }, + { + desc: "AltGr+Return keypress in a dirty tab", + type: "keypress", + details: { altGraphKey: true }, + url: START_VALUE, + }, + ]; + + for (let { desc, type, details, url } of tests) { + info(`Running test: ${desc}`); + + // Add a new tab. + let tab = await promiseOpenNewTab(url); + + // Trigger a load and check it occurs in a new tab. + let tabSwitchedPromise = promiseNewTabSwitched(); + await typeAndCommand(type, details); + await tabSwitchedPromise; + + // Check the load occurred in a new tab. + info("URL should be loaded in a new focused tab"); + is( + gURLBar.value, + UrlbarTestUtils.trimURL(TEST_VALUE), + "Urlbar still has the value we entered" + ); + await promiseCheckChildNoFocusedElement(gBrowser.selectedBrowser); + is( + document.activeElement, + gBrowser.selectedBrowser, + "Content window should be focused" + ); + isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab"); + + // Cleanup. + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); + } +}); + +add_task(async function go_button_after_tab_switch() { + // Add a new tab. + let tab = await promiseOpenNewTab(); + + await UrlbarTestUtils.inputIntoURLBar(window, TEST_VALUE); + await BrowserTestUtils.switchTab(gBrowser, gBrowser.visibleTabs[0]); + isnot( + gURLBar.value, + UrlbarTestUtils.trimURL(TEST_VALUE), + "Urlbar does not have the entered value after switching to a different tab" + ); + await BrowserTestUtils.switchTab(gBrowser, tab); + is( + gURLBar.value, + UrlbarTestUtils.trimURL(TEST_VALUE), + "Urlbar still has the entered value restored after switching back to the new tab" + ); + + // Trigger a load and check it occurs in the current tab. + let loadStartedPromise = promiseLoadStarted(); + await triggerCommand("click"); + await loadStartedPromise; + + info("URL should be loaded in the current tab"); + is( + gURLBar.value, + UrlbarTestUtils.trimURL(TEST_VALUE), + "Urlbar still has the value we entered" + ); + await promiseCheckChildNoFocusedElement(gBrowser.selectedBrowser); + is( + document.activeElement, + gBrowser.selectedBrowser, + "Content window should be focused" + ); + is(gBrowser.selectedTab, tab, "New URL was loaded in the current tab"); + + // Cleanup. + gBrowser.removeCurrentTab(); +}); + +async function typeAndCommand(eventType, details = {}) { + await UrlbarTestUtils.inputIntoURLBar(window, TEST_VALUE); + await triggerCommand(eventType, details); +} + +async function triggerCommand(eventType, details = {}) { + Assert.equal( + await UrlbarTestUtils.promiseUserContextId(window), + gBrowser.selectedTab.getAttribute("usercontextid"), + "userContextId must be the same as the originating tab" + ); + + switch (eventType) { + case "click": + ok( + gURLBar.hasAttribute("usertyping"), + "usertyping attribute must be set for the go button to be visible" + ); + EventUtils.synthesizeMouseAtCenter(gURLBar.goButton, details); + break; + case "keypress": + EventUtils.synthesizeKey("KEY_Enter", details); + break; + default: + throw new Error("Unsupported event type"); + } +} + +function promiseLoadStarted() { + return new Promise(resolve => { + gBrowser.addTabsProgressListener({ + onStateChange(browser, webProgress, req, flags, status) { + if (flags & Ci.nsIWebProgressListener.STATE_START) { + gBrowser.removeTabsProgressListener(this); + resolve(); + } + }, + }); + }); +} + +let gUserContextIdSerial = 1; +async function promiseOpenNewTab(url = "about:blank") { + let tab = BrowserTestUtils.addTab(gBrowser, url, { + userContextId: gUserContextIdSerial++, + }); + let tabSwitchPromise = BrowserTestUtils.switchTab(gBrowser, tab); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await tabSwitchPromise; + return tab; +} + +function promiseNewTabSwitched() { + return new Promise(resolve => { + gBrowser.addEventListener( + "TabSwitchDone", + function () { + executeSoon(resolve); + }, + { once: true } + ); + }); +} + +function promiseCheckChildNoFocusedElement(browser) { + if (!gMultiProcessBrowser) { + Assert.equal( + Services.focus.focusedElement, + null, + "There should be no focused element" + ); + return null; + } + + return ContentTask.spawn(browser, null, async function () { + Assert.equal( + Services.focus.focusedElement, + null, + "There should be no focused element" + ); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_locationBarExternalLoad.js b/browser/components/urlbar/tests/browser/browser_locationBarExternalLoad.js new file mode 100644 index 0000000000..5a44db54ce --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_locationBarExternalLoad.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", false]], + }); + const url = "data:text/html,<body>hi"; + await testURL(url, urlEnter); + await testURL(url, urlClick); +}); + +function urlEnter(url) { + gURLBar.value = url; + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); +} + +function urlClick(url) { + gURLBar.focus(); + gURLBar.value = ""; + EventUtils.sendString(url); + EventUtils.synthesizeMouseAtCenter(gURLBar.goButton, {}); +} + +function promiseNewTabSwitched() { + return new Promise(resolve => { + gBrowser.addEventListener( + "TabSwitchDone", + function () { + executeSoon(resolve); + }, + { once: true } + ); + }); +} + +function promiseLoaded(browser) { + return SpecialPowers.spawn(browser, [undefined], async () => { + if (!["interactive", "complete"].includes(content.document.readyState)) { + await new Promise(resolve => + docShell.chromeEventHandler.addEventListener( + "DOMContentLoaded", + resolve, + { + once: true, + capture: true, + } + ) + ); + } + }); +} + +async function testURL(url, loadFunc, endFunc) { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + let browser = tab.linkedBrowser; + + let pagePrincipal = gBrowser.contentPrincipal; + // We need to ensure that we set the pageshow event listener before running + // loadFunc, otherwise there's a chance that the content process will finish + // loading the page and fire pageshow before the event listener gets set. + let pageShowPromise = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow" + ); + loadFunc(url); + await pageShowPromise; + + await SpecialPowers.spawn( + browser, + [{ isRemote: gMultiProcessBrowser }], + async function (arg) { + Assert.equal( + Services.focus.focusedElement, + null, + "focusedElement not null" + ); + } + ); + + is(document.activeElement, browser, "content window should be focused"); + + ok( + !gBrowser.contentPrincipal.equals(pagePrincipal), + "load of " + + url + + " by " + + loadFunc.name + + " should produce a page with a different principal" + ); + + await BrowserTestUtils.removeTab(tab); +} diff --git a/browser/components/urlbar/tests/browser/browser_locationchange_urlbar_edit_dos.js b/browser/components/urlbar/tests/browser/browser_locationchange_urlbar_edit_dos.js new file mode 100644 index 0000000000..b50446a4c9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_locationchange_urlbar_edit_dos.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = `${TEST_BASE_URL}file_urlbar_edit_dos.html`; + +async function checkURLBarValueStays(browser) { + gURLBar.select(); + EventUtils.sendString("a"); + is(gURLBar.value, "a", "URL bar value should match after sending a key"); + await new Promise(resolve => { + let listener = { + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + ok( + aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT, + "Should only get a same document location change" + ); + gBrowser.selectedBrowser.removeProgressListener(filter); + filter = null; + // Wait an extra tick before resolving. We want to make sure that other + // web progress listeners queued after this one are called before we + // continue the test, in case the remainder of the test depends on those + // listeners. That should happen anyway since promises are resolved on + // the next tick, but do this to be a little safer. In particular we + // want to avoid having the test pass when it should fail. + executeSoon(resolve); + }, + }; + let filter = Cc[ + "@mozilla.org/appshell/component/browser-status-filter;1" + ].createInstance(Ci.nsIWebProgress); + filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL); + gBrowser.selectedBrowser.addProgressListener(filter); + }); + is( + gURLBar.value, + "a", + "URL bar should not have been changed by location changes." + ); +} + +add_task(async function () { + // Disable autofill so that when checkURLBarValueStays types "a", it's not + // autofilled to addons.mozilla.org (or anything else). + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", false]], + }); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_URL, + }, + async function (browser) { + let promise1 = checkURLBarValueStays(browser); + SpecialPowers.spawn(browser, [""], function () { + content.wrappedJSObject.dos_hash(); + }); + await promise1; + let promise2 = checkURLBarValueStays(browser); + SpecialPowers.spawn(browser, [""], function () { + content.wrappedJSObject.dos_pushState(); + }); + await promise2; + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_middleClick.js b/browser/components/urlbar/tests/browser/browser_middleClick.js new file mode 100644 index 0000000000..b2d567cff4 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_middleClick.js @@ -0,0 +1,279 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test for middle click behavior. + */ + +add_setup(async () => { + CustomizableUI.addWidgetToArea("home-button", "nav-bar"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.tabs.searchclipboardfor.middleclick", false]], + }); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("middlemouse.paste"); + Services.prefs.clearUserPref("middlemouse.openNewWindow"); + Services.prefs.clearUserPref("browser.tabs.opentabfor.middleclick"); + Services.prefs.clearUserPref("browser.startup.homepage"); + Services.prefs.clearUserPref("browser.tabs.loadBookmarksInBackground"); + SpecialPowers.clipboardCopyString(""); + + CustomizableUI.removeWidgetFromArea("home-button"); + }); +}); + +add_task(async function test_middleClickOnTab() { + await testMiddleClickOnTab(false); + await testMiddleClickOnTab(true); +}); + +add_task(async function test_middleClickToOpenNewTab() { + await testMiddleClickToOpenNewTab(false, "#tabs-newtab-button"); + await testMiddleClickToOpenNewTab(true, "#tabs-newtab-button"); + await testMiddleClickToOpenNewTab(false, "#TabsToolbar"); + await testMiddleClickToOpenNewTab(true, "#TabsToolbar"); +}); + +add_task(async function test_middleClickOnURLBar() { + await testMiddleClickOnURLBar(false); + await testMiddleClickOnURLBar(true); +}); + +add_task(async function test_middleClickOnHomeButton() { + const TEST_DATA = [ + { + isMiddleMousePastePrefOn: false, + isLoadInBackground: false, + startPagePref: "about:home", + expectedURLBarFocus: true, + expectedURLBarValue: "", + }, + { + isMiddleMousePastePrefOn: false, + isLoadInBackground: false, + startPagePref: "about:blank", + expectedURLBarFocus: true, + expectedURLBarValue: "", + }, + { + isMiddleMousePastePrefOn: false, + isLoadInBackground: false, + startPagePref: "https://example.com", + expectedURLBarFocus: false, + expectedURLBarValue: UrlbarTestUtils.trimURL("https://example.com"), + }, + { + isMiddleMousePastePrefOn: true, + isLoadInBackground: false, + startPagePref: "about:home", + expectedURLBarFocus: true, + expectedURLBarValue: "", + }, + { + isMiddleMousePastePrefOn: true, + isLoadInBackground: false, + startPagePref: "https://example.com", + expectedURLBarFocus: false, + expectedURLBarValue: UrlbarTestUtils.trimURL("https://example.com"), + }, + { + isMiddleMousePastePrefOn: false, + isLoadInBackground: true, + startPagePref: "about:home", + expectedURLBarFocus: true, + expectedURLBarValue: "", + }, + { + isMiddleMousePastePrefOn: false, + isLoadInBackground: true, + startPagePref: "https://example.com", + expectedURLBarFocus: true, + expectedURLBarValue: "", + }, + { + isMiddleMousePastePrefOn: true, + isLoadInBackground: true, + startPagePref: "about:home", + expectedURLBarFocus: true, + expectedURLBarValue: "", + }, + { + isMiddleMousePastePrefOn: true, + isLoadInBackground: true, + startPagePref: "https://example.com", + expectedURLBarFocus: true, + expectedURLBarValue: "", + }, + ]; + + for (const testData of TEST_DATA) { + await testMiddleClickOnHomeButton(testData); + } +}); + +add_task(async function test_middleClickOnHomeButtonWithNewWindow() { + await testMiddleClickOnHomeButtonWithNewWindow(false); + await testMiddleClickOnHomeButtonWithNewWindow(true); +}); + +add_task(async function test_middleClickOnComponentNotHandlingPasteEvent() { + Services.prefs.setBoolPref("middlemouse.paste", true); + + info("Set initial value"); + SpecialPowers.clipboardCopyString("test\nsample"); + gURLBar.value = ""; + gURLBar.focus(); + + info("Middle click on a component that does not handle paste event"); + const allTabsButton = document.getElementById("alltabs-button"); + const onMiddleClick = new Promise(r => + allTabsButton.addEventListener("auxclick", r, { once: true }) + ); + let pastedOnURLBar = false; + gURLBar.addEventListener("paste", () => { + pastedOnURLBar = true; + }); + EventUtils.synthesizeMouseAtCenter(allTabsButton, { button: 1 }); + await onMiddleClick; + + Assert.equal(gURLBar.value, "", "URLBar has no pasted value"); + Assert.ok(!pastedOnURLBar, "URLBar should not receive paste event"); +}); + +async function testMiddleClickOnTab(isMiddleMousePastePrefOn) { + info(`Set middlemouse.paste [${isMiddleMousePastePrefOn}]`); + Services.prefs.setBoolPref("middlemouse.paste", isMiddleMousePastePrefOn); + + info("Set initial value"); + SpecialPowers.clipboardCopyString("test\nsample"); + gURLBar.value = ""; + gURLBar.focus(); + + info("Open two tabs"); + const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + info("Middle click on tab2 to remove it"); + EventUtils.synthesizeMouseAtCenter(tab2, { button: 1 }); + + info("Wait until the tab1 is selected"); + await TestUtils.waitForCondition(() => gBrowser.selectedTab === tab1); + + Assert.equal(gURLBar.value, "", "URLBar has no pasted value"); + + BrowserTestUtils.removeTab(tab1); +} + +async function testMiddleClickToOpenNewTab(isMiddleMousePastePrefOn, selector) { + info(`Set middlemouse.paste [${isMiddleMousePastePrefOn}]`); + Services.prefs.setBoolPref("middlemouse.paste", isMiddleMousePastePrefOn); + + info("Set initial value"); + SpecialPowers.clipboardCopyString("test\nsample"); + gURLBar.value = ""; + gURLBar.focus(); + + info(`Click on ${selector}`); + const originalTab = gBrowser.selectedTab; + const element = document.querySelector(selector); + EventUtils.synthesizeMouseAtCenter(element, { button: 1 }); + + info("Wait until the new tab is opened"); + await TestUtils.waitForCondition(() => gBrowser.selectedTab !== originalTab); + + Assert.equal(gURLBar.value, "", "URLBar has no pasted value"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +} + +async function testMiddleClickOnURLBar(isMiddleMousePastePrefOn) { + info(`Set middlemouse.paste [${isMiddleMousePastePrefOn}]`); + Services.prefs.setBoolPref("middlemouse.paste", isMiddleMousePastePrefOn); + + info("Set initial value"); + SpecialPowers.clipboardCopyString("test\nsample"); + gURLBar.value = ""; + gURLBar.focus(); + + info("Middle click on the urlbar"); + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, { button: 1 }); + + if (isMiddleMousePastePrefOn) { + Assert.equal(gURLBar.value, "test sample", "URLBar has pasted value"); + } else { + Assert.equal(gURLBar.value, "", "URLBar has no pasted value"); + } +} + +async function testMiddleClickOnHomeButton({ + isMiddleMousePastePrefOn, + isLoadInBackground, + startPagePref, + expectedURLBarFocus, + expectedURLBarValue, +}) { + info(`middlemouse.paste [${isMiddleMousePastePrefOn}]`); + info(`browser.startup.homepage [${startPagePref}]`); + info(`browser.tabs.loadBookmarksInBackground [${isLoadInBackground}]`); + + info("Set initial value"); + Services.prefs.setCharPref("browser.startup.homepage", startPagePref); + Services.prefs.setBoolPref( + "browser.tabs.loadBookmarksInBackground", + isLoadInBackground + ); + Services.prefs.setBoolPref("middlemouse.paste", isMiddleMousePastePrefOn); + SpecialPowers.clipboardCopyString("test\nsample"); + gURLBar.value = ""; + gURLBar.focus(); + + info("Middle click on the home button"); + const currentTab = gBrowser.selectedTab; + const homeButton = document.getElementById("home-button"); + EventUtils.synthesizeMouseAtCenter(homeButton, { button: 1 }); + + if (!isLoadInBackground) { + info("Wait until the a new tab is selected"); + await TestUtils.waitForCondition(() => gBrowser.selectedTab !== currentTab); + } + + info("Wait until the focus moves"); + await TestUtils.waitForCondition( + () => + (document.activeElement === gURLBar.inputField) === expectedURLBarFocus + ); + + Assert.ok(true, "The focus is correct"); + Assert.equal(gURLBar.value, expectedURLBarValue, "URLBar value is correct"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +} + +async function testMiddleClickOnHomeButtonWithNewWindow( + isMiddleMousePastePrefOn +) { + info(`Set middlemouse.paste [${isMiddleMousePastePrefOn}]`); + Services.prefs.setBoolPref("middlemouse.paste", isMiddleMousePastePrefOn); + + info("Set prefs to open in a new window"); + Services.prefs.setBoolPref("middlemouse.openNewWindow", true); + Services.prefs.setBoolPref("browser.tabs.opentabfor.middleclick", false); + + info("Set initial value"); + SpecialPowers.clipboardCopyString("test\nsample"); + gURLBar.value = ""; + gURLBar.focus(); + + info("Middle click on the home button"); + const homeButton = document.getElementById("home-button"); + const onNewWindowOpened = BrowserTestUtils.waitForNewWindow(); + EventUtils.synthesizeMouseAtCenter(homeButton, { button: 1 }); + + const newWindow = await onNewWindowOpened; + Assert.equal(newWindow.gURLBar.value, "", "URLBar value is correct"); + + await BrowserTestUtils.closeWindow(newWindow); +} diff --git a/browser/components/urlbar/tests/browser/browser_move_tab_to_new_window.js b/browser/components/urlbar/tests/browser/browser_move_tab_to_new_window.js new file mode 100644 index 0000000000..3dfaedec81 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_move_tab_to_new_window.js @@ -0,0 +1,120 @@ +/* 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/. */ + +/* + These tests ensure that if the urlbar has a user typed value and the user + moves the tab into a new window, the user typed value moves with it. +*/ + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits(["https://example.com/"]); + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +async function moveTabIntoNewWindowAndBack(url = "about:blank") { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + + info("Replace urlbar value with a user typed value."); + gURLBar.value = "hello world"; + UrlbarTestUtils.fireInputEvent(window); + Assert.equal( + gBrowser.userTypedValue, + "hello world", + "The user typed value should be replaced with hello world." + ); + + info("Move the tab into its own window."); + let newWindow = gBrowser.replaceTabWithWindow(tab); + let swapDocShellPromise = BrowserTestUtils.waitForEvent( + tab.linkedBrowser, + "SwapDocShells" + ); + await swapDocShellPromise; + Assert.equal( + newWindow.gURLBar.value, + "hello world", + "The value of the urlbar should have been moved." + ); + + info("Return that tab back to its original window and select it."); + tab = newWindow.gBrowser.selectedTab; + swapDocShellPromise = BrowserTestUtils.waitForEvent( + tab.linkedBrowser, + "SwapDocShells" + ); + gBrowser.adoptTab(newWindow.gBrowser.selectedTab, 1, true); + await swapDocShellPromise; + Assert.equal( + gURLBar.value, + "hello world", + "The value of the urlbar should have been moved." + ); + + // Clean up. + await BrowserTestUtils.removeTab(gBrowser.selectedTab); +} + +add_task(async function move_newtab_with_value() { + info("Open a new tab."); + await moveTabIntoNewWindowAndBack(); +}); + +add_task(async function move_loaded_page_with_value() { + info("Open a new tab and load a URL."); + await moveTabIntoNewWindowAndBack("https://www.example.com/"); +}); + +add_task(async function move_tab_into_new_window_and_open_new_tab() { + info("Open a new tab."); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + info("Move the new tab into a new window."); + let swapDocShellPromise = BrowserTestUtils.waitForEvent( + tab.linkedBrowser, + "SwapDocShells" + ); + let newWindow = gBrowser.replaceTabWithWindow(tab); + await swapDocShellPromise; + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + info("Type in the urlbar to open it and see an autofill suggestion."); + await UrlbarTestUtils.promisePopupOpen(newWindow, async () => { + newWindow.gURLBar.focus(); + EventUtils.synthesizeKey("ex", {}, newWindow); + }); + + let details = await UrlbarTestUtils.getDetailsOfResultAt(newWindow, 0); + Assert.equal(details.autofill, true, "Heuristic result should be Autofill."); + Assert.equal( + details.result.autofill.value, + "example.com/", + "Autofill value is as expected." + ); + + info("Open an about:newtab page while address bar is focused."); + let tab2 = await BrowserTestUtils.openNewForegroundTab( + newWindow.gBrowser, + "about:newtab", + false + ); + + // To be certain autoOpen isn't triggered, wait a brief amount of time + // following the tab switch event. + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + await new Promise(resolve => setTimeout(resolve, 100)); + + Assert.equal(newWindow.gURLBar.value, "", "Urlbar should be empty."); + Assert.equal( + newWindow.gURLBar.view.isOpen, + false, + "Urlbar view should be closed." + ); + + await BrowserTestUtils.removeTab(tab2); + await BrowserTestUtils.closeWindow(newWindow); +}); diff --git a/browser/components/urlbar/tests/browser/browser_new_tab_urlbar_reset.js b/browser/components/urlbar/tests/browser/browser_new_tab_urlbar_reset.js new file mode 100644 index 0000000000..b2bce4b22e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_new_tab_urlbar_reset.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify that urlbar state is reset when opening a new tab, so searching for the + * same text will reopen the results popup. + */ +add_task(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank", + false + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "m", + }); + assertOpen(); + + let tab2 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank", + false + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "m", + }); + assertOpen(); + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); + +function assertOpen() { + Assert.equal(gURLBar.view.isOpen, true, "Should be showing the popup"); +} diff --git a/browser/components/urlbar/tests/browser/browser_observers_for_strip_on_share.js b/browser/components/urlbar/tests/browser/browser_observers_for_strip_on_share.js new file mode 100644 index 0000000000..02b404926b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_observers_for_strip_on_share.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let listService; + +const QPS_PREF = "privacy.query_stripping.enabled"; +const STRIP_ON_SHARE_PREF = "privacy.query_stripping.strip_on_share.enabled"; + +// Tests for the observers for both QPS and Strip on Share +add_setup(async function () { + // Get the list service so we can wait for it to be fully initialized before running tests. + listService = Cc["@mozilla.org/query-stripping-list-service;1"].getService( + Ci.nsIURLQueryStrippingListService + ); + + await listService.testWaitForInit(); +}); + +// Test if Strip on share observers are registered/unregistered depending if the +// Strip on Share Pref is enabled/disabled regardless of the state of QPS Pref +add_task( + async function checkStripOnShareObserversForVaryingStatesOfQPSAndStripOnShare() { + for (let queryStrippingEnabled of [false, true]) { + for (let stripOnShareEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [ + [QPS_PREF, queryStrippingEnabled], + [STRIP_ON_SHARE_PREF, stripOnShareEnabled], + ], + }); + + let areObserservesRegistered; + await BrowserTestUtils.waitForCondition(function () { + areObserservesRegistered = listService.testHasStripOnShareObservers(); + return areObserservesRegistered == stripOnShareEnabled; + }, "waiting for init of URLQueryStrippingListService ensuring observers have time to register if they need"); + + if (!stripOnShareEnabled) { + Assert.ok(!areObserservesRegistered, "Observers are unregistered"); + } else { + Assert.ok(areObserservesRegistered, "Observers are registered"); + } + + await SpecialPowers.popPrefEnv(); + } + } + } +); + +// Test if QPS observers are registered/unregistered depending if the QPS +// Pref is enabled/disabled regardless of the state of Strip on Share Pref +add_task( + async function checkQPSObserversForVaryingStatesOfQPSAndStripOnShare() { + for (let queryStrippingEnabled of [false, true]) { + for (let stripOnShareEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [ + [QPS_PREF, queryStrippingEnabled], + [STRIP_ON_SHARE_PREF, stripOnShareEnabled], + ], + }); + + let areObserservesRegistered; + await BrowserTestUtils.waitForCondition(function () { + areObserservesRegistered = listService.testHasQPSObservers(); + return areObserservesRegistered == queryStrippingEnabled; + }, "waiting for init of URLQueryStrippingListService ensuring observers have time to register if they need"); + + if (!queryStrippingEnabled) { + Assert.ok(!areObserservesRegistered, "Observers are unregistered"); + } else { + Assert.ok(areObserservesRegistered, "Observers are registered"); + } + + await SpecialPowers.popPrefEnv(); + } + } + } +); diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs.js b/browser/components/urlbar/tests/browser/browser_oneOffs.js new file mode 100644 index 0000000000..0c04f1e321 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs.js @@ -0,0 +1,999 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the one-off search buttons in the urlbar. + */ + +"use strict"; + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +let gMaxResults; +let engine; + +ChromeUtils.defineLazyGetter(this, "oneOffSearchButtons", () => { + return UrlbarTestUtils.getOneOffSearchButtons(window); +}); + +add_setup(async function () { + gMaxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults"); + + // Add a search suggestion engine and move it to the front so that it appears + // as the first one-off. + engine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + }); + await Services.search.moveEngine(engine, 0); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", false], + ["browser.urlbar.suggest.quickactions", false], + ["browser.urlbar.shortcuts.quickactions", true], + ], + }); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); + + // Initialize history with enough visits to fill up the view. + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + for (let i = 0; i < gMaxResults; i++) { + await PlacesTestUtils.addVisits( + "http://example.com/browser_urlbarOneOffs.js/?" + i + ); + } + + // Add some more visits to the last URL added above so that the top-sites view + // will be non-empty. + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits( + "http://example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - 1) + ); + } + await updateTopSites(sites => { + return sites && sites[0] && sites[0].url.startsWith("http://example.com/"); + }); + + // Move the mouse away from the view so that a result or one-off isn't + // inadvertently highlighted. See bug 1659011. + EventUtils.synthesizeMouse( + gURLBar.inputField, + 0, + 0, + { type: "mousemove" }, + window + ); +}); + +// Opens the view without showing the one-offs. They should be hidden and arrow +// key selection should work properly. +add_task(async function noOneOffs() { + // Do a search for "@" since we hide the one-offs in that case. + let value = "@"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + fireInputEvent: true, + }); + await TestUtils.waitForCondition( + () => !oneOffSearchButtons._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + false, + "One-offs should be hidden" + ); + assertState(-1, -1, value); + + // Get the result count. We don't particularly care what the results are, + // just what the count is so that we can key through them all. + let resultCount = UrlbarTestUtils.getResultCount(window); + + // Key down through all results. + for (let i = 0; i < resultCount; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(i, -1); + } + + // Key down again. Nothing should be selected. + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(-1, -1, value); + + // Key down again. The first result should be selected. + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(0, -1); + + // Key up. Nothing should be selected. + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(-1, -1, value); + + // Key up through all the results. + for (let i = resultCount - 1; i >= 0; i--) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(i, -1); + } + + // Key up again. Nothing should be selected. + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(-1, -1, value); + + await hidePopup(); +}); + +// Opens the top-sites view. The one-offs should be shown. +add_task(async function topSites() { + // Do a search that shows top sites. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + await TestUtils.waitForCondition( + () => !oneOffSearchButtons._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + + // There's one top sites result, the page with a lot of visits from init. + let resultURL = UrlbarTestUtils.trimURL( + "http://example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - 1) + ); + Assert.equal(UrlbarTestUtils.getResultCount(window), 1, "Result count"); + + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + true, + "One-offs are visible" + ); + + assertState(-1, -1, ""); + + // Key down into the result. + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(0, -1, resultURL); + + // Key down through each one-off. + let numButtons = oneOffSearchButtons.getSelectableButtons(true).length; + for (let i = 0; i < numButtons; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(-1, i, ""); + } + + // Key down again. The selection should go away. + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(-1, -1, ""); + + // Key down again. The result should be selected. + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(0, -1, resultURL); + + // Key back up. The selection should go away. + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(-1, -1, ""); + + // Key up again. The selection should wrap back around to the one-offs. Key + // up through all the one-offs. + for (let i = numButtons - 1; i >= 0; i--) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(-1, i, ""); + } + + // Key up. The result should be selected. + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(0, -1, resultURL); + + // Key up again. The selection should go away. + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(-1, -1, ""); + + await hidePopup(); +}); + +// Keys up and down through the non-top-sites view, i.e., the view that's shown +// when the input has been edited. +add_task(async function editedView() { + // Use a typed value that returns the visits added above but that doesn't + // trigger autofill since that would complicate the test. + let typedValue = "browser_urlbarOneOffs"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + fireInputEvent: true, + }); + await UrlbarTestUtils.waitForAutocompleteResultAt(window, gMaxResults - 1); + let heuristicResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + assertState(0, -1, typedValue); + + // Key down through each result. The first result is already selected, which + // is why gMaxResults - 1 is the correct number of times to do this. + for (let i = 0; i < gMaxResults - 1; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + // i starts at zero so that the textValue passed to assertState is correct. + // But that means that i + 1 is the expected selected index, since initially + // (when this loop starts) the first result is selected. + assertState( + i + 1, + -1, + UrlbarTestUtils.trimURL( + "http://example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1) + ) + ); + Assert.ok( + !BrowserTestUtils.isVisible(heuristicResult.element.action), + "The heuristic action should not be visible" + ); + } + + // Key down through each one-off. + let numButtons = oneOffSearchButtons.getSelectableButtons(true).length; + for (let i = 0; i < numButtons; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(-1, i, typedValue); + Assert.equal( + BrowserTestUtils.isVisible(heuristicResult.element.action), + !oneOffSearchButtons.selectedButton.classList.contains( + "search-setting-button" + ), + "The heuristic action should be visible when a one-off button is selected" + ); + } + + // Key down once more. The selection should wrap around to the first result. + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(0, -1, typedValue); + Assert.ok( + BrowserTestUtils.isVisible(heuristicResult.element.action), + "The heuristic action should be visible" + ); + + // Now key up. The selection should wrap back around to the one-offs. Key + // up through all the one-offs. + for (let i = numButtons - 1; i >= 0; i--) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(-1, i, typedValue); + Assert.equal( + BrowserTestUtils.isVisible(heuristicResult.element.action), + !oneOffSearchButtons.selectedButton.classList.contains( + "search-setting-button" + ), + "The heuristic action should be visible when a one-off button is selected" + ); + } + + // Key up through each non-heuristic result. + for (let i = gMaxResults - 2; i >= 0; i--) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState( + i + 1, + -1, + UrlbarTestUtils.trimURL( + "http://example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1) + ) + ); + Assert.ok( + !BrowserTestUtils.isVisible(heuristicResult.element.action), + "The heuristic action should not be visible" + ); + } + + // Key up once more. The heuristic result should be selected. + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(0, -1, typedValue); + Assert.ok( + BrowserTestUtils.isVisible(heuristicResult.element.action), + "The heuristic action should be visible" + ); + + await hidePopup(); +}); + +// Checks that "Search with Current Search Engine" items are updated to "Search +// with One-Off Engine" when a one-off is selected. +add_task(async function searchWith() { + // Enable suggestions for this subtest so we can check non-heuristic results. + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", true]], + }); + + let typedValue = "foo"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + assertState(0, -1, typedValue); + + Assert.equal( + result.displayed.action, + "Search with " + (await Services.search.getDefault()).name, + "Sanity check: first result's action text" + ); + + // Alt+Down to the second one-off. Now the first result and the second + // one-off should both be selected. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, repeat: 2 }); + assertState(0, 1, typedValue); + + let engineName = oneOffSearchButtons.selectedButton.engine.name; + Assert.notEqual( + engineName, + (await Services.search.getDefault()).name, + "Sanity check: Second one-off engine should not be the current engine" + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.displayed.action, + "Search with " + engineName, + "First result's action text should be updated" + ); + + // Check non-heuristic results. + await hidePopup(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + }); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + assertState(1, -1, typedValue + "foo"); + Assert.equal( + result.displayed.action, + "Search with " + engine.name, + "Sanity check: second result's action text" + ); + Assert.ok(!result.heuristic, "The second result is not heuristic."); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, repeat: 2 }); + assertState(1, 1, typedValue + "foo"); + + engineName = oneOffSearchButtons.selectedButton.engine.name; + Assert.notEqual( + engineName, + (await Services.search.getDefault()).name, + "Sanity check: Second one-off engine should not be the current engine" + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + + Assert.equal( + result.displayed.action, + "Search with " + engineName, + "Second result's action text should be updated" + ); + + await SpecialPowers.popPrefEnv(); + await Services.search.setDefault( + oldDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await hidePopup(); +}); + +// Clicks a one-off with an engine. +add_task(async function oneOffClick() { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + + // We are explicitly using something that looks like a url, to make the test + // stricter. Even if it looks like a url, we should search. + let typedValue = "foo.bar"; + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + }); + await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + assertState(0, -1, typedValue); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(oneOffs[0], {}); + await searchPromise; + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is still open."); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + + gBrowser.removeTab(gBrowser.selectedTab); + await UrlbarTestUtils.formHistory.clear(); +}); + +// Presses the Return key when a one-off with an engine is selected. +add_task(async function oneOffReturn() { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + + // We are explicitly using something that looks like a url, to make the test + // stricter. Even if it looks like a url, we should search. + let typedValue = "foo.bar"; + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + fireInputEvent: true, + }); + await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + assertState(0, -1, typedValue); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + + // Alt+Down to select the first one-off. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + assertState(0, 0, typedValue); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is still open."); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + + gBrowser.removeTab(gBrowser.selectedTab); + await UrlbarTestUtils.formHistory.clear(); + await hidePopup(); +}); + +// When all engines and local shortcuts are hidden except for the current +// engine, the one-offs container should be hidden. +add_task(async function allOneOffsHiddenExceptCurrentEngine() { + // Disable all the engines but the current one, check the oneoffs are + // hidden and that moving up selects the last match. + let defaultEngine = await Services.search.getDefault(); + let engines = (await Services.search.getVisibleEngines()).filter( + e => e.name != defaultEngine.name + ); + await SpecialPowers.pushPrefEnv({ + set: [ + ...UrlbarUtils.LOCAL_SEARCH_MODES.map(m => [ + `browser.urlbar.${m.pref}`, + false, + ]), + ], + }); + engines.forEach(e => { + e.hideOneOffButton = e.name !== defaultEngine.name; + }); + + let typedValue = "foo"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + fireInputEvent: true, + }); + await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + assertState(0, -1); + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + false, + "The one-off buttons should be hidden" + ); + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(0, -1); + await hidePopup(); + await SpecialPowers.popPrefEnv(); + engines.forEach(e => { + e.hideOneOffButton = false; + }); +}); + +// The one-offs should be hidden when searching with an "@engine" search engine +// alias. +add_task(async function hiddenWhenUsingSearchAlias() { + let typedValue = "@example"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + fireInputEvent: true, + }); + await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + false, + "Should not be showing the one-off buttons" + ); + await hidePopup(); + + typedValue = "not an engine alias"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + fireInputEvent: true, + }); + await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + true, + "Should be showing the one-off buttons" + ); + await hidePopup(); +}); + +// Makes sure the local shortcuts exist. +add_task(async function localShortcuts() { + oneOffSearchButtons.invalidateCache(); + await doLocalShortcutsShownTest(); +}); + +// Clicks a local shortcut button. +add_task(async function localShortcutClick() { + // We are explicitly using something that looks like a url, to make the test + // stricter. Even if it looks like a url, we should search. + let typedValue = "foo.bar"; + + oneOffSearchButtons.invalidateCache(); + let rebuildPromise = BrowserTestUtils.waitForEvent( + oneOffSearchButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + }); + await rebuildPromise; + + let buttons = oneOffSearchButtons.localButtons; + Assert.ok(buttons.length, "Sanity check: Local shortcuts exist"); + + for (let button of buttons) { + Assert.ok(button.source, "Sanity check: Button has a source"); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(button, {}); + await searchPromise; + Assert.ok( + UrlbarTestUtils.isPopupOpen(window), + "Urlbar view is still open." + ); + await UrlbarTestUtils.assertSearchMode(window, { + source: button.source, + entry: "oneoff", + }); + } + + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + await hidePopup(); +}); + +// Presses the Return key when a local shortcut is selected. +add_task(async function localShortcutReturn() { + // We are explicitly using something that looks like a url, to make the test + // stricter. Even if it looks like a url, we should search. + let typedValue = "foo.bar"; + + oneOffSearchButtons.invalidateCache(); + let rebuildPromise = BrowserTestUtils.waitForEvent( + oneOffSearchButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + }); + await rebuildPromise; + + let buttons = oneOffSearchButtons.localButtons; + Assert.ok(buttons.length, "Sanity check: Local shortcuts exist"); + + let allButtons = oneOffSearchButtons.getSelectableButtons(false); + let firstLocalIndex = allButtons.length - buttons.length; + + for (let i = 0; i < buttons.length; i++) { + let button = buttons[i]; + + // Alt+Down enough times to select the button. + let index = firstLocalIndex + i; + EventUtils.synthesizeKey("KEY_ArrowDown", { + altKey: true, + repeat: index + 1, + }); + await TestUtils.waitForCondition( + () => oneOffSearchButtons.selectedButtonIndex == index, + "Waiting for local shortcut to become selected" + ); + + let expectedSelectedResultIndex = -1; + let count = UrlbarTestUtils.getResultCount(window); + if (count > 0) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + if (result.heuristic) { + expectedSelectedResultIndex = 0; + } + } + assertState(expectedSelectedResultIndex, index, typedValue); + + Assert.ok(button.source, "Sanity check: Button has a source"); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + Assert.ok( + UrlbarTestUtils.isPopupOpen(window), + "Urlbar view is still open." + ); + await UrlbarTestUtils.assertSearchMode(window, { + source: button.source, + entry: "oneoff", + }); + } + + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + await hidePopup(); +}); + +// With an empty search string, clicking a local shortcut should result in no +// heuristic result. +add_task(async function localShortcutEmptySearchString() { + oneOffSearchButtons.invalidateCache(); + let rebuildPromise = BrowserTestUtils.waitForEvent( + oneOffSearchButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await rebuildPromise; + + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + true, + "One-offs are visible" + ); + + let buttons = oneOffSearchButtons.localButtons; + Assert.ok(buttons.length, "Sanity check: Local shortcuts exist"); + + for (let button of buttons) { + Assert.ok(button.source, "Sanity check: Button has a source"); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(button, {}); + await searchPromise; + Assert.ok( + UrlbarTestUtils.isPopupOpen(window), + "Urlbar view is still open." + ); + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + true, + "One-offs are visible" + ); + await UrlbarTestUtils.assertSearchMode(window, { + source: button.source, + entry: "oneoff", + }); + + let resultCount = UrlbarTestUtils.getResultCount(window); + if (!resultCount) { + Assert.equal( + gURLBar.panel.getAttribute("noresults"), + "true", + "Panel has no results, therefore should have noresults attribute" + ); + continue; + } + Assert.ok( + !gURLBar.panel.hasAttribute("noresults"), + "Panel has results, therefore should not have noresults attribute" + ); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(!result.heuristic, "The first result should not be heuristic"); + } + + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + + await hidePopup(); +}); + +// Trigger SearchOneOffs.willHide() outside of SearchOneOffs.__rebuild(). Ensure +// that we always show the correct engines in the one-offs. This effectively +// tests SearchOneOffs._engineInfo.domWasUpdated. +add_task(async function avoidWillHideRace() { + // We set maxHistoricalSearchSuggestions to 0 since this test depends on + // UrlbarView calling SearchOneOffs.willHide(). That only happens when the + // Urlbar is in search mode after a query that returned no results. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 0]], + }); + + oneOffSearchButtons.invalidateCache(); + + // Accel+K triggers SearchOneOffs.willHide() from UrlbarView instead of from + // SearchOneOffs.__rebuild. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("k", { accelKey: true }); + await searchPromise; + Assert.ok( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + "One-offs should be visible" + ); + await UrlbarTestUtils.promisePopupClose(window); + + info("Hide all engines but the test engine."); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + let engines = (await Services.search.getVisibleEngines()).filter( + e => e.name != engine.name + ); + await SpecialPowers.pushPrefEnv({ + set: [ + ...UrlbarUtils.LOCAL_SEARCH_MODES.map(m => [ + `browser.urlbar.${m.pref}`, + false, + ]), + ], + }); + engines.forEach(e => { + e.hideOneOffButton = true; + }); + Assert.ok( + !oneOffSearchButtons._engineInfo, + "_engineInfo should be nulled out." + ); + + // This call to SearchOneOffs.willHide() should repopulate _engineInfo but not + // rebuild the one-offs. _engineInfo.willHide will be true and thus UrlbarView + // will not open. + EventUtils.synthesizeKey("k", { accelKey: true }); + // We can't wait for UrlbarTestUtils.promiseSearchComplete here since we + // expect the popup will not open. We wait for _engineInfo to be populated + // instead. + await BrowserTestUtils.waitForCondition( + () => !!oneOffSearchButtons._engineInfo, + "_engineInfo is set." + ); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "The UrlbarView is closed."); + Assert.equal( + oneOffSearchButtons._engineInfo.willHide, + true, + "_engineInfo should be repopulated and willHide should be true." + ); + Assert.equal( + oneOffSearchButtons._engineInfo.domWasUpdated, + undefined, + "domWasUpdated should not be populated since we haven't yet tried to rebuild the one-offs." + ); + + // Now search. The view will open and the one-offs will rebuild, although + // the one-offs will not be shown since there is only one engine. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + Assert.equal( + oneOffSearchButtons._engineInfo.domWasUpdated, + true, + "domWasUpdated should be true" + ); + Assert.ok( + !UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + "One-offs should be hidden since there is only one engine." + ); + await UrlbarTestUtils.promisePopupClose(window); + + await SpecialPowers.popPrefEnv(); + await Services.search.setDefault( + oldDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await SpecialPowers.popPrefEnv(); + engines.forEach(e => { + e.hideOneOffButton = false; + }); +}); + +// Hides each of the local shortcuts one at a time. The search buttons should +// automatically rebuild themselves. +add_task(async function individualLocalShortcutsHidden() { + for (let { pref, source } of UrlbarUtils.LOCAL_SEARCH_MODES) { + await SpecialPowers.pushPrefEnv({ + set: [[`browser.urlbar.${pref}`, false]], + }); + + let rebuildPromise = BrowserTestUtils.waitForEvent( + oneOffSearchButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await rebuildPromise; + + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + true, + "One-offs are visible" + ); + + let buttons = oneOffSearchButtons.localButtons; + Assert.ok(buttons.length, "Sanity check: Local shortcuts exist"); + + let otherModes = UrlbarUtils.LOCAL_SEARCH_MODES.filter( + m => m.source != source + ); + Assert.equal( + buttons.length, + otherModes.length, + "Expected number of enabled local shortcut buttons" + ); + + for (let i = 0; i < buttons.length; i++) { + Assert.equal( + buttons[i].source, + otherModes[i].source, + "Button has the expected source" + ); + } + + await hidePopup(); + await SpecialPowers.popPrefEnv(); + } +}); + +// Hides all the local shortcuts at once. +add_task(async function allLocalShortcutsHidden() { + await SpecialPowers.pushPrefEnv({ + set: UrlbarUtils.LOCAL_SEARCH_MODES.map(m => [ + `browser.urlbar.${m.pref}`, + false, + ]), + }); + + let rebuildPromise = BrowserTestUtils.waitForEvent( + oneOffSearchButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await rebuildPromise; + + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + true, + "One-offs are visible" + ); + + Assert.equal( + oneOffSearchButtons.localButtons.length, + 0, + "All local shortcuts should be hidden" + ); + + Assert.greater( + oneOffSearchButtons.getSelectableButtons(false).filter(b => b.engine) + .length, + 0, + "Engine one-offs should not be hidden" + ); + + await hidePopup(); + await SpecialPowers.popPrefEnv(); +}); + +// Hides all the engines but none of the local shortcuts. +add_task(async function localShortcutsShownWhenEnginesHidden() { + let engines = await Services.search.getVisibleEngines(); + + engines.forEach(e => { + e.hideOneOffButton = true; + }); + + let rebuildPromise = BrowserTestUtils.waitForEvent( + oneOffSearchButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await rebuildPromise; + + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + true, + "One-offs are visible" + ); + + Assert.equal( + oneOffSearchButtons.localButtons.length, + UrlbarUtils.LOCAL_SEARCH_MODES.length, + "All local shortcuts are visible" + ); + + Assert.equal( + oneOffSearchButtons.getSelectableButtons(false).filter(b => b.engine) + .length, + 0, + "All engine one-offs are hidden" + ); + + await hidePopup(); + engines.forEach(e => { + e.hideOneOffButton = false; + }); +}); + +/** + * Checks that the local shortcuts are shown correctly. + */ +async function doLocalShortcutsShownTest() { + let rebuildPromise = BrowserTestUtils.waitForEvent( + oneOffSearchButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "doLocalShortcutsShownTest", + }); + await rebuildPromise; + + let buttons = oneOffSearchButtons.localButtons; + Assert.equal(buttons.length, 4, "Expected number of local shortcuts"); + + let expectedSource; + let seenIDs = new Set(); + for (let button of buttons) { + Assert.ok( + !seenIDs.has(button.id), + "Should not have already seen button.id" + ); + seenIDs.add(button.id); + switch (button.id) { + case "urlbar-engine-one-off-item-bookmarks": + expectedSource = UrlbarUtils.RESULT_SOURCE.BOOKMARKS; + break; + case "urlbar-engine-one-off-item-tabs": + expectedSource = UrlbarUtils.RESULT_SOURCE.TABS; + break; + case "urlbar-engine-one-off-item-history": + expectedSource = UrlbarUtils.RESULT_SOURCE.HISTORY; + break; + case "urlbar-engine-one-off-item-actions": + expectedSource = UrlbarUtils.RESULT_SOURCE.ACTIONS; + break; + default: + Assert.ok(false, `Unexpected local shortcut ID: ${button.id}`); + break; + } + Assert.equal(button.source, expectedSource, "Expected button.source"); + } + + await hidePopup(); +} + +function assertState(result, oneOff, textValue = undefined) { + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + result, + "Expected result should be selected" + ); + Assert.equal( + oneOffSearchButtons.selectedButtonIndex, + oneOff, + "Expected one-off should be selected" + ); + if (textValue !== undefined) { + Assert.equal(gURLBar.value, textValue, "Expected value"); + } +} + +function hidePopup() { + return UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js b/browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js new file mode 100644 index 0000000000..4ae083c51f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the right-click menu works correctly for the one-off buttons. + */ + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +let gMaxResults; + +ChromeUtils.defineLazyGetter(this, "oneOffSearchButtons", () => { + return UrlbarTestUtils.getOneOffSearchButtons(window); +}); + +let originalEngine; +let newEngine; + +// The one-off context menu should not be shown. +add_task(async function contextMenu_not_shown() { + // Add a popupshown listener on the context menu that sets this + // popupshownFired boolean. + let popupshownFired = false; + let onPopupshown = () => { + popupshownFired = true; + }; + let contextMenu = oneOffSearchButtons.querySelector( + ".search-one-offs-context-menu" + ); + contextMenu.addEventListener("popupshown", onPopupshown); + + // Do a search to open the view. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + + // First, try to open the context menu on a remote engine. + let allOneOffs = oneOffSearchButtons.getSelectableButtons(true); + Assert.greater(allOneOffs.length, 0, "There should be at least one one-off"); + Assert.ok( + allOneOffs[0].engine, + "The first one-off should be a remote one-off" + ); + EventUtils.synthesizeMouseAtCenter(allOneOffs[0], { + type: "contextmenu", + button: 2, + }); + let timeout = 500; + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, timeout)); + Assert.ok( + !popupshownFired, + "popupshown should not be fired on a remote one-off" + ); + + // Now try to open the context menu on a local one-off. + let localOneOffs = oneOffSearchButtons.localButtons; + Assert.greater( + localOneOffs.length, + 0, + "There should be at least one local one-off" + ); + EventUtils.synthesizeMouseAtCenter(localOneOffs[0], { + type: "contextmenu", + button: 2, + }); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, timeout)); + Assert.ok( + !popupshownFired, + "popupshown should not be fired on a local one-off" + ); + + contextMenu.removeEventListener("popupshown", onPopupshown); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_heuristicRestyle.js b/browser/components/urlbar/tests/browser/browser_oneOffs_heuristicRestyle.js new file mode 100644 index 0000000000..8f7f058dd8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs_heuristicRestyle.js @@ -0,0 +1,516 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that heuristic results are updated/restyled to search results when a + * one-off is selected. + */ + +"use strict"; + +ChromeUtils.defineLazyGetter(this, "oneOffSearchButtons", () => { + return UrlbarTestUtils.getOneOffSearchButtons(window); +}); + +const TEST_DEFAULT_ENGINE_NAME = "Test"; + +const HISTORY_URL = "https://mozilla.org/"; + +const KEYWORD = "kw"; +const KEYWORD_URL = "https://mozilla.org/search?q=%s"; + +// Expected result data for our test results. +const RESULT_DATA_BY_TYPE = { + [UrlbarUtils.RESULT_TYPE.URL]: { + icon: `page-icon:${HISTORY_URL}`, + actionL10n: { + id: "urlbar-result-action-visit", + }, + }, + [UrlbarUtils.RESULT_TYPE.SEARCH]: { + icon: "chrome://global/skin/icons/search-glass.svg", + actionL10n: { + id: "urlbar-result-action-search-w-engine", + args: { engine: TEST_DEFAULT_ENGINE_NAME }, + }, + }, + [UrlbarUtils.RESULT_TYPE.KEYWORD]: { + icon: `page-icon:${KEYWORD_URL}`, + }, +}; + +function getSourceIcon(source) { + switch (source) { + case UrlbarUtils.RESULT_SOURCE.BOOKMARKS: + return "chrome://browser/skin/bookmark.svg"; + case UrlbarUtils.RESULT_SOURCE.HISTORY: + return "chrome://browser/skin/history.svg"; + case UrlbarUtils.RESULT_SOURCE.TABS: + return "chrome://browser/skin/tab.svg"; + default: + return null; + } +} + +/** + * Asserts that the heuristic result is *not* restyled to look like a search + * result. + * + * @param {UrlbarUtils.RESULT_TYPE} expectedType + * The expected type of the heuristic. + * @param {object} resultDetails + * The return value of UrlbarTestUtils.getDetailsOfResultAt(window, 0). + */ +async function heuristicIsNotRestyled(expectedType, resultDetails) { + Assert.equal( + resultDetails.type, + expectedType, + "The restyled result is the expected type." + ); + + Assert.equal( + resultDetails.displayed.title, + resultDetails.title, + "The displayed title is equal to the payload title." + ); + + let data = RESULT_DATA_BY_TYPE[expectedType]; + Assert.ok(data, "Sanity check: Expected type is recognized"); + + let [actionText] = data.actionL10n + ? await document.l10n.formatValues([data.actionL10n]) + : [""]; + + if ( + expectedType === UrlbarUtils.RESULT_TYPE.URL && + resultDetails.result.heuristic && + resultDetails.result.payload.title + ) { + Assert.equal( + resultDetails.displayed.url, + resultDetails.result.payload.displayUrl + ); + } else { + Assert.equal( + resultDetails.displayed.action, + actionText, + "The result has the expected non-styled action text." + ); + } + + Assert.equal( + BrowserTestUtils.isVisible(resultDetails.element.separator), + !!actionText, + "The title separator is " + (actionText ? "visible" : "hidden") + ); + Assert.equal( + BrowserTestUtils.isVisible(resultDetails.element.action), + !!actionText, + "The action text is " + (actionText ? "visible" : "hidden") + ); + + Assert.equal( + resultDetails.image, + data.icon, + "The result has the expected non-styled icon." + ); +} + +/** + * Asserts that the heuristic result is restyled to look like a search result. + * + * @param {UrlbarUtils.RESULT_TYPE} expectedType + * The expected type of the heuristic. + * @param {object} resultDetails + * The return value of UrlbarTestUtils.getDetailsOfResultAt(window, 0). + * @param {string} searchString + * The current search string. The restyled heuristic result's title is + * expected to be this string. + * @param {element} selectedOneOff + * The selected one-off button. + */ +async function heuristicIsRestyled( + expectedType, + resultDetails, + searchString, + selectedOneOff +) { + let engine = selectedOneOff.engine; + let source = selectedOneOff.source; + if (!engine && !source) { + Assert.ok(false, "An invalid one-off was passed to urlbarResultIsRestyled"); + return; + } + Assert.equal( + resultDetails.type, + expectedType, + "The restyled result is still the expected type." + ); + + let actionText; + if (engine) { + [actionText] = await document.l10n.formatValues([ + { + id: "urlbar-result-action-search-w-engine", + args: { engine: engine.name }, + }, + ]); + } else if (source) { + [actionText] = await document.l10n.formatValues([ + { + id: `urlbar-result-action-search-${UrlbarUtils.getResultSourceName( + source + )}`, + }, + ]); + } + Assert.equal( + resultDetails.displayed.action, + actionText, + "Restyled result's action text should be updated" + ); + + Assert.equal( + resultDetails.displayed.title, + searchString, + "The restyled result's title should be equal to the search string." + ); + + Assert.ok( + BrowserTestUtils.isVisible(resultDetails.element.separator), + "The restyled result's title separator should be visible" + ); + Assert.ok( + BrowserTestUtils.isVisible(resultDetails.element.action), + "The restyled result's action text should be visible" + ); + + if (engine) { + Assert.equal( + resultDetails.image, + engine.getIconURL() || UrlbarUtils.ICON.SEARCH_GLASS, + "The restyled result's icon should be the engine's icon." + ); + } else if (source) { + Assert.equal( + resultDetails.image, + getSourceIcon(source), + "The restyled result's icon should be the local one-off's icon." + ); + } +} + +/** + * Asserts that the specified one-off (if any) is selected and that the + * heuristic result is either restyled or not restyled as appropriate. If + * there's a selected one-off, then the heuristic is expected to be restyled; if + * there's no selected one-off, then it's expected not to be restyled. + * + * @param {string} searchString + * The current search string. If a one-off is selected, then the restyled + * heuristic result's title is expected to be this string. + * @param {UrlbarUtils.RESULT_TYPE} expectedHeuristicType + * The expected type of the heuristic. + * @param {number} expectedSelectedOneOffIndex + * The index of the expected selected one-off button. If no one-off is + * expected to be selected, then pass -1. + */ +async function assertState( + searchString, + expectedHeuristicType, + expectedSelectedOneOffIndex +) { + Assert.equal( + oneOffSearchButtons.selectedButtonIndex, + expectedSelectedOneOffIndex, + "Expected one-off should be selected" + ); + + let resultDetails = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + if (expectedSelectedOneOffIndex >= 0) { + await heuristicIsRestyled( + expectedHeuristicType, + resultDetails, + searchString, + oneOffSearchButtons.selectedButton + ); + } else { + await heuristicIsNotRestyled(expectedHeuristicType, resultDetails); + } +} + +add_setup(async function () { + await SearchTestUtils.installSearchExtension( + { + name: TEST_DEFAULT_ENGINE_NAME, + keyword: "@test", + }, + { setAsDefault: true } + ); + let engine = Services.search.getEngineByName(TEST_DEFAULT_ENGINE_NAME); + await Services.search.moveEngine(engine, 0); + + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits(HISTORY_URL); + } + + await PlacesUtils.keywords.insert({ + keyword: KEYWORD, + url: KEYWORD_URL, + }); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + await PlacesUtils.keywords.remove(KEYWORD); + }); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", false]], + }); + + // Move the mouse away from the view so that a result or one-off isn't + // inadvertently highlighted. See bug 1659011. + EventUtils.synthesizeMouse( + gURLBar.inputField, + 0, + 0, + { type: "mousemove" }, + window + ); +}); + +add_task(async function arrow_engine_url() { + await doArrowTest("mozilla.or", UrlbarUtils.RESULT_TYPE.URL, false); +}); + +add_task(async function arrow_engine_search() { + await doArrowTest("test", UrlbarUtils.RESULT_TYPE.SEARCH, false); +}); + +add_task(async function arrow_engine_keyword() { + await doArrowTest(`${KEYWORD} test`, UrlbarUtils.RESULT_TYPE.KEYWORD, false); +}); + +add_task(async function arrow_local_url() { + await doArrowTest("mozilla.or", UrlbarUtils.RESULT_TYPE.URL, true); +}); + +add_task(async function arrow_local_search() { + await doArrowTest("test", UrlbarUtils.RESULT_TYPE.SEARCH, true); +}); + +add_task(async function arrow_local_keyword() { + await doArrowTest(`${KEYWORD} test`, UrlbarUtils.RESULT_TYPE.KEYWORD, true); +}); + +/** + * Arrows down to the one-offs, checks the heuristic, and clicks it. + * + * @param {string} searchString + * The search string to use. + * @param {UrlbarUtils.RESULT_TYPE} expectedHeuristicType + * The type of heuristic result that the search string is expected to trigger. + * @param {boolean} useLocal + * Whether to test a local one-off or an engine one-off. If true, test a + * local one-off. If false, test an engine one-off. + */ +async function doArrowTest(searchString, expectedHeuristicType, useLocal) { + await doTest(searchString, expectedHeuristicType, useLocal, async () => { + info( + "Arrow down to the one-offs, observe heuristic is restyled as a search result." + ); + let resultCount = UrlbarTestUtils.getResultCount(window); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: resultCount }); + await searchPromise; + await assertState(searchString, expectedHeuristicType, 0); + + let depth = 1; + if (useLocal) { + for (; !oneOffSearchButtons.selectedButton.source; depth++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + Assert.ok( + oneOffSearchButtons.selectedButton.source, + "Selected one-off is local" + ); + await assertState(searchString, expectedHeuristicType, depth - 1); + } + + info( + "Arrow up out of the one-offs, observe heuristic styling is restored." + ); + EventUtils.synthesizeKey("KEY_ArrowUp", { repeat: depth }); + await assertState(searchString, expectedHeuristicType, -1); + + info( + "Arrow back down into the one-offs, observe heuristic is restyled as a search result." + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: depth }); + await assertState(searchString, expectedHeuristicType, depth - 1); + }); +} + +add_task(async function altArrow_engine_url() { + await doAltArrowTest("mozilla.or", UrlbarUtils.RESULT_TYPE.URL, false); +}); + +add_task(async function altArrow_engine_search() { + await doAltArrowTest("test", UrlbarUtils.RESULT_TYPE.SEARCH, false); +}); + +add_task(async function altArrow_engine_keyword() { + await doAltArrowTest( + `${KEYWORD} test`, + UrlbarUtils.RESULT_TYPE.KEYWORD, + false + ); +}); + +add_task(async function altArrow_local_url() { + await doAltArrowTest("mozilla.or", UrlbarUtils.RESULT_TYPE.URL, true); +}); + +add_task(async function altArrow_local_search() { + await doAltArrowTest("test", UrlbarUtils.RESULT_TYPE.SEARCH, true); +}); + +add_task(async function altArrow_local_keyword() { + await doAltArrowTest( + `${KEYWORD} test`, + UrlbarUtils.RESULT_TYPE.KEYWORD, + true + ); +}); + +/** + * Alt-arrows down to the one-offs so that the heuristic remains selected, + * checks the heuristic, and clicks it. + * + * @param {string} searchString + * The search string to use. + * @param {UrlbarUtils.RESULT_TYPE} expectedHeuristicType + * The type of heuristic result that the search string is expected to trigger. + * @param {boolean} useLocal + * Whether to test a local one-off or an engine one-off. If true, test a + * local one-off. If false, test an engine one-off. + */ +async function doAltArrowTest(searchString, expectedHeuristicType, useLocal) { + await doTest(searchString, expectedHeuristicType, useLocal, async () => { + info( + "Alt+down into the one-offs, observe heuristic is restyled as a search result." + ); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + await searchPromise; + await assertState(searchString, expectedHeuristicType, 0); + + let depth = 1; + if (useLocal) { + for (; !oneOffSearchButtons.selectedButton.source; depth++) { + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + } + Assert.ok( + oneOffSearchButtons.selectedButton.source, + "Selected one-off is local" + ); + await assertState(searchString, expectedHeuristicType, depth - 1); + } + + info( + "Arrow down and then up to re-select the heuristic, observe its styling is restored." + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + await assertState(searchString, expectedHeuristicType, -1); + + info( + "Alt+down into the one-offs, observe the heuristic is restyled as a search result." + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, repeat: depth }); + await assertState(searchString, expectedHeuristicType, depth - 1); + + info("Alt+up out of the one-offs, observe the heuristic is restored."); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true, repeat: depth }); + await assertState(searchString, expectedHeuristicType, -1); + + info( + "Alt+down into the one-offs, observe the heuristic is restyled as a search result." + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, repeat: depth }); + await assertState(searchString, expectedHeuristicType, depth - 1); + }); +} + +/** + * The main test function. Starts a search, asserts that the heuristic has the + * expected type, calls a callback to run more checks, and then finally clicks + * the restyled heuristic to make sure search mode is confirmed. + * + * @param {string} searchString + * The search string to use. + * @param {UrlbarUtils.RESULT_TYPE} expectedHeuristicType + * The type of heuristic result that the search string is expected to trigger. + * @param {boolean} useLocal + * Whether to test a local one-off or an engine one-off. If true, test a + * local one-off. If false, test an engine one-off. + * @param {Function} callback + * This is called after the search completes. It should perform whatever + * checks are necessary for the test task. Important: When it returns, it + * should make sure that the first one-off is selected. + */ +async function doTest(searchString, expectedHeuristicType, useLocal, callback) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + await TestUtils.waitForCondition( + () => !oneOffSearchButtons._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(result.heuristic, "First result is heuristic"); + Assert.equal( + result.type, + expectedHeuristicType, + "Heuristic is expected type" + ); + await assertState(searchString, expectedHeuristicType, -1); + + await callback(); + + Assert.ok( + oneOffSearchButtons.selectedButton, + "The callback should leave a one-off selected so that the heuristic remains re-styled" + ); + + info("Click the heuristic result and observe it confirms search mode."); + let selectedButton = oneOffSearchButtons.selectedButton; + let expectedSearchMode = { + entry: "oneoff", + isPreview: true, + }; + if (useLocal) { + expectedSearchMode.source = selectedButton.source; + } else { + expectedSearchMode.engineName = selectedButton.engine.name; + } + + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + + let heuristicRow = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + 0 + ); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(heuristicRow, {}); + await searchPromise; + + expectedSearchMode.isPreview = false; + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +} diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_keyModifiers.js b/browser/components/urlbar/tests/browser/browser_oneOffs_keyModifiers.js new file mode 100644 index 0000000000..375dd6e9ae --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs_keyModifiers.js @@ -0,0 +1,392 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that one-offs behave differently with key modifiers. + */ + +"use strict"; + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; +const SEARCH_STRING = "foo.bar"; + +ChromeUtils.defineLazyGetter(this, "oneOffSearchButtons", () => { + return UrlbarTestUtils.getOneOffSearchButtons(window); +}); + +let engine; + +async function searchAndOpenPopup(value) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + fireInputEvent: true, + }); + await TestUtils.waitForCondition( + () => !oneOffSearchButtons._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); +} + +add_setup(async function () { + // Add a search suggestion engine and move it to the front so that it appears + // as the first one-off. + engine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + }); + await Services.search.moveEngine(engine, 0); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", false], + ["browser.urlbar.suggest.quickactions", false], + ], + }); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); + + // Initialize history with enough visits to fill up the view. + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults"); + for (let i = 0; i < maxResults; i++) { + await PlacesTestUtils.addVisits( + "http://mochi.test:8888/browser_urlbarOneOffs.js/?" + i + ); + } + + // Add some more visits to the last URL added above so that the top-sites view + // will be non-empty. + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits( + "http://mochi.test:8888/browser_urlbarOneOffs.js/?" + (maxResults - 1) + ); + } + await updateTopSites(sites => { + return ( + sites && sites[0] && sites[0].url.startsWith("http://mochi.test:8888/") + ); + }); + + // Move the mouse away from the view so that a result or one-off isn't + // inadvertently highlighted. See bug 1659011. + EventUtils.synthesizeMouse( + gURLBar.inputField, + 0, + 0, + { type: "mousemove" }, + window + ); +}); + +// Shift clicking with no search string should open search mode, like an +// unmodified click. +add_task(async function shift_click_empty() { + await searchAndOpenPopup(""); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(oneOffs[0], { shiftKey: true }); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Shift clicking with a search string should perform a search in the current +// tab. +add_task(async function shift_click_search() { + await searchAndOpenPopup(SEARCH_STRING); + let resultsPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "http://mochi.test:8888/?terms=foo.bar" + ); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + EventUtils.synthesizeMouseAtCenter(oneOffs[0], { shiftKey: true }); + await resultsPromise; + await UrlbarTestUtils.assertSearchMode(window, null); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Pressing Shift+Enter on a one-off with no search string should open search +// mode, like an unmodified click. +add_task(async function shift_enter_empty() { + await searchAndOpenPopup(""); + // Alt+Down to select the first one-off. + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter", { shiftKey: true }); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Pressing Shift+Enter on a one-off with a search string should perform a +// search in the current tab. +add_task(async function shift_enter_search() { + await searchAndOpenPopup(SEARCH_STRING); + let resultsPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "http://mochi.test:8888/?terms=foo.bar" + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + EventUtils.synthesizeKey("KEY_Enter", { shiftKey: true }); + await resultsPromise; + await UrlbarTestUtils.assertSearchMode(window, null); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Pressing Alt+Enter on a one-off on an "empty" page (e.g. new tab) should open +// search mode in the current tab. +add_task(async function alt_enter_emptypage() { + await BrowserTestUtils.withNewTab("about:home", async function (browser) { + await searchAndOpenPopup(SEARCH_STRING); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + // Alt+Down to select the first one-off. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter", { altKey: true }); + await searchPromise; + Assert.equal( + browser, + gBrowser.selectedBrowser, + "The foreground tab hasn't changed." + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Pressing Alt+Enter on a one-off with no search string and on a "non-empty" +// page should open search mode in a new foreground tab. +add_task(async function alt_enter_empty() { + await BrowserTestUtils.withNewTab("about:robots", async function (browser) { + await searchAndOpenPopup(""); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + // Alt+Down to select the first one-off. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + let tabOpenPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + EventUtils.synthesizeKey("KEY_Enter", { altKey: true }); + await tabOpenPromise; + Assert.notEqual( + browser, + gBrowser.selectedBrowser, + "The current foreground tab is new." + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + Assert.equal( + browser, + gBrowser.selectedBrowser, + "We're back in the original tab." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Pressing Alt+Enter on a remote one-off with a search string and on a +// "non-empty" page should perform a search in a new foreground tab. +add_task(async function alt_enter_search_remote() { + await BrowserTestUtils.withNewTab("about:robots", async function (browser) { + await searchAndOpenPopup(SEARCH_STRING); + // Alt+Down to select the first one-off. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + let tabOpenPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "http://mochi.test:8888/?terms=foo.bar", + true + ); + EventUtils.synthesizeKey("KEY_Enter", { altKey: true }); + // This implictly checks the correct page is loaded. + let newTab = await tabOpenPromise; + Assert.equal( + newTab, + gBrowser.selectedTab, + "The current foreground tab is new." + ); + // Check search mode is not activated in the new tab. + await UrlbarTestUtils.assertSearchMode(window, null); + BrowserTestUtils.removeTab(newTab); + Assert.equal( + browser, + gBrowser.selectedBrowser, + "We're back in the original tab." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Pressing Alt+Enter on a local one-off with a search string and on a +// "non-empty" page should open search mode in a new foreground tab with the +// search string already populated. +add_task(async function alt_enter_search_local() { + await BrowserTestUtils.withNewTab("about:robots", async function (browser) { + await searchAndOpenPopup(SEARCH_STRING); + // Alt+Down to select the first local one-off. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + while ( + oneOffSearchButtons.selectedButton.id != + "urlbar-engine-one-off-item-bookmarks" + ) { + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + } + let tabOpenPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + EventUtils.synthesizeKey("KEY_Enter", { altKey: true }); + await tabOpenPromise; + Assert.notEqual( + browser, + gBrowser.selectedBrowser, + "The current foreground tab is new." + ); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal( + gURLBar.value, + SEARCH_STRING, + "The search term was duplicated to the new tab." + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + Assert.equal( + browser, + gBrowser.selectedBrowser, + "We're back in the original tab." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Accel+Clicking a one-off with an empty search string should open search mode +// in a new background tab. +add_task(async function accel_click_empty() { + await searchAndOpenPopup(""); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + + // We have to listen for the new tab using this brute force method. + // about:newtab is preloaded in the background. When about:newtab is opened, + // the cached version is shown. Since the page is already loaded, + // waitForNewTab does not detect it. It also doesn't fire the TabOpen event. + let tabCount = gBrowser.tabs.length; + let tabOpenPromise = TestUtils.waitForCondition( + () => + gBrowser.tabs.length == tabCount + 1 + ? gBrowser.tabs[gBrowser.tabs.length - 1] + : false, + "Waiting for background about:newtab to open." + ); + EventUtils.synthesizeMouseAtCenter(oneOffs[0], { accelKey: true }); + let newTab = await tabOpenPromise; + Assert.notEqual( + newTab.linkedBrowser, + gBrowser.selectedBrowser, + "The foreground tab hasn't changed." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + BrowserTestUtils.switchTab(gBrowser, newTab); + // Check the new background tab is already in search mode. + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + BrowserTestUtils.removeTab(newTab); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Accel+Clicking a remote one-off with a search string should execute a search +// in a new background tab. +add_task(async function accel_click_search_remote() { + await searchAndOpenPopup(SEARCH_STRING); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + let tabOpenPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "http://mochi.test:8888/?terms=foo.bar", + true + ); + EventUtils.synthesizeMouseAtCenter(oneOffs[0], { accelKey: true }); + // This implictly checks the correct page is loaded. + let newTab = await tabOpenPromise; + Assert.notEqual( + gBrowser.selectedTab, + newTab, + "The foreground tab hasn't changed." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + // Switch to the background tab, which is the last tab in gBrowser.tabs. + BrowserTestUtils.switchTab(gBrowser, newTab); + // Check the new background tab is not search mode. + await UrlbarTestUtils.assertSearchMode(window, null); + BrowserTestUtils.removeTab(newTab); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Accel+Clicking a local one-off with a search string should open search mode +// in a new background tab with the search string already populated. +add_task(async function accel_click_search_local() { + await searchAndOpenPopup(SEARCH_STRING); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + let oneOff; + for (oneOff of oneOffs) { + if (oneOff.id == "urlbar-engine-one-off-item-bookmarks") { + break; + } + } + let tabCount = gBrowser.tabs.length; + let tabOpenPromise = TestUtils.waitForCondition( + () => + gBrowser.tabs.length == tabCount + 1 + ? gBrowser.tabs[gBrowser.tabs.length - 1] + : false, + "Waiting for background about:newtab to open." + ); + EventUtils.synthesizeMouseAtCenter(oneOff, { accelKey: true }); + let newTab = await tabOpenPromise; + Assert.notEqual( + newTab.linkedBrowser, + gBrowser.selectedBrowser, + "The foreground tab hasn't changed." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + BrowserTestUtils.switchTab(gBrowser, newTab); + // Check the new background tab is already in search mode. + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + // Check the search string is already populated. + Assert.equal( + gURLBar.value, + SEARCH_STRING, + "The search term was duplicated to the new tab." + ); + BrowserTestUtils.removeTab(newTab); + await UrlbarTestUtils.promisePopupClose(window); +}); diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js b/browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js new file mode 100644 index 0000000000..3d68b08f73 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js @@ -0,0 +1,358 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests various actions relating to search suggestions and the one-off buttons. + */ + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; +const TEST_ENGINE2_BASENAME = "searchSuggestionEngine2.xml"; + +const serverInfo = { + scheme: "http", + host: "localhost", + port: 20709, // Must be identical to what is in searchSuggestionEngine2.xml +}; + +var gEngine; +var gEngine2; + +add_setup(async function () { + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.maxHistoricalSearchSuggestions", 2], + ], + }); + gEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + }); + gEngine2 = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE2_BASENAME, + }); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.moveEngine(gEngine2, 0); + await Services.search.moveEngine(gEngine, 0); + await Services.search.setDefault( + gEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + registerCleanupFunction(async function () { + await Services.search.setDefault( + oldDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +async function withSuggestions(testFn) { + // First run with remote suggestions, and then run with form history. + await withSuggestionOnce(false, testFn); + await withSuggestionOnce(true, testFn); +} + +async function withSuggestionOnce(useFormHistory, testFn) { + if (useFormHistory) { + // Add foofoo twice so it's more frecent so it appears first so that the + // order of form history results matches the order of remote suggestion + // results. + await UrlbarTestUtils.formHistory.add(["foofoo", "foofoo", "foobar"]); + } + await BrowserTestUtils.withNewTab(gBrowser, async () => { + let value = "foo"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + fireInputEvent: true, + }); + let index = await UrlbarTestUtils.promiseSuggestionsPresent(window); + await assertState({ + inputValue: value, + resultIndex: 0, + }); + await withHttpServer(serverInfo, () => { + return testFn(index, useFormHistory); + }); + }); + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); +} + +async function selectSecondSuggestion(index, isFormHistory) { + // Down to select the first search suggestion. + for (let i = index; i > 0; --i) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + await assertState({ + inputValue: "foofoo", + resultIndex: index, + suggestion: { + isFormHistory, + }, + }); + + // Down to select the next search suggestion. + EventUtils.synthesizeKey("KEY_ArrowDown"); + await assertState({ + inputValue: "foobar", + resultIndex: index + 1, + suggestion: { + isFormHistory, + }, + }); +} + +// Presses the Return key when a one-off is selected after selecting a search +// suggestion. +add_task(async function test_returnAfterSuggestion() { + await withSuggestions(async (index, usingFormHistory) => { + await selectSecondSuggestion(index, usingFormHistory); + + // Alt+Down to select the first one-off. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + await assertState({ + inputValue: "foobar", + resultIndex: index + 1, + oneOffIndex: 0, + suggestion: { + isFormHistory: usingFormHistory, + }, + }); + + let heuristicResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok( + !BrowserTestUtils.isVisible(heuristicResult.element.action), + "The heuristic action should not be visible" + ); + + let resultsPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await resultsPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: gEngine.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + }); +}); + +// Presses the Return key when a non-default one-off is selected after selecting +// a search suggestion. +add_task(async function test_returnAfterSuggestion_nonDefault() { + await withSuggestions(async (index, usingFormHistory) => { + await selectSecondSuggestion(index, usingFormHistory); + + // Alt+Down twice to select the second one-off. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + await assertState({ + inputValue: "foobar", + resultIndex: index + 1, + oneOffIndex: 1, + suggestion: { + isFormHistory: usingFormHistory, + }, + }); + + let resultsPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await resultsPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: gEngine2.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + }); +}); + +// Clicks a one-off engine after selecting a search suggestion. +add_task(async function test_clickAfterSuggestion() { + await withSuggestions(async (index, usingFormHistory) => { + await selectSecondSuggestion(index, usingFormHistory); + + let oneOffs = + UrlbarTestUtils.getOneOffSearchButtons(window).getSelectableButtons(true); + let resultsPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(oneOffs[1], {}); + await resultsPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: gEngine2.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + }); +}); + +// Clicks a non-default one-off engine after selecting a search suggestion. +add_task(async function test_clickAfterSuggestion_nonDefault() { + await withSuggestions(async (index, usingFormHistory) => { + await selectSecondSuggestion(index, usingFormHistory); + + let oneOffs = + UrlbarTestUtils.getOneOffSearchButtons(window).getSelectableButtons(true); + let resultsPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(oneOffs[1], {}); + await resultsPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: gEngine2.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + }); +}); + +// Selects a non-default one-off engine and then clicks a search suggestion. +add_task(async function test_selectOneOffThenSuggestion() { + await withSuggestions(async (index, usingFormHistory) => { + // Select a non-default one-off engine. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + await assertState({ + inputValue: "foo", + resultIndex: 0, + oneOffIndex: 1, + }); + + let heuristicResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok( + BrowserTestUtils.isVisible(heuristicResult.element.action), + "The heuristic action should be visible because the result is selected" + ); + + // Now click the second suggestion. + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index + 1); + // Note search history results don't change their engine when the selected + // one-off button changes! + let resultsPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + usingFormHistory + ? `http://mochi.test:8888/?terms=foobar` + : `http://localhost:20709/?terms=foobar` + ); + EventUtils.synthesizeMouseAtCenter(result.element.row, {}); + await resultsPromise; + }); +}); + +add_task(async function overridden_engine_not_reused() { + info( + "An overridden search suggestion item should not be reused by a search with another engine" + ); + await BrowserTestUtils.withNewTab(gBrowser, async () => { + let typedValue = "foo"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + fireInputEvent: true, + }); + let index = await UrlbarTestUtils.promiseSuggestionsPresent(window); + // Down to select the first search suggestion. + for (let i = index; i > 0; --i) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + await assertState({ + inputValue: "foofoo", + resultIndex: index, + suggestion: { + isFormHistory: false, + }, + }); + + // ALT+Down to select the second search engine. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + await assertState({ + inputValue: "foofoo", + resultIndex: index, + oneOffIndex: 1, + suggestion: { + isFormHistory: false, + }, + }); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + let label = result.displayed.action; + // Run again the query, check the label has been replaced. + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + fireInputEvent: true, + }); + index = await UrlbarTestUtils.promiseSuggestionsPresent(window); + await assertState({ + inputValue: "foo", + resultIndex: 0, + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + Assert.notEqual( + result.displayed.action, + label, + "The label should have been updated" + ); + }); +}); + +async function assertState({ + resultIndex, + inputValue, + oneOffIndex = -1, + suggestion = null, +}) { + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + resultIndex, + "Expected result should be selected" + ); + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtons(window).selectedButtonIndex, + oneOffIndex, + "Expected one-off should be selected" + ); + if (inputValue !== undefined) { + Assert.equal(gURLBar.value, inputValue, "Expected input value"); + } + + if (suggestion) { + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + resultIndex + ); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Result type should be SEARCH" + ); + if (suggestion.isFormHistory) { + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.HISTORY, + "Result source should be HISTORY" + ); + } else { + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.SEARCH, + "Result source should be SEARCH" + ); + } + Assert.equal( + typeof result.searchParams.suggestion, + "string", + "Result should have a suggestion" + ); + Assert.equal( + result.searchParams.suggestion, + suggestion.value || inputValue, + "Result should have the expected suggestion" + ); + } +} diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_settings.js b/browser/components/urlbar/tests/browser/browser_oneOffs_settings.js new file mode 100644 index 0000000000..b4b1e7006e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs_settings.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This tests that the settings button in the one-off buttons display correctly + * loads the search preferences. + */ + +let gMaxResults; + +add_setup(async function () { + gMaxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults"); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); + + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + + let visits = []; + for (let i = 0; i < gMaxResults; i++) { + visits.push({ + uri: makeURI("http://example.com/browser_urlbarOneOffs.js/?" + i), + // TYPED so that the visit shows up when the urlbar's drop-down arrow is + // pressed. + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }); + } + await PlacesTestUtils.addVisits(visits); +}); + +async function selectSettings(win, activateFn) { + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: "about:blank" }, + async browser => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "example.com", + }); + await UrlbarTestUtils.waitForAutocompleteResultAt(win, gMaxResults - 1); + + await UrlbarTestUtils.promisePopupClose(win, async () => { + let prefPaneLoaded = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + + activateFn(); + + await prefPaneLoaded; + }); + + Assert.equal( + win.gBrowser.contentWindow.history.state, + "paneSearch", + "Should have opened the search preferences pane" + ); + } + ); +} + +add_task(async function test_open_settings_with_enter() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + await selectSettings(win, () => { + EventUtils.synthesizeKey("KEY_ArrowUp", {}, win); + + Assert.ok( + UrlbarTestUtils.getOneOffSearchButtons( + win + ).selectedButton.classList.contains("search-setting-button"), + "Should have selected the settings button" + ); + + EventUtils.synthesizeKey("KEY_Enter", {}, win); + }); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_open_settings_with_click() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + await selectSettings(win, () => { + UrlbarTestUtils.getOneOffSearchButtons(win).settingsButton.click(); + }); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/urlbar/tests/browser/browser_pasteAndGo.js b/browser/components/urlbar/tests/browser/browser_pasteAndGo.js new file mode 100644 index 0000000000..8d2a27afc3 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_pasteAndGo.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests for the paste and go functionality of the urlbar. + */ + +add_task(async function () { + const kURLs = [ + "http://example.com/1", + "http://example.org/2\n", + "http://\nexample.com/3\n", + ]; + for (let url of kURLs) { + await BrowserTestUtils.withNewTab("about:blank", async function (browser) { + gURLBar.focus(); + + await SimpleTest.promiseClipboardChange(url, () => { + clipboardHelper.copyString(url); + }); + let menuitem = await promiseContextualMenuitem("paste-and-go"); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + url.replace(/\n/g, "") + ); + menuitem.closest("menupopup").activateItem(menuitem); + // Using toSource in order to get the newlines escaped: + info("Paste and go, loading " + url.toSource()); + await browserLoadedPromise; + ok(true, "Successfully loaded " + url); + }); + } +}); + +add_task(async function test_invisible_char() { + const url = "http://example.com/4\u2028"; + await BrowserTestUtils.withNewTab("about:blank", async function (browser) { + gURLBar.focus(); + await SimpleTest.promiseClipboardChange(url, () => { + clipboardHelper.copyString(url); + }); + let menuitem = await promiseContextualMenuitem("paste-and-go"); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + url.replace(/\u2028/g, "") + ); + menuitem.closest("menupopup").activateItem(menuitem); + // Using toSource in order to get the newlines escaped: + info("Paste and go, loading " + url.toSource()); + await browserLoadedPromise; + ok(true, "Successfully loaded " + url); + }); +}); + +add_task(async function test_with_input_and_results() { + // Test paste and go When there's some input and the results pane is open. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + const url = "http://example.com/"; + await SimpleTest.promiseClipboardChange(url, () => { + clipboardHelper.copyString(url); + }); + let menuitem = await promiseContextualMenuitem("paste-and-go"); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + url + ); + menuitem.closest("menupopup").activateItem(menuitem); + // Using toSource in order to get the newlines escaped: + info("Paste and go, loading " + url.toSource()); + await browserLoadedPromise; + ok(true, "Successfully loaded " + url); +}); diff --git a/browser/components/urlbar/tests/browser/browser_paste_multi_lines.js b/browser/components/urlbar/tests/browser/browser_paste_multi_lines.js new file mode 100644 index 0000000000..3e7732e158 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_paste_multi_lines.js @@ -0,0 +1,239 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test handling whitespace chars such as "\n”. + +const TEST_DATA = [ + { + input: "this is a\ntest", + expected: { + urlbar: "this is a test", + autocomplete: "this is a test", + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }, + }, + { + input: "this is a\n\ttest", + expected: { + urlbar: "this is a test", + autocomplete: "this is a test", + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }, + }, + { + input: "http:\n//\nexample.\ncom", + expected: { + urlbar: "http://example.com", + autocomplete: "http://example.com/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "htp:example.\ncom", + expected: { + urlbar: "htp:example.com", + autocomplete: "http://example.com/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "example.\ncom", + expected: { + urlbar: "example.com", + autocomplete: "http://example.com/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "http://example.com/foo bar/", + expected: { + urlbar: "http://example.com/foo bar/", + autocomplete: "http://example.com/foo bar/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "http://exam\nple.com/foo bar/", + expected: { + urlbar: "http://example.com/foo bar/", + autocomplete: "http://example.com/foo bar/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "javasc\nript:\nalert(1)", + expected: { + urlbar: "alert(1)", + autocomplete: "alert(1)", + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }, + }, + { + input: "a\nb\nc", + expected: { + urlbar: "a b c", + autocomplete: "a b c", + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }, + }, + { + input: "lo\ncal\nhost", + expected: { + urlbar: "localhost", + autocomplete: "http://localhost/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "data:text/html,<iframe\n src='example\n.com'>\n</iframe>", + expected: { + urlbar: "data:text/html,<iframe src='example .com'> </iframe>", + autocomplete: "data:text/html,<iframe src='example .com'> </iframe>", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "data:,123\n4 5\n6", + expected: { + urlbar: "data:,123 4 5 6", + autocomplete: "data:,123 4 5 6", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "data:text/html;base64,123\n4 5\n6", + expected: { + urlbar: "data:text/html;base64,1234 56", + autocomplete: "data:text/html;base64,123456", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "http://example.com\n", + expected: { + urlbar: "http://example.com", + autocomplete: "http://example.com/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "http://example.com\r", + expected: { + urlbar: "http://example.com", + autocomplete: "http://example.com/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "http://ex\ra\nmp\r\nle.com\r\n", + expected: { + urlbar: "http://example.com", + autocomplete: "http://example.com/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "http://example.com/titled", + expected: { + urlbar: "http://example.com/titled", + autocomplete: "example title", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "127.0.0.1\r", + expected: { + urlbar: "127.0.0.1", + autocomplete: "http://127.0.0.1/", + type: UrlbarUtils.RESULT_TYPE.URL, + }, + }, + { + input: "\r\n\r\n\r\n\r\n\r\n", + expected: { + urlbar: "", + autocomplete: "", + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }, + }, +]; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // There are cases that URLBar loses focus before assertion of this test. + // In that case, this test will be failed since the result is closed + // before it. We use this pref so that keep the result even if lose focus. + ["ui.popup.disable_autohide", true], + ], + }); + + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits({ + uri: "http://example.com/titled", + title: "example title", + }); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + SpecialPowers.clipboardCopyString(""); + }); +}); + +add_task(async function test_paste_onto_urlbar() { + for (const { input, expected } of TEST_DATA) { + gURLBar.value = ""; + gURLBar.focus(); + + await paste(input); + await assertResult(expected); + + await UrlbarTestUtils.promisePopupClose(window); + } +}); + +add_task(async function test_paste_after_opening_autocomplete_panel() { + for (const { input, expected } of TEST_DATA) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + await paste(input); + await assertResult(expected); + + await UrlbarTestUtils.promisePopupClose(window); + } +}); + +async function assertResult(expected) { + Assert.equal(gURLBar.value, expected.urlbar, "Pasted value is correct"); + + const result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.title, + expected.autocomplete, + "Title of autocomplete is correct" + ); + Assert.equal(result.type, expected.type, "Type of autocomplete is correct"); + + if (gURLBar.value) { + Assert.ok(gURLBar.hasAttribute("usertyping")); + Assert.ok(BrowserTestUtils.isVisible(gURLBar.goButton)); + } else { + Assert.ok(!gURLBar.hasAttribute("usertyping")); + Assert.ok(BrowserTestUtils.isHidden(gURLBar.goButton)); + } +} + +async function paste(input) { + await SimpleTest.promiseClipboardChange(input.replace(/\r\n?/g, "\n"), () => { + clipboardHelper.copyString(input); + }); + + document.commandDispatcher + .getControllerForCommand("cmd_paste") + .doCommand("cmd_paste"); +} diff --git a/browser/components/urlbar/tests/browser/browser_paste_then_focus.js b/browser/components/urlbar/tests/browser/browser_paste_then_focus.js new file mode 100644 index 0000000000..23d603fd80 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_paste_then_focus.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the urlbar value when focusing after pasting value. + +const TEST_DATA = [ + { + input: "this is a\ntest", + expected: "this is a test", + }, + { + input: "http:\n//\nexample.\ncom", + expected: "http://example.com", + }, + { + input: "javasc\nript:\nalert(1)", + expected: "alert(1)", + }, + { + input: "javascript:alert(1)", + expected: "alert(1)", + }, + { + input: "test", + expected: "test", + }, +]; + +add_task(async function test_paste_then_focus() { + for (const testData of TEST_DATA) { + gURLBar.value = ""; + gURLBar.focus(); + + EventUtils.synthesizeKey("x"); + gURLBar.select(); + + await paste(testData.input); + + gURLBar.blur(); + gURLBar.focus(); + + Assert.equal( + gURLBar.value, + testData.expected, + "Value on urlbar is correct" + ); + } +}); + +async function paste(input) { + await SimpleTest.promiseClipboardChange(input, () => { + clipboardHelper.copyString(input); + }); + + document.commandDispatcher + .getControllerForCommand("cmd_paste") + .doCommand("cmd_paste"); +} diff --git a/browser/components/urlbar/tests/browser/browser_paste_then_switch_tab.js b/browser/components/urlbar/tests/browser/browser_paste_then_switch_tab.js new file mode 100644 index 0000000000..09d94f79e7 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_paste_then_switch_tab.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the urlbar value when switching tab after pasting value. + +const TEST_DATA = [ + { + input: "this is a\ntest", + expected: "this is a test", + }, + { + input: "https:\n//\nexample.\ncom", + expected: UrlbarTestUtils.trimURL("https://example.com"), + }, + { + input: "http:\n//\nexample.\ncom", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + expected: UrlbarTestUtils.trimURL("http://example.com"), + }, + { + input: "javasc\nript:\nalert(1)", + expected: "alert(1)", + }, + { + input: "javascript:alert(1)", + expected: "alert(1)", + }, + { + // Has U+3000 IDEOGRAPHIC SPACE. + input: "Mozilla Firefox", + expected: "Mozilla Firefox", + }, + { + input: "test", + expected: "test", + }, +]; + +add_task(async function test_paste_then_switch_tab() { + for (const testData of TEST_DATA) { + gURLBar.focus(); + gURLBar.select(); + + await paste(testData.input); + + // Switch to a new tab. + const originalTab = gBrowser.selectedTab; + const newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await BrowserTestUtils.waitForCondition(() => !gURLBar.value); + + // Switch back to original tab. + gBrowser.selectedTab = originalTab; + + Assert.equal( + gURLBar.value, + testData.expected, + "Value on urlbar is correct" + ); + + BrowserTestUtils.removeTab(newTab); + } +}); + +async function paste(input) { + await SimpleTest.promiseClipboardChange(input, () => { + clipboardHelper.copyString(input); + }); + + document.commandDispatcher + .getControllerForCommand("cmd_paste") + .doCommand("cmd_paste"); +} diff --git a/browser/components/urlbar/tests/browser/browser_percent_encoded.js b/browser/components/urlbar/tests/browser/browser_percent_encoded.js new file mode 100644 index 0000000000..c334c03a09 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_percent_encoded.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that searching history works for both encoded or decoded strings. + +add_task(async function test() { + const decoded = "日本"; + const TEST_URL = TEST_BASE_URL + "?" + decoded; + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); + + // Visit url in a new tab, going through normal urlbar workflow. + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + let promise = PlacesTestUtils.waitForNotification("page-visited", visits => { + Assert.equal( + visits.length, + 1, + "Was notified for the right number of visits." + ); + let { url, transitionType } = visits[0]; + return ( + url == encodeURI(TEST_URL) && + transitionType == PlacesUtils.history.TRANSITIONS.TYPED + ); + }); + gURLBar.focus(); + gURLBar.value = TEST_URL; + info("Visiting url"); + EventUtils.synthesizeKey("KEY_Enter"); + await promise; + gBrowser.removeCurrentTab({ skipPermitUnload: true }); + + info("Search for the decoded string."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: decoded, + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Check number of results" + ); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, encodeURI(TEST_URL), "Check result url"); + + info("Search for the encoded string."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: encodeURIComponent(decoded), + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Check number of results" + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, encodeURI(TEST_URL), "Check result url"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_placeholder.js b/browser/components/urlbar/tests/browser/browser_placeholder.js new file mode 100644 index 0000000000..e096c6fdf6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_placeholder.js @@ -0,0 +1,412 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test ensures the placeholder is set correctly for different search + * engines. + */ + +"use strict"; + +var originalEngine, extraEngine, extraPrivateEngine, expectedString; +var tabs = []; + +var noEngineString; + +add_setup(async function () { + originalEngine = await Services.search.getDefault(); + [noEngineString, expectedString] = ( + await document.l10n.formatMessages([ + { id: "urlbar-placeholder" }, + { + id: "urlbar-placeholder-with-name", + args: { name: originalEngine.name }, + }, + ]) + ).map(msg => msg.attributes[0].value); + + let rootUrl = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://mochi.test:8888/" + ); + await SearchTestUtils.installSearchExtension({ + name: "extraEngine", + search_url: "https://mochi.test:8888/", + suggest_url: `${rootUrl}/searchSuggestionEngine.sjs`, + }); + extraEngine = Services.search.getEngineByName("extraEngine"); + await SearchTestUtils.installSearchExtension({ + name: "extraPrivateEngine", + search_url: "https://mochi.test:8888/", + suggest_url: `${rootUrl}/searchSuggestionEngine.sjs`, + }); + extraPrivateEngine = Services.search.getEngineByName("extraPrivateEngine"); + + // Force display of a tab with a URL bar, to clear out any possible placeholder + // initialization listeners that happen on startup. + let urlTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + BrowserTestUtils.removeTab(urlTab); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault", false], + ["browser.urlbar.suggest.quickactions", false], + ], + }); + + registerCleanupFunction(async () => { + await Services.search.setDefault( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } + }); +}); + +add_task(async function test_change_default_engine_updates_placeholder() { + tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser)); + + await Services.search.setDefault( + extraEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + await TestUtils.waitForCondition( + () => gURLBar.placeholder == noEngineString, + "The placeholder should match the default placeholder for non-built-in engines." + ); + Assert.equal(gURLBar.placeholder, noEngineString); + + await Services.search.setDefault( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + await TestUtils.waitForCondition( + () => gURLBar.placeholder == expectedString, + "The placeholder should include the engine name for built-in engines." + ); + Assert.equal(gURLBar.placeholder, expectedString); +}); + +add_task(async function test_delayed_update_placeholder() { + // We remove the change of engine listener here as that is set so that + // if the engine is changed by the user then the placeholder is always updated + // straight away. As we want to test the delay update here, we remove the + // listener and call the placeholder update manually with the delay flag. + Services.obs.removeObserver(BrowserSearch, "browser-search-engine-modified"); + + // Since we can't easily test for startup changes, we'll at least test the delay + // of update for the placeholder works. + let urlTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + tabs.push(urlTab); + + // Open a tab with a blank URL bar. + let blankTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + tabs.push(blankTab); + + await Services.search.setDefault( + extraEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + // Pretend we've "initialized". + BrowserSearch._updateURLBarPlaceholder(extraEngine.name, false, true); + + Assert.equal( + gURLBar.placeholder, + expectedString, + "Placeholder should be unchanged." + ); + + // Now switch to a tab with something in the URL Bar. + await BrowserTestUtils.switchTab(gBrowser, urlTab); + + await TestUtils.waitForCondition( + () => gURLBar.placeholder == noEngineString, + "The placeholder should have updated in the background." + ); + + // Do it the other way to check both named engine and fallback code paths. + await BrowserTestUtils.switchTab(gBrowser, blankTab); + + await Services.search.setDefault( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + BrowserSearch._updateURLBarPlaceholder(originalEngine.name, false, true); + + Assert.equal( + gURLBar.placeholder, + noEngineString, + "Placeholder should be unchanged." + ); + Assert.deepEqual( + document.l10n.getAttributes(gURLBar.inputField), + { id: "urlbar-placeholder", args: null }, + "Placeholder data should be unchanged." + ); + + await BrowserTestUtils.switchTab(gBrowser, urlTab); + + await TestUtils.waitForCondition( + () => gURLBar.placeholder == expectedString, + "The placeholder should include the engine name for built-in engines." + ); + + // Now check when we have a URL displayed, the placeholder is updated straight away. + BrowserSearch._updateURLBarPlaceholder(extraEngine.name, false); + + await TestUtils.waitForCondition( + () => gURLBar.placeholder == noEngineString, + "The placeholder should go back to the default" + ); + Assert.equal( + gURLBar.placeholder, + noEngineString, + "Placeholder should be the default." + ); + + Services.obs.addObserver(BrowserSearch, "browser-search-engine-modified"); +}); + +add_task(async function test_private_window_no_separate_engine() { + const win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + await Services.search.setDefault( + extraEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + await TestUtils.waitForCondition( + () => win.gURLBar.placeholder == noEngineString, + "The placeholder should match the default placeholder for non-built-in engines." + ); + Assert.equal(win.gURLBar.placeholder, noEngineString); + + await Services.search.setDefault( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + await TestUtils.waitForCondition( + () => win.gURLBar.placeholder == expectedString, + "The placeholder should include the engine name for built-in engines." + ); + Assert.equal(win.gURLBar.placeholder, expectedString); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_private_window_separate_engine() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.separatePrivateDefault", true]], + }); + const originalPrivateEngine = await Services.search.getDefaultPrivate(); + registerCleanupFunction(async () => { + await Services.search.setDefaultPrivate( + originalPrivateEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + }); + + const win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + // Keep the normal default as a different string to the private, so that we + // can be sure we're testing the right thing. + await Services.search.setDefault( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.setDefaultPrivate( + extraPrivateEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + await TestUtils.waitForCondition( + () => win.gURLBar.placeholder == noEngineString, + "The placeholder should match the default placeholder for non-built-in engines." + ); + Assert.equal(win.gURLBar.placeholder, noEngineString); + + await Services.search.setDefault( + extraEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.setDefaultPrivate( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + await TestUtils.waitForCondition( + () => win.gURLBar.placeholder == expectedString, + "The placeholder should include the engine name for built-in engines." + ); + Assert.equal(win.gURLBar.placeholder, expectedString); + + await BrowserTestUtils.closeWindow(win); + + // Verify that the placeholder for private windows is updated even when no + // private window is visible (https://bugzilla.mozilla.org/1792816). + await Services.search.setDefault( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.setDefaultPrivate( + extraPrivateEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + const win2 = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + Assert.equal(win2.gURLBar.placeholder, noEngineString); + await BrowserTestUtils.closeWindow(win2); + + // And ensure this doesn't affect the placeholder for non private windows. + tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser)); + Assert.equal(win.gURLBar.placeholder, expectedString); +}); + +add_task(async function test_search_mode_engine_web() { + // Add our test engine to WEB_ENGINE_NAMES so that it's recognized as a web + // engine. + SearchUtils.GENERAL_SEARCH_ENGINE_IDS.add( + extraEngine.wrappedJSObject._extensionID + ); + + await doSearchModeTest( + { + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + engineName: extraEngine.name, + }, + { + id: "urlbar-placeholder-search-mode-web-2", + args: { name: extraEngine.name }, + } + ); + + SearchUtils.GENERAL_SEARCH_ENGINE_IDS.delete( + extraEngine.wrappedJSObject._extensionID + ); +}); + +add_task(async function test_search_mode_engine_other() { + await doSearchModeTest( + { engineName: extraEngine.name }, + { + id: "urlbar-placeholder-search-mode-other-engine", + args: { name: extraEngine.name }, + } + ); +}); + +add_task(async function test_search_mode_bookmarks() { + await doSearchModeTest( + { source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS }, + { id: "urlbar-placeholder-search-mode-other-bookmarks", args: null } + ); +}); + +add_task(async function test_search_mode_tabs() { + await doSearchModeTest( + { source: UrlbarUtils.RESULT_SOURCE.TABS }, + { id: "urlbar-placeholder-search-mode-other-tabs", args: null } + ); +}); + +add_task(async function test_search_mode_history() { + await doSearchModeTest( + { source: UrlbarUtils.RESULT_SOURCE.HISTORY }, + { id: "urlbar-placeholder-search-mode-other-history", args: null } + ); +}); + +add_task(async function test_change_default_engine_updates_placeholder() { + tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser)); + + info(`Set engine to ${extraEngine.name}`); + await Services.search.setDefault( + extraEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await TestUtils.waitForCondition( + () => gURLBar.placeholder == noEngineString, + "The placeholder should match the default placeholder for non-built-in engines." + ); + Assert.equal(gURLBar.placeholder, noEngineString); + + info(`Set engine to ${originalEngine.name}`); + await Services.search.setDefault( + originalEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await TestUtils.waitForCondition( + () => gURLBar.placeholder == expectedString, + "The placeholder should include the engine name for built-in engines." + ); + + // Simulate the placeholder not having changed due to the delayed update + // on startup. + BrowserSearch._setURLBarPlaceholder(""); + await TestUtils.waitForCondition( + () => gURLBar.placeholder == noEngineString, + "The placeholder should have been reset." + ); + + info("Show search engine removal info bar"); + await BrowserSearch.removalOfSearchEngineNotificationBox( + extraEngine.name, + originalEngine.name + ); + const notificationBox = gNotificationBox.getNotificationWithValue( + "search-engine-removal" + ); + Assert.ok(notificationBox, "Search engine removal should be shown."); + + await TestUtils.waitForCondition( + () => gURLBar.placeholder == expectedString, + "The placeholder should include the engine name for built-in engines." + ); + + Assert.equal(gURLBar.placeholder, expectedString); + + notificationBox.close(); +}); + +/** + * Opens the view, clicks a one-off button to enter search mode, and asserts + * that the placeholder is corrrect. + * + * @param {object} expectedSearchMode + * The expected search mode object for the one-off. + * @param {object} expectedPlaceholderL10n + * The expected l10n object for the one-off. + */ +async function doSearchModeTest(expectedSearchMode, expectedPlaceholderL10n) { + // Click the urlbar to open the top-sites view. + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + + // Enter search mode. + await UrlbarTestUtils.enterSearchMode(window, expectedSearchMode); + + // Check the placeholder. + Assert.deepEqual( + document.l10n.getAttributes(gURLBar.inputField), + expectedPlaceholderL10n, + "Placeholder has expected l10n" + ); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); +} diff --git a/browser/components/urlbar/tests/browser/browser_populateAfterPushState.js b/browser/components/urlbar/tests/browser/browser_populateAfterPushState.js new file mode 100644 index 0000000000..96c43326a9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_populateAfterPushState.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* When a user clears the URL bar, and then the page pushes state, we should + * re-fill the URL bar so it doesn't remain empty indefinitely. See bug 1441039. + * For normal loads, this happens automatically because a non-same-document state + * change takes place. + */ +add_task(async function () { + await BrowserTestUtils.withNewTab( + TEST_BASE_URL + "dummy_page.html", + async function (browser) { + gURLBar.value = ""; + + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_BASE_URL + "dummy_page2.html" + ); + await SpecialPowers.spawn(browser, [], function () { + content.history.pushState({}, "Page 2", "dummy_page2.html"); + }); + await locationChangePromise; + is( + gURLBar.value, + UrlbarTestUtils.trimURL(TEST_BASE_URL + "dummy_page2.html"), + "Should have updated the URL bar." + ); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js b/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js new file mode 100644 index 0000000000..2f8e871bfe --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Verify that the primary selection is unaffected by opening a new tab. + * + * The steps here follow STR for regression + * https://bugzilla.mozilla.org/show_bug.cgi?id=1457355. + */ + +"use strict"; + +let tabs = []; +let supportsPrimary = Services.clipboard.isClipboardTypeSupported( + Services.clipboard.kSelectionClipboard +); +const NON_EMPTY_URL = "data:text/html,Hello"; +const TEXT_FOR_PRIMARY = "Text for PRIMARY selection"; + +add_task(async function () { + tabs.push( + await BrowserTestUtils.openNewForegroundTab(gBrowser, NON_EMPTY_URL) + ); + + // Bug 1457355 reproduced only when the url had a non-empty selection. + gURLBar.select(); + Assert.equal(gURLBar.selectionStart, 0); + Assert.equal(gURLBar.selectionEnd, gURLBar.value.length); + + if (supportsPrimary) { + clipboardHelper.copyStringToClipboard( + TEXT_FOR_PRIMARY, + Services.clipboard.kSelectionClipboard + ); + } + + tabs.push( + await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: () => { + // Simulate tab open from user input such as keyboard shortcut or new + // tab button. + let userInput = window.windowUtils.setHandlingUserInput(true); + try { + BrowserOpenTab(); + } finally { + userInput.destruct(); + } + }, + waitForLoad: false, + }) + ); + + if (!supportsPrimary) { + info("Primary selection not supported. Skipping assertion."); + return; + } + + let primaryAsText = SpecialPowers.getClipboardData( + "text/plain", + SpecialPowers.Ci.nsIClipboard.kSelectionClipboard + ); + Assert.equal(primaryAsText, TEXT_FOR_PRIMARY); +}); + +registerCleanupFunction(() => { + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_privateBrowsingWindowChange.js b/browser/components/urlbar/tests/browser/browser_privateBrowsingWindowChange.js new file mode 100644 index 0000000000..eeeda93687 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_privateBrowsingWindowChange.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that when opening a private browsing window and typing in it before + * about:privatebrowsing loads, we don't clear the URL bar. + */ +add_task(async function () { + let urlbarTestValue = "Mary had a little lamb"; + let win = OpenBrowserWindow({ private: true }); + registerCleanupFunction(() => BrowserTestUtils.closeWindow(win)); + await BrowserTestUtils.waitForEvent(win, "load"); + let promise = new Promise(resolve => { + let wpl = { + onLocationChange(aWebProgress, aRequest, aLocation) { + if (aLocation && aLocation.spec == "about:privatebrowsing") { + win.gBrowser.removeProgressListener(wpl); + resolve(); + } + }, + }; + win.gBrowser.addProgressListener(wpl); + }); + Assert.notEqual( + win.gBrowser.selectedBrowser.currentURI.spec, + "about:privatebrowsing", + "Check privatebrowsing page has not been loaded yet" + ); + info("Search in urlbar"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: urlbarTestValue, + fireInputEvent: true, + }); + info("waiting for about:privatebrowsing load"); + await promise; + + let urlbar = win.gURLBar; + is( + urlbar.value, + urlbarTestValue, + "URL bar value should be the same once about:privatebrowsing has loaded" + ); + is( + win.gBrowser.selectedBrowser.userTypedValue, + urlbarTestValue, + "User typed value should be the same once about:privatebrowsing has loaded" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_queryContextCache.js b/browser/components/urlbar/tests/browser/browser_queryContextCache.js new file mode 100644 index 0000000000..88409e253d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_queryContextCache.js @@ -0,0 +1,490 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the view's QueryContextCache. When the view opens and a context is +// cached for the search, the view should *synchronously* open and update. + +"use strict"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + UrlbarProviderTopSites: "resource:///modules/UrlbarProviderTopSites.sys.mjs", +}); + +const TEST_URLS = []; +const TEST_URLS_COUNT = 5; +const TOP_SITES_VISIT_COUNT = 5; +const SEARCH_STRING = "example"; + +// Allow more time for Mac machines so they don't time out in verify mode. +if (AppConstants.platform == "macosx") { + requestLongerTimeout(3); +} + +add_setup(async function () { + // Clear history and bookmarks to make sure the URLs we add below are truly + // the top sites. If any existing history or bookmarks were the top sites, + // which is likely but not guaranteed, one or more "newtab-top-sites-changed" + // notifications will be sent, potentially interfering with the rest of the + // test. Waiting for Places updates to finish and then an extra tick should be + // enough to make sure no more notfications occur. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesTestUtils.promiseAsyncUpdates(); + await TestUtils.waitForTick(); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + // Add some URLs to populate both history and top sites. Each URL needs to + // match `SEARCH_STRING`. + for (let i = 0; i < TEST_URLS_COUNT; i++) { + let url = `https://${i}.example.com/${SEARCH_STRING}`; + TEST_URLS.unshift(url); + // Each URL needs to be added several times to boost its frecency enough to + // qualify as a top site. + for (let j = 0; j < TOP_SITES_VISIT_COUNT; j++) { + await PlacesTestUtils.addVisits(url); + } + } + await updateTopSitesAndAwaitChanged(TEST_URLS_COUNT); + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function search() { + await withNewBrowserWindow(async win => { + // Do a search and then close the view. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: SEARCH_STRING, + }); + await UrlbarTestUtils.promisePopupClose(win); + + // Open the view. It should open synchronously and the cached search context + // should be used. + await openViewAndAssertCached({ + win, + searchString: SEARCH_STRING, + cached: true, + }); + }); +}); + +add_task(async function topSites_simple() { + await withNewBrowserWindow(async win => { + // Open the view to show top sites and then close it. + await openViewAndAssertCached({ win, cached: false }); + + // Open the view again. It should open synchronously and the cached + // top-sites context should be used. + await openViewAndAssertCached({ win, cached: true }); + }); +}); + +add_task(async function topSites_nonEmptySearch() { + await withNewBrowserWindow(async win => { + // Open the view to show top sites and then close it. + await openViewAndAssertCached({ win, cached: false }); + + // Do a search, close the view, and revert the input. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "test", + }); + await UrlbarTestUtils.promisePopupClose(win); + win.gURLBar.handleRevert(); + + // Open the view. It should open synchronously and the cached top-sites + // context should be used. + await openViewAndAssertCached({ win, cached: true }); + }); +}); + +add_task(async function topSites_otherEmptySearch() { + await withNewBrowserWindow(async win => { + // Open the view to show top sites and then close it. + await openViewAndAssertCached({ win, cached: false }); + + // Enter search mode with an empty search string (by pressing accel+K), + // starting a new search. The view should *not* open synchronously and the + // cached top-sites context should not be used. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("k", { accelKey: true }, win); + Assert.ok(!win.gURLBar.view.isOpen, "View is not open"); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(win, { + engineName: Services.search.defaultEngine.name, + isGeneralPurposeEngine: true, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + isPreview: false, + entry: "shortcut", + }); + + // Close the view and revert the input. + await UrlbarTestUtils.promisePopupClose(win); + win.gURLBar.handleRevert(); + await UrlbarTestUtils.assertSearchMode(win, null); + + // Open the view. It should open synchronously and the cached top-sites + // context should be used. + await openViewAndAssertCached({ win, cached: true }); + }); +}); + +add_task(async function topSites_changed() { + await withNewBrowserWindow(async win => { + // Open the view to show top sites and then close it. + await openViewAndAssertCached({ win, cached: false }); + + // Change the top sites by adding visits to a new URL. + let newURL = "https://changed.example.com/"; + for (let j = 0; j < TOP_SITES_VISIT_COUNT; j++) { + await PlacesTestUtils.addVisits(newURL); + } + await updateTopSitesAndAwaitChanged(TEST_URLS_COUNT + 1); + + // Open the view. It should *not* open synchronously and the cached + // top-sites context should not be used. + await openViewAndAssertCached({ win, cached: false }); + + // Open the view again. It should open synchronously and the new cached + // top-sites context with the new URL should be used. + await openViewAndAssertCached({ + win, + cached: true, + urls: [newURL, ...TEST_URLS], + // The new URL is sometimes at the end of the list of top sites instead of + // the start, so ignore the order of the results. + ignoreOrder: true, + }); + + // Remove the new URL. The top sites will update themselves automatically, + // so we only need to wait for newtab-top-sites-changed. + info("Removing new URL and awaiting newtab-top-sites-changed"); + let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed"); + await PlacesUtils.history.remove([newURL]); + await changedPromise; + + // Open the view. It should *not* open synchronously and the cached + // top-sites context should not be used. + await openViewAndAssertCached({ win, cached: false }); + + // Open the view again. It should open synchronously and the new cached + // top-sites context with the new URL should be used. + await openViewAndAssertCached({ win, cached: true }); + }); +}); + +add_task(async function topSites_nonTopSitesResults() { + await withNewBrowserWindow(async win => { + // Open the view to show top sites and then close it. + await openViewAndAssertCached({ win, cached: false }); + + // Add a provider that returns a result with a suggested index of zero so + // that the first result in the view is not from the top-sites provider. + let suggestedIndexURL = "https://example.com/suggested-index-0"; + let provider = new UrlbarTestUtils.TestProvider({ + priority: lazy.UrlbarProviderTopSites.PRIORITY, + results: [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url: suggestedIndexURL, + } + ), + { suggestedIndex: 0 } + ), + ], + }); + UrlbarProvidersManager.registerProvider(provider); + + // Open the view. It should open synchronously and the cached top-sites + // context should be used. The suggested-index result should not be + // immediately present in the view since it's not in the cached context. + await openViewAndAssertCached({ win, cached: true, keepOpen: true }); + + // After the search has finished, the suggested-index result should be in + // the first row. The search's context should become the newly cached + // top-sites context and it should include the suggested-index result. + Assert.equal( + UrlbarTestUtils.getResultCount(win), + TEST_URLS.length + 1, + "Should be one more result after search finishes" + ); + let details = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + Assert.equal( + details.url, + suggestedIndexURL, + "First result after search finishes should be the suggested index result" + ); + + // At this point, the search's context should have become the newly cached + // top-sites context and it should include the suggested-index result. + + await UrlbarTestUtils.promisePopupClose(win); + + // Open the view again. It should open synchronously and the new cached + // top-sites context with the suggested-index URL should be used. + await openViewAndAssertCached({ + win, + cached: true, + urls: [suggestedIndexURL, ...TEST_URLS], + }); + + UrlbarProvidersManager.unregisterProvider(provider); + }); +}); + +add_task(async function topSites_disabled_1() { + await withNewBrowserWindow(async win => { + // Open the view to show top sites and then close it. + await openViewAndAssertCached({ win, cached: false }); + + // Disable `browser.urlbar.suggest.topsites`. + UrlbarPrefs.set("suggest.topsites", false); + + // Open the view. It should *not* open synchronously and the cached + // top-sites context should not be used. + await openViewAndAssertCached({ + win, + cached: false, + cachedAfterOpen: false, + }); + + // Clear the pref, open the view to show top sites, and close it. + UrlbarPrefs.clear("suggest.topsites"); + await openViewAndAssertCached({ win, cached: false }); + + // Open the view. It should open synchronously and the cached top-sites + // context should be used. + await openViewAndAssertCached({ win, cached: true }); + }); +}); + +add_task(async function topSites_disabled_2() { + await withNewBrowserWindow(async win => { + // Open the view to show top sites and then close it. + await openViewAndAssertCached({ win, cached: false }); + + // Disable `browser.newtabpage.activity-stream.feeds.system.topsites`. + Services.prefs.setBoolPref( + "browser.newtabpage.activity-stream.feeds.system.topsites", + false + ); + + // Open the view. It should *not* open synchronously and the cached + // top-sites context should not be used. + await openViewAndAssertCached({ + win, + cached: false, + cachedAfterOpen: false, + }); + + // Clear the pref, open the view to show top sites, and close it. + Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.feeds.system.topsites" + ); + await openViewAndAssertCached({ win, cached: false }); + + // Open the view. It should open synchronously and the cached top-sites + // context should be used. + await openViewAndAssertCached({ win, cached: true }); + }); +}); + +add_task(async function evict() { + await withNewBrowserWindow(async win => { + let cache = win.gURLBar.view.queryContextCache; + Assert.equal( + typeof cache.size, + "number", + "Sanity check: queryContextCache.size is a number" + ); + + // Open the view to show top sites and then close it. + await openViewAndAssertCached({ win, cached: false }); + + // Do `cache.size` + 1 searches. + for (let i = 0; i < cache.size + 1; i++) { + let searchString = "test" + i; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: searchString, + }); + await UrlbarTestUtils.promisePopupClose(win); + Assert.ok( + cache.get(searchString), + "Cache includes search string: " + searchString + ); + } + + // The first search string should have been evicted from the cache, but the + // one after that should still be cached. + Assert.ok(!cache.get("test0"), "test0 has been evicted from the cache"); + Assert.ok(cache.get("test1"), "Cache includes test1"); + + // Revert the input and open the view to show the top sites. It should open + // synchronously and the cached top-sites context should be used. + win.gURLBar.handleRevert(); + Assert.equal(win.gURLBar.value, "", "Input is empty after reverting"); + await openViewAndAssertCached({ win, cached: true }); + }); +}); + +/** + * Opens the view and checks that it is or is not synchronously opened and + * populated as specified. + * + * @param {object} options + * Options object. + * @param {window} options.win + * The window to open the view in. + * @param {boolean} options.cached + * Whether a query context is expected to already be cached for the search + * that's performed when the view opens. If true, then the view should + * synchronously open and populate using the cached context. If false, then + * the view should asynchronously open once the first results are fetched. + * @param {boolean} [options.cachedAfterOpen] + * Whether the context is expected to be cached after the view opens and the + * query finishes. + * @param {string} [options.searchString] + * The search string for which the context should or should not be cached. If + * falsey, then the relevant context is assumed to be the top-sites context. + * @param {Array} [options.urls] + * Array of URLs that are expected to be shown in the view. + * @param {boolean} [options.ignoreOrder] + * Whether to treat `urls` as an unordered set instead of an array. When true, + * the order of results is ignored. + * @param {boolean} [options.keepOpen] + * Whether to keep the view open when the function returns. + */ +async function openViewAndAssertCached({ + win, + cached, + cachedAfterOpen = true, + searchString = "", + urls = TEST_URLS, + ignoreOrder = false, + keepOpen = false, +}) { + let cache = win.gURLBar.view.queryContextCache; + let getContext = () => + searchString ? cache.get(searchString) : cache.topSitesContext; + + let cachedContext = getContext(); + Assert.equal( + !!cachedContext, + cached, + "Context is present or not in cache as expected for search string: " + + JSON.stringify(searchString) + ); + // Our payload schema validator allows for explicit undefined properties, + // thus we must transform them for stringify. + Assert.deepEqual( + cachedContext, + JSON.parse(JSON.stringify(cachedContext, (k, v) => v ?? null)), + "The query context should be made of serializable properties" + ); + + // Open the view by performing the accel+L command. + await SimpleTest.promiseFocus(win); + win.document.getElementById("Browser:OpenLocation").doCommand(); + + Assert.equal( + win.gURLBar.view.isOpen, + cached, + "View is open or not as expected" + ); + + if (!cached && cachedAfterOpen) { + // Wait for the search to finish and the context to be cached since callers + // generally expect it. + await TestUtils.waitForCondition( + getContext, + "Waiting for context to be cached for search string: " + + JSON.stringify(searchString) + ); + } else if (cached) { + // The view is expected to open synchronously. Check the results. We don't + // do this in the `!cached` case, when the view is expected to open + // asynchronously, because there are plenty of other tests for that. Here we + // want to make sure results are correct before the new search finishes in + // order to avoid any flicker. + let startIndex = 0; + let resultCount = urls.length; + if (searchString) { + // Plus heuristic + startIndex++; + resultCount++; + } + + // In all the checks below, check the rows container directly instead of + // relying on `UrlbarTestUtils` functions that wait for the search to + // finish. Here we're specifically checking cached results that should be + // used before the search finishes. + let rows = UrlbarTestUtils.getResultsContainer(win).children; + Assert.equal(rows.length, resultCount, "View has expected row count"); + + // Check the search heuristic row. + if (searchString) { + let result = rows[0].result; + Assert.ok(result.heuristic, "First row should be a heuristic"); + Assert.equal( + result.payload.query, + searchString, + "First row's query should be the search string" + ); + } + + // Check the URL rows. + let actualURLs = []; + let urlRows = Array.from(rows).slice(startIndex); + for (let row of urlRows) { + actualURLs.push(row.result.payload.url); + } + if (ignoreOrder) { + urls.sort(); + actualURLs.sort(); + } + Assert.deepEqual(actualURLs, urls, "View should contain the expected URLs"); + } + + // Now wait for the search to finish before returning. We await + // `lastQueryContextPromise` instead of the promise returned from + // `UrlbarTestUtils.promiseSearchComplete()` because the latter assumes the + // view will open, which isn't the case for every task here. + await win.gURLBar.lastQueryContextPromise; + if (!keepOpen) { + await UrlbarTestUtils.promisePopupClose(win); + } +} + +/** + * Updates the top sites and waits for the "newtab-top-sites-changed" + * notification. Note that this notification is not sent if the sites don't + * actually change. In that case, use only `updateTopSites()` instead. + * + * @param {number} expectedCount + * The new expected number of top sites. + */ +async function updateTopSitesAndAwaitChanged(expectedCount) { + info("Updating top sites and awaiting newtab-top-sites-changed"); + let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed").then( + () => info("Observed newtab-top-sites-changed") + ); + await updateTopSites(sites => sites?.length == expectedCount); + await changedPromise; +} + +async function withNewBrowserWindow(callback) { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await callback(win); + await BrowserTestUtils.closeWindow(win); +} diff --git a/browser/components/urlbar/tests/browser/browser_quickactions.js b/browser/components/urlbar/tests/browser/browser_quickactions.js new file mode 100644 index 0000000000..ccf045d9e8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_quickactions.js @@ -0,0 +1,737 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test QuickActions. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + UpdateService: "resource://gre/modules/UpdateService.sys.mjs", + UrlbarProviderQuickActions: + "resource:///modules/UrlbarProviderQuickActions.sys.mjs", +}); + +const DUMMY_PAGE = + "http://example.com/browser/browser/base/content/test/general/dummy_page.html"; + +let testActionCalled = 0; + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.quickactions.enabled", true], + ["browser.urlbar.suggest.quickactions", true], + ["browser.urlbar.shortcuts.quickactions", true], + ], + }); + + UrlbarProviderQuickActions.addAction("testaction", { + commands: ["testaction"], + label: "quickactions-downloads2", + onPick: () => testActionCalled++, + }); + + registerCleanupFunction(() => { + UrlbarProviderQuickActions.removeAction("testaction"); + }); +}); + +add_task(async function basic() { + info("The action isnt shown when not matched"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "nomatch", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "We did no match anything" + ); + + info("A prefix of the command matches"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "testact", + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "We matched the action" + ); + + info("The callback of the action is fired when selected"); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + Assert.equal(testActionCalled, 1, "Test actionwas called"); +}); + +add_task(async function test_label_command() { + info("A prefix of the label matches"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "View Dow", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "We matched the action" + ); + + let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC); + Assert.equal(result.providerName, "quickactions"); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); +}); + +add_task(async function enter_search_mode_button() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + + await clickQuickActionOneoffButton(); + + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + Assert.ok(true, "Actions are shown when we enter actions search mode."); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Escape"); +}); + +add_task(async function enter_search_mode_oneoff_by_key() { + // Select actions oneoff button by keyboard. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.enterSearchMode(window); + const oneOffButtons = UrlbarTestUtils.getOneOffSearchButtons(window); + for (;;) { + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + if ( + oneOffButtons.selectedButton.source === UrlbarUtils.RESULT_SOURCE.ACTIONS + ) { + break; + } + } + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: " ", + }); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.ACTIONS, + entry: "oneoff", + }); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Escape"); +}); + +add_task(async function enter_search_mode_key() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "> ", + }); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.ACTIONS, + entry: "typed", + }); + Assert.equal( + await hasQuickActions(window), + true, + "Actions are shown in search mode" + ); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Escape"); +}); + +add_task(async function test_disabled() { + UrlbarProviderQuickActions.addAction("disabledaction", { + commands: ["disabledaction"], + isActive: () => false, + label: "quickactions-restart", + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "disabled", + }); + + Assert.equal( + await hasQuickActions(window), + false, + "Result for quick actions is hidden" + ); + + await UrlbarTestUtils.promisePopupClose(window); + UrlbarProviderQuickActions.removeAction("disabledaction"); +}); + +/** + * The first part of this test confirms that when the screenshots component is enabled + * the screenshot quick action button will be enabled on about: pages. + * The second part confirms that when the screenshots extension is enabled the + * screenshot quick action button will be disbaled on about: pages. + */ +add_task(async function test_screenshot_enabled_or_disabled() { + let onLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "about:blank" + ); + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + "about:blank" + ); + await onLoaded; + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "screenshot", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "The action is displayed" + ); + let screenshotButton = window.document.querySelector( + ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button" + ); + Assert.ok( + !screenshotButton.hasAttribute("disabled"), + "Screenshot button is enabled on about pages" + ); + + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Escape"); + + await SpecialPowers.pushPrefEnv({ + set: [["screenshots.browser.component.enabled", false]], + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "screenshot", + }); + Assert.equal( + await hasQuickActions(window), + false, + "Result for quick actions is hidden" + ); + + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function match_in_phrase() { + UrlbarProviderQuickActions.addAction("newtestaction", { + commands: ["matchingstring"], + label: "quickactions-downloads2", + }); + + info("The action is matched when at end of input"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "Test we match at end of matchingstring", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "We matched the action" + ); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Escape"); + UrlbarProviderQuickActions.removeAction("newtestaction"); +}); + +add_task(async function test_other_search_mode() { + let defaultEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + }); + defaultEngine.alias = "testalias"; + let oldDefaultEngine = await Services.search.getDefault(); + Services.search.setDefault( + defaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: defaultEngine.alias + " ", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 0, + "The results should be empty as no actions are displayed in other search modes" + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: defaultEngine.name, + entry: "typed", + }); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); + Services.search.setDefault( + oldDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); +}); + +add_task(async function test_no_quickactions_suggestions() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quickactions", false], + ["screenshots.browser.component.enabled", true], + ], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "screenshot", + }); + Assert.ok( + !window.document.querySelector( + ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button" + ), + "Screenshot button is not suggested" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "> screenshot", + }); + Assert.ok( + window.document.querySelector( + ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button" + ), + "Screenshot button is suggested" + ); + + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Escape"); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_quickactions_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.quickactions.enabled", false], + ["browser.urlbar.suggest.quickactions", true], + ], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "screenshot", + }); + + Assert.ok( + !window.document.querySelector( + ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button" + ), + "Screenshot button is not suggested" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "> screenshot", + }); + Assert.ok( + !window.document.querySelector( + ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button" + ), + "Screenshot button is not suggested" + ); + + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Escape"); + + await SpecialPowers.popPrefEnv(); +}); + +let COMMANDS_TESTS = [ + { + cmd: "add-ons", + uri: "about:addons", + testFun: async () => isSelected("button[name=discover]"), + }, + { + cmd: "plugins", + uri: "about:addons", + testFun: async () => isSelected("button[name=plugin]"), + }, + { + cmd: "extensions", + uri: "about:addons", + testFun: async () => isSelected("button[name=extension]"), + }, + { + cmd: "themes", + uri: "about:addons", + testFun: async () => isSelected("button[name=theme]"), + }, + { + cmd: "add-ons", + setup: async () => { + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "http://example.com/" + ); + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + "http://example.com/" + ); + await onLoad; + }, + uri: "about:addons", + isNewTab: true, + testFun: async () => isSelected("button[name=discover]"), + }, + { + cmd: "plugins", + setup: async () => { + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "http://example.com/" + ); + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + "http://example.com/" + ); + await onLoad; + }, + uri: "about:addons", + isNewTab: true, + testFun: async () => isSelected("button[name=plugin]"), + }, + { + cmd: "extensions", + setup: async () => { + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "http://example.com/" + ); + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + "http://example.com/" + ); + await onLoad; + }, + uri: "about:addons", + isNewTab: true, + testFun: async () => isSelected("button[name=extension]"), + }, + { + cmd: "themes", + setup: async () => { + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "http://example.com/" + ); + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + "http://example.com/" + ); + await onLoad; + }, + uri: "about:addons", + isNewTab: true, + testFun: async () => isSelected("button[name=theme]"), + }, +]; + +let isSelected = async selector => + SpecialPowers.spawn(gBrowser.selectedBrowser, [selector], arg => { + return ContentTaskUtils.waitForCondition(() => + content.document.querySelector(arg)?.hasAttribute("selected") + ); + }); + +add_task(async function test_pages() { + for (const { cmd, uri, setup, isNewTab, testFun } of COMMANDS_TESTS) { + info(`Testing ${cmd} command is triggered`); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + if (setup) { + info("Setup"); + await setup(); + } + + let onLoad = isNewTab + ? BrowserTestUtils.waitForNewTab(gBrowser, uri, true) + : BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, uri); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: cmd, + }); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + + const newTab = await onLoad; + + Assert.ok( + await testFun(), + `The command "${cmd}" passed completed its test` + ); + + if (isNewTab) { + await BrowserTestUtils.removeTab(newTab); + } + await BrowserTestUtils.removeTab(tab); + } +}); + +const assertActionButtonStatus = async (name, expectedEnabled, description) => { + await BrowserTestUtils.waitForCondition(() => + window.document.querySelector(`[data-key=${name}]`) + ); + const target = window.document.querySelector(`[data-key=${name}]`); + Assert.equal(!target.hasAttribute("disabled"), expectedEnabled, description); +}; + +add_task(async function test_viewsource() { + info("Check the button status of when the page is not web content"); + const tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:home", + waitForLoad: true, + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "viewsource", + }); + await assertActionButtonStatus( + "viewsource", + true, + "Should be enabled even if the page is not web content" + ); + + info("Check the button status of when the page is web content"); + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + "http://example.com" + ); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "viewsource", + }); + await assertActionButtonStatus( + "viewsource", + true, + "Should be enabled on web content as well" + ); + + info("Do view source action"); + const onLoad = BrowserTestUtils.waitForNewTab( + gBrowser, + "view-source:http://example.com/" + ); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + const viewSourceTab = await onLoad; + + info("Do view source action on the view-source page"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "viewsource", + }); + + Assert.equal( + await hasQuickActions(window), + false, + "Result for quick actions is hidden" + ); + + // Clean up. + BrowserTestUtils.removeTab(viewSourceTab); + BrowserTestUtils.removeTab(tab); +}); + +async function doAlertDialogTest({ input, dialogContentURI }) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: input, + }); + + const onDialog = BrowserTestUtils.promiseAlertDialog(null, null, { + isSubDialog: true, + callback: win => { + Assert.equal(win.location.href, dialogContentURI, "The dialog is opened"); + EventUtils.synthesizeKey("KEY_Escape", {}, win); + }, + }); + + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + + await onDialog; +} + +add_task(async function test_refresh() { + await doAlertDialogTest({ + input: "refresh", + dialogContentURI: "chrome://global/content/resetProfile.xhtml", + }); +}); + +add_task(async function test_clear() { + let useOldClearHistoryDialog = Services.prefs.getBoolPref( + "privacy.sanitize.useOldClearHistoryDialog" + ); + let dialogURL = useOldClearHistoryDialog + ? "chrome://browser/content/sanitize.xhtml" + : "chrome://browser/content/sanitize_v2.xhtml"; + await doAlertDialogTest({ + input: "clear", + dialogContentURI: dialogURL, + }); +}); + +async function doUpdateActionTest(isActiveExpected, description) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "update", + }); + + if (isActiveExpected) { + await assertActionButtonStatus("update", isActiveExpected, description); + } else { + Assert.equal(await hasQuickActions(window), false, description); + } +} + +add_task(async function test_update() { + if (!AppConstants.MOZ_UPDATER) { + await doUpdateActionTest( + false, + "Should be disabled since not AppConstants.MOZ_UPDATER" + ); + return; + } + + const sandbox = sinon.createSandbox(); + try { + sandbox + .stub(UpdateService.prototype, "currentState") + .get(() => Ci.nsIApplicationUpdateService.STATE_IDLE); + await doUpdateActionTest( + false, + "Should be disabled since current update state is not pending" + ); + sandbox + .stub(UpdateService.prototype, "currentState") + .get(() => Ci.nsIApplicationUpdateService.STATE_PENDING); + await doUpdateActionTest( + true, + "Should be enabled since current update state is pending" + ); + } finally { + sandbox.restore(); + } +}); + +async function hasQuickActions(win) { + for (let i = 0, count = UrlbarTestUtils.getResultCount(win); i < count; i++) { + const { result } = await UrlbarTestUtils.getDetailsOfResultAt(win, i); + if (result.providerName === "quickactions") { + return true; + } + } + return false; +} + +add_task(async function test_show_in_zero_prefix() { + for (const minimumSearchString of [0, 3]) { + info( + `Test when quickactions.minimumSearchString pref is ${minimumSearchString}` + ); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.urlbar.quickactions.minimumSearchString", + minimumSearchString, + ], + ], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + Assert.equal( + await hasQuickActions(window), + !minimumSearchString, + "Result for quick actions is as expected" + ); + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function test_whitespace() { + info("Test with quickactions.showInZeroPrefix pref is false"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.quickactions.showInZeroPrefix", false]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: " ", + }); + Assert.equal( + await hasQuickActions(window), + false, + "Result for quick actions is not shown" + ); + await SpecialPowers.popPrefEnv(); + + info("Test with quickactions.showInZeroPrefix pref is true"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.quickactions.showInZeroPrefix", true]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + const countForEmpty = window.document.querySelectorAll( + ".urlbarView-quickaction-button" + ).length; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: " ", + }); + const countForWhitespace = window.document.querySelectorAll( + ".urlbarView-quickaction-button" + ).length; + Assert.equal( + countForEmpty, + countForWhitespace, + "Count of quick actions of empty and whitespace are same" + ); + await SpecialPowers.popPrefEnv(); +}); + +async function clickQuickActionOneoffButton() { + const oneOffButton = await TestUtils.waitForCondition(() => + window.document.getElementById("urlbar-engine-one-off-item-actions") + ); + Assert.ok(oneOffButton, "One off button is available when preffed on"); + + EventUtils.synthesizeMouseAtCenter(oneOffButton, {}, window); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.ACTIONS, + entry: "oneoff", + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_quickactions_devtools.js b/browser/components/urlbar/tests/browser/browser_quickactions_devtools.js new file mode 100644 index 0000000000..1e1e92fb31 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_quickactions_devtools.js @@ -0,0 +1,176 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests QuickActions related to DevTools. + */ + +"use strict"; + +requestLongerTimeout(2); + +ChromeUtils.defineESModuleGetters(this, { + DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs", +}); + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.quickactions.enabled", true], + ["browser.urlbar.suggest.quickactions", true], + ["browser.urlbar.shortcuts.quickactions", true], + ], + }); +}); + +const assertActionButtonStatus = async (name, expectedEnabled, description) => { + await BrowserTestUtils.waitForCondition(() => + window.document.querySelector(`[data-key=${name}]`) + ); + const target = window.document.querySelector(`[data-key=${name}]`); + Assert.equal(!target.hasAttribute("disabled"), expectedEnabled, description); +}; + +async function hasQuickActions(win) { + for (let i = 0, count = UrlbarTestUtils.getResultCount(win); i < count; i++) { + const { result } = await UrlbarTestUtils.getDetailsOfResultAt(win, i); + if (result.providerName === "quickactions") { + return true; + } + } + return false; +} + +add_task(async function test_inspector() { + const testData = [ + { + description: "Test for 'about:' page", + page: "about:home", + isDevToolsUser: true, + actionVisible: true, + actionEnabled: true, + }, + { + description: "Test for another 'about:' page", + page: "about:about", + isDevToolsUser: true, + actionVisible: true, + actionEnabled: true, + }, + { + description: "Test for another devtools-toolbox page", + page: "about:devtools-toolbox", + isDevToolsUser: true, + actionVisible: true, + actionEnabled: false, + }, + { + description: "Test for web content", + page: "https://example.com", + isDevToolsUser: true, + actionVisible: true, + actionEnabled: true, + }, + { + description: "Test for disabled DevTools", + page: "https://example.com", + prefs: [["devtools.policy.disabled", true]], + isDevToolsUser: true, + actionVisible: true, + actionEnabled: false, + }, + { + description: "Test for not DevTools user", + page: "https://example.com", + isDevToolsUser: false, + actionVisible: true, + actionEnabled: false, + }, + { + description: "Test for fully disabled", + page: "https://example.com", + prefs: [["devtools.policy.disabled", true]], + isDevToolsUser: false, + actionVisible: false, + }, + ]; + + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + for (const { + description, + page, + prefs = [], + isDevToolsUser, + actionEnabled, + actionVisible, + } of testData) { + info(description); + + info("Set preferences"); + await SpecialPowers.pushPrefEnv({ + set: [...prefs, ["devtools.selfxss.count", isDevToolsUser ? 5 : 0]], + }); + + info("Check the button status"); + const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, page); + await onLoad; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "inspector", + }); + + if (actionVisible && actionEnabled) { + await assertActionButtonStatus( + "inspect", + true, + "The status of action button is correct" + ); + } else { + Assert.equal( + await hasQuickActions(window), + false, + "Result for quick actions is not shown since the inspector tool is disabled" + ); + } + + await SpecialPowers.popPrefEnv(); + + if (!actionVisible || !actionEnabled) { + continue; + } + + info("Do inspect action"); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + await BrowserTestUtils.waitForCondition( + () => DevToolsShim.hasToolboxForTab(gBrowser.selectedTab), + "Wait for opening inspector for current selected tab" + ); + const toolbox = DevToolsShim.getToolboxForTab(gBrowser.selectedTab); + await BrowserTestUtils.waitForCondition( + () => toolbox.getPanel("inspector"), + "Wait until the inspector is ready" + ); + + info("Do inspect action again in the same page during opening inspector"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "inspector", + }); + Assert.equal( + await hasQuickActions(window), + false, + "Result for quick actions is not shown since the inspector is already opening" + ); + + info( + "Select another tool to check whether the inspector will be selected in next test even if the previous tool is not inspector" + ); + await toolbox.selectTool("options"); + await toolbox.destroy(); + } + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_quickactions_screenshot.js b/browser/components/urlbar/tests/browser/browser_quickactions_screenshot.js new file mode 100644 index 0000000000..c81442f0f5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_quickactions_screenshot.js @@ -0,0 +1,170 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * QuickActions tests that touch screenshot functionality. + */ + +"use strict"; + +requestLongerTimeout(3); + +const DUMMY_PAGE = + "https://example.com/browser/browser/base/content/test/general/dummy_page.html"; + +async function isScreenshotInitialized() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + let screenshotsChild = content.windowGlobalChild.getActor( + "ScreenshotsComponent" + ); + return screenshotsChild?.overlay?.initialized; + }); +} + +async function clickQuickActionOneoffButton() { + const oneOffButton = await TestUtils.waitForCondition(() => + window.document.getElementById("urlbar-engine-one-off-item-actions") + ); + Assert.ok(oneOffButton, "One off button is available when preffed on"); + + EventUtils.synthesizeMouseAtCenter(oneOffButton, {}, window); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.ACTIONS, + entry: "oneoff", + }); +} + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.quickactions.enabled", true], + ["browser.urlbar.suggest.quickactions", true], + ["browser.urlbar.shortcuts.quickactions", true], + ], + }); +}); + +add_task(async function test_screenshot() { + await SpecialPowers.pushPrefEnv({ + set: [["screenshots.browser.component.enabled", true]], + }); + + BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, DUMMY_PAGE); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + DUMMY_PAGE + ); + + info("The action is matched when at end of input"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "screenshot", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "We matched the action" + ); + let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC); + Assert.equal(result.providerName, "quickactions"); + + info("Trigger the screenshot mode"); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + await TestUtils.waitForCondition( + isScreenshotInitialized, + "Screenshot component is active", + 200, + 100 + ); + + info("Press Escape to exit screenshot mode"); + EventUtils.synthesizeKey("KEY_Escape", {}, window); + await TestUtils.waitForCondition( + async () => !(await isScreenshotInitialized()), + "Screenshot component has been dismissed" + ); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); +}); + +add_task(async function search_mode_on_webpage() { + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com" + ); + + info("Show result by click"); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}, window); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + + info("Enter quick action search mode"); + await clickQuickActionOneoffButton(); + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + Assert.ok(true, "Actions are shown when we enter actions search mode."); + + info("Trigger the screenshot mode"); + const initialActionButtons = window.document.querySelectorAll( + ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button" + ); + let screenshotButton; + for (let i = 0; i < initialActionButtons.length; i++) { + const item = initialActionButtons.item(i); + if (item.dataset.key === "screenshot") { + screenshotButton = item; + break; + } + } + EventUtils.synthesizeMouseAtCenter(screenshotButton, {}, window); + await TestUtils.waitForCondition( + isScreenshotInitialized, + "Screenshot component is active", + 200, + 100 + ); + + info("Press Escape to exit screenshot mode"); + EventUtils.synthesizeKey("KEY_Escape", {}, window); + await TestUtils.waitForCondition( + async () => !(await isScreenshotInitialized()), + "Screenshot component has been dismissed" + ); + + info("Check the urlbar state"); + Assert.equal(gURLBar.value, UrlbarTestUtils.trimURL("https://example.com")); + Assert.equal(gURLBar.getAttribute("pageproxystate"), "valid"); + + info("Show result again"); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}, window); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + + info("Enter quick action search mode again"); + await clickQuickActionOneoffButton(); + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + const finalActionButtons = window.document.querySelectorAll( + ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button" + ); + + info("Check the action buttons and the urlbar"); + Assert.equal( + finalActionButtons.length, + initialActionButtons.length, + "The same buttons as initially displayed will display" + ); + Assert.equal(gURLBar.value, ""); + + info("Clean up"); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Escape"); + BrowserTestUtils.removeTab(tab); + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_quickactions_tab_refocus.js b/browser/components/urlbar/tests/browser/browser_quickactions_tab_refocus.js new file mode 100644 index 0000000000..abac861931 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_quickactions_tab_refocus.js @@ -0,0 +1,194 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests for QuickActions that re-focus tab.. + */ + +"use strict"; + +requestLongerTimeout(3); + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.quickactions.enabled", true], + ["browser.urlbar.suggest.quickactions", true], + ["browser.urlbar.shortcuts.quickactions", true], + ], + }); +}); + +let isSelected = async selector => + SpecialPowers.spawn(gBrowser.selectedBrowser, [selector], arg => { + return ContentTaskUtils.waitForCondition(() => + content.document.querySelector(arg)?.hasAttribute("selected") + ); + }); + +add_task(async function test_about_pages() { + const testData = [ + { + firstInput: "downloads", + uri: "about:downloads", + }, + { + firstInput: "logins", + uri: "about:logins", + }, + { + firstInput: "settings", + uri: "about:preferences", + }, + { + firstInput: "add-ons", + uri: "about:addons", + component: "button[name=discover]", + }, + { + firstInput: "extensions", + uri: "about:addons", + component: "button[name=extension]", + }, + { + firstInput: "plugins", + uri: "about:addons", + component: "button[name=plugin]", + }, + { + firstInput: "themes", + uri: "about:addons", + component: "button[name=theme]", + }, + { + firstLoad: "about:preferences#home", + secondInput: "settings", + uri: "about:preferences#home", + }, + ]; + + for (const { + firstInput, + firstLoad, + secondInput, + uri, + component, + } of testData) { + info("Setup initial state"); + let firstTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + let onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + uri + ); + if (firstLoad) { + info("Load initial URI"); + BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, uri); + } else { + info("Open about page by quick action"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: firstInput, + }); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + } + await onLoad; + + if (component) { + info("Check whether the component is in the page"); + Assert.ok(await isSelected(component), "There is expected component"); + } + + info("Do the second quick action in second tab"); + let secondTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: secondInput || firstInput, + }); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + Assert.equal( + gBrowser.selectedTab, + firstTab, + "Switched to the tab that is opening the about page" + ); + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + uri, + "URI is not changed" + ); + Assert.equal(gBrowser.tabs.length, 3, "Not opened a new tab"); + + if (component) { + info("Check whether the component is still in the page"); + Assert.ok(await isSelected(component), "There is expected component"); + } + + BrowserTestUtils.removeTab(secondTab); + BrowserTestUtils.removeTab(firstTab); + } +}); + +add_task(async function test_about_addons_pages() { + let testData = [ + { + cmd: "add-ons", + testFun: async () => isSelected("button[name=discover]"), + }, + { + cmd: "plugins", + testFun: async () => isSelected("button[name=plugin]"), + }, + { + cmd: "extensions", + testFun: async () => isSelected("button[name=extension]"), + }, + { + cmd: "themes", + testFun: async () => isSelected("button[name=theme]"), + }, + ]; + + info("Pick all actions related about:addons"); + let originalTab = gBrowser.selectedTab; + for (const { cmd, testFun } of testData) { + await BrowserTestUtils.openNewForegroundTab(gBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: cmd, + }); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + Assert.ok(await testFun(), "The page content is correct"); + } + Assert.equal( + gBrowser.tabs.length, + testData.length + 1, + "Tab length is correct" + ); + + info("Pick all again"); + for (const { cmd, testFun } of testData) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: cmd, + }); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + await BrowserTestUtils.waitForCondition(() => testFun()); + Assert.ok(true, "The tab correspondent action is selected"); + } + Assert.equal( + gBrowser.tabs.length, + testData.length + 1, + "Tab length is not changed" + ); + + for (const tab of gBrowser.tabs) { + if (tab !== originalTab) { + BrowserTestUtils.removeTab(tab); + } + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_raceWithTabs.js b/browser/components/urlbar/tests/browser/browser_raceWithTabs.js new file mode 100644 index 0000000000..17560ea101 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_raceWithTabs.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_URL = `${TEST_BASE_URL}dummy_page.html`; + +async function addBookmark(bookmark) { + info("Creating bookmark and keyword"); + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: bookmark.url, + title: bookmark.title, + }); + if (bookmark.keyword) { + await PlacesUtils.keywords.insert({ + keyword: bookmark.keyword, + url: bookmark.url, + }); + } + + registerCleanupFunction(async function () { + if (bookmark.keyword) { + await PlacesUtils.keywords.remove(bookmark.keyword); + } + await PlacesUtils.bookmarks.remove(bm); + }); +} + +/** + * Check that if the user hits enter and ctrl-t at the same time, we open the + * URL in the right tab. + */ +add_task(async function hitEnterLoadInRightTab() { + await addBookmark({ + title: "Test for keyword bookmark and URL", + url: TEST_URL, + keyword: "urlbarkeyword", + }); + + info("Opening a tab"); + let oldTabOpenPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + BrowserOpenTab(); + let oldTab = (await oldTabOpenPromise).target; + let oldTabLoadedPromise = BrowserTestUtils.browserLoaded( + oldTab.linkedBrowser, + false, + TEST_URL + ).then(() => info("Old tab loaded")); + + info("Filling URL bar, sending <return> and opening a tab"); + let tabOpenPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + gURLBar.value = "urlbarkeyword"; + gURLBar.focus(); + gURLBar.select(); + EventUtils.sendKey("return"); + + info("Immediately open a second tab"); + BrowserOpenTab(); + let newTab = (await tabOpenPromise).target; + + info("Created new tab; waiting for tabs to load"); + let newTabLoadedPromise = BrowserTestUtils.browserLoaded( + newTab.linkedBrowser, + false, + "about:newtab" + ).then(() => info("New tab loaded")); + // If one of the tabs loads the wrong page, this will timeout, and that + // indicates we regressed this bug fix. + await Promise.all([newTabLoadedPromise, oldTabLoadedPromise]); + // These are not particularly useful, but the test must contain some checks. + is( + newTab.linkedBrowser.currentURI.spec, + "about:newtab", + "New tab loaded about:newtab" + ); + is(oldTab.linkedBrowser.currentURI.spec, TEST_URL, "Old tab loaded URL"); + + info("Closing tabs"); + BrowserTestUtils.removeTab(newTab); + BrowserTestUtils.removeTab(oldTab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_recentsearches.js b/browser/components/urlbar/tests/browser/browser_recentsearches.js new file mode 100644 index 0000000000..e0ba5f684f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_recentsearches.js @@ -0,0 +1,138 @@ +/* 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/. */ + +const CONFIG_DEFAULT = [ + { + webExtension: { id: "basic@search.mozilla.org" }, + appliesTo: [{ included: { everywhere: true } }], + default: "yes", + }, +]; + +const TOP_SITES = [ + "https://example-1.com/", + "https://example-2.com/", + "https://example-3.com/", +]; + +SearchTestUtils.init(this); + +add_setup(async () => { + // Use engines in test directory + let searchExtensions = getChromeDir(getResolvedURI(gTestPath)); + searchExtensions.append("search-engines"); + await SearchTestUtils.useMochitestEngines(searchExtensions); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.suggest.recentsearches", true], + ["browser.urlbar.recentsearches.featureGate", true], + // Disable UrlbarProviderSearchTips + [ + "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", + false, + ], + ], + }); + + SearchTestUtils.useMockIdleService(); + await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_DEFAULT); + Services.telemetry.clearScalars(); + + registerCleanupFunction(async () => { + let settingsWritten = SearchTestUtils.promiseSearchNotification( + "write-settings-to-disk-complete" + ); + await SearchTestUtils.updateRemoteSettingsConfig(); + await settingsWritten; + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async () => { + let tab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + "data:text/html," + ); + + info("Perform a search that will be added to search history."); + let browserLoaded = BrowserTestUtils.browserLoaded( + window.gBrowser.selectedBrowser + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "Bob Vylan", + }); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Enter", {}, window); + }); + await browserLoaded; + + info("Now check that is shown in search history."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "Previous search shown" + ); + let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.providerName, "RecentSearches"); + + info("Selecting the recent search should be indicated in telemetry."); + browserLoaded = BrowserTestUtils.browserLoaded( + window.gBrowser.selectedBrowser + ); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + EventUtils.synthesizeKey("KEY_Enter", {}, window); + }); + await browserLoaded; + + let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.picked.recent_search", + 0, + 1 + ); + await BrowserTestUtils.removeTab(tab); +}); + +// Ensure that top sites are shown above recent searches, even if trending +// suggestions are disabled. +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.trending", false], + ["browser.urlbar.suggest.topsites", true], + ["browser.newtabpage.activity-stream.default.sites", TOP_SITES.join(",")], + ], + }); + await updateTopSites(sites => sites && sites.length); + + let tab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + "data:text/html," + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + let count = UrlbarTestUtils.getResultCount(window); + let { result } = await UrlbarTestUtils.getDetailsOfResultAt( + window, + count - 1 + ); + Assert.equal(result.providerName, "RecentSearches"); + + await BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_redirect_error.js b/browser/components/urlbar/tests/browser/browser_redirect_error.js new file mode 100644 index 0000000000..ae8dec3da6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_redirect_error.js @@ -0,0 +1,137 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const REDIRECT_FROM = `${TEST_BASE_URL}redirect_error.sjs`; + +const REDIRECT_TO = "https://www.bank1.com/"; // Bad-cert host. + +function isRedirectedURISpec(aURISpec) { + return isRedirectedURI(Services.io.newURI(aURISpec)); +} + +function isRedirectedURI(aURI) { + // Compare only their before-hash portion. + return Services.io.newURI(REDIRECT_TO).equalsExceptRef(aURI); +} + +/* + Test. + +1. Load redirect_bug623155.sjs#BG in a background tab. + +2. The redirected URI is <https://www.bank1.com/#BG>, which displayes a cert + error page. + +3. Switch the tab to foreground. + +4. Check the URLbar's value, expecting <https://www.bank1.com/#BG> + +5. Load redirect_bug623155.sjs#FG in the foreground tab. + +6. The redirected URI is <https://www.bank1.com/#FG>. And this is also + a cert-error page. + +7. Check the URLbar's value, expecting <https://www.bank1.com/#FG> + +8. End. + + */ + +var gNewTab; + +function test() { + waitForExplicitFinish(); + + // Load a URI in the background. + gNewTab = BrowserTestUtils.addTab(gBrowser, REDIRECT_FROM + "#BG"); + gBrowser + .getBrowserForTab(gNewTab) + .webProgress.addProgressListener( + gWebProgressListener, + Ci.nsIWebProgress.NOTIFY_LOCATION + ); +} + +var gWebProgressListener = { + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + + // --------------------------------------------------------------------------- + // NOTIFY_LOCATION mode should work fine without these methods. + // + // onStateChange: function() {}, + // onStatusChange: function() {}, + // onProgressChange: function() {}, + // onSecurityChange: function() {}, + // ---------------------------------------------------------------------------- + + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + if (!aRequest) { + // This is bug 673752, or maybe initial "about:blank". + return; + } + + ok(gNewTab, "There is a new tab."); + ok( + isRedirectedURI(aLocation), + "onLocationChange catches only redirected URI." + ); + + if (aLocation.ref == "BG") { + // This is background tab's request. + isnot(gNewTab, gBrowser.selectedTab, "This is a background tab."); + } else if (aLocation.ref == "FG") { + // This is foreground tab's request. + is(gNewTab, gBrowser.selectedTab, "This is a foreground tab."); + } else { + // We shonuld not reach here. + ok(false, "This URI hash is not expected:" + aLocation.ref); + } + + let isSelectedTab = gNewTab.selected; + setTimeout(delayed, 0, isSelectedTab); + }, +}; + +function delayed(aIsSelectedTab) { + // Switch tab and confirm URL bar. + if (!aIsSelectedTab) { + gBrowser.selectedTab = gNewTab; + } + + let currentURI = gBrowser.selectedBrowser.currentURI.spec; + ok( + isRedirectedURISpec(currentURI), + "The content area is redirected. aIsSelectedTab:" + aIsSelectedTab + ); + is( + gURLBar.value, + UrlbarTestUtils.trimURL(currentURI), + "The URL bar shows the content URI. aIsSelectedTab:" + aIsSelectedTab + ); + + if (!aIsSelectedTab) { + // If this was a background request, go on a foreground request. + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + REDIRECT_FROM + "#FG" + ); + } else { + // Othrewise, nothing to do remains. + finish(); + } +} + +/* Cleanup */ +registerCleanupFunction(function () { + if (gNewTab) { + gBrowser + .getBrowserForTab(gNewTab) + .webProgress.removeProgressListener(gWebProgressListener); + + gBrowser.removeTab(gNewTab); + } + gNewTab = null; +}); diff --git a/browser/components/urlbar/tests/browser/browser_remoteness_switch.js b/browser/components/urlbar/tests/browser/browser_remoteness_switch.js new file mode 100644 index 0000000000..d4d64f81cb --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_remoteness_switch.js @@ -0,0 +1,56 @@ +"use strict"; + +/** + * Verify that when loading and going back/forward through history between URLs + * loaded in the content process, and URLs loaded in the parent process, we + * don't set the URL for the tab to about:blank inbetween the loads. + */ +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.navigation.requireUserInteraction", false]], + }); + let url = "http://www.example.com/foo.html"; + await BrowserTestUtils.withNewTab( + { gBrowser, url }, + async function (browser) { + let wpl = { + onLocationChange(unused, unused2, location) { + if (location.schemeIs("about")) { + is( + location.spec, + "about:config", + "Only about: location change should be for about:preferences" + ); + } else { + is( + location.spec, + url, + "Only non-about: location change should be for the http URL we're dealing with." + ); + } + }, + }; + gBrowser.addProgressListener(wpl); + + let didLoad = BrowserTestUtils.browserLoaded( + browser, + null, + function (loadedURL) { + return loadedURL == "about:config"; + } + ); + BrowserTestUtils.startLoadingURIString(browser, "about:config"); + await didLoad; + + gBrowser.goBack(); + await BrowserTestUtils.browserLoaded(browser, null, function (loadedURL) { + return url == loadedURL; + }); + gBrowser.goForward(); + await BrowserTestUtils.browserLoaded(browser, null, function (loadedURL) { + return loadedURL == "about:config"; + }); + gBrowser.removeProgressListener(wpl); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_remotetab.js b/browser/components/urlbar/tests/browser/browser_remotetab.js new file mode 100644 index 0000000000..1fde855dbd --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_remotetab.js @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests checks that the remote tab result is displayed and can be + * selected. + */ + +"use strict"; + +const { SyncedTabs } = ChromeUtils.importESModule( + "resource://services-sync/SyncedTabs.sys.mjs" +); + +const TEST_URL = `${TEST_BASE_URL}dummy_page.html`; + +const REMOTE_TAB = { + id: "7cqCr77ptzX3", + type: "client", + lastModified: 1492201200, + name: "zcarter's Nightly on MacBook-Pro-25", + clientType: "desktop", + tabs: [ + { + type: "tab", + title: "Test Remote", + url: TEST_URL, + icon: UrlbarUtils.ICON.DEFAULT, + client: "7cqCr77ptzX3", + lastUsed: Math.floor(Date.now() / 1000), + }, + ], +}; + +add_setup(async function () { + sandbox = sinon.createSandbox(); + + let originalSyncedTabsInternal = SyncedTabs._internal; + SyncedTabs._internal = { + isConfiguredToSyncTabs: true, + hasSyncedThisSession: true, + getTabClients() { + return Promise.resolve([]); + }, + syncTabs() { + return Promise.resolve(); + }, + }; + + // Tell the Sync XPCOM service it is initialized. + let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + let oldWeaveServiceReady = weaveXPCService.ready; + weaveXPCService.ready = true; + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.autoFill", false], + ["services.sync.username", "fake"], + ["services.sync.syncedTabs.showRemoteTabs", true], + ], + }); + + sandbox + .stub(SyncedTabs._internal, "getTabClients") + .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {}))); + + registerCleanupFunction(async () => { + sandbox.restore(); + weaveXPCService.ready = oldWeaveServiceReady; + SyncedTabs._internal = originalSyncedTabsInternal; + }); +}); + +add_task(async function test_remotetab_opens() { + await BrowserTestUtils.withNewTab( + { url: "about:robots", gBrowser }, + async function () { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "Test Remote", + }); + + // There should be two items in the pop-up, the first is the default search + // suggestion, the second is the remote tab. + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.REMOTE_TAB, + "Should be the remote tab entry" + ); + + // The URL is going to open in the current tab as it is currently about:blank + let promiseTabLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await promiseTabLoaded; + + Assert.equal( + gBrowser.selectedTab.linkedBrowser.currentURI.spec, + TEST_URL, + "correct URL loaded" + ); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_removeUnsafeProtocolsFromURLBarPaste.js b/browser/components/urlbar/tests/browser/browser_removeUnsafeProtocolsFromURLBarPaste.js new file mode 100644 index 0000000000..4dfbc5c01b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_removeUnsafeProtocolsFromURLBarPaste.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Ensures that pasting unsafe protocols in the urlbar have the protocol + * correctly stripped. + */ + +var pairs = [ + ["javascript:", ""], + ["javascript:1+1", "1+1"], + ["javascript:document.domain", "document.domain"], + [ + " \u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009javascript:document.domain", + "document.domain", + ], + ["java\nscript:foo", "foo"], + ["java\tscript:foo", "foo"], + ["http://\nexample.com", "http://example.com"], + ["http://\nexample.com\n", "http://example.com"], + ["data:text/html,<body>hi</body>", "data:text/html,<body>hi</body>"], + ["javaScript:foopy", "foopy"], + ["javaScript:javaScript:alert('hi')", "alert('hi')"], + // Nested things get confusing because some things don't parse as URIs: + ["javascript:javascript:alert('hi!')", "alert('hi!')"], + [ + "data:data:text/html,<body>hi</body>", + "data:data:text/html,<body>hi</body>", + ], + ["javascript:data:javascript:alert('hi!')", "data:javascript:alert('hi!')"], + [ + "javascript:data:text/html,javascript:alert('hi!')", + "data:text/html,javascript:alert('hi!')", + ], + [ + "data:data:text/html,javascript:alert('hi!')", + "data:data:text/html,javascript:alert('hi!')", + ], +]; + +let supportsNullBytes = AppConstants.platform == "macosx"; +// Note that \u000d (\r) is missing here; we test it separately because it +// makes the test sad on Windows. +let nonsense = + "\u000a\u000b\u000c\u000e\u000f\u0010\u0011\u0012\u0013\u0014javascript:foo"; +if (supportsNullBytes) { + nonsense = "\u0000" + nonsense; +} +pairs.push([nonsense, "foo"]); + +let supportsReturnWithoutNewline = + AppConstants.platform != "win" && AppConstants.platform != "linux"; +if (supportsReturnWithoutNewline) { + pairs.push(["java\rscript:foo", "foo"]); +} + +async function paste(input) { + try { + await SimpleTest.promiseClipboardChange( + aData => { + // This test checks how "\r" is treated. Therefore, we cannot specify + // string here and instead, we need to compare strictly with this + // function. + return aData === input; + }, + () => { + clipboardHelper.copyString(input); + } + ); + } catch (ex) { + Assert.ok(false, "Failed to copy string '" + input + "' to clipboard"); + } + + document.commandDispatcher + .getControllerForCommand("cmd_paste") + .doCommand("cmd_paste"); +} + +add_task(async function test_stripUnsafeProtocolPaste() { + for (let [inputValue, expectedURL] of pairs) { + gURLBar.value = ""; + gURLBar.focus(); + await paste(inputValue); + + Assert.equal( + gURLBar.value, + expectedURL, + `entering ${inputValue} strips relevant bits.` + ); + + await new Promise(resolve => setTimeout(resolve, 0)); + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_remove_match.js b/browser/components/urlbar/tests/browser/browser_remove_match.js new file mode 100644 index 0000000000..b9e97044e4 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_remove_match.js @@ -0,0 +1,218 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.defineESModuleGetters(this, { + FormHistory: "resource://gre/modules/FormHistory.sys.mjs", +}); + +add_setup(async function () { + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + let engine = Services.search.getEngineByName("Example"); + await Services.search.moveEngine(engine, 0); +}); + +add_task(async function test_remove_history() { + const TEST_URL = "http://remove.me/from_urlbar/"; + await PlacesTestUtils.addVisits(TEST_URL); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); + + let promiseVisitRemoved = PlacesTestUtils.waitForNotification( + "page-removed", + events => events[0].url === TEST_URL + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "from_urlbar", + }); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, TEST_URL, "Found the expected result"); + + let expectedResultCount = UrlbarTestUtils.getResultCount(window) - 1; + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 1); + EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true }); + + const removeEvents = await promiseVisitRemoved; + Assert.ok( + removeEvents[0].isRemovedFromStore, + "isRemovedFromStore should be true" + ); + + await TestUtils.waitForCondition( + () => UrlbarTestUtils.getResultCount(window) == expectedResultCount, + "Waiting for the result to disappear" + ); + + for (let i = 0; i < expectedResultCount; i++) { + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.notEqual( + details.url, + TEST_URL, + "Should not find the test URL in the remaining results" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function test_remove_form_history() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.maxHistoricalSearchSuggestions", 1], + ], + }); + + let formHistoryValue = "foobar"; + await UrlbarTestUtils.formHistory.add([formHistoryValue]); + + let formHistory = ( + await UrlbarTestUtils.formHistory.search({ + value: formHistoryValue, + }) + ).map(entry => entry.value); + Assert.deepEqual( + formHistory, + [formHistoryValue], + "Should find form history after adding it" + ); + + let promiseRemoved = UrlbarTestUtils.formHistory.promiseChanged("remove"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + + let index = 1; + let count = UrlbarTestUtils.getResultCount(window); + for (; index < count; index++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + if ( + result.type == UrlbarUtils.RESULT_TYPE.SEARCH && + result.source == UrlbarUtils.RESULT_SOURCE.HISTORY + ) { + break; + } + } + Assert.ok(index < count, "Result found"); + + EventUtils.synthesizeKey("KEY_Tab", { repeat: index }); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), index); + EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true }); + await promiseRemoved; + + await TestUtils.waitForCondition( + () => UrlbarTestUtils.getResultCount(window) == count - 1, + "Waiting for the result to disappear" + ); + + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.ok( + result.type != UrlbarUtils.RESULT_TYPE.SEARCH || + result.source != UrlbarUtils.RESULT_SOURCE.HISTORY, + "Should not find the form history result in the remaining results" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); + + formHistory = ( + await UrlbarTestUtils.formHistory.search({ + value: formHistoryValue, + }) + ).map(entry => entry.value); + Assert.deepEqual( + formHistory, + [], + "Should not find form history after removing it" + ); + + await SpecialPowers.popPrefEnv(); +}); + +// We shouldn't be able to remove a bookmark item. +add_task(async function test_remove_bookmark_doesnt() { + const TEST_URL = "http://dont.remove.me/from_urlbar/"; + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "test", + url: TEST_URL, + }); + + registerCleanupFunction(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "from_urlbar", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, TEST_URL, "Found the expected result"); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 1); + EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true }); + + // We don't have an easy way of determining if the event was process or not, + // so let any event queues clear before testing. + await new Promise(resolve => setTimeout(resolve, 0)); + await PlacesTestUtils.promiseAsyncUpdates(); + + Assert.ok( + await PlacesUtils.bookmarks.fetch({ url: TEST_URL }), + "Should still have the URL bookmarked." + ); +}); + +add_task(async function test_searchMode_removeRestyledHistory() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.maxHistoricalSearchSuggestions", 1], + ], + }); + + let query = "ciao"; + let url = `https://example.com/?q=${query}bar`; + await PlacesTestUtils.addVisits(url); + + await BrowserTestUtils.withNewTab("about:robots", async function (browser) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + await UrlbarTestUtils.enterSearchMode(window); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH); + Assert.equal(result.source, UrlbarUtils.RESULT_SOURCE.HISTORY); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 1); + EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true }); + await TestUtils.waitForCondition( + async () => !(await PlacesTestUtils.isPageInDB(url)), + "Wait for url to be removed from history" + ); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "Urlbar result should be removed" + ); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + }); + await PlacesUtils.history.clear(); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js b/browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js new file mode 100644 index 0000000000..096d8e2134 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// When the input is empty and the view is opened, keying down through the +// results and then out of the results should restore the empty input. + +"use strict"; + +add_task(async function test() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits("http://example.com/"); + } + // Update Top Sites to make sure the last Top Site is a URL. Otherwise, it + // would be a search shortcut and thus would not fill the Urlbar when + // selected. + await updateTopSites(sites => { + return ( + sites && + sites[sites.length - 1] && + sites[sites.length - 1].url == "http://example.com/" + ); + }); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + -1, + "Nothing selected" + ); + + let resultCount = UrlbarTestUtils.getResultCount(window); + Assert.greater(resultCount, 0, "At least one result"); + + for (let i = 0; i < resultCount; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + resultCount - 1, + "Last result selected" + ); + Assert.notEqual(gURLBar.value, "", "Input should not be empty"); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + -1, + "Nothing selected" + ); + Assert.equal(gURLBar.value, "", "Input should be empty"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_resultSpan.js b/browser/components/urlbar/tests/browser/browser_resultSpan.js new file mode 100644 index 0000000000..9b17fb71f5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_resultSpan.js @@ -0,0 +1,254 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that displaying results with resultSpan > 1 limits other results in +// the view. + +const TEST_RESULTS = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/1" } + ), + makeTipResult(), +]; + +const MAX_RESULTS = UrlbarPrefs.get("maxRichResults"); +const TIP_SPAN = UrlbarUtils.getSpanForResult({ + type: UrlbarUtils.RESULT_TYPE.TIP, +}); + +add_setup(async function () { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); +}); + +// A restricting provider with one tip result and many history results. +add_task(async function oneTip() { + let results = Array.from(TEST_RESULTS); + for (let i = TEST_RESULTS.length; i < MAX_RESULTS; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: `http://mozilla.org/${i}` } + ) + ); + } + + let expectedResults = Array.from(results).slice( + 0, + MAX_RESULTS - TIP_SPAN + 1 + ); + + let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 }); + UrlbarProvidersManager.registerProvider(provider); + + let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + + checkResults(context.results, expectedResults); + + UrlbarProvidersManager.unregisterProvider(provider); + gURLBar.view.close(); +}); + +// A restricting provider with three tip results and many history results. +add_task(async function threeTips() { + let results = Array.from(TEST_RESULTS); + for (let i = 1; i < 3; i++) { + results.push(makeTipResult()); + } + for (let i = 2; i < 15; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: `http://mozilla.org/${i}` } + ) + ); + } + + let expectedResults = Array.from(results).slice( + 0, + MAX_RESULTS - 3 * (TIP_SPAN - 1) + ); + + let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 }); + UrlbarProvidersManager.registerProvider(provider); + + let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + + checkResults(context.results, expectedResults); + + UrlbarProvidersManager.unregisterProvider(provider); + gURLBar.view.close(); +}); + +// A non-restricting provider with one tip result and many history results. +add_task(async function oneTip_nonRestricting() { + let results = Array.from(TEST_RESULTS); + for (let i = 2; i < 15; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: `http://mozilla.org/${i}` } + ) + ); + } + + let expectedResults = Array.from(results); + + // UrlbarProviderHeuristicFallback's heuristic search result + expectedResults.unshift({ + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + payload: { + engine: Services.search.defaultEngine.name, + query: "test", + }, + }); + + expectedResults = expectedResults.slice(0, MAX_RESULTS - TIP_SPAN + 1); + + let provider = new UrlbarTestUtils.TestProvider({ results }); + UrlbarProvidersManager.registerProvider(provider); + + let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + + checkResults(context.results, expectedResults); + + UrlbarProvidersManager.unregisterProvider(provider); + gURLBar.view.close(); +}); + +// A non-restricting provider with three tip results and many history results. +add_task(async function threeTips_nonRestricting() { + let results = Array.from(TEST_RESULTS); + for (let i = 1; i < 3; i++) { + results.push(makeTipResult()); + } + for (let i = 2; i < 15; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: `http://mozilla.org/${i}` } + ) + ); + } + + let expectedResults = Array.from(results); + + // UrlbarProviderHeuristicFallback's heuristic search result + expectedResults.unshift({ + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + payload: { + engine: Services.search.defaultEngine.name, + query: "test", + }, + }); + + expectedResults = expectedResults.slice(0, MAX_RESULTS - 3 * (TIP_SPAN - 1)); + + let provider = new UrlbarTestUtils.TestProvider({ results }); + UrlbarProvidersManager.registerProvider(provider); + + let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + + checkResults(context.results, expectedResults); + + UrlbarProvidersManager.unregisterProvider(provider); + gURLBar.view.close(); +}); + +add_task(async function customValue() { + let results = []; + for (let i = 0; i < 15; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: `http://mozilla.org/${i}` } + ) + ); + } + + results[1].resultSpan = 5; + + let expectedResults = Array.from(results); + expectedResults = expectedResults.slice(0, 6); + + let provider = new UrlbarTestUtils.TestProvider({ results }); + UrlbarProvidersManager.registerProvider(provider); + + let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + + checkResults(context.results, expectedResults); + + UrlbarProvidersManager.unregisterProvider(provider); + gURLBar.view.close(); +}); + +function checkResults(actual, expected) { + Assert.equal(actual.length, expected.length, "Number of results"); + for (let i = 0; i < expected.length; i++) { + info(`Checking results at index ${i}`); + let actualResult = collectExpectedProperties(actual[i], expected[i]); + Assert.deepEqual(actualResult, expected[i], "Actual vs. expected result"); + } +} + +function collectExpectedProperties(actualObj, expectedObj) { + let newActualObj = {}; + for (let name in expectedObj) { + if (typeof expectedObj[name] == "object") { + newActualObj[name] = collectExpectedProperties( + actualObj[name], + expectedObj[name] + ); + } else { + newActualObj[name] = expectedObj[name]; + } + } + return newActualObj; +} + +function makeTipResult() { + return new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + helpUrl: "http://example.com/", + type: "test", + titleL10n: { id: "urlbar-search-tips-confirm" }, + buttons: [ + { + url: "http://example.com/", + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + } + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_result_menu.js b/browser/components/urlbar/tests/browser/browser_result_menu.js new file mode 100644 index 0000000000..ccbe247598 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_result_menu.js @@ -0,0 +1,260 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function test_history() { + const TEST_URL = "https://remove.me/from_urlbar/"; + await PlacesTestUtils.addVisits(TEST_URL); + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); + + const resultIndex = 1; + let result; + let startQuery = async () => { + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "from_urlbar", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex); + Assert.equal(result.url, TEST_URL, "Found the expected result"); + gURLBar.view.selectedRowIndex = resultIndex; + }; + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.resultMenu.keyboardAccessible", false]], + }); + await startQuery(); + EventUtils.synthesizeKey("KEY_Tab"); + isnot( + UrlbarTestUtils.getSelectedElement(window), + UrlbarTestUtils.getButtonForResultIndex(window, "menu", resultIndex), + "Tab key skips over menu button with resultMenu.keyboardAccessible pref set to false" + ); + info( + "Checking that the mouse can still activate the menu button with resultMenu.keyboardAccessible = false" + ); + await UrlbarTestUtils.openResultMenu(window, { + byMouse: true, + resultIndex, + }); + gURLBar.view.resultMenu.hidePopup(); + await SpecialPowers.popPrefEnv(); + await startQuery(); + EventUtils.synthesizeKey("KEY_Tab"); + is( + UrlbarTestUtils.getSelectedElement(window), + UrlbarTestUtils.getButtonForResultIndex(window, "menu", resultIndex), + "Tab key doesn't skip over menu button with resultMenu.keyboardAccessible pref reset to true" + ); + + info("Checking that Space activates the menu button"); + await startQuery(); + await UrlbarTestUtils.openResultMenu(window, { + activationKey: " ", + }); + gURLBar.view.resultMenu.hidePopup(); + + info("Selecting Learn more item from the result menu"); + let tabOpenPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "awesome-bar-result-menu" + ); + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "L"); + info("Waiting for Learn more link to open in a new tab"); + await tabOpenPromise; + gBrowser.removeCurrentTab(); + + info("Restarting query in order to remove history entry via the menu"); + await startQuery(); + let promiseVisitRemoved = PlacesTestUtils.waitForNotification( + "page-removed", + events => events[0].url === TEST_URL + ); + let expectedResultCount = UrlbarTestUtils.getResultCount(window) - 1; + + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "R"); + const removeEvents = await promiseVisitRemoved; + Assert.ok( + removeEvents[0].isRemovedFromStore, + "isRemovedFromStore should be true" + ); + await TestUtils.waitForCondition( + () => UrlbarTestUtils.getResultCount(window) == expectedResultCount, + "Waiting for the result to disappear" + ); + for (let i = 0; i < expectedResultCount; i++) { + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.notEqual( + details.url, + TEST_URL, + "Should not find the test URL in the remaining results" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function test_remove_search_history() { + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + let engine = Services.search.getEngineByName("Example"); + await Services.search.moveEngine(engine, 0); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.maxHistoricalSearchSuggestions", 1], + ], + }); + + let formHistoryValue = "foobar"; + await UrlbarTestUtils.formHistory.add([formHistoryValue]); + + let formHistory = ( + await UrlbarTestUtils.formHistory.search({ + value: formHistoryValue, + }) + ).map(entry => entry.value); + Assert.deepEqual( + formHistory, + [formHistoryValue], + "Should find form history after adding it" + ); + + let promiseRemoved = UrlbarTestUtils.formHistory.promiseChanged("remove"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + + let resultIndex = 1; + let count = UrlbarTestUtils.getResultCount(window); + for (; resultIndex < count; resultIndex++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + resultIndex + ); + if ( + result.type == UrlbarUtils.RESULT_TYPE.SEARCH && + result.source == UrlbarUtils.RESULT_SOURCE.HISTORY + ) { + break; + } + } + Assert.ok(resultIndex < count, "Result found"); + + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "R", { + resultIndex, + }); + await promiseRemoved; + + await TestUtils.waitForCondition( + () => UrlbarTestUtils.getResultCount(window) == count - 1, + "Waiting for the result to disappear" + ); + + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.ok( + result.type != UrlbarUtils.RESULT_TYPE.SEARCH || + result.source != UrlbarUtils.RESULT_SOURCE.HISTORY, + "Should not find the form history result in the remaining results" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); + + formHistory = ( + await UrlbarTestUtils.formHistory.search({ + value: formHistoryValue, + }) + ).map(entry => entry.value); + Assert.deepEqual( + formHistory, + [], + "Should not find form history after removing it" + ); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function firefoxSuggest() { + const url = "https://example.com/hey-there"; + const helpUrl = "https://example.com/help"; + let provider = new UrlbarTestUtils.TestProvider({ + priority: Infinity, + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url, + isBlockable: true, + blockL10n: { id: "urlbar-result-menu-dismiss-firefox-suggest" }, + helpUrl, + helpL10n: { + id: "urlbar-result-menu-learn-more-about-firefox-suggest", + }, + } + ), + ], + }); + + // Implement the provider's `onEngagement()` so it removes the result. + let onEngagementCallCount = 0; + provider.onEngagement = (state, queryContext, details, controller) => { + onEngagementCallCount++; + controller.removeResult(details.result); + }; + + UrlbarProvidersManager.registerProvider(provider); + + async function openResults() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "There should be one result" + ); + + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + Assert.equal( + row.result.payload.url, + url, + "The result should be in the first row" + ); + } + + await openResults(); + let tabOpenPromise = BrowserTestUtils.waitForNewTab(gBrowser, helpUrl); + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "L", { + resultIndex: 0, + }); + info("Waiting for help URL to load in a new tab"); + await tabOpenPromise; + gBrowser.removeCurrentTab(); + + await openResults(); + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "D", { + resultIndex: 0, + }); + + Assert.greater( + onEngagementCallCount, + 0, + "onEngagement() should have been called" + ); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 0, + "There should be no results after blocking" + ); + + await UrlbarTestUtils.promisePopupClose(window); + UrlbarProvidersManager.unregisterProvider(provider); +}); diff --git a/browser/components/urlbar/tests/browser/browser_result_menu_general.js b/browser/components/urlbar/tests/browser/browser_result_menu_general.js new file mode 100644 index 0000000000..ece48de20a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_result_menu_general.js @@ -0,0 +1,416 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// General tests for the result menu that aren't related to specific result +// types. + +"use strict"; + +const MAX_RESULTS = UrlbarPrefs.get("maxRichResults"); +const RESULT_URL = "https://example.com/test"; +const RESULT_HELP_URL = "https://example.com/help"; + +add_setup(async function () { + // Add enough results to fill up the view. + await PlacesUtils.history.clear(); + for (let i = 0; i < MAX_RESULTS; i++) { + await PlacesTestUtils.addVisits("https://example.com/" + i); + } + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +// Sets `helpUrl` on a result payload and makes sure the result menu ends up +// with a help command. +add_task(async function help() { + let provider = registerTestProvider(1); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "example", + window, + }); + + await assertIsTestResult(1); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + let menuButton = result.element.row._buttons.get("menu"); + Assert.ok(menuButton, "Sanity check: menu button should exist"); + + let menuitem = await UrlbarTestUtils.openResultMenuAndGetItem({ + window, + command: "help", + resultIndex: 1, + openByMouse: true, + }); + Assert.ok(menuitem, "Help menu item should exist"); + + let l10nAttrs = document.l10n.getAttributes(menuitem); + Assert.deepEqual( + l10nAttrs, + { id: "urlbar-result-menu-tip-get-help", args: null }, + "The l10n ID attribute was correctly set" + ); + + // The result menu needs to be closed before calling + // `openResultMenuAndClickItem()` below; otherwise it will wait on a + // `popupshown` event that will never come. + gURLBar.view.resultMenu.hidePopup(true); + + // We assume clicking "help" will load a page in a new tab. + let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser); + + await UrlbarTestUtils.openResultMenuAndClickItem(window, "help", { + resultIndex: 1, + openByMouse: true, + }); + + info("Waiting for load"); + await loadPromise; + await TestUtils.waitForTick(); + Assert.equal( + gBrowser.currentURI.spec, + RESULT_HELP_URL, + "The load URL should be the help URL" + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + UrlbarProvidersManager.unregisterProvider(provider); +}); + +// (SHIFT+)TABs through a result with a menu button. The result is the second +// result and has other results after it. +add_task(async function keyboardSelection_secondResult() { + let provider = registerTestProvider(1); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "example", + window, + }); + + // Sanity-check initial state. + Assert.equal( + UrlbarTestUtils.getResultCount(window), + MAX_RESULTS, + "There should be MAX_RESULTS results in the view" + ); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + 0, + "The heuristic result should be selected" + ); + await assertIsTestResult(1); + + info("Arrow down to the main part of the result."); + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertMainPartSelected(1); + + info("TAB to the button."); + EventUtils.synthesizeKey("KEY_Tab"); + assertButtonSelected(2); + + info("TAB to the next (third) result."); + EventUtils.synthesizeKey("KEY_Tab"); + assertOtherResultSelected(3, "next result"); + + info("SHIFT+TAB to the menu button."); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + assertButtonSelected(2); + + info("SHIFT+TAB to the main part of the result."); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + assertMainPartSelected(1); + + info("Arrow up to the previous (first) result."); + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertOtherResultSelected(0, "previous result"); + + await UrlbarTestUtils.promisePopupClose(window); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +// (SHIFT+)TABs through a result with a help button. The result is the +// last result. +add_task(async function keyboardSelection_lastResult() { + let provider = registerTestProvider(MAX_RESULTS - 1); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "example", + window, + }); + + // Sanity-check initial state. + Assert.equal( + UrlbarTestUtils.getResultCount(window), + MAX_RESULTS, + "There should be MAX_RESULTS results in the view" + ); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + 0, + "The heuristic result should be selected" + ); + await assertIsTestResult(MAX_RESULTS - 1); + + let numSelectable = MAX_RESULTS * 2 - 2; + + // Arrow down to the main part of the result. + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: MAX_RESULTS - 1 }); + assertMainPartSelected(numSelectable - 1); + + // TAB to the menu button. + EventUtils.synthesizeKey("KEY_Tab"); + assertButtonSelected(numSelectable); + + // Arrow down to the first one-off. If this test is running alone, the + // one-offs will rebuild themselves when the view is opened above, and they + // may not be visible yet. Wait for the first one to become visible before + // trying to select it. + await TestUtils.waitForCondition(() => { + return ( + gURLBar.view.oneOffSearchButtons.buttons.firstElementChild && + BrowserTestUtils.isVisible( + gURLBar.view.oneOffSearchButtons.buttons.firstElementChild + ) + ); + }, "Waiting for first one-off to become visible."); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + await TestUtils.waitForCondition(() => { + return gURLBar.view.oneOffSearchButtons.selectedButton; + }, "Waiting for one-off to become selected."); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + -1, + "No results should be selected." + ); + + // SHIFT+TAB to the menu button. + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + assertButtonSelected(numSelectable); + + // SHIFT+TAB to the main part of the result. + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + assertMainPartSelected(numSelectable - 1); + + // Arrow up to the previous result. + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertOtherResultSelected(numSelectable - 3, "previous result"); + + await UrlbarTestUtils.promisePopupClose(window); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +// Picks the main part of the test result with the keyboard. +add_task(async function pick_mainPart_keyboard() { + await doPickTest({ pickHelp: false, useKeyboard: true }); +}); + +// Picks the help command with the keyboard. +add_task(async function pick_help_keyboard() { + await doPickTest({ pickHelp: true, useKeyboard: true }); +}); + +// Picks the main part of the test result with the mouse. +add_task(async function pick_mainPart_mouse() { + await doPickTest({ pickHelp: false, useKeyboard: false }); +}); + +// Picks the help command with the mouse. +add_task(async function pick_help_mouse() { + await doPickTest({ pickHelp: true, useKeyboard: false }); +}); + +async function doPickTest({ pickHelp, useKeyboard }) { + await BrowserTestUtils.withNewTab("about:blank", async () => { + let index = 1; + let provider = registerTestProvider(index); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "example", + window, + }); + + // Sanity-check initial state. + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + 0, + "The heuristic result should be selected" + ); + await assertIsTestResult(index); + + if (useKeyboard) { + // Arrow down to the result. + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: index }); + assertMainPartSelected(index * 2 - 1); + } + + // Pick the result. The appropriate URL should load. + let loadPromise = pickHelp + ? BrowserTestUtils.waitForNewTab(gBrowser) + : BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await Promise.all([ + loadPromise, + UrlbarTestUtils.promisePopupClose(window, async () => { + if (pickHelp) { + await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "h", { + openByMouse: !useKeyboard, + resultIndex: index, + }); + } else if (useKeyboard) { + EventUtils.synthesizeKey("KEY_Enter"); + } else { + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + index + ); + EventUtils.synthesizeMouseAtCenter(result.element.row._content, {}); + } + }), + ]); + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + pickHelp ? RESULT_HELP_URL : RESULT_URL, + "Expected URL should have loaded" + ); + + if (pickHelp) { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + UrlbarProvidersManager.unregisterProvider(provider); + + // Avoid showing adaptive history autofill. + await PlacesTestUtils.clearInputHistory(); + }); +} + +/** + * Registers a provider that creates a result with a help URL. + * + * @param {number} suggestedIndex + * The result's suggestedIndex. + * @returns {UrlbarProvider} + * The new provider. + */ +function registerTestProvider(suggestedIndex) { + let results = [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url: RESULT_URL, + helpUrl: RESULT_HELP_URL, + helpL10n: { + id: "urlbar-result-menu-tip-get-help", + }, + } + ), + { suggestedIndex } + ), + ]; + let provider = new UrlbarTestUtils.TestProvider({ results }); + UrlbarProvidersManager.registerProvider(provider); + return provider; +} + +/** + * Asserts that the result at the given index is our test result with a menu + * button. + * + * @param {number} index + * The expected index of the test result. + */ +async function assertIsTestResult(index) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.URL, + "The second result should be a URL" + ); + Assert.equal( + result.url, + RESULT_URL, + "The result's URL should be the expected URL" + ); + + let { row } = result.element; + Assert.ok(row._buttons.get("menu"), "The result should have a menu button"); + Assert.ok(row._content.id, "Row-inner has an ID"); + Assert.equal( + row.getAttribute("role"), + "presentation", + "Row should have role=presentation" + ); + Assert.equal( + row._content.getAttribute("role"), + "option", + "Row-inner should have role=option" + ); +} + +/** + * Asserts that a particular element is selected. + * + * @param {number} expectedSelectedElementIndex + * The expected selected element index. + * @param {string} expectedClassName + * A class name of the expected selected element. + * @param {string} msg + * A string to include in the assertion message. + */ +function assertSelection(expectedSelectedElementIndex, expectedClassName, msg) { + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + expectedSelectedElementIndex, + "Expected selected element index: " + msg + ); + Assert.ok( + UrlbarTestUtils.getSelectedElement(window).classList.contains( + expectedClassName + ), + `Expected selected element: ${msg} (${ + UrlbarTestUtils.getSelectedElement(window).classList + } == ${expectedClassName})` + ); +} + +/** + * Asserts that the main part of our test result is selected. + * + * @param {number} expectedSelectedElementIndex + * The expected selected element index. + */ +function assertMainPartSelected(expectedSelectedElementIndex) { + assertSelection( + expectedSelectedElementIndex, + "urlbarView-row-inner", + "main part of test result" + ); +} + +/** + * Asserts that the menu button is selected. + * + * @param {number} expectedSelectedElementIndex + * The expected selected element index. + */ +function assertButtonSelected(expectedSelectedElementIndex) { + assertSelection( + expectedSelectedElementIndex, + "urlbarView-button-menu", + "menu button" + ); +} + +/** + * Asserts that a result other than our test result is selected. + * + * @param {number} expectedSelectedElementIndex + * The expected selected element index. + * @param {string} msg + * A string to include in the assertion message. + */ +function assertOtherResultSelected(expectedSelectedElementIndex, msg) { + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + expectedSelectedElementIndex, + "Expected other selected element index: " + msg + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_result_onSelection.js b/browser/components/urlbar/tests/browser/browser_result_onSelection.js new file mode 100644 index 0000000000..2a5f8c3760 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_result_onSelection.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test() { + let results = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { + url: "http://mozilla.org/1", + helpUrl: "http://example.com/", + isBlockable: true, + blockL10n: { id: "urlbar-result-menu-remove-from-history" }, + } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { + url: "http://mozilla.org/2", + helpUrl: "http://example.com/", + isBlockable: true, + blockL10n: { id: "urlbar-result-menu-remove-from-history" }, + } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + helpUrl: "http://example.com/", + type: "test", + titleL10n: { id: "urlbar-search-tips-confirm" }, + buttons: [ + { + url: "http://example.com/", + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + } + ), + ]; + + results[0].heuristic = true; + + let selectionCount = 0; + let provider = new UrlbarTestUtils.TestProvider({ + results, + priority: 1, + onSelection: (result, element) => { + selectionCount++; + }, + }); + UrlbarProvidersManager.registerProvider(provider); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + + EventUtils.synthesizeKey("KEY_Tab", { + repeat: 5, + }); + EventUtils.synthesizeKey("KEY_ArrowDown"); + ok( + UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton, + "a one off button is selected" + ); + + Assert.equal(selectionCount, 6, "Number of elements selected in the view."); + UrlbarProvidersManager.unregisterProvider(provider); +}); diff --git a/browser/components/urlbar/tests/browser/browser_results_format_displayValue.js b/browser/components/urlbar/tests/browser/browser_results_format_displayValue.js new file mode 100644 index 0000000000..d0ec3d3818 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_results_format_displayValue.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_receive_punycode_result() { + let url = "https://www.اختبار.اختبار.org:5000/"; + + // eslint-disable-next-line jsdoc/require-jsdoc + class ResultWithHighlightsProvider extends UrlbarTestUtils.TestProvider { + startQuery(context, addCallback) { + let result = Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + ...UrlbarResult.payloadAndSimpleHighlights(context.tokens, { + url: [url, UrlbarUtils.HIGHLIGHT.TYPED], + }) + ), + { suggestedIndex: 0 } + ); + addCallback(this, result); + } + + getViewUpdate(result, idsByName) { + return {}; + } + } + let provider = new ResultWithHighlightsProvider(); + + registerCleanupFunction(async () => { + UrlbarProvidersManager.unregisterProvider(provider); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + gURLBar.handleRevert(); + }); + UrlbarProvidersManager.registerProvider(provider); + + info("Open the result popup"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "org", + window, + fireInputEvent: true, + }); + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + is(row.result.type, UrlbarUtils.RESULT_TYPE.URL, "row.result.type"); + is( + row.result.payload.displayUrl, + "اختبار.اختبار.org:5000", + "Result is trimmed and formatted correctly." + ); + is( + row.result.payload.title, + "www.اختبار.اختبار.org:5000", + "Result is trimmed and formatted correctly." + ); + + let firstRow = document.querySelector(".urlbarView-row"); + let firstRowUrl = firstRow.querySelector(".urlbarView-url"); + + is( + firstRowUrl.innerHTML.charAt(0), + "\u200e", + "UrlbarView row url contains LRM" + ); + // Tests if highlights are correct after inserting lrm symbol + is( + firstRowUrl.querySelector("strong")?.innerText, + "org", + "Correct part of url is highlighted" + ); + is( + firstRow.querySelector(".urlbarView-title strong")?.innerText, + "org", + "Correct part of title is highlighted" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_retainedResultsOnFocus.js b/browser/components/urlbar/tests/browser/browser_retainedResultsOnFocus.js new file mode 100644 index 0000000000..3cc26a5757 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_retainedResultsOnFocus.js @@ -0,0 +1,438 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests retained results. +// When there is a pending search (user typed a search string and blurred +// without picking a result), on focus we should the search results again. + +async function checkPanelStatePersists(win, isOpen) { + // Check for popup events, we should not see any of them because the urlbar + // popup state should not change. This also ensures we don't cause flickering + // open/close actions. + function handler(event) { + Assert.ok(false, `Received unexpected event ${event.type}`); + } + win.gURLBar.addEventListener("popupshowing", handler); + win.gURLBar.addEventListener("popuphiding", handler); + // Because the panel opening may not be immediate, we must wait a bit. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 300)); + win.gURLBar.removeEventListener("popupshowing", handler); + win.gURLBar.removeEventListener("popuphiding", handler); + Assert.equal( + isOpen, + win.gURLBar.view.isOpen, + `check urlbar remains ${isOpen ? "open" : "closed"}` + ); +} + +async function checkOpensOnFocus(win, state) { + Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open"); + win.gURLBar.blur(); + + info("Check the keyboard shortcut."); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + + await UrlbarTestUtils.promiseSearchComplete(win); + Assert.equal(state.selectionStart, win.gURLBar.selectionStart); + Assert.equal(state.selectionEnd, win.gURLBar.selectionEnd); + + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + info("Focus with the mouse."); + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win); + }); + + await UrlbarTestUtils.promiseSearchComplete(win); + Assert.equal(state.selectionStart, win.gURLBar.selectionStart); + Assert.equal(state.selectionEnd, win.gURLBar.selectionEnd); +} + +async function checkDoesNotOpenOnFocus(win) { + Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open"); + win.gURLBar.blur(); + + info("Check the keyboard shortcut."); + let promiseState = checkPanelStatePersists(win, false); + win.document.getElementById("Browser:OpenLocation").doCommand(); + await promiseState; + win.gURLBar.blur(); + info("Focus with the mouse."); + promiseState = checkPanelStatePersists(win, false); + EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win); + await promiseState; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", true]], + }); + // Add some history for the empty panel and autofill. + await PlacesTestUtils.addVisits([ + { + uri: "https://example.com/", + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }, + { + uri: "https://example.com/foo/", + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }, + ]); + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +async function test_window(win) { + for (let url of ["about:newtab", "about:home", "https://example.com/"]) { + // withNewTab may hang on preloaded pages, thus instead of waiting for load + // we just wait for the expected currentURI value. + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url, waitForLoad: false }, + async browser => { + await TestUtils.waitForCondition( + () => win.gBrowser.currentURI.spec == url, + "Ensure we're on the expected page" + ); + + // In one case use a value that triggers autofill. + let autofill = url == "https://example.com/"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: autofill ? "ex" : "foo", + fireInputEvent: true, + }); + let { value, selectionStart, selectionEnd } = win.gURLBar; + if (!autofill) { + selectionStart = 0; + } + info("expected " + value + " " + selectionStart + " " + selectionEnd); + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + + info("The panel should open when there's a search string"); + await checkOpensOnFocus(win, { value, selectionStart, selectionEnd }); + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + } + ); + } +} + +add_task(async function test_normalWindow() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await test_window(win); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_privateWindow() { + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await test_window(privateWin); + await BrowserTestUtils.closeWindow(privateWin); +}); + +add_task(async function test_tabSwitch() { + info("Check that switching tabs reopens the view."); + let win = await BrowserTestUtils.openNewBrowserWindow(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "ex", + fireInputEvent: true, + }); + let { value, selectionStart, selectionEnd } = win.gURLBar; + Assert.equal(value, "example.com/", "Check autofill value"); + Assert.ok( + selectionStart > 0 && selectionEnd > selectionStart, + "Check autofill selection" + ); + + Assert.ok(win.gURLBar.focused, "The urlbar should be focused"); + let tab1 = win.gBrowser.selectedTab; + + async function check_autofill() { + // The urlbar code waits for both TabSelect and the focus change, thus + // we can't just wait for search completion here, we have to poll for a + // value. + await TestUtils.waitForCondition( + () => win.gURLBar.value == "example.com/", + "wait for autofill value" + ); + // Ensure stable results. + await UrlbarTestUtils.promiseSearchComplete(win); + Assert.equal(selectionStart, win.gURLBar.selectionStart); + Assert.equal(selectionEnd, win.gURLBar.selectionEnd); + } + + info("Open a new tab with the same search"); + let tab2 = await BrowserTestUtils.openNewForegroundTab(win.gBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "ex", + fireInputEvent: true, + }); + + info("Switch across tabs"); + for (let tab of win.gBrowser.tabs) { + await UrlbarTestUtils.promisePopupOpen(win, async () => { + await BrowserTestUtils.switchTab(win.gBrowser, tab); + }); + await check_autofill(); + } + + info("Close tab and check the view is open."); + await UrlbarTestUtils.promisePopupOpen(win, () => { + BrowserTestUtils.removeTab(tab2); + }); + await check_autofill(); + + info("Open a new tab with a different search"); + tab2 = await BrowserTestUtils.openNewForegroundTab(win.gBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "xam", + fireInputEvent: true, + }); + + info("Switch to the first tab and check the panel remains open"); + let promiseState = checkPanelStatePersists(win, true); + await BrowserTestUtils.switchTab(win.gBrowser, tab1); + await promiseState; + await UrlbarTestUtils.promiseSearchComplete(win); + await check_autofill(); + + info("Switch to the second tab and check the panel remains open"); + promiseState = checkPanelStatePersists(win, true); + await BrowserTestUtils.switchTab(win.gBrowser, tab2); + await promiseState; + await UrlbarTestUtils.promiseSearchComplete(win); + Assert.equal(win.gURLBar.value, "xam", "check value"); + Assert.equal(win.gURLBar.selectionStart, 3); + Assert.equal(win.gURLBar.selectionEnd, 3); + + info("autofill in tab2, switch to tab1, then back to tab2 with the mouse"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "e", + fireInputEvent: true, + }); + // Adjust selection start, we are using a different search string. + await BrowserTestUtils.switchTab(win.gBrowser, tab1); + await UrlbarTestUtils.promiseSearchComplete(win); + await check_autofill(); + tab2.click(); + selectionStart = 1; + await check_autofill(); + + info("Check we don't rerun a search if the shortcut is used on an open view"); + EventUtils.synthesizeKey("KEY_Backspace", {}, win); + await UrlbarTestUtils.promiseSearchComplete(win); + Assert.ok(win.gURLBar.view.isOpen, "The view should be open"); + Assert.equal(win.gURLBar.value, "e", "The value should be the typed one"); + win.document.getElementById("Browser:OpenLocation").doCommand(); + // A search should not run here, so there's nothing to wait for. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 300)); + Assert.ok(win.gURLBar.view.isOpen, "The view should be open"); + Assert.equal(win.gURLBar.value, "e", "The value should not change"); + + info( + "Tab switch from an empty search tab with unfocused urlbar to a tab with a search string and a focused urlbar" + ); + win.gURLBar.value = ""; + win.gURLBar.blur(); + await UrlbarTestUtils.promisePopupOpen(win, async () => { + await BrowserTestUtils.switchTab(win.gBrowser, tab1); + }); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_tabSwitch_pageproxystate() { + info("Switching tabs on valid pageproxystate doesn't reopen."); + + info("Adding some visits for the empty panel"); + await PlacesTestUtils.addVisits([ + "https://example.com/", + "https://example.org/", + ]); + registerCleanupFunction(PlacesUtils.history.clear); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + BrowserTestUtils.startLoadingURIString( + win.gBrowser.selectedBrowser, + "about:robots" + ); + let tab1 = win.gBrowser.selectedTab; + + info("Open a new tab and the empty search"); + let tab2 = await BrowserTestUtils.openNewForegroundTab(win.gBrowser); + await UrlbarTestUtils.promisePopupOpen(win, async () => { + win.gURLBar.focus(); + // On Linux and Mac down moves caret to the end of the text unless it's + // there already. + win.gURLBar.selectionStart = win.gURLBar.selectionEnd = + win.gURLBar.value.length; + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + let result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + Assert.notEqual(result.url, "about:robots"); + + info("Switch to the first tab and start searching with DOWN"); + await UrlbarTestUtils.promisePopupClose(win, async () => { + await BrowserTestUtils.switchTab(win.gBrowser, tab1); + }); + await checkPanelStatePersists(win, false); + await UrlbarTestUtils.promisePopupOpen(win, async () => { + win.gURLBar.focus(); + // On Linux and Mac down moves caret to the end of the text unless it's + // there already. + win.gURLBar.selectionStart = win.gURLBar.selectionEnd = + win.gURLBar.value.length; + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + + info("Switcihng to the second tab should not reopen the search"); + await UrlbarTestUtils.promisePopupClose(win, async () => { + await BrowserTestUtils.switchTab(win.gBrowser, tab2); + }); + await checkPanelStatePersists(win, false); + + info("Switching to the first tab should not reopen the search"); + let promiseState = await checkPanelStatePersists(win, false); + await BrowserTestUtils.switchTab(win.gBrowser, tab1); + await promiseState; + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_tabSwitch_emptySearch() { + info("Switching between empty-search tabs should not reopen the view."); + let win = await BrowserTestUtils.openNewBrowserWindow(); + + info("Open the empty search"); + let tab1 = win.gBrowser.selectedTab; + await UrlbarTestUtils.promisePopupOpen(win, async () => { + win.gURLBar.focus(); + // On Linux and Mac down moves caret to the end of the text unless it's + // there already. + win.gURLBar.selectionStart = win.gURLBar.selectionEnd = + win.gURLBar.value.length; + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + + info("Open a new tab and the empty search"); + let tab2 = await BrowserTestUtils.openNewForegroundTab(win.gBrowser); + await UrlbarTestUtils.promisePopupOpen(win, async () => { + win.gURLBar.focus(); + // On Linux and Mac down moves caret to the end of the text unless it's + // there already. + win.gURLBar.selectionStart = win.gURLBar.selectionEnd = + win.gURLBar.value.length; + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + + info("Switching to the first tab should not reopen the view"); + await UrlbarTestUtils.promisePopupClose(win, async () => { + await BrowserTestUtils.switchTab(win.gBrowser, tab1); + }); + await checkPanelStatePersists(win, false); + + info("Switching to the second tab should not reopen the view"); + let promiseState = await checkPanelStatePersists(win, false); + await BrowserTestUtils.switchTab(win.gBrowser, tab2); + await promiseState; + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_pageproxystate_valid() { + info("Focusing on valid pageproxystate should not reopen the view."); + let win = await BrowserTestUtils.openNewBrowserWindow(); + + info("Search for a full url and confirm it with Enter"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "about:robots", + fireInputEvent: true, + }); + let loadedPromise = BrowserTestUtils.browserLoaded( + win.gBrowser.selectedBrowser + ); + EventUtils.synthesizeKey("KEY_Enter", {}, win); + await loadedPromise; + + Assert.ok(!win.gURLBar.focused, "The urlbar should not be focused"); + info("Focus the urlbar"); + win.document.getElementById("Browser:OpenLocation").doCommand(); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_allowAutofill() { + info("Check we respect allowAutofill from the last search"); + let win = await BrowserTestUtils.openNewBrowserWindow(); + + await UrlbarTestUtils.promisePopupOpen(win, async () => { + await selectAndPaste("e", win); + }); + Assert.equal(win.gURLBar.value, "e", "Should not autofill"); + let context = await win.gURLBar.lastQueryContextPromise; + Assert.equal(context.allowAutofill, false, "Check initial allowAutofill"); + await UrlbarTestUtils.promisePopupClose(win); + + await UrlbarTestUtils.promisePopupOpen(win, async () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + Assert.equal(win.gURLBar.value, "e", "Should not autofill"); + context = await win.gURLBar.lastQueryContextPromise; + Assert.equal(context.allowAutofill, false, "Check reopened allowAutofill"); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_clicks_after_autofill() { + info( + "Check that clickin on an autofilled input field doesn't requery, causing loss of the caret position" + ); + let win = await BrowserTestUtils.openNewBrowserWindow(); + info("autofill in tab2, switch to tab1, then back to tab2 with the mouse"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "e", + fireInputEvent: true, + }); + Assert.equal(win.gURLBar.value, "example.com/", "Should have autofilled"); + + // Check single click. + let input = win.gURLBar.inputField; + EventUtils.synthesizeMouse(input, 30, 10, {}, win); + // Wait a bit, in case of a mistake this would run a query, otherwise not. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 300)); + Assert.ok(win.gURLBar.selectionStart < win.gURLBar.value.length); + Assert.equal(win.gURLBar.selectionStart, win.gURLBar.selectionEnd); + + // Check double click. + EventUtils.synthesizeMouse(input, 30, 10, { clickCount: 2 }, win); + // Wait a bit, in case of a mistake this would run a query, otherwise not. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 300)); + Assert.ok(win.gURLBar.selectionStart < win.gURLBar.value.length); + Assert.ok(win.gURLBar.selectionEnd > win.gURLBar.selectionStart); + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/urlbar/tests/browser/browser_revert.js b/browser/components/urlbar/tests/browser/browser_revert.js new file mode 100644 index 0000000000..b68ad0ff91 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_revert.js @@ -0,0 +1,33 @@ +// Test reverting the urlbar value with ESC after a tab switch. + +add_task(async function () { + registerCleanupFunction(PlacesUtils.history.clear); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "http://example.com", + }, + async function (browser) { + let originalValue = gURLBar.value; + let tab = gBrowser.selectedTab; + info("Put a typed value."); + gBrowser.userTypedValue = "foobar"; + info("Switch tabs."); + gBrowser.selectedTab = gBrowser.tabs[0]; + gBrowser.selectedTab = tab; + Assert.equal( + gURLBar.value, + "foobar", + "location bar displays typed value" + ); + + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Escape"); + Assert.equal( + gURLBar.value, + originalValue, + "ESC reverted the location bar value" + ); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchFunction.js b/browser/components/urlbar/tests/browser/browser_searchFunction.js new file mode 100644 index 0000000000..0a272f9f01 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchFunction.js @@ -0,0 +1,278 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test checks the urlbar.search() function. + +"use strict"; + +const ALIAS = "@enginealias"; +let aliasEngine; + +add_setup(async function () { + // Run this in a new tab, to ensure all the locationchange notifications have + // fired. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await SearchTestUtils.installSearchExtension({ + keyword: ALIAS, + }); + aliasEngine = Services.search.getEngineByName("Example"); + + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(tab); + gURLBar.handleRevert(); + }); +}); + +// Calls search() with a normal, non-"@engine" search-string argument. +add_task(async function basic() { + gURLBar.blur(); + gURLBar.search("basic"); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + await assertUrlbarValue("basic"); + + assertOneOffButtonsVisible(true); + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Calls search() with an invalid "@engine" search engine alias so that the +// one-off search buttons are disabled. +add_task(async function searchEngineAlias() { + gURLBar.blur(); + await UrlbarTestUtils.promisePopupOpen(window, () => + gURLBar.search("@example") + ); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + UrlbarTestUtils.assertSearchMode(window, null); + await assertUrlbarValue("@example"); + + assertOneOffButtonsVisible(false); + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + + // Open the popup again (by doing another search) to make sure the one-off + // buttons are shown -- i.e., that we didn't accidentally break them. + await UrlbarTestUtils.promisePopupOpen(window, () => + gURLBar.search("not an engine alias") + ); + await assertUrlbarValue("not an engine alias"); + assertOneOffButtonsVisible(true); + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +add_task(async function searchRestriction() { + gURLBar.blur(); + await UrlbarTestUtils.promisePopupOpen(window, () => + gURLBar.search(UrlbarTokenizer.RESTRICT.SEARCH) + ); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: UrlbarSearchUtils.getDefaultEngine().name, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + // Entry is "other" because we didn't pass searchModeEntry to search(). + entry: "other", + }); + assertOneOffButtonsVisible(true); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function historyRestriction() { + gURLBar.blur(); + await UrlbarTestUtils.promisePopupOpen(window, () => + gURLBar.search(UrlbarTokenizer.RESTRICT.HISTORY) + ); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + entry: "other", + }); + assertOneOffButtonsVisible(true); + Assert.ok(!gURLBar.value, "The Urlbar has no value."); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function historyRestrictionWithString() { + gURLBar.blur(); + // The leading and trailing spaces are intentional to verify that search() + // preserves them. + let searchString = " foo bar "; + await UrlbarTestUtils.promisePopupOpen(window, () => + gURLBar.search(`${UrlbarTokenizer.RESTRICT.HISTORY} ${searchString}`) + ); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + entry: "other", + }); + // We don't use assertUrlbarValue here since we expect to open a local search + // mode. In those modes, we don't show a heuristic search result, which + // assertUrlbarValue checks for. + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal( + gURLBar.value, + searchString, + "The Urlbar value should be the search string." + ); + assertOneOffButtonsVisible(true); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function tagRestriction() { + gURLBar.blur(); + await UrlbarTestUtils.promisePopupOpen(window, () => + gURLBar.search(UrlbarTokenizer.RESTRICT.TAG) + ); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + // Since tags are not a supported search mode, we should just insert the tag + // restriction token and not enter search mode. + await UrlbarTestUtils.assertSearchMode(window, null); + await assertUrlbarValue(`${UrlbarTokenizer.RESTRICT.TAG} `); + assertOneOffButtonsVisible(true); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Calls search() twice with the same value. The popup should reopen. +add_task(async function searchTwice() { + gURLBar.blur(); + await UrlbarTestUtils.promisePopupOpen(window, () => gURLBar.search("test")); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + await assertUrlbarValue("test"); + assertOneOffButtonsVisible(true); + await UrlbarTestUtils.promisePopupClose(window); + + await UrlbarTestUtils.promisePopupOpen(window, () => gURLBar.search("test")); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + await assertUrlbarValue("test"); + assertOneOffButtonsVisible(true); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Calls search() during an IME composition. +add_task(async function searchIME() { + // First run a search. + gURLBar.blur(); + await UrlbarTestUtils.promisePopupOpen(window, () => gURLBar.search("test")); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + await assertUrlbarValue("test"); + // Start composition. + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeComposition({ type: "compositionstart" }) + ); + + gURLBar.search("test"); + // Unfortunately there's no other way to check we don't open the view than to + // wait for it. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + ok(!UrlbarTestUtils.isPopupOpen(window), "The panel should still be closed"); + + await UrlbarTestUtils.promisePopupOpen(window, () => + EventUtils.synthesizeComposition({ type: "compositioncommitasis" }) + ); + + assertOneOffButtonsVisible(true); + + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Calls search() with an engine alias. +add_task(async function searchWithAlias() { + await UrlbarTestUtils.promisePopupOpen(window, async () => + gURLBar.search(`${ALIAS} test`, { + searchEngine: aliasEngine, + searchModeEntry: "topsites_urlbar", + }) + ); + Assert.ok(gURLBar.hasAttribute("focused"), "Urlbar is focused"); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: aliasEngine.name, + entry: "topsites_urlbar", + }); + await assertUrlbarValue("test"); + assertOneOffButtonsVisible(true); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Calls search() and passes in a search engine without including a restriction +// token or engine alias in the search string. Simulates pasting into the newtab +// handoff field with search suggestions disabled. +add_task(async function searchEngineWithNoToken() { + await UrlbarTestUtils.promisePopupOpen(window, async () => + gURLBar.search("no-alias", { + searchEngine: aliasEngine, + searchModeEntry: "handoff", + }) + ); + + Assert.ok(gURLBar.hasAttribute("focused"), "Urlbar is focused"); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: aliasEngine.name, + entry: "handoff", + }); + await assertUrlbarValue("no-alias"); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +/** + * Asserts that the one-off search buttons are or aren't visible. + * + * @param {boolean} visible + * True if they should be visible, false if not. + */ +function assertOneOffButtonsVisible(visible) { + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + visible, + "Should show or not the one-off search buttons" + ); +} + +/** + * Asserts that the urlbar's input value is the given value. Also asserts that + * the first (heuristic) result in the popup is a search suggestion whose search + * query is the given value. + * + * @param {string} value + * The urlbar's expected value. + */ +async function assertUrlbarValue(value) { + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + + Assert.equal(gURLBar.value, value); + Assert.greater( + UrlbarTestUtils.getResultCount(window), + 0, + "Should have at least one result" + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Should have type search for the first result" + ); + // Strip search restriction token from value. + if (value[0] == UrlbarTokenizer.RESTRICT.SEARCH) { + value = value.substring(1).trim(); + } + Assert.equal( + result.searchParams.query, + value, + "Should have the correct query for the first result" + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_searchHistoryLimit.js b/browser/components/urlbar/tests/browser/browser_searchHistoryLimit.js new file mode 100644 index 0000000000..6fcde0882b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchHistoryLimit.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test checks that search values longer than + * SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH are not added to + * search history. + */ + +"use strict"; + +const { SearchSuggestionController } = ChromeUtils.importESModule( + "resource://gre/modules/SearchSuggestionController.sys.mjs" +); + +let gEngine; + +add_setup(async function () { + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + gEngine = Services.search.getEngineByName("Example"); + await UrlbarTestUtils.formHistory.clear(); + + registerCleanupFunction(async function () { + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async function sanityCheckShortString() { + const shortString = new Array( + SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH + ) + .fill("a") + .join(""); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: shortString, + }); + let url = gEngine.getSubmission(shortString).uri.spec; + let loadPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + url + ); + let addPromise = UrlbarTestUtils.formHistory.promiseChanged("add"); + EventUtils.synthesizeKey("VK_RETURN"); + await Promise.all([loadPromise, addPromise]); + + let formHistory = ( + await UrlbarTestUtils.formHistory.search({ source: gEngine.name }) + ).map(entry => entry.value); + Assert.deepEqual( + formHistory, + [shortString], + "Should find form history after adding it" + ); + + await UrlbarTestUtils.formHistory.clear(); +}); + +add_task(async function urlbar_checkLongString() { + const longString = new Array( + SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH + 1 + ) + .fill("a") + .join(""); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: longString, + }); + let url = gEngine.getSubmission(longString).uri.spec; + let loadPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + url + ); + EventUtils.synthesizeKey("VK_RETURN"); + await loadPromise; + // There's nothing we can wait for, since addition should not be happening. + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + await new Promise(resolve => setTimeout(resolve, 500)); + let formHistory = ( + await UrlbarTestUtils.formHistory.search({ source: gEngine.name }) + ).map(entry => entry.value); + Assert.deepEqual(formHistory, [], "Should not find form history"); + + await UrlbarTestUtils.formHistory.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_alias_replacement.js b/browser/components/urlbar/tests/browser/browser_searchMode_alias_replacement.js new file mode 100644 index 0000000000..9f4558e6c9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_alias_replacement.js @@ -0,0 +1,274 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that user-defined aliases are replaced by the search mode indicator. + */ + +const ALIAS = "testalias"; +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +// We make sure that aliases and search terms are correctly recognized when they +// are separated by each of these different types of spaces and combinations of +// spaces. U+3000 is the ideographic space in CJK and is commonly used by CJK +// speakers. +const TEST_SPACES = [" ", "\u3000", " \u3000", "\u3000 "]; + +let defaultEngine, aliasEngine; + +add_setup(async function () { + defaultEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + defaultEngine.alias = "@default"; + await SearchTestUtils.installSearchExtension({ + keyword: ALIAS, + }); + aliasEngine = Services.search.getEngineByName("Example"); +}); + +// An incomplete alias should not be replaced. +add_task(async function incompleteAlias() { + // Check that a non-fully typed alias is not replaced. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS.slice(0, -1), + }); + await UrlbarTestUtils.assertSearchMode(window, null); + + // Type a space just to make sure it's not replaced. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey(" "); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.value, + ALIAS.slice(0, -1) + " ", + "The typed value should be unchanged except for the space." + ); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// A complete alias without a trailing space should not be replaced. +add_task(async function noTrailingSpace() { + let value = ALIAS; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + }); + await UrlbarTestUtils.assertSearchMode(window, null); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// A complete typed alias without a trailing space should not be replaced. +add_task(async function noTrailingSpace_typed() { + // Start by searching for the alias minus its last char. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS.slice(0, -1), + }); + await UrlbarTestUtils.assertSearchMode(window, null); + + // Now type the last char. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey(ALIAS.slice(-1)); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.value, + ALIAS, + "The typed value should be the full alias." + ); + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// A complete alias with a trailing space should be replaced. +add_task(async function trailingSpace() { + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS + spaces, + }); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: aliasEngine.name, + entry: "typed", + }); + Assert.ok(!gURLBar.value, "The urlbar value should be cleared."); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + } +}); + +// A complete alias should be replaced after typing a space. +add_task(async function trailingSpace_typed() { + for (let spaces of TEST_SPACES) { + if (spaces.length != 1) { + continue; + } + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS, + }); + await UrlbarTestUtils.assertSearchMode(window, null); + + // We need to wait for two searches: The first enters search mode, the second + // does the search in search mode. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey(spaces); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: aliasEngine.name, + entry: "typed", + }); + Assert.ok(!gURLBar.value, "The urlbar value should be cleared."); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + } +}); + +// A complete alias with a trailing space should be replaced, and the query +// after the trailing space should be the new value of the input. +add_task(async function trailingSpace_query() { + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS + spaces + "query", + }); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: aliasEngine.name, + entry: "typed", + }); + Assert.equal( + gURLBar.value, + "query", + "The urlbar value should be the query." + ); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + } +}); + +add_task(async function () { + info("Test search mode when typing an alias after selecting one-off button"); + + info("Open the result popup"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + info("Select one of one-off button"); + const oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + ok(oneOffs.selectedButton, "There is a selected one-off button"); + const selectedEngine = oneOffs.selectedButton.engine; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: selectedEngine.name, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + entry: "oneoff", + isPreview: true, + }); + + info("Type a search engine alias and query"); + const inputString = "@default query"; + inputString.split("").forEach(c => EventUtils.synthesizeKey(c)); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal( + gURLBar.value, + inputString, + "Alias and query is inputed correctly to the urlbar" + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: selectedEngine.name, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + entry: "oneoff", + }); + + // When starting typing, as the search mode is confirmed, the one-off + // selection is removed. + ok(!oneOffs.selectedButton, "There is no any selected one-off button"); + + // Clean up + gURLBar.value = ""; + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function () { + info( + "Test search mode after removing current search mode when multiple aliases are written" + ); + + info("Open the result popup with multiple aliases"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@default testalias @default", + }); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: defaultEngine.name, + entry: "typed", + }); + Assert.equal( + gURLBar.value, + "testalias @default", + "The value on the urlbar is correct" + ); + + info("Exit search mode by clicking"); + const indicator = gURLBar.querySelector("#urlbar-search-mode-indicator"); + EventUtils.synthesizeMouseAtCenter(indicator, { type: "mouseover" }, window); + const closeButton = gURLBar.querySelector( + "#urlbar-search-mode-indicator-close" + ); + const searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(closeButton, {}, window); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: aliasEngine.name, + entry: "typed", + }); + Assert.equal(gURLBar.value, "@default", "The value on the urlbar is correct"); + + // Clean up + gURLBar.value = ""; + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +/** + * Returns an array of code points in the given string. Each code point is + * returned as a hexidecimal string. + * + * @param {string} str + * The code points of this string will be returned. + * @returns {Array} + * Array of code points in the string, where each is a hexidecimal string. + */ +function codePoints(str) { + return str.split("").map(s => s.charCodeAt(0).toString(16)); +} diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_autofill.js b/browser/components/urlbar/tests/browser/browser_searchMode_autofill.js new file mode 100644 index 0000000000..96c9b7212f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_autofill.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that autofill is cleared if a remote search mode is entered but still + * works for local search modes. + */ + +"use strict"; + +add_setup(async function () { + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits([{ uri: "http://example.com/" }]); + } + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + let defaultEngine = Services.search.getEngineByName("Example"); + await Services.search.moveEngine(defaultEngine, 0); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +// Tests that autofill is cleared when entering a remote search mode and that +// autofill doesn't happen when in that mode. +add_task(async function remote() { + info("Sanity check: we autofill in a normal search."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ex", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill, "We're autofilling."); + Assert.equal( + gURLBar.value, + "example.com/", + "Urlbar contains the autofilled URL." + ); + info("Enter remote search mode and check autofill is cleared."); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal(gURLBar.value, "ex", "Urlbar contains the typed string."); + + info("Continue typing and check that we're not autofilling."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exa", + fireInputEvent: true, + }); + + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(!details.autofill, "We're not autofilling."); + Assert.equal(gURLBar.value, "exa", "Urlbar contains the typed string."); + + info("Exit remote search mode and check that we now autofill."); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill, "We're autofilling."); + Assert.equal( + gURLBar.value, + "example.com/", + "Urlbar contains the typed string." + ); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); +}); + +// Tests that autofill works as normal when entering and when in a local search +// mode. +add_task(async function local() { + info("Sanity check: we autofill in a normal search."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ex", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill, "We're autofilling."); + Assert.equal( + gURLBar.value, + "example.com/", + "Urlbar contains the autofilled URL." + ); + info("Enter local search mode and check autofill is preserved."); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }); + Assert.equal( + gURLBar.value, + "example.com/", + "Urlbar contains the autofilled URL." + ); + + info("Continue typing and check that we're autofilling."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exa", + fireInputEvent: true, + }); + + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill, "We're autofilling."); + Assert.equal( + gURLBar.value, + "example.com/", + "Urlbar contains the autofilled URL." + ); + + info("Exit local search mode and check that nothing has changed."); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill, "We're autofilling."); + Assert.equal( + gURLBar.value, + "example.com/", + "Urlbar contains the typed string." + ); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_clickLink.js b/browser/components/urlbar/tests/browser/browser_searchMode_clickLink.js new file mode 100644 index 0000000000..d037c77bbb --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_clickLink.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that search mode is exited after clicking a link and loading a page in + * the current tab. + */ + +"use strict"; + +const LINK_PAGE_URL = + "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/dummy_page.html"; + +// Opens a new tab containing a link, enters search mode, and clicks the link. +// Uses a variety of search strings and link hrefs in order to hit different +// branches in setURI. Search mode should be exited in all cases, and the href +// in the link should be opened. +add_task(async function clickLink() { + for (let test of [ + // searchString, href to use in the link + [LINK_PAGE_URL, LINK_PAGE_URL], + [LINK_PAGE_URL, "http://www.example.com/"], + ["test", LINK_PAGE_URL], + ["test", "http://www.example.com/"], + [null, LINK_PAGE_URL], + [null, "http://www.example.com/"], + ]) { + await doClickLinkTest(...test); + } +}); + +async function doClickLinkTest(searchString, href) { + info( + "doClickLinkTest with args: " + + JSON.stringify({ + searchString, + href, + }) + ); + + await BrowserTestUtils.withNewTab(LINK_PAGE_URL, async () => { + if (searchString) { + // Do a search with the search string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + Assert.ok( + gBrowser.selectedBrowser.userTypedValue, + "userTypedValue should be defined" + ); + } else { + // Open top sites. + await UrlbarTestUtils.promisePopupOpen(window, () => { + document.getElementById("Browser:OpenLocation").doCommand(); + }); + Assert.strictEqual( + gBrowser.selectedBrowser.userTypedValue, + null, + "userTypedValue should be null" + ); + } + + // Enter search mode and then close the popup so we can click the link in + // the page. + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + + // Add a link to the page and click it. + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await ContentTask.spawn(gBrowser.selectedBrowser, href, async cHref => { + let link = this.content.document.createElement("a"); + link.textContent = "Click me"; + link.href = cHref; + this.content.document.body.append(link); + link.click(); + }); + await loadPromise; + Assert.equal( + gBrowser.currentURI.spec, + href, + "Should have loaded the href URL" + ); + + await UrlbarTestUtils.assertSearchMode(window, null); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_engineRemoval.js b/browser/components/urlbar/tests/browser/browser_searchMode_engineRemoval.js new file mode 100644 index 0000000000..f5eab77789 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_engineRemoval.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that we exit search mode when the search mode engine is removed. + */ + +"use strict"; + +// Tests that we exit search mode in the active tab when the search mode engine +// is removed. +add_task(async function activeTab() { + let extension = await SearchTestUtils.installSearchExtension( + {}, + { skipUnload: true } + ); + let engine = Services.search.getEngineByName("Example"); + await Services.search.moveEngine(engine, 0); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ex", + }); + await UrlbarTestUtils.enterSearchMode(window); + // Sanity check: we are in the correct search mode. + await UrlbarTestUtils.assertSearchMode(window, { + engineName: engine.name, + entry: "oneoff", + }); + await extension.unload(); + // Check that we are no longer in search mode. + await UrlbarTestUtils.assertSearchMode(window, null); +}); + +// Tests that we exit search mode in a background tab when the search mode +// engine is removed. +add_task(async function backgroundTab() { + let extension = await SearchTestUtils.installSearchExtension( + {}, + { skipUnload: true } + ); + let engine = Services.search.getEngineByName("Example"); + await Services.search.moveEngine(engine, 0); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ex", + }); + await UrlbarTestUtils.enterSearchMode(window); + let tab1 = gBrowser.selectedTab; + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // Sanity check: tab1 is still in search mode. + await BrowserTestUtils.switchTab(gBrowser, tab1); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: engine.name, + entry: "oneoff", + }); + + // Switch back to tab2 so tab1 is in the background when the engine is + // removed. + await BrowserTestUtils.switchTab(gBrowser, tab2); + // tab2 shouldn't be in search mode. + await UrlbarTestUtils.assertSearchMode(window, null); + await extension.unload(); + + // tab1 should have exited search mode. + await BrowserTestUtils.switchTab(gBrowser, tab1); + await UrlbarTestUtils.assertSearchMode(window, null); + BrowserTestUtils.removeTab(tab2); +}); + +// Tests that we exit search mode in a background window when the search mode +// engine is removed. +add_task(async function backgroundWindow() { + let extension = await SearchTestUtils.installSearchExtension( + {}, + { skipUnload: true } + ); + let engine = Services.search.getEngineByName("Example"); + await Services.search.moveEngine(engine, 0); + + let win1 = window; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win1, + value: "ex", + }); + await UrlbarTestUtils.enterSearchMode(win1); + let win2 = await BrowserTestUtils.openNewBrowserWindow(); + + // Sanity check: win1 is still in search mode. + win1.focus(); + await UrlbarTestUtils.assertSearchMode(win1, { + engineName: engine.name, + entry: "oneoff", + }); + + // Switch back to win2 so win1 is in the background when the engine is + // removed. + win2.focus(); + // win2 shouldn't be in search mode. + await UrlbarTestUtils.assertSearchMode(win2, null); + await extension.unload(); + + // win1 should not be in search mode. + win1.focus(); + await UrlbarTestUtils.assertSearchMode(win1, null); + await BrowserTestUtils.closeWindow(win2); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_excludeResults.js b/browser/components/urlbar/tests/browser/browser_searchMode_excludeResults.js new file mode 100644 index 0000000000..0e9471280e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_excludeResults.js @@ -0,0 +1,217 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that results with hostnames other than the search mode engine are not + * shown. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs", +}); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", false], + ["browser.urlbar.autoFill", false], + // Special prefs for remote tabs. + ["services.sync.username", "fake"], + ["services.sync.syncedTabs.showRemoteTabs", true], + ], + }); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + // Note that the result domain is subdomain.example.ca. We still expect to + // match with example.com results because we ignore subdomains and the public + // suffix in this check. + await SearchTestUtils.installSearchExtension( + { + search_url: "https://subdomain.example.ca/", + }, + { setAsDefault: true } + ); + let engine = Services.search.getEngineByName("Example"); + await Services.search.moveEngine(engine, 0); + + const REMOTE_TAB = { + id: "7cqCr77ptzX3", + type: "client", + lastModified: 1492201200, + name: "Nightly on MacBook-Pro", + clientType: "desktop", + tabs: [ + { + type: "tab", + title: "Test Remote", + url: "https://example.com", + icon: UrlbarUtils.ICON.DEFAULT, + client: "7cqCr77ptzX3", + lastUsed: Math.floor(Date.now() / 1000), + }, + { + type: "tab", + title: "Test Remote 2", + url: "https://example-2.com", + icon: UrlbarUtils.ICON.DEFAULT, + client: "7cqCr77ptzX3", + lastUsed: Math.floor(Date.now() / 1000), + }, + ], + }; + + const sandbox = sinon.createSandbox(); + + let originalSyncedTabsInternal = SyncedTabs._internal; + SyncedTabs._internal = { + isConfiguredToSyncTabs: true, + hasSyncedThisSession: true, + getTabClients() { + return Promise.resolve([]); + }, + syncTabs() { + return Promise.resolve(); + }, + }; + + // Tell the Sync XPCOM service it is initialized. + let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + let oldWeaveServiceReady = weaveXPCService.ready; + weaveXPCService.ready = true; + + sandbox + .stub(SyncedTabs._internal, "getTabClients") + .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {}))); + + // Reset internal cache in UrlbarProviderRemoteTabs. + Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs"); + + registerCleanupFunction(async function () { + sandbox.restore(); + weaveXPCService.ready = oldWeaveServiceReady; + SyncedTabs._internal = originalSyncedTabsInternal; + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function basic() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 3, + "We have three results" + ); + let firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + firstResult.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "The first result is the heuristic search result." + ); + let secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + secondResult.type, + UrlbarUtils.RESULT_TYPE.REMOTE_TAB, + "The second result is a remote tab." + ); + let thirdResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + thirdResult.type, + UrlbarUtils.RESULT_TYPE.REMOTE_TAB, + "The third result is a remote tab." + ); + + await UrlbarTestUtils.enterSearchMode(window); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "We have two results. The second remote tab result is excluded despite matching the search string." + ); + firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + firstResult.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "The first result is the heuristic search result." + ); + secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + secondResult.type, + UrlbarUtils.RESULT_TYPE.REMOTE_TAB, + "The second result is a remote tab." + ); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// For engines with an invalid TLD, we filter on the entire domain. +add_task(async function malformedEngine() { + await SearchTestUtils.installSearchExtension({ + name: "TestMalformed", + search_url: "https://example.foobar/", + }); + let badEngine = Services.search.getEngineByName("TestMalformed"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 4, + "We have four results" + ); + let firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + firstResult.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "The first result is the heuristic search result." + ); + let secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + secondResult.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "The second result is the tab-to-search onboarding result for our malformed engine." + ); + let thirdResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal( + thirdResult.type, + UrlbarUtils.RESULT_TYPE.REMOTE_TAB, + "The third result is a remote tab." + ); + let fourthResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 3); + Assert.equal( + fourthResult.type, + UrlbarUtils.RESULT_TYPE.REMOTE_TAB, + "The fourth result is a remote tab." + ); + + await UrlbarTestUtils.enterSearchMode(window, { + engineName: badEngine.name, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "We only have one result." + ); + firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(firstResult.heuristic, "The first result is heuristic."); + Assert.equal( + firstResult.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "The first result is the heuristic search result." + ); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js b/browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js new file mode 100644 index 0000000000..c979e86235 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js @@ -0,0 +1,219 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests heuristic results in search mode. + */ + +"use strict"; + +add_setup(async function () { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + // Add a new mock default engine so we don't hit the network. + await SearchTestUtils.installSearchExtension( + { name: "Test" }, + { setAsDefault: true } + ); + + // Add one bookmark we'll use below. + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "https://example.com/bookmark", + }); + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +// Enters search mode with no results. +add_task(async function noResults() { + // Do a search that doesn't match our bookmark and enter bookmark search mode. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "doesn't match anything", + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 0, + "Zero results since no bookmark matches" + ); + + // Press enter. Nothing should happen. + let promise = waitForLoadStartOrTimeout(); + EventUtils.synthesizeKey("KEY_Enter"); + await Assert.rejects(promise, /timed out/, "Nothing should have loaded"); + + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Enters a local search mode (bookmarks) with a matching result. No heuristic +// should be present. +add_task(async function localNoHeuristic() { + // Do a search that matches our bookmark and enter bookmarks search mode. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "bookmark", + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "There should be one result" + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + "Result source should be BOOKMARKS" + ); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.URL, + "Result type should be URL" + ); + Assert.equal( + result.url, + "https://example.com/bookmark", + "Result URL is our bookmark URL" + ); + Assert.ok(!result.heuristic, "Result should not be heuristic"); + + // Press enter. Nothing should happen. + let promise = waitForLoadStartOrTimeout(); + EventUtils.synthesizeKey("KEY_Enter"); + await Assert.rejects(promise, /timed out/, "Nothing should have loaded"); + + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Enters a local search mode (bookmarks) with a matching autofill result. The +// result should be the heuristic. +add_task(async function localAutofill() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + // Do a search that autofills our bookmark's origin and enter bookmarks + // search mode. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example", + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "There should be two results" + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.HISTORY, + "Result source should be HISTORY" + ); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.URL, + "Result type should be URL" + ); + Assert.equal( + result.url, + "https://example.com/", + "Result URL is our bookmark's origin" + ); + Assert.ok(result.heuristic, "Result should be heuristic"); + Assert.ok(result.autofill, "Result should be autofill"); + + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + "Result source should be BOOKMARKS" + ); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.URL, + "Result type should be URL" + ); + Assert.equal( + result.url, + "https://example.com/bookmark", + "Result URL is our bookmark URL" + ); + + // Press enter. Our bookmark's origin should be loaded. + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + Assert.equal( + gBrowser.currentURI.spec, + "https://example.com/", + "Bookmark's origin should have loaded" + ); + }); +}); + +// Enters a remote engine search mode. There should be a heuristic. +add_task(async function remote() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + // Do a search and enter search mode with our test engine. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "remote", + }); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: "Test", + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "There should be one result" + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.SEARCH, + "Result source should be SEARCH" + ); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Result type should be SEARCH" + ); + Assert.ok(result.searchParams, "searchParams should be present"); + Assert.equal( + result.searchParams.engine, + "Test", + "searchParams.engine should be our test engine" + ); + Assert.equal( + result.searchParams.query, + "remote", + "searchParams.query should be our query" + ); + Assert.ok(result.heuristic, "Result should be heuristic"); + + // Press enter. The engine's SERP should load. + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + Assert.equal( + gBrowser.currentURI.spec, + "https://example.com/?q=remote", + "Engine's SERP should have loaded" + ); + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_indicator.js b/browser/components/urlbar/tests/browser/browser_searchMode_indicator.js new file mode 100644 index 0000000000..707a4ea38e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_indicator.js @@ -0,0 +1,377 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests interactions with the search mode indicator. See browser_oneOffs.js for + * more coverage. + */ + +const TEST_QUERY = "test string"; +const SUGGESTIONS_ENGINE_NAME = "searchSuggestionEngine.xml"; + +// These need to have different domains because otherwise new tab and/or +// activity stream collapses them. +const TOP_SITES_URLS = [ + "http://top-site-0.com/", + "http://top-site-1.com/", + "http://top-site-2.com/", +]; + +let suggestionsEngine; +let defaultEngine; + +add_setup(async function () { + suggestionsEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + SUGGESTIONS_ENGINE_NAME, + }); + + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + defaultEngine = Services.search.getEngineByName("Example"); + await Services.search.moveEngine(suggestionsEngine, 0); + + // Set our top sites. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.newtabpage.activity-stream.default.sites", + TOP_SITES_URLS.join(","), + ], + ], + }); + await updateTopSites(sites => + ObjectUtils.deepEqual( + sites.map(s => s.url), + TOP_SITES_URLS + ) + ); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", false], + ["browser.urlbar.suggest.quickactions", false], + ], + }); +}); + +async function verifySearchModeResultsAdded(window) { + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 3, + "There should be three results." + ); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.searchParams.engine, + suggestionsEngine.name, + "The first result should be a search result for our suggestion engine." + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + result.searchParams.suggestion, + `${TEST_QUERY}foo`, + "The second result should be a suggestion result." + ); + Assert.equal( + result.searchParams.engine, + suggestionsEngine.name, + "The second result should be a search result for our suggestion engine." + ); +} + +async function verifySearchModeResultsRemoved(window) { + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "There should only be one result." + ); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.searchParams.engine, + defaultEngine.name, + "The first result should be a search result for our default engine." + ); +} + +async function verifyTopSitesResultsAdded(window) { + Assert.equal( + UrlbarTestUtils.getResultCount(window), + TOP_SITES_URLS.length, + "Expected number of top sites results" + ); + for (let i = 0; i < TOP_SITES_URLS; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.equal( + result.url, + TOP_SITES_URLS[i], + `Expected top sites result URL at index ${i}` + ); + } +} + +// Tests that the indicator is removed when backspacing at the beginning of +// the search string. +add_task(async function backspace() { + // View open, with string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + await UrlbarTestUtils.enterSearchMode(window); + await verifySearchModeResultsAdded(window); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + await verifySearchModeResultsRemoved(window); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open."); + + // View open, no string (i.e., top sites). + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.enterSearchMode(window); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open."); + await verifyTopSitesResultsAdded(window); + await UrlbarTestUtils.promisePopupClose(window); + + // View closed, with string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + await UrlbarTestUtils.enterSearchMode(window); + await verifySearchModeResultsAdded(window); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + await verifySearchModeResultsRemoved(window); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is now open."); + + // View closed, no string (i.e., top sites). + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.enterSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open."); + await verifyTopSitesResultsAdded(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function escapeOnInitialPage() { + info("Tests the indicator's interaction with the ESC key"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + await UrlbarTestUtils.enterSearchMode(window); + await verifySearchModeResultsAdded(window); + + EventUtils.synthesizeKey("KEY_Escape"); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window, "UrlbarView is closed.")); + Assert.equal(gURLBar.value, TEST_QUERY, "Urlbar value hasn't changed."); + + let oneOffs = + UrlbarTestUtils.getOneOffSearchButtons(window).getSelectableButtons(true); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + + EventUtils.synthesizeKey("KEY_Escape"); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window, "UrlbarView is closed.")); + Assert.ok(!gURLBar.value, "Urlbar value is empty."); + await UrlbarTestUtils.assertSearchMode(window, null); +}); + +add_task(async function escapeOnBrowsingPage() { + info("Tests the indicator's interaction with the ESC key on browsing page"); + + await BrowserTestUtils.withNewTab("http://example.com", async browser => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + await UrlbarTestUtils.enterSearchMode(window); + await verifySearchModeResultsAdded(window); + + EventUtils.synthesizeKey("KEY_Escape"); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window, "UrlbarView is closed.")); + Assert.equal(gURLBar.value, TEST_QUERY, "Urlbar value hasn't changed."); + + const oneOffs = + UrlbarTestUtils.getOneOffSearchButtons(window).getSelectableButtons(true); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + + EventUtils.synthesizeKey("KEY_Escape"); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window, "UrlbarView is closed.")); + Assert.equal( + gURLBar.value, + UrlbarTestUtils.trimURL("http://example.com"), + "Urlbar value indicates the browsing page." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + }); +}); + +// Tests that the indicator is removed when its close button is clicked. +add_task(async function click_close() { + // Clicking close with the view open, with string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + await UrlbarTestUtils.enterSearchMode(window); + await verifySearchModeResultsAdded(window); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await verifySearchModeResultsRemoved(window); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open."); + await UrlbarTestUtils.promisePopupClose(window); + + // Clicking close with the view open, no string (i.e., top sites). + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.enterSearchMode(window); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open."); + await verifyTopSitesResultsAdded(window); + await UrlbarTestUtils.promisePopupClose(window); + + // Clicking close with the view closed, with string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + await UrlbarTestUtils.enterSearchMode(window); + await verifySearchModeResultsAdded(window); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.exitSearchMode(window, { + clickClose: true, + waitForSearch: false, + }); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Urlbar view is closed."); + + // Clicking close with the view closed, no string (i.e., top sites). + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.enterSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.exitSearchMode(window, { + clickClose: true, + waitForSearch: false, + }); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Urlbar view is closed."); +}); + +// Tests that Accel+K enters search mode with the default engine. Also tests +// that Accel+K highlights the typed search string. +add_task(async function keyboard_shortcut() { + const query = "test query"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.selectionStart, + gURLBar.selectionEnd, + "The search string is not highlighted." + ); + EventUtils.synthesizeKey("k", { accelKey: true }); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + engineName: defaultEngine.name, + entry: "shortcut", + }); + Assert.equal(gURLBar.value, query, "The search string was not cleared."); + Assert.equal(gURLBar.selectionStart, 0); + Assert.equal( + gURLBar.selectionEnd, + query.length, + "The search string is highlighted." + ); + await UrlbarTestUtils.exitSearchMode(window, { + clickClose: true, + waitForSearch: false, + }); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); +}); + +// Tests that the Tools:Search menu item enters search mode with the default +// engine. Also tests that Tools:Search highlights the typed search string. +add_task(async function menubar_item() { + const query = "test query 2"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.selectionStart, + gURLBar.selectionEnd, + "The search string is not highlighted." + ); + let command = window.document.getElementById("Tools:Search"); + command.doCommand(); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + engineName: defaultEngine.name, + entry: "shortcut", + }); + Assert.equal(gURLBar.value, query, "The search string was not cleared."); + Assert.equal(gURLBar.selectionStart, 0); + Assert.equal( + gURLBar.selectionEnd, + query.length, + "The search string is highlighted." + ); + await UrlbarTestUtils.exitSearchMode(window, { + clickClose: true, + waitForSearch: false, + }); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); +}); + +// Tests that entering search mode invalidates pageproxystate and that +// pageproxystate remains invalid after exiting search mode. +add_task(async function invalidate_pageproxystate() { + await BrowserTestUtils.withNewTab("about:robots", async function (browser) { + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.equal(gURLBar.getAttribute("pageproxystate"), "valid"); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "invalid", + "Entering search mode should clear pageproxystate." + ); + Assert.equal(gURLBar.value, "", "Urlbar value should be cleared."); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "invalid", + "Pageproxystate should still be invalid after exiting search mode." + ); + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_indicator_clickthrough.js b/browser/components/urlbar/tests/browser/browser_searchMode_indicator_clickthrough.js new file mode 100644 index 0000000000..214448ee61 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_indicator_clickthrough.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check clicking on the search mode indicator when the urlbar is not focused puts + * focus in the urlbar. + */ + +add_task(async function test() { + // Avoid remote connections. + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.suggest.enabled", false]], + }); + + await BrowserTestUtils.withNewTab("about:robots", async browser => { + // View open, with string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + const indicator = document.getElementById("urlbar-search-mode-indicator"); + Assert.ok(!BrowserTestUtils.isVisible(indicator)); + const indicatorCloseButton = document.getElementById( + "urlbar-search-mode-indicator-close" + ); + Assert.ok(!BrowserTestUtils.isVisible(indicatorCloseButton)); + const labelBox = document.getElementById("urlbar-label-box"); + Assert.ok(!BrowserTestUtils.isVisible(labelBox)); + + await UrlbarTestUtils.enterSearchMode(window); + Assert.ok(BrowserTestUtils.isVisible(indicator)); + Assert.ok(BrowserTestUtils.isVisible(indicatorCloseButton)); + Assert.ok(!BrowserTestUtils.isVisible(labelBox)); + + info("Blur the urlbar"); + gURLBar.blur(); + Assert.ok(BrowserTestUtils.isVisible(indicator)); + Assert.ok(BrowserTestUtils.isVisible(indicatorCloseButton)); + Assert.ok(!BrowserTestUtils.isVisible(labelBox)); + Assert.notEqual( + document.activeElement, + gURLBar.inputField, + "URL Bar should not be focused" + ); + + info("Focus the urlbar clicking on the indicator"); + // We intentionally turn off a11y_checks for the following click, because + // it is send to send a focus on the URL Bar with the mouse, while other + // ways to focus it are accessible for users of assistive technology and + // keyboards, thus this test can be excluded from the accessibility tests. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + EventUtils.synthesizeMouseAtCenter(indicator, {}); + AccessibilityUtils.resetEnv(); + Assert.ok(BrowserTestUtils.isVisible(indicator)); + Assert.ok(BrowserTestUtils.isVisible(indicatorCloseButton)); + Assert.ok(!BrowserTestUtils.isVisible(labelBox)); + Assert.equal( + document.activeElement, + gURLBar.inputField, + "URL Bar should be focused" + ); + + info("Leave search mode clicking on the close button"); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + Assert.ok(!BrowserTestUtils.isVisible(indicator)); + Assert.ok(!BrowserTestUtils.isVisible(indicatorCloseButton)); + Assert.ok(!BrowserTestUtils.isVisible(labelBox)); + }); + + await BrowserTestUtils.withNewTab("about:robots", async browser => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + const indicator = document.getElementById("urlbar-search-mode-indicator"); + Assert.ok(!BrowserTestUtils.isVisible(indicator)); + const indicatorCloseButton = document.getElementById( + "urlbar-search-mode-indicator-close" + ); + Assert.ok(!BrowserTestUtils.isVisible(indicatorCloseButton)); + + await UrlbarTestUtils.enterSearchMode(window); + Assert.ok(BrowserTestUtils.isVisible(indicator)); + Assert.ok(BrowserTestUtils.isVisible(indicatorCloseButton)); + + info("Blur the urlbar"); + gURLBar.blur(); + Assert.ok(BrowserTestUtils.isVisible(indicator)); + Assert.ok(BrowserTestUtils.isVisible(indicatorCloseButton)); + Assert.notEqual( + document.activeElement, + gURLBar.inputField, + "URL Bar should not be focused" + ); + + info("Leave search mode clicking on the close button while unfocussing"); + await UrlbarTestUtils.exitSearchMode(window, { + clickClose: true, + waitForSearch: false, + }); + Assert.ok(!BrowserTestUtils.isVisible(indicator)); + Assert.ok(!BrowserTestUtils.isVisible(indicatorCloseButton)); + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_localOneOffs_actionText.js b/browser/components/urlbar/tests/browser/browser_searchMode_localOneOffs_actionText.js new file mode 100644 index 0000000000..2068d4c1d5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_localOneOffs_actionText.js @@ -0,0 +1,459 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests action text shown on heuristic and search suggestions when keyboard + * navigating local one-off buttons. + */ + +"use strict"; + +const DEFAULT_ENGINE_NAME = "Test"; +const SUGGESTIONS_ENGINE_NAME = "searchSuggestionEngine.xml"; + +let engine; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.suggest.quickactions", false], + ["browser.urlbar.shortcuts.quickactions", false], + ], + }); + engine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + SUGGESTIONS_ENGINE_NAME, + setAsDefault: true, + }); + await Services.search.moveEngine(engine, 0); + + await PlacesUtils.history.clear(); + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function localOneOff() { + info("Type some text, select a local one-off, check heuristic action"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "query", + }); + Assert.ok(UrlbarTestUtils.getResultCount(window) > 1, "Sanity check results"); + + info("Alt UP to select the last local one-off."); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "the heuristic result should be selected" + ); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + let oneOffButtons = UrlbarTestUtils.getOneOffSearchButtons(window); + Assert.equal( + oneOffButtons.selectedButton.source, + UrlbarUtils.RESULT_SOURCE.HISTORY, + "A local one-off button should be selected" + ); + Assert.ok( + BrowserTestUtils.isVisible(result.element.action), + "The heuristic action should be visible" + ); + let [actionHistory, actionBookmarks] = await document.l10n.formatValues([ + { id: "urlbar-result-action-search-history" }, + { id: "urlbar-result-action-search-bookmarks" }, + ]); + Assert.equal( + result.displayed.action, + actionHistory, + "Check the heuristic action" + ); + Assert.equal( + result.image, + "chrome://browser/skin/history.svg", + "Check the heuristic icon" + ); + + info("Move to an engine one-off and check heuristic action"); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + Assert.ok( + oneOffButtons.selectedButton.engine, + "A one-off search button should be selected" + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok( + BrowserTestUtils.isVisible(result.element.action), + "The heuristic action should be visible" + ); + Assert.ok( + result.displayed.action.includes(oneOffButtons.selectedButton.engine.name), + "Check the heuristic action" + ); + Assert.equal( + result.image, + oneOffButtons.selectedButton.engine.getIconURL(), + "Check the heuristic icon" + ); + + info("Move again to a local one-off, deselect and reselect the heuristic"); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + oneOffButtons.selectedButton.source, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + "A local one-off button should be selected" + ); + Assert.equal( + result.displayed.action, + actionBookmarks, + "Check the heuristic action" + ); + Assert.equal( + result.image, + "chrome://browser/skin/bookmark.svg", + "Check the heuristic icon" + ); + + info( + "Select the next result, then reselect the heuristic, check it's a search with the default engine" + ); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "the heuristic result should be selected" + ); + EventUtils.synthesizeKey("KEY_ArrowDown", {}); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "the heuristic result should not be selected" + ); + EventUtils.synthesizeKey("KEY_ArrowUp", {}); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "the heuristic result should be selected" + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH); + Assert.equal(result.searchParams.engine, engine.name); + Assert.ok( + result.displayed.action.includes(engine.name), + "Check the heuristic action" + ); + Assert.equal( + result.image, + "chrome://global/skin/icons/search-glass.svg", + "Check the heuristic icon" + ); +}); + +add_task(async function localOneOff_withVisit() { + info("Type a url, select a local one-off, check heuristic action"); + await PlacesUtils.history.clear(); + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits("https://mozilla.org/"); + await PlacesTestUtils.addVisits("https://other.mozilla.org/"); + } + const searchString = "mozilla.org"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + }); + Assert.ok(UrlbarTestUtils.getResultCount(window) > 1, "Sanity check results"); + let oneOffButtons = UrlbarTestUtils.getOneOffSearchButtons(window); + + let [actionHistory, actionTabs, actionBookmarks] = + await document.l10n.formatValues([ + { id: "urlbar-result-action-search-history" }, + { id: "urlbar-result-action-search-tabs" }, + { id: "urlbar-result-action-search-bookmarks" }, + ]); + + info("Alt UP to select the history one-off."); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "the heuristic result should be selected" + ); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + oneOffButtons.selectedButton.source, + UrlbarUtils.RESULT_SOURCE.HISTORY, + "The history one-off button should be selected" + ); + Assert.ok( + BrowserTestUtils.isVisible(result.element.action), + "The heuristic action should be visible" + ); + Assert.equal( + result.displayed.action, + actionHistory, + "Check the heuristic action" + ); + Assert.equal( + result.image, + "chrome://browser/skin/history.svg", + "Check the heuristic icon" + ); + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + Assert.equal( + row.querySelector(".urlbarView-title").textContent, + searchString, + "Check that the result title has been replaced with the search string." + ); + + info("Alt UP to select the tabs one-off."); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + oneOffButtons.selectedButton.source, + UrlbarUtils.RESULT_SOURCE.TABS, + "The tabs one-off button should be selected" + ); + Assert.ok( + BrowserTestUtils.isVisible(result.element.action), + "The heuristic action should be visible" + ); + Assert.equal( + result.displayed.action, + actionTabs, + "Check the heuristic action" + ); + Assert.equal( + result.image, + "chrome://browser/skin/tab.svg", + "Check the heuristic icon" + ); + + info("Alt UP to select the bookmarks one-off."); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + oneOffButtons.selectedButton.source, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + "The bookmarks one-off button should be selected" + ); + Assert.ok( + BrowserTestUtils.isVisible(result.element.action), + "The heuristic action should be visible" + ); + Assert.equal( + result.displayed.action, + actionBookmarks, + "Check the heuristic action" + ); + Assert.equal( + result.image, + "chrome://browser/skin/bookmark.svg", + "Check the heuristic icon" + ); + + info( + "Select the next result, then reselect the heuristic, check it's a visit" + ); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "the heuristic result should be selected" + ); + EventUtils.synthesizeKey("KEY_ArrowDown", {}); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "the heuristic result should not be selected" + ); + EventUtils.synthesizeKey("KEY_ArrowUp", {}); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "the heuristic result should be selected" + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + oneOffButtons.selectedButton, + null, + "No one-off button should be selected" + ); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL); + Assert.equal( + result.displayed.url, + result.result.payload.displayUrl, + "Check the heuristic action" + ); + Assert.notEqual( + result.image, + "chrome://browser/skin/history.svg", + "Check the heuristic icon" + ); + row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + Assert.equal( + row.querySelector(".urlbarView-title").textContent, + result.result.payload.title || `https://${searchString}`, + "Check that the result title has been restored to the fixed-up URI." + ); + + await PlacesUtils.history.clear(); +}); + +add_task(async function localOneOff_suggestion() { + info("Type some text, select the first suggestion, then a local one-off"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "query", + }); + let count = UrlbarTestUtils.getResultCount(window); + Assert.ok(count > 1, "Sanity check results"); + let result = null; + let suggestionIndex = -1; + for (let i = 1; i < count; ++i) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + let index = await UrlbarTestUtils.getSelectedRowIndex(window); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + if ( + result.type == UrlbarUtils.RESULT_TYPE.SEARCH && + result.searchParams.suggestion + ) { + suggestionIndex = i; + break; + } + } + Assert.ok( + result.searchParams.suggestion, + "Should have selected a search suggestion" + ); + Assert.ok( + result.displayed.action.includes(result.searchParams.engine), + "Check the search suggestion action" + ); + + info("Alt UP to select the last local one-off."); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + Assert.equal( + await UrlbarTestUtils.getSelectedRowIndex(window), + suggestionIndex, + "the suggestion should still be selected" + ); + + let [actionHistory] = await document.l10n.formatValues([ + { id: "urlbar-result-action-search-history" }, + ]); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, suggestionIndex); + Assert.equal( + result.displayed.action, + actionHistory, + "Check the search suggestion action changed to local one-off" + ); + // Like in the normal engine one-offs case, we don't replace the favicon. + Assert.equal( + result.image, + "chrome://global/skin/icons/search-glass.svg", + "Check the suggestion icon" + ); + + info("DOWN to select the next suggestion"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + suggestionIndex + 1 + ); + Assert.ok( + result.searchParams.suggestion, + "Should have selected a search suggestion" + ); + Assert.ok( + result.displayed.action.includes(result.searchParams.engine), + "Check the search suggestion action" + ); + Assert.equal( + result.image, + "chrome://global/skin/icons/search-glass.svg", + "Check the suggestion icon" + ); + + info("UP back to the previous suggestion"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, suggestionIndex); + Assert.ok( + result.displayed.action.includes(result.searchParams.engine), + "Check the search suggestion action" + ); + Assert.equal( + result.image, + "chrome://global/skin/icons/search-glass.svg", + "Check the suggestion icon" + ); +}); + +add_task(async function localOneOff_shortcut() { + info("Select a search shortcut, then a local one-off"); + + await PlacesUtils.history.clear(); + // Enough vists to get this site into Top Sites. + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits("http://example.com/"); + } + + await updateTopSites( + sites => sites && sites[0] && sites[0].searchTopSite, + true + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + let count = UrlbarTestUtils.getResultCount(window); + Assert.ok(count > 1, "Sanity check results"); + let result = null; + let shortcutIndex = -1; + for (let i = 0; i < count; ++i) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + let index = await UrlbarTestUtils.getSelectedRowIndex(window); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + if ( + result.type == UrlbarUtils.RESULT_TYPE.SEARCH && + result.searchParams.keyword + ) { + shortcutIndex = i; + break; + } + } + Assert.ok(result.searchParams.keyword, "Should have selected a shortcut"); + let shortcutEngine = result.searchParams.engine; + + info("Alt UP to select the last local one-off."); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + Assert.equal( + await UrlbarTestUtils.getSelectedRowIndex(window), + shortcutIndex, + "the shortcut should still be selected" + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, shortcutIndex); + Assert.equal( + result.displayed.action, + "", + "Check the shortcut action is empty" + ); + Assert.equal( + result.searchParams.engine, + shortcutEngine, + "Check the shortcut engine" + ); + Assert.ok( + result.displayed.title.includes(shortcutEngine), + "Check the shortcut title" + ); + Assert.notEqual( + result.image, + "chrome://global/skin/icons/search-glass.svg", + "Check the icon was not replaced" + ); + + await UrlbarTestUtils.exitSearchMode(window); + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_newWindow.js b/browser/components/urlbar/tests/browser/browser_searchMode_newWindow.js new file mode 100644 index 0000000000..e5a3eb848a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_newWindow.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests immediately entering search mode in a new window and then exiting it. +// No errors should be thrown and search mode should be exited successfully. + +"use strict"; + +add_task(async function escape() { + await doTest(win => + EventUtils.synthesizeKey("KEY_Escape", { repeat: 2 }, win) + ); +}); + +add_task(async function backspace() { + await doTest(win => EventUtils.synthesizeKey("KEY_Backspace", {}, win)); +}); + +async function doTest(exitSearchMode) { + let win = await BrowserTestUtils.openNewBrowserWindow(); + + // Press accel+K to enter search mode. + await UrlbarTestUtils.promisePopupOpen(win, () => + EventUtils.synthesizeKey("k", { accelKey: true }, win) + ); + await UrlbarTestUtils.assertSearchMode(win, { + engineName: Services.search.defaultEngine.name, + isGeneralPurposeEngine: true, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + isPreview: false, + entry: "shortcut", + }); + + // Exit search mode. + await exitSearchMode(win); + await UrlbarTestUtils.assertSearchMode(win, null); + + await UrlbarTestUtils.promisePopupClose(win); + await BrowserTestUtils.closeWindow(win); +} diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_no_results.js b/browser/components/urlbar/tests/browser/browser_searchMode_no_results.js new file mode 100644 index 0000000000..9ecc5573fc --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_no_results.js @@ -0,0 +1,290 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests entering search mode and there are no results in the view. + */ + +"use strict"; + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +add_setup(async function () { + // In order to open the view without any results, we need to be in search mode + // with an empty search string so that no heuristic result is shown, and the + // empty search must yield zero additional results. We'll enter search mode + // using the bookmarks one-off, and first we'll delete all bookmarks so that + // there are no results. + await PlacesUtils.bookmarks.eraseEverything(); + + // Also clear history so that using the alias of our test engine doesn't + // inadvertently return any history results due to bug 1658646. + await PlacesUtils.history.clear(); + + // Add a top site so we're guaranteed the view has at least one result to + // show initially with an empty search. Otherwise the view won't even open. + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.newtabpage.activity-stream.default.sites", + "http://example.com/", + ], + ], + }); + await updateTopSites(sites => sites.length); +}); + +// Basic test for entering search mode with no results. +add_task(async function basic() { + await withNewWindow(async win => { + // Do an empty search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "", + }); + + // Initially there should be at least the top site we added above. + Assert.greater( + UrlbarTestUtils.getResultCount(win), + 0, + "Top sites should be present initially" + ); + Assert.ok( + !win.gURLBar.panel.hasAttribute("noresults"), + "Panel has results, therefore should not have noresults attribute" + ); + + // Enter search mode by clicking the bookmarks one-off. + await UrlbarTestUtils.enterSearchMode(win, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(win), + 0, + "Zero results since no bookmarks exist" + ); + Assert.equal( + win.gURLBar.panel.getAttribute("noresults"), + "true", + "Panel has no results, therefore should have noresults attribute" + ); + + // Exit search mode by backspacing. The top sites should be shown again. + await UrlbarTestUtils.exitSearchMode(win, { backspace: true }); + Assert.greater( + UrlbarTestUtils.getResultCount(win), + 0, + "Top sites should be present again" + ); + Assert.ok( + !win.gURLBar.panel.hasAttribute("noresults"), + "noresults attribute should be absent again" + ); + + await UrlbarTestUtils.promisePopupClose(win); + }); +}); + +// When the urlbar is in search mode, has no results, and is not focused, +// focusing it should auto-open the view. +add_task(async function autoOpen() { + await withNewWindow(async win => { + // Do an empty search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "", + }); + + // Initially there should be at least the top site we added above. + Assert.greater( + UrlbarTestUtils.getResultCount(win), + 0, + "Top sites should be present initially" + ); + Assert.ok( + !win.gURLBar.panel.hasAttribute("noresults"), + "Panel has results, therefore should not have noresults attribute" + ); + + // Enter search mode by clicking the bookmarks one-off. + await UrlbarTestUtils.enterSearchMode(win, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + Assert.equal( + UrlbarTestUtils.getResultCount(win), + 0, + "Zero results since no bookmarks exist" + ); + Assert.equal( + win.gURLBar.panel.getAttribute("noresults"), + "true", + "Panel has no results, therefore should have noresults attribute" + ); + + // Blur the urlbar. + win.gURLBar.blur(); + await UrlbarTestUtils.assertSearchMode(win, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + + // Click the urlbar. + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win); + }); + Assert.equal( + UrlbarTestUtils.getResultCount(win), + 0, + "Still zero results since no bookmarks exist" + ); + Assert.equal( + win.gURLBar.panel.getAttribute("noresults"), + "true", + "Panel still has no results, therefore should have noresults attribute" + ); + + // Exit search mode by backspacing. The top sites should be shown again. + await UrlbarTestUtils.exitSearchMode(win, { backspace: true }); + Assert.greater( + UrlbarTestUtils.getResultCount(win), + 0, + "Top sites should be present again" + ); + Assert.ok( + !win.gURLBar.panel.hasAttribute("noresults"), + "noresults attribute should be absent again" + ); + + await UrlbarTestUtils.promisePopupClose(win); + }); +}); + +// When the urlbar is in search mode, the user backspaces over the final char +// (but remains in search mode), and there are no results, the view should +// remain open. +add_task(async function backspaceRemainOpen() { + await withNewWindow(async win => { + // Do a one-char search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "x", + }); + Assert.greater( + UrlbarTestUtils.getResultCount(win), + 0, + "At least the heuristic result should be shown" + ); + Assert.ok( + !win.gURLBar.panel.hasAttribute("noresults"), + "Panel has results, therefore should not have noresults attribute" + ); + + // Enter search mode by clicking the bookmarks one-off. + await UrlbarTestUtils.enterSearchMode(win, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + // The heursitic should not be shown since we don't show it in local search + // modes. + Assert.equal( + UrlbarTestUtils.getResultCount(win), + 0, + "No results should be present" + ); + Assert.ok( + win.gURLBar.panel.hasAttribute("noresults"), + "Panel has no results, therefore should have noresults attribute" + ); + + // Backspace. The search string will now be empty. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("KEY_Backspace", {}, win); + await searchPromise; + Assert.ok(UrlbarTestUtils.isPopupOpen(win), "View remains open"); + await UrlbarTestUtils.assertSearchMode(win, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(win), + 0, + "Zero results since no bookmarks exist" + ); + Assert.equal( + win.gURLBar.panel.getAttribute("noresults"), + "true", + "Panel has no results, therefore should have noresults attribute" + ); + + // Exit search mode by backspacing. The top sites should be shown. + await UrlbarTestUtils.exitSearchMode(win, { backspace: true }); + Assert.greater( + UrlbarTestUtils.getResultCount(win), + 0, + "Top sites should be present again" + ); + Assert.ok( + !win.gURLBar.panel.hasAttribute("noresults"), + "noresults attribute should be absent again" + ); + + await UrlbarTestUtils.promisePopupClose(win); + }); +}); + +// Types a search alias and then a space to enter search mode, with no results. +// The one-offs should be shown. +add_task(async function spaceToEnterSearchMode() { + let engine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + }); + engine.alias = "@test"; + + await withNewWindow(async win => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: engine.alias, + }); + + // We need to wait for two searches: The first enters search mode, the + // second does the search in search mode. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey(" ", {}, win); + await searchPromise; + + Assert.equal(UrlbarTestUtils.getResultCount(win), 0, "Zero results"); + Assert.equal( + win.gURLBar.panel.getAttribute("noresults"), + "true", + "Panel has no results, therefore should have noresults attribute" + ); + await UrlbarTestUtils.assertSearchMode(win, { + engineName: engine.name, + entry: "typed", + }); + this.Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + true, + "One-offs are visible" + ); + + await UrlbarTestUtils.exitSearchMode(win, { backspace: true }); + await UrlbarTestUtils.promisePopupClose(win); + }); +}); + +/** + * Opens a new window, waits for it to load, calls a callback, and closes the + * window. We use a new window in each task so that the view starts with a + * blank slate each time. + * + * @param {Function} callback + * Will be called as: callback(newWindow) + */ +async function withNewWindow(callback) { + // Start in a new window so we have a blank slate. + let win = await BrowserTestUtils.openNewBrowserWindow(); + await callback(win); + await BrowserTestUtils.closeWindow(win); +} diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_oneOffButton.js b/browser/components/urlbar/tests/browser/browser_searchMode_oneOffButton.js new file mode 100644 index 0000000000..1ba0d3283b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_oneOffButton.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests one-off search button behavior with search mode. + */ + +const TEST_ENGINE_NAME = "test engine"; + +add_setup(async function () { + await SearchTestUtils.installSearchExtension({ + name: TEST_ENGINE_NAME, + keyword: "@test", + }); +}); + +add_task(async function test() { + info("Test no one-off buttons are selected when entering search mode"); + + info("Open the result popup"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + info("Select one of one-off button"); + const oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + ok(oneOffs.selectedButton, "There is a selected one-off button"); + + info("Enter search mode"); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: TEST_ENGINE_NAME, + }); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "oneoff", + }); + ok(!oneOffs.selectedButton, "There is no selected one-off button"); +}); + +add_task(async function () { + info( + "Test the status of the selected one-off button when exiting search mode with backspace" + ); + + info("Open the result popup"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + info("Select one of one-off button"); + const oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + ok(oneOffs.selectedButton, "There is a selected one-off button"); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs.selectedButton.engine.name, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + entry: "oneoff", + isPreview: true, + }); + + info("Exit from search mode"); + await UrlbarTestUtils.exitSearchMode(window); + ok(!oneOffs.selectedButton, "There is no any selected one-off button"); +}); + +add_task(async function () { + info( + "Test the status of the selected one-off button when exiting search mode with clicking close button" + ); + + info("Open the result popup"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + info("Select one of one-off button"); + const oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + ok(oneOffs.selectedButton, "There is a selected one-off button"); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs.selectedButton.engine.name, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + entry: "oneoff", + isPreview: true, + }); + + info("Exit from search mode"); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + ok(!oneOffs.selectedButton, "There is no any selected one-off button"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js b/browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js new file mode 100644 index 0000000000..ac45b3e5c7 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that search mode is exited after picking a result. + */ + +"use strict"; + +const BOOKMARK_URL = "http://www.example.com/browser_searchMode_pickResult.js"; + +add_setup(async function () { + // Add a bookmark so we can enter bookmarks search mode and pick it. + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: BOOKMARK_URL, + }); + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +// Opens a new tab, enters search mode, does a search for our test bookmark, and +// picks it. Uses a variety of initial URLs and search strings in order to hit +// different branches in setURI. Search mode should be exited in all cases. +add_task(async function pickResult() { + for (let test of [ + // initialURL, searchString + ["about:blank", BOOKMARK_URL], + ["about:blank", new URL(BOOKMARK_URL).origin], + ["about:blank", new URL(BOOKMARK_URL).pathname], + [BOOKMARK_URL, BOOKMARK_URL], + [BOOKMARK_URL, new URL(BOOKMARK_URL).origin], + [BOOKMARK_URL, new URL(BOOKMARK_URL).pathname], + ]) { + await doPickResultTest(...test); + } +}); + +async function doPickResultTest(initialURL, searchString) { + info( + "doPickResultTest with args: " + + JSON.stringify({ + initialURL, + searchString, + }) + ); + + await BrowserTestUtils.withNewTab(initialURL, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + // Arrow down to the bookmark result. + let firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + if (!firstResult.heuristic) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + let foundResult = false; + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (result.url == BOOKMARK_URL) { + foundResult = true; + break; + } + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + Assert.ok(foundResult, "The bookmark result should have been found"); + + // Press enter. + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + Assert.equal( + gBrowser.currentURI.spec, + BOOKMARK_URL, + "Should have loaded the bookmarked URL" + ); + + await UrlbarTestUtils.assertSearchMode(window, null); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_preview.js b/browser/components/urlbar/tests/browser/browser_searchMode_preview.js new file mode 100644 index 0000000000..19df744663 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_preview.js @@ -0,0 +1,489 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests search mode preview. + */ + +"use strict"; + +const TEST_ENGINE_NAME = "Test"; + +add_setup(async function () { + await SearchTestUtils.installSearchExtension({ + name: TEST_ENGINE_NAME, + keyword: "@test", + }); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +/** + * @param {Node} button + * A one-off button. + * @param {boolean} [isPreview] + * Whether the expected search mode should be a preview. Defaults to true. + * @returns {object} + * The search mode object expected when that one-off is selected. + */ +function getExpectedSearchMode(button, isPreview = true) { + let expectedSearchMode = { + entry: "oneoff", + isPreview, + }; + if (button.engine) { + expectedSearchMode.engineName = button.engine.name; + let engine = Services.search.getEngineByName(button.engine.name); + if (engine.isGeneralPurposeEngine) { + expectedSearchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH; + } + } else { + expectedSearchMode.source = button.source; + } + + return expectedSearchMode; +} + +// Tests that cycling through token alias engines enters search mode preview. +add_task(async function tokenAlias() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + + let result; + while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + let index = UrlbarTestUtils.getSelectedRowIndex(window); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + let expectedSearchMode = { + engineName: result.searchParams.engine, + isPreview: true, + entry: "keywordoffer", + }; + let engine = Services.search.getEngineByName(result.searchParams.engine); + if (engine.isGeneralPurposeEngine) { + expectedSearchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH; + } + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + } + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + // Test that we are in confirmed search mode. + await UrlbarTestUtils.assertSearchMode(window, { + engineName: result.searchParams.engine, + entry: "keywordoffer", + }); + await UrlbarTestUtils.exitSearchMode(window); +}); + +// Tests that starting to type a query exits search mode preview in favour of +// full search mode. +add_task(async function startTyping() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + isPreview: true, + entry: "keywordoffer", + }); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("M"); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "keywordoffer", + }); + await UrlbarTestUtils.exitSearchMode(window); +}); + +// Tests that highlighting a search shortcut Top Site enters search mode +// preview. +add_task(async function topSites() { + // Enable search shortcut Top Sites. + await PlacesUtils.history.clear(); + await updateTopSites( + sites => sites && sites[0] && sites[0].searchTopSite, + true + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + + // We previously verified that the first Top Site is a search shortcut. + EventUtils.synthesizeKey("KEY_ArrowDown"); + let searchTopSite = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: searchTopSite.searchParams.engine, + isPreview: true, + entry: "topsites_urlbar", + }); + await UrlbarTestUtils.exitSearchMode(window); +}); + +// Tests that search mode preview is exited when the view is closed. +add_task(async function closeView() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + + while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + isPreview: true, + entry: "keywordoffer", + }); + + // We should close search mode when closing the view. + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + await UrlbarTestUtils.assertSearchMode(window, null); + + // Check search mode isn't re-entered when re-opening the view. + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.assertSearchMode(window, null); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Tests that search more preview is exited when the user switches tabs. +add_task(async function tabSwitch() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + + while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + isPreview: true, + entry: "keywordoffer", + }); + + // Open a new tab then switch back to the original tab. + let tab1 = gBrowser.selectedTab; + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await BrowserTestUtils.switchTab(gBrowser, tab1); + + await UrlbarTestUtils.assertSearchMode(window, null); + BrowserTestUtils.removeTab(tab2); +}); + +// Tests that search mode is previewed when the user down arrows through the +// one-offs. +add_task(async function oneOff_downArrow() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + let resultCount = UrlbarTestUtils.getResultCount(window); + + // Key down through all results. + for (let i = 0; i < resultCount; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + + // Key down again. The first one-off should be selected. + EventUtils.synthesizeKey("KEY_ArrowDown"); + + // Check for the one-off's search mode previews. + while (oneOffs.selectedButton != oneOffs.settingsButton) { + await UrlbarTestUtils.assertSearchMode( + window, + getExpectedSearchMode(oneOffs.selectedButton) + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + + // Check that selecting the search settings button leaves search mode preview. + Assert.equal( + oneOffs.selectedButton, + oneOffs.settingsButton, + "The settings button is selected." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + + // Closing the view should also exit search mode preview. + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.assertSearchMode(window, null); +}); + +// Tests that search mode is previewed when the user Alt+down arrows through the +// one-offs. This subtest also highlights a keywordoffer result (the first Top +// Site) before Alt+Arrowing to the one-offs. This checks that the search mode +// previews from keywordoffer results are overwritten by selected one-offs. +add_task(async function oneOff_alt_downArrow() { + // Add some visits to a URL so we have multiple Top Sites. + await PlacesUtils.history.clear(); + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits("https://example.com/"); + } + await updateTopSites( + sites => + sites && + sites[0]?.searchTopSite && + sites[1]?.url == "https://example.com/", + true + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + + // Key down to the first result and check that it enters search mode preview. + EventUtils.synthesizeKey("KEY_ArrowDown"); + let searchTopSite = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: searchTopSite.searchParams.engine, + isPreview: true, + entry: "topsites_urlbar", + }); + + // Alt+down. The first one-off should be selected. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + // Check for the one-offs' search mode previews. + while (oneOffs.selectedButton) { + await UrlbarTestUtils.assertSearchMode( + window, + getExpectedSearchMode(oneOffs.selectedButton) + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + } + + // Now key down without a modifier. We should move to the second result and + // have no search mode preview. + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "The second result is selected." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + + // Arrow back up to the keywordoffer result and check for search mode preview. + EventUtils.synthesizeKey("KEY_ArrowUp"); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: searchTopSite.searchParams.engine, + isPreview: true, + entry: "topsites_urlbar", + }); + + await PlacesUtils.history.clear(); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.assertSearchMode(window, null); +}); + +// Tests that search mode is previewed when the user is in full search mode +// and down arrows through the one-offs. +add_task(async function fullSearchMode_oneOff_downArrow() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + let oneOffButtons = oneOffs.getSelectableButtons(true); + + await UrlbarTestUtils.enterSearchMode(window); + let expectedSearchMode = getExpectedSearchMode(oneOffButtons[0], false); + // Sanity check: we are in the correct search mode. + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + + // Key down through all results. + let resultCount = UrlbarTestUtils.getResultCount(window); + for (let i = 0; i < resultCount; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + // If the result is a shortcut, it will enter preview mode. + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + await UrlbarTestUtils.assertSearchMode( + window, + Object.assign(expectedSearchMode, { + isPreview: !!result.searchParams.keyword, + }) + ); + } + + // Key down again. The first one-off should be selected. + EventUtils.synthesizeKey("KEY_ArrowDown"); + // Check that we show the correct preview as we cycle through the one-offs. + while (oneOffs.selectedButton != oneOffs.settingsButton) { + await UrlbarTestUtils.assertSearchMode( + window, + getExpectedSearchMode(oneOffs.selectedButton, true) + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + + // We should still be in the same search mode after cycling through all the + // one-offs. + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Tests that search mode is previewed when the user is in full search mode +// and alt+down arrows through the one-offs. This subtest also checks that +// exiting full search mode while alt+arrowing through the one-offs enters +// search mode preview for subsequent one-offs. +add_task(async function fullSearchMode_oneOff_alt_downArrow() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + let oneOffButtons = oneOffs.getSelectableButtons(true); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + + await UrlbarTestUtils.enterSearchMode(window); + let expectedSearchMode = getExpectedSearchMode(oneOffButtons[0], false); + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + + // Key down to the first result. + EventUtils.synthesizeKey("KEY_ArrowDown"); + + // Alt+down. The first one-off should be selected. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + // Cycle through the first half of the one-offs and verify that search mode + // preview is entered. + Assert.greater( + oneOffButtons.length, + 1, + "Sanity check: We should have at least two one-offs." + ); + for (let i = 1; i < oneOffButtons.length / 2; i++) { + await UrlbarTestUtils.assertSearchMode( + window, + getExpectedSearchMode(oneOffs.selectedButton, true) + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + } + // Now click out of search mode. + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + // Now check for the remaining one-offs' search mode previews. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + while (oneOffs.selectedButton) { + await UrlbarTestUtils.assertSearchMode( + window, + getExpectedSearchMode(oneOffs.selectedButton, true) + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + } + + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.assertSearchMode(window, null); +}); + +// Tests that the original search mode is preserved when going through some +// one-off buttons and then back down in the results list. +add_task(async function fullSearchMode_oneOff_restore_on_down() { + info("Add a few visits to top sites"); + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits([ + "https://1.example.com/", + "https://2.example.com/", + "https://3.example.com/", + ]); + } + await updateTopSites(sites => sites?.length > 2, false); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + let oneOffButtons = oneOffs.getSelectableButtons(true); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }); + let expectedSearchMode = getExpectedSearchMode( + oneOffButtons.find(b => b.source == UrlbarUtils.RESULT_SOURCE.HISTORY), + false + ); + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + info("Down to the first result"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + info("Alt+down to the first one-off."); + Assert.greater( + oneOffButtons.length, + 1, + "Sanity check: We should have at least two one-offs." + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + await UrlbarTestUtils.assertSearchMode( + window, + getExpectedSearchMode(oneOffs.selectedButton, true) + ); + info("Go again down through the list of results"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + + // Now do a similar test without initial search mode. + info("Exit search mode."); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + info("Down to the first result"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.assertSearchMode(window, null); + info("select a one-off to start preview"); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + await UrlbarTestUtils.assertSearchMode( + window, + getExpectedSearchMode(oneOffs.selectedButton, true) + ); + info("Go again through the list of results"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.assertSearchMode(window, null); + + await UrlbarTestUtils.promisePopupClose(window); + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_sessionStore.js b/browser/components/urlbar/tests/browser/browser_searchMode_sessionStore.js new file mode 100644 index 0000000000..ef3fabe636 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_sessionStore.js @@ -0,0 +1,332 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests search mode and session store. Also tests that search mode is + * duplicated when duplicating tabs, since tab duplication is handled by session + * store. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs", +}); + +// This test takes a long time on the OS X 10.14 machines, so request a longer +// timeout. See bug 1671045. This may also fix a different failure on Linux in +// bug 1671087, but it's not clear. Regardless, a longer timeout won't hurt. +requestLongerTimeout(5); + +const SEARCH_STRING = "test browser_sessionStore.js"; +const URL = "http://example.com/"; + +// A URL in gInitialPages. We test this separately since SessionStore sometimes +// takes different paths for these URLs. +const INITIAL_URL = "about:newtab"; + +// The following tasks make sure non-null search mode is restored. + +add_task(async function initialPageOnRestore() { + await doTest({ + urls: [INITIAL_URL], + searchModeTabIndex: 0, + exitSearchMode: false, + switchTabsAfterEnteringSearchMode: false, + }); +}); + +add_task(async function switchToInitialPage() { + await doTest({ + urls: [URL, INITIAL_URL], + searchModeTabIndex: 1, + exitSearchMode: false, + switchTabsAfterEnteringSearchMode: true, + }); +}); + +add_task(async function nonInitialPageOnRestore() { + await doTest({ + urls: [URL], + searchModeTabIndex: 0, + exitSearchMode: false, + switchTabsAfterEnteringSearchMode: false, + }); +}); + +add_task(async function switchToNonInitialPage() { + await doTest({ + urls: [INITIAL_URL, URL], + searchModeTabIndex: 1, + exitSearchMode: false, + switchTabsAfterEnteringSearchMode: true, + }); +}); + +// The following tasks enter and then exit search mode to make sure that no +// search mode is restored. + +add_task(async function initialPageOnRestore_exit() { + await doTest({ + urls: [INITIAL_URL], + searchModeTabIndex: 0, + exitSearchMode: true, + switchTabsAfterEnteringSearchMode: false, + }); +}); + +add_task(async function switchToInitialPage_exit() { + await doTest({ + urls: [URL, INITIAL_URL], + searchModeTabIndex: 1, + exitSearchMode: true, + switchTabsAfterEnteringSearchMode: true, + }); +}); + +add_task(async function nonInitialPageOnRestore_exit() { + await doTest({ + urls: [URL], + searchModeTabIndex: 0, + exitSearchMode: true, + switchTabsAfterEnteringSearchMode: false, + }); +}); + +add_task(async function switchToNonInitialPage_exit() { + await doTest({ + urls: [INITIAL_URL, URL], + searchModeTabIndex: 1, + exitSearchMode: true, + switchTabsAfterEnteringSearchMode: true, + }); +}); + +/** + * The main test function. Opens some URLs in a new window, enters search mode + * in one of the tabs, closes the window, restores it, and makes sure that + * search mode is restored properly. + * + * @param {object} options + * Options object + * @param {Array} options.urls + * Array of string URLs to open. + * @param {number} options.searchModeTabIndex + * The index of the tab in which to enter search mode. + * @param {boolean} options.exitSearchMode + * If true, search mode will be immediately exited after entering it. Use + * this to make sure search mode is *not* restored after it's exited. + * @param {boolean} options.switchTabsAfterEnteringSearchMode + * If true, we'll switch to a tab other than the one that search mode was + * entered in before closing the window. `urls` should contain more than one + * URL in this case. + */ +async function doTest({ + urls, + searchModeTabIndex, + exitSearchMode, + switchTabsAfterEnteringSearchMode, +}) { + let searchModeURL = urls[searchModeTabIndex]; + let otherTabIndex = (searchModeTabIndex + 1) % urls.length; + let otherURL = urls[otherTabIndex]; + + await withNewWindow(urls, async win => { + if (win.gBrowser.selectedTab != win.gBrowser.tabs[searchModeTabIndex]) { + await BrowserTestUtils.switchTab( + win.gBrowser, + win.gBrowser.tabs[searchModeTabIndex] + ); + } + + Assert.equal( + win.gBrowser.currentURI.spec, + searchModeURL, + `Sanity check: Tab at index ${searchModeTabIndex} is correct` + ); + Assert.equal( + searchModeURL == INITIAL_URL, + win.gInitialPages.includes(win.gBrowser.currentURI.spec), + `Sanity check: ${searchModeURL} is or is not in gInitialPages as expected` + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: SEARCH_STRING, + fireInputEvent: true, + }); + await UrlbarTestUtils.enterSearchMode(win, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + if (exitSearchMode) { + await UrlbarTestUtils.exitSearchMode(win); + } + + // Make sure session store is updated. + await TabStateFlusher.flush(win.gBrowser.selectedBrowser); + + if (switchTabsAfterEnteringSearchMode) { + await BrowserTestUtils.switchTab( + win.gBrowser, + win.gBrowser.tabs[otherTabIndex] + ); + } + }); + + let restoredURL = switchTabsAfterEnteringSearchMode + ? otherURL + : searchModeURL; + + let win = await restoreWindow(restoredURL); + + Assert.equal( + win.gBrowser.currentURI.spec, + restoredURL, + "Sanity check: Initially selected tab in restored window is correct" + ); + + if (switchTabsAfterEnteringSearchMode) { + // Switch back to the tab with search mode. + await BrowserTestUtils.switchTab( + win.gBrowser, + win.gBrowser.tabs[searchModeTabIndex] + ); + } + + if (exitSearchMode) { + // If we exited search mode, it should be null. + await new Promise(r => win.setTimeout(r, 500)); + await UrlbarTestUtils.assertSearchMode(win, null); + } else { + // If we didn't exit search mode, it should be restored. + await TestUtils.waitForCondition( + () => win.gURLBar.searchMode, + "Waiting for search mode to be restored" + ); + await UrlbarTestUtils.assertSearchMode(win, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal( + win.gURLBar.value, + SEARCH_STRING, + "Search string should be restored" + ); + } + + await BrowserTestUtils.closeWindow(win); +} + +async function openTabMenuFor(tab) { + let tabMenu = tab.ownerDocument.getElementById("tabContextMenu"); + + let tabMenuShown = BrowserTestUtils.waitForEvent(tabMenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter( + tab, + { type: "contextmenu" }, + tab.ownerGlobal + ); + await tabMenuShown; + + return tabMenu; +} + +// Tests that search mode is duplicated when duplicating tabs. Note that tab +// duplication is handled by session store. +add_task(async function duplicateTabs() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.net/" + ); + gBrowser.selectedTab = tab; + // Enter search mode with a search string in the current tab. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: SEARCH_STRING, + fireInputEvent: true, + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + // Now duplicate the current tab using the context menu item. + const menu = await openTabMenuFor(gBrowser.selectedTab); + let tabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + gBrowser.currentURI.spec + ); + menu.activateItem(document.getElementById("context_duplicateTab")); + let newTab = await tabPromise; + Assert.equal( + gBrowser.selectedTab, + newTab, + "Sanity check: The duplicated tab is now the selected tab" + ); + + // Wait for search mode, then check it and the input value. + await TestUtils.waitForCondition( + () => gURLBar.searchMode, + "Waiting for search mode to be duplicated/restored" + ); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal( + gURLBar.value, + SEARCH_STRING, + "Search string should be duplicated/restored" + ); + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(newTab); + gURLBar.handleRevert(); +}); + +/** + * Opens a new browser window with the given URLs, calls a callback, and then + * closes the window. + * + * @param {Array} urls + * Array of string URLs to open. + * @param {Function} callback + * The callback. + */ +async function withNewWindow(urls, callback) { + let win = await BrowserTestUtils.openNewBrowserWindow(); + for (let url of urls) { + await BrowserTestUtils.openNewForegroundTab({ + url, + gBrowser: win.gBrowser, + waitForLoad: url != "about:newtab", + }); + if (url == "about:newtab") { + await TestUtils.waitForCondition( + () => win.gBrowser.currentURI.spec == "about:newtab", + "Waiting for about:newtab" + ); + } + } + BrowserTestUtils.removeTab(win.gBrowser.tabs[0]); + await callback(win); + await BrowserTestUtils.closeWindow(win); +} + +/** + * Uses SessionStore to reopen the last closed window. + * + * @param {string} expectedRestoredURL + * The URL you expect will be restored in the selected browser. + */ +async function restoreWindow(expectedRestoredURL) { + let winPromise = BrowserTestUtils.waitForNewWindow(); + let win = SessionStore.undoCloseWindow(0); + await winPromise; + await TestUtils.waitForCondition( + () => win.gBrowser.currentURI.spec == expectedRestoredURL, + "Waiting for restored selected browser to have expected URI" + ); + return win; +} diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_setURI.js b/browser/components/urlbar/tests/browser/browser_searchMode_setURI.js new file mode 100644 index 0000000000..46f0a84256 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_setURI.js @@ -0,0 +1,119 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that search mode remains active or is exited when setURI is called, + * depending on the situation. + */ + +"use strict"; + +// Opens a new tab, does a search, enters search mode, and then manually calls +// setURI. Uses a variety of initial URLs, search strings, and setURI arguments +// in order to hit different branches in setURI. Search mode should remain +// active or be exited as appropriate. +add_task(async function setURI() { + for (let test of [ + // initialURL, searchString, url, expectSearchMode + + ["about:blank", "", null, true], + ["about:blank", "", "about:blank", true], + ["about:blank", "", "http://www.example.com/", true], + + ["about:blank", "about:blank", null, false], + ["about:blank", "about:blank", "about:blank", false], + ["about:blank", "about:blank", "http://www.example.com/", false], + + ["about:blank", "http://www.example.com/", null, true], + ["about:blank", "http://www.example.com/", "about:blank", true], + ["about:blank", "http://www.example.com/", "http://www.example.com/", true], + + ["about:blank", "not a URL", null, true], + ["about:blank", "not a URL", "about:blank", true], + ["about:blank", "not a URL", "http://www.example.com/", true], + + ["http://www.example.com/", "", null, true], + ["http://www.example.com/", "", "about:blank", true], + ["http://www.example.com/", "", "http://www.example.com/", true], + + ["http://www.example.com/", "about:blank", null, false], + ["http://www.example.com/", "about:blank", "about:blank", false], + [ + "http://www.example.com/", + "about:blank", + "http://www.example.com/", + false, + ], + + ["http://www.example.com/", "http://www.example.com/", null, true], + ["http://www.example.com/", "http://www.example.com/", "about:blank", true], + [ + "http://www.example.com/", + "http://www.example.com/", + "http://www.example.com/", + true, + ], + + ["http://www.example.com/", "not a URL", null, true], + ["http://www.example.com/", "not a URL", "about:blank", true], + ["http://www.example.com/", "not a URL", "http://www.example.com/", true], + ]) { + await doSetURITest(...test); + } +}); + +async function doSetURITest(initialURL, searchString, url, expectSearchMode) { + info( + "doSetURITest with args: " + + JSON.stringify({ + initialURL, + searchString, + url, + expectSearchMode, + }) + ); + + await BrowserTestUtils.withNewTab(initialURL, async () => { + if (searchString) { + // Do a search with the search string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + } else { + // Open top sites. + await UrlbarTestUtils.promisePopupOpen(window, () => { + document.getElementById("Browser:OpenLocation").doCommand(); + }); + } + + // Enter search mode and close the view. + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + await UrlbarTestUtils.promisePopupClose(window); + Assert.strictEqual( + gBrowser.selectedBrowser.userTypedValue, + searchString, + `userTypedValue should be ${searchString}` + ); + + // Call setURI. + let uri = url ? Services.io.newURI(url) : null; + gURLBar.setURI(uri); + + await UrlbarTestUtils.assertSearchMode( + window, + !expectSearchMode + ? null + : { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + } + ); + + gURLBar.handleRevert(); + await UrlbarTestUtils.assertSearchMode(window, null); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_suggestions.js b/browser/components/urlbar/tests/browser/browser_searchMode_suggestions.js new file mode 100644 index 0000000000..6e9b3c1031 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_suggestions.js @@ -0,0 +1,581 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests search suggestions in search mode. + */ + +const DEFAULT_ENGINE_NAME = "Test"; +const SUGGESTIONS_ENGINE_NAME = "searchSuggestionEngine.xml"; +const MANY_SUGGESTIONS_ENGINE_NAME = "searchSuggestionEngineMany.xml"; +const MAX_RESULT_COUNT = UrlbarPrefs.get("maxRichResults"); + +let suggestionsEngine; +let expectedFormHistoryResults = []; + +add_setup(async function () { + suggestionsEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + SUGGESTIONS_ENGINE_NAME, + }); + + await SearchTestUtils.installSearchExtension( + { + name: DEFAULT_ENGINE_NAME, + keyword: "@test", + }, + { setAsDefault: true } + ); + await Services.search.moveEngine(suggestionsEngine, 0); + + async function cleanup() { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + } + await cleanup(); + registerCleanupFunction(cleanup); + + // Add some form history for our test engine. + for (let i = 0; i < MAX_RESULT_COUNT; i++) { + let value = `hello formHistory ${i}`; + await UrlbarTestUtils.formHistory.add([ + { value, source: suggestionsEngine.name }, + ]); + expectedFormHistoryResults.push({ + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + searchParams: { + suggestion: value, + engine: suggestionsEngine.name, + }, + }); + } + + // Add other form history. + await UrlbarTestUtils.formHistory.add([ + { value: "hello formHistory global" }, + { value: "hello formHistory other", source: "other engine" }, + ]); + + registerCleanupFunction(async () => { + await UrlbarTestUtils.formHistory.clear(); + }); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", false], + ["browser.urlbar.suggest.quickactions", false], + ["browser.urlbar.suggest.trending", false], + ["browser.urlbar.suggest.recentsearches", false], + ], + }); +}); + +add_task(async function emptySearch() { + await BrowserTestUtils.withNewTab("about:robots", async function (browser) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.update2.emptySearchBehavior", 2]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal(gURLBar.value, "", "Urlbar value should be cleared."); + // For the empty search case, we expect to get the form history relative to + // the picked engine and no heuristic. + await checkResults(expectedFormHistoryResults); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + }); +}); + +add_task(async function emptySearch_withRestyledHistory() { + // URLs with the same host as the search engine. + await PlacesTestUtils.addVisits([ + `http://mochi.test/`, + `http://mochi.test/redirect`, + // Should not be returned because it's a redirect target. + { + uri: `http://mochi.test/target`, + transition: PlacesUtils.history.TRANSITIONS.REDIRECT_TEMPORARY, + referrer: `http://mochi.test/redirect`, + }, + // Can be restyled and dupes form history. + "http://mochi.test:8888/?terms=hello+formHistory+0", + // Can be restyled but does not dupe form history. + "http://mochi.test:8888/?terms=ciao", + ]); + await BrowserTestUtils.withNewTab("about:robots", async function (browser) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.update2.emptySearchBehavior", 2]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal(gURLBar.value, "", "Urlbar value should be cleared."); + // For the empty search case, we expect to get the form history relative to + // the picked engine, history without redirects, and no heuristic. + await checkResults([ + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + searchParams: { + suggestion: "ciao", + engine: suggestionsEngine.name, + }, + }, + ...expectedFormHistoryResults.slice(0, MAX_RESULT_COUNT - 3), + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/`, + }, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/redirect`, + }, + ]); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + }); + + await PlacesUtils.history.clear(); +}); + +add_task(async function emptySearch_withRestyledHistory_noSearchHistory() { + // URLs with the same host as the search engine. + await PlacesTestUtils.addVisits([ + `http://mochi.test/`, + `http://mochi.test/redirect`, + // Can be restyled but does not dupe form history. + "http://mochi.test:8888/?terms=ciao", + ]); + await BrowserTestUtils.withNewTab("about:robots", async function (browser) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.update2.emptySearchBehavior", 2], + ["browser.urlbar.maxHistoricalSearchSuggestions", 0], + ], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal(gURLBar.value, "", "Urlbar value should be cleared."); + // maxHistoricalSearchSuggestions == 0, so form history should not be + // present. + await checkResults([ + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/redirect`, + }, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/`, + }, + ]); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + }); + + await PlacesUtils.history.clear(); +}); + +add_task(async function emptySearch_behavior() { + // URLs with the same host as the search engine. + await PlacesTestUtils.addVisits([`http://mochi.test/`]); + + await BrowserTestUtils.withNewTab("about:robots", async function (browser) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.update2.emptySearchBehavior", 0]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal(gURLBar.value, "", "Urlbar value should be cleared."); + // For the empty search case, we expect to get the form history relative to + // the picked engine, history without redirects, and no heuristic. + await checkResults([]); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + + // We should still show history for empty searches when not in search mode. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: " ", + }); + await checkResults([ + { + heuristic: true, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query: " ", + engine: DEFAULT_ENGINE_NAME, + }, + }, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/`, + }, + ]); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + }); + + await BrowserTestUtils.withNewTab("about:robots", async function (browser) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.update2.emptySearchBehavior", 1]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal(gURLBar.value, "", "Urlbar value should be cleared."); + // For the empty search case, we expect to get the form history relative to + // the picked engine, history without redirects, and no heuristic. + await checkResults([...expectedFormHistoryResults]); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + }); + + await PlacesUtils.history.clear(); +}); + +add_task(async function emptySearch_local() { + await PlacesTestUtils.addVisits([`http://mochi.test/`]); + + await BrowserTestUtils.withNewTab("about:robots", async function (browser) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.update2.emptySearchBehavior", 0]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }); + Assert.equal(gURLBar.value, "", "Urlbar value should be cleared."); + // Even when emptySearchBehavior is 0, we still show the user's most frecent + // history for an empty search. + await checkResults([ + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/`, + }, + ]); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + }); + + await PlacesUtils.history.clear(); +}); + +add_task(async function nonEmptySearch() { + await BrowserTestUtils.withNewTab("about:robots", async function (browser) { + let query = "hello"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal(gURLBar.value, query, "Urlbar value should be set."); + // We expect to get the heuristic and all the suggestions. + await checkResults([ + { + heuristic: true, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + engine: suggestionsEngine.name, + }, + }, + ...expectedFormHistoryResults.slice(0, MAX_RESULT_COUNT - 3), + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + suggestion: `${query}foo`, + engine: suggestionsEngine.name, + }, + }, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + suggestion: `${query}bar`, + engine: suggestionsEngine.name, + }, + }, + ]); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +add_task(async function nonEmptySearch_nonMatching() { + await BrowserTestUtils.withNewTab("about:robots", async function (browser) { + let query = "ciao"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal(gURLBar.value, query, "Urlbar value should be set."); + // We expect to get the heuristic and the remote suggestions since the local + // ones don't match. + await checkResults([ + { + heuristic: true, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + engine: suggestionsEngine.name, + }, + }, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + suggestion: `${query}foo`, + engine: suggestionsEngine.name, + }, + }, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + suggestion: `${query}bar`, + engine: suggestionsEngine.name, + }, + }, + ]); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +add_task(async function nonEmptySearch_withHistory() { + let manySuggestionsEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + MANY_SUGGESTIONS_ENGINE_NAME, + }); + // URLs with the same host as the search engine. + let query = "ciao"; + await PlacesTestUtils.addVisits([ + `http://mochi.test/${query}`, + `http://mochi.test/${query}1`, + // Should not be returned because it has a different host, even if it + // matches the host in the path. + `http://example.com/mochi.test/${query}`, + ]); + + function makeSuggestionResult(suffix) { + return { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + suggestion: `${query}${suffix}`, + engine: manySuggestionsEngine.name, + }, + }; + } + + await BrowserTestUtils.withNewTab("about:robots", async function (browser) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: manySuggestionsEngine.name, + }); + Assert.equal(gURLBar.value, query, "Urlbar value should be set."); + + await checkResults([ + { + heuristic: true, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + engine: manySuggestionsEngine.name, + }, + }, + makeSuggestionResult("foo"), + makeSuggestionResult("bar"), + makeSuggestionResult("1"), + makeSuggestionResult("2"), + makeSuggestionResult("3"), + makeSuggestionResult("4"), + makeSuggestionResult("5"), + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/${query}1`, + }, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/${query}`, + }, + ]); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + + info("Test again with history before suggestions"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchSuggestionsFirst", false]], + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: manySuggestionsEngine.name, + }); + Assert.equal(gURLBar.value, query, "Urlbar value should be set."); + + await checkResults([ + { + heuristic: true, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + engine: manySuggestionsEngine.name, + }, + }, + makeSuggestionResult("foo"), + makeSuggestionResult("bar"), + makeSuggestionResult("1"), + makeSuggestionResult("2"), + makeSuggestionResult("3"), + makeSuggestionResult("4"), + makeSuggestionResult("5"), + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/${query}1`, + }, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/${query}`, + }, + ]); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + }); + + await PlacesUtils.history.clear(); +}); + +add_task(async function nonEmptySearch_url() { + await BrowserTestUtils.withNewTab("about:robots", async function (browser) { + let query = "http://www.example.com/"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + await UrlbarTestUtils.enterSearchMode(window); + + // The heuristic result for a search that's a valid URL should be a search + // result, not a URL result. + await checkResults([ + { + heuristic: true, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + engine: suggestionsEngine.name, + }, + }, + ]); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +async function checkResults(expectedResults) { + Assert.equal( + UrlbarTestUtils.getResultCount(window), + expectedResults.length, + "Check results count." + ); + for (let i = 0; i < expectedResults.length; ++i) { + info(`Checking result at index ${i}`); + let expected = expectedResults[i]; + let actual = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + + // Check each property defined in the expected result against the property + // in the actual result. + for (let key of Object.keys(expected)) { + // For searchParams, remove undefined properties in the actual result so + // that the expected result doesn't need to include them. + if (key == "searchParams") { + let actualSearchParams = actual.searchParams; + for (let spKey of Object.keys(actualSearchParams)) { + if (actualSearchParams[spKey] === undefined) { + delete actualSearchParams[spKey]; + } + } + } + Assert.deepEqual( + actual[key], + expected[key], + `${key} should match at result index ${i}.` + ); + } + } +} diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_switchTabs.js b/browser/components/urlbar/tests/browser/browser_searchMode_switchTabs.js new file mode 100644 index 0000000000..db278ad9ba --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_switchTabs.js @@ -0,0 +1,317 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that search mode is stored per tab and restored when switching tabs. + */ + +"use strict"; + +// Enters search mode using the one-off buttons. +add_task(async function switchTabs() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + // Open three tabs. We'll enter search mode in tabs 0 and 2. + let tabs = []; + for (let i = 0; i < 3; i++) { + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "http://example.com/" + i, + }); + tabs.push(tab); + } + + // Switch to tab 0. + await BrowserTestUtils.switchTab(gBrowser, tabs[0]); + + // Do a search and enter search mode. Pass fireInputEvent so that + // userTypedValue is set and restored when we switch back to this tab. This + // isn't really necessary but it simulates the user's typing, and it also + // means that we'll start a search when we switch back to this tab. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + fireInputEvent: true, + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + // Switch to tab 1. Search mode should be exited. + await BrowserTestUtils.switchTab(gBrowser, tabs[1]); + await UrlbarTestUtils.assertSearchMode(window, null); + + // Switch back to tab 0. We should do a search (for "test") and re-enter + // search mode. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + await BrowserTestUtils.switchTab(gBrowser, tabs[0]); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal( + gURLBar.value, + "test", + "Value should remain the search string after switching back" + ); + + // Switch to tab 2. Search mode should be exited. + await BrowserTestUtils.switchTab(gBrowser, tabs[2]); + await UrlbarTestUtils.assertSearchMode(window, null); + + // Do another search (in tab 2) and enter search mode. Use a different source + // from tab 0 just to use something different. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test tab 2", + fireInputEvent: true, + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.TABS, + }); + + // Switch back to tab 0. We should do a search and still be in search mode. + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + await BrowserTestUtils.switchTab(gBrowser, tabs[0]); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal( + gURLBar.value, + "test", + "Value should remain the search string after switching back" + ); + + // Switch to tab 1. Search mode should be exited. + await BrowserTestUtils.switchTab(gBrowser, tabs[1]); + await UrlbarTestUtils.assertSearchMode(window, null); + + // Switch back to tab 2. We should do a search and re-enter search mode. + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + await BrowserTestUtils.switchTab(gBrowser, tabs[2]); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.TABS, + entry: "oneoff", + }); + Assert.equal( + gURLBar.value, + "test tab 2", + "Value should remain the search string after switching back" + ); + + // Exit search mode. + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + + // Switch to tab 0. We should do a search and re-enter search mode. + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + await BrowserTestUtils.switchTab(gBrowser, tabs[0]); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal( + gURLBar.value, + "test", + "Value should remain the search string after switching back" + ); + + // Switch back to tab 2. We should do a search but search mode should be + // inactive. + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + await BrowserTestUtils.switchTab(gBrowser, tabs[2]); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.value, + "test tab 2", + "Value should remain the search string after switching back" + ); + + // Switch back to tab 0. We should do a search and re-enter search mode. + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + await BrowserTestUtils.switchTab(gBrowser, tabs[0]); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal( + gURLBar.value, + "test", + "Value should remain the search string after switching back" + ); + + // Exit search mode. + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + + // Switch back to tab 2. We should do a search but search mode should be + // inactive. + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + await BrowserTestUtils.switchTab(gBrowser, tabs[2]); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.value, + "test tab 2", + "Value should remain the search string after switching back" + ); + + // Switch back to tab 0. We should do a search but search mode should be + // inactive. + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + await BrowserTestUtils.switchTab(gBrowser, tabs[0]); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.value, + "test", + "Value should remain the search string after switching back" + ); + + await UrlbarTestUtils.promisePopupClose(window); + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } +}); + +// Start loading a SERP from search mode then immediately switch to a new tab so +// the SERP finishes loading in the background. Switch back to the SERP tab and +// observe that we don't re-enter search mode despite having an entry for that +// tab in UrlbarInput._searchModesByBrowser. See bug 1675926. +// +// This subtest intermittently does not test bug 1675926 (NB: this does not mean +// it is an intermittent failure). The false-positive occurs if the SERP page +// finishes loading before we switch tabs. We include this subtest so we have +// one covering real-world behaviour. A subtest that is guaranteed to test this +// behaviour that does not simulate real world behaviour is included below. +add_task(async function slow_load() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", false]], + }); + const engineName = "Test"; + let extension = await SearchTestUtils.installSearchExtension( + { + name: engineName, + }, + { skipUnload: true } + ); + + const originalTab = gBrowser.selectedTab; + const newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + fireInputEvent: true, + }); + await UrlbarTestUtils.enterSearchMode(window, { engineName }); + + const loadPromise = BrowserTestUtils.browserLoaded(newTab.linkedBrowser); + // Select the search mode heuristic to load the example.com SERP. + EventUtils.synthesizeKey("KEY_Enter"); + // Switch away from the tab before we let it load. + await BrowserTestUtils.switchTab(gBrowser, originalTab); + await loadPromise; + + // Switch back to the search mode tab and confirm we don't restore search + // mode. + await BrowserTestUtils.switchTab(gBrowser, newTab); + await UrlbarTestUtils.assertSearchMode(window, null); + + BrowserTestUtils.removeTab(newTab); + await SpecialPowers.popPrefEnv(); + await extension.unload(); +}); + +// Tests the same behaviour as slow_load, but in a more reliable way using +// non-real-world behaviour. +add_task(async function slow_load_guaranteed() { + const engineName = "Test"; + let extension = await SearchTestUtils.installSearchExtension( + { + name: engineName, + }, + { skipUnload: true } + ); + + const backgroundTab = BrowserTestUtils.addTab(gBrowser); + + // Simulate a tab that was in search mode, loaded a SERP, then was switched + // away from before setURI was called. + backgroundTab.ownerGlobal.gURLBar.searchMode = { engineName }; + let loadPromise = BrowserTestUtils.browserLoaded(backgroundTab.linkedBrowser); + BrowserTestUtils.startLoadingURIString( + backgroundTab.linkedBrowser, + "http://example.com/?search=test" + ); + await loadPromise; + + // Switch to the background mode tab and confirm we don't restore search mode. + await BrowserTestUtils.switchTab(gBrowser, backgroundTab); + await UrlbarTestUtils.assertSearchMode(window, null); + + BrowserTestUtils.removeTab(backgroundTab); + await extension.unload(); +}); + +// Enters search mode by typing a restriction char with no search string. +// Search mode and the search string should be restored after switching back to +// the tab. +add_task(async function userTypedValue_empty() { + await doUserTypedValueTest(""); +}); + +// Enters search mode by typing a restriction char followed by a search string. +// Search mode and the search string should be restored after switching back to +// the tab. +add_task(async function userTypedValue_nonEmpty() { + await doUserTypedValueTest("foo bar"); +}); + +/** + * Enters search mode by typing a restriction char followed by a search string, + * opens a new tab and immediately closes it so we switch back to the search + * mode tab, and checks the search mode state and input value. + * + * @param {string} searchString + * The search string to enter search mode with. + */ +async function doUserTypedValueTest(searchString) { + let value = `${UrlbarTokenizer.RESTRICT.BOOKMARK} ${searchString}`; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + fireInputEvent: true, + }); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "typed", + }); + Assert.equal( + gURLBar.value, + searchString, + "Sanity check: Value is the search string" + ); + + let tab = await BrowserTestUtils.openNewForegroundTab({ gBrowser }); + BrowserTestUtils.removeTab(tab); + + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "typed", + }); + Assert.equal( + gURLBar.value, + searchString, + "Value should remain the search string after switching back" + ); + + gURLBar.handleRevert(); +} diff --git a/browser/components/urlbar/tests/browser/browser_searchSettings.js b/browser/components/urlbar/tests/browser/browser_searchSettings.js new file mode 100644 index 0000000000..2cded38c99 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchSettings.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async function () { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "a", + }); + + // Since the current tab is blank the preferences pane will load there + let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await UrlbarTestUtils.promisePopupClose(window, () => { + let button = document.getElementById("urlbar-anon-search-settings"); + EventUtils.synthesizeMouseAtCenter(button, {}); + }); + await loaded; + + is( + gBrowser.selectedBrowser.currentURI.spec, + "about:preferences#search", + "Should have loaded the right page" + ); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js b/browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js new file mode 100644 index 0000000000..36a065d58e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js @@ -0,0 +1,372 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +"use strict"; + +let gDNSResolved = false; +add_setup(async function () { + registerCleanupFunction(function () { + Services.prefs.clearUserPref("browser.fixup.domainwhitelist.localhost"); + }); +}); + +function promiseNotification(aBrowser, value, expected, input) { + return new Promise(resolve => { + let notificationBox = aBrowser.getNotificationBox(aBrowser.selectedBrowser); + if (expected) { + info("Waiting for " + value + " notification"); + resolve( + BrowserTestUtils.waitForNotificationInNotificationBox( + notificationBox, + value + ) + ); + } else { + setTimeout(() => { + is( + notificationBox.getNotificationWithValue(value), + null, + `We are expecting to not get a notification for ${input}` + ); + resolve(); + }, 1000); + } + }); +} + +async function runURLBarSearchTest({ + valueToOpen, + enterSearchMode, + expectSearch, + expectNotification, + expectDNSResolve, + aWindow = window, +}) { + gDNSResolved = false; + // Test both directly setting a value and pressing enter, or setting the + // value through input events, like the user would do. + const setValueFns = [ + value => { + aWindow.gURLBar.value = value; + if (enterSearchMode) { + // Ensure to open the panel. + UrlbarTestUtils.fireInputEvent(aWindow); + } + }, + value => { + return UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: aWindow, + value, + }); + }, + ]; + + for (let i = 0; i < setValueFns.length; ++i) { + await setValueFns[i](valueToOpen); + let topic = "uri-fixup-check-dns"; + let observer = (aSubject, aTopicInner, aData) => { + if (aTopicInner == topic) { + gDNSResolved = true; + } + }; + Services.obs.addObserver(observer, topic); + + if (enterSearchMode) { + if (!expectSearch) { + throw new Error("Must execute a search in search mode"); + } + await UrlbarTestUtils.enterSearchMode(aWindow); + } + + let expectedURI; + if (!expectSearch) { + expectedURI = "http://" + valueToOpen + "/"; + } else { + expectedURI = (await Services.search.getDefault()).getSubmission( + valueToOpen, + null, + "keyword" + ).uri.spec; + } + aWindow.gURLBar.focus(); + let docLoadPromise = BrowserTestUtils.waitForDocLoadAndStopIt( + expectedURI, + aWindow.gBrowser.selectedBrowser + ); + EventUtils.synthesizeKey("VK_RETURN", {}, aWindow); + + if (!enterSearchMode) { + await promiseNotification( + aWindow.gBrowser, + "keyword-uri-fixup", + expectNotification, + valueToOpen + ); + } + await docLoadPromise; + + if (expectNotification) { + let notificationBox = aWindow.gBrowser.getNotificationBox( + aWindow.gBrowser.selectedBrowser + ); + let notification = + notificationBox.getNotificationWithValue("keyword-uri-fixup"); + // Confirm the notification only on the last loop. + if (i == setValueFns.length - 1) { + docLoadPromise = BrowserTestUtils.waitForDocLoadAndStopIt( + "http://" + valueToOpen + "/", + aWindow.gBrowser.selectedBrowser + ); + notification.buttonContainer.querySelector("button").click(); + await docLoadPromise; + } else { + notificationBox.currentNotification.close(); + } + } + + Services.obs.removeObserver(observer, topic); + Assert.equal( + gDNSResolved, + expectDNSResolve, + `Should${expectDNSResolve ? "" : " not"} DNS resolve "${valueToOpen}"` + ); + } +} + +add_task(async function test_navigate_full_domain() { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await runURLBarSearchTest({ + valueToOpen: "www.singlewordtest.org", + expectSearch: false, + expectNotification: false, + expectDNSResolve: false, + }); + gBrowser.removeTab(tab); +}); + +add_task(async function test_navigate_decimal_ip() { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await runURLBarSearchTest({ + valueToOpen: "1234", + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, // Possible IP in numeric format. + }); + gBrowser.removeTab(tab); +}); + +add_task(async function test_navigate_decimal_ip_with_path() { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await runURLBarSearchTest({ + valueToOpen: "1234/12", + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, + }); + gBrowser.removeTab(tab); +}); + +add_task(async function test_navigate_large_number() { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await runURLBarSearchTest({ + valueToOpen: "123456789012345", + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, // Possible IP in numeric format. + }); + gBrowser.removeTab(tab); +}); + +add_task(async function test_navigate_small_hex_number() { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await runURLBarSearchTest({ + valueToOpen: "0x1f00ffff", + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, // Possible IP in numeric format. + }); + gBrowser.removeTab(tab); +}); + +add_task(async function test_navigate_large_hex_number() { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await runURLBarSearchTest({ + valueToOpen: "0x7f0000017f000001", + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, // Possible IP in numeric format. + }); + gBrowser.removeTab(tab); +}); + +function get_test_function_for_localhost_with_hostname( + hostName, + isPrivate = false +) { + return async function test_navigate_single_host() { + info(`Test ${hostName}${isPrivate ? " in Private Browsing mode" : ""}`); + const pref = "browser.fixup.domainwhitelist.localhost"; + let win; + if (isPrivate) { + let promiseWin = BrowserTestUtils.waitForNewWindow(); + win = OpenBrowserWindow({ private: true }); + await promiseWin; + await SimpleTest.promiseFocus(win); + } else { + win = window; + } + + // Remove the domain from the whitelist + Services.prefs.setBoolPref(pref, false); + + // The notification should not appear because the default value of + // browser.urlbar.dnsResolveSingleWordsAfterSearch is 0 + await BrowserTestUtils.withNewTab( + { + gBrowser: win.gBrowser, + url: "about:blank", + }, + browser => + runURLBarSearchTest({ + valueToOpen: hostName, + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, + aWindow: win, + }) + ); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.dnsResolveSingleWordsAfterSearch", 1]], + }); + + // The notification should appear, unless we are in private browsing mode. + await BrowserTestUtils.withNewTab( + { + gBrowser: win.gBrowser, + url: "about:blank", + }, + browser => + runURLBarSearchTest({ + valueToOpen: hostName, + expectSearch: true, + expectNotification: true, + expectDNSResolve: true, + aWindow: win, + }) + ); + + // check pref value + let prefValue = Services.prefs.getBoolPref(pref); + is(prefValue, !isPrivate, "Pref should have the correct state."); + + // Now try again with the pref set. + // In a private window, the notification should appear again. + await BrowserTestUtils.withNewTab( + { + gBrowser: win.gBrowser, + url: "about:blank", + }, + browser => + runURLBarSearchTest({ + valueToOpen: hostName, + expectSearch: isPrivate, + expectNotification: isPrivate, + expectDNSResolve: isPrivate, + aWindow: win, + }) + ); + + if (isPrivate) { + info("Waiting for private window to close"); + await BrowserTestUtils.closeWindow(win); + await SimpleTest.promiseFocus(window); + } + + await SpecialPowers.popPrefEnv(); + }; +} + +add_task(get_test_function_for_localhost_with_hostname("localhost")); +add_task(get_test_function_for_localhost_with_hostname("localhost.")); +add_task(get_test_function_for_localhost_with_hostname("localhost", true)); + +add_task(async function test_dnsResolveSingleWordsAfterSearch() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.dnsResolveSingleWordsAfterSearch", 0], + ["browser.fixup.domainwhitelist.localhost", false], + ], + }); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:blank", + }, + browser => + runURLBarSearchTest({ + valueToOpen: "localhost", + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, + }) + ); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_navigate_invalid_url() { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await runURLBarSearchTest({ + valueToOpen: "mozilla is awesome", + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, + }); + gBrowser.removeTab(tab); +}); + +add_task(async function test_search_mode() { + info("When in search mode we should never query the DNS"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.suggest.enabled", false]], + }); + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await runURLBarSearchTest({ + enterSearchMode: true, + valueToOpen: "mozilla", + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, + }); + gBrowser.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchSuggestions.js b/browser/components/urlbar/tests/browser/browser_searchSuggestions.js new file mode 100644 index 0000000000..8a226a3c4c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchSuggestions.js @@ -0,0 +1,341 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests checks that search suggestions can be acted upon correctly + * e.g. selection with modifiers, copying text. + */ + +"use strict"; + +const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches"; +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +const MAX_CHARS_PREF = "browser.urlbar.maxCharsForSearchSuggestions"; + +// Must run first. +add_task(async function prepare() { + let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF); + Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true); + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + await UrlbarTestUtils.formHistory.clear(); + registerCleanupFunction(async function () { + Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled); + + // Clicking suggestions causes visits to search results pages, so clear that + // history now. + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async function clickSuggestion() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let [idx, suggestion, engineName] = await getFirstSuggestion(); + Assert.equal( + engineName, + "browser_searchSuggestionEngine searchSuggestionEngine.xml", + "Expected suggestion engine" + ); + + let uri = (await Services.search.getDefault()).getSubmission(suggestion).uri; + let loadPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + uri.spec + ); + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, idx); + EventUtils.synthesizeMouseAtCenter(element, {}, window); + await loadPromise; + + let formHistory = ( + await UrlbarTestUtils.formHistory.search({ source: engineName }) + ).map(entry => entry.value); + Assert.deepEqual( + formHistory, + ["foofoo"], + "Should find form history after adding it" + ); + + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); +}); + +async function testPressEnterOnSuggestion( + expectedUrl = null, + keyModifiers = {} +) { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let [idx, suggestion, engineName] = await getFirstSuggestion(); + Assert.equal( + engineName, + "browser_searchSuggestionEngine searchSuggestionEngine.xml", + "Expected suggestion engine" + ); + + let hasExpectedUrl = !!expectedUrl; + if (!expectedUrl) { + expectedUrl = (await Services.search.getDefault()).getSubmission(suggestion) + .uri.spec; + } + + let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt( + expectedUrl, + gBrowser.selectedBrowser + ); + + let promiseFormHistory; + if (!hasExpectedUrl) { + promiseFormHistory = UrlbarTestUtils.formHistory.promiseChanged("add"); + } + + for (let i = 0; i < idx; ++i) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + EventUtils.synthesizeKey("KEY_Enter", keyModifiers); + + await promiseLoad; + + if (!hasExpectedUrl) { + await promiseFormHistory; + let formHistory = ( + await UrlbarTestUtils.formHistory.search({ source: engineName }) + ).map(entry => entry.value); + Assert.deepEqual( + formHistory, + ["foofoo"], + "Should find form history after adding it" + ); + } + + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); +} + +add_task(async function plainEnterOnSuggestion() { + await testPressEnterOnSuggestion(); +}); + +add_task(async function ctrlEnterOnSuggestion() { + await testPressEnterOnSuggestion("https://www.foofoo.com/", { + ctrlKey: true, + }); +}); + +add_task(async function copySuggestionText() { + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let [idx, suggestion] = await getFirstSuggestion(); + for (let i = 0; i < idx; ++i) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + gURLBar.select(); + await SimpleTest.promiseClipboardChange(suggestion, () => { + goDoCommand("cmd_copy"); + }); +}); + +add_task(async function typeMaxChars() { + gURLBar.focus(); + + let maxChars = 10; + await SpecialPowers.pushPrefEnv({ + set: [[MAX_CHARS_PREF, maxChars]], + }); + + // Make a string as long as maxChars and type it. + let value = ""; + for (let i = 0; i < maxChars; i++) { + value += String.fromCharCode("a".charCodeAt(0) + i); + } + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + }); + + // Suggestions should be fetched since we allow them when typing, and the + // value so far isn't longer than maxChars anyway. + await assertSuggestions([value + "foo", value + "bar"]); + + // Now type some additional chars. Suggestions should still be fetched since + // we allow them when typing. + for (let i = 0; i < 3; i++) { + let char = String.fromCharCode("z".charCodeAt(0) - i); + value += char; + EventUtils.synthesizeKey(char); + await assertSuggestions([value + "foo", value + "bar"]); + } + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function pasteMaxChars() { + gURLBar.focus(); + + let maxChars = 10; + await SpecialPowers.pushPrefEnv({ + set: [[MAX_CHARS_PREF, maxChars]], + }); + + // Make a string as long as maxChars and paste it. + let value = ""; + for (let i = 0; i < maxChars; i++) { + value += String.fromCharCode("a".charCodeAt(0) + i); + } + await selectAndPaste(value); + + // Suggestions should be fetched since the pasted string is not longer than + // maxChars. + await assertSuggestions([value + "foo", value + "bar"]); + + // Now type some additional chars. Suggestions should still be fetched since + // we allow them when typing. + for (let i = 0; i < 3; i++) { + let char = String.fromCharCode("z".charCodeAt(0) - i); + value += char; + EventUtils.synthesizeKey(char); + await assertSuggestions([value + "foo", value + "bar"]); + } + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function pasteMoreThanMaxChars() { + gURLBar.focus(); + + let maxChars = 10; + await SpecialPowers.pushPrefEnv({ + set: [[MAX_CHARS_PREF, maxChars]], + }); + + // Make a string longer than maxChars and paste it. + let value = ""; + for (let i = 0; i < 2 * maxChars; i++) { + value += String.fromCharCode("a".charCodeAt(0) + i); + } + await selectAndPaste(value); + + // Suggestions should not be fetched since the value was pasted and it was + // longer than maxChars. + await assertSuggestions([]); + + // Now type some additional chars. Suggestions should now be fetched since we + // allow them when typing. + for (let i = 0; i < 3; i++) { + let char = String.fromCharCode("z".charCodeAt(0) - i); + value += char; + EventUtils.synthesizeKey(char); + await assertSuggestions([value + "foo", value + "bar"]); + } + + // Paste again. The string is longer than maxChars, so suggestions should not + // be fetched. + await selectAndPaste(value); + await assertSuggestions([]); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function heuristicAddsFormHistory() { + await UrlbarTestUtils.formHistory.clear(); + let formHistory = (await UrlbarTestUtils.formHistory.search()).map( + entry => entry.value + ); + Assert.deepEqual(formHistory, [], "Form history should be empty initially"); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(result.heuristic); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH); + Assert.equal(result.searchParams.query, "foo"); + + let uri = (await Services.search.getDefault()).getSubmission("foo").uri; + let loadPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + uri.spec + ); + let formHistoryPromise = UrlbarTestUtils.formHistory.promiseChanged("add"); + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + EventUtils.synthesizeMouseAtCenter(element, {}, window); + await loadPromise; + + await formHistoryPromise; + formHistory = ( + await UrlbarTestUtils.formHistory.search({ + source: result.searchParams.engine, + }) + ).map(entry => entry.value); + Assert.deepEqual( + formHistory, + ["foo"], + "Should find form history after adding it" + ); + + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); +}); + +async function getFirstSuggestion() { + let results = await getSuggestionResults(); + if (!results.length) { + return [-1, null, null]; + } + let result = results[0]; + return [ + result.index, + result.searchParams.suggestion, + result.searchParams.engine, + ]; +} + +async function getSuggestionResults() { + await UrlbarTestUtils.promiseSearchComplete(window); + + let results = []; + let matchCount = UrlbarTestUtils.getResultCount(window); + for (let i = 0; i < matchCount; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if ( + result.type == UrlbarUtils.RESULT_TYPE.SEARCH && + result.searchParams.suggestion + ) { + result.index = i; + results.push(result); + } + } + return results; +} + +async function assertSuggestions(expectedSuggestions) { + let results = await getSuggestionResults(); + let actualSuggestions = results.map(r => r.searchParams.suggestion); + Assert.deepEqual( + actualSuggestions, + expectedSuggestions, + "Expected suggestions" + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_searchTelemetry.js b/browser/components/urlbar/tests/browser/browser_searchTelemetry.js new file mode 100644 index 0000000000..61ddff4c2d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchTelemetry.js @@ -0,0 +1,220 @@ +"use strict"; + +const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches"; +const MAX_FORM_HISTORY_PREF = "browser.urlbar.maxHistoricalSearchSuggestions"; +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +// Must run first. +add_task(async function prepare() { + await SpecialPowers.pushPrefEnv({ + set: [ + [SUGGEST_URLBAR_PREF, true], + [MAX_FORM_HISTORY_PREF, 2], + ], + }); + + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + + registerCleanupFunction(async function () { + // Clicking urlbar results causes visits to their associated pages, so clear + // that history now. + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); + + // Move the mouse away from the urlbar one-offs so that a one-off engine is + // not inadvertently selected. + await EventUtils.promiseNativeMouseEvent({ + type: "mousemove", + target: window.document.documentElement, + offsetX: 0, + offsetY: 0, + }); +}); + +add_task(async function heuristicResultMouse() { + await compareCounts(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "heuristicResult", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Should be of type search" + ); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + EventUtils.synthesizeMouseAtCenter(element, {}); + await loadPromise; + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async function heuristicResultKeyboard() { + await compareCounts(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "heuristicResult", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Should be of type search" + ); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.sendKey("return"); + await loadPromise; + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async function searchSuggestionMouse() { + await compareCounts(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "searchSuggestion", + }); + let idx = await getFirstSuggestionIndex(); + Assert.greaterOrEqual(idx, 0, "there should be a first suggestion"); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + let element = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + idx + ); + EventUtils.synthesizeMouseAtCenter(element, {}); + await loadPromise; + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async function searchSuggestionKeyboard() { + await compareCounts(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "searchSuggestion", + }); + let idx = await getFirstSuggestionIndex(); + Assert.greaterOrEqual(idx, 0, "there should be a first suggestion"); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + while (idx--) { + EventUtils.sendKey("down"); + } + EventUtils.sendKey("return"); + await loadPromise; + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async function formHistoryMouse() { + await compareCounts(async function () { + await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let index = await getFirstSuggestionIndex(); + Assert.greaterOrEqual(index, 0, "there should be a first suggestion"); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH); + Assert.equal(result.source, UrlbarUtils.RESULT_SOURCE.HISTORY); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + let element = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + index + ); + EventUtils.synthesizeMouseAtCenter(element, {}); + await loadPromise; + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async function formHistoryKeyboard() { + await compareCounts(async function () { + await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let index = await getFirstSuggestionIndex(); + Assert.greaterOrEqual(index, 0, "there should be a first suggestion"); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH); + Assert.equal(result.source, UrlbarUtils.RESULT_SOURCE.HISTORY); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + while (index--) { + EventUtils.sendKey("down"); + } + EventUtils.sendKey("return"); + await loadPromise; + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +/** + * This does three things: gets current telemetry/FHR counts, calls + * clickCallback, gets telemetry/FHR counts again to compare them to the old + * counts. + * + * @param {Function} clickCallback Use this to open the urlbar popup and choose + * and click a result. + */ +async function compareCounts(clickCallback) { + // Search events triggered by clicks (not the Return key in the urlbar) are + // recorded in three places: + // * Telemetry histogram named "SEARCH_COUNTS" + // * FHR + + let engine = await Services.search.getDefault(); + + let histogramKey = `other-${engine.name}.urlbar`; + let histogram = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS"); + histogram.clear(); + + gURLBar.focus(); + await clickCallback(); + + TelemetryTestUtils.assertKeyedHistogramSum(histogram, histogramKey, 1); +} + +/** + * Returns the index of the first search suggestion in the urlbar results. + * + * @returns {number} An index, or -1 if there are no search suggestions. + */ +async function getFirstSuggestionIndex() { + const matchCount = UrlbarTestUtils.getResultCount(window); + for (let i = 0; i < matchCount; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if ( + result.type == UrlbarUtils.RESULT_TYPE.SEARCH && + result.searchParams.suggestion + ) { + return i; + } + } + return -1; +} diff --git a/browser/components/urlbar/tests/browser/browser_search_bookmarks_from_bookmarks_menu.js b/browser/components/urlbar/tests/browser/browser_search_bookmarks_from_bookmarks_menu.js new file mode 100644 index 0000000000..499399db3a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_search_bookmarks_from_bookmarks_menu.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function searchBookmarksFromBooksmarksMenu() { + // Add Button to toolbar + CustomizableUI.addWidgetToArea( + "bookmarks-menu-button", + CustomizableUI.AREA_NAVBAR, + 0 + ); + let bookmarksMenuButton = document.getElementById("bookmarks-menu-button"); + ok(bookmarksMenuButton, "Bookmarks Menu Button added"); + + // Open Bookmarks-Menu-Popup + let bookmarksMenuPopup = document.getElementById("BMB_bookmarksPopup"); + let PopupShownPromise = BrowserTestUtils.waitForEvent( + bookmarksMenuPopup, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(bookmarksMenuButton, { + type: "mousedown", + }); + await PopupShownPromise; + ok(true, "Bookmarks Menu Popup shown"); + + // Click on 'Search Bookmarks' + let searchBookmarksButton = document.getElementById("BMB_searchBookmarks"); + ok( + BrowserTestUtils.isVisible( + searchBookmarksButton, + "'Search Bookmarks Button' is visible." + ) + ); + EventUtils.synthesizeMouseAtCenter(searchBookmarksButton, {}); + + await new Promise(resolve => { + window.gURLBar.controller.addQueryListener({ + onViewOpen() { + window.gURLBar.controller.removeQueryListener(this); + resolve(); + }, + }); + }); + + // Verify URLBar is in search mode with correct restriction + is( + gURLBar.searchMode?.source, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + "Addressbar in correct mode." + ); + + resetCUIAndReinitUrlbarInput(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_search_continuation.js b/browser/components/urlbar/tests/browser/browser_search_continuation.js new file mode 100644 index 0000000000..8a24d57856 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_search_continuation.js @@ -0,0 +1,113 @@ +/* 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/. */ + +/** + * Tests how trending and recent searches work together. + */ + +const CONFIG_DEFAULT = [ + { + webExtension: { id: "basic@search.mozilla.org" }, + urls: { + trending: { + fullPath: + "https://example.com/browser/browser/components/search/test/browser/trendingSuggestionEngine.sjs", + query: "", + }, + }, + appliesTo: [{ included: { everywhere: true } }], + default: "yes", + }, +]; + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.suggest.trending", true], + ["browser.urlbar.maxRichResults", 3], + ["browser.urlbar.trending.featureGate", true], + ["browser.urlbar.trending.requireSearchMode", false], + ["browser.urlbar.suggest.recentsearches", true], + ["browser.urlbar.recentsearches.featureGate", true], + [ + "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", + false, + ], + ], + }); + + await UrlbarTestUtils.formHistory.clear(); + await SearchTestUtils.setupTestEngines("search-engines", CONFIG_DEFAULT); + + registerCleanupFunction(async () => { + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async function test_trending_results() { + await check_results([ + "SearchSuggestions", + "SearchSuggestions", + "SearchSuggestions", + ]); + await doSearch("Testing 1"); + await check_results([ + "RecentSearches", + "SearchSuggestions", + "SearchSuggestions", + ]); + await doSearch("Testing 2"); + await check_results([ + "RecentSearches", + "RecentSearches", + "SearchSuggestions", + ]); + await doSearch("Testing 3"); + await check_results(["RecentSearches", "RecentSearches", "RecentSearches"]); +}); + +async function check_results(results) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + waitForFocus: SimpleTest.waitForFocus, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + results.length, + "We matched the expected number of results" + ); + + for (let i = 0; i < results.length; i++) { + let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.equal(result.providerName, results[i]); + } + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); +} + +async function doSearch(search) { + info("Perform a search that will be added to search history."); + let tab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + "data:text/html," + ); + let browserLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: search, + waitForFocus: SimpleTest.waitForFocus, + }); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Enter", {}, window); + }); + await browserLoaded; + + await BrowserTestUtils.removeTab(tab); +} diff --git a/browser/components/urlbar/tests/browser/browser_search_history_from_history_panel.js b/browser/components/urlbar/tests/browser/browser_search_history_from_history_panel.js new file mode 100644 index 0000000000..a61f9a6eed --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_search_history_from_history_panel.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { CustomizableUITestUtils } = ChromeUtils.importESModule( + "resource://testing-common/CustomizableUITestUtils.sys.mjs" +); +let gCUITestUtils = new CustomizableUITestUtils(window); + +add_task(async function searchHistoryFromHistoryPanel() { + // Add Button to toolbar + CustomizableUI.addWidgetToArea( + "history-panelmenu", + CustomizableUI.AREA_NAVBAR, + 0 + ); + registerCleanupFunction(() => { + resetCUIAndReinitUrlbarInput(); + }); + + let historyButton = document.getElementById("history-panelmenu"); + ok(historyButton, "History button appears in Panel Menu"); + + historyButton.click(); + + let historyPanel = document.getElementById("PanelUI-history"); + let promise = BrowserTestUtils.waitForEvent(historyPanel, "ViewShown"); + await promise; + ok(historyPanel.getAttribute("visible"), "History Panel is in view"); + + // Click on 'Search Bookmarks' + let searchHistoryButton = document.getElementById("appMenuSearchHistory"); + ok( + BrowserTestUtils.isVisible( + searchHistoryButton, + "'Search History Button' is visible." + ) + ); + EventUtils.synthesizeMouseAtCenter(searchHistoryButton, {}); + + await new Promise(resolve => { + window.gURLBar.controller.addQueryListener({ + onViewOpen() { + window.gURLBar.controller.removeQueryListener(this); + resolve(); + }, + }); + }); + + // Verify URLBar is in search mode with correct restriction + is( + gURLBar.searchMode?.source, + UrlbarUtils.RESULT_SOURCE.HISTORY, + "Addressbar in correct mode." + ); + gURLBar.searchMode = null; + gURLBar.blur(); +}); + +add_task(async function searchHistoryFromAppMenuHistoryButton() { + // Open main menu and click on 'History' button + await gCUITestUtils.openMainMenu(); + let historyButton = document.getElementById("appMenu-history-button"); + historyButton.click(); + + let historyPanel = document.getElementById("PanelUI-history"); + let promise = BrowserTestUtils.waitForEvent(historyPanel, "ViewShown"); + await promise; + ok(historyPanel.getAttribute("visible"), "History Panel is in view"); + + // Click on 'Search Bookmarks' + let searchHistoryButton = document.getElementById("appMenuSearchHistory"); + ok( + BrowserTestUtils.isVisible( + searchHistoryButton, + "'Search History Button' is visible." + ) + ); + EventUtils.synthesizeMouseAtCenter(searchHistoryButton, {}); + + await new Promise(resolve => { + window.gURLBar.controller.addQueryListener({ + onViewOpen() { + window.gURLBar.controller.removeQueryListener(this); + resolve(); + }, + }); + }); + + // Verify URLBar is in search mode with correct restriction + is( + gURLBar.searchMode?.source, + UrlbarUtils.RESULT_SOURCE.HISTORY, + "Addressbar in correct mode." + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_selectStaleResults.js b/browser/components/urlbar/tests/browser/browser_selectStaleResults.js new file mode 100644 index 0000000000..c381478712 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_selectStaleResults.js @@ -0,0 +1,329 @@ +/* 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 test makes sure that arrowing down and up through the view's results +// works correctly with regard to stale results. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarView: "resource:///modules/UrlbarView.sys.mjs", +}); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + // We'll later replace this, so ensure it's restored. + let originalRemoveStaleRowsTimeout = UrlbarView.removeStaleRowsTimeout; + registerCleanupFunction(() => { + UrlbarView.removeStaleRowsTimeout = originalRemoveStaleRowsTimeout; + }); +}); + +// This tests the case where queryContext.results.length < the number of rows in +// the view, i.e., the view contains stale rows. +add_task(async function viewContainsStaleRows() { + // Set the remove-stale-rows timer to a very large value, so there's no + // possibility it interferes with this test. + UrlbarView.removeStaleRowsTimeout = 10000; + + // For the test stability we need a slow provider that ensures the search + // doesn't complete too fast. + let slowProvider = new UrlbarTestUtils.TestProvider({ + results: [], + name: "emptySlowProvider", + addTimeout: 1000, + }); + UrlbarProvidersManager.registerProvider(slowProvider); + registerCleanupFunction(() => { + UrlbarProvidersManager.unregisterProvider(slowProvider); + }); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + let maxResults = UrlbarPrefs.get("maxRichResults"); + let halfResults = Math.floor(maxResults / 2); + + // Add enough visits to pages with "xx" in the title to fill up half the view. + for (let i = 0; i < halfResults; i++) { + await PlacesTestUtils.addVisits({ + uri: "http://mochi.test:8888/" + i, + title: "xx" + i, + }); + } + + // Add enough visits to pages with "x" in the title to fill up the entire + // view. + for (let i = 0; i < maxResults; i++) { + await PlacesTestUtils.addVisits({ + uri: "http://example.com/" + i, + title: "x" + i, + }); + } + + gURLBar.focus(); + + // Search for "x" and wait for the search to finish. All the "x" results + // added above should be in the view. (Actually one fewer will be in the + // view due to the heuristic result, but that's not important.) + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "x", + fireInputEvent: true, + }); + + // Below we'll do a search for "xx". Get the row that will show the last + // result in that search, and await for it to be updated. + Assert.ok( + !UrlbarTestUtils.getRowAt(window, halfResults).hasAttribute("stale"), + "Should not be stale" + ); + + let lastMatchingResultUpdatedPromise = TestUtils.waitForCondition(() => { + let row = UrlbarTestUtils.getRowAt(window, halfResults); + console.log(row.result.title); + return row.result.title.startsWith("xx"); + }, "Wait for the result to be updated"); + + // Type another "x" so that we search for "xx", but don't wait for the search + // to finish. Instead, wait for the row to be updated. + EventUtils.synthesizeKey("x"); + await lastMatchingResultUpdatedPromise; + + // Now arrow down. The search, which is still ongoing, will now stop and the + // view won't be updated anymore. + EventUtils.synthesizeKey("KEY_ArrowDown"); + + // Wait for the search to stop. + info("Waiting for the search to stop... "); + await gURLBar.lastQueryContextPromise; + + // Check stale status of results. + Assert.ok( + !UrlbarTestUtils.getRowAt(window, halfResults).hasAttribute("stale"), + "Should not be stale" + ); + Assert.ok( + UrlbarTestUtils.getRowAt(window, halfResults + 1).hasAttribute("stale"), + "Should be stale" + ); + + // The query context for the last search ("xx") should contain only + // halfResults + 1 results (+ 1 for the heuristic). + Assert.ok(gURLBar.controller._lastQueryContextWrapper); + let { queryContext } = gURLBar.controller._lastQueryContextWrapper; + Assert.ok(queryContext); + Assert.equal(queryContext.results.length, halfResults + 1); + + // But there should be maxResults visible rows in the view. + let items = Array.from( + UrlbarTestUtils.getResultsContainer(window).children + ).filter(r => BrowserTestUtils.isVisible(r)); + Assert.equal(items.length, maxResults); + + // Arrow down through all the results. After arrowing down from the last "xx" + // result, the stale "x" results should be selected. We should *not* enter + // the one-off search buttons at that point. + for (let i = 1; i < maxResults; i++) { + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.equal(result.element.row.result.rowIndex, i); + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + + // Now the first one-off should be selected. + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), -1); + Assert.equal(gURLBar.view.oneOffSearchButtons.selectedButtonIndex, 0); + + // Arrow back up through all the results. + for (let i = maxResults - 1; i >= 0; i--) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i); + } + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + UrlbarProvidersManager.unregisterProvider(slowProvider); +}); + +// This tests the case where, before the search finishes, stale results have +// been removed and replaced with non-stale results. +add_task(async function staleReplacedWithFresh() { + // For this test, we need one set of results that's added quickly and another + // set that's added after a delay. We do an initial search and wait for both + // sets to be added. Then we do another search, but this time only wait for + // the fast results to be added, and then we arrow down to stop the search + // before the delayed results are added. The order in which things should + // happen after the second search goes like this: + // + // (1) second search + // (2) fast results are added + // (3) remove-stale-rows timer fires and removes stale rows (the rows from + // the delayed set of results from the first search) + // (4) we arrow down to stop the search + // + // We use history for the fast results and a slow search engine for the + // delayed results. + // + // NB: If this test ends up failing, it may be because the remove-stale-rows + // timer fires before the history results are added. i.e., steps 2 and 3 + // above happen out of order. If that happens, try increasing it. + UrlbarView.removeStaleRowsTimeout = 1000; + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + // Enable search suggestions, and add an engine that returns suggestions on a + // delay. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", true]], + }); + let engine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngineSlow.xml", + }); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.moveEngine(engine, 0); + await Services.search.setDefault( + engine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + let maxResults = UrlbarPrefs.get("maxRichResults"); + + // Add enough visits to pages with "test" in the title to fill up the entire + // view. + for (let i = 0; i < maxResults; i++) { + await PlacesTestUtils.addVisits({ + uri: "http://example.com/" + i, + title: "test" + i, + }); + } + + gURLBar.focus(); + + // Search for "tes" and wait for the search to finish. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "tes", + fireInputEvent: true, + }); + + // Sanity check the results. They should be: + // + // tes -- Search with searchSuggestionEngineSlow [heuristic] + // tesfoo [search suggestion] + // tesbar [search suggestion] + // test9 [history] + // test8 [history] + // test7 [history] + // test6 [history] + // test5 [history] + // test4 [history] + // test3 [history] + let count = UrlbarTestUtils.getResultCount(window); + Assert.equal(count, maxResults); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(result.heuristic); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.ok(result.searchParams); + Assert.equal(result.searchParams.suggestion, "tesfoo"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.ok(result.searchParams); + Assert.equal(result.searchParams.suggestion, "tesbar"); + for (let i = 3; i < maxResults; i++) { + result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL); + Assert.equal(result.title, "test" + (maxResults - i + 2)); + } + + // Below we'll do a search for "test" *but* not wait for the two search + // suggestion results to be added. We'll only wait for the history results to + // be added. To determine when the history results are added, use a mutation + // listener on the node containing the rows, and wait until the title of the + // next-to-last row is "test2". At that point, the results should be: + // + // test -- Search with searchSuggestionEngineSlow + // test9 + // test8 + // test7 + // test6 + // test5 + // test4 + // test3 + // test2 + // test1 + let mutationPromise = new Promise(resolve => { + let observer = new MutationObserver(mutations => { + let row = UrlbarTestUtils.getRowAt(window, maxResults - 2); + if (row && row._elements.get("title").textContent == "test2") { + observer.disconnect(); + resolve(); + } + }); + observer.observe(UrlbarTestUtils.getResultsContainer(window), { + subtree: true, + characterData: true, + childList: true, + attributes: true, + }); + }); + + // Now type a "t" so that we search for "test", but only wait for history + // results to be added, as described above. + EventUtils.synthesizeKey("t"); + info("Waiting for the 'test2' row... "); + await mutationPromise; + + // Now arrow down. The search, which is still ongoing, will now stop and the + // view won't be updated anymore. + EventUtils.synthesizeKey("KEY_ArrowDown"); + + // Wait for the search to stop. + info("Waiting for the search to stop... "); + await gURLBar.lastQueryContextPromise; + + // Sanity check the results. They should be as described above. + count = UrlbarTestUtils.getResultCount(window); + Assert.equal(count, maxResults); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(result.heuristic); + Assert.equal(result.element.row.result.rowIndex, 0); + for (let i = 1; i < maxResults; i++) { + result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL); + Assert.equal(result.title, "test" + (maxResults - i)); + Assert.equal(result.element.row.result.rowIndex, i); + } + + // Arrow down through all the results. After arrowing down from "test3", we + // should continue on to "test2". We should *not* enter the one-off search + // buttons at that point. + for (let i = 1; i < maxResults; i++) { + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i); + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + + // Now the first one-off should be selected. + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), -1); + Assert.equal(gURLBar.view.oneOffSearchButtons.selectedButtonIndex, 0); + + // Arrow back up through all the results. + for (let i = maxResults - 1; i >= 0; i--) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i); + } + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + await SpecialPowers.popPrefEnv(); + await Services.search.setDefault( + oldDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_selectionKeyNavigation.js b/browser/components/urlbar/tests/browser/browser_selectionKeyNavigation.js new file mode 100644 index 0000000000..89ba179833 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_selectionKeyNavigation.js @@ -0,0 +1,200 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test makes sure that the up/down and page-up/down properly adjust the +// selection. See also browser_caret_navigation.js and +// browser_urlbar_tabKeyBehavior.js. + +"use strict"; + +const MAX_RESULTS = UrlbarPrefs.get("maxRichResults"); + +add_setup(async function () { + for (let i = 0; i < MAX_RESULTS; i++) { + await PlacesTestUtils.addVisits("http://example.com/" + i); + } + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function downKey() { + for (const ctrlKey of [false, true]) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "The heuristic autofill result should be selected initially" + ); + for (let i = 1; i < MAX_RESULTS; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown", { ctrlKey }); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i); + } + EventUtils.synthesizeKey("KEY_ArrowDown", { ctrlKey }); + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + Assert.ok(oneOffs.selectedButton, "A one-off should now be selected"); + while (oneOffs.selectedButton) { + EventUtils.synthesizeKey("KEY_ArrowDown", { ctrlKey }); + } + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "The heuristic autofill result should be selected again" + ); + } +}); + +add_task(async function upKey() { + for (const ctrlKey of [false, true]) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "The heuristic autofill result should be selected initially" + ); + EventUtils.synthesizeKey("KEY_ArrowUp", { ctrlKey }); + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + Assert.ok(oneOffs.selectedButton, "A one-off should now be selected"); + while (oneOffs.selectedButton) { + EventUtils.synthesizeKey("KEY_ArrowUp", { ctrlKey }); + } + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + MAX_RESULTS - 1, + "The last result should be selected" + ); + for (let i = 1; i < MAX_RESULTS; i++) { + EventUtils.synthesizeKey("KEY_ArrowUp", { ctrlKey }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + MAX_RESULTS - i - 1 + ); + } + } +}); + +add_task(async function pageDownKey() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "The heuristic autofill result should be selected initially" + ); + let pageCount = Math.ceil((MAX_RESULTS - 1) / UrlbarUtils.PAGE_UP_DOWN_DELTA); + for (let i = 0; i < pageCount; i++) { + EventUtils.synthesizeKey("KEY_PageDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + Math.min((i + 1) * UrlbarUtils.PAGE_UP_DOWN_DELTA, MAX_RESULTS - 1) + ); + } + EventUtils.synthesizeKey("KEY_PageDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "Page down at end should wrap around to first result" + ); +}); + +add_task(async function pageUpKey() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "The heuristic autofill result should be selected initially" + ); + EventUtils.synthesizeKey("KEY_PageUp"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + MAX_RESULTS - 1, + "Page up at start should wrap around to last result" + ); + let pageCount = Math.ceil((MAX_RESULTS - 1) / UrlbarUtils.PAGE_UP_DOWN_DELTA); + for (let i = 0; i < pageCount; i++) { + EventUtils.synthesizeKey("KEY_PageUp"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + Math.max(MAX_RESULTS - 1 - (i + 1) * UrlbarUtils.PAGE_UP_DOWN_DELTA, 0) + ); + } +}); + +add_task(async function pageDownKeyShowsView() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_PageDown"); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.ok(UrlbarTestUtils.isPopupOpen(window)); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0); +}); + +add_task(async function pageUpKeyShowsView() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_PageUp"); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.ok(UrlbarTestUtils.isPopupOpen(window)); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0); +}); + +add_task(async function pageDownKeyWithCtrlKey() { + const previousTab = gBrowser.selectedTab; + const currentTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_PageDown", { ctrlKey: true }); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal(gBrowser.selectedTab, previousTab); + BrowserTestUtils.removeTab(currentTab); +}); + +add_task(async function pageUpKeyWithCtrlKey() { + const previousTab = gBrowser.selectedTab; + const currentTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_PageUp", { ctrlKey: true }); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal(gBrowser.selectedTab, previousTab); + BrowserTestUtils.removeTab(currentTab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_separatePrivateDefault.js b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault.js new file mode 100644 index 0000000000..8cdc0e746b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault.js @@ -0,0 +1,223 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests the 'Search in a Private Window' result of the address bar. +// Tests here don't have a different private engine, for that see +// browser_separatePrivateDefault_differentPrivateEngine.js + +const serverInfo = { + scheme: "http", + host: "localhost", + port: 20709, // Must be identical to what is in searchSuggestionEngine2.xml +}; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault.urlbarResult.enabled", true], + ["browser.search.separatePrivateDefault", true], + ["browser.urlbar.suggest.searches", true], + ], + }); + + // Add some history for the empty panel. + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/", + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }, + ]); + + // Add a search suggestion engine and move it to the front so that it appears + // as the first one-off. + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + setAsDefault: true, + setAsDefaultPrivate: true, + }); + + // Add another engine in the first one-off position. + let engine2 = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "POSTSearchEngine.xml", + }); + await Services.search.moveEngine(engine2, 0); + + // Add an engine with an alias. + await SearchTestUtils.installSearchExtension({ + name: "MozSearch", + keyword: "alias", + }); + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +async function AssertNoPrivateResult(win) { + let count = await UrlbarTestUtils.getResultCount(win); + Assert.ok(count > 0, "Sanity check result count"); + for (let i = 0; i < count; ++i) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(win, i); + Assert.ok( + result.type != UrlbarUtils.RESULT_TYPE.SEARCH || + !result.searchParams.inPrivateWindow, + "Check this result is not a 'Search in a Private Window' one" + ); + } +} + +async function AssertPrivateResult(win, engine, isPrivateEngine) { + let count = await UrlbarTestUtils.getResultCount(win); + Assert.ok(count > 1, "Sanity check result count"); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Check result type" + ); + Assert.ok(result.searchParams.inPrivateWindow, "Check inPrivateWindow"); + Assert.equal( + result.searchParams.isPrivateEngine, + isPrivateEngine, + "Check isPrivateEngine" + ); + Assert.equal( + result.searchParams.engine, + engine.name, + "Check the search engine" + ); + return result; +} + +add_task(async function test_nonsearch() { + info( + "Test that 'Search in a Private Window' does not appear with non-search results" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exa", + }); + await AssertNoPrivateResult(window); +}); + +add_task(async function test_search() { + info( + "Test that 'Search in a Private Window' appears with only search results" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "unique198273982173", + }); + await AssertPrivateResult(window, await Services.search.getDefault(), false); +}); + +add_task(async function test_search_urlbar_result_disabled() { + info("Test that 'Search in a Private Window' does not appear when disabled"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.urlbarResult.enabled", false], + ], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "unique198273982173", + }); + await AssertNoPrivateResult(window); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_search_disabled_suggestions() { + info( + "Test that 'Search in a Private Window' appears if suggestions are disabled" + ); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", false]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "unique198273982173", + }); + await AssertPrivateResult(window, await Services.search.getDefault(), false); + await SpecialPowers.popPrefEnv(); +}); + +// TODO: (Bug 1658620) Write a new subtest for this behaviour with the update2 +// pref on. +// add_task(async function test_oneoff_selected_keyboard() { +// info( +// "Test that 'Search in a Private Window' with keyboard opens the selected one-off engine if there's no private engine" +// ); +// await SpecialPowers.pushPrefEnv({ +// set: [ +// ["browser.urlbar.update2", false], +// ["browser.urlbar.update2.oneOffsRefresh", false], +// ], +// }); +// await UrlbarTestUtils.promiseAutocompleteResultPopup({ +// window, +// value: "unique198273982173", +// }); +// await AssertPrivateResult(window, await Services.search.getDefault(), false); +// // Select the 'Search in a Private Window' result, alt down to select the +// // first one-off button, Enter. It should open a pb window, but using the +// // selected one-off engine. +// let promiseWindow = BrowserTestUtils.waitForNewWindow({ +// url: +// "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/print_postdata.sjs", +// }); +// // Select the private result. +// EventUtils.synthesizeKey("KEY_ArrowDown"); +// // Select the first one-off button. +// EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); +// EventUtils.synthesizeKey("VK_RETURN"); +// let win = await promiseWindow; +// Assert.ok( +// PrivateBrowsingUtils.isWindowPrivate(win), +// "Should open a private window" +// ); +// await BrowserTestUtils.closeWindow(win); +// await SpecialPowers.popPrefEnv(); +// }); + +// TODO: (Bug 1658620) Write a new subtest for this behaviour with the update2 +// pref on. +// add_task(async function test_oneoff_selected_mouse() { +// info( +// "Test that 'Search in a Private Window' with mouse opens the selected one-off engine if there's no private engine" +// ); +// await SpecialPowers.pushPrefEnv({ +// set: [ +// ["browser.urlbar.update2", false], +// ["browser.urlbar.update2.oneOffsRefresh", false], +// ], +// }); +// await UrlbarTestUtils.promiseAutocompleteResultPopup({ +// window, +// value: "unique198273982173", +// }); +// await AssertPrivateResult(window, await Services.search.getDefault(), false); +// // Select the 'Search in a Private Window' result, alt down to select the +// // first one-off button, Enter. It should open a pb window, but using the +// // selected one-off engine. +// let promiseWindow = BrowserTestUtils.waitForNewWindow({ +// url: +// "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/print_postdata.sjs", +// }); +// // Select the private result. +// EventUtils.synthesizeKey("KEY_ArrowDown"); +// // Select the first one-off button. +// EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); +// // Click on the result. +// let element = UrlbarTestUtils.getSelectedRow(window); +// EventUtils.synthesizeMouseAtCenter(element, {}); +// let win = await promiseWindow; +// Assert.ok( +// PrivateBrowsingUtils.isWindowPrivate(win), +// "Should open a private window" +// ); +// await BrowserTestUtils.closeWindow(win); +// await SpecialPowers.popPrefEnv(); +// }); diff --git a/browser/components/urlbar/tests/browser/browser_separatePrivateDefault_differentEngine.js b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault_differentEngine.js new file mode 100644 index 0000000000..58a60d68a9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault_differentEngine.js @@ -0,0 +1,354 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests the 'Search in a Private Window' result of the address bar. + +const serverInfo = { + scheme: "http", + host: "localhost", + port: 20709, // Must be identical to what is in searchSuggestionEngine2.xml +}; + +let gAliasEngine; +let gPrivateEngine; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault.urlbarResult.enabled", true], + ["browser.search.separatePrivateDefault", true], + ["browser.urlbar.suggest.searches", true], + ], + }); + + // Add some history for the empty panel. + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/", + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }, + ]); + + // Add a search suggestion engine and move it to the front so that it appears + // as the first one-off. + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml", + setAsDefault: true, + }); + gPrivateEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "searchSuggestionEngine2.xml", + setAsDefaultPrivate: true, + }); + + // Add another engine in the first one-off position. + let engine2 = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "POSTSearchEngine.xml", + }); + await Services.search.moveEngine(engine2, 0); + + // Add an engine with an alias. + await SearchTestUtils.installSearchExtension({ + name: "MozSearch", + keyword: "alias", + }); + gAliasEngine = Services.search.getEngineByName("MozSearch"); + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +async function AssertNoPrivateResult(win) { + let count = await UrlbarTestUtils.getResultCount(win); + for (let i = 0; i < count; ++i) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(win, i); + Assert.ok( + result.type != UrlbarUtils.RESULT_TYPE.SEARCH || + !result.searchParams.inPrivateWindow, + "Check this result is not a 'Search in a Private Window' one" + ); + } +} + +async function AssertPrivateResult(win, engine, isPrivateEngine) { + let count = await UrlbarTestUtils.getResultCount(win); + Assert.ok(count > 1, "Sanity check result count"); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Check result type" + ); + Assert.ok(result.searchParams.inPrivateWindow, "Check inPrivateWindow"); + Assert.equal( + result.searchParams.isPrivateEngine, + isPrivateEngine, + "Check isPrivateEngine" + ); + Assert.equal( + result.searchParams.engine, + engine.name, + "Check the search engine" + ); + return result; +} + +// Tests from here on have a different default private engine. + +add_task(async function test_search_private_engine() { + info( + "Test that 'Search in a Private Window' reports a separate private engine" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "unique198273982173", + }); + await AssertPrivateResult(window, gPrivateEngine, true); +}); + +add_task(async function test_privateWindow() { + info( + "Test that 'Search in a Private Window' does not appear in a private window" + ); + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: privateWin, + value: "unique198273982173", + }); + await AssertNoPrivateResult(privateWin); + await BrowserTestUtils.closeWindow(privateWin); +}); + +add_task(async function test_permanentPB() { + info( + "Test that 'Search in a Private Window' does not appear in Permanent Private Browsing" + ); + await SpecialPowers.pushPrefEnv({ + set: [["browser.privatebrowsing.autostart", true]], + }); + let win = await BrowserTestUtils.openNewBrowserWindow(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "unique198273982173", + }); + await AssertNoPrivateResult(win); + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_openPBWindow() { + info( + "Test that 'Search in a Private Window' opens the search in a new Private Window" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "unique198273982173", + }); + await AssertPrivateResult( + window, + await Services.search.getDefaultPrivate(), + true + ); + + await withHttpServer(serverInfo, async () => { + let promiseWindow = BrowserTestUtils.waitForNewWindow({ + url: "http://localhost:20709/?terms=unique198273982173", + maybeErrorPage: true, + }); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("VK_RETURN"); + let win = await promiseWindow; + Assert.ok( + PrivateBrowsingUtils.isWindowPrivate(win), + "Should open a private window" + ); + await BrowserTestUtils.closeWindow(win); + }); +}); + +// TODO: (Bug 1658620) Write a new subtest for this behaviour with the update2 +// pref on. +// add_task(async function test_oneoff_selected_with_private_engine_mouse() { +// info( +// "Test that 'Search in a Private Window' opens the private engine even if a one-off is selected" +// ); +// await SpecialPowers.pushPrefEnv({ +// set: [ +// ["browser.urlbar.update2", false], +// ["browser.urlbar.update2.oneOffsRefresh", false], +// ], +// }); +// await UrlbarTestUtils.promiseAutocompleteResultPopup({ +// window, +// value: "unique198273982173", +// }); +// await AssertPrivateResult( +// window, +// await Services.search.getDefaultPrivate(), +// true +// ); + +// await withHttpServer(serverInfo, async () => { +// // Select the 'Search in a Private Window' result, alt down to select the +// // first one-off button, Click on the result. It should open a pb window using +// // the private search engine, because it has been set. +// let promiseWindow = BrowserTestUtils.waitForNewWindow({ +// url: "http://localhost:20709/?terms=unique198273982173", +// maybeErrorPage: true, +// }); +// // Select the private result. +// EventUtils.synthesizeKey("KEY_ArrowDown"); +// // Select the first one-off button. +// EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); +// // Click on the result. +// let element = UrlbarTestUtils.getSelectedRow(window); +// EventUtils.synthesizeMouseAtCenter(element, {}); +// let win = await promiseWindow; +// Assert.ok( +// PrivateBrowsingUtils.isWindowPrivate(win), +// "Should open a private window" +// ); +// await BrowserTestUtils.closeWindow(win); +// }); +// await SpecialPowers.popPrefEnv(); +// }); + +// TODO: (Bug 1658620) Write a new subtest for this behaviour with the update2 +// pref on. +// add_task(async function test_oneoff_selected_with_private_engine_keyboard() { +// info( +// "Test that 'Search in a Private Window' opens the private engine even if a one-off is selected" +// ); +// await SpecialPowers.pushPrefEnv({ +// set: [ +// ["browser.urlbar.update2", false], +// ["browser.urlbar.update2.oneOffsRefresh", false], +// ], +// }); +// await UrlbarTestUtils.promiseAutocompleteResultPopup({ +// window, +// value: "unique198273982173", +// }); +// await AssertPrivateResult( +// window, +// await Services.search.getDefaultPrivate(), +// true +// ); + +// await withHttpServer(serverInfo, async () => { +// // Select the 'Search in a Private Window' result, alt down to select the +// // first one-off button, Enter. It should open a pb window, but using the +// // selected one-off engine. +// let promiseWindow = BrowserTestUtils.waitForNewWindow({ +// url: "http://localhost:20709/?terms=unique198273982173", +// maybeErrorPage: true, +// }); +// // Select the private result. +// EventUtils.synthesizeKey("KEY_ArrowDown"); +// // Select the first one-off button. +// EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); +// EventUtils.synthesizeKey("VK_RETURN"); +// let win = await promiseWindow; +// Assert.ok( +// PrivateBrowsingUtils.isWindowPrivate(win), +// "Should open a private window" +// ); +// await BrowserTestUtils.closeWindow(win); +// }); +// await SpecialPowers.popPrefEnv(); +// }); + +add_task(async function test_alias_no_query() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.update2.emptySearchBehavior", 2]], + }); + info( + "Test that 'Search in a Private Window' doesn't appear if an alias is typed with no query" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "alias ", + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: gAliasEngine.name, + entry: "typed", + }); + await AssertNoPrivateResult(window); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_alias_query() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.update2.emptySearchBehavior", 2]], + }); + info( + "Test that 'Search in a Private Window' appears when an alias is typed with a query" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "alias something", + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: "MozSearch", + entry: "typed", + }); + await AssertPrivateResult(window, gAliasEngine, true); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_restrict() { + info( + "Test that 'Search in a Private Window' doesn's appear for just the restriction token" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: UrlbarTokenizer.RESTRICT.SEARCH, + }); + await AssertNoPrivateResult(window); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: UrlbarTokenizer.RESTRICT.SEARCH + " ", + }); + await AssertNoPrivateResult(window); + await UrlbarTestUtils.exitSearchMode(window); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: " " + UrlbarTokenizer.RESTRICT.SEARCH, + }); + await AssertNoPrivateResult(window); +}); + +add_task(async function test_restrict_search() { + info( + "Test that 'Search in a Private Window' has the right string with the restriction token" + ); + let engine = await Services.search.getDefaultPrivate(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: UrlbarTokenizer.RESTRICT.SEARCH + "test", + }); + let result = await AssertPrivateResult(window, engine, true); + Assert.equal(result.searchParams.query, "test"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test" + UrlbarTokenizer.RESTRICT.SEARCH, + }); + result = await AssertPrivateResult(window, engine, true); + Assert.equal(result.searchParams.query, "test"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_shortcuts_add_search_engine.js b/browser/components/urlbar/tests/browser/browser_shortcuts_add_search_engine.js new file mode 100644 index 0000000000..92eebf1997 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_shortcuts_add_search_engine.js @@ -0,0 +1,243 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test adding engines through search shortcut buttons. +// A more complete coverage of the detection of engines is available in +// browser_add_search_engine.js + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); +const BASE_URL = + "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", false]], + }); + // Ensure initial state. + UrlbarTestUtils.getOneOffSearchButtons(window).invalidateCache(); +}); + +add_task(async function shortcuts_none() { + info("Checks the shortcuts with a page that doesn't offer any engines."); + let url = "http://mochi.test:8888/"; + await BrowserTestUtils.withNewTab(url, async () => { + let shortcutButtons = UrlbarTestUtils.getOneOffSearchButtons(window); + let rebuildPromise = BrowserTestUtils.waitForEvent( + shortcutButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await rebuildPromise; + + Assert.ok( + !Array.from(shortcutButtons.buttons.children).some(b => + b.classList.contains("searchbar-engine-one-off-add-engine") + ), + "Check there's no buttons to add engines" + ); + }); +}); + +add_task(async function test_shortcuts() { + await do_test_shortcuts(button => { + info("Click on button"); + EventUtils.synthesizeMouseAtCenter(button, {}); + }); + await do_test_shortcuts(button => { + info("Enter on button"); + let shortcuts = UrlbarTestUtils.getOneOffSearchButtons(window); + while (shortcuts.selectedButton != button) { + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + } + EventUtils.synthesizeKey("KEY_Enter"); + }); +}); + +/** + * Test add engine shortcuts. + * + * @param {Function} activateTask a function receiveing the shortcut button to + * activate as argument. The scope of this function is to activate the + * shortcut button. + */ +async function do_test_shortcuts(activateTask) { + info("Checks the shortcuts with a page that offers two engines."); + let url = getRootDirectory(gTestPath) + "add_search_engine_two.html"; + await BrowserTestUtils.withNewTab(url, async () => { + let shortcutButtons = UrlbarTestUtils.getOneOffSearchButtons(window); + let rebuildPromise = BrowserTestUtils.waitForEvent( + shortcutButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await rebuildPromise; + + let addEngineButtons = Array.from(shortcutButtons.buttons.children).filter( + b => b.classList.contains("searchbar-engine-one-off-add-engine") + ); + Assert.equal( + addEngineButtons.length, + 2, + "Check there's two buttons to add engines" + ); + + for (let button of addEngineButtons) { + Assert.ok(BrowserTestUtils.isVisible(button)); + Assert.ok(button.hasAttribute("image")); + await document.l10n.translateElements([button]); + Assert.ok( + button.getAttribute("tooltiptext").includes("add_search_engine_") + ); + Assert.ok( + button.getAttribute("engine-name").startsWith("add_search_engine_") + ); + Assert.ok( + button.classList.contains("searchbar-engine-one-off-add-engine") + ); + } + + info("Activate the first button"); + rebuildPromise = BrowserTestUtils.waitForEvent(shortcutButtons, "rebuild"); + let enginePromise = promiseEngine("engine-added", "add_search_engine_0"); + await activateTask(addEngineButtons[0]); + info("await engine install"); + let engine = await enginePromise; + info("await rebuild"); + await rebuildPromise; + + Assert.ok( + UrlbarTestUtils.isPopupOpen(window), + "Urlbar view is still open." + ); + + addEngineButtons = Array.from(shortcutButtons.buttons.children).filter(b => + b.classList.contains("searchbar-engine-one-off-add-engine") + ); + Assert.equal( + addEngineButtons.length, + 1, + "Check there's one button to add engines" + ); + Assert.equal( + addEngineButtons[0].getAttribute("engine-name"), + "add_search_engine_1" + ); + let installedEngineButton = addEngineButtons[0].previousElementSibling; + Assert.equal(installedEngineButton.engine.name, "add_search_engine_0"); + + info("Remove the added engine"); + rebuildPromise = BrowserTestUtils.waitForEvent(shortcutButtons, "rebuild"); + await Services.search.removeEngine(engine); + await rebuildPromise; + Assert.equal( + Array.from(shortcutButtons.buttons.children).filter(b => + b.classList.contains("searchbar-engine-one-off-add-engine") + ).length, + 2, + "Check there's two buttons to add engines" + ); + await UrlbarTestUtils.promisePopupClose(window); + + info("Switch to a new tab and check the buttons are not persisted"); + await BrowserTestUtils.withNewTab("about:robots", async () => { + rebuildPromise = BrowserTestUtils.waitForEvent( + shortcutButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await rebuildPromise; + Assert.ok( + !Array.from(shortcutButtons.buttons.children).some(b => + b.classList.contains("searchbar-engine-one-off-add-engine") + ), + "Check there's no option to add engines" + ); + }); + }); +} + +add_task(async function shortcuts_many() { + info("Checks the shortcuts with a page that offers many engines."); + let url = getRootDirectory(gTestPath) + "add_search_engine_many.html"; + await BrowserTestUtils.withNewTab(url, async () => { + let shortcutButtons = UrlbarTestUtils.getOneOffSearchButtons(window); + let rebuildPromise = BrowserTestUtils.waitForEvent( + shortcutButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await rebuildPromise; + + let addEngineButtons = Array.from(shortcutButtons.buttons.children).filter( + b => b.classList.contains("searchbar-engine-one-off-add-engine") + ); + Assert.equal( + addEngineButtons.length, + gURLBar.addSearchEngineHelper.maxInlineEngines, + "Check there's a maximum of `maxInlineEngines` buttons to add engines" + ); + }); +}); + +function promiseEngine(expectedData, expectedEngineName) { + info(`Waiting for engine ${expectedData}`); + return TestUtils.topicObserved( + "browser-search-engine-modified", + (engine, data) => { + info(`Got engine ${engine.wrappedJSObject.name} ${data}`); + return ( + expectedData == data && + expectedEngineName == engine.wrappedJSObject.name + ); + } + ).then(([engine, data]) => engine); +} + +add_task(async function shortcuts_without_other_engines() { + info("Checks the shortcuts without other engines."); + + info("Remove search engines except default"); + const defaultEngine = Services.search.defaultEngine; + const engines = await Services.search.getVisibleEngines(); + for (const engine of engines) { + if (defaultEngine.name !== engine.name) { + await Services.search.removeEngine(engine); + } + } + + info("Remove local engines"); + for (const { pref } of UrlbarUtils.LOCAL_SEARCH_MODES) { + await SpecialPowers.pushPrefEnv({ + set: [[`browser.urlbar.${pref}`, false]], + }); + } + + const url = getRootDirectory(gTestPath) + "add_search_engine_many.html"; + await BrowserTestUtils.withNewTab(url, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + + const shortcutButtons = UrlbarTestUtils.getOneOffSearchButtons(window); + Assert.ok(shortcutButtons.container.hidden, "It should be hidden"); + }); + + Services.search.restoreDefaultEngines(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_slow_heuristic.js b/browser/components/urlbar/tests/browser/browser_slow_heuristic.js new file mode 100644 index 0000000000..22b71d87b6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_slow_heuristic.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that slow heuristic results are still waited for on selection. + +"use strict"; + +add_task(async function test_slow_heuristic() { + // Must be between CHUNK_RESULTS_DELAY_MS and DEFERRING_TIMEOUT_MS + let timeout = 150; + Assert.greater(timeout, UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS); + Assert.greater(UrlbarEventBufferer.DEFERRING_TIMEOUT_MS, timeout); + + // First, add a provider that adds a heuristic result on a delay. + let heuristicResult = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "https://example.com/" } + ); + heuristicResult.heuristic = true; + let heuristicProvider = new UrlbarTestUtils.TestProvider({ + results: [heuristicResult], + name: "heuristicProvider", + priority: Infinity, + addTimeout: timeout, + }); + UrlbarProvidersManager.registerProvider(heuristicProvider); + registerCleanupFunction(() => { + UrlbarProvidersManager.unregisterProvider(heuristicProvider); + }); + + // Do a search without waiting for a result. + const win = await BrowserTestUtils.openNewBrowserWindow(); + let promiseLoaded = BrowserTestUtils.browserLoaded( + win.gBrowser.selectedBrowser + ); + + win.gURLBar.focus(); + EventUtils.sendString("test", win); + EventUtils.synthesizeKey("KEY_Enter", {}, win); + await promiseLoaded; + + await UrlbarTestUtils.promisePopupClose(win); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_fast_heuristic() { + let longTimeoutMs = 1000000; + let originalHeuristicTimeout = UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS; + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = longTimeoutMs; + registerCleanupFunction(() => { + UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS = originalHeuristicTimeout; + }); + + // Add a fast heuristic provider. + let heuristicResult = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "https://example.com/" } + ); + heuristicResult.heuristic = true; + let heuristicProvider = new UrlbarTestUtils.TestProvider({ + results: [heuristicResult], + name: "heuristicProvider", + priority: Infinity, + }); + UrlbarProvidersManager.registerProvider(heuristicProvider); + registerCleanupFunction(() => { + UrlbarProvidersManager.unregisterProvider(heuristicProvider); + }); + + // Do a search. + const win = await BrowserTestUtils.openNewBrowserWindow(); + + let startTime = Cu.now(); + Assert.greater( + longTimeoutMs, + Cu.now() - startTime, + "Heuristic result is returned faster than CHUNK_RESULTS_DELAY_MS" + ); + + await UrlbarTestUtils.promisePopupClose(win); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/urlbar/tests/browser/browser_speculative_connect.js b/browser/components/urlbar/tests/browser/browser_speculative_connect.js new file mode 100644 index 0000000000..dc1b4a4c11 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_speculative_connect.js @@ -0,0 +1,199 @@ +/* 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/. */ + +"use strict"; + +// This test ensures that we setup a speculative network connection to +// the site in various cases: +// 1. search engine if it's the first result +// 2. mousedown event before the http request happens(in mouseup). + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine2.xml"; + +const serverInfo = { + scheme: "http", + host: "localhost", + port: 20709, // Must be identical to what is in searchSuggestionEngine2.xml +}; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.autoFill", true], + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.speculativeConnect.enabled", true], + // In mochitest this number is 0 by default but we have to turn it on. + ["network.http.speculative-parallel-limit", 6], + // The http server is using IPv4, so it's better to disable IPv6 to avoid + // weird networking problem. + ["network.dns.disableIPv6", true], + ], + }); + + // Ensure we start from a clean situation. + await PlacesUtils.history.clear(); + + await PlacesTestUtils.addVisits([ + { + uri: `${serverInfo.scheme}://${serverInfo.host}:${serverInfo.port}`, + title: "test visit for speculative connection", + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }, + ]); + + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function search_test() { + // We speculative connect to the search engine only if suggestions are enabled. + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.suggest.enabled", true]], + }); + await withHttpServer(serverInfo, async server => { + let connectionNumber = server.connectionNumber; + info("Searching for 'foo'"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + fireInputEvent: true, + }); + // Check if the first result is with type "searchengine" + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + details.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "The first result is a search" + ); + await UrlbarTestUtils.promiseSpeculativeConnections( + server, + connectionNumber + 1 + ); + }); +}); + +add_task(async function popup_mousedown_test() { + // Disable search suggestions and autofill, to avoid other speculative + // connections. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.suggest.enabled", false], + ["browser.urlbar.autoFill", false], + ], + }); + await withHttpServer(serverInfo, async server => { + let connectionNumber = server.connectionNumber; + let searchString = "ocal"; + let completeValue = `${serverInfo.scheme}://${serverInfo.host}:${serverInfo.port}/`; + info(`Searching for '${searchString}'`); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + let listitem = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + details.url, + completeValue, + "The second item has the url we visited." + ); + + info("Clicking on the second result"); + EventUtils.synthesizeMouseAtCenter(listitem, { type: "mousedown" }, window); + Assert.equal( + UrlbarTestUtils.getSelectedRow(window), + listitem, + "The second item is selected" + ); + await UrlbarTestUtils.promiseSpeculativeConnections( + server, + connectionNumber + 1 + ); + }); +}); + +add_task(async function test_autofill() { + // Disable search suggestions but enable autofill. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.suggest.enabled", false], + ["browser.urlbar.autoFill", true], + ], + }); + await withHttpServer(serverInfo, async server => { + let connectionNumber = server.connectionNumber; + let searchString = serverInfo.host; + info(`Searching for '${searchString}'`); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + let completeValue = `${serverInfo.scheme}://${serverInfo.host}:${serverInfo.port}/`; + Assert.equal(details.url, completeValue, `Autofilled value is as expected`); + await UrlbarTestUtils.promiseSpeculativeConnections( + server, + connectionNumber + 1 + ); + }); +}); + +add_task(async function test_autofill_privateContext() { + info("Autofill in private context."); + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + registerCleanupFunction(async () => { + let promisePBExit = TestUtils.topicObserved("last-pb-context-exited"); + await BrowserTestUtils.closeWindow(privateWin); + await promisePBExit; + }); + await withHttpServer(serverInfo, async server => { + let connectionNumber = server.connectionNumber; + let searchString = serverInfo.host; + info(`Searching for '${searchString}'`); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: privateWin, + value: searchString, + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(privateWin, 0); + let completeValue = `${serverInfo.scheme}://${serverInfo.host}:${serverInfo.port}/`; + Assert.equal(details.url, completeValue, `Autofilled value is as expected`); + await UrlbarTestUtils.promiseSpeculativeConnections( + server, + connectionNumber + ); + }); +}); + +add_task(async function test_no_heuristic_result() { + info("Don't speculative connect on results addition if there's no heuristic"); + await withHttpServer(serverInfo, async server => { + let connectionNumber = server.connectionNumber; + info(`Searching for the empty string`); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + Assert.greater(UrlbarTestUtils.getResultCount(window), 0, "Has results"); + let result = await UrlbarTestUtils.getSelectedRow(window); + Assert.strictEqual(result, null, `Should have no selection`); + await UrlbarTestUtils.promiseSpeculativeConnections( + server, + connectionNumber + ); + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_speculative_connect_not_with_client_cert.js b/browser/components/urlbar/tests/browser/browser_speculative_connect_not_with_client_cert.js new file mode 100644 index 0000000000..62aec6f67a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_speculative_connect_not_with_client_cert.js @@ -0,0 +1,230 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +/* 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/. */ + +"use strict"; + +// Tests that we don't speculatively connect when user certificates are installed + +const { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); + +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); + +const host = "localhost"; +let uri; +let handshakeDone = false; +let expectingChooseCertificate = false; +let chooseCertificateCalled = false; + +const clientAuthDialogService = { + chooseCertificate(hostname, certArray, loadContext, callback) { + ok( + expectingChooseCertificate, + `${ + expectingChooseCertificate ? "" : "not " + }expecting chooseCertificate to be called` + ); + is( + certArray.length, + 1, + "should have only one client certificate available" + ); + ok( + !chooseCertificateCalled, + "chooseCertificate should only be called once" + ); + chooseCertificateCalled = true; + callback.certificateChosen(certArray[0], false); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIClientAuthDialogService"]), +}; + +/** + * A helper class to use with nsITLSServerConnectionInfo.setSecurityObserver. + * Implements nsITLSServerSecurityObserver and simulates an extremely + * rudimentary HTTP server that expects an HTTP/1.1 GET request and responds + * with a 200 OK. + */ +class SecurityObserver { + constructor(input, output) { + this.input = input; + this.output = output; + } + + onHandshakeDone(socket, status) { + info("TLS handshake done"); + handshakeDone = true; + + let output = this.output; + this.input.asyncWait( + { + onInputStreamReady(readyInput) { + try { + let request = NetUtil.readInputStreamToString( + readyInput, + readyInput.available() + ); + ok( + request.startsWith("GET /") && request.includes("HTTP/1.1"), + "expecting an HTTP/1.1 GET request" + ); + let response = + "HTTP/1.1 200 OK\r\nContent-Type:text/plain\r\n" + + "Connection:Close\r\nContent-Length:2\r\n\r\nOK"; + output.write(response, response.length); + } catch (e) { + console.log(e.message); + // This will fail when we close the speculative connection. + } + }, + }, + 0, + 0, + Services.tm.currentThread + ); + } +} + +function startServer(cert) { + let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance( + Ci.nsITLSServerSocket + ); + tlsServer.init(-1, true, -1); + tlsServer.serverCert = cert; + + let securityObservers = []; + + let listener = { + onSocketAccepted(socket, transport) { + info("Accepted TLS client connection"); + let connectionInfo = transport.securityCallbacks.getInterface( + Ci.nsITLSServerConnectionInfo + ); + let input = transport.openInputStream(0, 0, 0); + let output = transport.openOutputStream(0, 0, 0); + connectionInfo.setSecurityObserver(new SecurityObserver(input, output)); + }, + + onStopListening() { + info("onStopListening"); + for (let securityObserver of securityObservers) { + securityObserver.input.close(); + securityObserver.output.close(); + } + }, + }; + + tlsServer.setSessionTickets(false); + tlsServer.setRequestClientCertificate(Ci.nsITLSServerSocket.REQUEST_ALWAYS); + + tlsServer.asyncListen(listener); + + return tlsServer; +} + +let server; + +function getTestServerCertificate() { + const certDB = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + for (const cert of certDB.getCerts()) { + if (cert.commonName == "Mochitest client") { + return cert; + } + } + return null; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.autoFill", true], + // Turn off search suggestion so we won't speculative connect to the search engine. + ["browser.search.suggest.enabled", false], + ["browser.urlbar.speculativeConnect.enabled", true], + // In mochitest this number is 0 by default but we have to turn it on. + ["network.http.speculative-parallel-limit", 6], + // The http server is using IPv4, so it's better to disable IPv6 to avoid weird + // networking problem. + ["network.dns.disableIPv6", true], + ["security.default_personal_cert", "Ask Every Time"], + ], + }); + + let clientAuthDialogServiceCID = MockRegistrar.register( + "@mozilla.org/security/ClientAuthDialogService;1", + clientAuthDialogService + ); + + let cert = getTestServerCertificate(); + server = startServer(cert); + uri = `https://${host}:${server.port}/`; + info(`running tls server at ${uri}`); + await PlacesTestUtils.addVisits([ + { + uri, + title: "test visit for speculative connection", + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }, + ]); + + certOverrideService.rememberValidityOverride( + "localhost", + server.port, + {}, + cert, + true + ); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + MockRegistrar.unregister(clientAuthDialogServiceCID); + certOverrideService.clearValidityOverride("localhost", server.port, {}); + }); +}); + +add_task( + async function popup_mousedown_no_client_cert_dialog_until_navigate_test() { + // To not trigger autofill, search keyword starts from the second character. + let searchString = host.substr(1, 4); + let completeValue = uri; + info(`Searching for '${searchString}'`); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + let listitem = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + info(`The url of the second item is ${details.url}`); + is(details.url, completeValue, "The second item has the url we visited."); + + expectingChooseCertificate = false; + EventUtils.synthesizeMouseAtCenter(listitem, { type: "mousedown" }, window); + is( + UrlbarTestUtils.getSelectedRow(window), + listitem, + "The second item is selected" + ); + + // We shouldn't have triggered a speculative connection, because a client + // certificate is installed. + SimpleTest.requestFlakyTimeout("Wait for UI"); + await new Promise(resolve => setTimeout(resolve, 200)); + + // Now mouseup, expect that we choose a client certificate, and expect that + // we successfully load a page. + expectingChooseCertificate = true; + EventUtils.synthesizeMouseAtCenter(listitem, { type: "mouseup" }, window); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + ok(chooseCertificateCalled, "chooseCertificate must have been called"); + server.close(); + } +); diff --git a/browser/components/urlbar/tests/browser/browser_stop.js b/browser/components/urlbar/tests/browser/browser_stop.js new file mode 100644 index 0000000000..285071a3ff --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_stop.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests ensures the urlbar reflects the correct value if a page load is + * stopped immediately after loading. + */ + +"use strict"; + +const goodURL = "http://mochi.test:8888/"; +const badURL = "http://mochi.test:8888/whatever.html"; + +add_task(async function () { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, goodURL); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + is( + gURLBar.value, + BrowserUIUtils.trimURL(goodURL), + "location bar reflects loaded page" + ); + + await typeAndSubmitAndStop(badURL); + is( + gURLBar.value, + BrowserUIUtils.trimURL(goodURL), + "location bar reflects loaded page after stop()" + ); + gBrowser.removeCurrentTab(); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + is(gURLBar.value, "", "location bar is empty"); + + await typeAndSubmitAndStop(badURL); + is( + gURLBar.value, + BrowserUIUtils.trimURL(badURL), + "location bar reflects stopped page in an empty tab" + ); + gBrowser.removeCurrentTab(); +}); + +async function typeAndSubmitAndStop(url) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: url, + fireInputEvent: true, + }); + + let docLoadPromise = BrowserTestUtils.waitForDocLoadAndStopIt( + url, + gBrowser.selectedBrowser + ); + + // When the load is stopped, tabbrowser calls gURLBar.setURI and then calls + // onStateChange on its progress listeners. So to properly wait until the + // urlbar value has been updated, add our own progress listener here. + let progressPromise = new Promise(resolve => { + let listener = { + onStateChange(browser, webProgress, request, stateFlags, status) { + if ( + webProgress.isTopLevel && + stateFlags & Ci.nsIWebProgressListener.STATE_STOP + ) { + gBrowser.removeTabsProgressListener(listener); + resolve(); + } + }, + }; + gBrowser.addTabsProgressListener(listener); + }); + + gURLBar.handleCommand(); + await Promise.all([docLoadPromise, progressPromise]); +} diff --git a/browser/components/urlbar/tests/browser/browser_stopSearchOnSelection.js b/browser/components/urlbar/tests/browser/browser_stopSearchOnSelection.js new file mode 100644 index 0000000000..0a1ef1b057 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_stopSearchOnSelection.js @@ -0,0 +1,113 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests that when a search is stopped due to the user selecting a result, + * the view doesn't update after that. + */ + +"use strict"; + +const TEST_ENGINE_BASENAME = "searchSuggestionEngineSlow.xml"; + +// This should match the `timeout` query param used in the suggestions URL in +// the test engine. +const TEST_ENGINE_SUGGESTIONS_TIMEOUT = 3000; + +// The number of suggestions returned by the test engine. +const TEST_ENGINE_NUM_EXPECTED_RESULTS = 2; + +add_setup(async function () { + await PlacesUtils.history.clear(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", true]], + }); + // Add a test search engine that returns suggestions on a delay. + let engine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + await Services.search.moveEngine(engine, 0); + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function mainTest() { + // Open a tab that will match the search string below so that we're guaranteed + // to have more than one result (the heuristic result) so that we can change + // the selected result. We open a tab instead of adding a page in history + // because open tabs are kept in a memory SQLite table, so open-tab results + // are more likely than history results to be fetched before our slow search + // suggestions. This is important when the test runs on slow debug builds on + // slow machines. + await BrowserTestUtils.withNewTab("http://example.com/", async () => { + await BrowserTestUtils.withNewTab("about:blank", async () => { + // Do an initial search. There should be 4 results: heuristic, open tab, + // and the two suggestions. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "amp", + }); + await TestUtils.waitForCondition(() => { + return ( + UrlbarTestUtils.getResultCount(window) == + 2 + TEST_ENGINE_NUM_EXPECTED_RESULTS + ); + }); + + // Type a character to start a new search. The new search should still + // match the open tab so that the open-tab result appears again. + EventUtils.synthesizeKey("l"); + + // There should be 2 results immediately: heuristic and open tab. + await TestUtils.waitForCondition(() => { + return UrlbarTestUtils.getResultCount(window) == 2; + }); + + // Before the search completes, change the selected result. Pressing only + // the down arrow key ends up selecting the first one-off on Linux debug + // builds on the infrastructure for some reason, so arrow back up to + // select the heuristic result again. The important thing is to change + // the selection. It doesn't matter which result ends up selected. + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + + // Wait for the new search to complete. It should be canceled due to the + // selection change, but it should still complete. + await UrlbarTestUtils.promiseSearchComplete(window); + + // To make absolutely sure the suggestions don't appear after the search + // completes, wait a bit. + await new Promise(r => + setTimeout(r, 1 + TEST_ENGINE_SUGGESTIONS_TIMEOUT) + ); + + // The heuristic result should reflect the new search, "ampl". + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Should have the correct result type" + ); + Assert.equal( + result.searchParams.query, + "ampl", + "Should have the correct query" + ); + + // None of the other results should be "ampl" suggestions, i.e., amplfoo + // and amplbar should not be in the results. + let count = UrlbarTestUtils.getResultCount(window); + for (let i = 1; i < count; i++) { + result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.ok( + result.type != UrlbarUtils.RESULT_TYPE.SEARCH || + !["amplfoo", "amplbar"].includes(result.searchParams.suggestion), + "Suggestions should not contain the typed l char" + ); + } + }); + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_stop_pending.js b/browser/components/urlbar/tests/browser/browser_stop_pending.js new file mode 100644 index 0000000000..50f5dfdeec --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_stop_pending.js @@ -0,0 +1,459 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +"use strict"; + +const SLOW_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://www.example.com" + ) + "slow-page.sjs"; +const SLOW_PAGE2 = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://mochi.test:8888" + ) + "slow-page.sjs?faster"; + +/** + * Check that if we: + * 1) have a loaded page + * 2) load a separate URL + * 3) before the URL for step 2 has finished loading, load a third URL + * we don't revert to the URL from (1). + */ +add_task(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com", + true, + true + ); + + let initialValue = gURLBar.untrimmedValue; + let expectedURLBarChange = SLOW_PAGE; + let sawChange = false; + let handler = () => { + isnot( + gURLBar.untrimmedValue, + initialValue, + "Should not revert URL bar value!" + ); + if (gURLBar.getAttribute("pageproxystate") == "valid") { + sawChange = true; + is( + gURLBar.untrimmedValue, + expectedURLBarChange, + "Should set expected URL bar value!" + ); + } + }; + + let obs = new MutationObserver(handler); + + obs.observe(gURLBar.textbox, { attributes: true }); + gURLBar.value = SLOW_PAGE; + gURLBar.handleCommand(); + + // If this ever starts going intermittent, we've broken this. + await new Promise(resolve => setTimeout(resolve, 200)); + expectedURLBarChange = SLOW_PAGE2; + let pageLoadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + gURLBar.value = expectedURLBarChange; + gURLBar.handleCommand(); + is( + gURLBar.untrimmedValue, + expectedURLBarChange, + "Should not have changed URL bar value synchronously." + ); + await pageLoadPromise; + ok( + sawChange, + "The URL bar change handler should have been called by the time the page was loaded" + ); + obs.disconnect(); + obs = null; + BrowserTestUtils.removeTab(tab); +}); + +/** + * Check that if we: + * 1) middle-click a link to a separate page whose server doesn't respond + * 2) we switch to that tab and stop the request + * + * The URL bar continues to contain the URL of the page we wanted to visit. + */ +add_task(async function () { + let socket = Cc["@mozilla.org/network/server-socket;1"].createInstance( + Ci.nsIServerSocket + ); + socket.init(-1, true, -1); + const PORT = socket.port; + registerCleanupFunction(() => { + socket.close(); + }); + + const BASE_PAGE = TEST_BASE_URL + "dummy_page.html"; + const SLOW_HOST = `https://localhost:${PORT}/`; + info("Using URLs: " + SLOW_HOST); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_PAGE); + info("opened tab"); + await SpecialPowers.spawn(tab.linkedBrowser, [SLOW_HOST], URL => { + let link = content.document.createElement("a"); + link.href = URL; + link.textContent = "click me to open a slow page"; + link.id = "clickme"; + content.document.body.appendChild(link); + }); + info("added link"); + let newTabPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + // Middle click the link: + await BrowserTestUtils.synthesizeMouseAtCenter( + "#clickme", + { button: 1 }, + tab.linkedBrowser + ); + // get new tab, switch to it + let newTab = (await newTabPromise).target; + await BrowserTestUtils.switchTab(gBrowser, newTab); + is(gURLBar.untrimmedValue, SLOW_HOST, "Should have slow page in URL bar"); + let browserStoppedPromise = BrowserTestUtils.browserStopped( + newTab.linkedBrowser, + null, + true + ); + BrowserStop(); + await browserStoppedPromise; + + is( + gURLBar.untrimmedValue, + SLOW_HOST, + "Should still have slow page in URL bar after stop" + ); + BrowserTestUtils.removeTab(newTab); + BrowserTestUtils.removeTab(tab); +}); +/** + * Check that if we: + * 1) middle-click a link to a separate page whose server doesn't respond + * 2) we alter the URL on that page to some other server that doesn't respond + * 3) we stop the request + * + * The URL bar continues to contain the second URL. + */ +add_task(async function () { + let socket = Cc["@mozilla.org/network/server-socket;1"].createInstance( + Ci.nsIServerSocket + ); + socket.init(-1, true, -1); + const PORT1 = socket.port; + let socket2 = Cc["@mozilla.org/network/server-socket;1"].createInstance( + Ci.nsIServerSocket + ); + socket2.init(-1, true, -1); + const PORT2 = socket2.port; + registerCleanupFunction(() => { + socket.close(); + socket2.close(); + }); + + const BASE_PAGE = TEST_BASE_URL + "dummy_page.html"; + const SLOW_HOST1 = `https://localhost:${PORT1}/`; + const SLOW_HOST2 = `https://localhost:${PORT2}/`; + info("Using URLs: " + SLOW_HOST1 + " and " + SLOW_HOST2); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_PAGE); + info("opened tab"); + await SpecialPowers.spawn(tab.linkedBrowser, [SLOW_HOST1], URL => { + let link = content.document.createElement("a"); + link.href = URL; + link.textContent = "click me to open a slow page"; + link.id = "clickme"; + content.document.body.appendChild(link); + }); + info("added link"); + let newTabPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + // Middle click the link: + await BrowserTestUtils.synthesizeMouseAtCenter( + "#clickme", + { button: 1 }, + tab.linkedBrowser + ); + // get new tab, switch to it + let newTab = (await newTabPromise).target; + await BrowserTestUtils.switchTab(gBrowser, newTab); + is(gURLBar.untrimmedValue, SLOW_HOST1, "Should have slow page in URL bar"); + let browserStoppedPromise = BrowserTestUtils.browserStopped( + newTab.linkedBrowser, + null, + true + ); + gURLBar.value = SLOW_HOST2; + gURLBar.handleCommand(); + await browserStoppedPromise; + + is( + gURLBar.untrimmedValue, + SLOW_HOST2, + "Should have second slow page in URL bar" + ); + browserStoppedPromise = BrowserTestUtils.browserStopped( + newTab.linkedBrowser, + null, + true + ); + BrowserStop(); + await browserStoppedPromise; + + is( + gURLBar.untrimmedValue, + SLOW_HOST2, + "Should still have second slow page in URL bar after stop" + ); + BrowserTestUtils.removeTab(newTab); + BrowserTestUtils.removeTab(tab); +}); + +/** + * 1) Try to load page 0 and wait for it to finish loading. + * 2) Try to load page 1 and wait for it to finish loading. + * 3) Try to load SLOW_PAGE, and then before it finishes loading, navigate back. + * - We should be taken to page 0. + */ +add_task(async function testCorrectUrlBarAfterGoingBackDuringAnotherLoad() { + // Load example.org + let page0 = "http://example.org/"; + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + page0, + true, + true + ); + + // Load example.com in the same browser + let page1 = "http://example.com/"; + let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, page1); + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, page1); + await loaded; + + let initialValue = gURLBar.untrimmedValue; + let expectedURLBarChange = SLOW_PAGE; + let sawChange = false; + let goneBack = false; + let handler = () => { + if (!goneBack) { + isnot( + gURLBar.untrimmedValue, + initialValue, + `Should not revert URL bar value to ${initialValue}` + ); + } + + if (gURLBar.getAttribute("pageproxystate") == "valid") { + sawChange = true; + is( + gURLBar.untrimmedValue, + expectedURLBarChange, + `Should set expected URL bar value - ${expectedURLBarChange}` + ); + } + }; + + let obs = new MutationObserver(handler); + + obs.observe(gURLBar.textbox, { attributes: true }); + // Set the value of url bar to SLOW_PAGE + gURLBar.value = SLOW_PAGE; + gURLBar.handleCommand(); + + // Copied from the first test case: + // If this ever starts going intermittent, we've broken this. + await new Promise(resolve => setTimeout(resolve, 200)); + + expectedURLBarChange = page0; + let pageLoadPromise = BrowserTestUtils.browserStopped( + tab.linkedBrowser, + page0 + ); + + // Wait until we can go back + await TestUtils.waitForCondition(() => tab.linkedBrowser.canGoBack); + ok(tab.linkedBrowser.canGoBack, "can go back"); + + // Navigate back from SLOW_PAGE. We should be taken to page 0 now. + tab.linkedBrowser.goBack(); + goneBack = true; + is( + gURLBar.untrimmedValue, + SLOW_PAGE, + "Should not have changed URL bar value synchronously." + ); + // Wait until page 0 have finished loading. + await pageLoadPromise; + is( + gURLBar.untrimmedValue, + page0, + "Should not have changed URL bar value synchronously." + ); + ok( + sawChange, + "The URL bar change handler should have been called by the time the page was loaded" + ); + obs.disconnect(); + obs = null; + BrowserTestUtils.removeTab(tab); +}); + +/** + * 1) Try to load page 1 and wait for it to finish loading. + * 2) Start loading SLOW_PAGE (it won't finish loading) + * 3) Reload the page. We should have loaded page 1 now. + */ +add_task(async function testCorrectUrlBarAfterReloadingDuringSlowPageLoad() { + // Load page 1 - example.com + let page1 = "http://example.com/"; + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + page1, + true, + true + ); + + let initialValue = gURLBar.untrimmedValue; + let expectedURLBarChange = SLOW_PAGE; + let sawChange = false; + let hasReloaded = false; + let handler = () => { + if (!hasReloaded) { + isnot( + gURLBar.untrimmedValue, + initialValue, + "Should not revert URL bar value!" + ); + } + if (gURLBar.getAttribute("pageproxystate") == "valid") { + sawChange = true; + is( + gURLBar.untrimmedValue, + expectedURLBarChange, + "Should set expected URL bar value!" + ); + } + }; + + let obs = new MutationObserver(handler); + + obs.observe(gURLBar.textbox, { attributes: true }); + // Start loading SLOW_PAGE + gURLBar.value = SLOW_PAGE; + gURLBar.handleCommand(); + + // Copied from the first test: If this ever starts going intermittent, + // we've broken this. + await new Promise(resolve => setTimeout(resolve, 200)); + + expectedURLBarChange = page1; + let pageLoadPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + page1 + ); + // Reload the page + tab.linkedBrowser.reload(); + hasReloaded = true; + is( + gURLBar.untrimmedValue, + SLOW_PAGE, + "Should not have changed URL bar value synchronously." + ); + // Wait for page1 to be loaded due to a reload while the slow page was still loading + await pageLoadPromise; + ok( + sawChange, + "The URL bar change handler should have been called by the time the page was loaded" + ); + obs.disconnect(); + obs = null; + BrowserTestUtils.removeTab(tab); +}); + +/** + * 1) Try to load example.com and wait for it to finish loading. + * 2) Start loading SLOW_PAGE and then stop the load before the load completes + * 3) Check that example.com has been loaded as a result of stopping SLOW_PAGE + * load. + */ +add_task(async function testCorrectUrlBarAfterStoppingTheLoad() { + // Load page 1 + let page1 = "http://example.com/"; + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + page1, + true, + true + ); + + let initialValue = gURLBar.untrimmedValue; + let expectedURLBarChange = SLOW_PAGE; + let sawChange = false; + let hasStopped = false; + let handler = () => { + if (!hasStopped) { + isnot( + gURLBar.untrimmedValue, + initialValue, + "Should not revert URL bar value!" + ); + } + if (gURLBar.getAttribute("pageproxystate") == "valid") { + sawChange = true; + is( + gURLBar.untrimmedValue, + expectedURLBarChange, + "Should set expected URL bar value!" + ); + } + }; + + let obs = new MutationObserver(handler); + + obs.observe(gURLBar.textbox, { attributes: true }); + // Start loading SLOW_PAGE + gURLBar.value = SLOW_PAGE; + gURLBar.handleCommand(); + + // Copied from the first test case: + // If this ever starts going intermittent, we've broken this. + await new Promise(resolve => setTimeout(resolve, 200)); + + // We expect page 1 to be loaded after the SLOW_PAGE load is stopped. + expectedURLBarChange = page1; + let pageLoadPromise = BrowserTestUtils.browserStopped( + tab.linkedBrowser, + SLOW_PAGE, + true + ); + // Stop the SLOW_PAGE load + tab.linkedBrowser.stop(); + hasStopped = true; + is( + gURLBar.untrimmedValue, + SLOW_PAGE, + "Should not have changed URL bar value synchronously." + ); + // Wait for SLOW_PAGE load to stop + await pageLoadPromise; + + ok( + sawChange, + "The URL bar change handler should have been called by the time the page was loaded" + ); + obs.disconnect(); + obs = null; + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_strip_on_share.js b/browser/components/urlbar/tests/browser/browser_strip_on_share.js new file mode 100644 index 0000000000..508106ccdc --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_strip_on_share.js @@ -0,0 +1,197 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let listService; + +// Tests for the strip on share functionality of the urlbar. + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.strip_list", "stripParam"], + ["privacy.query_stripping.enabled", false], + ], + }); + + // Get the list service so we can wait for it to be fully initialized before running tests. + listService = Cc["@mozilla.org/query-stripping-list-service;1"].getService( + Ci.nsIURLQueryStrippingListService + ); + + await listService.testWaitForInit(); +}); + +// Selection is not a valid URI, menu item should be hidden +add_task(async function testInvalidURI() { + await testMenuItemDisabled( + "https://www.example.com/?stripParam=1234", + true, + true + ); +}); + +// Pref is not enabled, menu item should be hidden +add_task(async function testPrefDisabled() { + await testMenuItemDisabled( + "https://www.example.com/?stripParam=1234", + false, + false + ); +}); + +// Menu item should be visible, the whole url is copied without a selection, url should be stripped. +add_task(async function testQueryParamIsStripped() { + let originalUrl = "https://www.example.com/?stripParam=1234"; + let shortenedUrl = "https://www.example.com/"; + await testMenuItemEnabled({ + selectWholeUrl: false, + validUrl: originalUrl, + strippedUrl: shortenedUrl, + useTestList: false, + }); +}); + +// Menu item should be visible, selecting the whole url, url should be stripped. +add_task(async function testQueryParamIsStrippedSelectURL() { + let originalUrl = "https://www.example.com/?stripParam=1234"; + let shortenedUrl = "https://www.example.com/"; + await testMenuItemEnabled({ + selectWholeUrl: true, + validUrl: originalUrl, + strippedUrl: shortenedUrl, + useTestList: false, + }); +}); + +// Menu item should be visible, selecting the whole url, url should be the same. +add_task(async function testURLIsCopiedWithNoParams() { + let originalUrl = "https://www.example.com/"; + let shortenedUrl = "https://www.example.com/"; + await testMenuItemEnabled({ + selectWholeUrl: true, + validUrl: originalUrl, + strippedUrl: shortenedUrl, + useTestList: false, + }); +}); + +// Testing site specific parameter stripping +add_task(async function testQueryParamIsStrippedForSiteSpecific() { + let originalUrl = "https://www.example.com/?test_2=1234"; + let shortenedUrl = "https://www.example.com/"; + await testMenuItemEnabled({ + selectWholeUrl: true, + validUrl: originalUrl, + strippedUrl: shortenedUrl, + useTestList: true, + }); +}); + +// Ensuring site specific parameters are not stripped for other sites +add_task(async function testQueryParamIsNotStrippedForWrongSiteSpecific() { + let originalUrl = "https://www.example.com/?test_3=1234"; + let shortenedUrl = "https://www.example.com/?test_3=1234"; + await testMenuItemEnabled({ + selectWholeUrl: true, + validUrl: originalUrl, + strippedUrl: shortenedUrl, + useTestList: true, + }); +}); + +/** + * Opens a new tab, opens the ulr bar context menu and checks that the strip-on-share menu item is not visible + * + * @param {string} url - The url to be loaded + * @param {boolean} prefEnabled - Whether privacy.query_stripping.strip_on_share.enabled should be enabled for the test + * @param {boolean} selection - True: The whole url will be selected, false: Only part of the url will be selected + */ +async function testMenuItemDisabled(url, prefEnabled, selection) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.strip_on_share.enabled", prefEnabled]], + }); + await BrowserTestUtils.withNewTab(url, async function (browser) { + gURLBar.focus(); + if (selection) { + //select only part of the url + gURLBar.selectionStart = url.indexOf("example"); + gURLBar.selectionEnd = url.indexOf("4"); + } + let menuitem = await promiseContextualMenuitem("strip-on-share"); + Assert.ok( + !BrowserTestUtils.isVisible(menuitem), + "Menu item is not visible" + ); + let hidePromise = BrowserTestUtils.waitForEvent( + menuitem.parentElement, + "popuphidden" + ); + menuitem.parentElement.hidePopup(); + await hidePromise; + }); +} + +/** + * Opens a new tab, opens the url bar context menu and checks that the strip-on-share menu item is visible. + * Checks that the stripped version of the url is copied to the clipboard. + * + * @param {object} options - method options + * @param {boolean} options.selectWholeUrl - Whether the whole url should be selected + * @param {string} options.validUrl - The original url before the stripping occurs + * @param {string} options.strippedUrl - The expected url after stripping occurs + * @param {boolean} options.useTestList - Whether the StripOnShare or Test list should be used + */ +async function testMenuItemEnabled({ + selectWholeUrl, + validUrl, + strippedUrl, + useTestList, +}) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.strip_on_share.enabled", true], + ["privacy.query_stripping.strip_on_share.enableTestMode", useTestList], + ], + }); + + if (useTestList) { + let testJson = { + global: { + queryParams: ["utm_ad"], + topLevelSites: ["*"], + }, + example: { + queryParams: ["test_2", "test_1"], + topLevelSites: ["www.example.com"], + }, + exampleNet: { + queryParams: ["test_3", "test_4"], + topLevelSites: ["www.example.net"], + }, + }; + + await listService.testSetList(testJson); + } + + await BrowserTestUtils.withNewTab(validUrl, async function (browser) { + gURLBar.focus(); + if (selectWholeUrl) { + gURLBar.select(); + } + let menuitem = await promiseContextualMenuitem("strip-on-share"); + Assert.ok(BrowserTestUtils.isVisible(menuitem), "Menu item is visible"); + let hidePromise = BrowserTestUtils.waitForEvent( + menuitem.parentElement, + "popuphidden" + ); + // Make sure the clean copy of the link will be copied to the clipboard + await SimpleTest.promiseClipboardChange(strippedUrl, () => { + menuitem.closest("menupopup").activateItem(menuitem); + }); + await hidePromise; + }); + + await SpecialPowers.popPrefEnv(); +} diff --git a/browser/components/urlbar/tests/browser/browser_strip_on_share_telemetry.js b/browser/components/urlbar/tests/browser/browser_strip_on_share_telemetry.js new file mode 100644 index 0000000000..48a8b6c729 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_strip_on_share_telemetry.js @@ -0,0 +1,98 @@ +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +let listService; + +const STRIP_ON_SHARE_PARAMS_REMOVED = "STRIP_ON_SHARE_PARAMS_REMOVED"; +const STRIP_ON_SHARE_LENGTH_DECREASE = "STRIP_ON_SHARE_LENGTH_DECREASE"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.strip_on_share.enabled", true], + ["privacy.query_stripping.enabled", false], + ], + }); + + // Get the list service so we can wait for it to be fully initialized before running tests. + listService = Cc["@mozilla.org/query-stripping-list-service;1"].getService( + Ci.nsIURLQueryStrippingListService + ); + + await listService.testWaitForInit(); +}); + +// Checking telemetry for single query params being stripped +add_task(async function testSingleQueryParam() { + let originalURI = "https://www.example.com/?utm_source=1"; + let strippedURI = "https://www.example.com/"; + + // Calculating length difference between URLs to check correct telemetry label + let lengthDiff = originalURI.length - strippedURI.length; + + let paramHistogram = TelemetryTestUtils.getAndClearHistogram( + STRIP_ON_SHARE_PARAMS_REMOVED + ); + let lengthHistogram = TelemetryTestUtils.getAndClearHistogram( + STRIP_ON_SHARE_LENGTH_DECREASE + ); + + await testStripOnShare(originalURI, strippedURI); + + // The "1" Label is being checked as 1 Query Param is being stripped + TelemetryTestUtils.assertHistogram(paramHistogram, 1, 1); + TelemetryTestUtils.assertHistogram(lengthHistogram, lengthDiff, 1); + + await testStripOnShare(originalURI, strippedURI); + + TelemetryTestUtils.assertHistogram(paramHistogram, 1, 2); + TelemetryTestUtils.assertHistogram(lengthHistogram, lengthDiff, 2); +}); + +// Checking telemetry for mutliple query params being stripped +add_task(async function testMultiQueryParams() { + let originalURI = "https://www.example.com/?utm_source=1&utm_ad=1&utm_id=1"; + let strippedURI = "https://www.example.com/"; + + // Calculating length difference between URLs to check correct telemetry label + let lengthDiff = originalURI.length - strippedURI.length; + + let paramHistogram = TelemetryTestUtils.getAndClearHistogram( + STRIP_ON_SHARE_PARAMS_REMOVED + ); + let lengthHistogram = TelemetryTestUtils.getAndClearHistogram( + STRIP_ON_SHARE_LENGTH_DECREASE + ); + + await testStripOnShare(originalURI, strippedURI); + + // The "3" Label is being checked as 3 Query Params are being stripped + TelemetryTestUtils.assertHistogram(paramHistogram, 3, 1); + TelemetryTestUtils.assertHistogram(lengthHistogram, lengthDiff, 1); + + await testStripOnShare(originalURI, strippedURI); + + TelemetryTestUtils.assertHistogram(paramHistogram, 3, 2); + TelemetryTestUtils.assertHistogram(lengthHistogram, lengthDiff, 2); +}); + +async function testStripOnShare(validUrl, strippedUrl) { + await BrowserTestUtils.withNewTab(validUrl, async function (browser) { + gURLBar.focus(); + gURLBar.select(); + let menuitem = await promiseContextualMenuitem("strip-on-share"); + Assert.ok(BrowserTestUtils.isVisible(menuitem), "Menu item is visible"); + let hidePromise = BrowserTestUtils.waitForEvent( + menuitem.parentElement, + "popuphidden" + ); + // Make sure the clean copy of the link will be copied to the clipboard + await SimpleTest.promiseClipboardChange(strippedUrl, () => { + menuitem.closest("menupopup").activateItem(menuitem); + }); + await hidePromise; + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_suggestedIndex.js b/browser/components/urlbar/tests/browser/browser_suggestedIndex.js new file mode 100644 index 0000000000..563202036a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_suggestedIndex.js @@ -0,0 +1,120 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that results with a suggestedIndex property end up in the expected +// position. + +add_task(async function suggestedIndex() { + let result1 = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/1" } + ); + result1.suggestedIndex = 2; + let result2 = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/2" } + ); + result2.suggestedIndex = 6; + + let provider = new UrlbarTestUtils.TestProvider({ + results: [result1, result2], + }); + UrlbarProvidersManager.registerProvider(provider); + async function clean() { + UrlbarProvidersManager.unregisterProvider(provider); + await PlacesUtils.history.clear(); + } + registerCleanupFunction(clean); + + let urls = []; + let maxResults = UrlbarPrefs.get("maxRichResults"); + // Add more results, so that the sum of these results plus the above ones, + // will be greater than maxResults. + for (let i = 0; i < maxResults; ++i) { + urls.push("http://example.com/foo" + i); + } + await PlacesTestUtils.addVisits(urls); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + maxResults, + `There should be ${maxResults} results in the view.` + ); + + urls.reverse(); + urls.unshift( + (await Services.search.getDefault()).getSubmission("foo").uri.spec + ); + urls.splice(result1.suggestedIndex, 0, result1.payload.url); + urls.splice(result2.suggestedIndex, 0, result2.payload.url); + urls = urls.slice(0, maxResults); + + let expected = []; + for (let i = 0; i < maxResults; ++i) { + let url = (await UrlbarTestUtils.getDetailsOfResultAt(window, i)).url; + expected.push(url); + } + // Check all the results. + Assert.deepEqual(expected, urls); + + await clean(); + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function suggestedIndex_append() { + // When suggestedIndex is greater than the number of results the result is + // appended. + let result = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/append/" } + ); + result.suggestedIndex = 4; + + let provider = new UrlbarTestUtils.TestProvider({ results: [result] }); + UrlbarProvidersManager.registerProvider(provider); + async function clean() { + UrlbarProvidersManager.unregisterProvider(provider); + await PlacesUtils.history.clear(); + } + registerCleanupFunction(clean); + + await PlacesTestUtils.addVisits("http://example.com/bar"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "bar", + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 3, + `There should be 3 results in the view.` + ); + + let urls = [ + (await Services.search.getDefault()).getSubmission("bar").uri.spec, + "http://example.com/bar", + "http://mozilla.org/append/", + ]; + + let expected = []; + for (let i = 0; i < 3; ++i) { + let url = (await UrlbarTestUtils.getDetailsOfResultAt(window, i)).url; + expected.push(url); + } + // Check all the results. + Assert.deepEqual(expected, urls); + + await clean(); + await UrlbarTestUtils.promisePopupClose(window); +}); diff --git a/browser/components/urlbar/tests/browser/browser_suppressFocusBorder.js b/browser/components/urlbar/tests/browser/browser_suppressFocusBorder.js new file mode 100644 index 0000000000..769c1790a9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_suppressFocusBorder.js @@ -0,0 +1,391 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the suppress-focus-border attribute is applied to the Urlbar + * correctly. Its purpose is to hide the focus border after the panel is closed. + * It also ensures we don't flash the border at the user after they click the + * Urlbar but before we decide we're opening the view. + */ + +let TEST_RESULT = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/" } +); + +/** + * A test provider that awaits a promise before returning results. + */ +class AwaitPromiseProvider extends UrlbarTestUtils.TestProvider { + /** + * @param {object} args + * The constructor arguments for UrlbarTestUtils.TestProvider. + * @param {Promise} promise + * The promise that will be awaited before returning results. + */ + constructor(args, promise) { + super(args); + this._promise = promise; + } + + async startQuery(context, add) { + await this._promise; + for (let result of this.results) { + add(this, result); + } + } +} + +add_setup(async function () { + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + registerCleanupFunction(function () { + SpecialPowers.clipboardCopyString(""); + }); +}); + +add_task(async function afterMousedown_topSites() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + win.gURLBar.blur(); + + await withAwaitProvider( + { results: [TEST_RESULT], priority: Infinity }, + getSuppressFocusPromise(win), + async () => { + Assert.ok( + !win.gURLBar.hasAttribute("suppress-focus-border"), + "Sanity check: the Urlbar does not have the supress-focus-border attribute." + ); + + await UrlbarTestUtils.promisePopupOpen(win, () => { + if (win.gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win); + }); + + let result = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 0); + Assert.ok( + result, + "The provider returned a result after waiting for the suppress-focus-border attribute." + ); + + await UrlbarTestUtils.promisePopupClose(win); + Assert.ok( + !gURLBar.hasAttribute("suppress-focus-border"), + "The Urlbar no longer has the supress-focus-border attribute after close." + ); + } + ); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function openLocation_topSites() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + + await withAwaitProvider( + { results: [TEST_RESULT], priority: Infinity }, + getSuppressFocusPromise(win), + async () => { + Assert.ok( + !win.gURLBar.hasAttribute("suppress-focus-border"), + "Sanity check: the Urlbar does not have the supress-focus-border attribute." + ); + + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeKey("l", { accelKey: true }, win); + }); + + let result = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 0); + Assert.ok( + result, + "The provider returned a result after waiting for the suppress-focus-border attribute." + ); + + await UrlbarTestUtils.promisePopupClose(win); + Assert.ok( + !win.gURLBar.hasAttribute("suppress-focus-border"), + "The Urlbar no longer has the supress-focus-border attribute after close." + ); + } + ); + + await BrowserTestUtils.closeWindow(win); +}); + +// Tests that the address bar loses the suppress-focus-border attribute if no +// results are returned by a query. This simulates the user disabling Top Sites +// then clicking the address bar. +add_task(async function afterMousedown_noTopSites() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + + await withAwaitProvider( + // Note that the provider returns no results. + { results: [], priority: Infinity }, + getSuppressFocusPromise(win), + async () => { + Assert.ok( + !win.gURLBar.hasAttribute("suppress-focus-border"), + "Sanity check: the Urlbar does not have the supress-focus-border attribute." + ); + + EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win); + // Because the panel opening may not be immediate, we must wait a bit. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 500)); + Assert.ok(!UrlbarTestUtils.isPopupOpen(win), "The popup is not open."); + + Assert.ok( + !win.gURLBar.hasAttribute("suppress-focus-border"), + "The Urlbar no longer has the supress-focus-border attribute." + ); + } + ); + + await BrowserTestUtils.closeWindow(win); +}); + +// Tests that we show the focus border when new tabs are opened. +add_task(async function newTab() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + + // Tabs opened with withNewTab don't focus the Urlbar, so we have to open one + // manually. + let tab = await openAboutNewTab(win); + await BrowserTestUtils.waitForCondition( + () => win.gURLBar.hasAttribute("focused"), + "Waiting for the Urlbar to become focused." + ); + Assert.ok( + !win.gURLBar.hasAttribute( + "suppress-focus-border", + "The Urlbar does not have the suppress-focus-border attribute." + ) + ); + + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); +}); + +// Tests that we show the focus border when a new tab is opened and the address +// bar panel is already open. +add_task(async function newTab_alreadyOpen() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + + await withAwaitProvider( + { results: [TEST_RESULT], priority: Infinity }, + getSuppressFocusPromise(win), + async () => { + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeKey("l", { accelKey: true }, win); + }); + + let tab = await openAboutNewTab(win); + await BrowserTestUtils.waitForCondition( + () => !UrlbarTestUtils.isPopupOpen(win), + "Waiting for the Urlbar panel to close." + ); + Assert.ok( + !win.gURLBar.hasAttribute( + "suppress-focus-border", + "The Urlbar does not have the suppress-focus-border attribute." + ) + ); + BrowserTestUtils.removeTab(tab); + } + ); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function searchTip() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + + info("Set a pref to show a search tip button."); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.searchTips.test.ignoreShowLimits", true]], + }); + + info("Open new tab."); + const tab = await openAboutNewTab(win); + + info("Click the tip button."); + const result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + const button = result.element.row._buttons.get("0"); + await UrlbarTestUtils.promisePopupClose(win, () => { + EventUtils.synthesizeMouseAtCenter(button, {}, win); + }); + + Assert.ok( + !win.gURLBar.hasAttribute( + "suppress-focus-border", + "The Urlbar does not have the suppress-focus-border attribute." + ) + ); + + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function interactionOnNewTab() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + + info("Open about:newtab in new tab"); + const tab = await openAboutNewTab(win); + await BrowserTestUtils.waitForCondition( + () => win.gBrowser.selectedTab === tab + ); + + await testInteractionsOnAboutNewTab(win); + + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function interactionOnNewTabInPrivateWindow() { + const win = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + waitForTabURL: "about:privatebrowsing", + }); + await testInteractionsOnAboutNewTab(win); + await BrowserTestUtils.closeWindow(win); + await SimpleTest.promiseFocus(window); +}); + +add_task(async function clickOnEdgeOfURLBar() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + win.gURLBar.blur(); + + Assert.ok( + !win.gURLBar.hasAttribute("suppress-focus-border"), + "URLBar does not have suppress-focus-border attribute" + ); + + const onHiddenFocusRemoved = BrowserTestUtils.waitForCondition( + () => !win.gURLBar._hideFocus + ); + + const container = win.document.getElementById("urlbar-input-container"); + container.click(); + + await onHiddenFocusRemoved; + Assert.ok( + win.gURLBar.hasAttribute("suppress-focus-border"), + "suppress-focus-border is set from the beginning" + ); + + await UrlbarTestUtils.promisePopupClose(win.window); + await BrowserTestUtils.closeWindow(win); +}); + +async function testInteractionsOnAboutNewTab(win) { + info("Test for clicking on URLBar while showing about:newtab"); + await testInteractionFeature(() => { + info("Click on URLBar"); + EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win); + }, win); + + info("Test for typing on .fake-editable while showing about:newtab"); + await testInteractionFeature(() => { + info("Type a character on .fake-editable"); + EventUtils.synthesizeKey("v", {}, win); + }, win); + Assert.equal(win.gURLBar.value, "v", "URLBar value is correct"); + + info("Test for typing on .fake-editable while showing about:newtab"); + await testInteractionFeature(() => { + info("Paste some words on .fake-editable"); + SpecialPowers.clipboardCopyString("paste test"); + win.document.commandDispatcher + .getControllerForCommand("cmd_paste") + .doCommand("cmd_paste"); + SpecialPowers.clipboardCopyString(""); + }, win); + Assert.equal(win.gURLBar.value, "paste test", "URLBar value is correct"); +} + +async function testInteractionFeature(interaction, win) { + info("Focus on URLBar"); + win.gURLBar.value = ""; + win.gURLBar.focus(); + Assert.ok( + !win.gURLBar.hasAttribute("suppress-focus-border"), + "URLBar does not have suppress-focus-border attribute" + ); + + info("Click on search-handoff-button in newtab page"); + await ContentTask.spawn(win.gBrowser.selectedBrowser, null, async () => { + await ContentTaskUtils.waitForCondition(() => + content.document.querySelector(".search-handoff-button") + ); + content.document.querySelector(".search-handoff-button").click(); + }); + + await BrowserTestUtils.waitForCondition( + () => win.gURLBar._hideFocus, + "Wait until _hideFocus will be true" + ); + + const onHiddenFocusRemoved = BrowserTestUtils.waitForCondition( + () => !win.gURLBar._hideFocus + ); + + await interaction(); + + await onHiddenFocusRemoved; + Assert.ok( + win.gURLBar.hasAttribute("suppress-focus-border"), + "suppress-focus-border is set from the beginning" + ); + + const result = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 0); + Assert.ok(result, "The provider returned a result"); + await UrlbarTestUtils.promisePopupClose(win); +} + +function getSuppressFocusPromise(win = window) { + return new Promise(resolve => { + let observer = new MutationObserver(() => { + if ( + win.gURLBar.hasAttribute("suppress-focus-border") && + !UrlbarTestUtils.isPopupOpen(win) + ) { + resolve(); + observer.disconnect(); + } + }); + observer.observe(win.gURLBar.textbox, { + attributes: true, + attributeFilter: ["suppress-focus-border"], + }); + }); +} + +async function withAwaitProvider(args, promise, callback) { + let provider = new AwaitPromiseProvider(args, promise); + UrlbarProvidersManager.registerProvider(provider); + try { + await callback(); + } catch (ex) { + console.error(ex); + } finally { + UrlbarProvidersManager.unregisterProvider(provider); + } +} + +async function openAboutNewTab(win = window) { + // We have to listen for the new tab using this brute force method. + // about:newtab is preloaded in the background. When about:newtab is opened, + // the cached version is shown. Since the page is already loaded, + // waitForNewTab does not detect it. It also doesn't fire the TabOpen event. + const tabCount = win.gBrowser.tabs.length; + EventUtils.synthesizeKey("t", { accelKey: true }, win); + await TestUtils.waitForCondition( + () => win.gBrowser.tabs.length === tabCount + 1, + "Waiting for background about:newtab to open." + ); + return win.gBrowser.tabs[win.gBrowser.tabs.length - 1]; +} diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_closesUrlbarPopup.js b/browser/components/urlbar/tests/browser/browser_switchTab_closesUrlbarPopup.js new file mode 100644 index 0000000000..a9b0eb7b1a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchTab_closesUrlbarPopup.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Checks that switching tabs closes the urlbar popup. + */ + +"use strict"; + +add_task(async function () { + let tab1 = BrowserTestUtils.addTab(gBrowser); + let tab2 = BrowserTestUtils.addTab(gBrowser); + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + }); + + // Add a couple of dummy entries to ensure the history popup will open. + await PlacesTestUtils.addVisits([ + { uri: makeURI("http://example.com/foo") }, + { uri: makeURI("http://example.com/foo/bar") }, + ]); + + // When urlbar in a new tab is focused, and a tab switch occurs, + // the urlbar popup should be closed + await BrowserTestUtils.switchTab(gBrowser, tab2); + gURLBar.focus(); // focus the urlbar in the tab we will switch to + await BrowserTestUtils.switchTab(gBrowser, tab1); + // Now open the popup. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + // Check that the popup closes when we switch tab. + await UrlbarTestUtils.promisePopupClose(window, () => { + return BrowserTestUtils.switchTab(gBrowser, tab2); + }); + Assert.ok(true, "Popup was successfully closed"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_currentTab.js b/browser/components/urlbar/tests/browser/browser_switchTab_currentTab.js new file mode 100644 index 0000000000..eccee800e3 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchTab_currentTab.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test ensures that switch to tab still works when the URI contains an + * encoded part. + */ + +"use strict"; + +add_task(async function test_switchTab_currentTab() { + registerCleanupFunction(PlacesUtils.history.clear); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:robots#1" }, + async () => { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:robots#2" }, + async () => { + let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "robot", + }); + Assert.ok( + context.results.some( + result => + result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH && + result.payload.url == "about:robots#1" + ) + ); + Assert.ok( + !context.results.some( + result => + result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH && + result.payload.url == "about:robots#2" + ) + ); + } + ); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_decodeuri.js b/browser/components/urlbar/tests/browser/browser_switchTab_decodeuri.js new file mode 100644 index 0000000000..fe23eceaf9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchTab_decodeuri.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test ensures that switch to tab still works when the URI contains an + * encoded part. + */ + +"use strict"; + +const TEST_URL = `${TEST_BASE_URL}dummy_page.html#test%7C1`; + +add_task(async function test_switchtab_decodeuri() { + info("Opening first tab"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + info("Opening and selecting second tab"); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + + info("Wait for autocomplete"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "dummy_page", + }); + + info("Select autocomplete popup entry"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + UrlbarTestUtils.getSelectedRowIndex(window) + ); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TAB_SWITCH); + + info("switch-to-tab"); + let tabSelectPromise = BrowserTestUtils.waitForEvent( + window, + "TabSelect", + false + ); + EventUtils.synthesizeKey("KEY_Enter"); + await tabSelectPromise; + + Assert.equal( + gBrowser.selectedTab, + tab, + "Should have switched to the right tab" + ); + + gBrowser.removeCurrentTab(); + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_inputHistory.js b/browser/components/urlbar/tests/browser/browser_switchTab_inputHistory.js new file mode 100644 index 0000000000..0da3161d0e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchTab_inputHistory.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests ensures that the urlbar adaptive behavior updates + * when using switch to tab in the address bar dropdown. + */ + +"use strict"; + +add_setup(async function () { + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function test_adaptive_with_search_term_and_switch_tab() { + await PlacesUtils.history.clear(); + let urls = [ + "https://example.com/", + "https://example.com/#cat", + "https://example.com/#cake", + "https://example.com/#car", + ]; + + info(`Load tabs in same order as urls`); + let tabs = []; + for (let url of urls) { + let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url, false, true); + gBrowser.loadTabs([url], { + inBackground: true, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + + let tab = await tabPromise; + tabs.push(tab); + } + + info(`Switch to tab 0`); + await BrowserTestUtils.switchTab(gBrowser, tabs[0]); + + info("Wait for autocomplete"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ca", + }); + + let result1 = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.notEqual(result1.url, urls[1], `${urls[1]} url should not be first`); + + info(`Scroll down to select the ${urls[1]} entry using keyboard`); + let result2 = await UrlbarTestUtils.getDetailsOfResultAt( + window, + UrlbarTestUtils.getSelectedRowIndex(window) + ); + + while (result2.url != urls[1]) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + result2 = await UrlbarTestUtils.getDetailsOfResultAt( + window, + UrlbarTestUtils.getSelectedRowIndex(window) + ); + } + + Assert.equal( + result2.type, + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + "Selected entry should be tab switch" + ); + Assert.equal(result2.url, urls[1]); + + info("Visiting tab 1"); + EventUtils.synthesizeKey("KEY_Enter"); + Assert.equal(gBrowser.selectedTab, tabs[1], "Should have switched to tab 1"); + + info("Switch back to tab 0"); + await BrowserTestUtils.switchTab(gBrowser, tabs[0]); + + info("Wait for autocomplete"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ca", + }); + + let result3 = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result3.url, urls[1], `${urls[1]} url should be first`); + + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_override.js b/browser/components/urlbar/tests/browser/browser_switchTab_override.js new file mode 100644 index 0000000000..66426a154b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchTab_override.js @@ -0,0 +1,100 @@ +/* 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 test ensures that overriding switch-to-tab correctly loads the page + * rather than switching to it. + */ + +"use strict"; + +const TEST_URL = `${TEST_BASE_URL}dummy_page.html`; + +add_task(async function test_switchtab_override() { + info("Opening first tab"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + info("Opening and selecting second tab"); + let secondTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + registerCleanupFunction(() => { + try { + gBrowser.removeTab(tab); + gBrowser.removeTab(secondTab); + } catch (ex) { + /* tabs may have already been closed in case of failure */ + } + }); + + info("Wait for autocomplete"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "dummy_page", + }); + + info("Select second autocomplete popup entry"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + UrlbarTestUtils.getSelectedRowIndex(window) + ); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TAB_SWITCH); + + // Check to see if the switchtab label is visible and + // all other labels are hidden + const allLabels = document.getElementById("urlbar-label-box").children; + for (let label of allLabels) { + if (label.id == "urlbar-label-switchtab") { + Assert.ok(BrowserTestUtils.isVisible(label)); + } else { + Assert.ok(BrowserTestUtils.isHidden(label)); + } + } + + info("Override switch-to-tab"); + let deferred = Promise.withResolvers(); + // In case of failure this would switch tab. + let onTabSelect = event => { + deferred.reject(new Error("Should have overridden switch to tab")); + }; + gBrowser.tabContainer.addEventListener("TabSelect", onTabSelect); + registerCleanupFunction(() => { + gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelect); + }); + // Otherwise it would load the page. + BrowserTestUtils.browserLoaded(secondTab.linkedBrowser).then( + deferred.resolve + ); + + EventUtils.synthesizeKey("KEY_Shift", { type: "keydown" }); + + // Checks that all labels are hidden when Shift is held down on the SwitchToTab result + for (let label of allLabels) { + Assert.ok(BrowserTestUtils.isHidden(label)); + } + + registerCleanupFunction(() => { + // Avoid confusing next tests by leaving a pending keydown. + EventUtils.synthesizeKey("KEY_Shift", { type: "keyup" }); + }); + + let attribute = "action-override"; + Assert.ok( + gURLBar.view.panel.hasAttribute(attribute), + "We should be overriding" + ); + + EventUtils.synthesizeKey("KEY_Enter"); + info(`gURLBar.value = ${gURLBar.value}`); + await deferred.promise; + + // Blurring the urlbar should have cleared the override. + Assert.ok( + !gURLBar.view.panel.hasAttribute(attribute), + "We should not be overriding anymore" + ); + + await PlacesUtils.history.clear(); + gBrowser.removeTab(tab); + gBrowser.removeTab(secondTab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_switchToTabHavingURI_aOpenParams.js b/browser/components/urlbar/tests/browser/browser_switchToTabHavingURI_aOpenParams.js new file mode 100644 index 0000000000..1a0d2eef70 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchToTabHavingURI_aOpenParams.js @@ -0,0 +1,217 @@ +/* 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/. */ + +add_task(async function test_ignoreFragment() { + let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:home#1" + ); + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla"); + let numTabsAtStart = gBrowser.tabs.length; + + switchTab("about:home#1", true); + switchTab("about:mozilla", true); + + let hashChangePromise = ContentTask.spawn( + tabRefAboutHome.linkedBrowser, + [], + async function () { + await ContentTaskUtils.waitForEvent(this, "hashchange", true); + } + ); + switchTab("about:home#2", true, { + ignoreFragment: "whenComparingAndReplace", + }); + is( + tabRefAboutHome, + gBrowser.selectedTab, + "The same about:home tab should be switched to" + ); + await hashChangePromise; + is(gBrowser.currentURI.ref, "2", "The ref should be updated to the new ref"); + switchTab("about:mozilla", true); + switchTab("about:home#3", true, { ignoreFragment: "whenComparing" }); + is( + tabRefAboutHome, + gBrowser.selectedTab, + "The same about:home tab should be switched to" + ); + is( + gBrowser.currentURI.ref, + "2", + "The ref should be unchanged since the fragment is only ignored when comparing" + ); + switchTab("about:mozilla", true); + switchTab("about:home#1", false); + isnot( + tabRefAboutHome, + gBrowser.selectedTab, + "Selected tab should not be initial about:blank tab" + ); + is( + gBrowser.tabs.length, + numTabsAtStart + 1, + "Should have one new tab opened" + ); + switchTab("about:mozilla", true); + switchTab("about:home", true, { ignoreFragment: "whenComparingAndReplace" }); + await BrowserTestUtils.waitForCondition(function () { + return tabRefAboutHome.linkedBrowser.currentURI.spec == "about:home"; + }); + is( + tabRefAboutHome.linkedBrowser.currentURI.spec, + "about:home", + "about:home shouldn't have hash" + ); + switchTab("about:about", false, { + ignoreFragment: "whenComparingAndReplace", + }); + cleanupTestTabs(); +}); + +add_task(async function test_ignoreQueryString() { + let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:home?hello=firefox" + ); + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla"); + + switchTab("about:home?hello=firefox", true); + switchTab("about:home?hello=firefoxos", false); + // Remove the last opened tab to test ignoreQueryString option. + gBrowser.removeCurrentTab(); + switchTab("about:home?hello=firefoxos", true, { ignoreQueryString: true }); + is( + tabRefAboutHome, + gBrowser.selectedTab, + "Selected tab should be the initial about:home tab" + ); + is( + gBrowser.currentURI.spec, + "about:home?hello=firefox", + "The spec should NOT be updated to the new query string" + ); + cleanupTestTabs(); +}); + +add_task(async function test_replaceQueryString() { + let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:home?hello=firefox" + ); + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla"); + + switchTab("about:home", false); + switchTab("about:home?hello=firefox", true); + switchTab("about:home?hello=firefoxos", false); + // Remove the last opened tab to test replaceQueryString option. + gBrowser.removeCurrentTab(); + switchTab("about:home?hello=firefoxos", true, { replaceQueryString: true }); + is( + tabRefAboutHome, + gBrowser.selectedTab, + "Selected tab should be the initial about:home tab" + ); + // Wait for the tab to load the new URI spec. + await BrowserTestUtils.browserLoaded(tabRefAboutHome.linkedBrowser); + is( + gBrowser.currentURI.spec, + "about:home?hello=firefoxos", + "The spec should be updated to the new spec" + ); + cleanupTestTabs(); +}); + +add_task(async function test_replaceQueryStringAndFragment() { + let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:home?hello=firefox#aaa" + ); + let tabRefAboutMozilla = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla?hello=firefoxos#aaa" + ); + + switchTab("about:home", false); + gBrowser.removeCurrentTab(); + switchTab("about:home?hello=firefox#aaa", true); + is( + tabRefAboutHome, + gBrowser.selectedTab, + "Selected tab should be the initial about:home tab" + ); + switchTab("about:mozilla?hello=firefox#bbb", true, { + replaceQueryString: true, + ignoreFragment: "whenComparingAndReplace", + }); + is( + tabRefAboutMozilla, + gBrowser.selectedTab, + "Selected tab should be the initial about:mozilla tab" + ); + switchTab("about:home?hello=firefoxos#bbb", true, { + ignoreQueryString: true, + ignoreFragment: "whenComparingAndReplace", + }); + is( + tabRefAboutHome, + gBrowser.selectedTab, + "Selected tab should be the initial about:home tab" + ); + cleanupTestTabs(); +}); + +add_task(async function test_ignoreQueryStringIgnoresFragment() { + let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:home?hello=firefox#aaa" + ); + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla?hello=firefoxos#aaa" + ); + + switchTab("about:home?hello=firefox#bbb", false, { ignoreQueryString: true }); + gBrowser.removeCurrentTab(); + switchTab("about:home?hello=firefoxos#aaa", true, { + ignoreQueryString: true, + }); + is( + tabRefAboutHome, + gBrowser.selectedTab, + "Selected tab should be the initial about:home tab" + ); + cleanupTestTabs(); +}); + +// Begin helpers + +function cleanupTestTabs() { + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } +} + +function switchTab(aURI, aShouldFindExistingTab, aOpenParams = {}) { + // Build the description before switchToTabHavingURI deletes the object properties. + let msg = + `Should switch to existing ${aURI} tab if one existed, ` + + `${ + aOpenParams.ignoreFragment ? "ignoring" : "including" + } fragment portion, `; + if (aOpenParams.replaceQueryString) { + msg += "replacing"; + } else if (aOpenParams.ignoreQueryString) { + msg += "ignoring"; + } else { + msg += "including"; + } + msg += " query string."; + aOpenParams.triggeringPrincipal = + Services.scriptSecurityManager.getSystemPrincipal(); + let tabFound = switchToTabHavingURI(aURI, true, aOpenParams); + is(tabFound, aShouldFindExistingTab, msg); +} + +registerCleanupFunction(cleanupTestTabs); diff --git a/browser/components/urlbar/tests/browser/browser_switchToTab_chiclet.js b/browser/components/urlbar/tests/browser/browser_switchToTab_chiclet.js new file mode 100644 index 0000000000..ee887c6796 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchToTab_chiclet.js @@ -0,0 +1,122 @@ +/* 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/. */ + +/** + * Test for chiclet upon switching tab mode. + */ + +"use strict"; + +const TEST_URL = `${TEST_BASE_URL}dummy_page.html`; + +add_task(async function test_with_oneoff_button() { + info("Loading test page into first tab"); + let promiseLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + TEST_URL + ); + BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, TEST_URL); + await promiseLoad; + + info("Opening a new tab"); + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + info("Wait for autocomplete"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + info("Enter Tabs mode"); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.TABS, + }); + + info("Select first popup entry"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "dummy", + }); + EventUtils.synthesizeKey("KEY_ArrowDown"); + const result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + UrlbarTestUtils.getSelectedRowIndex(window) + ); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TAB_SWITCH); + + info("Enter escape key"); + EventUtils.synthesizeKey("KEY_Escape"); + + info("Check label visibility"); + const searchModeTitle = document.getElementById( + "urlbar-search-mode-indicator-title" + ); + const switchTabLabel = document.getElementById("urlbar-label-switchtab"); + await BrowserTestUtils.waitForCondition( + () => + BrowserTestUtils.isVisible(searchModeTitle) && + searchModeTitle.textContent === "Tabs", + "Waiting until the search mode title will be visible" + ); + await BrowserTestUtils.waitForCondition( + () => BrowserTestUtils.isHidden(switchTabLabel), + "Waiting until the switch tab label will be hidden" + ); + + await PlacesUtils.history.clear(); + gBrowser.removeTab(tab); +}); + +add_task(async function test_with_keytype() { + info("Loading test page into first tab"); + let promiseLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + TEST_URL + ); + BrowserTestUtils.startLoadingURIString(gBrowser, TEST_URL); + await promiseLoad; + + info("Opening a new tab"); + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + info("Enter Tabs mode with keytype"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "%", + }); + + info("Select second popup entry"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "dummy", + }); + EventUtils.synthesizeKey("KEY_ArrowDown"); + const result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + UrlbarTestUtils.getSelectedRowIndex(window) + ); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TAB_SWITCH); + + info("Enter escape key"); + EventUtils.synthesizeKey("KEY_Escape"); + + info("Check label visibility"); + const searchModeTitle = document.getElementById( + "urlbar-search-mode-indicator-title" + ); + const switchTabLabel = document.getElementById("urlbar-label-switchtab"); + await BrowserTestUtils.waitForCondition( + () => BrowserTestUtils.isHidden(searchModeTitle), + "Waiting until the search mode title will be hidden" + ); + await BrowserTestUtils.waitForCondition( + () => BrowserTestUtils.isVisible(switchTabLabel), + "Waiting until the switch tab label will be visible" + ); + + await PlacesUtils.history.clear(); + gBrowser.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_switchToTab_closed_tab.js b/browser/components/urlbar/tests/browser/browser_switchToTab_closed_tab.js new file mode 100644 index 0000000000..85b428db61 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchToTab_closed_tab.js @@ -0,0 +1,90 @@ +/* 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 tests that the "switch to tab" result in the urlbar + * will still load the relevant URL if the tab being referred + * to does not exist. + */ + +"use strict"; + +const { UrlbarProviderOpenTabs } = ChromeUtils.importESModule( + "resource:///modules/UrlbarProviderOpenTabs.sys.mjs" +); + +async function openPagesCount() { + let conn = await PlacesUtils.promiseLargeCacheDBConnection(); + let res = await conn.executeCached( + "SELECT COUNT(*) AS count FROM moz_openpages_temp;" + ); + return res[0].getResultByName("count"); +} + +add_task(async function test_switchToTab_tab_closed() { + let testURL = + "https://example.org/browser/browser/components/urlbar/tests/browser/dummy_page.html"; + + // Open a blank tab to start the test from. + let testTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.org" + ); + + // Check how many currently open pages are registered + let pagesCount = await openPagesCount(); + + // Register an open tab that does not exist, this simulates a tab being + // opened but not properly unregistered. + await UrlbarProviderOpenTabs.registerOpenTab( + testURL, + gBrowser.contentPrincipal.userContextId, + false + ); + + Assert.equal( + await openPagesCount(), + pagesCount + 1, + "We registered a new open page" + ); + + let tabOpenPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen", + false + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: testURL, + }); + + // The first result is the heuristic, the second will be the switch to tab. + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + EventUtils.synthesizeMouseAtCenter(element, {}, window); + + await tabOpenPromise; + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + testURL + ); + + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + testURL, + "We opened a new tab with the URL" + ); + + gBrowser.removeTab(gBrowser.selectedTab); + + Assert.equal( + await openPagesCount(), + pagesCount, + "We unregistered the orphaned open tab" + ); + + gBrowser.removeTab(testTab); + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_switchToTab_closes_newtab.js b/browser/components/urlbar/tests/browser/browser_switchToTab_closes_newtab.js new file mode 100644 index 0000000000..5031491d7e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchToTab_closes_newtab.js @@ -0,0 +1,63 @@ +/* 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 tests that switch to tab from a blank tab switches and then closes + * the blank tab. + */ + +"use strict"; + +add_task(async function test_switchToTab_closes() { + let testURL = + "http://example.org/browser/browser/components/urlbar/tests/browser/dummy_page.html"; + + // Open the base tab + let baseTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, testURL); + + if (baseTab.linkedBrowser.currentURI.spec == "about:blank") { + return; + } + + // Open a blank tab to start the test from. + let testTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // Functions for TabClose and TabSelect + let tabClosePromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabClose", + false, + event => { + return event.originalTarget == testTab; + } + ); + let tabSelectPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabSelect", + false, + event => { + return event.originalTarget == baseTab; + } + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "dummy", + }); + + // The first result is the heuristic, the second will be the switch to tab. + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + EventUtils.synthesizeMouseAtCenter(element, {}, window); + + await Promise.all([tabSelectPromise, tabClosePromise]); + + // Confirm that the selected tab is now the base tab + Assert.equal( + gBrowser.selectedTab, + baseTab, + "Should have switched to the correct tab" + ); + + gBrowser.removeTab(baseTab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_switchToTab_fullUrl_repeatedKeydown.js b/browser/components/urlbar/tests/browser/browser_switchToTab_fullUrl_repeatedKeydown.js new file mode 100644 index 0000000000..8f80ac5841 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchToTab_fullUrl_repeatedKeydown.js @@ -0,0 +1,60 @@ +/* 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 tests that typing a url and picking a switch to tab actually switches + * to the right tab. Also tests repeated keydown/keyup events don't confuse + * override. + */ + +"use strict"; + +add_task(async function test_switchToTab_url() { + const TEST_URL = "https://example.org/browser/"; + + let baseTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + let testTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // Functions for TabClose and TabSelect + let tabClosePromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabClose", + false, + event => event.target == testTab + ); + let tabSelectPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabSelect", + false, + event => event.target == baseTab + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_URL, + fireInputEvent: true, + }); + // The first result is the heuristic, the second will be the switch to tab. + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + + // Simulate a long press, on some platforms (Windows) it can generate multiple + // keydown events. + EventUtils.synthesizeKey("VK_SHIFT", { type: "keydown", repeat: 3 }); + EventUtils.synthesizeKey("VK_SHIFT", { type: "keyup" }); + + // Pick the switch to tab result. + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + + await Promise.all([tabSelectPromise, tabClosePromise]); + + // Confirm that the selected tab is now the base tab + Assert.equal( + gBrowser.selectedTab, + baseTab, + "Should have switched to the correct tab" + ); + + gBrowser.removeTab(baseTab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_tabKeyBehavior.js b/browser/components/urlbar/tests/browser/browser_tabKeyBehavior.js new file mode 100644 index 0000000000..32e842d43e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_tabKeyBehavior.js @@ -0,0 +1,378 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test makes sure that the tab key properly adjusts the selection or moves +// through toolbar items, depending on the urlbar state. +// When the view is open, tab should go through results if the urlbar was +// focused with the mouse, or has a typed string. + +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + for (let i = 0; i < UrlbarPrefs.get("maxRichResults"); i++) { + await PlacesTestUtils.addVisits("http://example.com/" + i); + } + + registerCleanupFunction(PlacesUtils.history.clear); + + CustomizableUI.addWidgetToArea("home-button", "nav-bar", 0); + CustomizableUI.addWidgetToArea("sidebar-button", "nav-bar"); + registerCleanupFunction(() => { + CustomizableUI.removeWidgetFromArea("home-button"); + CustomizableUI.removeWidgetFromArea("sidebar-button"); + }); +}); + +add_task(async function tabWithSearchString() { + info("Tab with a search string"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + await expectTabThroughResults(); + info("Reverse Tab with a search string"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + await expectTabThroughResults({ reverse: true }); +}); + +add_task(async function tabNoSearchString() { + info("Tab without a search string"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + await expectTabThroughToolbar(); + info("Reverse Tab without a search string"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + await expectTabThroughToolbar({ reverse: true }); +}); + +add_task(async function tabAfterBlur() { + info("Tab after closing the view"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + await expectTabThroughToolbar(); +}); + +add_task(async function tabNoSearchStringMouseFocus() { + info("Tab in a new blank tab after mouse focus"); + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + await expectTabThroughResults(); + }); + info("Tab in a loaded tab after mouse focus"); + await BrowserTestUtils.withNewTab("example.com", async () => { + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + await expectTabThroughResults(); + }); +}); + +add_task(async function tabNoSearchStringKeyboardFocus() { + info("Tab in a new blank tab after keyboard focus"); + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeKey("l", { accelKey: true }); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + await expectTabThroughToolbar(); + }); + info("Tab in a loaded tab after keyboard focus"); + await BrowserTestUtils.withNewTab("example.com", async () => { + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeKey("l", { accelKey: true }); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + await expectTabThroughToolbar(); + }); +}); + +add_task(async function tabRetainedResultMouseFocus() { + info("Tab after retained results with mouse focus"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + await expectTabThroughResults(); +}); + +add_task(async function tabRetainedResultsKeyboardFocus() { + info("Tab after retained results with keyboard focus"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeKey("l", { accelKey: true }); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + await expectTabThroughResults(); +}); + +add_task(async function tabRetainedResults() { + info("Tab with a search string after mouse focus."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + await expectTabThroughResults(); +}); + +add_task(async function tabSearchModePreview() { + info( + "Tab past a search mode preview keywordoffer after focusing with the keyboard." + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeKey("l", { accelKey: true }); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok( + result.searchParams.keyword, + "The first result is a keyword offer." + ); + + // Sanity check: the Urlbar value is cleared when keywordoffer results are + // selected. + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.ok(!gURLBar.value, "The Urlbar should have no value."); + EventUtils.synthesizeKey("KEY_ArrowUp"); + + await expectTabThroughResults(); + + await UrlbarTestUtils.promisePopupClose(window, async () => { + gURLBar.blur(); + // Verify that blur closes search mode preview. + await UrlbarTestUtils.assertSearchMode(window, null); + }); +}); + +add_task(async function tabTabToSearch() { + info("Tab past a tab-to-search result after focusing with the keyboard."); + await SearchTestUtils.installSearchExtension(); + + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits(["https://example.com/"]); + } + + // Search for a tab-to-search result. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + }); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeKey("l", { accelKey: true }); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + + await expectTabThroughResults(); + + await UrlbarTestUtils.promisePopupClose(window, async () => { + gURLBar.blur(); + await UrlbarTestUtils.assertSearchMode(window, null); + }); + await PlacesUtils.history.clear(); +}); + +add_task(async function tabNoSearchStringSearchMode() { + info( + "Tab through the toolbar when refocusing a Urlbar in search mode with the keyboard." + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + // Enter history search mode to avoid hitting the network. + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeKey("l", { accelKey: true }); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + + await expectTabThroughToolbar(); + + // We have to reopen the view to exit search mode. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function tabOnTopSites() { + info("Tab through the toolbar when focusing the Address Bar on top sites."); + for (let val of [true, false]) { + info(`Test with keyboard_navigation set to "${val}"`); + await SpecialPowers.pushPrefEnv({ + set: [["browser.toolbars.keyboard_navigation", val]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + Assert.ok( + UrlbarTestUtils.getResultCount(window) > 0, + "There should be some results" + ); + Assert.deepEqual( + UrlbarTestUtils.getSelectedElement(window), + null, + "There should be no selection" + ); + + await expectTabThroughToolbar(); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + } +}); + +async function expectTabThroughResults(options = { reverse: false }) { + let resultCount = UrlbarTestUtils.getResultCount(window); + Assert.ok(resultCount > 0, "There should be results"); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + let initiallySelectedIndex = result.heuristic ? 0 : -1; + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + initiallySelectedIndex, + "Check the initial selection." + ); + + for (let i = initiallySelectedIndex + 1; i < resultCount; i++) { + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: options.reverse }); + if ( + UrlbarTestUtils.getButtonForResultIndex( + window, + "menu", + UrlbarTestUtils.getSelectedRowIndex(window) + ) + ) { + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: options.reverse }); + } + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + options.reverse ? resultCount - i : i + ); + } + + EventUtils.synthesizeKey("KEY_Tab"); + + if (!options.reverse) { + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + initiallySelectedIndex, + "Should be back at the initial selection." + ); + } + + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); +} + +async function expectTabThroughToolbar(options = { reverse: false }) { + if (gURLBar.getAttribute("pageproxystate") == "valid") { + Assert.equal(document.activeElement, gURLBar.inputField); + EventUtils.synthesizeKey("KEY_Tab"); + Assert.notEqual(document.activeElement, gURLBar.inputField); + } else { + let focusPromise = waitForFocusOnNextFocusableElement(options.reverse); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: options.reverse }); + await focusPromise; + } + Assert.ok(!gURLBar.view.isOpen, "The urlbar view should be closed."); +} + +async function waitForFocusOnNextFocusableElement(reverse = false) { + if ( + !Services.prefs.getBoolPref("browser.toolbars.keyboard_navigation", true) + ) { + return BrowserTestUtils.waitForCondition( + () => document.activeElement == gBrowser.selectedBrowser + ); + } + let urlbar = document.getElementById("urlbar-container"); + let nextFocusableElement = reverse + ? urlbar.previousElementSibling + : urlbar.nextElementSibling; + while ( + nextFocusableElement && + (!nextFocusableElement.classList.contains("toolbarbutton-1") || + nextFocusableElement.hasAttribute("hidden") || + nextFocusableElement.hasAttribute("disabled") || + BrowserTestUtils.isHidden(nextFocusableElement)) + ) { + nextFocusableElement = reverse + ? nextFocusableElement.previousElementSibling + : nextFocusableElement.nextElementSibling; + } + info( + `Next focusable element: ${nextFocusableElement.localName}.#${nextFocusableElement.id}` + ); + + Assert.ok( + nextFocusableElement.classList.contains("toolbarbutton-1"), + "We should have a reference to the next focusable element after the Urlbar." + ); + + return BrowserTestUtils.waitForCondition( + () => nextFocusableElement.tabIndex == -1 + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js new file mode 100644 index 0000000000..354cd3a802 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js @@ -0,0 +1,224 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim:set ts=2 sw=2 sts=2 et: + * 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/. */ + +"use strict"; + +/** + * Tests for ensuring that the tab switch results correctly match what is + * currently available. + */ + +requestLongerTimeout(2); + +const TEST_URL_BASES = [ + `${TEST_BASE_URL}dummy_page.html#tabmatch`, + `${TEST_BASE_URL}moz.png#tabmatch`, +]; + +const RESTRICT_TOKEN_OPENPAGE = "%"; + +var gTabCounter = 0; + +add_task(async function step_1() { + info("Running step 1"); + let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults"); + let promises = []; + for (let i = 0; i < maxResults - 1; i++) { + let tab = BrowserTestUtils.addTab(gBrowser); + promises.push(loadTab(tab, TEST_URL_BASES[0] + ++gTabCounter)); + } + + await Promise.all(promises); + await ensure_opentabs_match_db(); +}); + +add_task(async function step_2() { + info("Running step 2"); + gBrowser.selectTabAtIndex(1); + gBrowser.removeCurrentTab(); + gBrowser.selectTabAtIndex(1); + gBrowser.removeCurrentTab(); + gBrowser.selectTabAtIndex(0); + + let promises = []; + for (let i = 1; i < gBrowser.tabs.length; i++) { + promises.push(loadTab(gBrowser.tabs[i], TEST_URL_BASES[1] + ++gTabCounter)); + } + + await Promise.all(promises); + await ensure_opentabs_match_db(); +}); + +add_task(async function step_3() { + info("Running step 3"); + let promises = []; + for (let i = 1; i < gBrowser.tabs.length; i++) { + promises.push(loadTab(gBrowser.tabs[i], TEST_URL_BASES[0] + gTabCounter)); + } + + await Promise.all(promises); + await ensure_opentabs_match_db(); +}); + +add_task(async function step_4() { + info("Running step 4 - ensure we don't register subframes as open pages"); + let tab = BrowserTestUtils.addTab( + gBrowser, + 'data:text/html,<body><iframe src=""></iframe></body>' + ); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + let iframe_loaded = ContentTaskUtils.waitForEvent( + content.document, + "load", + true + ); + content.document.querySelector("iframe").src = "http://test2.example.org/"; + await iframe_loaded; + }); + + await ensure_opentabs_match_db(); +}); + +add_task(async function step_5() { + info("Running step 5 - remove tab immediately"); + let tab = BrowserTestUtils.addTab(gBrowser, "about:logo"); + BrowserTestUtils.removeTab(tab); + await ensure_opentabs_match_db(); +}); + +add_task(async function step_6() { + info( + "Running step 6 - check swapBrowsersAndCloseOther preserves registered switch-to-tab result" + ); + let tabToKeep = BrowserTestUtils.addTab(gBrowser); + let tab = BrowserTestUtils.addTab(gBrowser, "about:mozilla"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + gBrowser.updateBrowserRemoteness(tabToKeep.linkedBrowser, { + remoteType: tab.linkedBrowser.isRemoteBrowser + ? E10SUtils.DEFAULT_REMOTE_TYPE + : E10SUtils.NOT_REMOTE, + }); + gBrowser.swapBrowsersAndCloseOther(tabToKeep, tab); + + await ensure_opentabs_match_db(); + + BrowserTestUtils.removeTab(tabToKeep); + + await ensure_opentabs_match_db(); +}); + +add_task(async function step_7() { + info("Running step 7 - close all tabs"); + + Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand"); + + BrowserTestUtils.addTab(gBrowser, "about:blank", { skipAnimation: true }); + while (gBrowser.tabs.length > 1) { + info("Removing tab: " + gBrowser.tabs[0].linkedBrowser.currentURI.spec); + gBrowser.selectTabAtIndex(0); + gBrowser.removeCurrentTab(); + } + + await ensure_opentabs_match_db(); +}); + +add_task(async function cleanup() { + info("Cleaning up"); + + await PlacesUtils.history.clear(); +}); + +function loadTab(tab, url) { + // Because adding visits is async, we will not be notified immediately. + let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + let visited = new Promise(resolve => { + Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + if (url != aSubject.QueryInterface(Ci.nsIURI).spec) { + return; + } + Services.obs.removeObserver(observer, aTopic); + resolve(); + }, "uri-visit-saved"); + }); + + info("Loading page: " + url); + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url); + return Promise.all([loaded, visited]); +} + +function ensure_opentabs_match_db() { + let tabs = {}; + + for (let browserWin of Services.wm.getEnumerator("navigator:browser")) { + // skip closed-but-not-destroyed windows + if (browserWin.closed) { + continue; + } + + for (let i = 0; i < browserWin.gBrowser.tabs.length; i++) { + let browser = browserWin.gBrowser.getBrowserAtIndex(i); + let url = browser.currentURI.spec; + if (browserWin.isBlankPageURL(url)) { + continue; + } + if (!(url in tabs)) { + tabs[url] = 1; + } else { + tabs[url]++; + } + } + } + + return checkAutocompleteResults(tabs); +} + +async function checkAutocompleteResults(expected) { + info("Searching open pages."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: RESTRICT_TOKEN_OPENPAGE, + }); + + let resultCount = UrlbarTestUtils.getResultCount(window); + for (let i = 0; i < resultCount; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (result.heuristic) { + info("Skip heuristic match"); + continue; + } + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + "Should have a tab switch result" + ); + + let url = result.url; + + info(`Search for ${url} in open tabs.`); + let inExpected = url in expected; + Assert.ok( + inExpected, + `${url} was found in autocomplete, was ${ + inExpected ? "" : "not " + } expected` + ); + // Remove the found entry from expected results. + delete expected[url]; + } + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + + // Make sure there is no reported open page that is not open. + for (let entry in expected) { + Assert.ok(!entry, `Should have been found in autocomplete`); + } +} diff --git a/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar_perwindowpb.js b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar_perwindowpb.js new file mode 100644 index 0000000000..9aac30e6b6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar_perwindowpb.js @@ -0,0 +1,174 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test ensures that we don't switch between tabs from normal window to + * private browsing window or opposite. + */ + +const TEST_URL = `${TEST_BASE_URL}dummy_page.html`; + +add_task(async function () { + let normalWindow = await BrowserTestUtils.openNewBrowserWindow(); + let privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await runTest(normalWindow, privateWindow, false); + await BrowserTestUtils.closeWindow(normalWindow); + await BrowserTestUtils.closeWindow(privateWindow); + + normalWindow = await BrowserTestUtils.openNewBrowserWindow(); + privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await runTest(privateWindow, normalWindow, false); + await BrowserTestUtils.closeWindow(normalWindow); + await BrowserTestUtils.closeWindow(privateWindow); + + privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await runTest(privateWindow, privateWindow, true); + await BrowserTestUtils.closeWindow(privateWindow); + + normalWindow = await BrowserTestUtils.openNewBrowserWindow(); + await runTest(normalWindow, normalWindow, true); + await BrowserTestUtils.closeWindow(normalWindow); +}); + +async function runTest(aSourceWindow, aDestWindow, aExpectSwitch) { + BrowserTestUtils.addTab(aSourceWindow.gBrowser, TEST_URL, { + userContextId: 1, + }); + await BrowserTestUtils.openNewForegroundTab(aSourceWindow.gBrowser, TEST_URL); + let testTab = await BrowserTestUtils.openNewForegroundTab( + aDestWindow.gBrowser + ); + + info("waiting for focus on the window"); + await SimpleTest.promiseFocus(aDestWindow); + info("got focus on the window"); + + // Select the testTab + aDestWindow.gBrowser.selectedTab = testTab; + + // Ensure that this tab has no history entries + let sessionHistoryCount = await new Promise(resolve => { + SessionStore.getSessionHistory( + gBrowser.selectedTab, + function (sessionHistory) { + resolve(sessionHistory.entries.length); + } + ); + }); + + Assert.less( + sessionHistoryCount, + 2, + `The test tab has 1 or fewer history entries. sessionHistoryCount=${sessionHistoryCount}` + ); + // Ensure that this tab is on about:blank + is( + testTab.linkedBrowser.currentURI.spec, + "about:blank", + "The test tab is on about:blank" + ); + // Ensure that this tab's document has no child nodes + await SpecialPowers.spawn(testTab.linkedBrowser, [], async function () { + ok( + !content.document.body.hasChildNodes(), + "The test tab has no child nodes" + ); + }); + ok( + !testTab.hasAttribute("busy"), + "The test tab doesn't have the busy attribute" + ); + + // Wait for the Awesomebar popup to appear. + let searchString = TEST_URL; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: aDestWindow, + value: searchString, + }); + + info(`awesomebar popup appeared. aExpectSwitch: ${aExpectSwitch}`); + // Make sure the last match is selected. + while ( + UrlbarTestUtils.getSelectedRowIndex(aDestWindow) < + UrlbarTestUtils.getResultCount(aDestWindow) - 1 + ) { + info("handling key navigation for DOM_VK_DOWN key"); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, aDestWindow); + } + + let awaitTabSwitch; + if (aExpectSwitch) { + awaitTabSwitch = BrowserTestUtils.waitForTabClosing(testTab); + } + + // Execute the selected action. + EventUtils.synthesizeKey("KEY_Enter", {}, aDestWindow); + info("sent Enter command to the controller"); + + if (aExpectSwitch) { + // If we expect a tab switch then the current tab + // will be closed and we switch to the other tab. + await awaitTabSwitch; + } else { + // If we don't expect a tab switch then wait for the tab to load. + await BrowserTestUtils.browserLoaded(testTab.linkedBrowser); + } +} + +// Ensure that if the same page is opened in a non-private and a private window, +// the address bar in the non-private window doesn't show the private tab. +add_task(async function same_url_both_windows() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL); + + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await BrowserTestUtils.openNewForegroundTab(privateWin.gBrowser, TEST_URL); + + // The current tab is not suggested, so open and focus another tab. + await BrowserTestUtils.openNewForegroundTab(win.gBrowser); + + // Check the switch-tab is not shown twice (one per window). + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "dummy_page", + }); + Assert.equal(2, UrlbarTestUtils.getResultCount(win), "Check results count"); + let result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + Assert.ok(result.heuristic, "First result is heuristic"); + result = await UrlbarTestUtils.getDetailsOfResultAt(win, 1); + Assert.equal( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + result.type, + "Second result is tab switch" + ); + + // Now close the non-private tab, and check there's no switch-tab entry in + // the non-private window. + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "dummy_page", + }); + Assert.equal(2, UrlbarTestUtils.getResultCount(win), "Check results count"); + result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + Assert.ok(result.heuristic, "First result is heuristic"); + result = await UrlbarTestUtils.getDetailsOfResultAt(win, 1); + Assert.notEqual( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + result.type, + "Second result is not tab switch" + ); + + await BrowserTestUtils.closeWindow(privateWin); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/urlbar/tests/browser/browser_tabToSearch.js b/browser/components/urlbar/tests/browser/browser_tabToSearch.js new file mode 100644 index 0000000000..a336980583 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_tabToSearch.js @@ -0,0 +1,647 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests tab-to-search results. See also + * browser/components/urlbar/tests/unit/test_providerTabToSearch.js. + */ + +"use strict"; + +const TEST_ENGINE_NAME = "Test"; +const TEST_ENGINE_DOMAIN = "example.com"; + +const DYNAMIC_RESULT_TYPE = "onboardTabToSearch"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderTabToSearch: + "resource:///modules/UrlbarProviderTabToSearch.sys.mjs", +}); + +add_setup(async function () { + await PlacesUtils.history.clear(); + await SpecialPowers.pushPrefEnv({ + set: [ + // Disable onboarding results for general tests. They are enabled in tests + // that specifically address onboarding. + ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0], + ], + }); + + await SearchTestUtils.installSearchExtension({ + name: TEST_ENGINE_NAME, + search_url: `https://${TEST_ENGINE_DOMAIN}/`, + }); + + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits([`https://${TEST_ENGINE_DOMAIN}/`]); + } + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +// Tests that tab-to-search results preview search mode when highlighted. These +// results are worth testing separately since they do not set the +// payload.keyword parameter. +add_task(async function basic() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + fireInputEvent: true, + }); + let autofillResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(autofillResult.autofill); + Assert.equal( + autofillResult.url, + `https://${TEST_ENGINE_DOMAIN}/`, + "The autofilled URL matches the engine domain." + ); + + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + Assert.equal( + tabToSearchResult.payload.engine, + TEST_ENGINE_NAME, + "The tab-to-search result is for the correct engine." + ); + let tabToSearchDetails = await UrlbarTestUtils.getDetailsOfResultAt( + window, + 1 + ); + let [actionTabToSearch] = await document.l10n.formatValues([ + { + id: Services.search.getEngineByName( + tabToSearchDetails.searchParams.engine + ).isGeneralPurposeEngine + ? "urlbar-result-action-tabtosearch-web" + : "urlbar-result-action-tabtosearch-other-engine", + args: { engine: tabToSearchDetails.searchParams.engine }, + }, + ]); + Assert.equal( + tabToSearchDetails.displayed.title, + `Search with ${tabToSearchDetails.searchParams.engine}`, + "The result's title is set correctly." + ); + Assert.equal( + tabToSearchDetails.displayed.action, + actionTabToSearch, + "The correct action text is displayed in the tab-to-search result." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "Sanity check: The second result is selected." + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch", + isPreview: true, + }); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); +}); + +// Tests that we do not set aria-activedescendant after tabbing to a +// tab-to-search result when the pref +// browser.urlbar.accessibility.tabToSearch.announceResults is true. If that +// pref is true, the result was already announced while the user was typing. +add_task(async function activedescendant_tab() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.accessibility.tabToSearch.announceResults", true]], + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + fireInputEvent: true, + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "There should be two results." + ); + let tabToSearchRow = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + 1 + ); + Assert.equal( + tabToSearchRow.result.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + + EventUtils.synthesizeKey("KEY_Tab"); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch", + isPreview: true, + }); + let aadID = gURLBar.inputField.getAttribute("aria-activedescendant"); + Assert.equal(aadID, null, "aria-activedescendant was not set."); + + // Cycle through all the results then return to the tab-to-search result. It + // should be announced. + EventUtils.synthesizeKey("KEY_Tab"); + aadID = gURLBar.inputField.getAttribute("aria-activedescendant"); + let firstRow = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + Assert.equal( + aadID, + firstRow._content.id, + "aria-activedescendant was set to the row after the tab-to-search result." + ); + EventUtils.synthesizeKey("KEY_Tab"); + aadID = gURLBar.inputField.getAttribute("aria-activedescendant"); + Assert.equal( + aadID, + tabToSearchRow._content.id, + "aria-activedescendant was set to the tab-to-search result." + ); + + // Now close and reopen the view, then do another search that yields a + // tab-to-search result. aria-activedescendant should not be set when it is + // selected. + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + fireInputEvent: true, + }); + tabToSearchRow = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + Assert.equal( + tabToSearchRow.result.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + + EventUtils.synthesizeKey("KEY_Tab"); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch", + isPreview: true, + }); + aadID = gURLBar.inputField.getAttribute("aria-activedescendant"); + Assert.equal(aadID, null, "aria-activedescendant was not set."); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + await SpecialPowers.popPrefEnv(); +}); + +// Tests that we set aria-activedescendant after accessing a tab-to-search +// result with the arrow keys. +add_task(async function activedescendant_arrow() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + fireInputEvent: true, + }); + let tabToSearchRow = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + 1 + ); + Assert.equal( + tabToSearchRow.result.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch", + isPreview: true, + }); + let aadID = gURLBar.inputField.getAttribute("aria-activedescendant"); + Assert.equal( + aadID, + tabToSearchRow._content.id, + "aria-activedescendant was set to the tab-to-search result." + ); + + // Move selection away from the tab-to-search result then return. It should + // be announced. + EventUtils.synthesizeKey("KEY_ArrowDown"); + aadID = gURLBar.inputField.getAttribute("aria-activedescendant"); + Assert.equal( + aadID, + UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton.id, + "aria-activedescendant was moved to the first one-off." + ); + EventUtils.synthesizeKey("KEY_ArrowUp"); + aadID = gURLBar.inputField.getAttribute("aria-activedescendant"); + Assert.equal( + aadID, + tabToSearchRow._content.id, + "aria-activedescendant was set to the tab-to-search result." + ); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); +}); + +add_task(async function tab_key_race() { + // Mac Debug tinderboxes are just too slow and fail intermittently + // even if the EventBufferer timeout is set to an high value. + if (AppConstants.platform == "macosx" && AppConstants.DEBUG) { + return; + } + info( + "Test typing a letter followed shortly by down arrow consistently selects a tab-to-search result" + ); + Assert.equal(gURLBar.value, "", "Sanity check urlbar is empty"); + let promiseQueryStarted = new Promise(resolve => { + /** + * A no-op test provider. + * We use this to wait for the query to start, because otherwise TAB will + * move to the next widget since the panel is closed and there's no running + * query. This means waiting for the UrlbarProvidersManager to at least + * evaluate the isActive status of providers. + * In the future we should try to reduce this latency, to defer user events + * even more efficiently. + */ + class ListeningTestProvider extends UrlbarProvider { + constructor() { + super(); + } + get name() { + return "ListeningTestProvider"; + } + get type() { + return UrlbarUtils.PROVIDER_TYPE.PROFILE; + } + isActive(context) { + executeSoon(resolve); + return false; + } + isRestricting(context) { + return false; + } + async startQuery(context, addCallback) { + // Nothing to do. + } + } + let provider = new ListeningTestProvider(); + UrlbarProvidersManager.registerProvider(provider); + registerCleanupFunction(async function () { + UrlbarProvidersManager.unregisterProvider(provider); + }); + }); + gURLBar.focus(); + info("Type the beginning of the search string to get tab-to-search"); + EventUtils.synthesizeKey(TEST_ENGINE_DOMAIN.slice(0, 1)); + info("Awaiting for the query to start"); + await promiseQueryStarted; + EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.promiseSearchComplete(window); + await TestUtils.waitForCondition( + () => UrlbarTestUtils.getSelectedRowIndex(window) == 1, + "Wait for down arrow key to be handled" + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch", + isPreview: true, + }); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); +}); + +// Test that large-style onboarding results appear and have the correct +// properties. +add_task(async function onboard() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]], + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + fireInputEvent: true, + }); + let autofillResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(autofillResult.autofill); + Assert.equal( + autofillResult.url, + `https://${TEST_ENGINE_DOMAIN}/`, + "The autofilled URL matches the engine domain." + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "Sanity check: The second result is selected." + ); + + // Now check the properties of the onboarding result. + let onboardingElement = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + 1 + ); + Assert.equal( + onboardingElement.result.payload.dynamicType, + DYNAMIC_RESULT_TYPE, + "The tab-to-search result is an onboarding result." + ); + Assert.equal( + onboardingElement.result.resultSpan, + 2, + "The correct resultSpan was set." + ); + Assert.ok( + onboardingElement + .querySelector(".urlbarView-row-inner") + .hasAttribute("selected"), + "The onboarding element set the selected attribute." + ); + + let [titleOnboarding, actionOnboarding, descriptionOnboarding] = + await document.l10n.formatValues([ + { + id: "urlbar-result-action-search-w-engine", + args: { + engine: onboardingElement.result.payload.engine, + }, + }, + { + id: Services.search.getEngineByName( + onboardingElement.result.payload.engine + ).isGeneralPurposeEngine + ? "urlbar-result-action-tabtosearch-web" + : "urlbar-result-action-tabtosearch-other-engine", + args: { engine: onboardingElement.result.payload.engine }, + }, + { + id: "urlbar-tabtosearch-onboard", + }, + ]); + let onboardingDetails = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + onboardingDetails.displayed.title, + titleOnboarding, + "The correct title was set." + ); + Assert.equal( + onboardingDetails.displayed.action, + actionOnboarding, + "The correct action text was set." + ); + Assert.equal( + onboardingDetails.element.row.querySelector( + ".urlbarView-dynamic-onboardTabToSearch-description" + ).textContent, + descriptionOnboarding, + "The correct description was set." + ); + Assert.ok( + BrowserTestUtils.isVisible( + onboardingDetails.element.row.querySelector(".urlbarView-title-separator") + ), + "The title separator should be visible." + ); + + // Check that the onboarding result enters search mode. + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch_onboard", + isPreview: true, + }); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch_onboard", + }); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3); + delete UrlbarProviderTabToSearch.onboardingInteractionAtTime; + await SpecialPowers.popPrefEnv(); +}); + +// Tests that we show the onboarding result until the user interacts with it +// `browser.urlbar.tabToSearch.onboard.interactionsLeft` times. +add_task(async function onboard_limit() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]], + }); + + Assert.equal( + UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"), + 3, + "Sanity check: interactionsLeft is 3." + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + fireInputEvent: true, + }); + let onboardingResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + onboardingResult.payload.dynamicType, + DYNAMIC_RESULT_TYPE, + "The second result is an onboarding result." + ); + await EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch_onboard", + isPreview: true, + }); + Assert.equal(UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"), 2); + await UrlbarTestUtils.exitSearchMode(window); + + // We don't increment the counter if we showed the onboarding result less than + // 5 minutes ago. + for (let i = 0; i < 5; i++) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + fireInputEvent: true, + }); + onboardingResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + onboardingResult.payload.dynamicType, + DYNAMIC_RESULT_TYPE, + "The second result is an onboarding result." + ); + await EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch_onboard", + isPreview: true, + }); + Assert.equal( + UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"), + 2, + "We shouldn't decrement interactionsLeft if an onboarding result was just shown." + ); + await UrlbarTestUtils.exitSearchMode(window); + } + + // If the user doesn't interact with the result, we don't increment the + // counter. + for (let i = 0; i < 5; i++) { + delete UrlbarProviderTabToSearch.onboardingInteractionAtTime; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + fireInputEvent: true, + }); + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + Assert.equal( + tabToSearchResult.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "The tab-to-search result is an onboarding result." + ); + Assert.equal( + UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"), + 2, + "We shouldn't decrement interactionsLeft if the user doesn't interact with onboarding." + ); + } + + // Test that we increment the counter if the user interacts with the result + // and it's been 5+ minutes since they last interacted with it. + for (let i = 1; i >= 0; i--) { + delete UrlbarProviderTabToSearch.onboardingInteractionAtTime; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + fireInputEvent: true, + }); + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + Assert.equal( + onboardingResult.payload.dynamicType, + DYNAMIC_RESULT_TYPE, + "The second result is an onboarding result." + ); + await EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch_onboard", + isPreview: true, + }); + Assert.equal( + UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"), + i, + "We decremented interactionsLeft." + ); + await UrlbarTestUtils.exitSearchMode(window); + } + + delete UrlbarProviderTabToSearch.onboardingInteractionAtTime; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + fireInputEvent: true, + }); + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + Assert.notEqual( + tabToSearchResult.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "Now that interactionsLeft is 0, we don't show onboarding results." + ); + + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3); + delete UrlbarProviderTabToSearch.onboardingInteractionAtTime; + await SpecialPowers.popPrefEnv(); +}); + +// Tests that we show at most one onboarding result at a time. See +// tests/unit/test_providerTabToSearch.js:multipleEnginesForHostname for a test +// that ensures only one normal tab-to-search result is shown in this scenario. +add_task(async function onboard_multipleEnginesForHostname() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]], + }); + + let extension = await SearchTestUtils.installSearchExtension( + { + name: `${TEST_ENGINE_NAME}Maps`, + search_url: `https://${TEST_ENGINE_DOMAIN}/maps/`, + }, + { skipUnload: true } + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + fireInputEvent: true, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Only two results are shown." + ); + let firstResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0) + ).result; + Assert.notEqual( + firstResult.providerName, + "TabToSearch", + "The first result is not from TabToSearch." + ); + let secondResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + secondResult.providerName, + "TabToSearch", + "The second result is from TabToSearch." + ); + Assert.equal( + secondResult.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "The tab-to-search result is the only onboarding result." + ); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + await extension.unload(); + UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3); + delete UrlbarProviderTabToSearch.onboardingInteractionAtTime; + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_textruns.js b/browser/components/urlbar/tests/browser/browser_textruns.js new file mode 100644 index 0000000000..ed7a61e6b0 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_textruns.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test ensures that we limit textruns in case of very long urls or titles. + */ + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", true]], + }); + await SearchTestUtils.installSearchExtension( + { name: "Test" }, + { setAsDefault: true } + ); + + let lotsOfSpaces = "%20".repeat(300); + await PlacesTestUtils.addVisits({ + uri: `https://textruns.mozilla.org/${lotsOfSpaces}/test/`, + title: `A long ${lotsOfSpaces} title`, + }); + await UrlbarTestUtils.formHistory.add([ + { value: `A long ${lotsOfSpaces} textruns suggestion` }, + ]); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "textruns", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.searchParams.engine, "Test", "Sanity check engine"); + Assert.equal( + result.displayed.title.length, + UrlbarUtils.MAX_TEXT_LENGTH, + "Result title should be limited" + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal( + result.displayed.title.length, + UrlbarUtils.MAX_TEXT_LENGTH, + "Result title should be limited" + ); + Assert.equal( + result.displayed.url.length, + UrlbarUtils.MAX_TEXT_LENGTH, + "Result url should be limited" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_tokenAlias.js b/browser/components/urlbar/tests/browser/browser_tokenAlias.js new file mode 100644 index 0000000000..d215c2536f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_tokenAlias.js @@ -0,0 +1,861 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test checks "@" search engine aliases ("token aliases") in the urlbar. + +"use strict"; + +const TEST_ALIAS_ENGINE_NAME = "Test"; +const ALIAS = "@test"; +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +// We make sure that aliases and search terms are correctly recognized when they +// are separated by each of these different types of spaces and combinations of +// spaces. U+3000 is the ideographic space in CJK and is commonly used by CJK +// speakers. +const TEST_SPACES = [" ", "\u3000", " \u3000", "\u3000 "]; + +// Allow more time for Mac machines so they don't time out in verify mode. See +// bug 1673062. +if (AppConstants.platform == "macosx") { + requestLongerTimeout(5); +} + +add_setup(async function () { + // Add a default engine with suggestions, to avoid hitting the network when + // fetching them. + let defaultEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + defaultEngine.alias = "@default"; + await SearchTestUtils.installSearchExtension({ + name: TEST_ALIAS_ENGINE_NAME, + keyword: ALIAS, + }); + + // Search results aren't shown in quantumbar unless search suggestions are + // enabled. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", true]], + }); +}); + +// Simple test that tries different variations of an alias, without reverting +// the urlbar value in between. +add_task(async function testNoRevert() { + await doSimpleTest(false); +}); + +// Simple test that tries different variations of an alias, reverting the urlbar +// value in between. +add_task(async function testRevert() { + await doSimpleTest(true); +}); + +async function doSimpleTest(revertBetweenSteps) { + // When autofill is enabled, searching for "@tes" will autofill to "@test", + // which gets in the way of this test task, so temporarily disable it. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", false]], + }); + + // "@tes" -- not an alias, no search mode + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS.substr(0, ALIAS.length - 1), + }); + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.value, + ALIAS.substr(0, ALIAS.length - 1), + "value should be alias substring" + ); + + if (revertBetweenSteps) { + gURLBar.handleRevert(); + gURLBar.blur(); + } + + // "@test" -- alias but no trailing space, no search mode + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS, + }); + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal(gURLBar.value, ALIAS, "value should be alias"); + + if (revertBetweenSteps) { + gURLBar.handleRevert(); + gURLBar.blur(); + } + + // "@test " -- alias with trailing space, search mode + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS + spaces, + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ALIAS_ENGINE_NAME, + entry: "typed", + }); + Assert.equal(gURLBar.value, "", "value should be empty"); + await UrlbarTestUtils.exitSearchMode(window); + + if (revertBetweenSteps) { + gURLBar.handleRevert(); + gURLBar.blur(); + } + } + + // "@test foo" -- alias, search mode + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS + spaces + "foo", + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ALIAS_ENGINE_NAME, + entry: "typed", + }); + Assert.equal(gURLBar.value, "foo", "value should be query"); + await UrlbarTestUtils.exitSearchMode(window); + + if (revertBetweenSteps) { + gURLBar.handleRevert(); + gURLBar.blur(); + } + } + + // "@test " -- alias with trailing space, search mode + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS + spaces, + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ALIAS_ENGINE_NAME, + entry: "typed", + }); + Assert.equal(gURLBar.value, "", "value should be empty"); + await UrlbarTestUtils.exitSearchMode(window); + + if (revertBetweenSteps) { + gURLBar.handleRevert(); + gURLBar.blur(); + } + } + + // "@test" -- alias but no trailing space, no highlight + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS, + }); + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal(gURLBar.value, ALIAS, "value should be alias"); + + if (revertBetweenSteps) { + gURLBar.handleRevert(); + gURLBar.blur(); + } + + // "@tes" -- not an alias, no highlight + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS.substr(0, ALIAS.length - 1), + }); + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.value, + ALIAS.substr(0, ALIAS.length - 1), + "value should be alias substring" + ); + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + + await SpecialPowers.popPrefEnv(); +} + +// An alias should be recognized even when there are spaces before it, and +// search mode should be entered. +add_task(async function spacesBeforeAlias() { + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: spaces + ALIAS + spaces, + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ALIAS_ENGINE_NAME, + entry: "typed", + }); + Assert.equal(gURLBar.value, "", "value should be empty"); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + } +}); + +// An alias in the middle of a string should not be recognized and search mode +// should not be entered. +add_task(async function charsBeforeAlias() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "not an alias " + ALIAS + " ", + }); + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.value, + "not an alias " + ALIAS + " ", + "value should be unchanged" + ); + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// While already in search mode, an alias should not be recognized. +add_task(async function alreadyInSearchMode() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS + " ", + }); + + // Search mode source should still be bookmarks. + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal(gURLBar.value, ALIAS + " ", "value should be unchanged"); + + // Exit search mode, but first remove the value in the input. Since the value + // is "alias ", we'd actually immediately re-enter search mode otherwise. + gURLBar.value = ""; + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Types a space while typing an alias to ensure we stop autofilling. +add_task(async function spaceWhileTypingAlias() { + for (let spaces of TEST_SPACES) { + if (spaces.length != 1) { + continue; + } + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + + let value = ALIAS.substring(0, ALIAS.length - 1); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + selectionStart: value.length, + selectionEnd: value.length, + }); + Assert.equal(gURLBar.value, ALIAS + " ", "Alias should be autofilled"); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey(spaces); + await searchPromise; + + Assert.equal( + gURLBar.value, + value + spaces, + "Alias should not be autofilled" + ); + await UrlbarTestUtils.assertSearchMode(window, null); + + await UrlbarTestUtils.promisePopupClose(window); + } +}); + +// Aliases are case insensitive. Make sure that searching with an alias using a +// weird case still causes the alias to be recognized and search mode entered. +add_task(async function aliasCase() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@TeSt ", + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ALIAS_ENGINE_NAME, + entry: "typed", + }); + Assert.equal(gURLBar.value, "", "value should be empty"); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Same as previous but with a query. +add_task(async function aliasCase_query() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@tEsT query", + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ALIAS_ENGINE_NAME, + entry: "typed", + }); + Assert.equal(gURLBar.value, "query", "value should be query"); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Selecting a non-heuristic (non-first) search engine result with an alias and +// empty query should put the alias in the urlbar and highlight it. +// Also checks that internal aliases appear with the "@" keyword. +add_task(async function nonHeuristicAliases() { + // Get the list of token alias engines (those with aliases that start with + // "@"). + let tokenEngines = []; + for (let engine of await Services.search.getEngines()) { + let aliases = []; + if (engine.alias) { + aliases.push(engine.alias); + } + aliases.push(...engine.aliases); + let tokenAliases = aliases.filter(a => a.startsWith("@")); + if (tokenAliases.length) { + tokenEngines.push({ engine, tokenAliases }); + } + } + if (!tokenEngines.length) { + Assert.ok(true, "No token alias engines, skipping task."); + return; + } + info( + "Got token alias engines: " + tokenEngines.map(({ engine }) => engine.name) + ); + + // Populate the results with the list of token alias engines by searching for + // "@". + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + tokenEngines.length - 1 + ); + // Key down to select each result in turn. The urlbar should preview search + // mode for each engine. + for (let { tokenAliases } of tokenEngines) { + let alias = tokenAliases[0]; + let engineName = (await UrlbarSearchUtils.engineForAlias(alias)).name; + EventUtils.synthesizeKey("KEY_ArrowDown"); + let expectedSearchMode = { + engineName, + entry: "keywordoffer", + isPreview: true, + }; + if (Services.search.getEngineByName(engineName).isGeneralPurposeEngine) { + expectedSearchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH; + } + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + Assert.ok(!gURLBar.value, "The Urlbar should be empty."); + } + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Clicking on an @ alias offer (an @ alias with an empty search string) in the +// view should enter search mode. +add_task(async function clickAndFillAlias() { + // Do a search for "@" to show all the @ aliases. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + + // Find our test engine in the results. It's probably last, but for + // robustness don't assume it is. + let testEngineItem; + for (let i = 0; !testEngineItem; i++) { + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.equal( + details.displayed.title, + `Search with ${details.searchParams.engine}`, + "The result's title is set correctly." + ); + Assert.ok(!details.action, "The result should have no action text."); + if (details.searchParams && details.searchParams.keyword == ALIAS) { + testEngineItem = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + i + ); + } + } + + // Click it. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(testEngineItem, {}); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: testEngineItem.result.payload.engine, + entry: "keywordoffer", + }); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Pressing enter on an @ alias offer (an @ alias with an empty search string) +// in the view should enter search mode. +add_task(async function enterAndFillAlias() { + // Do a search for "@" to show all the @ aliases. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + + // Find our test engine in the results. It's probably last, but for + // robustness don't assume it is. + let details; + let index = 0; + for (; ; index++) { + details = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + if (details.searchParams && details.searchParams.keyword == ALIAS) { + index++; + break; + } + } + + // Key down to it and press enter. + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: index }); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: details.searchParams.engine, + entry: "keywordoffer", + }); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Pressing Enter on an @ alias autofill should enter search mode. +add_task(async function enterAutofillsAlias() { + for (let value of [ALIAS.substring(0, ALIAS.length - 1), ALIAS]) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + selectionStart: value.length, + selectionEnd: value.length, + }); + + // Press Enter. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ALIAS_ENGINE_NAME, + entry: "keywordoffer", + }); + + await UrlbarTestUtils.exitSearchMode(window); + } + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Pressing Right on an @ alias autofill should enter search mode. +add_task(async function rightEntersSearchMode() { + for (let value of [ALIAS.substring(0, ALIAS.length - 1), ALIAS]) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + selectionStart: value.length, + selectionEnd: value.length, + }); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_ArrowRight"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ALIAS_ENGINE_NAME, + entry: "typed", + }); + Assert.equal(gURLBar.value, "", "value should be empty"); + await UrlbarTestUtils.exitSearchMode(window); + } + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Pressing Tab when an @ alias is autofilled should enter search mode preview. +add_task(async function rightEntersSearchMode() { + for (let value of [ALIAS.substring(0, ALIAS.length - 1), ALIAS]) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + selectionStart: value.length, + selectionEnd: value.length, + }); + + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + -1, + "There is no selected result." + ); + + EventUtils.synthesizeKey("KEY_Tab"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "The first result is selected." + ); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ALIAS_ENGINE_NAME, + entry: "keywordoffer", + isPreview: true, + }); + Assert.equal(gURLBar.value, "", "value should be empty"); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ALIAS_ENGINE_NAME, + entry: "keywordoffer", + isPreview: false, + }); + await UrlbarTestUtils.exitSearchMode(window); + } + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +/** + * This test checks that if an engine is marked as hidden then + * it should not appear in the popup when using the "@" token alias in the search bar. + */ +add_task(async function hiddenEngine() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + fireInputEvent: true, + }); + + const defaultEngine = await Services.search.getDefault(); + + let foundDefaultEngineInPopup = false; + + // Checks that the default engine appears in the urlbar's popup. + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (defaultEngine.name == details.searchParams.engine) { + foundDefaultEngineInPopup = true; + break; + } + } + Assert.ok(foundDefaultEngineInPopup, "Default engine appears in the popup."); + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + + // Checks that a hidden default engine (i.e. an engine removed from + // a user's search settings) does not appear in the urlbar's popup. + defaultEngine.hidden = true; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + fireInputEvent: true, + }); + foundDefaultEngineInPopup = false; + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (defaultEngine.name == details.searchParams.engine) { + foundDefaultEngineInPopup = true; + break; + } + } + Assert.ok( + !foundDefaultEngineInPopup, + "Hidden default engine does not appear in the popup" + ); + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + + defaultEngine.hidden = false; +}); + +/** + * This test checks that if an engines alias is not prefixed with + * @ it still appears in the popup when using the "@" token + * alias in the search bar. + */ +add_task(async function nonPrefixedKeyword() { + let name = "Custom"; + let alias = "customkeyword"; + let extension = await SearchTestUtils.installSearchExtension( + { + name, + keyword: alias, + }, + { skipUnload: true } + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + + let foundEngineInPopup = false; + + // Checks that the default engine appears in the urlbar's popup. + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (details.searchParams.engine === name) { + foundEngineInPopup = true; + break; + } + } + Assert.ok(foundEngineInPopup, "Custom engine appears in the popup."); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@" + alias, + }); + + let keywordOfferResult = await UrlbarTestUtils.getDetailsOfResultAt( + window, + 0 + ); + + Assert.equal( + keywordOfferResult.searchParams.engine, + name, + "The first result should be a keyword search result with the correct engine." + ); + + await extension.unload(); +}); + +// Tests that we show all engines with a token alias that match the search +// string. +add_task(async function multipleMatchingEngines() { + let extension = await SearchTestUtils.installSearchExtension( + { + name: "TestFoo", + keyword: `${ALIAS}foo`, + }, + { skipUnload: true } + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@te", + fireInputEvent: true, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Two results are shown." + ); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + -1, + "Neither result is selected." + ); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(result.autofill, "The first result is autofilling."); + Assert.equal( + result.searchParams.keyword, + ALIAS, + "The autofilled engine is shown first." + ); + + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + result.searchParams.keyword, + `${ALIAS}foo`, + "The other engine is shown second." + ); + + EventUtils.synthesizeKey("KEY_Tab"); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0); + Assert.equal(gURLBar.value, "", "Urlbar should be empty."); + EventUtils.synthesizeKey("KEY_Tab"); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 1); + Assert.equal(gURLBar.value, "", "Urlbar should be empty."); + EventUtils.synthesizeKey("KEY_Tab"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + -1, + "Tabbing all the way through the matching engines should return to the input." + ); + Assert.equal( + gURLBar.value, + "@te", + "Urlbar should contain the search string." + ); + + await extension.unload(); +}); + +// Tests that UrlbarProviderTokenAliasEngines is disabled in search mode. +add_task(async function doNotShowInSearchMode() { + // Do a search for "@" to show all the @ aliases. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + + // Find our test engine in the results. It's probably last, but for + // robustness don't assume it is. + let testEngineItem; + for (let i = 0; !testEngineItem; i++) { + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (details.searchParams && details.searchParams.keyword == ALIAS) { + testEngineItem = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + i + ); + } + } + + Assert.equal( + testEngineItem.result.payload.keyword, + ALIAS, + "Sanity check: we found our engine." + ); + + // Click it. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(testEngineItem, {}); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: testEngineItem.result.payload.engine, + entry: "keywordoffer", + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + fireInputEvent: true, + }); + + let resultCount = UrlbarTestUtils.getResultCount(window); + for (let i = 0; i < resultCount; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.ok( + !result.searchParams.keyword, + `Result at index ${i} is not a keywordoffer.` + ); + } +}); + +async function assertFirstResultIsAlias(isAlias, expectedAlias) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Should have the correct type" + ); + + if (isAlias) { + Assert.equal( + result.searchParams.keyword, + expectedAlias, + "Payload keyword should be the alias" + ); + } else { + Assert.notEqual( + result.searchParams.keyword, + expectedAlias, + "Payload keyword should be absent or not the alias" + ); + } +} + +function assertHighlighted(highlighted, expectedAlias) { + let selection = gURLBar.editor.selectionController.getSelection( + Ci.nsISelectionController.SELECTION_FIND + ); + Assert.ok(selection); + if (!highlighted) { + Assert.equal(selection.rangeCount, 0); + return; + } + Assert.equal(selection.rangeCount, 1); + let index = gURLBar.value.indexOf(expectedAlias); + Assert.ok( + index >= 0, + `gURLBar.value="${gURLBar.value}" expectedAlias="${expectedAlias}"` + ); + let range = selection.getRangeAt(0); + Assert.ok(range); + Assert.equal(range.startOffset, index); + Assert.equal(range.endOffset, index + expectedAlias.length); +} + +/** + * Returns an array of code points in the given string. Each code point is + * returned as a hexidecimal string. + * + * @param {string} str + * The code points of this string will be returned. + * @returns {Array} + * Array of code points in the string, where each is a hexidecimal string. + */ +function codePoints(str) { + return str.split("").map(s => s.charCodeAt(0).toString(16)); +} diff --git a/browser/components/urlbar/tests/browser/browser_top_sites.js b/browser/components/urlbar/tests/browser/browser_top_sites.js new file mode 100644 index 0000000000..a473216ab1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_top_sites.js @@ -0,0 +1,478 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", +}); + +const EN_US_TOPSITES = + "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/"; + +async function addTestVisits() { + // Add some visits to a URL. + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits("http://example.com/"); + } + + // Wait for example.com to be listed first. + await updateTopSites(sites => { + return sites && sites[0] && sites[0].url == "http://example.com/"; + }); + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "https://www.youtube.com/", + title: "YouTube", + }); +} + +async function checkDoesNotOpenOnFocus(win = window) { + // The view should not open when the input is focused programmatically. + win.gURLBar.blur(); + win.gURLBar.focus(); + Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open"); + win.gURLBar.blur(); + + // Check the keyboard shortcut. + win.document.getElementById("Browser:OpenLocation").doCommand(); + // Because the panel opening may not be immediate, we must wait a bit. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 500)); + Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open"); + win.gURLBar.blur(); + + // Focus with the mouse. + EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win); + // Because the panel opening may not be immediate, we must wait a bit. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 500)); + Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open"); + win.gURLBar.blur(); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.topsites", true], + ["browser.urlbar.suggest.quickactions", false], + ["browser.newtabpage.activity-stream.default.sites", EN_US_TOPSITES], + ], + }); + + await updateTopSites( + sites => sites && sites.length == EN_US_TOPSITES.split(",").length + ); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com/" + ); + + registerCleanupFunction(() => { + BrowserTestUtils.removeTab(tab); + }); +}); + +add_task(async function topSitesShown() { + let sites = AboutNewTab.getTopSites(); + + for (let prefVal of [true, false]) { + // This test should work regardless of whether Top Sites are enabled on + // about:newtab. + await SpecialPowers.pushPrefEnv({ + set: [["browser.newtabpage.activity-stream.feeds.topsites", prefVal]], + }); + // We don't expect this to change, but we run updateTopSites just in case + // feeds.topsites were to have an effect on the composition of Top Sites. + await updateTopSites(siteList => siteList.length == 6); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + sites.length, + "The number of results should be the same as the number of Top Sites (6)." + ); + + for (let i = 0; i < sites.length; i++) { + let site = sites[i]; + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (site.searchTopSite) { + Assert.equal( + result.searchParams.keyword, + site.label, + "The search Top Site should have an alias." + ); + continue; + } + + Assert.equal( + site.url, + result.url, + "The Top Site URL and the result URL shoud match." + ); + Assert.equal( + site.label || site.title || site.hostname, + result.title, + "The Top Site title and the result title shoud match." + ); + } + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + // This pops updateTopSites changes. + await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function selectSearchTopSite() { + await updateTopSites( + sites => sites && sites[0] && sites[0].searchTopSite, + true + ); + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + + let amazonSearch = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + 0 + ); + + Assert.equal( + amazonSearch.result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "First result should have SEARCH type." + ); + + Assert.equal( + amazonSearch.result.payload.keyword, + "@amazon", + "First result should have the Amazon keyword." + ); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(amazonSearch, {}); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: amazonSearch.result.payload.engine, + entry: "topsites_urlbar", + }); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); +}); + +add_task(async function topSitesBookmarksAndTabs() { + await addTestVisits(); + let sites = AboutNewTab.getTopSites(); + Assert.equal( + sites.length, + 7, + "The test suite browser should have 7 Top Sites." + ); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(window); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 7, + "The number of results should be the same as the number of Top Sites (7)." + ); + + let exampleResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + exampleResult.url, + "http://example.com/", + "The example.com Top Site should be the first result." + ); + Assert.equal( + exampleResult.source, + UrlbarUtils.RESULT_SOURCE.TABS, + "The example.com Top Site should appear in the view as an open tab result." + ); + + let youtubeResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + youtubeResult.url, + "https://www.youtube.com/", + "The YouTube Top Site should be the second result." + ); + Assert.equal( + youtubeResult.source, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + "The YouTube Top Site should appear in the view as a bookmark result." + ); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +add_task(async function topSitesKeywordNavigationPageproxystate() { + await addTestVisits(); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Sanity check initial state" + ); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(window); + + let count = UrlbarTestUtils.getResultCount(window); + Assert.equal(count, 7, "The number of results should be the expected one."); + + for (let i = 0; i < count; ++i) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "invalid", + "Moving through results" + ); + } + for (let i = 0; i < count; ++i) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "invalid", + "Moving through results" + ); + } + + // Double ESC should restore state. + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); + EventUtils.synthesizeKey("KEY_Escape"); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Double ESC should restore state" + ); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +add_task(async function topSitesPinned() { + await addTestVisits(); + let info = { url: "http://example.com/" }; + NewTabUtils.pinnedLinks.pin(info, 0); + + await updateTopSites(sites => sites && sites[0] && sites[0].isPinned); + + let sites = AboutNewTab.getTopSites(); + Assert.equal( + sites.length, + 7, + "The test suite browser should have 7 Top Sites." + ); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(window); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 7, + "The number of results should be the same as the number of Top Sites (7)." + ); + + let exampleResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + exampleResult.url, + "http://example.com/", + "The example.com Top Site should be the first result." + ); + + Assert.equal( + exampleResult.source, + UrlbarUtils.RESULT_SOURCE.TABS, + "The example.com Top Site should be an open tab result." + ); + + Assert.ok( + exampleResult.element.row.hasAttribute("pinned"), + "The example.com Top Site should have the pinned property." + ); + + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + NewTabUtils.pinnedLinks.unpin(info); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +add_task(async function topSitesBookmarksAndTabsDisabled() { + await addTestVisits(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.openpage", false], + ["browser.urlbar.suggest.bookmark", false], + ], + }); + + let sites = AboutNewTab.getTopSites(); + Assert.equal( + sites.length, + 7, + "The test suite browser should have 7 Top Sites." + ); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(window); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 7, + "The number of results should be the same as the number of Top Sites (7)." + ); + + let exampleResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + exampleResult.url, + "http://example.com/", + "The example.com Top Site should be the second result." + ); + Assert.equal( + exampleResult.source, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + "The example.com Top Site should appear as a normal result even though it's open in a tab." + ); + + let youtubeResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + youtubeResult.url, + "https://www.youtube.com/", + "The YouTube Top Site should be the third result." + ); + Assert.equal( + youtubeResult.source, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + "The YouTube Top Site should appear as a normal result even though it's bookmarked." + ); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function topSitesDisabled() { + // Disable Top Sites feed. + await SpecialPowers.pushPrefEnv({ + set: [["browser.newtabpage.activity-stream.feeds.system.topsites", false]], + }); + await checkDoesNotOpenOnFocus(); + await SpecialPowers.popPrefEnv(); + + // Top Sites should also not be shown when Urlbar Top Sites are disabled. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.topsites", false]], + }); + await checkDoesNotOpenOnFocus(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function topSitesNumber() { + // Add some visits + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits([ + "http://example-a.com/", + "http://example-b.com/", + "http://example-c.com/", + "http://example-d.com/", + "http://example-e.com/", + ]); + } + + // Wait for the expected number of Top sites. + await updateTopSites(sites => sites && sites.length == 8); + Assert.equal( + AboutNewTab.getTopSites().length, + 8, + "The test suite browser should have 8 Top Sites." + ); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 8, + "The number of results should be the default (8)." + ); + await UrlbarTestUtils.promisePopupClose(window); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.newtabpage.activity-stream.topSitesRows", 2]], + }); + // Wait for the expected number of Top sites. + await updateTopSites(sites => sites && sites.length == 11); + Assert.equal( + AboutNewTab.getTopSites().length, + 11, + "The test suite browser should have 11 Top Sites." + ); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(window); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 10, + "The number of results should be maxRichResults (10)." + ); + await UrlbarTestUtils.promisePopupClose(window); + + await SpecialPowers.popPrefEnv(); + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_top_sites_private.js b/browser/components/urlbar/tests/browser/browser_top_sites_private.js new file mode 100644 index 0000000000..c52239a800 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_top_sites_private.js @@ -0,0 +1,171 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", +}); + +const EN_US_TOPSITES = + "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/"; + +async function addTestVisits() { + // Add some visits to a URL. + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits("http://example.com/"); + } + + // Wait for example.com to be listed first. + await updateTopSites(sites => { + return sites && sites[0] && sites[0].url == "http://example.com/"; + }); + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "https://www.youtube.com/", + title: "YouTube", + }); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.topsites", true], + ["browser.urlbar.suggest.quickactions", false], + ["browser.newtabpage.activity-stream.default.sites", EN_US_TOPSITES], + ], + }); + + await updateTopSites( + sites => sites && sites.length == EN_US_TOPSITES.split(",").length + ); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com/" + ); + + registerCleanupFunction(() => { + BrowserTestUtils.removeTab(tab); + }); +}); + +add_task(async function topSitesPrivateWindow() { + // Top Sites should also be shown in private windows. + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await addTestVisits(); + let sites = AboutNewTab.getTopSites(); + Assert.equal( + sites.length, + 7, + "The test suite browser should have 7 Top Sites." + ); + let urlbar = privateWin.gURLBar; + await UrlbarTestUtils.promisePopupOpen(privateWin, () => { + if (urlbar.getAttribute("pageproxystate") == "invalid") { + urlbar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(urlbar.inputField, {}, privateWin); + }); + Assert.ok(urlbar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(privateWin); + + Assert.equal( + UrlbarTestUtils.getResultCount(privateWin), + 7, + "The number of results should be the same as the number of Top Sites (7)." + ); + + // Top sites should also be shown in a private window if the search string + // gets cleared. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: privateWin, + value: "example", + }); + urlbar.select(); + EventUtils.synthesizeKey("KEY_Backspace", {}, privateWin); + Assert.ok(urlbar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(privateWin); + Assert.equal( + UrlbarTestUtils.getResultCount(privateWin), + 7, + "The number of results should be the same as the number of Top Sites (7)." + ); + + await BrowserTestUtils.closeWindow(privateWin); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); +}); + +add_task(async function topSitesTabSwitch() { + // Add some visits + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits(["http://example.com/"]); + } + + // Switch to the originating tab, to check for switch to the current tab. + gBrowser.selectedTab = gBrowser.tabs[0]; + + // Wait for the expected number of Top sites. + await updateTopSites(sites => sites?.length == 7); + Assert.equal( + AboutNewTab.getTopSites().length, + 7, + "The test suite browser should have 7 Top Sites." + ); + + async function checkResults(win, expectedResultType) { + let resultCount = UrlbarTestUtils.getResultCount(win); + let result; + for (let i = 0; i < resultCount; ++i) { + result = await UrlbarTestUtils.getDetailsOfResultAt(win, i); + if (result.url == "http://example.com/") { + break; + } + } + Assert.equal( + result.type, + expectedResultType, + `Should provide a result of type ${expectedResultType}.` + ); + } + + info("Test in a non-private window"); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(window); + await checkResults(window, UrlbarUtils.RESULT_TYPE.TAB_SWITCH); + await UrlbarTestUtils.promisePopupClose(window); + + info("Test in a private window, switch to tab should not be offered"); + // Top Sites should also be shown in private windows. + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + let urlbar = privateWin.gURLBar; + await UrlbarTestUtils.promisePopupOpen(privateWin, () => { + if (urlbar.getAttribute("pageproxystate") == "invalid") { + urlbar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(urlbar.inputField, {}, privateWin); + }); + + Assert.ok(urlbar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(privateWin); + await checkResults(privateWin, UrlbarUtils.RESULT_TYPE.URL); + await UrlbarTestUtils.promisePopupClose(privateWin); + await BrowserTestUtils.closeWindow(privateWin); + + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_typed_value.js b/browser/components/urlbar/tests/browser/browser_typed_value.js new file mode 100644 index 0000000000..01a957b5df --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_typed_value.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test ensures that the urlbar is restored to the typed value on blur. + +"use strict"; + +add_setup(async function () { + registerCleanupFunction(async function () { + Services.prefs.clearUserPref("browser.urlbar.autoFill"); + Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions"); + gURLBar.handleRevert(); + await PlacesUtils.history.clear(); + }); + Services.prefs.setBoolPref("browser.urlbar.autoFill", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false); + + await PlacesTestUtils.addVisits([ + "http://example.com/", + "http://example.com/foo", + ]); +}); + +add_task(async function test_autofill() { + let typed = "ex"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typed, + fireInputEvent: true, + }); + Assert.equal(gURLBar.value, "example.com/", "autofilled value as expected"); + Assert.equal(gURLBar.selectionStart, typed.length); + Assert.equal(gURLBar.selectionEnd, gURLBar.value.length); + + gURLBar.blur(); + Assert.equal(gURLBar.value, typed, "Value should have been restored"); + gURLBar.focus(); + Assert.equal(gURLBar.value, typed, "Value should not have changed"); + Assert.equal(gURLBar.selectionStart, typed.length); + Assert.equal(gURLBar.selectionEnd, typed.length); +}); + +add_task(async function test_complete_selection() { + let typed = "ex"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typed, + fireInputEvent: true, + }); + Assert.equal(gURLBar.value, "example.com/", "autofilled value as expected"); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Should have the correct number of matches" + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + gURLBar.value, + UrlbarTestUtils.trimURL("http://example.com/foo"), + "Value should have been completed" + ); + + gURLBar.blur(); + Assert.equal(gURLBar.value, typed, "Value should have been restored"); + gURLBar.focus(); + Assert.equal(gURLBar.value, typed, "Value should not have changed"); + Assert.equal(gURLBar.selectionStart, typed.length); + Assert.equal(gURLBar.selectionEnd, typed.length); +}); diff --git a/browser/components/urlbar/tests/browser/browser_unitConversion.js b/browser/components/urlbar/tests/browser/browser_unitConversion.js new file mode 100644 index 0000000000..566300b7d4 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_unitConversion.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests unit conversion on browser. + */ + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.unitConversion.enabled", true]], + }); + + registerCleanupFunction(function () { + SpecialPowers.clipboardCopyString(""); + }); +}); + +add_task(async function test_selectByMouse() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + + // Clear clipboard content. + SpecialPowers.clipboardCopyString(""); + + const row = await doUnitConversion(win); + + info("Check if the result is copied to clipboard when selecting by mouse"); + EventUtils.synthesizeMouseAtCenter( + row.querySelector(".urlbarView-no-wrap"), + {}, + win + ); + assertClipboard(); + + await UrlbarTestUtils.promisePopupClose(win); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_selectByKey() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + + // Clear clipboard content. + SpecialPowers.clipboardCopyString(""); + + await doUnitConversion(win); + + // As gURLBar might lost focus, + // give focus again in order to enable key event on the result. + win.gURLBar.focus(); + + info("Check if the result is copied to clipboard when selecting by key"); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + EventUtils.synthesizeKey("KEY_Enter", {}, win); + assertClipboard(); + + await UrlbarTestUtils.promisePopupClose(win); + await BrowserTestUtils.closeWindow(win); +}); + +function assertClipboard() { + Assert.equal( + SpecialPowers.getClipboardData("text/plain"), + "100 cm", + "The result of conversion is copied to clipboard" + ); +} + +async function doUnitConversion(win) { + info("Do unit conversion then wait the result"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "1m to cm", + waitForFocus: SimpleTest.waitForFocus, + }); + + const row = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 1); + + Assert.ok(row.querySelector(".urlbarView-favicon"), "The icon is displayed"); + Assert.equal( + row.querySelector(".urlbarView-dynamic-unitConversion-output").textContent, + "100 cm", + "The unit is converted" + ); + + return row; +} diff --git a/browser/components/urlbar/tests/browser/browser_updateForDomainCompletion.js b/browser/components/urlbar/tests/browser/browser_updateForDomainCompletion.js new file mode 100644 index 0000000000..ee49f9d477 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_updateForDomainCompletion.js @@ -0,0 +1,22 @@ +"use strict"; + +/** + * Disable keyword.enabled (so no keyword search), and check that when + * you type in "example" and hit enter, the browser shows an error page. + */ +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["keyword.enabled", false]], + }); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async function (browser) { + gURLBar.value = "example"; + gURLBar.select(); + const loadPromise = BrowserTestUtils.waitForErrorPage(browser); + EventUtils.sendKey("return"); + await loadPromise; + ok(true, "error page is loaded correctly"); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_url_formatted_correctly_on_load.js b/browser/components/urlbar/tests/browser/browser_url_formatted_correctly_on_load.js new file mode 100644 index 0000000000..fe923b4ebf --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_url_formatted_correctly_on_load.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + XPCShellContentUtils: + "resource://testing-common/XPCShellContentUtils.sys.mjs", +}); + +let PUNYCODE_PAGE = "xn--31b1c3b9b.com"; +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +let DECODED_PAGE = "http://योगा.com/"; + +function startServer() { + XPCShellContentUtils.ensureInitialized(this); + let server = XPCShellContentUtils.createHttpServer({ + hosts: [PUNYCODE_PAGE], + }); + server.registerPathHandler("/", (request, response) => { + response.write("<html>A page without icon</html>"); + }); +} + +add_task(async function test_url_formatted_correctly_on_page_load() { + SpecialPowers.pushPrefEnv({ set: [["browser.urlbar.trimURLs", false]] }); + startServer(); + + let onValueChangeCalledAtLeastOnce = false; + let onValueChanged = _ => { + is(gURLBar.value, DECODED_PAGE, "Value is decoded."); + onValueChangeCalledAtLeastOnce = true; + }; + + gURLBar.inputField.addEventListener("ValueChange", onValueChanged); + registerCleanupFunction(() => { + gURLBar.inputField.removeEventListener("ValueChange", onValueChanged); + }); + + BrowserTestUtils.startLoadingURIString(gBrowser, PUNYCODE_PAGE); + // Check that whenever the value of the urlbar is changed, the correct + // decoded punycode url is used. + await BrowserTestUtils.browserLoaded(gBrowser, false, null, true); + + ok( + onValueChangeCalledAtLeastOnce, + "OnValueChanged of UrlbarInput was called at least once." + ); + // Check that the final value is decoded punycode as well. + is(gURLBar.value, DECODED_PAGE, "Final Urlbar value is correct"); + + // Cleanup. + SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_annotation.js b/browser/components/urlbar/tests/browser/browser_urlbar_annotation.js new file mode 100644 index 0000000000..d737fb3561 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_annotation.js @@ -0,0 +1,333 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether a visit information is annotated correctly when picking a result. + +if (AppConstants.platform === "macosx") { + requestLongerTimeout(2); +} + +const FRECENCY = { + ORGANIC: 2000, + SPONSORED: -1, + BOOKMARKED: 2075, + SEARCHED: 100, +}; + +const { + VISIT_SOURCE_ORGANIC, + VISIT_SOURCE_SPONSORED, + VISIT_SOURCE_BOOKMARKED, + VISIT_SOURCE_SEARCHED, +} = PlacesUtils.history; + +/** + * To be used before checking database contents when they depend on a visit + * being added to History. + * + * @param {string} href the page to await notifications for. + */ +async function waitForVisitNotification(href) { + await PlacesTestUtils.waitForNotification("page-visited", events => + events.some(e => e.url === href) + ); +} + +async function assertDatabase({ targetURL, expected }) { + const frecency = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "frecency", + { url: targetURL } + ); + Assert.equal(frecency, expected.frecency, "Frecency is correct"); + + const placesId = await PlacesTestUtils.getDatabaseValue("moz_places", "id", { + url: targetURL, + }); + const expectedTriggeringPlaceId = expected.triggerURL + ? await PlacesTestUtils.getDatabaseValue("moz_places", "id", { + url: expected.triggerURL, + }) + : null; + const db = await PlacesUtils.promiseDBConnection(); + const rows = await db.execute( + "SELECT source, triggeringPlaceId FROM moz_historyvisits WHERE place_id = :place_id AND source = :source", + { + place_id: placesId, + source: expected.source, + } + ); + Assert.equal(rows.length, 1); + Assert.equal( + rows[0].getResultByName("triggeringPlaceId"), + expectedTriggeringPlaceId, + `The triggeringPlaceId in database is correct for ${targetURL}` + ); +} + +function registerProvider(payload) { + const provider = new UrlbarTestUtils.TestProvider({ + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.SEARCH, + ...UrlbarResult.payloadAndSimpleHighlights([], { + ...payload, + }) + ), + ], + priority: Infinity, + }); + UrlbarProvidersManager.registerProvider(provider); + return provider; +} + +async function pickResult({ input, payloadURL, redirectTo }) { + const destinationURL = redirectTo || payloadURL; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: input, + fireInputEvent: true, + }); + + const result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.url, payloadURL); + UrlbarTestUtils.setSelectedRowIndex(window, 0); + + info("Show result and wait for loading"); + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + destinationURL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; +} + +add_setup(async function () { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function basic() { + const testData = [ + { + description: "Sponsored result", + input: "exa", + payload: { + url: "https://example.com/", + isSponsored: true, + }, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + }, + }, + { + description: "Bookmarked result", + input: "exa", + payload: { + url: "https://example.com/", + }, + bookmarks: [ + { + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: Services.io.newURI("https://example.com/"), + title: "test bookmark", + }, + ], + expected: { + source: VISIT_SOURCE_BOOKMARKED, + frecency: FRECENCY.BOOKMARKED, + }, + }, + { + description: "Sponsored and bookmarked result", + input: "exa", + payload: { + url: "https://example.com/", + isSponsored: true, + }, + bookmarks: [ + { + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: Services.io.newURI("https://example.com/"), + title: "test bookmark", + }, + ], + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.BOOKMARKED, + }, + }, + { + description: "Organic result", + input: "exa", + payload: { + url: "https://example.com/", + }, + expected: { + source: VISIT_SOURCE_ORGANIC, + frecency: FRECENCY.ORGANIC, + }, + }, + ]; + + for (const { description, input, payload, bookmarks, expected } of testData) { + info(description); + const provider = registerProvider(payload); + + for (const bookmark of bookmarks || []) { + await PlacesUtils.bookmarks.insert(bookmark); + } + + await BrowserTestUtils.withNewTab("about:blank", async () => { + info("Pick result"); + let promiseVisited = waitForVisitNotification(payload.url); + await pickResult({ input, payloadURL: payload.url }); + await promiseVisited; + info("Check database"); + await assertDatabase({ targetURL: payload.url, expected }); + }); + + UrlbarProvidersManager.unregisterProvider(provider); + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + } +}); + +add_task(async function redirection() { + const redirectTo = "https://example.com/"; + const payload = { + url: "https://example.com/browser/browser/components/urlbar/tests/browser/redirect_to.sjs?/", + isSponsored: true, + }; + const input = "exa"; + const provider = registerProvider(payload); + + await BrowserTestUtils.withNewTab("about:home", async () => { + info("Pick result"); + let promises = [ + waitForVisitNotification(payload.url), + waitForVisitNotification(redirectTo), + ]; + await pickResult({ input, payloadURL: payload.url, redirectTo }); + await Promise.all(promises); + + info("Check database"); + await assertDatabase({ + targetURL: payload.url, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + }, + }); + await assertDatabase({ + targetURL: redirectTo, + expected: { + source: VISIT_SOURCE_SPONSORED, + triggerURL: payload.url, + frecency: FRECENCY.SPONSORED, + }, + }); + }); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +add_task(async function search() { + const originalDefaultEngine = await Services.search.getDefault(); + await SearchTestUtils.installSearchExtension({ + name: "test engine", + keyword: "@test", + }); + + const testData = [ + { + description: "Searched result", + input: "@test abc", + resultURL: "https://example.com/?q=abc", + expected: { + source: VISIT_SOURCE_SEARCHED, + frecency: FRECENCY.SEARCHED, + }, + }, + { + description: "Searched bookmarked result", + input: "@test abc", + resultURL: "https://example.com/?q=abc", + bookmarks: [ + { + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: Services.io.newURI("https://example.com/?q=abc"), + title: "test bookmark", + }, + ], + expected: { + source: VISIT_SOURCE_BOOKMARKED, + frecency: FRECENCY.BOOKMARKED, + }, + }, + ]; + + for (const { + description, + input, + resultURL, + bookmarks, + expected, + } of testData) { + info(description); + await BrowserTestUtils.withNewTab("about:blank", async () => { + for (const bookmark of bookmarks || []) { + await PlacesUtils.bookmarks.insert(bookmark); + } + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: input, + }); + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + resultURL + ); + let promiseVisited = waitForVisitNotification(resultURL); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + await promiseVisited; + await assertDatabase({ targetURL: resultURL, expected }); + + // Open another URL to check whther the source is not inherited. + const payload = { url: "https://example.com/" }; + const provider = registerProvider(payload); + promiseVisited = waitForVisitNotification(payload.url); + await pickResult({ input, payloadURL: payload.url }); + await promiseVisited; + await assertDatabase({ + targetURL: payload.url, + expected: { + source: VISIT_SOURCE_ORGANIC, + frecency: FRECENCY.ORGANIC, + }, + }); + UrlbarProvidersManager.unregisterProvider(provider); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + }); + } + + await Services.search.setDefault( + originalDefaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_selection.js b/browser/components/urlbar/tests/browser/browser_urlbar_selection.js new file mode 100644 index 0000000000..233f61e4eb --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_selection.js @@ -0,0 +1,307 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const exampleSearch = "f oo bar"; +const exampleUrl = "https://example.com/1"; + +function click(target) { + let promise = BrowserTestUtils.waitForEvent(target, "click"); + EventUtils.synthesizeMouseAtCenter(target, {}, target.ownerGlobal); + return promise; +} + +function openContextMenu(target) { + let popupShownPromise = BrowserTestUtils.waitForEvent( + target.ownerGlobal, + "contextmenu" + ); + + EventUtils.synthesizeMouseAtCenter( + target, + { + type: "contextmenu", + button: 2, + }, + target.ownerGlobal + ); + return popupShownPromise; +} + +function drag(target, fromX, fromY, toX, toY) { + let promise = BrowserTestUtils.waitForEvent(target, "mouseup"); + EventUtils.synthesizeMouse( + target, + fromX, + fromY, + { type: "mousemove" }, + target.ownerGlobal + ); + EventUtils.synthesizeMouse( + target, + fromX, + fromY, + { type: "mousedown" }, + target.ownerGlobal + ); + EventUtils.synthesizeMouse( + target, + toX, + toY, + { type: "mousemove" }, + target.ownerGlobal + ); + EventUtils.synthesizeMouse( + target, + toX, + toY, + { type: "mouseup" }, + target.ownerGlobal + ); + return promise; +} + +function resetPrimarySelection(val = "") { + if ( + Services.clipboard.isClipboardTypeSupported( + Services.clipboard.kSelectionClipboard + ) + ) { + // Reset the clipboard. + clipboardHelper.copyStringToClipboard( + val, + Services.clipboard.kSelectionClipboard + ); + } +} + +function checkPrimarySelection(expectedVal = "") { + if ( + Services.clipboard.isClipboardTypeSupported( + Services.clipboard.kSelectionClipboard + ) + ) { + let primaryAsText = SpecialPowers.getClipboardData( + "text/plain", + SpecialPowers.Ci.nsIClipboard.kSelectionClipboard + ); + Assert.equal(primaryAsText, expectedVal); + } +} + +add_setup(async function () { + // On macOS, we must "warm up" the Urlbar to get the first test to pass. + gURLBar.value = ""; + await click(gURLBar.inputField); + gURLBar.blur(); +}); + +add_task(async function leftClickSelectsAll() { + resetPrimarySelection(); + gURLBar.value = exampleSearch; + await click(gURLBar.inputField); + Assert.equal( + gURLBar.selectionStart, + 0, + "The entire search term should be selected." + ); + Assert.equal( + gURLBar.selectionEnd, + exampleSearch.length, + "The entire search term should be selected." + ); + gURLBar.blur(); + checkPrimarySelection(); +}); + +add_task(async function leftClickSelectsUrl() { + resetPrimarySelection(); + gURLBar.value = exampleUrl; + await click(gURLBar.inputField); + Assert.equal(gURLBar.selectionStart, 0, "The entire url should be selected."); + Assert.equal( + gURLBar.selectionEnd, + UrlbarTestUtils.trimURL(exampleUrl).length, + "The entire url should be selected." + ); + gURLBar.blur(); + checkPrimarySelection(); +}); + +add_task(async function rightClickSelectsAll() { + gURLBar.inputField.focus(); + gURLBar.value = exampleUrl; + + // Remove the selection so the focus() call above doesn't influence the test. + gURLBar.selectionStart = gURLBar.selectionEnd = 0; + + resetPrimarySelection(); + + await openContextMenu(gURLBar.inputField); + + Assert.equal(gURLBar.selectionStart, 0, "The entire URL should be selected."); + Assert.equal( + gURLBar.selectionEnd, + UrlbarTestUtils.trimURL(exampleUrl).length, + "The entire URL should be selected." + ); + + checkPrimarySelection(); + + let contextMenu = gURLBar.querySelector("moz-input-box").menupopup; + + // While the context menu is open, test the "Select All" button. + let contextMenuItem = contextMenu.firstElementChild; + while ( + contextMenuItem.nextElementSibling && + contextMenuItem.getAttribute("cmd") != "cmd_selectAll" + ) { + contextMenuItem = contextMenuItem.nextElementSibling; + } + Assert.ok( + contextMenuItem, + "The context menu should have the select all menu item." + ); + + let controller = document.commandDispatcher.getControllerForCommand( + contextMenuItem.getAttribute("cmd") + ); + let enabled = controller.isCommandEnabled( + contextMenuItem.getAttribute("cmd") + ); + Assert.ok(enabled, "The context menu select all item should be enabled."); + + await click(contextMenuItem); + Assert.equal( + gURLBar.selectionStart, + 0, + "The entire URL should be selected after clicking selectAll button." + ); + Assert.equal( + gURLBar.selectionEnd, + UrlbarTestUtils.trimURL(exampleUrl).length, + "The entire URL should be selected after clicking selectAll button." + ); + + gURLBar.querySelector("moz-input-box").menupopup.hidePopup(); + gURLBar.blur(); + checkPrimarySelection(gURLBar._untrimmedValue); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function contextMenuDoesNotCancelSelection() { + gURLBar.inputField.focus(); + gURLBar.value = exampleUrl; + + gURLBar.selectionStart = 3; + gURLBar.selectionEnd = 7; + + resetPrimarySelection(); + + await openContextMenu(gURLBar.inputField); + + Assert.equal( + gURLBar.selectionStart, + 3, + "The selection should not have changed." + ); + Assert.equal( + gURLBar.selectionEnd, + 7, + "The selection should not have changed." + ); + + gURLBar.querySelector("moz-input-box").menupopup.hidePopup(); + gURLBar.blur(); + checkPrimarySelection(); +}); + +add_task(async function dragSelect() { + resetPrimarySelection(); + gURLBar.value = exampleSearch.repeat(10); + // Drags from an artibrary offset of 30 to test for bug 1562145: that the + // selection does not start at the beginning. + await drag(gURLBar.inputField, 30, 0, 60, 0); + Assert.greater( + gURLBar.selectionStart, + 0, + "Selection should not start at the beginning of the string." + ); + + let selectedVal = gURLBar.value.substring( + gURLBar.selectionStart, + gURLBar.selectionEnd + ); + gURLBar.blur(); + checkPrimarySelection(selectedVal); +}); + +/** + * Testing for bug 1571018: that the entire Urlbar isn't selected when the + * Urlbar is dragged following a selectsAll event then a blur. + */ +add_task(async function dragAfterSelectAll() { + resetPrimarySelection(); + gURLBar.value = exampleSearch.repeat(10); + await click(gURLBar.inputField); + Assert.equal( + gURLBar.selectionStart, + 0, + "The entire search term should be selected." + ); + Assert.equal( + gURLBar.selectionEnd, + exampleSearch.repeat(10).length, + "The entire search term should be selected." + ); + + gURLBar.blur(); + checkPrimarySelection(); + + // The offset of 30 is arbitrary. + await drag(gURLBar.inputField, 30, 0, 60, 0); + + Assert.notEqual( + gURLBar.selectionStart, + 0, + "Only part of the search term should be selected." + ); + Assert.notEqual( + gURLBar.selectionEnd, + exampleSearch.repeat(10).length, + "Only part of the search term should be selected." + ); + + checkPrimarySelection( + gURLBar.value.substring(gURLBar.selectionStart, gURLBar.selectionEnd) + ); +}); + +/** + * Testing for bug 1571018: that the entire Urlbar is selected when the Urlbar + * is refocused following a partial text selection then a blur. + */ +add_task(async function selectAllAfterDrag() { + gURLBar.value = exampleSearch; + + gURLBar.selectionStart = 3; + gURLBar.selectionEnd = 7; + + gURLBar.blur(); + + await click(gURLBar.inputField); + + Assert.equal( + gURLBar.selectionStart, + 0, + "The entire search term should be selected." + ); + Assert.equal( + gURLBar.selectionEnd, + exampleSearch.length, + "The entire search term should be selected." + ); + + gURLBar.blur(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js new file mode 100644 index 0000000000..679beb5752 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js @@ -0,0 +1,1218 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry with search related actions. + */ + +"use strict"; + +const SCALAR_URLBAR = "browser.engagement.navigation.urlbar"; +const SCALAR_SEARCHMODE = "browser.engagement.navigation.urlbar_searchmode"; + +// The preference to enable suggestions in the urlbar. +const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches"; + +ChromeUtils.defineESModuleGetters(this, { + SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs", +}); + +function searchInAwesomebar(value, win = window) { + return UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + waitForFocus, + value, + fireInputEvent: true, + }); +} + +/** + * Click one of the entries in the urlbar suggestion popup. + * + * @param {string} resultTitle + * The title of the result to click on. + * @param {number} button [optional] + * which button to click. + * @returns {number} + * The index of the result that was clicked, or -1 if not found. + */ +async function clickURLBarSuggestion(resultTitle, button = 1) { + await UrlbarTestUtils.promiseSearchComplete(window); + + const count = UrlbarTestUtils.getResultCount(window); + for (let i = 0; i < count; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (result.displayed.title == resultTitle) { + // This entry is the search suggestion we're looking for. + let element = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + i + ); + if (button == 1) { + EventUtils.synthesizeMouseAtCenter(element, {}); + } else if (button == 2) { + EventUtils.synthesizeMouseAtCenter(element, { + type: "mousedown", + button: 2, + }); + } + return i; + } + } + return -1; +} + +/** + * Create an engine to generate search suggestions and add it as default + * for this test. + * + * @param {Function} taskFn + * The function to run with the new search engine as default. + */ +async function withNewSearchEngine(taskFn) { + let suggestionEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "urlbarTelemetrySearchSuggestions.xml", + }); + let previousEngine = await Services.search.getDefault(); + await Services.search.setDefault( + suggestionEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + try { + await taskFn(suggestionEngine); + } finally { + await Services.search.setDefault( + previousEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.removeEngine(suggestionEngine); + } +} + +add_setup(async function () { + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + keyword: "mozalias", + search_url: "https://example.com/", + }, + { setAsDefault: true } + ); + + // Make it the first one-off engine. + let engine = Services.search.getEngineByName("MozSearch"); + await Services.search.moveEngine(engine, 0); + + // Enable search suggestions in the urlbar. + let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF); + Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true); + + // Enable local telemetry recording for the duration of the tests. + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + + // Enable event recording for the events tested here. + Services.telemetry.setEventRecordingEnabled("navigation", true); + + // Clear history so that history added by previous tests doesn't mess up this + // test when it selects results in the urlbar. + await PlacesUtils.history.clear(); + + // Clear historical search suggestions to avoid interference from previous + // tests. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 0]], + }); + + // This test assumes that general results are shown before suggestions. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchSuggestionsFirst", false]], + }); + + // Make sure to restore the engine once we're done. + registerCleanupFunction(async function () { + Services.telemetry.canRecordExtended = oldCanRecord; + Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled); + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + Services.telemetry.setEventRecordingEnabled("navigation", false); + }); +}); + +add_task(async function test_simpleQuery() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + let search_hist = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Simulate entering a simple search."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("simple query"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + // Check if the scalars contain the expected values. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + TelemetryTestUtils.assertKeyedScalar( + scalars, + SCALAR_URLBAR, + "search_enter", + 1 + ); + Assert.equal( + Object.keys(scalars[SCALAR_URLBAR]).length, + 1, + "This search must only increment one entry in the scalar." + ); + + // SEARCH_COUNTS should be incremented, but only the urlbar source since an + // internal @search keyword was not used. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 1 + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.alias", + undefined + ); + + // Also check events. + TelemetryTestUtils.assertEvents( + [ + [ + "navigation", + "search", + "urlbar", + "enter", + { engine: "other-MozSearch" }, + ], + ], + { category: "navigation", method: "search" } + ); + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter, + 1 + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_searchMode_enter() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Enter search mode using an alias and a query."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("mozalias query"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + // Check if the scalars contain the expected values. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + TelemetryTestUtils.assertKeyedScalar( + scalars, + SCALAR_SEARCHMODE, + "search_enter", + 1 + ); + Assert.equal( + Object.keys(scalars[SCALAR_SEARCHMODE]).length, + 1, + "This search must only increment one entry in the scalar." + ); + + // Also check events. + TelemetryTestUtils.assertEvents( + [ + [ + "navigation", + "search", + "urlbar_searchmode", + "enter", + { engine: "other-MozSearch" }, + ], + ], + { category: "navigation", method: "search" } + ); + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter, + 1 + ); + + BrowserTestUtils.removeTab(tab); +}); + +// Performs a search using the first result, a one-off button, and the Return +// (Enter) key. +add_task(async function test_oneOff_enter() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + let search_hist = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Perform a one-off search using the first engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + + info("Pressing Alt+Down to take us to the first one-off engine."); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + let engine = + UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton.engine; + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: engine.name, + entry: "oneoff", + }); + + // Now that we're in search mode, execute the search. + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + // Check if the scalars contain the expected values. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + TelemetryTestUtils.assertKeyedScalar( + scalars, + SCALAR_SEARCHMODE, + "search_enter", + 1 + ); + Assert.equal( + Object.keys(scalars[SCALAR_SEARCHMODE]).length, + 1, + "This search must only increment one entry in the scalar." + ); + + // SEARCH_COUNTS should be incremented, but only the urlbar-searchmode source + // since aliases aren't counted separately in search mode. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar-searchmode", + 1 + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.alias", + undefined + ); + + // Also check events. + TelemetryTestUtils.assertEvents( + [ + [ + "navigation", + "search", + "urlbar_searchmode", + "enter", + { engine: "other-MozSearch" }, + ], + ], + { category: "navigation", method: "search" } + ); + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter, + 1 + ); + + BrowserTestUtils.removeTab(tab); +}); + +// Performs a search using the second result, a one-off button, and the Return +// (Enter) key. This only tests the FX_URLBAR_SELECTED_RESULT_METHOD histogram +// since test_oneOff_enter covers everything else. +add_task(async function test_oneOff_enterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await withNewSearchEngine(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + + info( + "Select the second result, press Alt+Down to take us to the first one-off engine." + ); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + let engine = + UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton.engine; + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: engine.name, + entry: "oneoff", + }); + + // Now that we're in search mode, execute the search. + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection, + 1 + ); + + await SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(tab); + }); +}); + +// Performs a search using a click on a one-off button. This only tests the +// FX_URLBAR_SELECTED_RESULT_METHOD histogram since test_oneOff_enter covers +// everything else. +add_task(async function test_oneOff_click() { + Services.telemetry.clearScalars(); + + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + + info("Click the first one-off button."); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + let oneOffButton = + UrlbarTestUtils.getOneOffSearchButtons(window).getSelectableButtons( + false + )[0]; + oneOffButton.click(); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffButton.engine.name, + entry: "oneoff", + }); + + // Now that we're in search mode, execute the search. + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + Assert.ok(element, "Found result after entering search mode."); + EventUtils.synthesizeMouseAtCenter(element, {}); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.click, + 1 + ); + + BrowserTestUtils.removeTab(tab); +}); + +// Clicks the first suggestion offered by the test search engine. +add_task(async function test_suggestion_click() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + await UrlbarTestUtils.formHistory.clear(); + + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + let search_hist = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + await withNewSearchEngine(async function (engine) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + info("Clicking the urlbar suggestion."); + await clickURLBarSuggestion("queryfoo"); + await p; + + // Check if the scalars contain the expected values. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + TelemetryTestUtils.assertKeyedScalar( + scalars, + SCALAR_URLBAR, + "search_suggestion", + 1 + ); + Assert.equal( + Object.keys(scalars[SCALAR_URLBAR]).length, + 1, + "This search must only increment one entry in the scalar." + ); + + // SEARCH_COUNTS should be incremented. + let searchEngineId = "other-" + engine.name; + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + searchEngineId + ".urlbar", + 1 + ); + + // Also check events. + TelemetryTestUtils.assertEvents( + [ + [ + "navigation", + "search", + "urlbar", + "suggestion", + { engine: searchEngineId }, + ], + ], + { category: "navigation", method: "search" } + ); + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.click, + 1 + ); + + BrowserTestUtils.removeTab(tab); + }); +}); + +// Selects and presses the Return (Enter) key on the first suggestion offered by +// the test search engine. This only tests the FX_URLBAR_SELECTED_RESULT_METHOD +// histogram since test_suggestion_click covers everything else. +add_task(async function test_suggestion_arrowEnterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await withNewSearchEngine(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + info("Select the second result and press Return."); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection, + 1 + ); + + BrowserTestUtils.removeTab(tab); + }); +}); + +// Selects through tab and presses the Return (Enter) key on the first +// suggestion offered by the test search engine. +add_task(async function test_suggestion_tabEnterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await withNewSearchEngine(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + info("Select the second result and press Return."); + EventUtils.synthesizeKey("KEY_Tab"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.tabEnterSelection, + 1 + ); + + BrowserTestUtils.removeTab(tab); + }); +}); + +// Selects through code and presses the Return (Enter) key on the first +// suggestion offered by the test search engine. +add_task(async function test_suggestion_enterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await withNewSearchEngine(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + info("Select the second result and press Return."); + UrlbarTestUtils.setSelectedRowIndex(window, 1); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enterSelection, + 1 + ); + + BrowserTestUtils.removeTab(tab); + }); +}); + +// Clicks the first suggestion offered by the test search engine when in search +// mode. +add_task(async function test_searchmode_suggestion_click() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + let search_hist = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + await withNewSearchEngine(async function (engine) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + await searchInAwesomebar("query"); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: engine.name, + }); + info("Clicking the urlbar suggestion."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await clickURLBarSuggestion("queryfoo"); + await p; + + // Check if the scalars contain the expected values. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + TelemetryTestUtils.assertKeyedScalar( + scalars, + SCALAR_SEARCHMODE, + "search_suggestion", + 1 + ); + Assert.equal( + Object.keys(scalars[SCALAR_SEARCHMODE]).length, + 1, + "This search must only increment one entry in the scalar." + ); + + // SEARCH_COUNTS should be incremented. + let searchEngineId = "other-" + engine.name; + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + searchEngineId + ".urlbar-searchmode", + 1 + ); + + // Also check events. + TelemetryTestUtils.assertEvents( + [ + [ + "navigation", + "search", + "urlbar_searchmode", + "suggestion", + { engine: searchEngineId }, + ], + ], + { category: "navigation", method: "search" } + ); + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.click, + 1 + ); + + BrowserTestUtils.removeTab(tab); + }); +}); + +// Selects and presses the Return (Enter) key on the first suggestion offered by +// the test search engine in search mode. This only tests the +// FX_URLBAR_SELECTED_RESULT_METHOD histogram since +// test_searchmode_suggestion_click covers everything else. +add_task(async function test_searchmode_suggestion_arrowEnterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await withNewSearchEngine(async function (engine) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: engine.name, + }); + info("Select the second result and press Return."); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection, + 1 + ); + BrowserTestUtils.removeTab(tab); + }); +}); + +// Selects through tab and presses the Return (Enter) key on the first +// suggestion offered by the test search engine in search mode. +add_task(async function test_suggestion_tabEnterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await withNewSearchEngine(async function (engine) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: engine.name, + }); + info("Select the second result and press Return."); + EventUtils.synthesizeKey("KEY_Tab"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.tabEnterSelection, + 1 + ); + + BrowserTestUtils.removeTab(tab); + }); +}); + +// Selects through code and presses the Return (Enter) key on the first +// suggestion offered by the test search engine in search mode. +add_task(async function test_suggestion_enterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await withNewSearchEngine(async function (engine) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: engine.name, + }); + info("Select the second result and press Return."); + UrlbarTestUtils.setSelectedRowIndex(window, 1); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enterSelection, + 1 + ); + + BrowserTestUtils.removeTab(tab); + }); +}); + +// Clicks a form history result. +add_task(async function test_formHistory_click() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + await UrlbarTestUtils.formHistory.clear(); + await UrlbarTestUtils.formHistory.add(["foobar"]); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]], + }); + + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + let search_hist = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + await withNewSearchEngine(async engine => { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. There should be form history."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("foo"); + info("Clicking the form history."); + await clickURLBarSuggestion("foobar"); + await p; + + // Check if the scalars contain the expected values. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + TelemetryTestUtils.assertKeyedScalar( + scalars, + SCALAR_URLBAR, + "search_formhistory", + 1 + ); + Assert.equal( + Object.keys(scalars[SCALAR_URLBAR]).length, + 1, + "This search must only increment one entry in the scalar." + ); + + // SEARCH_COUNTS should be incremented. + let searchEngineId = "other-" + engine.name; + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + searchEngineId + ".urlbar", + 1 + ); + + // Also check events. + TelemetryTestUtils.assertEvents( + [ + [ + "navigation", + "search", + "urlbar", + "formhistory", + { engine: searchEngineId }, + ], + ], + { category: "navigation", method: "search" } + ); + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.click, + 1 + ); + + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + await SpecialPowers.popPrefEnv(); + }); +}); + +// Selects and presses the Return (Enter) key on a form history result. This +// only tests the FX_URLBAR_SELECTED_RESULT_METHOD histogram since +// test_formHistory_click covers everything else. +add_task(async function test_formHistory_arrowEnterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await UrlbarTestUtils.formHistory.clear(); + await UrlbarTestUtils.formHistory.add(["foobar"]); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]], + }); + + await withNewSearchEngine(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. There should be form history."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("foo"); + info("Select the form history result and press Return."); + while (gURLBar.untrimmedValue != "foobar") { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection, + 1 + ); + + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + await SpecialPowers.popPrefEnv(); + }); +}); + +// Selects through tab and presses the Return (Enter) key on a form history +// result. +add_task(async function test_formHistory_tabEnterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await UrlbarTestUtils.formHistory.clear(); + await UrlbarTestUtils.formHistory.add(["foobar"]); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]], + }); + + await withNewSearchEngine(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. There should be form history."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("foo"); + info("Select the form history result and press Return."); + while (gURLBar.untrimmedValue != "foobar") { + EventUtils.synthesizeKey("KEY_Tab"); + } + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.tabEnterSelection, + 1 + ); + + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + await SpecialPowers.popPrefEnv(); + }); +}); + +// Selects through code and presses the Return (Enter) key on a form history +// result. +add_task(async function test_formHistory_enterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await UrlbarTestUtils.formHistory.clear(); + await UrlbarTestUtils.formHistory.add(["foobar"]); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]], + }); + + await withNewSearchEngine(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. There should be form history."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("foo"); + info("Select the second result and press Return."); + let index = 1; + while (gURLBar.untrimmedValue != "foobar") { + UrlbarTestUtils.setSelectedRowIndex(window, index++); + } + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enterSelection, + 1 + ); + + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + await SpecialPowers.popPrefEnv(); + }); +}); + +add_task(async function test_privateWindow() { + // This test assumes the showSearchTerms feature is not enabled, + // as multiple searches are made one after another, relying on + // urlbar as the keyed scalar SAP, not urlbar_persisted. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", false]], + }); + + // Override the search telemetry search provider info to + // count in-content SEARCH_COUNTs telemetry for our test engine. + SearchSERPTelemetry.overrideSearchTelemetryForTests([ + { + telemetryId: "example", + searchPageRegexp: "^https://example\\.com/", + queryParamNames: ["q"], + }, + ]); + + let search_hist = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + // First, do a bunch of searches in a private window. + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + info("Search in a private window and the pref does not exist"); + let p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await searchInAwesomebar("query", win); + EventUtils.synthesizeKey("KEY_Enter", undefined, win); + await p; + + // SEARCH_COUNTS should be incremented. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 1 + ); + let scalars = TelemetryTestUtils.getProcessScalars("parent", true); + console.log(scalars); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "browser.search.content.urlbar", + "example:organic:none", + 1 + ); + + info("Search again in a private window after setting the pref to true"); + Services.prefs.setBoolPref("browser.engagement.search_counts.pbm", true); + p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await searchInAwesomebar("another query", win); + EventUtils.synthesizeKey("KEY_Enter", undefined, win); + await p; + + // SEARCH_COUNTS should *not* be incremented. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 1 + ); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "browser.search.content.urlbar", + "example:organic:none", + 1 + ); + + info("Search again in a private window after setting the pref to false"); + Services.prefs.setBoolPref("browser.engagement.search_counts.pbm", false); + p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await searchInAwesomebar("another query", win); + EventUtils.synthesizeKey("KEY_Enter", undefined, win); + await p; + + // SEARCH_COUNTS should be incremented. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 2 + ); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "browser.search.content.urlbar", + "example:organic:none", + 2 + ); + + info("Search again in a private window after clearing the pref"); + Services.prefs.clearUserPref("browser.engagement.search_counts.pbm"); + p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await searchInAwesomebar("another query", win); + EventUtils.synthesizeKey("KEY_Enter", undefined, win); + await p; + + // SEARCH_COUNTS should be incremented. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 3 + ); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "browser.search.content.urlbar", + "example:organic:none", + 3 + ); + + await BrowserTestUtils.closeWindow(win); + + // Now, do a bunch of searches in a non-private window. Telemetry should + // always be recorded regardless of the pref's value. + win = await BrowserTestUtils.openNewBrowserWindow(); + + info("Search in a non-private window and the pref does not exist"); + p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await searchInAwesomebar("query", win); + EventUtils.synthesizeKey("KEY_Enter", undefined, win); + await p; + + // SEARCH_COUNTS should be incremented. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 4 + ); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "browser.search.content.urlbar", + "example:organic:none", + 4 + ); + + info("Search again in a non-private window after setting the pref to true"); + Services.prefs.setBoolPref("browser.engagement.search_counts.pbm", true); + p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await searchInAwesomebar("another query", win); + EventUtils.synthesizeKey("KEY_Enter", undefined, win); + await p; + + // SEARCH_COUNTS should be incremented. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 5 + ); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "browser.search.content.urlbar", + "example:organic:none", + 5 + ); + + info("Search again in a non-private window after setting the pref to false"); + Services.prefs.setBoolPref("browser.engagement.search_counts.pbm", false); + p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await searchInAwesomebar("another query", win); + EventUtils.synthesizeKey("KEY_Enter", undefined, win); + await p; + + // SEARCH_COUNTS should be incremented. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 6 + ); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "browser.search.content.urlbar", + "example:organic:none", + 6 + ); + + info("Search again in a non-private window after clearing the pref"); + Services.prefs.clearUserPref("browser.engagement.search_counts.pbm"); + p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await searchInAwesomebar("another query", win); + EventUtils.synthesizeKey("KEY_Enter", undefined, win); + await p; + + // SEARCH_COUNTS should be incremented. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 7 + ); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "browser.search.content.urlbar", + "example:organic:none", + 7 + ); + + await BrowserTestUtils.closeWindow(win); + + // Reset the search provider info. + SearchSERPTelemetry.overrideSearchTelemetryForTests(); + await UrlbarTestUtils.formHistory.clear(); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_autofill.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_autofill.js new file mode 100644 index 0000000000..9abd990700 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_autofill.js @@ -0,0 +1,684 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests urlbar autofill telemetry. + */ + +"use strict"; + +const SCALAR_URLBAR = "browser.engagement.navigation.urlbar"; + +function assertSearchTelemetryEmpty(search_hist) { + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + Assert.ok( + !(SCALAR_URLBAR in scalars), + `Should not have recorded ${SCALAR_URLBAR}` + ); + + // SEARCH_COUNTS should not contain any engine counts at all. The keys in this + // histogram are search engine telemetry identifiers. + Assert.deepEqual( + Object.keys(search_hist.snapshot()), + [], + "SEARCH_COUNTS is empty" + ); + + // Also check events. + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + false + ); + events = (events.parent || []).filter( + e => e[1] == "navigation" && e[2] == "search" + ); + Assert.deepEqual( + events, + [], + "Should not have recorded any navigation search events" + ); +} + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +/** + * Performs a search and picks the first result. + * + * @param {string} searchString + * The search string. Assumed to trigger an autofill result + * @param {string} autofilledValue + * The input's expected value after autofill occurs. + * @param {string} unpickResult + * Optional: If true, do not pick any result. Default value is false. + * @param {string} urlToSelect + * Optional: If want to select result except autofill, pass the URL. + */ +async function triggerAutofillAndPickResult( + searchString, + autofilledValue, + unpickResult = false, + urlToSelect = null +) { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill, "Result is autofill"); + Assert.equal(gURLBar.value, autofilledValue, "gURLBar.value"); + Assert.equal(gURLBar.selectionStart, searchString.length, "selectionStart"); + Assert.equal(gURLBar.selectionEnd, autofilledValue.length, "selectionEnd"); + + if (urlToSelect) { + for (let row = 0; row < UrlbarTestUtils.getResultCount(window); row++) { + const result = await UrlbarTestUtils.getDetailsOfResultAt(window, row); + if (result.url === urlToSelect) { + UrlbarTestUtils.setSelectedRowIndex(window, row); + break; + } + } + } + + if (unpickResult) { + // Close popup without any action. + await UrlbarTestUtils.promisePopupClose(window); + return; + } + + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + + let url; + if (urlToSelect) { + url = urlToSelect; + } else { + url = autofilledValue.includes(":") + ? autofilledValue + : "http://" + autofilledValue; + } + Assert.equal(gBrowser.currentURI.spec, url, "Loaded URL is correct"); + }); +} + +function createOtherAutofillProvider(searchString, autofilledValue) { + return new UrlbarTestUtils.TestProvider({ + priority: Infinity, + type: UrlbarUtils.PROVIDER_TYPE.HEURISTIC, + results: [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + title: "Test", + url: "http://example.com/", + } + ), + { + heuristic: true, + autofill: { + value: autofilledValue, + selectionStart: searchString.length, + selectionEnd: autofilledValue.length, + // Leave out `type` to trigger "other" + }, + } + ), + ], + }); +} + +// Allow more time for Mac machines so they don't time out in verify mode. +if (AppConstants.platform == "macosx") { + requestLongerTimeout(3); +} + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await PlacesTestUtils.clearInputHistory(); + + // Enable local telemetry recording for the duration of the tests. + const originalCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + + // Make sure autofill is tested without upgrading pages to https + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first_schemeless", false]], + }); + + registerCleanupFunction(async () => { + Services.telemetry.canRecordExtended = originalCanRecord; + await PlacesTestUtils.clearInputHistory(); + await PlacesUtils.history.clear(); + }); +}); + +// Checks adaptive history, origin, and URL autofill. +add_task(async function history() { + const testData = [ + { + useAdaptiveHistory: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "ex", + autofilled: "example.com/", + expected: "autofill_origin", + }, + { + useAdaptiveHistory: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "exa", + autofilled: "example.com/test", + expected: "autofill_adaptive", + }, + { + useAdaptiveHistory: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "exam", + autofilled: "example.com/test", + expected: "autofill_adaptive", + }, + { + useAdaptiveHistory: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "example.com", + autofilled: "example.com/test", + expected: "autofill_adaptive", + }, + { + useAdaptiveHistory: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "example.com/", + autofilled: "example.com/test", + expected: "autofill_adaptive", + }, + { + useAdaptiveHistory: true, + visitHistory: ["http://example.com/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "example.com/test", + autofilled: "example.com/test", + expected: "autofill_adaptive", + }, + { + useAdaptiveHistory: true, + visitHistory: ["http://example.com/test", "http://example.org/test"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "example.org", + autofilled: "example.org/", + expected: "autofill_origin", + }, + { + useAdaptiveHistory: true, + visitHistory: ["http://example.com/test", "http://example.com/test/url"], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "example.com/test/", + autofilled: "example.com/test/", + expected: "autofill_url", + }, + { + useAdaptiveHistory: true, + visitHistory: [{ uri: "http://example.com/test" }], + inputHistory: [ + { uri: "http://example.com/test", input: "http://example.com/test" }, + ], + userInput: "http://example.com/test", + autofilled: "http://example.com/test", + expected: "autofill_adaptive", + }, + { + useAdaptiveHistory: false, + visitHistory: [{ uri: "http://example.com/test" }], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "example", + autofilled: "example.com/", + expected: "autofill_origin", + }, + { + useAdaptiveHistory: false, + visitHistory: [{ uri: "http://example.com/test" }], + inputHistory: [{ uri: "http://example.com/test", input: "exa" }], + userInput: "example.com/te", + autofilled: "example.com/test", + expected: "autofill_url", + }, + ]; + + for (const { + useAdaptiveHistory, + visitHistory, + inputHistory, + userInput, + autofilled, + expected, + } of testData) { + const histograms = snapshotHistograms(); + + await PlacesTestUtils.addVisits(visitHistory); + for (const { uri, input } of inputHistory) { + await UrlbarUtils.addToInputHistory(uri, input); + } + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", useAdaptiveHistory); + + await triggerAutofillAndPickResult(userInput, autofilled); + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + expected, + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled"); + await PlacesTestUtils.clearInputHistory(); + await PlacesUtils.history.clear(); + } +}); + +// Checks about-page autofill (e.g., "about:about"). +add_task(async function about() { + let histograms = snapshotHistograms(); + await triggerAutofillAndPickResult("about:abou", "about:about"); + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "autofill_about", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + await PlacesUtils.history.clear(); +}); + +// Checks the "other" fallback, which shouldn't normally happen. +add_task(async function other() { + let searchString = "exam"; + let autofilledValue = "example.com/"; + let provider = createOtherAutofillProvider(searchString, autofilledValue); + UrlbarProvidersManager.registerProvider(provider); + + let histograms = snapshotHistograms(); + await triggerAutofillAndPickResult(searchString, autofilledValue); + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "autofill_other", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + await PlacesUtils.history.clear(); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +// Checks impression telemetry. +add_task(async function impression() { + const testData = [ + { + description: "Adaptive history autofill and pick it", + useAdaptiveHistory: true, + visitHistory: ["http://example.com/first", "http://example.com/second"], + inputHistory: [{ uri: "http://example.com/first", input: "exa" }], + userInput: "exa", + autofilled: "example.com/first", + expected: "autofill_adaptive", + }, + { + description: "Adaptive history autofill but pick another result", + useAdaptiveHistory: true, + visitHistory: ["http://example.com/first", "http://example.com/second"], + inputHistory: [{ uri: "http://example.com/first", input: "exa" }], + userInput: "exa", + urlToSelect: "http://example.com/second", + autofilled: "example.com/first", + expected: "autofill_adaptive", + }, + { + description: "Adaptive history autofill but not pick any result", + unpickResult: true, + useAdaptiveHistory: true, + visitHistory: ["http://example.com/first", "http://example.com/second"], + inputHistory: [{ uri: "http://example.com/first", input: "exa" }], + userInput: "exa", + autofilled: "example.com/first", + }, + { + description: "Origin autofill and pick it", + visitHistory: ["http://example.com/first", "http://example.com/second"], + userInput: "exa", + autofilled: "example.com/", + expected: "autofill_origin", + }, + { + description: "Origin autofill but pick another result", + visitHistory: ["http://example.com/first", "http://example.com/second"], + userInput: "exa", + urlToSelect: "http://example.com/second", + autofilled: "example.com/", + expected: "autofill_origin", + }, + { + description: "Origin autofill but not pick any result", + unpickResult: true, + visitHistory: ["http://example.com/first", "http://example.com/second"], + userInput: "exa", + autofilled: "example.com/", + }, + { + description: "URL autofill and pick it", + visitHistory: ["http://example.com/first", "http://example.com/second"], + userInput: "example.com/", + autofilled: "example.com/", + expected: "autofill_url", + }, + { + description: "URL autofill but pick another result", + visitHistory: ["http://example.com/first", "http://example.com/second"], + userInput: "example.com/", + urlToSelect: "http://example.com/second", + autofilled: "example.com/", + expected: "autofill_url", + }, + { + description: "URL autofill but not pick any result", + unpickResult: true, + visitHistory: ["http://example.com/first", "http://example.com/second"], + userInput: "example.com/", + autofilled: "example.com/", + }, + { + description: "about page autofill and pick it", + userInput: "about:a", + autofilled: "about:about", + expected: "autofill_about", + }, + { + description: "about page autofill but pick another result", + userInput: "about:a", + urlToSelect: "about:addons", + autofilled: "about:about", + expected: "autofill_about", + }, + { + description: "about page autofill but not pick any result", + unpickResult: true, + userInput: "about:a", + autofilled: "about:about", + }, + { + description: "Other provider's autofill and pick it", + useOtherProvider: true, + userInput: "example", + autofilled: "example.com/", + expected: "autofill_other", + }, + { + description: "Other provider's autofill but not pick any result", + unpickResult: true, + useOtherProvider: true, + userInput: "example", + autofilled: "example.com/", + }, + ]; + + for (const { + description, + useAdaptiveHistory = false, + useOtherProvider = false, + unpickResult = false, + visitHistory, + inputHistory, + userInput, + select, + autofilled, + expected, + } of testData) { + info(description); + + UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", useAdaptiveHistory); + let otherProvider; + if (useOtherProvider) { + otherProvider = createOtherAutofillProvider(userInput, autofilled); + UrlbarProvidersManager.registerProvider(otherProvider); + } + + if (visitHistory) { + await PlacesTestUtils.addVisits(visitHistory); + } + if (inputHistory) { + for (const { uri, input } of inputHistory) { + await UrlbarUtils.addToInputHistory(uri, input); + } + } + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + await triggerAutofillAndPickResult( + userInput, + autofilled, + unpickResult, + select + ); + + const scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + if (unpickResult) { + TelemetryTestUtils.assertScalarUnset( + scalars, + "urlbar.impression.autofill_adaptive" + ); + TelemetryTestUtils.assertScalarUnset( + scalars, + "urlbar.impression.autofill_origin" + ); + TelemetryTestUtils.assertScalarUnset( + scalars, + "urlbar.impression.autofill_url" + ); + TelemetryTestUtils.assertScalarUnset( + scalars, + "urlbar.impression.autofill_about" + ); + } else { + TelemetryTestUtils.assertScalar( + scalars, + `urlbar.impression.${expected}`, + 1 + ); + } + + UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled"); + + if (otherProvider) { + UrlbarProvidersManager.unregisterProvider(otherProvider); + } + + await PlacesTestUtils.clearInputHistory(); + await PlacesUtils.history.clear(); + } +}); + +// Checks autofill deletion telemetry. +add_task(async function deletion() { + await PlacesTestUtils.addVisits(["http://example.com/"]); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + info("Delete autofilled value by DELETE key"); + await doDeletionTest({ + firstSearchString: "exa", + firstAutofilledValue: "example.com/", + trigger: () => { + EventUtils.synthesizeKey("KEY_Delete"); + Assert.equal(gURLBar.value, "exa"); + }, + expectedScalar: 1, + }); + + info("Delete autofilled value by BACKSPACE key"); + await doDeletionTest({ + firstSearchString: "exa", + firstAutofilledValue: "example.com/", + trigger: () => { + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.equal(gURLBar.value, "exa"); + }, + expectedScalar: 1, + }); + + info("Delete autofilled value twice"); + await doDeletionTest({ + firstSearchString: "exa", + firstAutofilledValue: "example.com/", + trigger: () => { + // Delete autofilled string. + EventUtils.synthesizeKey("KEY_Delete"); + Assert.equal(gURLBar.value, "exa"); + + // Re-autofilling. + EventUtils.synthesizeKey("m"); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "exam".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + // Delete autofilled string again. + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.equal(gURLBar.value, "exam"); + }, + expectedScalar: 2, + }); + + info("Delete one char after unselecting autofilled string"); + await doDeletionTest({ + firstSearchString: "exa", + firstAutofilledValue: "example.com/", + trigger: () => { + // Cancel selection. + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + Assert.equal(gURLBar.selectionStart, "example.com/".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.equal(gURLBar.value, "example.com"); + }, + expectedScalar: 0, + }); + + info("Delete autofilled value after unselecting autofilled string"); + await doDeletionTest({ + firstSearchString: "exa", + firstAutofilledValue: "example.com/", + trigger: () => { + // Cancel selection. + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + Assert.equal(gURLBar.selectionStart, "example.com/".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + // Delete autofilled string one by one. + for (let i = 0; i < "mple.com/".length; i++) { + EventUtils.synthesizeKey("KEY_Backspace"); + } + Assert.equal(gURLBar.value, "exa"); + }, + expectedScalar: 0, + }); + + info( + "Delete autofilled value after unselecting autofilled string then selecting them manually again" + ); + await doDeletionTest({ + firstSearchString: "exa", + firstAutofilledValue: "example.com/", + trigger: () => { + // Cancel selection. + const previousSelectionStart = gURLBar.selectionStart; + const previousSelectionEnd = gURLBar.selectionEnd; + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + Assert.equal(gURLBar.selectionStart, "example.com/".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + // Select same range again. + gURLBar.selectionStart = previousSelectionStart; + gURLBar.selectionEnd = previousSelectionEnd; + + EventUtils.synthesizeKey("KEY_Backspace"); + Assert.equal(gURLBar.value, "exa"); + }, + expectedScalar: 1, + }); + + await PlacesUtils.history.clear(); +}); + +async function doDeletionTest({ + firstSearchString, + firstAutofilledValue, + trigger, + expectedScalar, +}) { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: firstSearchString, + fireInputEvent: true, + }); + const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill, "Result is autofill"); + Assert.equal(gURLBar.value, firstAutofilledValue, "gURLBar.value"); + Assert.equal( + gURLBar.selectionStart, + firstSearchString.length, + "selectionStart" + ); + Assert.equal( + gURLBar.selectionEnd, + firstAutofilledValue.length, + "selectionEnd" + ); + + await trigger(); + + const scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + if (expectedScalar) { + TelemetryTestUtils.assertScalar( + scalars, + "urlbar.autofill_deletion", + expectedScalar + ); + } else { + TelemetryTestUtils.assertScalarUnset(scalars, "urlbar.autofill_deletion"); + } + + await UrlbarTestUtils.promisePopupClose(window); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_dynamic.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_dynamic.js new file mode 100644 index 0000000000..d4f4e77d57 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_dynamic.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry for dynamic results. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", + UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", + UrlbarView: "resource:///modules/UrlbarView.sys.mjs", +}); + +const DYNAMIC_TYPE_NAME = "test"; + +/** + * A test URLBar provider. + */ +class TestProvider extends UrlbarTestUtils.TestProvider { + constructor() { + super({ + priority: Infinity, + results: [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.DYNAMIC, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + dynamicType: DYNAMIC_TYPE_NAME, + } + ), + { heuristic: true } + ), + ], + }); + } + + getViewUpdate(result, idsByName) { + return { + title: { + textContent: "This is a dynamic result.", + }, + button: { + textContent: "Click Me", + }, + }; + } +} + +add_task(async function test() { + // Add a dynamic result type. + UrlbarResult.addDynamicResultType(DYNAMIC_TYPE_NAME); + UrlbarView.addDynamicViewTemplate(DYNAMIC_TYPE_NAME, { + stylesheet: + getRootDirectory(gTestPath) + "urlbarTelemetryUrlbarDynamic.css", + children: [ + { + name: "title", + tag: "span", + }, + { + name: "buttonSpacer", + tag: "span", + }, + { + name: "button", + tag: "span", + attributes: { + role: "button", + }, + }, + ], + }); + registerCleanupFunction(() => { + UrlbarView.removeDynamicViewTemplate(DYNAMIC_TYPE_NAME); + UrlbarResult.removeDynamicResultType(DYNAMIC_TYPE_NAME); + }); + + // Register a provider that returns the dynamic result type. + let provider = new TestProvider(); + UrlbarProvidersManager.registerProvider(provider); + registerCleanupFunction(() => { + UrlbarProvidersManager.unregisterProvider(provider); + }); + + const histograms = snapshotHistograms(); + + // Do a search to show the dynamic result. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "test", + fireInputEvent: true, + }); + + // Press enter on the result's button. It will be preselected since the + // result is the heuristic. + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Enter") + ); + + assertTelemetryResults( + histograms, + "dynamic", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + // Clean up for subsequent tests. + gURLBar.handleRevert(); +}); + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_extension.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_extension.js new file mode 100644 index 0000000000..28eae06a6f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_extension.js @@ -0,0 +1,155 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry with extension actions. + */ + +"use strict"; + +const SCALAR_URLBAR = "browser.engagement.navigation.urlbar"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +function assertSearchTelemetryEmpty(search_hist) { + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + Assert.ok( + !(SCALAR_URLBAR in scalars), + `Should not have recorded ${SCALAR_URLBAR}` + ); + + // Make sure SEARCH_COUNTS contains identical values. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + undefined + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.alias", + undefined + ); + + // Also check events. + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + false + ); + events = (events.parent || []).filter( + e => e[1] == "navigation" && e[2] == "search" + ); + Assert.deepEqual( + events, + [], + "Should not have recorded any navigation search events" + ); +} + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Disable search suggestions in the urlbar. + ["browser.urlbar.suggest.searches", false], + // Clear historical search suggestions to avoid interference from previous + // tests. + ["browser.urlbar.maxHistoricalSearchSuggestions", 0], + // Turn autofill off. + ["browser.urlbar.autoFill", false], + ], + }); + + // Enable local telemetry recording for the duration of the tests. + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + + // Enable event recording for the events tested here. + Services.telemetry.setEventRecordingEnabled("navigation", true); + + // Clear history so that history added by previous tests doesn't mess up this + // test when it selects results in the urlbar. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + // Make sure to restore the engine once we're done. + registerCleanupFunction(async function () { + Services.telemetry.canRecordExtended = oldCanRecord; + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + Services.telemetry.setEventRecordingEnabled("navigation", false); + }); +}); + +add_task(async function test_extension() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + omnibox: { + keyword: "omniboxtest", + }, + + background() { + /* global browser */ + browser.omnibox.setDefaultSuggestion({ + description: "doit", + }); + // Just do nothing for this test. + browser.omnibox.onInputEntered.addListener(() => {}); + browser.omnibox.onInputChanged.addListener((text, suggest) => { + suggest([]); + }); + }, + }, + }); + + await extension.startup(); + + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "omniboxtest ", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "extension", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + await extension.unload(); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_handoff.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_handoff.js new file mode 100644 index 0000000000..6a0f84fbd0 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_handoff.js @@ -0,0 +1,182 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SearchSERPTelemetry } = ChromeUtils.importESModule( + "resource:///modules/SearchSERPTelemetry.sys.mjs" +); + +const TEST_PROVIDER_INFO = [ + { + telemetryId: "example", + searchPageRegexp: + /^https:\/\/example.com\/browser\/browser\/components\/search\/test\/browser\/searchTelemetry(?:Ad)?.html/, + queryParamNames: ["s"], + codeParamName: "abc", + taggedCodes: ["ff"], + followOnParamNames: ["a"], + extraAdServersRegexps: [/^https:\/\/example\.com\/ad2?/], + }, +]; + +function getPageUrl(useAdPage = false) { + let page = useAdPage ? "searchTelemetryAd.html" : "searchTelemetry.html"; + return `https://example.com/browser/browser/components/search/test/browser/${page}`; +} + +// sharedData messages are only passed to the child on idle. Therefore +// we wait for a few idles to try and ensure the messages have been able +// to be passed across and handled. +async function waitForIdle() { + for (let i = 0; i < 10; i++) { + await new Promise(resolve => Services.tm.idleDispatchToMainThread(resolve)); + } +} + +add_setup(async function () { + SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO); + await waitForIdle(); + + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", + true, + ], + ], + }); + + await SearchTestUtils.installSearchExtension( + { + search_url: getPageUrl(true), + search_url_get_params: "s={searchTerms}&abc=ff", + suggest_url: + "https://example.com/browser/browser/components/search/test/browser/searchSuggestionEngine.sjs", + suggest_url_get_params: "query={searchTerms}", + }, + { setAsDefault: true } + ); + + const oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + Services.telemetry.setEventRecordingEnabled("navigation", true); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + + Services.telemetry.canRecordExtended = oldCanRecord; + Services.telemetry.setEventRecordingEnabled("navigation", false); + + SearchSERPTelemetry.overrideSearchTelemetryForTests(); + + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + }); +}); + +add_task(async function test_search() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + const histogram = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + info("Load about:newtab in new window"); + const newtab = "about:newtab"; + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, newtab); + await BrowserTestUtils.browserStopped(tab.linkedBrowser, newtab); + + info("Focus on search input in newtab content"); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const searchInput = content.document.querySelector(".fake-editable"); + searchInput.click(); + }); + + info("Search and wait the result"); + const onLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("q"); + EventUtils.synthesizeKey("VK_RETURN"); + await onLoaded; + + info("Check the telemetries"); + await assertHandoffResult(histogram); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_search_private_mode() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + const histogram = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + info("Open private window"); + let privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + let tab = privateWindow.gBrowser.selectedTab; + + info("Focus on search input in newtab content"); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const searchInput = content.document.querySelector(".fake-editable"); + searchInput.click(); + }); + + info("Search and wait the result"); + const onLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("q", {}, privateWindow); + EventUtils.synthesizeKey("VK_RETURN", {}, privateWindow); + await onLoaded; + + info("Check the telemetries"); + await assertHandoffResult(histogram); + + await BrowserTestUtils.closeWindow(privateWindow); +}); + +async function assertHandoffResult(histogram) { + await assertScalars([ + ["browser.engagement.navigation.urlbar_handoff", "search_enter", 1], + ["browser.search.content.urlbar_handoff", "example:tagged:ff", 1], + ]); + await assertHistogram(histogram, [["other-Example.urlbar-handoff", 1]]); + TelemetryTestUtils.assertEvents( + [ + [ + "navigation", + "search", + "urlbar_handoff", + "enter", + { engine: "other-Example" }, + ], + ], + { category: "navigation", method: "search" } + ); +} + +async function assertHistogram(histogram, expectedResults) { + await TestUtils.waitForCondition(() => { + const snapshot = histogram.snapshot(); + return expectedResults.every(([key]) => key in snapshot); + }, "Wait until the histogram has expected keys"); + + for (const [key, value] of expectedResults) { + TelemetryTestUtils.assertKeyedHistogramSum(histogram, key, value); + } +} + +async function assertScalars(expectedResults) { + await TestUtils.waitForCondition(() => { + const scalars = TelemetryTestUtils.getProcessScalars("parent", true); + return expectedResults.every(([scalarName]) => scalarName in scalars); + }, "Wait until the scalars have expected keyed scalars"); + + const scalars = TelemetryTestUtils.getProcessScalars("parent", true); + + for (const [scalarName, key, value] of expectedResults) { + TelemetryTestUtils.assertKeyedScalar(scalars, scalarName, key, value); + } +} diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_persisted.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_persisted.js new file mode 100644 index 0000000000..629e39855c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_persisted.js @@ -0,0 +1,270 @@ +/* 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 file tests browser.engagement.navigation.urlbar_persisted and the + * event navigation.search.urlbar_persisted + */ + +"use strict"; + +const { SearchSERPTelemetry } = ChromeUtils.importESModule( + "resource:///modules/SearchSERPTelemetry.sys.mjs" +); + +const SCALAR_URLBAR_PERSISTED = + "browser.engagement.navigation.urlbar_persisted"; + +const SEARCH_STRING = "chocolate"; + +let testEngine; +add_setup(async () => { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://www.example.com/", + search_url_get_params: "q={searchTerms}&pc=fake_code", + }, + { setAsDefault: true } + ); + + testEngine = Services.search.getEngineByName("MozSearch"); + + // Enable event recording for the events. + Services.telemetry.setEventRecordingEnabled("navigation", true); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + Services.telemetry.setEventRecordingEnabled("navigation", false); + }); +}); + +async function searchForString(searchString, tab) { + info(`Search for string: ${searchString}.`); + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl( + testEngine, + searchString + ); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + expectedSearchUrl + ); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: searchString, + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + await browserLoadedPromise; + info("Finished loading search."); + return expectedSearchUrl; +} + +async function gotoUrl(url, tab) { + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + url + ); + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url); + await browserLoadedPromise; + info(`Loaded page: ${url}`); +} + +async function goBack(browser) { + let pageShowPromise = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow" + ); + browser.goBack(); + await pageShowPromise; + info("Go back a page."); +} + +async function goForward(browser) { + let pageShowPromise = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow" + ); + browser.goForward(); + await pageShowPromise; + info("Go forward a page."); +} + +function assertScalarSearchEnter(number) { + let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + SCALAR_URLBAR_PERSISTED, + "search_enter", + number + ); +} + +function assertScalarDoesNotExist(scalar) { + let scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + Assert.ok(!(scalar in scalars), scalar + " must not be recorded."); +} + +function assertTelemetryEvents() { + TelemetryTestUtils.assertEvents( + [ + [ + "navigation", + "search", + "urlbar", + "enter", + { engine: "other-MozSearch" }, + ], + [ + "navigation", + "search", + "urlbar_persisted", + "enter", + { engine: "other-MozSearch" }, + ], + ], + { + category: "navigation", + method: "search", + } + ); +} + +// A user making a search after making a search should result +// in the telemetry being recorded. +add_task(async function search_after_search() { + let search_hist = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await searchForString(SEARCH_STRING, tab); + + // Scalar should not exist from a blank page, only when a search + // is conducted from a default SERP. + await assertScalarDoesNotExist(SCALAR_URLBAR_PERSISTED); + + // After the first search, we should expect the SAP to change + // because the search term should show up on the SERP. + await searchForString(SEARCH_STRING, tab); + assertScalarSearchEnter(1); + + // Check search counts. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar-persisted", + 1 + ); + + // Check events. + assertTelemetryEvents(); + + BrowserTestUtils.removeTab(tab); +}); + +// A user going to a tab that contains a SERP should +// trigger the telemetry when conducting a search. +add_task(async function switch_to_tab_and_search() { + let search_hist = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await searchForString(SEARCH_STRING, tab1); + + const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await gotoUrl("https://www.example.com/some-place", tab2); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await searchForString(SEARCH_STRING, tab1); + assertScalarSearchEnter(1); + + // Check search count. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar-persisted", + 1 + ); + + // Check events. + assertTelemetryEvents(); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// When a user reverts the Urlbar after the search terms persist, +// conducting another search should still be registered as a +// urlbar-persisted SAP. +add_task(async function handle_revert() { + let search_hist = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await searchForString(SEARCH_STRING, tab); + + gURLBar.handleRevert(); + await searchForString(SEARCH_STRING, tab); + + assertScalarSearchEnter(1); + + // Check search count. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar-persisted", + 1 + ); + + // Check events. + assertTelemetryEvents(); + + BrowserTestUtils.removeTab(tab); +}); + +// A user going back and forth in history should trigger +// urlbar-persisted telemetry when returning to a SERP +// and conducting a search. +add_task(async function back_and_forth() { + let search_hist = + TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"); + + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // Create three pages in history: a page, a SERP, and a page. + await gotoUrl("https://www.example.com/some-place", tab); + await searchForString(SEARCH_STRING, tab); + await gotoUrl("https://www.example.com/another-page", tab); + + // Go back to the SERP by using both back and forward. + await goBack(tab.linkedBrowser); + await goBack(tab.linkedBrowser); + await goForward(tab.linkedBrowser); + await assertScalarDoesNotExist(SCALAR_URLBAR_PERSISTED); + + // Then do a search. + await searchForString(SEARCH_STRING, tab); + assertScalarSearchEnter(1); + + // Check search count. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar-persisted", + 1 + ); + + // Check events. + assertTelemetryEvents(); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_places.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_places.js new file mode 100644 index 0000000000..671ff9320b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_places.js @@ -0,0 +1,321 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry with places related actions (e.g. history/ + * bookmark selection). + */ + +"use strict"; + +const SCALAR_URLBAR = "browser.engagement.navigation.urlbar"; + +const TEST_URL = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://mochi.test:8888" +); + +ChromeUtils.defineESModuleGetters(this, { + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +function searchInAwesomebar(value, win = window) { + return UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + waitForFocus, + value, + fireInputEvent: true, + }); +} + +function assertSearchTelemetryEmpty(search_hist) { + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + Assert.ok( + !(SCALAR_URLBAR in scalars), + `Should not have recorded ${SCALAR_URLBAR}` + ); + + // Make sure SEARCH_COUNTS contains identical values. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + undefined + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.alias", + undefined + ); + + // Also check events. + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + false + ); + events = (events.parent || []).filter( + e => e[1] == "navigation" && e[2] == "search" + ); + Assert.deepEqual( + events, + [], + "Should not have recorded any navigation search events" + ); +} + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Disable search suggestions in the urlbar. + ["browser.urlbar.suggest.searches", false], + // Clear historical search suggestions to avoid interference from previous + // tests. + ["browser.urlbar.maxHistoricalSearchSuggestions", 0], + // Turn autofill off. + ["browser.urlbar.autoFill", false], + ], + }); + + // Enable local telemetry recording for the duration of the tests. + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + + // Enable event recording for the events tested here. + Services.telemetry.setEventRecordingEnabled("navigation", true); + + // Clear history so that history added by previous tests doesn't mess up this + // test when it selects results in the urlbar. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + await PlacesUtils.keywords.insert({ + keyword: "get", + url: TEST_URL + "?q=%s", + }); + + // Make sure to restore the engine once we're done. + registerCleanupFunction(async function () { + await PlacesUtils.keywords.remove("get"); + Services.telemetry.canRecordExtended = oldCanRecord; + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + Services.telemetry.setEventRecordingEnabled("navigation", false); + }); +}); + +add_task(async function test_history() { + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com", + title: "example", + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }, + ]); + + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("example"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "history", + 1, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_history_adaptive() { + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("example"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "history_adaptive", + 1, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_bookmark_without_history() { + await PlacesUtils.history.clear(); + + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + let bm = await PlacesUtils.bookmarks.insert({ + url: "http://example.com", + title: "example", + parentGuid: PlacesUtils.bookmarks.menuGuid, + }); + + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("example"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "bookmark", + 1, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection + ); + + await PlacesUtils.bookmarks.remove(bm); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_bookmark_with_history() { + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + let bm = await PlacesUtils.bookmarks.insert({ + url: "http://example.com", + title: "example", + parentGuid: PlacesUtils.bookmarks.menuGuid, + }); + + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("example"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "bookmark_adaptive", + 1, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection + ); + + await PlacesUtils.bookmarks.remove(bm); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_keyword() { + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("get example"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "keyword", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_switchtab() { + const histograms = snapshotHistograms(); + + let homeTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:buildconfig" + ); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + + let p = BrowserTestUtils.waitForEvent(gBrowser, "TabSwitchDone"); + await searchInAwesomebar("about:buildconfig"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "switchtab", + 1, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection + ); + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(homeTab); +}); + +add_task(async function test_visitURL() { + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("http://example.com/a/"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "visiturl", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_quickactions.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_quickactions.js new file mode 100644 index 0000000000..b29807900b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_quickactions.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry for quickactions. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderQuickActions: + "resource:///modules/UrlbarProviderQuickActions.sys.mjs", +}); + +let testActionCalled = 0; + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quickactions", true], + ["browser.urlbar.quickactions.enabled", true], + ], + }); + + UrlbarProviderQuickActions.addAction("testaction", { + commands: ["testaction"], + label: "quickactions-downloads2", + onPick: () => testActionCalled++, + }); + + registerCleanupFunction(() => { + UrlbarProviderQuickActions.removeAction("testaction"); + }); +}); + +add_task(async function test() { + const histograms = snapshotHistograms(); + + // Do a search to show the quickaction. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "testaction", + waitForFocus, + fireInputEvent: true, + }); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + }); + + Assert.equal(testActionCalled, 1, "Test action was called"); + + TelemetryTestUtils.assertHistogram( + histograms.resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection, + 1 + ); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + `urlbar.picked.quickaction`, + 1, + 1 + ); + + TelemetryTestUtils.assertKeyedScalar( + scalars, + "quickaction.picked", + "testaction-10", + 1 + ); + + TelemetryTestUtils.assertKeyedScalar( + scalars, + "quickaction.impression", + "testaction-10", + 1 + ); + + // Clean up for subsequent tests. + gURLBar.handleRevert(); +}); + +add_task(async function test_impressions() { + UrlbarProviderQuickActions.addAction("testaction2", { + commands: ["testaction2"], + label: "quickactions-downloads2", + onPick: () => {}, + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "testaction", + waitForFocus, + fireInputEvent: true, + }); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + }); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + + TelemetryTestUtils.assertKeyedScalar( + scalars, + "quickaction.impression", + `testaction-10`, + 1 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "quickaction.impression", + `testaction2-10`, + 1 + ); + + UrlbarProviderQuickActions.removeAction("testaction2"); + gURLBar.handleRevert(); +}); + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + }; +} diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_remotetab.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_remotetab.js new file mode 100644 index 0000000000..ffa3158f2b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_remotetab.js @@ -0,0 +1,185 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry with remote tab action. + */ + +"use strict"; + +const SCALAR_URLBAR = "browser.engagement.navigation.urlbar"; + +ChromeUtils.defineESModuleGetters(this, { + SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +function assertSearchTelemetryEmpty(search_hist) { + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + Assert.ok( + !(SCALAR_URLBAR in scalars), + `Should not have recorded ${SCALAR_URLBAR}` + ); + + // Make sure SEARCH_COUNTS contains identical values. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + undefined + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.alias", + undefined + ); + + // Also check events. + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + false + ); + events = (events.parent || []).filter( + e => e[1] == "navigation" && e[2] == "search" + ); + Assert.deepEqual( + events, + [], + "Should not have recorded any navigation search events" + ); +} + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Disable search suggestions in the urlbar. + ["browser.urlbar.suggest.searches", false], + // Clear historical search suggestions to avoid interference from previous + // tests. + ["browser.urlbar.maxHistoricalSearchSuggestions", 0], + // Turn autofill off. + ["browser.urlbar.autoFill", false], + // Special prefs for remote tabs. + ["services.sync.username", "fake"], + ["services.sync.syncedTabs.showRemoteTabs", true], + ], + }); + + // Enable local telemetry recording for the duration of the tests. + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + + // Enable event recording for the events tested here. + Services.telemetry.setEventRecordingEnabled("navigation", true); + + // Clear history so that history added by previous tests doesn't mess up this + // test when it selects results in the urlbar. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + const REMOTE_TAB = { + id: "7cqCr77ptzX3", + type: "client", + lastModified: 1492201200, + name: "zcarter's Nightly on MacBook-Pro-25", + clientType: "desktop", + tabs: [ + { + type: "tab", + title: "Test Remote", + url: "http://example.com", + icon: UrlbarUtils.ICON.DEFAULT, + client: "7cqCr77ptzX3", + lastUsed: Math.floor(Date.now() / 1000), + }, + ], + }; + + const sandbox = sinon.createSandbox(); + + let originalSyncedTabsInternal = SyncedTabs._internal; + SyncedTabs._internal = { + isConfiguredToSyncTabs: true, + hasSyncedThisSession: true, + getTabClients() { + return Promise.resolve([]); + }, + syncTabs() { + return Promise.resolve(); + }, + }; + + // Tell the Sync XPCOM service it is initialized. + let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + let oldWeaveServiceReady = weaveXPCService.ready; + weaveXPCService.ready = true; + + sandbox + .stub(SyncedTabs._internal, "getTabClients") + .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {}))); + + // Make sure to restore the engine once we're done. + registerCleanupFunction(async function () { + sandbox.restore(); + weaveXPCService.ready = oldWeaveServiceReady; + SyncedTabs._internal = originalSyncedTabsInternal; + Services.telemetry.canRecordExtended = oldCanRecord; + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + Services.telemetry.setEventRecordingEnabled("navigation", false); + }); +}); + +add_task(async function test_remotetab() { + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "example", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "remotetab", + 1, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js new file mode 100644 index 0000000000..7830102cf6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js @@ -0,0 +1,592 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests the urlbar.searchmode.* scalars telemetry with search mode + * related actions. + */ + +"use strict"; + +const ENTRY_SCALAR_PREFIX = "urlbar.searchmode."; +const PICKED_SCALAR_PREFIX = "urlbar.picked.searchmode."; +const ENGINE_ALIAS = "alias"; +const TEST_QUERY = "test"; +let engineName; +let engineDomain; + +// The preference to enable suggestions. +const SUGGEST_PREF = "browser.search.suggest.enabled"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderTabToSearch: + "resource:///modules/UrlbarProviderTabToSearch.sys.mjs", +}); + +XPCOMUtils.defineLazyServiceGetter( + this, + "TouchBarHelper", + "@mozilla.org/widget/touchbarhelper;1", + "nsITouchBarHelper" +); + +/** + * Asserts that search mode telemetry was recorded correctly. Checks both the + * urlbar.searchmode.* and urlbar.searchmode_picked.* probes. + * + * @param {string} entry + * A search mode entry point. + * @param {string} engineOrSource + * An engine name or a search mode source. + * @param {number} [resultIndex] + * The index of the result picked while in search mode. Only pass this + * parameter if a result is picked. + */ +function assertSearchModeScalars(entry, engineOrSource, resultIndex = -1) { + // Check if the urlbar.searchmode.entry scalar contains the expected value. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + TelemetryTestUtils.assertKeyedScalar( + scalars, + ENTRY_SCALAR_PREFIX + entry, + engineOrSource, + 1 + ); + + for (let e of UrlbarUtils.SEARCH_MODE_ENTRY) { + if (e == entry) { + Assert.equal( + Object.keys(scalars[ENTRY_SCALAR_PREFIX + entry]).length, + 1, + `This search must only increment one entry in the correct scalar: ${e}` + ); + } else { + Assert.ok( + !scalars[ENTRY_SCALAR_PREFIX + e], + `No other urlbar.searchmode scalars should be recorded. Checking ${e}` + ); + } + } + + if (resultIndex >= 0) { + TelemetryTestUtils.assertKeyedScalar( + scalars, + PICKED_SCALAR_PREFIX + entry, + resultIndex, + 1 + ); + } + + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Disable tab-to-search onboarding results for general tests. They are + // enabled in tests that specifically address onboarding. + ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0], + ], + }); + + // Create an engine to generate search suggestions and add it as default + // for this test. + let suggestionEngine = await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + "urlbarTelemetrySearchSuggestions.xml", + setAsDefault: true, + }); + suggestionEngine.alias = ENGINE_ALIAS; + engineDomain = suggestionEngine.searchUrlDomain; + engineName = suggestionEngine.name; + + // And the first one-off engine. + await Services.search.moveEngine(suggestionEngine, 0); + + // Enable local telemetry recording for the duration of the tests. + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + + // Clear history so that history added by previous tests doesn't mess up this + // test when it selects results in the urlbar. + await PlacesUtils.history.clear(); + + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + // Clear historical search suggestions to avoid interference from previous + // tests. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 0]], + }); + + // Make sure to restore the engine once we're done. + registerCleanupFunction(async function () { + Services.telemetry.canRecordExtended = oldCanRecord; + await PlacesUtils.history.clear(); + Services.telemetry.setEventRecordingEnabled("navigation", false); + }); +}); + +// Clicks the first one off. +add_task(async function test_oneoff_remote() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + // Enters search mode by clicking a one-off. + await UrlbarTestUtils.enterSearchMode(window); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + assertSearchModeScalars("oneoff", "other", 0); + + BrowserTestUtils.removeTab(tab); +}); + +// Clicks the history one off. +add_task(async function test_oneoff_local() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + // Enters search mode by clicking a one-off. + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + assertSearchModeScalars("oneoff", "history", 0); + + BrowserTestUtils.removeTab(tab); +}); + +// Checks that the Amazon search mode name is collapsed to "Amazon". +add_task(async function test_oneoff_amazon() { + // Disable suggestions to avoid hitting Amazon servers. + await SpecialPowers.pushPrefEnv({ + set: [[SUGGEST_PREF, false]], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + // Enters search mode by clicking a one-off. + await UrlbarTestUtils.enterSearchMode(window, { + engineName: "Amazon.com", + }); + assertSearchModeScalars("oneoff", "Amazon"); + await UrlbarTestUtils.exitSearchMode(window); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// Checks that the Wikipedia search mode name is collapsed to "Wikipedia". +add_task(async function test_oneoff_wikipedia() { + // Disable suggestions to avoid hitting Wikipedia servers. + await SpecialPowers.pushPrefEnv({ + set: [[SUGGEST_PREF, false]], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + // Enters search mode by clicking a one-off. + await UrlbarTestUtils.enterSearchMode(window, { + engineName: "Wikipedia (en)", + }); + assertSearchModeScalars("oneoff", "Wikipedia"); + await UrlbarTestUtils.exitSearchMode(window); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// Enters search mode by pressing the keyboard shortcut. +add_task(async function test_shortcut() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + // Enter search mode by pressing the keyboard shortcut. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("k", { accelKey: true }); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + entry: "shortcut", + }); + assertSearchModeScalars("shortcut", "other"); + + BrowserTestUtils.removeTab(tab); +}); + +// Enters search mode by selecting a Top Site from the Urlbar. +add_task(async function test_topsites_urlbar() { + // Disable suggestions to avoid hitting Amazon servers. + await SpecialPowers.pushPrefEnv({ + set: [[SUGGEST_PREF, false]], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // Enter search mode by selecting a Top Site from the Urlbar. + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + + let amazonSearch = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + 0 + ); + Assert.equal( + amazonSearch.result.payload.keyword, + "@amazon", + "First result should have the Amazon keyword." + ); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(amazonSearch, {}); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: amazonSearch.result.payload.engine, + entry: "topsites_urlbar", + }); + assertSearchModeScalars("topsites_urlbar", "Amazon"); + await UrlbarTestUtils.exitSearchMode(window); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// Enters search mode by selecting a keyword offer result. +add_task(async function test_keywordoffer() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // Do a search for "@" + our test alias. It should autofill with a trailing + // space, and the heuristic result should be an autofill result with a keyword + // offer. + let alias = "@" + ENGINE_ALIAS; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: alias, + }); + let keywordOfferResult = await UrlbarTestUtils.getDetailsOfResultAt( + window, + 0 + ); + Assert.equal( + keywordOfferResult.searchParams.keyword, + alias, + "The first result should be a keyword search result with the correct alias." + ); + + // Pick the keyword offer result. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName, + entry: "keywordoffer", + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + assertSearchModeScalars("keywordoffer", "other", 0); + + BrowserTestUtils.removeTab(tab); +}); + +// Enters search mode by typing an alias. +add_task(async function test_typed() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // Enter search mode by selecting a keywordoffer result. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: `${ENGINE_ALIAS} `, + }); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey(" "); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName, + entry: "typed", + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + assertSearchModeScalars("typed", "other", 0); + + BrowserTestUtils.removeTab(tab); +}); + +// Enters search mode by calling the same function called by the Search +// Bookmarks menu item in Library > Bookmarks. +add_task(async function test_bookmarkmenu() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + PlacesCommandHook.searchBookmarks(); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "bookmarkmenu", + }); + assertSearchModeScalars("bookmarkmenu", "bookmarks"); + BrowserTestUtils.removeTab(tab); +}); + +// Enters search mode by calling the same function called from a History +// menu. +add_task(async function test_historymenu() { + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + PlacesCommandHook.searchHistory(); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + entry: "historymenu", + }); + assertSearchModeScalars("historymenu", "history"); +}); + +// Enters search mode by calling the same function called by the Search Tabs +// menu item in the tab overflow menu. +add_task(async function test_tabmenu() { + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + gTabsPanel.searchTabs(); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.TABS, + entry: "tabmenu", + }); + assertSearchModeScalars("tabmenu", "tabs"); +}); + +// Enters search mode by performing a search handoff on about:privatebrowsing. +// Note that handoff-to-search-mode only occurs when suggestions are disabled +// in the Urlbar. +// NOTE: We don't test handoff on about:home. Running mochitests on about:home +// is quite difficult. This subtest verifies that `handoff` is a valid scalar +// suffix and that a call to UrlbarInput.handoff(value, searchEngine) records +// values in the urlbar.searchmode.handoff scalar. PlacesFeed.test.js verfies that +// about:home handoff makes that exact call. +add_task(async function test_handoff_pbm() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", false]], + }); + let win = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + waitForTabURL: "about:privatebrowsing", + }); + let tab = win.gBrowser.selectedBrowser; + + await SpecialPowers.spawn(tab, [], async function () { + let btn = content.document.getElementById("search-handoff-button"); + btn.click(); + }); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + await new Promise(r => EventUtils.synthesizeKey("f", {}, win, r)); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(win, { + engineName, + entry: "handoff", + }); + assertSearchModeScalars("handoff", "other"); + + await UrlbarTestUtils.exitSearchMode(win); + await UrlbarTestUtils.promisePopupClose(win); + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); +}); + +// Enters search mode by tapping a search shortcut on the Touch Bar. +add_task(async function test_touchbar() { + if (AppConstants.platform != "macosx") { + return; + } + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + // We have to fake the tap on the Touch Bar since mochitests have no way of + // interacting with the Touch Bar. + TouchBarHelper.insertRestrictionInUrlbar(UrlbarTokenizer.RESTRICT.HISTORY); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + entry: "touchbar", + }); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + assertSearchModeScalars("touchbar", "history", 0); + BrowserTestUtils.removeTab(tab); +}); + +// Enters search mode by selecting a tab-to-search result. +// Tests that tab-to-search results preview search mode when highlighted. These +// results are worth testing separately since they do not set the +// payload.keyword parameter. +add_task(async function test_tabtosearch() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Do not show the onboarding result for this subtest. + ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0], + ], + }); + await PlacesTestUtils.addVisits([`http://${engineDomain}/`]); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: engineDomain.slice(0, 4), + }); + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + Assert.equal( + tabToSearchResult.payload.engine, + engineName, + "The tab-to-search result is for the correct engine." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "Sanity check: The second result is selected." + ); + // Pick the tab-to-search result. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName, + entry: "tabtosearch", + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + assertSearchModeScalars("tabtosearch", "other", 0); + + BrowserTestUtils.removeTab(tab); + + await PlacesUtils.history.clear(); + await SpecialPowers.popPrefEnv(); +}); + +// Enters search mode by selecting a tab-to-search onboarding result. +add_task(async function test_tabtosearch_onboard() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]], + }); + await PlacesTestUtils.addVisits([`http://${engineDomain}/`]); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: engineDomain.slice(0, 4), + fireInputEvent: true, + }); + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + Assert.equal( + tabToSearchResult.payload.engine, + engineName, + "The tab-to-search result is for the correct engine." + ); + Assert.equal( + tabToSearchResult.payload.dynamicType, + "onboardTabToSearch", + "The tab-to-search result is an onboarding result." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "Sanity check: The second result is selected." + ); + // Pick the tab-to-search onboarding result. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName, + entry: "tabtosearch_onboard", + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + assertSearchModeScalars("tabtosearch_onboard", "other", 0); + + UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3); + delete UrlbarProviderTabToSearch.onboardingInteractionAtTime; + + BrowserTestUtils.removeTab(tab); + await PlacesUtils.history.clear(); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js new file mode 100644 index 0000000000..318b29ad19 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js @@ -0,0 +1,418 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests telemetry for tabtosearch results. + * NB: This file does not test the search mode `entry` field for tab-to-search + * results. That is tested in browser_UsageTelemetry_urlbar_searchmode.js. + */ + +"use strict"; + +const ENGINE_NAME = "MozSearch"; +const ENGINE_DOMAIN = "example.com"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderTabToSearch: + "resource:///modules/UrlbarProviderTabToSearch.sys.mjs", +}); + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +/** + * Checks to see if the second result in the Urlbar is a tab-to-search result + * with the correct engine. + * + * @param {string} engineName + * The expected engine name. + * @param {boolean} [isOnboarding] + * If true, expects the tab-to-search result to be an onbarding result. + */ +async function checkForTabToSearchResult(engineName, isOnboarding) { + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open."); + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + Assert.equal( + tabToSearchResult.payload.engine, + engineName, + "The tab-to-search result is for the first engine." + ); + if (isOnboarding) { + Assert.equal( + tabToSearchResult.payload.dynamicType, + "onboardTabToSearch", + "The tab-to-search result is an onboarding result." + ); + } else { + Assert.ok( + !tabToSearchResult.payload.dynamicType, + "The tab-to-search result should not be an onboarding result." + ); + } +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]], + }); + + await SearchTestUtils.installSearchExtension({ + name: ENGINE_NAME, + search_url: `https://${ENGINE_DOMAIN}/`, + }); + + // Reset the enginesShown sets in case a previous test showed a tab-to-search + // result but did not end its engagement. + UrlbarProviderTabToSearch.enginesShown.regular.clear(); + UrlbarProviderTabToSearch.enginesShown.onboarding.clear(); + + // Enable local telemetry recording for the duration of the tests. + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + + registerCleanupFunction(async () => { + Services.telemetry.canRecordExtended = oldCanRecord; + }); +}); + +add_task(async function test() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + const histograms = snapshotHistograms(); + + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits([`https://${ENGINE_DOMAIN}/`]); + } + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ENGINE_DOMAIN.slice(0, 4), + fireInputEvent: true, + }); + + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + Assert.equal( + tabToSearchResult.payload.engine, + ENGINE_NAME, + "The tab-to-search result is for the correct engine." + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "Sanity check: The second result is selected." + ); + + // Select the tab-to-search result. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: ENGINE_NAME, + entry: "tabtosearch", + }); + + assertTelemetryResults( + histograms, + "tabtosearch", + 1, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection + ); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + await PlacesUtils.history.clear(); + }); + + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); +}); + +add_task(async function impressions() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]], + }); + await impressions_test(false); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function onboarding_impressions() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]], + }); + await impressions_test(true); + await SpecialPowers.popPrefEnv(); + delete UrlbarProviderTabToSearch.onboardingInteractionAtTime; +}); + +async function impressions_test(isOnboarding) { + await BrowserTestUtils.withNewTab("about:blank", async browser => { + const firstEngineHost = "example"; + let extension = await SearchTestUtils.installSearchExtension( + { + name: `${ENGINE_NAME}2`, + search_url: `https://${firstEngineHost}-2.com/`, + }, + { skipUnload: true } + ); + + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits([`https://${firstEngineHost}-2.com`]); + await PlacesTestUtils.addVisits([`https://${ENGINE_DOMAIN}/`]); + } + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + // First do multiple searches for substrings of firstEngineHost. The view + // should show the same tab-to-search onboarding result the entire time, so + // we should not continue to increment urlbar.tips. + for (let i = 1; i < firstEngineHost.length; i++) { + info( + `Search for "${firstEngineHost.slice( + 0, + i + )}". Only record one impression for this sequence.` + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: firstEngineHost.slice(0, i), + fireInputEvent: true, + }); + await checkForTabToSearchResult(ENGINE_NAME, isOnboarding); + } + + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + let scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown", + 1 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + isOnboarding + ? "urlbar.tabtosearch.impressions_onboarding" + : "urlbar.tabtosearch.impressions", + // "other" is recorded as the engine name because we're not using a built-in engine. + "other", + 1 + ); + + info("Type through autofill to second engine hostname. Record impression."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: firstEngineHost, + fireInputEvent: true, + }); + await checkForTabToSearchResult(ENGINE_NAME, isOnboarding); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: `${firstEngineHost}-`, + fireInputEvent: true, + }); + await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + // Since the user typed past the autofill for the first engine, we showed a + // different onboarding result and now we increment + // tabtosearch_onboard-shown. + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown", + 3 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + isOnboarding + ? "urlbar.tabtosearch.impressions_onboarding" + : "urlbar.tabtosearch.impressions", + "other", + 3 + ); + + info("Make a typo and return to autofill. Do not record impression."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: `${firstEngineHost}-`, + fireInputEvent: true, + }); + await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: `${firstEngineHost}-3`, + fireInputEvent: true, + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "We are not showing a tab-to-search result." + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: `${firstEngineHost}-2`, + fireInputEvent: true, + }); + await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown", + 4 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + isOnboarding + ? "urlbar.tabtosearch.impressions_onboarding" + : "urlbar.tabtosearch.impressions", + "other", + 4 + ); + + info( + "Cancel then restart autofill. Continue to show the tab-to-search result." + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: `${firstEngineHost}-2`, + fireInputEvent: true, + }); + await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Backspace"); + await searchPromise; + await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding); + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + // Type the "." from `example-2.com`. + EventUtils.synthesizeKey("."); + await searchPromise; + await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown", + 5 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + isOnboarding + ? "urlbar.tabtosearch.impressions_onboarding" + : "urlbar.tabtosearch.impressions", + // "other" is recorded as the engine name because we're not using a built-in engine. + "other", + 5 + ); + + // See javadoc for UrlbarProviderTabToSearch.onEngagement for discussion + // about retained results. + info("Reopen the result set with retained results. Record impression."); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await checkForTabToSearchResult(`${ENGINE_NAME}2`, isOnboarding); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + scalars = TelemetryTestUtils.getProcessScalars("parent", true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown", + 6 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + isOnboarding + ? "urlbar.tabtosearch.impressions_onboarding" + : "urlbar.tabtosearch.impressions", + "other", + 6 + ); + + info( + "Open a result page and then autofill engine host. Record impression." + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: firstEngineHost, + fireInputEvent: true, + }); + await checkForTabToSearchResult(ENGINE_NAME, isOnboarding); + // Press enter on the heuristic result so we visit example.com without + // doing an additional search. + let loadPromise = BrowserTestUtils.browserLoaded(browser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + // Click the Urlbar and type to simulate what a user would actually do. If + // we use promiseAutocompleteResultPopup, no query would be made between + // this one and the previous tab-to-search query. Thus + // `onboardingEnginesShown` would not be cleared. This would not happen + // in real-world usage. + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey(firstEngineHost.slice(0, 4)); + await searchPromise; + await checkForTabToSearchResult(ENGINE_NAME, isOnboarding); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + // We clear the scalar this time. + scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + isOnboarding ? "tabtosearch_onboard-shown" : "tabtosearch-shown", + 8 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + isOnboarding + ? "urlbar.tabtosearch.impressions_onboarding" + : "urlbar.tabtosearch.impressions", + "other", + 8 + ); + + await PlacesUtils.history.clear(); + await extension.unload(); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tip.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tip.js new file mode 100644 index 0000000000..345b063441 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tip.js @@ -0,0 +1,130 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry for tip results. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProvider: "resource:///modules/UrlbarUtils.sys.mjs", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", + UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Disable search suggestions in the urlbar. + ["browser.urlbar.suggest.searches", false], + // Turn autofill off. + ["browser.urlbar.autoFill", false], + ], + }); + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test() { + // Add a restricting provider that returns a preselected heuristic tip result. + let provider = new TipProvider([ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + helpUrl: "https://example.com/", + type: "test", + titleL10n: { id: "urlbar-search-tips-confirm" }, + buttons: [ + { + url: "https://example.com/", + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + } + ), + { heuristic: true } + ), + ]); + UrlbarProvidersManager.registerProvider(provider); + + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + // Show the view and press enter to select the tip. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "test", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + + assertTelemetryResults( + histograms, + "tip", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + UrlbarProvidersManager.unregisterProvider(provider); + BrowserTestUtils.removeTab(tab); +}); + +/** + * A test URLBar provider. + */ +class TipProvider extends UrlbarProvider { + constructor(results) { + super(); + this.results = results; + } + get name() { + return "TestProviderTip"; + } + get type() { + return UrlbarUtils.PROVIDER_TYPE.PROFILE; + } + isActive(context) { + return true; + } + getPriority(context) { + return 1; + } + async startQuery(context, addCallback) { + context.preselected = true; + for (const result of this.results) { + addCallback(this, result); + } + } +} diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_topsite.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_topsite.js new file mode 100644 index 0000000000..c4e44bf778 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_topsite.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry for topsite results. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +const EN_US_TOPSITES = + "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/"; + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +/** + * Updates the Top Sites feed. + * + * @param {Function} condition + * A callback that returns true after Top Sites are successfully updated. + * @param {boolean} searchShortcuts + * True if Top Sites search shortcuts should be enabled. + */ +async function updateTopSites(condition, searchShortcuts = false) { + // Toggle the pref to clear the feed cache and force an update. + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.newtabpage.activity-stream.discoverystream.endpointSpocsClear", + "", + ], + ["browser.newtabpage.activity-stream.feeds.system.topsites", false], + ["browser.newtabpage.activity-stream.feeds.system.topsites", true], + [ + "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts", + searchShortcuts, + ], + ], + }); + + // Wait for the feed to be updated. + await TestUtils.waitForCondition(() => { + let sites = AboutNewTab.getTopSites(); + return condition(sites); + }, "Waiting for top sites to be updated"); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.topsites", true], + ["browser.newtabpage.activity-stream.default.sites", EN_US_TOPSITES], + ["browser.urlbar.suggest.quickactions", false], + ], + }); + await updateTopSites( + sites => sites && sites.length == EN_US_TOPSITES.split(",").length + ); +}); + +add_task(async function test() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + let sites = AboutNewTab.getTopSites(); + Assert.equal( + sites.length, + 6, + "The test suite browser should have 6 Top Sites." + ); + + const histograms = snapshotHistograms(); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + sites.length, + "The number of results should be the same as the number of Top Sites (6)." + ); + // Select the first resultm and confirm it. + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "The first result should be selected" + ); + + let loadPromise = BrowserTestUtils.waitForDocLoadAndStopIt( + result.url, + gBrowser.selectedBrowser + ); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + + assertTelemetryResults( + histograms, + "topsite", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection + ); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_zeroPrefix.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_zeroPrefix.js new file mode 100644 index 0000000000..9c3e63ae12 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_zeroPrefix.js @@ -0,0 +1,266 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry related to the zero-prefix view, i.e., when + * the search string is empty. + */ + +"use strict"; + +const HISTOGRAM_DWELL_TIME = "FX_URLBAR_ZERO_PREFIX_DWELL_TIME_MS"; +const SCALARS = { + ABANDONMENT: "urlbar.zeroprefix.abandonment", + ENGAGEMENT: "urlbar.zeroprefix.engagement", + EXPOSURE: "urlbar.zeroprefix.exposure", +}; + +add_setup(async function () { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + Services.telemetry.clearScalars(); + + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + await updateTopSitesAndAwaitChanged(); +}); + +// zero prefix engagement +add_task(async function engagement() { + let dwellHistogram = + TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME); + + await BrowserTestUtils.withNewTab("about:blank", async () => { + await showZeroPrefix(); + checkScalars({ + [SCALARS.EXPOSURE]: 1, + }); + checkAndClearHistogram(dwellHistogram, false); + + info("Finding row with result type URL"); + let foundURLRow = false; + let count = UrlbarTestUtils.getResultCount(window); + for (let i = 0; i < count && !foundURLRow; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + let index = UrlbarTestUtils.getSelectedRowIndex(window); + Assert.equal(index, i, "The expected row index should be selected"); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + info(`Checked row at index ${i}, result type is: ${details.type}`); + if (details.type == UrlbarUtils.RESULT_TYPE.URL) { + foundURLRow = true; + } + } + Assert.ok(foundURLRow, "Should have found a row with result type URL"); + + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + }); + + checkScalars({ + [SCALARS.ENGAGEMENT]: 1, + }); + checkAndClearHistogram(dwellHistogram, true); +}); + +// zero prefix abandonment +add_task(async function abandonment() { + let dwellHistogram = + TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME); + + // Open and close the view twice. The second time the view will used a cached + // query context and that shouldn't interfere with telemetry. + for (let i = 0; i < 2; i++) { + await showZeroPrefix(); + checkScalars({ + [SCALARS.EXPOSURE]: 1, + }); + checkAndClearHistogram(dwellHistogram, false); + + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + checkScalars({ + [SCALARS.ABANDONMENT]: 1, + }); + dwellHistogram = checkAndClearHistogram(dwellHistogram, true); + } +}); + +// Shows the zero-prefix view, does some searches, then shows it again by doing +// a search for an empty string. +add_task(async function searches() { + let dwellHistogram = + TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME); + + info("Show zero prefix"); + await showZeroPrefix(); + checkScalars({ + [SCALARS.EXPOSURE]: 1, + }); + checkAndClearHistogram(dwellHistogram, false); + + info("Search for 't'"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "t", + }); + checkScalars({}); + dwellHistogram = checkAndClearHistogram(dwellHistogram, true); + + info("Search for 'te'"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "te", + }); + checkScalars({}); + checkAndClearHistogram(dwellHistogram, false); + + info("Search for 't'"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "t", + }); + checkScalars({}); + checkAndClearHistogram(dwellHistogram, false); + + info("Search for ''"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + checkScalars({ + [SCALARS.EXPOSURE]: 1, + }); + checkAndClearHistogram(dwellHistogram, false); + + info("Blur urlbar and close view"); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + checkScalars({ + [SCALARS.ABANDONMENT]: 1, + }); + checkAndClearHistogram(dwellHistogram, true); +}); + +// A zero prefix engagement should not be recorded when the view isn't showing +// zero prefix. +add_task(async function notZeroPrefix_engagement() { + let dwellHistogram = + TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME); + + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + }); + + checkScalars({}); + checkAndClearHistogram(dwellHistogram, false); +}); + +// A zero prefix abandonment should not be recorded when the view isn't showing +// zero prefix. +add_task(async function notZeroPrefix_abandonment() { + let dwellHistogram = + TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + + checkScalars({}); + checkAndClearHistogram(dwellHistogram, false); +}); + +function checkScalars(expected) { + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + for (let scalar of Object.values(SCALARS)) { + if (expected.hasOwnProperty(scalar)) { + TelemetryTestUtils.assertScalar(scalars, scalar, expected[scalar]); + } else { + Assert.ok( + !scalars.hasOwnProperty(scalar), + "Scalar should not be recorded: " + scalar + ); + } + } +} + +function checkAndClearHistogram(histogram, expected) { + if (expected) { + Assert.deepEqual( + Object.values(histogram.snapshot().values).filter(v => v > 0), + [1], + "Dwell histogram should be updated" + ); + } else { + Assert.strictEqual( + histogram.snapshot().sum, + 0, + "Dwell histogram should not be updated" + ); + } + + return TelemetryTestUtils.getAndClearHistogram(histogram.name()); +} + +async function showZeroPrefix() { + let { promise, cleanup } = waitForQueryFinished(); + await SimpleTest.promiseFocus(window); + await UrlbarTestUtils.promisePopupOpen(window, () => + document.getElementById("Browser:OpenLocation").doCommand() + ); + await promise; + cleanup(); + + Assert.greater( + UrlbarTestUtils.getResultCount(window), + 0, + "There should be at least one row in the zero prefix view" + ); +} + +/** + * Returns a promise that's resolved on the next `onQueryFinished()`. It's + * important to wait for `onQueryFinished()` because that's when the view checks + * whether it's showing zero prefix. + * + * @returns {object} + * An object with the following properties: + * {Promise} promise + * Resolved when `onQueryFinished()` is called. + * {Function} cleanup + * This should be called to remove the listener. + */ +function waitForQueryFinished() { + let deferred = Promise.withResolvers(); + let listener = { + onQueryFinished: () => deferred.resolve(), + }; + gURLBar.controller.addQueryListener(listener); + + return { + promise: deferred.promise, + cleanup() { + gURLBar.controller.removeQueryListener(listener); + }, + }; +} + +async function updateTopSitesAndAwaitChanged() { + let url = "http://mochi.test:8888/topsite"; + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits(url); + } + + info("Updating top sites and awaiting newtab-top-sites-changed"); + let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed").then( + () => info("Observed newtab-top-sites-changed") + ); + await updateTopSites(sites => sites?.length); + await changedPromise; +} diff --git a/browser/components/urlbar/tests/browser/browser_userTypedValue.js b/browser/components/urlbar/tests/browser/browser_userTypedValue.js new file mode 100644 index 0000000000..14749c6e82 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_userTypedValue.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + const URI = TEST_BASE_URL + "file_userTypedValue.html"; + window.browserDOMWindow.openURI( + makeURI(URI), + null, + Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, + Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL, + Services.scriptSecurityManager.getSystemPrincipal() + ); + + is(gBrowser.userTypedValue, URI, "userTypedValue matches test URI"); + is( + gURLBar.value, + UrlbarTestUtils.trimURL(URI), + "location bar value matches test URI" + ); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + gBrowser.removeCurrentTab({ skipPermitUnload: true }); + is( + gBrowser.userTypedValue, + URI, + "userTypedValue matches test URI after switching tabs" + ); + is( + gURLBar.value, + UrlbarTestUtils.trimURL(URI), + "location bar value matches test URI after switching tabs" + ); + + waitForExplicitFinish(); + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => { + is( + gBrowser.userTypedValue, + null, + "userTypedValue is null as the page has loaded" + ); + is( + gURLBar.value, + UrlbarTestUtils.trimURL(URI), + "location bar value matches test URI as the page has loaded" + ); + + gBrowser.removeCurrentTab({ skipPermitUnload: true }); + finish(); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_valueOnTabSwitch.js b/browser/components/urlbar/tests/browser/browser_valueOnTabSwitch.js new file mode 100644 index 0000000000..ba249adb3b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_valueOnTabSwitch.js @@ -0,0 +1,166 @@ +/* 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 tests for the correct URL being displayed in the URL bar after switching + * tabs which are in different states (e.g. deleted, partially deleted). + */ + +"use strict"; + +const TEST_URL = `${TEST_BASE_URL}dummy_page.html`; + +add_task(async function () { + // autofill may conflict with the test scope, by filling missing parts of + // the url due to autoOpen. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", false]], + }); + + let charsToDelete, + deletedURLTab, + fullURLTab, + partialURLTab, + testPartialURL, + testURL; + + charsToDelete = 5; + deletedURLTab = BrowserTestUtils.addTab(gBrowser); + fullURLTab = BrowserTestUtils.addTab(gBrowser); + partialURLTab = BrowserTestUtils.addTab(gBrowser); + testURL = TEST_URL; + + let loaded1 = BrowserTestUtils.browserLoaded( + deletedURLTab.linkedBrowser, + false, + testURL + ); + let loaded2 = BrowserTestUtils.browserLoaded( + fullURLTab.linkedBrowser, + false, + testURL + ); + let loaded3 = BrowserTestUtils.browserLoaded( + partialURLTab.linkedBrowser, + false, + testURL + ); + BrowserTestUtils.startLoadingURIString(deletedURLTab.linkedBrowser, testURL); + BrowserTestUtils.startLoadingURIString(fullURLTab.linkedBrowser, testURL); + BrowserTestUtils.startLoadingURIString(partialURLTab.linkedBrowser, testURL); + await Promise.all([loaded1, loaded2, loaded3]); + + testURL = BrowserUIUtils.trimURL(testURL); + testPartialURL = testURL.substr(0, testURL.length - charsToDelete); + + function cleanUp() { + gBrowser.removeTab(fullURLTab); + gBrowser.removeTab(partialURLTab); + gBrowser.removeTab(deletedURLTab); + } + + async function cycleTabs() { + await BrowserTestUtils.switchTab(gBrowser, fullURLTab); + is( + gURLBar.value, + testURL, + "gURLBar.value should be testURL after switching back to fullURLTab" + ); + + await BrowserTestUtils.switchTab(gBrowser, partialURLTab); + is( + gURLBar.value, + testPartialURL, + "gURLBar.value should be testPartialURL after switching back to partialURLTab" + ); + await BrowserTestUtils.switchTab(gBrowser, deletedURLTab); + is( + gURLBar.value, + testURL, + "gURLBar.value should be testURL after switching back to deletedURLTab" + ); + + await BrowserTestUtils.switchTab(gBrowser, fullURLTab); + is( + gURLBar.value, + testURL, + "gURLBar.value should be testURL after switching back to fullURLTab" + ); + } + + function urlbarBackspace(removeAll) { + return new Promise((resolve, reject) => { + gBrowser.selectedBrowser.focus(); + gURLBar.addEventListener( + "input", + function () { + resolve(); + }, + { once: true } + ); + gURLBar.focus(); + if (removeAll) { + gURLBar.select(); + } else { + gURLBar.selectionStart = gURLBar.selectionEnd = gURLBar.value.length; + } + EventUtils.synthesizeKey("KEY_Backspace"); + }); + } + + async function prepareDeletedURLTab() { + await BrowserTestUtils.switchTab(gBrowser, deletedURLTab); + is( + gURLBar.value, + testURL, + "gURLBar.value should be testURL after initial switch to deletedURLTab" + ); + + // simulate the user removing the whole url from the location bar + await urlbarBackspace(true); + is(gURLBar.value, "", 'gURLBar.value should be "" (just set)'); + } + + async function prepareFullURLTab() { + await BrowserTestUtils.switchTab(gBrowser, fullURLTab); + is( + gURLBar.value, + testURL, + "gURLBar.value should be testURL after initial switch to fullURLTab" + ); + } + + async function preparePartialURLTab() { + await BrowserTestUtils.switchTab(gBrowser, partialURLTab); + is( + gURLBar.value, + testURL, + "gURLBar.value should be testURL after initial switch to partialURLTab" + ); + + // simulate the user removing part of the url from the location bar + let deleted = 0; + while (deleted < charsToDelete) { + await urlbarBackspace(false); + deleted++; + } + + is( + gURLBar.value, + testPartialURL, + "gURLBar.value should be testPartialURL (just set)" + ); + } + + // prepare the three tabs required by this test + + // First tab + await prepareFullURLTab(); + await preparePartialURLTab(); + await prepareDeletedURLTab(); + + // now cycle the tabs and make sure everything looks good + await cycleTabs(); + cleanUp(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_view_emptyResultSet.js b/browser/components/urlbar/tests/browser/browser_view_emptyResultSet.js new file mode 100644 index 0000000000..f7a2721093 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_view_emptyResultSet.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the view results are cleared and the view is closed, when an empty +// result set arrives after a non-empty one. + +add_task(async function () { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + Assert.ok( + UrlbarTestUtils.getResultCount(window) > 0, + `There should be some results in the view.` + ); + Assert.ok(gURLBar.view.isOpen, `The view should be open.`); + + // Register an high priority empty result provider. + let provider = new UrlbarTestUtils.TestProvider({ + results: [], + priority: 999, + }); + UrlbarProvidersManager.registerProvider(provider); + registerCleanupFunction(async function () { + UrlbarProvidersManager.unregisterProvider(provider); + await PlacesUtils.history.clear(); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + Assert.ok( + UrlbarTestUtils.getResultCount(window) == 0, + `There should be no results in the view.` + ); + Assert.ok(!gURLBar.view.isOpen, `The view should have been closed.`); +}); diff --git a/browser/components/urlbar/tests/browser/browser_view_removedSelectedElement.js b/browser/components/urlbar/tests/browser/browser_view_removedSelectedElement.js new file mode 100644 index 0000000000..532f9e10a2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_view_removedSelectedElement.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that if the selectedElement is removed from the DOM, the view still +// sets a selection on the next received results. + +add_task(async function () { + let view = gURLBar.view; + // We need a heuristic provider that the Muxer will prefer over other + // heuristics and that will return results after the first onQueryResults. + // Luckily TEST providers come first in the heuristic group! + let result = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { url: "https://example.com/1", title: "example" } + ); + result.heuristic = true; + // To ensure the selectedElement is removed, we use this special property that + // asks the view to generate new content for the row. + result.testForceNewContent = true; + + let receivedResults = false; + let firstSelectedElement; + let delayResultsPromise = new Promise(resolve => { + gURLBar.controller.addQueryListener({ + async onQueryResults(queryContext) { + Assert.ok(!receivedResults, "Should execute only once"); + gURLBar.controller.removeQueryListener(this); + receivedResults = true; + // Store the corrent selection. + firstSelectedElement = view.selectedElement; + Assert.ok(firstSelectedElement, "There should be a selected element"); + Assert.ok( + view.selectedResult.heuristic, + "Selected result should be a heuristic" + ); + Assert.notEqual( + result, + view.selectedResult, + "Should not immediately select our result" + ); + resolve(); + }, + }); + }); + + let delayedHeuristicProvider = new UrlbarTestUtils.TestProvider({ + delayResultsPromise, + results: [result], + type: UrlbarUtils.PROVIDER_TYPE.HEURISTIC, + }); + UrlbarProvidersManager.registerProvider(delayedHeuristicProvider); + registerCleanupFunction(async function () { + UrlbarProvidersManager.unregisterProvider(delayedHeuristicProvider); + await UrlbarTestUtils.promisePopupClose(window); + gURLBar.handleRevert(); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exa", + }); + Assert.ok(receivedResults, "Results observer was invoked"); + Assert.ok( + UrlbarTestUtils.getResultCount(window) > 0, + `There should be some results in the view.` + ); + Assert.ok(view.isOpen, `The view should be open.`); + Assert.ok(view.selectedElement.isConnected, "selectedElement is connected"); + Assert.equal(view.selectedElementIndex, 0, "selectedElementIndex is correct"); + Assert.deepEqual( + view.getResultFromElement(view.selectedElement), + result, + "result is the expected one" + ); + Assert.notEqual( + view.selectedElement, + firstSelectedElement, + "Selected element should have changed" + ); + Assert.ok( + !firstSelectedElement.isConnected, + "Previous selected element should be disconnected" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_view_resultDisplay.js b/browser/components/urlbar/tests/browser/browser_view_resultDisplay.js new file mode 100644 index 0000000000..c4053eaed7 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_view_resultDisplay.js @@ -0,0 +1,354 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that a result has the various elements displayed in the URL bar as + * we expect them to be. + */ + +add_setup(async function () { + await PlacesUtils.history.clear(); + + registerCleanupFunction(async function () { + await PlacesUtils.history.clear(); + Services.prefs.clearUserPref("browser.urlbar.trimURLs"); + }); +}); + +async function testResult(input, expected, index = 1) { + const ESCAPED_URL = encodeURI(input.url); + + await PlacesUtils.history.clear(); + if (index > 0) { + await PlacesTestUtils.addVisits({ + uri: input.url, + title: input.title, + }); + } + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: input.query, + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + Assert.equal(result.url, ESCAPED_URL, "Should have the correct url to load"); + Assert.equal( + result.displayed.url, + expected.displayedUrl, + "Should have the correct displayed url" + ); + Assert.equal( + result.displayed.title, + input.title, + "Should have the expected title" + ); + Assert.equal( + result.displayed.typeIcon, + "none", + "Should not have a type icon" + ); + if (index > 0) { + Assert.equal( + result.image, + `page-icon:${ESCAPED_URL}`, + "Should have the correct favicon" + ); + } + + assertDisplayedHighlights( + "title", + result.element.title, + expected.highlightedTitle + ); + + assertDisplayedHighlights("url", result.element.url, expected.highlightedUrl); +} + +function assertDisplayedHighlights(elementName, element, expectedResults) { + Assert.equal( + element.childNodes.length, + expectedResults.length, + `Should have the correct number of child nodes for ${elementName}` + ); + + for (let i = 0; i < element.childNodes.length; i++) { + let child = element.childNodes[i]; + Assert.equal( + child.textContent, + expectedResults[i][0], + `Should have the correct text for the ${i} part of the ${elementName}` + ); + Assert.equal( + child.nodeName, + expectedResults[i][1] ? "strong" : "#text", + `Should have the correct text/strong status for the ${i} part of the ${elementName}` + ); + } +} + +add_task(async function test_url_result() { + await testResult( + { + query: "\u6e2C\u8a66", + title: "The \u6e2C\u8a66 URL", + url: "https://example.com/\u6e2C\u8a66test", + }, + { + displayedUrl: "example.com/\u6e2C\u8a66test", + highlightedTitle: [ + ["The ", false], + ["\u6e2C\u8a66", true], + [" URL", false], + ], + highlightedUrl: [ + ["example.com/", false], + ["\u6e2C\u8a66", true], + ["test", false], + ], + } + ); +}); + +add_task(async function test_url_result_no_path() { + await testResult( + { + query: "ample", + title: "The Title", + url: "https://example.com/", + }, + { + displayedUrl: "example.com", + highlightedTitle: [["The Title", false]], + highlightedUrl: [ + ["ex", false], + ["ample", true], + [".com", false], + ], + } + ); +}); + +add_task(async function test_url_result_www() { + await testResult( + { + query: "ample", + title: "The Title", + url: "https://www.example.com/", + }, + { + displayedUrl: "example.com", + highlightedTitle: [["The Title", false]], + highlightedUrl: [ + ["ex", false], + ["ample", true], + [".com", false], + ], + } + ); +}); + +add_task(async function test_url_result_no_trimming() { + Services.prefs.setBoolPref("browser.urlbar.trimURLs", false); + + await testResult( + { + query: "\u6e2C\u8a66", + title: "The \u6e2C\u8a66 URL", + url: "http://example.com/\u6e2C\u8a66test", + }, + { + displayedUrl: "http://example.com/\u6e2C\u8a66test", + highlightedTitle: [ + ["The ", false], + ["\u6e2C\u8a66", true], + [" URL", false], + ], + highlightedUrl: [ + ["http://example.com/", false], + ["\u6e2C\u8a66", true], + ["test", false], + ], + } + ); + + Services.prefs.clearUserPref("browser.urlbar.trimURLs"); +}); + +add_task(async function test_case_insensitive_highlights_1() { + await testResult( + { + query: "exam", + title: "The examPLE URL EXAMple", + url: "https://example.com/ExAm", + }, + { + displayedUrl: "example.com/ExAm", + highlightedTitle: [ + ["The ", false], + ["exam", true], + ["PLE URL ", false], + ["EXAM", true], + ["ple", false], + ], + highlightedUrl: [ + ["exam", true], + ["ple.com/", false], + ["ExAm", true], + ], + } + ); +}); + +add_task(async function test_case_insensitive_highlights_2() { + await testResult( + { + query: "EXAM", + title: "The examPLE URL EXAMple", + url: "https://example.com/ExAm", + }, + { + displayedUrl: "example.com/ExAm", + highlightedTitle: [ + ["The ", false], + ["exam", true], + ["PLE URL ", false], + ["EXAM", true], + ["ple", false], + ], + highlightedUrl: [ + ["exam", true], + ["ple.com/", false], + ["ExAm", true], + ], + } + ); +}); + +add_task(async function test_case_insensitive_highlights_3() { + await testResult( + { + query: "eXaM", + title: "The examPLE URL EXAMple", + url: "https://example.com/ExAm", + }, + { + displayedUrl: "example.com/ExAm", + highlightedTitle: [ + ["The ", false], + ["exam", true], + ["PLE URL ", false], + ["EXAM", true], + ["ple", false], + ], + highlightedUrl: [ + ["exam", true], + ["ple.com/", false], + ["ExAm", true], + ], + } + ); +}); + +add_task(async function test_case_insensitive_highlights_4() { + await testResult( + { + query: "ExAm", + title: "The examPLE URL EXAMple", + url: "https://example.com/ExAm", + }, + { + displayedUrl: "example.com/ExAm", + highlightedTitle: [ + ["The ", false], + ["exam", true], + ["PLE URL ", false], + ["EXAM", true], + ["ple", false], + ], + highlightedUrl: [ + ["exam", true], + ["ple.com/", false], + ["ExAm", true], + ], + } + ); +}); + +add_task(async function test_case_insensitive_highlights_5() { + await testResult( + { + query: "exam foo", + title: "The examPLE URL foo EXAMple FOO", + url: "https://example.com/ExAm/fOo", + }, + { + displayedUrl: "example.com/ExAm/fOo", + highlightedTitle: [ + ["The ", false], + ["exam", true], + ["PLE URL ", false], + ["foo", true], + [" ", false], + ["EXAM", true], + ["ple ", false], + ["FOO", true], + ], + highlightedUrl: [ + ["exam", true], + ["ple.com/", false], + ["ExAm", true], + ["/", false], + ["fOo", true], + ], + } + ); +}); + +add_task(async function test_case_insensitive_highlights_6() { + await testResult( + { + query: "EXAM FOO", + title: "The examPLE URL foo EXAMple FOO", + url: "https://example.com/ExAm/fOo", + }, + { + displayedUrl: "example.com/ExAm/fOo", + highlightedTitle: [ + ["The ", false], + ["exam", true], + ["PLE URL ", false], + ["foo", true], + [" ", false], + ["EXAM", true], + ["ple ", false], + ["FOO", true], + ], + highlightedUrl: [ + ["exam", true], + ["ple.com/", false], + ["ExAm", true], + ["/", false], + ["fOo", true], + ], + } + ); +}); + +add_task(async function test_no_highlight_fallback_heuristic_url() { + info("Test unvisited heuristic (fallback provider)"); + await testResult( + { + query: "nonexisting.com", + title: "http://nonexisting.com/", + url: "http://nonexisting.com/", + }, + { + displayedUrl: "", // URL heuristic only has title. + highlightedTitle: [["http://nonexisting.com/", false]], + highlightedUrl: [], + }, + 0 // Test the heuristic result. + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_view_resultTypes_display.js b/browser/components/urlbar/tests/browser/browser_view_resultTypes_display.js new file mode 100644 index 0000000000..c9bd4750f8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_view_resultTypes_display.js @@ -0,0 +1,317 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SyncedTabs } = ChromeUtils.importESModule( + "resource://services-sync/SyncedTabs.sys.mjs" +); + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +function assertElementsDisplayed(details, expected) { + Assert.equal( + details.type, + expected.type, + "Should be displaying a row of the correct type" + ); + Assert.equal( + details.title, + expected.title, + "Should be displaying the correct title" + ); + let separatorVisible = + window.getComputedStyle(details.element.separator).display != "none" && + window.getComputedStyle(details.element.separator).visibility != "collapse"; + Assert.equal( + expected.separator, + separatorVisible, + `Should${expected.separator ? " " : " not "}be displaying a separator` + ); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", false], + // Disable search suggestions in the urlbar. + ["browser.urlbar.suggest.searches", false], + // Clear historical search suggestions to avoid interference from previous + // tests. + ["browser.urlbar.maxHistoricalSearchSuggestions", 0], + // Turn autofill off. + ["browser.urlbar.autoFill", false], + ], + }); + + await SearchTestUtils.promiseNewSearchEngine({ + url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME, + setAsDefault: true, + }); + + // Move the mouse away from the results panel, because hovering a result may + // change its aspect (e.g. by showing a " - search with Engine" suffix). + await EventUtils.promiseNativeMouseEvent({ + type: "mousemove", + target: gURLBar.inputField, + offsetX: 0, + offsetY: 0, + }); +}); + +add_task(async function test_tab_switch_result() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + + await BrowserTestUtils.withNewTab({ gBrowser }, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "about:mozilla", + fireInputEvent: true, + }); + + const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + + assertElementsDisplayed(details, { + separator: true, + title: "about:mozilla", + type: UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + }); + }); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_search_result() { + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", true); + + await BrowserTestUtils.withNewTab({ gBrowser }, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + fireInputEvent: true, + }); + + let index = await UrlbarTestUtils.promiseSuggestionsPresent(window); + const details = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + + // We'll initially display no separator. + assertElementsDisplayed(details, { + separator: false, + title: "foofoo", + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }); + + // Down to select the first search suggestion. + for (let i = index; i > 0; --i) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + + // We should now be displaying one. + assertElementsDisplayed(details, { + separator: true, + title: "foofoo", + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }); + }); + + await PlacesUtils.history.clear(); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); +}); + +add_task(async function test_url_result() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com", + title: "example", + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }, + ]); + + await BrowserTestUtils.withNewTab({ gBrowser }, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example", + fireInputEvent: true, + }); + + const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + + assertElementsDisplayed(details, { + separator: true, + title: "example", + type: UrlbarUtils.RESULT_TYPE.URL, + }); + }); + + await PlacesUtils.history.clear(); +}); + +add_task(async function test_keyword_result() { + const TEST_URL = `${TEST_BASE_URL}print_postdata.sjs`; + + await PlacesUtils.keywords.insert({ + keyword: "get", + url: TEST_URL + "?q=%s", + }); + + await BrowserTestUtils.withNewTab({ gBrowser }, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "get ", + fireInputEvent: true, + }); + + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + // Because only the keyword is typed, we show the bookmark url. + assertElementsDisplayed(details, { + separator: true, + title: TEST_URL.substring("https://".length) + "?q=", + type: UrlbarUtils.RESULT_TYPE.KEYWORD, + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "get test", + fireInputEvent: true, + }); + + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + assertElementsDisplayed(details, { + separator: false, + title: "example.com: test", + type: UrlbarUtils.RESULT_TYPE.KEYWORD, + }); + }); +}); + +add_task(async function test_omnibox_result() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + omnibox: { + keyword: "omniboxtest", + }, + + background() { + /* global browser */ + browser.omnibox.setDefaultSuggestion({ + description: "doit", + }); + // Just do nothing for this test. + browser.omnibox.onInputEntered.addListener(() => {}); + browser.omnibox.onInputChanged.addListener((text, suggest) => { + suggest([]); + }); + }, + }, + }); + + await extension.startup(); + + await BrowserTestUtils.withNewTab({ gBrowser }, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "omniboxtest ", + fireInputEvent: true, + }); + + const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + assertElementsDisplayed(details, { + separator: true, + title: "Generated extension", + type: UrlbarUtils.RESULT_TYPE.OMNIBOX, + }); + }); + + await extension.unload(); +}); + +add_task(async function test_remote_tab_result() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["services.sync.username", "fake"], + ["services.sync.syncedTabs.showRemoteTabs", true], + ], + }); + // Clear history so that history added by previous tests doesn't mess up this + // test when it selects results in the urlbar. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + const REMOTE_TAB = { + id: "7cqCr77ptzX3", + type: "client", + lastModified: 1492201200, + name: "zcarter's Nightly on MacBook-Pro-25", + clientType: "desktop", + tabs: [ + { + type: "tab", + title: "Test Remote", + url: "http://example.com", + icon: UrlbarUtils.ICON.DEFAULT, + client: "7cqCr77ptzX3", + lastUsed: Math.floor(Date.now() / 1000), + }, + ], + }; + + const sandbox = sinon.createSandbox(); + + let originalSyncedTabsInternal = SyncedTabs._internal; + SyncedTabs._internal = { + isConfiguredToSyncTabs: true, + hasSyncedThisSession: true, + getTabClients() { + return Promise.resolve([]); + }, + syncTabs() { + return Promise.resolve(); + }, + }; + + // Tell the Sync XPCOM service it is initialized. + let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + let oldWeaveServiceReady = weaveXPCService.ready; + weaveXPCService.ready = true; + + sandbox + .stub(SyncedTabs._internal, "getTabClients") + .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {}))); + + // Reset internal cache in UrlbarProviderRemoteTabs. + Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs"); + + registerCleanupFunction(async function () { + sandbox.restore(); + weaveXPCService.ready = oldWeaveServiceReady; + SyncedTabs._internal = originalSyncedTabsInternal; + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + Services.telemetry.setEventRecordingEnabled("navigation", false); + }); + + await BrowserTestUtils.withNewTab({ gBrowser }, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example", + fireInputEvent: true, + }); + + const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + + assertElementsDisplayed(details, { + separator: true, + title: "Test Remote", + type: UrlbarUtils.RESULT_TYPE.REMOTE_TAB, + }); + }); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_view_selectionByMouse.js b/browser/components/urlbar/tests/browser/browser_view_selectionByMouse.js new file mode 100644 index 0000000000..fc617220b6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_view_selectionByMouse.js @@ -0,0 +1,567 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test selection on result view by mouse. + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderQuickActions: + "resource:///modules/UrlbarProviderQuickActions.sys.mjs", +}); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.quickactions.enabled", true], + ["browser.urlbar.suggest.quickactions", true], + ["browser.urlbar.shortcuts.quickactions", true], + ], + }); + + UrlbarTestUtils.disableResultMenuAutohide(window); + + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + UrlbarProviderQuickActions.addAction("test-addons", { + commands: ["test-addons"], + label: "quickactions-addons", + onPick: () => + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + "about:about" + ), + }); + UrlbarProviderQuickActions.addAction("test-downloads", { + commands: ["test-downloads"], + label: "quickactions-downloads2", + onPick: () => + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + "about:downloads" + ), + }); + + registerCleanupFunction(function () { + UrlbarProviderQuickActions.removeAction("test-addons"); + UrlbarProviderQuickActions.removeAction("test-downloads"); + }); +}); + +add_task(async function basic() { + const testData = [ + { + description: "Normal result to quick action button", + mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", + mouseup: ".urlbarView-quickaction-button[data-key=test-downloads]", + expected: "about:downloads", + }, + { + description: "Normal result to out of result", + mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", + mouseup: "body", + expected: false, + }, + { + description: "Quick action button to normal result", + mousedown: ".urlbarView-quickaction-button[data-key=test-addons]", + mouseup: ".urlbarView-row:nth-child(1)", + expected: "https://example.com/?q=test", + }, + { + description: "Quick action button to quick action button", + mousedown: ".urlbarView-quickaction-button[data-key=test-addons]", + mouseup: ".urlbarView-quickaction-button[data-key=test-downloads]", + expected: "about:downloads", + }, + { + description: "Quick action button to out of result", + mousedown: ".urlbarView-quickaction-button[data-key=test-downloads]", + mouseup: "body", + expected: false, + }, + ]; + + for (const { description, mousedown, mouseup, expected } of testData) { + info(description); + + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + let [downElement, upElement] = await waitForElements([ + mousedown, + mouseup, + ]); + + EventUtils.synthesizeMouseAtCenter(downElement, { + type: "mousedown", + }); + Assert.ok( + downElement.hasAttribute("selected"), + "Mousedown element should be selected after mousedown" + ); + + if (upElement.tagName === "html:body") { + // We intentionally turn off this a11y check, because the following + // click is sent to test the selection behavior using an alternative way + // of the urlbar dismissal, where other ways are accessible, therefore + // this test can be ignored. + AccessibilityUtils.setEnv({ + mustHaveAccessibleRule: false, + }); + } + EventUtils.synthesizeMouseAtCenter(upElement, { type: "mouseup" }); + AccessibilityUtils.resetEnv(); + Assert.ok( + !downElement.hasAttribute("selected"), + "Mousedown element should not be selected after mouseup" + ); + Assert.ok( + !upElement.hasAttribute("selected"), + "Mouseup element should not be selected after mouseup" + ); + + if (expected) { + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + expected + ); + Assert.ok(true, "Expected page is opened"); + } + }); + } +}); + +add_task(async function outOfBrowser() { + const testData = [ + { + description: "Normal result to out of browser", + mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", + }, + { + description: "Normal result to out of result", + mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", + expected: false, + }, + { + description: "Quick action button to out of browser", + mousedown: ".urlbarView-quickaction-button[data-key=test-addons]", + }, + ]; + + for (const { description, mousedown } of testData) { + info(description); + + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + let [downElement] = await waitForElements([mousedown]); + + EventUtils.synthesizeMouseAtCenter(downElement, { + type: "mousedown", + }); + Assert.ok( + downElement.hasAttribute("selected"), + "Mousedown element should be selected after mousedown" + ); + + // Mouseup at out of browser. + EventUtils.synthesizeMouse(document.documentElement, -1, -1, { + type: "mouseup", + }); + + Assert.ok( + !downElement.hasAttribute("selected"), + "Mousedown element should not be selected after mouseup" + ); + }); + } +}); + +add_task(async function withSelectionByKeyboard() { + const testData = [ + { + description: "Select normal result, then click on out of result", + mousedown: "body", + mouseup: "body", + expected: { + selectedElementByKey: + "#urlbar-results .urlbarView-row > .urlbarView-row-inner[selected]", + selectedElementAfterMouseDown: + "#urlbar-results .urlbarView-row > .urlbarView-row-inner[selected]", + actionedPage: false, + }, + }, + { + description: "Select quick action button, then click on out of result", + arrowDown: 1, + mousedown: "body", + mouseup: "body", + expected: { + selectedElementByKey: + "#urlbar-results .urlbarView-quickaction-button[selected]", + selectedElementAfterMouseDown: + "#urlbar-results .urlbarView-quickaction-button[selected]", + actionedPage: false, + }, + }, + { + description: "Select normal result, then click on about:downloads", + mousedown: ".urlbarView-quickaction-button[data-key=test-downloads]", + mouseup: ".urlbarView-quickaction-button[data-key=test-downloads]", + expected: { + selectedElementByKey: + "#urlbar-results .urlbarView-row > .urlbarView-row-inner[selected]", + selectedElementAfterMouseDown: + ".urlbarView-quickaction-button[data-key=test-downloads]", + actionedPage: "about:downloads", + }, + }, + ]; + + for (const { + description, + arrowDown, + mousedown, + mouseup, + expected, + } of testData) { + info(description); + + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + let [downElement, upElement] = await waitForElements([ + mousedown, + mouseup, + ]); + + if (arrowDown) { + EventUtils.synthesizeKey( + "KEY_ArrowDown", + { repeat: arrowDown }, + window + ); + } + + let [selectedElementByKey] = await waitForElements([ + expected.selectedElementByKey, + ]); + Assert.ok( + selectedElementByKey.hasAttribute("selected"), + "selectedElementByKey should be selected after arrow down" + ); + + EventUtils.synthesizeMouseAtCenter(downElement, { + type: "mousedown", + }); + + if ( + expected.selectedElementByKey !== expected.selectedElementAfterMouseDown + ) { + let [selectedElementAfterMouseDown] = await waitForElements([ + expected.selectedElementAfterMouseDown, + ]); + Assert.ok( + selectedElementAfterMouseDown.hasAttribute("selected"), + "selectedElementAfterMouseDown should be selected after mousedown" + ); + Assert.ok( + !selectedElementByKey.hasAttribute("selected"), + "selectedElementByKey should not be selected after mousedown" + ); + } + + if (upElement.tagName === "html:body") { + // We intentionally turn off this a11y check, because the following + // click is sent to test the selection behavior using an alternative way + // of the urlbar dismissal, where other ways are accessible, therefore + // this test can be ignored. + AccessibilityUtils.setEnv({ + mustHaveAccessibleRule: false, + }); + } + EventUtils.synthesizeMouseAtCenter(upElement, { + type: "mouseup", + }); + AccessibilityUtils.resetEnv(); + + if (expected.actionedPage) { + Assert.ok( + !selectedElementByKey.hasAttribute("selected"), + "selectedElementByKey should not be selected after page starts load" + ); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + expected.actionedPage + ); + Assert.ok(true, "Expected page is opened"); + } else { + Assert.ok( + selectedElementByKey.hasAttribute("selected"), + "selectedElementByKey should remain selected" + ); + } + }); + } +}); + +add_task(async function withDnsFirstForSingleWordsPref() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.fixup.dns_first_for_single_words", true]], + }); + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "https://example.org/", + title: "example", + }); + await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "ex", + window, + }); + + const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + const target = details.element.action; + EventUtils.synthesizeMouseAtCenter(target, { type: "mousedown" }); + const onLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "https://example.org/" + ); + EventUtils.synthesizeMouseAtCenter(target, { type: "mouseup" }); + await onLoaded; + Assert.ok(true, "Expected page is opened"); + + await PlacesUtils.bookmarks.eraseEverything(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function buttons() { + let initialTabUrl = "https://example.com/initial"; + let mainResultUrl = "https://example.com/main"; + let mainResultHelpUrl = "https://example.com/help"; + let otherResultUrl = "https://example.com/other"; + + let searchString = "test"; + + // Add a provider with two results: The first has buttons and the second has a + // URL that should or shouldn't become the input's value when the block button + // in the first result is clicked, depending on the test. + let provider = new UrlbarTestUtils.TestProvider({ + priority: Infinity, + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url: mainResultUrl, + helpUrl: mainResultHelpUrl, + helpL10n: { + id: "urlbar-result-menu-learn-more-about-firefox-suggest", + }, + isBlockable: true, + blockL10n: { + id: "urlbar-result-menu-dismiss-firefox-suggest", + }, + } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url: otherResultUrl, + } + ), + ], + }); + + UrlbarProvidersManager.registerProvider(provider); + + let assertResultMenuOpen = () => { + Assert.equal( + gURLBar.view.resultMenu.state, + "showing", + "Result menu is showing" + ); + EventUtils.synthesizeKey("KEY_Escape"); + }; + + let testData = [ + { + description: "Menu button to menu button", + mousedown: ".urlbarView-row:nth-child(1) .urlbarView-button-menu", + afterMouseupCallback: assertResultMenuOpen, + expected: { + mousedownSelected: false, + topSites: { + pageProxyState: "valid", + value: initialTabUrl, + }, + searchString: { + pageProxyState: "invalid", + value: searchString, + }, + }, + }, + { + description: "Row-inner to menu button", + mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", + mouseup: ".urlbarView-row:nth-child(1) .urlbarView-button-menu", + afterMouseupCallback: assertResultMenuOpen, + expected: { + mousedownSelected: true, + topSites: { + pageProxyState: "valid", + value: initialTabUrl, + }, + searchString: { + pageProxyState: "invalid", + value: searchString, + }, + }, + }, + { + description: "Menu button to row-inner", + mousedown: ".urlbarView-row:nth-child(1) .urlbarView-button-menu", + mouseup: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", + expected: { + mousedownSelected: false, + url: mainResultUrl, + newTab: false, + }, + }, + ]; + + for (let showTopSites of [true, false]) { + for (let { + description, + mousedown, + mouseup, + expected, + afterMouseupCallback = null, + } of testData) { + info(`Running test with showTopSites = ${showTopSites}: ${description}`); + mouseup ||= mousedown; + + await BrowserTestUtils.withNewTab(initialTabUrl, async () => { + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Sanity check: pageproxystate should be valid initially" + ); + Assert.equal( + gURLBar.value, + UrlbarTestUtils.trimURL(initialTabUrl), + "Sanity check: input.value should be the initial URL initially" + ); + + if (showTopSites) { + // Open the view and show top sites by performing the accel+L command. + await SimpleTest.promiseFocus(window); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + document.getElementById("Browser:OpenLocation").doCommand(); + await searchPromise; + } else { + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + }); + } + + let [downElement, upElement] = await waitForElements([ + mousedown, + mouseup, + ]); + + // Mousedown and check the selection. + EventUtils.synthesizeMouseAtCenter(downElement, { + type: "mousedown", + }); + if (expected.mousedownSelected) { + Assert.ok( + downElement.hasAttribute("selected"), + "Mousedown element should be selected after mousedown" + ); + } else { + Assert.ok( + !downElement.hasAttribute("selected"), + "Mousedown element should not be selected after mousedown" + ); + } + + let loadPromise; + if (expected.url) { + loadPromise = expected.newTab + ? BrowserTestUtils.waitForNewTab(gBrowser, expected.url) + : BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + null, + expected.url + ); + } + + // Mouseup and check the selection. + EventUtils.synthesizeMouseAtCenter(upElement, { type: "mouseup" }); + Assert.ok( + !downElement.hasAttribute("selected"), + "Mousedown element should not be selected after mouseup" + ); + Assert.ok( + !upElement.hasAttribute("selected"), + "Mouseup element should not be selected after mouseup" + ); + + // If we expect a URL to load, we're done since the view will close and + // the input value will be set to the URL. + if (loadPromise) { + info("Waiting for URL to load: " + expected.url); + let tab = await loadPromise; + Assert.ok(true, "Expected URL loaded"); + if (expected.newTab) { + BrowserTestUtils.removeTab(tab); + } + return; + } + + if (afterMouseupCallback) { + await afterMouseupCallback(); + } + + let state = showTopSites ? expected.topSites : expected.searchString; + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + state.pageProxyState, + "pageproxystate should be as expected" + ); + Assert.equal( + gURLBar.value, + UrlbarTestUtils.trimURL(state.value), + "input.value should be as expected" + ); + }); + } + } + + UrlbarProvidersManager.unregisterProvider(provider); +}); + +async function waitForElements(selectors) { + let elements; + await BrowserTestUtils.waitForCondition(() => { + elements = selectors.map(s => document.querySelector(s)); + return elements.every(e => e && BrowserTestUtils.isVisible(e)); + }, "Waiting for elements to become visible: " + JSON.stringify(selectors)); + return elements; +} diff --git a/browser/components/urlbar/tests/browser/browser_waitForLoadStartOrTimeout.js b/browser/components/urlbar/tests/browser/browser_waitForLoadStartOrTimeout.js new file mode 100644 index 0000000000..0fc6f0739f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_waitForLoadStartOrTimeout.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the waitForLoadStartOrTimeout test helper function in head.js. + */ + +"use strict"; + +add_task(async function load() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + let url = "https://example.com/"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: url, + }); + + let loadPromise = waitForLoadStartOrTimeout(); + EventUtils.synthesizeKey("KEY_Enter"); + let uri = await loadPromise; + info("Page should have loaded before timeout"); + + Assert.equal(uri.spec, url, "example.com should have loaded"); + }); +}); + +add_task(async function timeout() { + await Assert.rejects( + waitForLoadStartOrTimeout(), + /timed out/, + "Should have timed out" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_whereToOpen.js b/browser/components/urlbar/tests/browser/browser_whereToOpen.js new file mode 100644 index 0000000000..339a20d90e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_whereToOpen.js @@ -0,0 +1,192 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const NON_EMPTY_TAB = "example.com/non-empty"; +const EMPTY_TAB = "about:blank"; +const META_KEY = AppConstants.platform == "macosx" ? "metaKey" : "ctrlKey"; +const ENTER = new KeyboardEvent("keydown", {}); +const ALT_ENTER = new KeyboardEvent("keydown", { altKey: true }); +const ALTGR_ENTER = new KeyboardEvent("keydown", { modifierAltGraph: true }); +const CLICK = new MouseEvent("click", { button: 0 }); +const META_CLICK = new MouseEvent("click", { button: 0, [META_KEY]: true }); +const MIDDLE_CLICK = new MouseEvent("click", { button: 1 }); + +let old_openintab = Preferences.get("browser.urlbar.openintab"); +registerCleanupFunction(async function () { + Preferences.set("browser.urlbar.openintab", old_openintab); +}); + +add_task(async function openInTab() { + // Open a non-empty tab. + let tab = (gBrowser.selectedTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + NON_EMPTY_TAB + )); + + for (let test of [ + { + pref: false, + event: ALT_ENTER, + desc: "Alt+Enter, non-empty tab, default prefs", + }, + { + pref: false, + event: ALTGR_ENTER, + desc: "AltGr+Enter, non-empty tab, default prefs", + }, + { + pref: false, + event: META_CLICK, + desc: "Meta+click, non-empty tab, default prefs", + }, + { + pref: false, + event: MIDDLE_CLICK, + desc: "Middle click, non-empty tab, default prefs", + }, + { pref: true, event: ENTER, desc: "Enter, non-empty tab, openInTab" }, + { + pref: true, + event: CLICK, + desc: "Normal click, non-empty tab, openInTab", + }, + ]) { + info(test.desc); + + Preferences.set("browser.urlbar.openintab", test.pref); + let where = gURLBar._whereToOpen(test.event); + is(where, "tab", "URL would be loaded in a new tab"); + } + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function keepEmptyTab() { + // Open an empty tab. + let tab = (gBrowser.selectedTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + EMPTY_TAB + )); + + for (let test of [ + { + pref: false, + event: META_CLICK, + desc: "Meta+click, empty tab, default prefs", + }, + { + pref: false, + event: MIDDLE_CLICK, + desc: "Middle click, empty tab, default prefs", + }, + ]) { + info(test.desc); + + Preferences.set("browser.urlbar.openintab", test.pref); + let where = gURLBar._whereToOpen(test.event); + is(where, "tab", "URL would be loaded in a new tab"); + } + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function reuseEmptyTab() { + // Open an empty tab. + let tab = (gBrowser.selectedTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + EMPTY_TAB + )); + + for (let test of [ + { + pref: false, + event: ALT_ENTER, + desc: "Alt+Enter, empty tab, default prefs", + }, + { + pref: false, + event: ALTGR_ENTER, + desc: "AltGr+Enter, empty tab, default prefs", + }, + { pref: true, event: ENTER, desc: "Enter, empty tab, openInTab" }, + { pref: true, event: CLICK, desc: "Normal click, empty tab, openInTab" }, + ]) { + info(test.desc); + Preferences.set("browser.urlbar.openintab", test.pref); + let where = gURLBar._whereToOpen(test.event); + is(where, "current", "New URL would reuse the current empty tab"); + } + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function openInCurrentTab() { + for (let test of [ + { + pref: false, + url: NON_EMPTY_TAB, + event: ENTER, + desc: "Enter, non-empty tab, default prefs", + }, + { + pref: false, + url: NON_EMPTY_TAB, + event: CLICK, + desc: "Normal click, non-empty tab, default prefs", + }, + { + pref: false, + url: EMPTY_TAB, + event: ENTER, + desc: "Enter, empty tab, default prefs", + }, + { + pref: false, + url: EMPTY_TAB, + event: CLICK, + desc: "Normal click, empty tab, default prefs", + }, + { + pref: true, + url: NON_EMPTY_TAB, + event: ALT_ENTER, + desc: "Alt+Enter, non-empty tab, openInTab", + }, + { + pref: true, + url: NON_EMPTY_TAB, + event: ALTGR_ENTER, + desc: "AltGr+Enter, non-empty tab, openInTab", + }, + { + pref: true, + url: NON_EMPTY_TAB, + event: META_CLICK, + desc: "Meta+click, non-empty tab, openInTab", + }, + { + pref: true, + url: NON_EMPTY_TAB, + event: MIDDLE_CLICK, + desc: "Middle click, non-empty tab, openInTab", + }, + ]) { + info(test.desc); + + // Open a new tab. + let tab = (gBrowser.selectedTab = + await BrowserTestUtils.openNewForegroundTab(gBrowser, test.url)); + + Preferences.set("browser.urlbar.openintab", test.pref); + let where = gURLBar._whereToOpen(test.event); + is(where, "current", "URL would open in the current tab"); + + // Clean up. + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/components/urlbar/tests/browser/dummy_page.html b/browser/components/urlbar/tests/browser/dummy_page.html new file mode 100644 index 0000000000..1a87e28408 --- /dev/null +++ b/browser/components/urlbar/tests/browser/dummy_page.html @@ -0,0 +1,9 @@ +<html> +<head> +<title>Dummy test page</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> +<p>Dummy test page</p> +</body> +</html> diff --git a/browser/components/urlbar/tests/browser/dynamicResult0.css b/browser/components/urlbar/tests/browser/dynamicResult0.css new file mode 100644 index 0000000000..328127b594 --- /dev/null +++ b/browser/components/urlbar/tests/browser/dynamicResult0.css @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +#urlbar { + --testDynamicResult0: ok0; +} + +.urlbarView-row[dynamicType=test] > .urlbarView-row-inner { + display: flex; + align-items: center; + min-height: 32px; + width: 100%; +} + +.urlbarView-dynamic-test-buttonBox { + display: flex; + align-items: center; + min-height: 32px; +} + +.urlbarView-dynamic-test-text { + flex-grow: 1; + flex-shrink: 1; + padding: 10px; +} + +.urlbarView-dynamic-test-selectable, +.urlbarView-dynamic-test-button1, +.urlbarView-dynamic-test-button2 { + min-height: 16px; + padding: 8px; + border: none; + border-radius: 2px; + font-size: 0.93em; + color: inherit; + background-color: var(--urlbarView-button-background); + min-width: 8.75em; + text-align: center; + flex-basis: initial; + flex-shrink: 0; + margin-inline-end: 10px; +} + +.urlbarView-dynamic-test-selectable[selected], +.urlbarView-dynamic-test-button1[selected], +.urlbarView-dynamic-test-button2[selected] { + color: white; + background-color: var(--urlbarView-primary-button-background); + box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3); +} diff --git a/browser/components/urlbar/tests/browser/dynamicResult1.css b/browser/components/urlbar/tests/browser/dynamicResult1.css new file mode 100644 index 0000000000..ae43fd3f9a --- /dev/null +++ b/browser/components/urlbar/tests/browser/dynamicResult1.css @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +#urlbar { + --testDynamicResult1: ok1; +} + +.urlbarView-row[dynamicType=test] > .urlbarView-row-inner { + display: flex; + align-items: center; + min-height: 32px; + width: 100%; +} + +.urlbarView-dynamic-test-buttonBox { + display: flex; + align-items: center; + min-height: 32px; +} + +.urlbarView-dynamic-test-text { + flex-grow: 1; + flex-shrink: 1; + padding: 10px; +} + +.urlbarView-dynamic-test-selectable, +.urlbarView-dynamic-test-button1, +.urlbarView-dynamic-test-button2 { + min-height: 16px; + padding: 8px; + border: none; + border-radius: 2px; + font-size: 0.93em; + color: inherit; + background-color: var(--urlbarView-button-background); + min-width: 8.75em; + text-align: center; + flex-basis: initial; + flex-shrink: 0; + margin-inline-end: 10px; +} + +.urlbarView-dynamic-test-selectable[selected], +.urlbarView-dynamic-test-button1[selected], +.urlbarView-dynamic-test-button2[selected] { + color: white; + background-color: var(--urlbarView-primary-button-background); + box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3); +} diff --git a/browser/components/urlbar/tests/browser/file_blank_but_not_blank.html b/browser/components/urlbar/tests/browser/file_blank_but_not_blank.html new file mode 100644 index 0000000000..1f5fea8dcf --- /dev/null +++ b/browser/components/urlbar/tests/browser/file_blank_but_not_blank.html @@ -0,0 +1,2 @@ +<script>var q = "1";</script> +<a href="javascript:q">Click me</a> diff --git a/browser/components/urlbar/tests/browser/file_copying_home.html b/browser/components/urlbar/tests/browser/file_copying_home.html new file mode 100644 index 0000000000..7aaafc26af --- /dev/null +++ b/browser/components/urlbar/tests/browser/file_copying_home.html @@ -0,0 +1 @@ +<a href="wait-a-bit.sjs" target="_blank">wait-a-bit.sjs</a> diff --git a/browser/components/urlbar/tests/browser/file_urlbar_edit_dos.html b/browser/components/urlbar/tests/browser/file_urlbar_edit_dos.html new file mode 100644 index 0000000000..e02242f6a1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/file_urlbar_edit_dos.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<title>Try editing the URL bar</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> +<script> +function dos_hash() { + location.hash = "#"; +} + +function dos_pushState() { + history.pushState({}, "Some title", ""); +} +</script> +</body> +</html> diff --git a/browser/components/urlbar/tests/browser/file_userTypedValue.html b/browser/components/urlbar/tests/browser/file_userTypedValue.html new file mode 100644 index 0000000000..a787b70898 --- /dev/null +++ b/browser/components/urlbar/tests/browser/file_userTypedValue.html @@ -0,0 +1 @@ +<html><body>bug562649</body></html> diff --git a/browser/components/urlbar/tests/browser/head-common.js b/browser/components/urlbar/tests/browser/head-common.js new file mode 100644 index 0000000000..2119d33123 --- /dev/null +++ b/browser/components/urlbar/tests/browser/head-common.js @@ -0,0 +1,153 @@ +ChromeUtils.defineESModuleGetters(this, { + HttpServer: "resource://testing-common/httpd.sys.mjs", + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + Preferences: "resource://gre/modules/Preferences.sys.mjs", + UrlbarProvider: "resource:///modules/UrlbarUtils.sys.mjs", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", + UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs", + UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(this, "TEST_BASE_URL", () => + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "clipboardHelper", + "@mozilla.org/widget/clipboardhelper;1", + "nsIClipboardHelper" +); + +ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => { + const { UrlbarTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +ChromeUtils.defineLazyGetter(this, "SearchTestUtils", () => { + const { SearchTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +/** + * Initializes an HTTP Server, and runs a task with it. + * + * @param {object} details {scheme, host, port} + * @param {Function} taskFn The task to run, gets the server as argument. + */ +async function withHttpServer( + details = { scheme: "http", host: "localhost", port: -1 }, + taskFn +) { + let server = new HttpServer(); + let url = `${details.scheme}://${details.host}:${details.port}`; + try { + info(`starting HTTP Server for ${url}`); + try { + server.start(details.port); + details.port = server.identity.primaryPort; + server.identity.setPrimary(details.scheme, details.host, details.port); + } catch (ex) { + throw new Error("We can't launch our http server successfully. " + ex); + } + Assert.ok( + server.identity.has(details.scheme, details.host, details.port), + `${url} is listening.` + ); + try { + await taskFn(server); + } catch (ex) { + throw new Error("Exception in the task function " + ex); + } + } finally { + server.identity.remove(details.scheme, details.host, details.port); + try { + await new Promise(resolve => server.stop(resolve)); + } catch (ex) {} + server = null; + } +} + +/** + * Updates the Top Sites feed. + * + * @param {Function} condition + * A callback that returns true after Top Sites are successfully updated. + * @param {boolean} searchShortcuts + * True if Top Sites search shortcuts should be enabled. + */ +async function updateTopSites(condition, searchShortcuts = false) { + // Toggle the pref to clear the feed cache and force an update. + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.newtabpage.activity-stream.discoverystream.endpointSpocsClear", + "", + ], + ["browser.newtabpage.activity-stream.feeds.system.topsites", false], + ["browser.newtabpage.activity-stream.feeds.system.topsites", true], + [ + "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts", + searchShortcuts, + ], + ], + }); + + // Wait for the feed to be updated. + await TestUtils.waitForCondition(() => { + let sites = AboutNewTab.getTopSites(); + return condition(sites); + }, "Waiting for top sites to be updated"); +} + +/** + * Asserts a search term is in the url bar and state values are + * what they should be. + * + * @param {string} searchString + * String that should be matched in the url bar. + * @param {object | null} options + * Options for the assertions. + * @param {Window | null} options.window + * Window to use for tests. + * @param {string | null} options.pageProxyState + * The pageproxystate that should be expected. Defaults to "valid". + * @param {string | null} options.userTypedValue + * The userTypedValue that should be expected. Defaults to null. + */ +function assertSearchStringIsInUrlbar( + searchString, + { win = window, pageProxyState = "valid", userTypedValue = null } = {} +) { + Assert.equal( + win.gURLBar.value, + searchString, + `Search string should be the urlbar value.` + ); + Assert.equal( + win.gBrowser.selectedBrowser.searchTerms, + searchString, + `Search terms should match.` + ); + Assert.equal( + win.gBrowser.userTypedValue, + userTypedValue, + "userTypedValue should match." + ); + Assert.equal( + win.gURLBar.getAttribute("pageproxystate"), + pageProxyState, + "Pageproxystate should match." + ); +} diff --git a/browser/components/urlbar/tests/browser/head.js b/browser/components/urlbar/tests/browser/head.js new file mode 100644 index 0000000000..a81e8e4811 --- /dev/null +++ b/browser/components/urlbar/tests/browser/head.js @@ -0,0 +1,248 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", + ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", + ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs", + ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", + PromptTestUtils: "resource://testing-common/PromptTestUtils.sys.mjs", + ResetProfile: "resource://gre/modules/ResetProfile.sys.mjs", + SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", + UrlbarController: "resource:///modules/UrlbarController.sys.mjs", + UrlbarEventBufferer: "resource:///modules/UrlbarEventBufferer.sys.mjs", + UrlbarQueryContext: "resource:///modules/UrlbarUtils.sys.mjs", + UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs", + UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", + UrlbarView: "resource:///modules/UrlbarView.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(this, "PlacesFrecencyRecalculator", () => { + return Cc["@mozilla.org/places/frecency-recalculator;1"].getService( + Ci.nsIObserver + ).wrappedJSObject; +}); + +let sandbox; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/head-common.js", + this +); + +registerCleanupFunction(async () => { + // Ensure the Urlbar popup is always closed at the end of a test, to save having + // to do it within each test. + await UrlbarTestUtils.promisePopupClose(window); +}); + +async function selectAndPaste(str, win = window) { + await SimpleTest.promiseClipboardChange(str, () => { + clipboardHelper.copyString(str); + }); + win.gURLBar.select(); + win.document.commandDispatcher + .getControllerForCommand("cmd_paste") + .doCommand("cmd_paste"); +} + +/** + * Waits for a load starting in any browser or a timeout, whichever comes first. + * + * @param {window} win + * The top-level browser window to listen in. + * @param {number} timeoutMs + * The timeout in ms. + * @returns {Promise} resolved to the loading uri in case of load, rejected in + * case of timeout. + */ +function waitForLoadStartOrTimeout(win = window, timeoutMs = 1000) { + let listener; + let timeout; + return Promise.race([ + new Promise(resolve => { + listener = { + onStateChange(browser, webprogress, request, flags, status) { + if (flags & Ci.nsIWebProgressListener.STATE_START) { + resolve(request.QueryInterface(Ci.nsIChannel).URI); + } + }, + }; + win.gBrowser.addTabsProgressListener(listener); + }), + new Promise((resolve, reject) => { + timeout = win.setTimeout(() => reject("timed out"), timeoutMs); + }), + ]).finally(() => { + win.gBrowser.removeTabsProgressListener(listener); + win.clearTimeout(timeout); + }); +} + +/** + * Opens the url bar context menu by synthesizing a click. + * Returns a menu item that is specified by an id. + * + * @param {string} anonid - Identifier of a menu item of the url bar context menu. + * @returns {string} - The element that has the corresponding identifier. + */ +async function promiseContextualMenuitem(anonid) { + let textBox = gURLBar.querySelector("moz-input-box"); + let cxmenu = textBox.menupopup; + let cxmenuPromise = BrowserTestUtils.waitForEvent(cxmenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, { + type: "contextmenu", + button: 2, + }); + await cxmenuPromise; + return textBox.getMenuItem(anonid); +} + +/** + * Puts all CustomizableUI widgetry back to their default locations, and + * then fires the `aftercustomization` toolbox event so that UrlbarInput + * knows to reinitialize itself. + * + * @param {window} [win=window] + * The top-level browser window to fire the `aftercustomization` event in. + */ +function resetCUIAndReinitUrlbarInput(win = window) { + CustomizableUI.reset(); + CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, win); +} + +/** + * This function does the following: + * + * 1. Starts a search with `searchString` but doesn't wait for it to complete. + * 2. Compares the input value to `valueBefore`. If anything is autofilled at + * this point, it will be due to the placeholder. + * 3. Waits for the search to complete. + * 4. Compares the input value to `valueAfter`. If anything is autofilled at + * this point, it will be due to the autofill result fetched by the search. + * 5. Compares the placeholder to `placeholderAfter`. + * + * @param {object} options + * The options object. + * @param {string} options.searchString + * The search string. + * @param {string} options.valueBefore + * The expected input value before the search completes. + * @param {string} options.valueAfter + * The expected input value after the search completes. + * @param {string} options.placeholderAfter + * The expected placeholder value after the search completes. + * @returns {Promise} + */ +async function search({ + searchString, + valueBefore, + valueAfter, + placeholderAfter, +}) { + info( + "Searching: " + + JSON.stringify({ + searchString, + valueBefore, + valueAfter, + placeholderAfter, + }) + ); + + await SimpleTest.promiseFocus(window); + gURLBar.inputField.focus(); + + // Set the input value and move the caret to the end to simulate the user + // typing. It's important the caret is at the end because otherwise autofill + // won't happen. + gURLBar.value = searchString; + gURLBar.inputField.setSelectionRange( + searchString.length, + searchString.length + ); + + // Placeholder autofill is done on input, so fire an input event. We can't use + // `promiseAutocompleteResultPopup()` or other helpers that wait for the + // search to complete because we are specifically checking placeholder + // autofill before the search completes. + UrlbarTestUtils.fireInputEvent(window); + + // Subtract the protocol length, when the searchString contains the https:// + // protocol and trimHttps is enabled. + let trimmedProtocolWSlashes = UrlbarTestUtils.getTrimmedProtocolWithSlashes(); + let selectionOffset = searchString.includes(trimmedProtocolWSlashes) + ? trimmedProtocolWSlashes.length + : 0; + + // Check the input value and selection immediately, before waiting on the + // search to complete. + Assert.equal( + gURLBar.value, + UrlbarTestUtils.trimURL(valueBefore), + "gURLBar.value before the search completes" + ); + Assert.equal( + gURLBar.selectionStart, + searchString.length - selectionOffset, + "gURLBar.selectionStart before the search completes" + ); + Assert.equal( + gURLBar.selectionEnd, + valueBefore.length - selectionOffset, + "gURLBar.selectionEnd before the search completes" + ); + + // Wait for the search to complete. + info("Waiting for the search to complete"); + await UrlbarTestUtils.promiseSearchComplete(window); + + // Check the final value after the results arrived. + Assert.equal( + gURLBar.value, + UrlbarTestUtils.trimURL(valueAfter), + "gURLBar.value after the search completes" + ); + Assert.equal( + gURLBar.selectionStart, + searchString.length - selectionOffset, + "gURLBar.selectionStart after the search completes" + ); + Assert.equal( + gURLBar.selectionEnd, + valueAfter.length - selectionOffset, + "gURLBar.selectionEnd after the search completes" + ); + + // Check the placeholder. + if (placeholderAfter) { + Assert.ok( + gURLBar._autofillPlaceholder, + "gURLBar._autofillPlaceholder exists after the search completes" + ); + Assert.strictEqual( + gURLBar._autofillPlaceholder.value, + UrlbarTestUtils.trimURL(placeholderAfter), + "gURLBar._autofillPlaceholder.value after the search completes" + ); + } else { + Assert.strictEqual( + gURLBar._autofillPlaceholder, + null, + "gURLBar._autofillPlaceholder does not exist after the search completes" + ); + } + + // Check the first result. + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + !!details.autofill, + !!placeholderAfter, + "First result is an autofill result iff a placeholder is expected" + ); +} diff --git a/browser/components/urlbar/tests/browser/mixed_active.html b/browser/components/urlbar/tests/browser/mixed_active.html new file mode 100644 index 0000000000..4ce8e78dc4 --- /dev/null +++ b/browser/components/urlbar/tests/browser/mixed_active.html @@ -0,0 +1,14 @@ +<!-- 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/. --> + +<!DOCTYPE html> +<html> + <head> + <meta charset="utf8" /> + <title>Mixed Active Content test</title> + </head> + <body> + <iframe style="visibility: hidden" src="http://example.com"></iframe> + </body> +</html> diff --git a/browser/components/urlbar/tests/browser/moz.png b/browser/components/urlbar/tests/browser/moz.png Binary files differnew file mode 100644 index 0000000000..769c636340 --- /dev/null +++ b/browser/components/urlbar/tests/browser/moz.png diff --git a/browser/components/urlbar/tests/browser/print_postdata.sjs b/browser/components/urlbar/tests/browser/print_postdata.sjs new file mode 100644 index 0000000000..5884a1d598 --- /dev/null +++ b/browser/components/urlbar/tests/browser/print_postdata.sjs @@ -0,0 +1,25 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain", false); + if (request.method == "GET") { + response.write(request.queryString); + } else { + let body = new BinaryInputStream(request.bodyInputStream); + + let avail; + let bytes = []; + + while ((avail = body.available()) > 0) { + Array.prototype.push.apply(bytes, body.readByteArray(avail)); + } + + let data = String.fromCharCode.apply(null, bytes); + response.bodyOutputStream.write(data, data.length); + } +} diff --git a/browser/components/urlbar/tests/browser/redirect_error.sjs b/browser/components/urlbar/tests/browser/redirect_error.sjs new file mode 100644 index 0000000000..a3937b0e7a --- /dev/null +++ b/browser/components/urlbar/tests/browser/redirect_error.sjs @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const REDIRECT_TO = "https://www.bank1.com/"; // Bad-cert host. + +function handleRequest(aRequest, aResponse) { + // Set HTTP Status + aResponse.setStatusLine(aRequest.httpVersion, 301, "Moved Permanently"); + + // Set redirect URI, mirroring the hash value. + let hash = /\#.+/.test(aRequest.path) + ? "#" + aRequest.path.split("#")[1] + : ""; + aResponse.setHeader("Location", REDIRECT_TO + hash); +} diff --git a/browser/components/urlbar/tests/browser/redirect_to.sjs b/browser/components/urlbar/tests/browser/redirect_to.sjs new file mode 100644 index 0000000000..b52ebdc63e --- /dev/null +++ b/browser/components/urlbar/tests/browser/redirect_to.sjs @@ -0,0 +1,9 @@ +"use strict"; + +function handleRequest(request, response) { + // redirect_to.sjs?ctxmenu-image.png + // redirects to : ctxmenu-image.png + const redirectUrl = request.queryString; + response.setStatusLine(request.httpVersion, "302", "Found"); + response.setHeader("Location", redirectUrl, false); +} diff --git a/browser/components/urlbar/tests/browser/search-engines/basic/manifest.json b/browser/components/urlbar/tests/browser/search-engines/basic/manifest.json new file mode 100644 index 0000000000..d66c1ed3d8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/search-engines/basic/manifest.json @@ -0,0 +1,20 @@ +{ + "name": "basic", + "manifest_version": 2, + "version": "1.0", + "description": "basic", + "browser_specific_settings": { + "gecko": { + "id": "basic@search.mozilla.org" + } + }, + "hidden": true, + "chrome_settings_overrides": { + "search_provider": { + "name": "basic", + "keyword": "@basic", + "search_url": "https://example.com/?search={searchTerms}&foo=1", + "suggest_url": "https://example.com/browser/browser/components/search/test/browser/trendingSuggestionEngine.sjs?richsuggestions=true&query={searchTerms}" + } + } +} diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs b/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs new file mode 100644 index 0000000000..145392fcf2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +let gTimer; + +function handleRequest(req, resp) { + // Parse the query params. If the params aren't in the form "foo=bar", then + // treat the entire query string as a search string. + let params = req.queryString.split("&").reduce((memo, pair) => { + let [key, val] = pair.split("="); + if (!val) { + // This part isn't in the form "foo=bar". Treat it as the search string + // (the "query"). + val = key; + key = "query"; + } + memo[decode(key)] = decode(val); + return memo; + }, {}); + + let timeout = parseInt(params.timeout); + if (timeout) { + // Write the response after a timeout. + resp.processAsync(); + gTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + gTimer.init( + () => { + writeResponse(params, resp); + resp.finish(); + }, + timeout, + Ci.nsITimer.TYPE_ONE_SHOT + ); + return; + } + + writeResponse(params, resp); +} + +function writeResponse(params, resp) { + // Echo back the search string with "foo" and "bar" appended. + let suffixes = ["foo", "bar"]; + if (params.count) { + // Add more suffixes. + let serial = 0; + while (suffixes.length < params.count) { + suffixes.push(++serial); + } + } + let data = [params.query, suffixes.map(s => params.query + s)]; + resp.setHeader("Content-Type", "application/json", false); + resp.write(JSON.stringify(data)); +} + +function decode(str) { + return decodeURIComponent(str.replace(/\+/g, encodeURIComponent(" "))); +} diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngine.xml b/browser/components/urlbar/tests/browser/searchSuggestionEngine.xml new file mode 100644 index 0000000000..142c91849c --- /dev/null +++ b/browser/components/urlbar/tests/browser/searchSuggestionEngine.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>browser_searchSuggestionEngine searchSuggestionEngine.xml</ShortName> +<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?{searchTerms}"/> +<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform"> + <Param name="terms" value="{searchTerms}"/> +</Url> +</SearchPlugin> diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngine2.xml b/browser/components/urlbar/tests/browser/searchSuggestionEngine2.xml new file mode 100644 index 0000000000..565aaf2bc0 --- /dev/null +++ b/browser/components/urlbar/tests/browser/searchSuggestionEngine2.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>browser_searchSuggestionEngine2 searchSuggestionEngine2.xml</ShortName> +<!-- Redirect the actual search request to the test-server because of proxy restriction --> +<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?{searchTerms}"/> +<!-- Redirect speculative connect to a local http server we run for this test --> +<Url type="text/html" method="GET" template="http://localhost:20709/" rel="searchform"> + <Param name="terms" value="{searchTerms}"/> +</Url> +</SearchPlugin> diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngineMany.xml b/browser/components/urlbar/tests/browser/searchSuggestionEngineMany.xml new file mode 100644 index 0000000000..7e77e32029 --- /dev/null +++ b/browser/components/urlbar/tests/browser/searchSuggestionEngineMany.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>browser_searchSuggestionEngineMany searchSuggestionEngineMany.xml</ShortName> +<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?{searchTerms}&count=10"/> +<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform"> + <Param name="terms" value="{searchTerms}"/> +</Url> +</SearchPlugin> diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngineSlow.xml b/browser/components/urlbar/tests/browser/searchSuggestionEngineSlow.xml new file mode 100644 index 0000000000..e7214e65cc --- /dev/null +++ b/browser/components/urlbar/tests/browser/searchSuggestionEngineSlow.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>searchSuggestionEngineSlow.xml</ShortName> +<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?query={searchTerms}&timeout=3000"/> +<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform"> + <Param name="terms" value="{searchTerms}"/> +</Url> +</SearchPlugin> diff --git a/browser/components/urlbar/tests/browser/slow-page.sjs b/browser/components/urlbar/tests/browser/slow-page.sjs new file mode 100644 index 0000000000..ce9a759744 --- /dev/null +++ b/browser/components/urlbar/tests/browser/slow-page.sjs @@ -0,0 +1,23 @@ +"use strict"; + +let timer; + +const DELAY_MS = 5000; +function handleRequest(request, response) { + if (request.queryString.endsWith("faster")) { + response.setHeader("Content-Type", "text/html", false); + response.write("<body>Not so slow!</body>"); + return; + } + response.processAsync(); + timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.init( + () => { + response.setHeader("Content-Type", "text/html", false); + response.write("<body>This is a slow loading page.</body>"); + response.finish(); + }, + DELAY_MS, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs b/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs new file mode 100644 index 0000000000..1978b4f665 --- /dev/null +++ b/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(req, resp) { + let suffixes = ["foo", "bar"]; + let data = [req.queryString, suffixes.map(s => req.queryString + s)]; + resp.setHeader("Content-Type", "application/json", false); + resp.write(JSON.stringify(data)); +} diff --git a/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.xml b/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.xml new file mode 100644 index 0000000000..8ed4fef6f1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>browser_urlbar_telemetry urlbarTelemetrySearchSuggestions.xml</ShortName> +<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs?{searchTerms}"/> +<Url type="text/html" method="GET" template="http://example.com" rel="searchform"/> +</SearchPlugin> diff --git a/browser/components/urlbar/tests/browser/urlbarTelemetryUrlbarDynamic.css b/browser/components/urlbar/tests/browser/urlbarTelemetryUrlbarDynamic.css new file mode 100644 index 0000000000..e81052522f --- /dev/null +++ b/browser/components/urlbar/tests/browser/urlbarTelemetryUrlbarDynamic.css @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +.urlbarView-row[dynamicType=test] > .urlbarView-row-inner { + display: flex; + align-items: center; + min-height: 32px; + width: 100%; +} + +.urlbarView-dynamic-test-button { + min-height: 16px; + padding: 8px; + border: none; + border-radius: 2px; + font-size: 0.93em; + color: inherit; + background-color: var(--urlbarView-button-background); + min-width: 8.75em; + text-align: center; + flex-basis: initial; + flex-shrink: 0; +} + +.urlbarView-dynamic-test-button[selected] { + color: white; + background-color: var(--urlbarView-primary-button-background); + box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3); +} + +.urlbarView-dynamic-test-button:hover { + color: white; + background-color: var(--urlbarView-primary-button-background-hover); +} + +.urlbarView-dynamic-test-button:active { + color: white; + background-color: var(--urlbarView-primary-button-background-active); +} + +.urlbarView-dynamic-test-buttonSpacer { + flex-basis: 48px; + flex-grow: 1; + flex-shrink: 1; +} diff --git a/browser/components/urlbar/tests/browser/wait-a-bit.sjs b/browser/components/urlbar/tests/browser/wait-a-bit.sjs new file mode 100644 index 0000000000..52a6ae2c22 --- /dev/null +++ b/browser/components/urlbar/tests/browser/wait-a-bit.sjs @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function handleRequest(request, response) { + response.processAsync(); + + const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.init(response.finish, 3000, Ci.nsITimer.TYPE_ONE_SHOT); +} |