summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/test/mochitest
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/test/mochitest')
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/.eslintrc.js17
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs216
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/chrome_timeout.js14
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/file_history_back.html14
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/form_basic_bfcache.html61
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html31
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_each_field_in_its_own_shadow_root.html31
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html33
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_both_fields_together_in_a_shadow_root.html34
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_each_field_in_its_own_shadow_root.html38
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_form_and_fields_together_in_a_shadow_root.html37
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html28
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_each_field_in_its_own_shadow_root.html28
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html30
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/mochitest.ini267
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/multiple_forms_shadow_DOM_all_known_variants.html111
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/pwmgr_common.js1063
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/pwmgr_common_parent.js249
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/slow_image.html9
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/slow_image.sjs25
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/subtst_prefilled_form.html18
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/subtst_primary_pass.html8
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/subtst_prompt_async.html12
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_DOMInputPasswordAdded_fired_between_DOMContentLoaded_and_load_events.html73
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_LoginManagerContent_passwordEditedOrGenerated.html160
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_autofill_related_realms_no_dupes.html92
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form.html894
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_formActionOrigin.html75
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_insecure.html849
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_related_realms.html106
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_subdomain.html117
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_hasBeenTypePassword.html101
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight.html87
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight_non_login.html91
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight_username_only_form.html56
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_downgrade.html105
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html191
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation.html574
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html518
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_open.html90
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_sandboxed.html70
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_tab_between_fields.html167
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_autocomplete_types.html112
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_different_formActionOrigin.html91
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_different_subdomain.html150
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_from_bfcache.html58
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_hasBeenTypePassword.html64
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight.html58
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight_empty_username.html60
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight_username_only_form.html50
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_https_downgrade.html118
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html148
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html135
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_sandboxed.html100
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_tab_between_fields.html154
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_username-only.html107
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofill_username-only_threshold.html83
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html114
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form.html48
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html70
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html171
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html115
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html190
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html111
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html259
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_honor_autocomplete_off.html153
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html165
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html50
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html212
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html161
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html57
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_case_differences.html100
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_dismissed_doorhanger_in_shadow_DOM.html112
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_formLike_rootElement_with_Shadow_DOM.html151
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html140
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html173
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html47
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html144
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html243
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal.html292
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal_negative.html205
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html272
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html149
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_input_events.html56
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html52
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html91
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_maxlength.html144
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_munged_values.html364
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_one_doorhanger_per_un_pw.html59
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_onsubmit_value_change.html70
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html198
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_password_length.html150
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html114
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_primary_password.html298
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_prompt.html669
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_prompt_async.html621
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html319
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html72
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html370
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html269
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html212
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_submit_without_field_modifications.html313
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_username_focus.html166
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_xhr.html164
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html56
105 files changed, 17009 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/test/mochitest/.eslintrc.js b/toolkit/components/passwordmgr/test/mochitest/.eslintrc.js
new file mode 100644
index 0000000000..beb8ec4738
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/.eslintrc.js
@@ -0,0 +1,17 @@
+"use strict";
+
+module.exports = {
+ globals: {
+ promptDone: true,
+ startTest: true,
+ // Make no-undef happy with our runInParent mixed environments since you
+ // can't indicate a single function is a new env.
+ assert: true,
+ addMessageListener: true,
+ sendAsyncMessage: true,
+ Assert: true,
+ },
+ rules: {
+ "no-var": "off",
+ },
+};
diff --git a/toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs b/toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs
new file mode 100644
index 0000000000..bc11bb29f8
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs
@@ -0,0 +1,216 @@
+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 new Error("Couldn't parse auth header: " + authHeader);
+ }
+
+ let userpass = atob(match[1]);
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3) {
+ throw new 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 new Error("Couldn't parse auth header: " + authHeader);
+ }
+
+ let userpass = atob(match[1]);
+ match = /(.*):(.*)/.exec(userpass);
+ if (match.length != 3) {
+ throw new 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/toolkit/components/passwordmgr/test/mochitest/chrome_timeout.js b/toolkit/components/passwordmgr/test/mochitest/chrome_timeout.js
new file mode 100644
index 0000000000..25a797e1d2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/chrome_timeout.js
@@ -0,0 +1,14 @@
+/* eslint-env mozilla/chrome-script */
+
+const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+addMessageListener("setTimeout", msg => {
+ timer.init(
+ _ => {
+ sendAsyncMessage("timeout");
+ },
+ msg.delay,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+});
+
+sendAsyncMessage("ready");
diff --git a/toolkit/components/passwordmgr/test/mochitest/file_history_back.html b/toolkit/components/passwordmgr/test/mochitest/file_history_back.html
new file mode 100644
index 0000000000..4e3e071a71
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/file_history_back.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ This page should navigate back in history upon load.
+ <script>
+ window.onload = function goBack() {
+ window.history.back();
+ };
+ </script>
+ </body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/form_basic_bfcache.html b/toolkit/components/passwordmgr/test/mochitest/form_basic_bfcache.html
new file mode 100644
index 0000000000..e91a88d599
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/form_basic_bfcache.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script>
+ var bc = new BroadcastChannel("form_basic_bfcache");
+ bc.onmessage = function(event) {
+ if (event.data == "nextpage") {
+ location.href = "https://example.org/tests/toolkit/components/passwordmgr/test/mochitest/file_history_back.html";
+ } else if (event.data == "close") {
+ bc.postMessage("closed");
+ bc.close();
+ window.close();
+ SimpleTest.finish();
+ }
+ }
+
+ function is(val1, val2, msg) {
+ bc.postMessage({type: "is", value1: val1, value2: val2, message: msg});
+ }
+
+ function ok(val, msg) {
+ bc.postMessage({type: "ok", value: val, message: msg});
+ }
+ </script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script>
+ // Need to use waitForExplicitFinish also in this support file to
+ // stop SimpleTest complaining about missing checks.
+ // pwmgr_common.js uses internally some helper methods from SimpleTest.
+ SimpleTest.waitForExplicitFinish();
+
+ runChecksAfterCommonInit();
+
+ onpageshow = async function(pageShow) {
+ if (!pageShow.persisted) {
+ // This is the initial page load.
+ let loginAddedPromise = promiseStorageChanged(["addLogin"]);
+ let origin = window.location.origin;
+ await addLoginsInParent([origin, "", null, "autofilled", "pass1", "", ""]);
+ await loginAddedPromise;
+ } else {
+ await promiseFormsProcessedInSameProcess();
+ let uname = document.getElementById("form-basic-username");
+ let pword = document.getElementById("form-basic-password");
+ checkLoginForm(uname, "autofilled", pword, "pass1");
+ }
+
+ bc.postMessage({type: pageShow.type, persisted: pageShow.persisted});
+ }
+ </script>
+ </head>
+ <body>
+ <form id="form-basic">
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-submit" type="submit">
+ </form>
+ </body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html
new file mode 100644
index 0000000000..ab558c4955
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Simple form with username and password fields together in a shadow root with a <form> ancestor -->
+<!-- This form is based off of toolkit/components/passwordmgr/test/browser/form_basic.html -->
+<form id="both-fields-together-in-a-shadow-root">
+ <!-- username and password inputs generated programmatically below -->
+ <input id="submit" type="submit">
+</form>
+
+<script>
+ const form = document.getElementById("both-fields-together-in-a-shadow-root");
+ const submitButton = document.getElementById("submit");
+ const wrapper = document.createElement("span");
+ wrapper.id = "wrapper-un-and-pw";
+ const shadow = wrapper.attachShadow({mode: "closed"});
+ const fields = ["username", "password"];
+ for (let field of fields) {
+ const inputEle = document.createElement("input");
+ inputEle.id = field;
+ inputEle.name = field;
+ if (field === "password") {
+ inputEle.type = field;
+ }
+ shadow.append(inputEle);
+ }
+ submitButton.before(wrapper);
+</script>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_each_field_in_its_own_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_each_field_in_its_own_shadow_root.html
new file mode 100644
index 0000000000..19edd12330
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_each_field_in_its_own_shadow_root.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Simple form with username and password fields each in their own shadow root -->
+<!-- This form is based off of toolkit/components/passwordmgr/test/browser/form_basic.html -->
+<form id="each-field-its-own-shadow">
+ <!-- username and password inputs generated programmatically below -->
+ <input id="submit" type="submit">
+</form>
+
+<script>
+ const form = document.getElementById("each-field-its-own-shadow");
+ const submitButton = document.getElementById("submit");
+ const fields = ["username", "password"];
+ for (let field of fields) {
+ const inputEle = document.createElement("input");
+ inputEle.id = field;
+ inputEle.name = field;
+ if (field === "password") {
+ inputEle.type = field;
+ }
+ const wrapper = document.createElement("span");
+ wrapper.id = `wrapper-${field}`;
+ const shadow = wrapper.attachShadow({mode: "closed"});
+ shadow.append(inputEle);
+ submitButton.before(wrapper);
+ }
+</script>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html
new file mode 100644
index 0000000000..225fb4e7f8
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Simple form with form, username and password fields together in a shadow root -->
+<!-- This form is based off of toolkit/components/passwordmgr/test/browser/form_basic.html -->
+<span id="wrapper">
+ <!-- form and all inputs generated programmatically below -->
+</span>
+
+<script>
+ const wrapper = document.getElementById("wrapper");
+ const shadow = wrapper.attachShadow({mode: "closed"});
+ const form = document.createElement("form");
+ form.id = "form-and-fields-in-a-shadow-root";
+ const submitButton = document.createElement("input");
+ submitButton.id = "submit";
+ submitButton.type = "submit";
+ shadow.append(form);
+ form.append(submitButton);
+ const fields = ["username", "password"];
+ for (let field of fields) {
+ const inputEle = document.createElement("input");
+ inputEle.id = field;
+ inputEle.name = field;
+ if (field === "password") {
+ inputEle.type = field;
+ }
+ submitButton.before(inputEle);
+ }
+</script>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_both_fields_together_in_a_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_both_fields_together_in_a_shadow_root.html
new file mode 100644
index 0000000000..876c7d3b85
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_both_fields_together_in_a_shadow_root.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Simple form with username and password fields together in nested shadow roots -->
+<!-- This form is based off of toolkit/components/passwordmgr/test/browser/form_basic.html -->
+<form id="each-field-its-own-shadow">
+ <span id="outer-wrapper">
+ <!-- username and password inputs generated programmatically below -->
+ </span>
+ <input id="submit" type="submit">
+</form>
+
+<script>
+ const submitButton = document.getElementById("submit");
+ const innerWrapper = document.createElement("span");
+ innerWrapper.id = "inner-wrapper";
+ const innerShadow = innerWrapper.attachShadow({mode: "closed"});
+ const outerWrapper = document.getElementById("outer-wrapper");
+ const outerShadow = outerWrapper.attachShadow({mode: "closed"});
+ const fields = ["username", "password"];
+ for (let field of fields) {
+ const inputEle = document.createElement("input");
+ inputEle.id = field;
+ inputEle.name = field;
+ if (field === "password") {
+ inputEle.type = field;
+ }
+ innerShadow.append(inputEle);
+ }
+ outerShadow.append(innerWrapper);
+</script>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_each_field_in_its_own_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_each_field_in_its_own_shadow_root.html
new file mode 100644
index 0000000000..9a844f236a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_each_field_in_its_own_shadow_root.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Simple form with username and password fields each in their own nested shadow roots -->
+<!-- This form is based off of toolkit/components/passwordmgr/test/browser/form_basic.html -->
+<form id="each-field-its-own-shadow">
+ <span id="outer-wrapper-username">
+ <!-- username input generated programmatically below -->
+ </span>
+ <span id="outer-wrapper-password">
+ <!-- password input generated programmatically below -->
+ </span>
+ <input id="submit" type="submit">
+</form>
+
+<script>
+ const submitButton = document.getElementById("submit");
+ const fields = ["username", "password"];
+ for (let field of fields) {
+ const inputEle = document.createElement("input");
+ inputEle.id = field;
+ inputEle.name = field;
+ if (field === "password") {
+ inputEle.type = field;
+ }
+ const wrapper = document.createElement("span");
+ wrapper.id = `wrapper-${field}`;
+ const shadow = wrapper.attachShadow({mode: "closed"});
+ shadow.append(inputEle);
+
+ const outerWrapper = document.getElementById(`outer-wrapper-${field}`);
+ const outerShadow = outerWrapper.attachShadow({mode: "closed"});
+ outerShadow.append(wrapper);
+ }
+</script>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_form_and_fields_together_in_a_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_form_and_fields_together_in_a_shadow_root.html
new file mode 100644
index 0000000000..79481c4a3a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_form_and_fields_together_in_a_shadow_root.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Simple form with form, username and password fields together in nested shadow roots -->
+<!-- This form is based off of toolkit/components/passwordmgr/test/browser/form_basic.html -->
+<span id="outer-wrapper">
+ <!-- form and all inputs generated programmatically below -->
+</span>
+
+<script>
+ const outerWrapper = document.getElementById("outer-wrapper");
+ const innerWrapper = document.createElement("span");
+ innerWrapper.id = "inner-wrapper";
+ const innerShadow = innerWrapper.attachShadow({mode: "closed"});
+ const outerShadow = outerWrapper.attachShadow({mode: "closed"});
+ const form = document.createElement("form");
+ form.id = "form-and-fields-in-a-shadow-root";
+ const submitButton = document.createElement("input");
+ submitButton.id = "submit";
+ submitButton.type = "submit";
+ innerShadow.append(form);
+ form.append(submitButton);
+ const fields = ["username", "password"];
+ for (let field of fields) {
+ const inputEle = document.createElement("input");
+ inputEle.id = field;
+ inputEle.name = field;
+ if (field === "password") {
+ inputEle.type = field;
+ }
+ submitButton.before(inputEle);
+ }
+ outerShadow.append(innerWrapper);
+</script>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html
new file mode 100644
index 0000000000..ed261af9aa
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Simple form with username and password fields together in a shadow root -->
+<!-- This form is based off of toolkit/components/passwordmgr/test/browser/formless_basic.html -->
+<!-- username and password inputs generated programmatically below -->
+<input id="submit" type="submit">
+
+<script>
+ const submitButton = document.getElementById("submit");
+ const wrapper = document.createElement("span");
+ wrapper.id = "wrapper-un-and-pw";
+ const shadow = wrapper.attachShadow({mode: "closed"});
+ const fields = ["username", "password"];
+ for (let field of fields) {
+ const inputEle = document.createElement("input");
+ inputEle.id = field;
+ inputEle.name = field;
+ if (field === "password") {
+ inputEle.type = field;
+ }
+ shadow.append(inputEle);
+ }
+ submitButton.before(wrapper);
+</script>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_each_field_in_its_own_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_each_field_in_its_own_shadow_root.html
new file mode 100644
index 0000000000..b0ea0dc486
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_each_field_in_its_own_shadow_root.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Simple form with username and password fields each in their own shadow root -->
+<!-- This form is based off of toolkit/components/passwordmgr/test/browser/formless_basic.html -->
+<!-- username and password inputs generated programmatically below -->
+<input id="submit" type="submit">
+
+<script>
+ const submitButton = document.getElementById("submit");
+ const fields = ["username", "password"];
+ for (let field of fields) {
+ const inputEle = document.createElement("input");
+ inputEle.id = field;
+ inputEle.name = field;
+ if (field === "password") {
+ inputEle.type = field;
+ }
+ const wrapper = document.createElement("span");
+ wrapper.id = `wrapper-${field}`;
+ const shadow = wrapper.attachShadow({mode: "closed"});
+ shadow.append(inputEle);
+ submitButton.before(wrapper);
+ }
+</script>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html
new file mode 100644
index 0000000000..a93e5ea752
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Simple form with form, username and password fields together in a shadow root -->
+<!-- This form is based off of toolkit/components/passwordmgr/test/browser/formless_basic.html -->
+<span id="wrapper">
+</span>
+<!-- username, password and submit inputs generated programmatically below -->
+
+<script>
+ const wrapper = document.getElementById("wrapper");
+ const shadow = wrapper.attachShadow({mode: "closed"});
+ const fields = ["username", "password"];
+ for (let field of fields) {
+ const inputEle = document.createElement("input");
+ inputEle.id = field;
+ inputEle.name = field;
+ if (field === "password") {
+ inputEle.type = field;
+ }
+ shadow.append(inputEle);
+ }
+ const submitButton = document.createElement("input");
+ submitButton.id = "submit";
+ submitButton.type = "submit";
+ shadow.append(submitButton);
+</script>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
new file mode 100644
index 0000000000..feb825d412
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
@@ -0,0 +1,267 @@
+[DEFAULT]
+prefs =
+ signon.rememberSignons=true
+ signon.autofillForms.http=true
+ signon.showAutoCompleteFooter=true
+ signon.showAutoCompleteImport=""
+ signon.testOnlyUserHasInteractedByPrefValue=true
+ signon.testOnlyUserHasInteractedWithDocument=true
+ network.auth.non-web-content-triggered-resources-http-auth-allow=true
+ # signon.relatedRealms.enabled pref needed until Bug 1699698 lands
+ signon.relatedRealms.enabled=true
+ signon.usernameOnlyForm.enabled=true
+ signon.usernameOnlyForm.lookupThreshold=100
+
+support-files =
+ ../../../prompts/test/chromeScript.js
+ !/toolkit/components/prompts/test/prompt_common.js
+ ../../../satchel/test/parent_utils.js
+ !/toolkit/components/satchel/test/satchel_common.js
+ ../blank.html
+ ../browser/form_autofocus_js.html
+ ../browser/form_basic.html
+ ../browser/formless_basic.html
+ ../browser/form_cross_origin_secure_action.html
+ ../browser/form_same_origin_action.html
+ auth2/authenticate.sjs
+ file_history_back.html
+ form_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html
+ form_basic_shadow_DOM_each_field_in_its_own_shadow_root.html
+ form_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html
+ form_nested_shadow_DOM_both_fields_together_in_a_shadow_root.html
+ form_nested_shadow_DOM_each_field_in_its_own_shadow_root.html
+ form_nested_shadow_DOM_form_and_fields_together_in_a_shadow_root.html
+ formless_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html
+ formless_basic_shadow_DOM_each_field_in_its_own_shadow_root.html
+ formless_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html
+ multiple_forms_shadow_DOM_all_known_variants.html
+ pwmgr_common.js
+ pwmgr_common_parent.js
+ ../authenticate.sjs
+skip-if = toolkit == 'android' # Don't run on GeckoView
+
+# Note: new tests should use scheme = https unless they have a specific reason not to
+
+[test_autocomplete_autofill_related_realms_no_dupes.html]
+skip-if =
+ fission && xorigin # Bug 1716412 - New fission platform triage
+scheme = https
+[test_autocomplete_basic_form.html]
+skip-if =
+ toolkit == 'android' # autocomplete
+ debug && (os == 'linux' || os == 'win') # Bug 1541945
+ os == 'linux' && tsan # Bug 1590928
+ fission && xorigin && (!debug || os == "mac") # Bug 1716412 - New fission platform triage
+scheme = https
+[test_autocomplete_basic_form_insecure.html]
+skip-if =
+ toolkit == 'android' # autocomplete
+ os == 'linux' # bug 1325778
+ fission && xorigin && (os == "win" || os == "mac") # Bug 1716412 - New fission platform triage
+ win11_2009 # Bug 1781648
+[test_autocomplete_basic_form_formActionOrigin.html]
+skip-if = toolkit == 'android' # android:autocomplete.
+scheme = https
+[test_autocomplete_basic_form_related_realms.html]
+skip-if =
+ fission && xorigin # Bug 1716412 - New fission platform triage
+scheme = https
+[test_autocomplete_basic_form_subdomain.html]
+skip-if = toolkit == 'android' # android:autocomplete.
+scheme = https
+[test_autocomplete_hasBeenTypePassword.html]
+scheme = https
+skip-if = toolkit == 'android' # autocomplete
+[test_autocomplete_highlight.html]
+scheme = https
+skip-if = toolkit == 'android' # autocomplete
+[test_autocomplete_highlight_non_login.html]
+scheme = https
+skip-if = toolkit == 'android' # autocomplete
+[test_autocomplete_highlight_username_only_form.html]
+scheme = https
+skip-if = toolkit == 'android' # autocomplete
+[test_autocomplete_https_downgrade.html]
+scheme = http # Tests downgrading
+skip-if =
+ toolkit == 'android' # autocomplete
+ os == 'linux' && debug # Bug 1554959
+ fission && xorigin # Bug 1716412 - New fission platform triage
+[test_autocomplete_https_upgrade.html]
+scheme = https
+skip-if = verify || toolkit == 'android' || (os == 'linux' && debug) # autocomplete && Bug 1554959 for linux debug disable
+[test_autocomplete_password_generation.html]
+scheme = https
+skip-if = xorigin || toolkit == 'android' # autocomplete
+[test_autocomplete_password_generation_confirm.html]
+scheme = https
+skip-if = toolkit == 'android' # autocomplete
+[test_autocomplete_password_open.html]
+scheme = https
+skip-if = toolkit == 'android' || verify # autocomplete
+[test_autocomplete_sandboxed.html]
+scheme = https
+skip-if = toolkit == 'android' # autocomplete
+[test_autocomplete_tab_between_fields.html]
+scheme = https
+skip-if =
+ xorigin || toolkit == 'android' # autocomplete
+[test_autofill_autocomplete_types.html]
+scheme = https
+skip-if = toolkit == 'android' # bug 1533965
+[test_autofill_different_formActionOrigin.html]
+scheme = https
+skip-if = toolkit == 'android' # Bug 1259768
+[test_autofill_different_subdomain.html]
+scheme = https
+skip-if =
+ toolkit == 'android' # Bug 1259768
+ http3
+[test_autofill_from_bfcache.html]
+scheme = https
+skip-if = toolkit == 'android' # bug 1527403
+support-files = form_basic_bfcache.html
+[test_autofill_hasBeenTypePassword.html]
+scheme = https
+[test_autofill_highlight.html]
+scheme = https
+skip-if = toolkit == 'android' # Bug 1531185
+[test_autofill_highlight_empty_username.html]
+scheme = https
+[test_autofill_highlight_username_only_form.html]
+scheme = https
+[test_autofill_https_downgrade.html]
+scheme = http # we need http to test handling of https logins on http forms
+skip-if =
+ http3
+[test_autofill_https_upgrade.html]
+skip-if =
+ toolkit == 'android' # Bug 1259768
+ http3
+[test_autofill_sandboxed.html]
+scheme = https
+skip-if = toolkit == 'android'
+[test_autofill_password-only.html]
+[test_autofill_username-only.html]
+[test_autofill_username-only_threshold.html]
+[test_autofocus_js.html]
+scheme = https
+skip-if = toolkit == 'android' # autocomplete
+[test_basic_form.html]
+[test_basic_form_0pw.html]
+[test_basic_form_1pw.html]
+[test_basic_form_1pw_2.html]
+[test_basic_form_2pw_1.html]
+[test_basic_form_2pw_2.html]
+[test_basic_form_3pw_1.html]
+[test_basic_form_honor_autocomplete_off.html]
+scheme = https
+skip-if = xorigin || toolkit == 'android' # android:autocomplete.
+[test_formless_submit_form_removal.html]
+skip-if =
+ http3
+[test_formless_submit_form_removal_negative.html]
+skip-if =
+ http3
+[test_password_field_autocomplete.html]
+skip-if = toolkit == 'android' # android:autocomplete.
+[test_insecure_form_field_no_saved_login.html]
+skip-if = toolkit == 'android' # android:autocomplete.
+[test_basic_form_html5.html]
+[test_basic_form_pwevent.html]
+skip-if = xorigin
+[test_basic_form_pwonly.html]
+[test_bug_627616.html]
+skip-if =
+ toolkit == 'android' # Tests desktop prompts
+ http3
+[test_bug_776171.html]
+[test_case_differences.html]
+skip-if = toolkit == 'android' # autocomplete
+scheme = https
+[test_dismissed_doorhanger_in_shadow_DOM.html]
+skip-if = toolkit == 'android' # Tests desktop prompt
+scheme = https
+[test_DOMInputPasswordAdded_fired_between_DOMContentLoaded_and_load_events.html]
+scheme = https
+support-files =
+ slow_image.sjs
+ slow_image.html
+[test_form_action_1.html]
+[test_form_action_2.html]
+[test_form_action_javascript.html]
+[test_formless_autofill.html]
+skip-if =
+ xorigin
+ http3
+[test_formless_submit.html]
+skip-if =
+ toolkit == 'android' && debug # bug 1397615
+ http3
+[test_formless_submit_navigation.html]
+skip-if =
+ toolkit == 'android' && debug # bug 1397615
+ http3
+[test_formless_submit_navigation_negative.html]
+skip-if =
+ toolkit == 'android' && debug # bug 1397615
+ http3
+[test_formLike_rootElement_with_Shadow_DOM.html]
+scheme = https
+[test_input_events.html]
+skip-if = xorigin
+[test_input_events_for_identical_values.html]
+[test_LoginManagerContent_passwordEditedOrGenerated.html]
+scheme = https
+skip-if = toolkit == 'android' # password generation
+[test_primary_password.html]
+scheme = https
+skip-if = os != 'mac' || verify || xorigin # Tests desktop prompts and bug 1333264
+support-files =
+ chrome_timeout.js
+ subtst_primary_pass.html
+[test_maxlength.html]
+[test_munged_values.html]
+scheme = https
+skip-if = toolkit == 'android' # bug 1527403
+[test_one_doorhanger_per_un_pw.html]
+scheme = https
+skip-if = toolkit == 'android' # bug 1535505
+[test_onsubmit_value_change.html]
+[test_passwords_in_type_password.html]
+[test_prompt.html]
+skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts
+[test_prompt_async.html]
+skip-if =
+ toolkit == 'android' # Tests desktop prompts
+ http3
+support-files = subtst_prompt_async.html
+[test_prompt_http.html]
+skip-if =
+ toolkit == 'android' # Tests desktop prompts
+ os == "linux"
+ fission && xorigin # Bug 1716412 - New fission platform triage
+[test_prompt_noWindow.html]
+skip-if = toolkit == 'android' # Tests desktop prompts.
+[test_password_length.html]
+scheme = https
+skip-if = toolkit == 'android' # bug 1527403
+[test_prompt_promptAuth.html]
+skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts
+[test_prompt_promptAuth_proxy.html]
+skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts
+[test_recipe_login_fields.html]
+skip-if = xorigin
+[test_submit_without_field_modifications.html]
+support-files =
+ subtst_prefilled_form.html
+skip-if =
+ xorigin
+ http3
+[test_username_focus.html]
+skip-if = xorigin || toolkit == 'android' # android:autocomplete.
+[test_xhr.html]
+skip-if = toolkit == 'android' # Tests desktop prompts
+[test_xhr_2.html]
+[test_autofill_tab_between_fields.html]
+scheme = https
diff --git a/toolkit/components/passwordmgr/test/mochitest/multiple_forms_shadow_DOM_all_known_variants.html b/toolkit/components/passwordmgr/test/mochitest/multiple_forms_shadow_DOM_all_known_variants.html
new file mode 100644
index 0000000000..5ba547e671
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/multiple_forms_shadow_DOM_all_known_variants.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Page with multiple forms containing the following Shadow DOM variants: -->
+<!-- Case 1: Each field (username and password) in its own shadow root -->
+<!-- Case 2: Both fields (username and password) together in a shadow root with a form ancestor -->
+<!-- Case 3: Form and fields (username and password) together in a shadow root -->
+<span id="outer-wrapper">
+</span>
+
+<script>
+ const outerWrapper = document.getElementById("outer-wrapper");
+ const outerShadow = outerWrapper.attachShadow({mode: "closed"});
+
+ function makeFormlessOuterForm(scenario) {
+ const fields = ["username", "password"];
+ for (let field of fields) {
+ const inputEle = document.createElement("input");
+ inputEle.id = `${field}-${scenario}`;
+ inputEle.name = `${field}-${scenario}`;
+ if (field === "password") {
+ inputEle.type = field;
+ }
+ outerShadow.append(inputEle);
+ }
+ const submitButton = document.createElement("input");
+ submitButton.id = `submit-${scenario}`;
+ submitButton.type = "submit";
+ outerShadow.append(submitButton);
+ }
+
+ function makeFormEachFieldInItsOwnShadowRoot(scenario) {
+ const form = document.createElement("form");
+ form.id = scenario;
+ const submitButton = document.createElement("input");
+ submitButton.id = `submit-${scenario}`;
+ submitButton.type = "submit";
+ form.append(submitButton);
+ const fields = ["username", "password"];
+ for (let field of fields) {
+ const inputEle = document.createElement("input");
+ inputEle.id = `${field}-${scenario}`;
+ inputEle.name = `${field}-${scenario}`;
+ if (field === "password") {
+ inputEle.type = field;
+ }
+ const wrapper = document.createElement("span");
+ wrapper.id = `wrapper-${field}-${scenario}`;
+ const shadow = wrapper.attachShadow({mode: "closed"});
+ shadow.append(inputEle);
+ submitButton.before(wrapper);
+ }
+ outerShadow.append(form);
+ }
+
+ function makeFormBothFieldsTogetherInAShadowRoot(scenario) {
+ const form = document.createElement("form");
+ form.id = scenario;
+ const submitButton = document.createElement("input");
+ submitButton.id = `submit-${scenario}`;
+ submitButton.type = "submit";
+ form.append(submitButton);
+ const wrapper = document.createElement("span");
+ wrapper.id = `wrapper-${scenario}`;
+ const shadow = wrapper.attachShadow({mode: "closed"});
+ const fields = ["username", "password"];
+ for (let field of fields) {
+ const inputEle = document.createElement("input");
+ inputEle.id = `${field}-${scenario}`;
+ inputEle.name = `${field}-${scenario}`;
+ if (field === "password") {
+ inputEle.type = field;
+ }
+ shadow.append(inputEle);
+ }
+ submitButton.before(wrapper);
+ outerShadow.append(form);
+ }
+
+ function makeFormFormAndFieldsTogetherInAShadowRoot(scenario) {
+ const wrapper = document.createElement("span");
+ wrapper.id = `wrapper-${scenario}`;
+ const shadow = wrapper.attachShadow({mode: "closed"});
+ const form = document.createElement("form");
+ form.id = scenario;
+ shadow.append(form);
+ const submitButton = document.createElement("input");
+ submitButton.id = `submit-${scenario}`;
+ submitButton.type = "submit";
+ form.append(submitButton);
+ const fields = ["username", "password"];
+ for (let field of fields) {
+ const inputEle = document.createElement("input");
+ inputEle.id = field;
+ inputEle.name = field;
+ if (field === "password") {
+ inputEle.type = field;
+ }
+ submitButton.before(inputEle);
+ }
+ outerShadow.append(wrapper);
+ }
+
+ makeFormlessOuterForm("formless-case-2");
+ makeFormEachFieldInItsOwnShadowRoot("form-case-1");
+ makeFormBothFieldsTogetherInAShadowRoot("form-case-2");
+ makeFormFormAndFieldsTogetherInAShadowRoot("form-case-3");
+</script>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/pwmgr_common.js b/toolkit/components/passwordmgr/test/mochitest/pwmgr_common.js
new file mode 100644
index 0000000000..81a31b0f4e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/pwmgr_common.js
@@ -0,0 +1,1063 @@
+/**
+ * Helpers for password manager mochitest-plain tests.
+ */
+
+/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
+
+const { LoginTestUtils } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://testing-common/LoginTestUtils.sys.mjs"
+);
+const Services = SpecialPowers.Services;
+
+// Setup LoginTestUtils to report assertions to the mochitest harness.
+LoginTestUtils.setAssertReporter(
+ SpecialPowers.wrapCallback((err, message, stack) => {
+ SimpleTest.record(!err, err ? err.message : message, null, stack);
+ })
+);
+
+const { LoginHelper } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/LoginHelper.sys.mjs"
+);
+
+const { LENGTH: GENERATED_PASSWORD_LENGTH, REGEX: GENERATED_PASSWORD_REGEX } =
+ LoginTestUtils.generation;
+const LOGIN_FIELD_UTILS = LoginTestUtils.loginField;
+const TESTS_DIR = "/tests/toolkit/components/passwordmgr/test/";
+
+// Depending on pref state we either show auth prompts as windows or on tab level.
+let authPromptModalType = SpecialPowers.Services.prefs.getIntPref(
+ "prompts.modalType.httpAuth"
+);
+
+// Whether the auth prompt is a commonDialog.xhtml or a TabModalPrompt
+let authPromptIsCommonDialog =
+ authPromptModalType === SpecialPowers.Services.prompt.MODAL_TYPE_WINDOW ||
+ (authPromptModalType === SpecialPowers.Services.prompt.MODAL_TYPE_TAB &&
+ SpecialPowers.Services.prefs.getBoolPref(
+ "prompts.tabChromePromptSubDialog",
+ false
+ ));
+
+/**
+ * Recreate a DOM tree using the outerHTML to ensure that any event listeners
+ * and internal state for the elements are removed.
+ */
+function recreateTree(element) {
+ // eslint-disable-next-line no-unsanitized/property, no-self-assign
+ element.outerHTML = element.outerHTML;
+}
+
+function _checkArrayValues(actualValues, expectedValues, msg) {
+ is(
+ actualValues.length,
+ expectedValues.length,
+ "Checking array values: " + msg
+ );
+ for (let i = 0; i < expectedValues.length; i++) {
+ is(actualValues[i], expectedValues[i], msg + " Checking array entry #" + i);
+ }
+}
+
+/**
+ * Check autocomplete popup results to ensure that expected
+ * *labels* are being shown correctly as items in the popup.
+ */
+function checkAutoCompleteResults(actualValues, expectedValues, hostname, msg) {
+ if (hostname === null) {
+ _checkArrayValues(actualValues, expectedValues, msg);
+ return;
+ }
+
+ is(
+ typeof hostname,
+ "string",
+ "checkAutoCompleteResults: hostname must be a string"
+ );
+
+ isnot(
+ actualValues.length,
+ 0,
+ "There should be items in the autocomplete popup: " +
+ JSON.stringify(actualValues)
+ );
+
+ // Check the footer first.
+ let footerResult = actualValues[actualValues.length - 1];
+ is(footerResult, "View Saved Logins", "the footer text is shown correctly");
+
+ if (actualValues.length == 1) {
+ is(
+ expectedValues.length,
+ 0,
+ "If only the footer is present then there should be no expectedValues"
+ );
+ info("Only the footer is present in the popup");
+ return;
+ }
+
+ // Check the rest of the autocomplete item values.
+ _checkArrayValues(actualValues.slice(0, -1), expectedValues, msg);
+}
+
+function getIframeBrowsingContext(window, iframeNumber = 0) {
+ let bc = SpecialPowers.wrap(window).windowGlobalChild.browsingContext;
+ return SpecialPowers.unwrap(bc.children[iframeNumber]);
+}
+
+/**
+ * Set input values via setUserInput to emulate user input
+ * and distinguish them from declarative or script-assigned values
+ */
+function setUserInputValues(parentNode, selectorValues, userInput = true) {
+ for (let [selector, newValue] of Object.entries(selectorValues)) {
+ info(`setUserInputValues, selector: ${selector}`);
+ try {
+ let field = SpecialPowers.wrap(parentNode.querySelector(selector));
+ if (field.value == newValue) {
+ // we don't get an input event if the new value == the old
+ field.value += "#";
+ }
+ if (userInput) {
+ field.setUserInput(newValue);
+ } else {
+ field.value = newValue;
+ }
+ } catch (ex) {
+ info(ex.message);
+ info(ex.stack);
+ ok(
+ false,
+ `setUserInputValues: Couldn't set value of field: ${ex.message}`
+ );
+ }
+ }
+}
+
+/**
+ * @param {Function} [aFilterFn = undefined] Function to filter out irrelevant submissions.
+ * @return {Promise} resolving when a relevant form submission was processed.
+ */
+function getSubmitMessage(aFilterFn = undefined) {
+ info("getSubmitMessage");
+ return new Promise((resolve, reject) => {
+ PWMGR_COMMON_PARENT.addMessageListener(
+ "formSubmissionProcessed",
+ function processed(...args) {
+ if (aFilterFn && !aFilterFn(...args)) {
+ // This submission isn't the one we're waiting for.
+ return;
+ }
+
+ info("got formSubmissionProcessed");
+ PWMGR_COMMON_PARENT.removeMessageListener(
+ "formSubmissionProcessed",
+ processed
+ );
+ resolve(args[0]);
+ }
+ );
+ });
+}
+
+/**
+ * @return {Promise} resolves when a onPasswordEditedOrGenerated message is received at the parent
+ */
+function getPasswordEditedMessage() {
+ info("getPasswordEditedMessage");
+ return new Promise((resolve, reject) => {
+ PWMGR_COMMON_PARENT.addMessageListener(
+ "passwordEditedOrGenerated",
+ function listener(...args) {
+ info("got passwordEditedOrGenerated");
+ PWMGR_COMMON_PARENT.removeMessageListener(
+ "passwordEditedOrGenerated",
+ listener
+ );
+ resolve(args[0]);
+ }
+ );
+ });
+}
+
+/**
+ * Create a login form and insert into contents dom (identified by id
+ * `content`). If the form (identified by its number) is already present in the
+ * dom, it gets replaced.
+ *
+ * @param {number} [num = 1] - number of the form, used as id, eg `form1`
+ * @param {string} [action = ""] - action attribute of the form
+ * @param {string} [autocomplete = null] - forms autocomplete attribute. Default is none
+ * @param {object} [username = {}] - object describing attributes to the username field:
+ * @param {string} [username.id = null] - id of the field
+ * @param {string} [username.name = "uname"] - name attribute
+ * @param {string} [username.type = "text"] - type of the field
+ * @param {string} [username.value = null] - initial value of the field
+ * @param {string} [username.autocomplete = null] - autocomplete attribute
+ * @param {object} [password = {}] - an object describing attributes to the password field. If falsy, do not create a password field
+ * @param {string} [password.id = null] - id of the field
+ * @param {string} [password.name = "pword"] - name attribute
+ * @param {string} [password.type = "password"] - type of the field
+ * @param {string} [password.value = null] - initial value of the field
+ * @param {string} [password.label = null] - if present, wrap field in a label containing its value
+ * @param {string} [password.autocomplete = null] - autocomplete attribute
+ *
+ * @return {HTMLDomElement} the form
+ */
+function createLoginForm({
+ num = 1,
+ action = "",
+ autocomplete = null,
+ username = {},
+ password = {},
+} = {}) {
+ username.id ||= null;
+ username.name ||= "uname";
+ username.type ||= "text";
+ username.value ||= null;
+ username.autocomplete ||= null;
+ password.id ||= null;
+ password.name ||= "pword";
+ password.type ||= "password";
+ password.value ||= null;
+ password.label ||= null;
+ password.autocomplete ||= null;
+
+ info(
+ `Creating login form ${JSON.stringify({ num, action, username, password })}`
+ );
+
+ const form = document.createElement("form");
+ form.id = `form${num}`;
+ form.action = action;
+ form.onsubmit = () => false;
+
+ if (autocomplete != null) {
+ form.setAttribute("autocomplete", autocomplete);
+ }
+
+ const usernameInput = document.createElement("input");
+ if (username.id != null) {
+ usernameInput.id = username.id;
+ }
+ usernameInput.type = username.type;
+ usernameInput.name = username.name;
+ if (username.value != null) {
+ usernameInput.value = username.value;
+ }
+ if (username.autocomplete != null) {
+ usernameInput.setAttribute("autocomplete", username.autocomplete);
+ }
+ form.appendChild(usernameInput);
+
+ if (password) {
+ const passwordInput = document.createElement("input");
+ if (password.id != null) {
+ passwordInput.id = password.id;
+ }
+ passwordInput.type = password.type;
+ passwordInput.name = password.name;
+ if (password.value != null) {
+ passwordInput.value = password.value;
+ }
+ if (password.autocomplete != null) {
+ passwordInput.setAttribute("autocomplete", password.autocomplete);
+ }
+ if (password.label != null) {
+ const passwordLabel = document.createElement("label");
+ passwordLabel.innerText = password.label;
+ passwordLabel.appendChild(passwordInput);
+ form.appendChild(passwordLabel);
+ } else {
+ form.appendChild(passwordInput);
+ }
+ }
+
+ const submitButton = document.createElement("button");
+ submitButton.type = "submit";
+ submitButton.name = "submit";
+ submitButton.innerText = "Submit";
+ form.appendChild(submitButton);
+
+ const content = document.getElementById("content");
+
+ const oldForm = document.getElementById(form.id);
+ if (oldForm) {
+ content.replaceChild(form, oldForm);
+ } else {
+ content.appendChild(form);
+ }
+
+ return form;
+}
+
+/**
+ * Check for expected username/password in form.
+ * @see `checkForm` below for a similar function.
+ */
+function checkLoginForm(
+ usernameField,
+ expectedUsername,
+ passwordField,
+ expectedPassword
+) {
+ let formID = usernameField.parentNode.id;
+ is(
+ usernameField.value,
+ expectedUsername,
+ "Checking " + formID + " username is: " + expectedUsername
+ );
+ is(
+ passwordField.value,
+ expectedPassword,
+ "Checking " + formID + " password is: " + expectedPassword
+ );
+}
+
+/**
+ * Check repeatedly for a while to see if a particular condition still applies.
+ * This function checks the return value of `condition` repeatedly until either
+ * the condition has a falsy return value, or `retryTimes` is exceeded.
+ */
+
+function ensureCondition(
+ condition,
+ errorMsg = "Condition did not last.",
+ retryTimes = 10
+) {
+ return new Promise((resolve, reject) => {
+ let tries = 0;
+ let conditionFailed = false;
+ let interval = setInterval(async function () {
+ try {
+ const conditionPassed = await condition();
+ conditionFailed ||= !conditionPassed;
+ } catch (e) {
+ ok(false, e + "\n" + e.stack);
+ conditionFailed = true;
+ }
+ if (conditionFailed || tries >= retryTimes) {
+ ok(!conditionFailed, errorMsg);
+ clearInterval(interval);
+ if (conditionFailed) {
+ reject(errorMsg);
+ } else {
+ resolve();
+ }
+ }
+ tries++;
+ }, 100);
+ });
+}
+
+/**
+ * Wait a while to ensure login form stays filled with username and password
+ * @see `checkLoginForm` below for a similar function.
+ * @returns a promise, resolving when done
+ *
+ * TODO: eventually get rid of this time based check, and transition to an
+ * event based approach. See Bug 1811142.
+ * Filling happens by `_fillForm()` which can report it's decision and we can
+ * wait for it. One of the options is to have `didFillFormAsync()` from
+ * https://phabricator.services.mozilla.com/D167214#change-3njWgUgqswws
+ */
+function ensureLoginFormStaysFilledWith(
+ usernameField,
+ expectedUsername,
+ passwordField,
+ expectedPassword
+) {
+ return ensureCondition(() => {
+ return (
+ Object.is(usernameField.value, expectedUsername) &&
+ Object.is(passwordField.value, expectedPassword)
+ );
+ }, `Ensuring form ${usernameField.parentNode.id} stays filled with "${expectedUsername}:${expectedPassword}"`);
+}
+
+function checkLoginFormInFrame(
+ iframeBC,
+ usernameFieldId,
+ expectedUsername,
+ passwordFieldId,
+ expectedPassword
+) {
+ return SpecialPowers.spawn(
+ iframeBC,
+ [usernameFieldId, expectedUsername, passwordFieldId, expectedPassword],
+ (
+ usernameFieldIdF,
+ expectedUsernameF,
+ passwordFieldIdF,
+ expectedPasswordF
+ ) => {
+ let usernameField =
+ this.content.document.getElementById(usernameFieldIdF);
+ let passwordField =
+ this.content.document.getElementById(passwordFieldIdF);
+
+ let formID = usernameField.parentNode.id;
+ Assert.equal(
+ usernameField.value,
+ expectedUsernameF,
+ "Checking " + formID + " username is: " + expectedUsernameF
+ );
+ Assert.equal(
+ passwordField.value,
+ expectedPasswordF,
+ "Checking " + formID + " password is: " + expectedPasswordF
+ );
+ }
+ );
+}
+
+async function checkUnmodifiedFormInFrame(bc, formNum) {
+ return SpecialPowers.spawn(bc, [formNum], formNumF => {
+ let form = this.content.document.getElementById(`form${formNumF}`);
+ ok(form, "Locating form " + formNumF);
+
+ for (var i = 0; i < form.elements.length; i++) {
+ var ele = form.elements[i];
+
+ // No point in checking form submit/reset buttons.
+ if (ele.type == "submit" || ele.type == "reset") {
+ continue;
+ }
+
+ is(
+ ele.value,
+ ele.defaultValue,
+ "Test to default value of field " + ele.name + " in form " + formNumF
+ );
+ }
+ });
+}
+
+/**
+ * Check a form for expected values even if it is in a different top level window
+ * or process. If an argument is null, a field's expected value will be the default
+ * value.
+ *
+ * Similar to the checkForm helper, but it works across (cross-origin) frames.
+ *
+ * <form id="form#">
+ * checkLoginFormInFrameWithElementValues(#, "foo");
+ */
+async function checkLoginFormInFrameWithElementValues(
+ browsingContext,
+ formNum,
+ ...values
+) {
+ return SpecialPowers.spawn(
+ browsingContext,
+ [formNum, values],
+ function checkFormWithElementValues(formNumF, valuesF) {
+ let [val1F, val2F, val3F] = valuesF;
+ let doc = this.content.document;
+ let e;
+ let form = doc.getElementById("form" + formNumF);
+ ok(form, "Locating form " + formNumF);
+
+ let numToCheck = arguments.length - 1;
+
+ if (!numToCheck--) {
+ return;
+ }
+ e = form.elements[0];
+ if (val1F == null) {
+ is(
+ e.value,
+ e.defaultValue,
+ "Test default value of field " + e.name + " in form " + formNumF
+ );
+ } else {
+ is(
+ e.value,
+ val1F,
+ "Test value of field " + e.name + " in form " + formNumF
+ );
+ }
+
+ if (!numToCheck--) {
+ return;
+ }
+
+ e = form.elements[1];
+ if (val2F == null) {
+ is(
+ e.value,
+ e.defaultValue,
+ "Test default value of field " + e.name + " in form " + formNumF
+ );
+ } else {
+ is(
+ e.value,
+ val2F,
+ "Test value of field " + e.name + " in form " + formNumF
+ );
+ }
+
+ if (!numToCheck--) {
+ return;
+ }
+ e = form.elements[2];
+ if (val3F == null) {
+ is(
+ e.value,
+ e.defaultValue,
+ "Test default value of field " + e.name + " in form " + formNumF
+ );
+ } else {
+ is(
+ e.value,
+ val3F,
+ "Test value of field " + e.name + " in form " + formNumF
+ );
+ }
+ }
+ );
+}
+
+/**
+ * Check a form for expected values. If an argument is null, a field's
+ * expected value will be the default value.
+ *
+ * <form id="form#">
+ * checkForm(#, "foo");
+ */
+function checkForm(formNum, val1, val2, val3) {
+ var e,
+ form = document.getElementById("form" + formNum);
+ ok(form, "Locating form " + formNum);
+
+ var numToCheck = arguments.length - 1;
+
+ if (!numToCheck--) {
+ return;
+ }
+ e = form.elements[0];
+ if (val1 == null) {
+ is(
+ e.value,
+ e.defaultValue,
+ "Test default value of field " + e.name + " in form " + formNum
+ );
+ } else {
+ is(e.value, val1, "Test value of field " + e.name + " in form " + formNum);
+ }
+
+ if (!numToCheck--) {
+ return;
+ }
+ e = form.elements[1];
+ if (val2 == null) {
+ is(
+ e.value,
+ e.defaultValue,
+ "Test default value of field " + e.name + " in form " + formNum
+ );
+ } else {
+ is(e.value, val2, "Test value of field " + e.name + " in form " + formNum);
+ }
+
+ if (!numToCheck--) {
+ return;
+ }
+ e = form.elements[2];
+ if (val3 == null) {
+ is(
+ e.value,
+ e.defaultValue,
+ "Test default value of field " + e.name + " in form " + formNum
+ );
+ } else {
+ is(e.value, val3, "Test value of field " + e.name + " in form " + formNum);
+ }
+}
+
+/**
+ * Check a form for unmodified values from when page was loaded.
+ *
+ * <form id="form#">
+ * checkUnmodifiedForm(#);
+ */
+function checkUnmodifiedForm(formNum) {
+ var form = document.getElementById("form" + formNum);
+ ok(form, "Locating form " + formNum);
+
+ for (var i = 0; i < form.elements.length; i++) {
+ var ele = form.elements[i];
+
+ // No point in checking form submit/reset buttons.
+ if (ele.type == "submit" || ele.type == "reset") {
+ continue;
+ }
+
+ is(
+ ele.value,
+ ele.defaultValue,
+ "Test to default value of field " + ele.name + " in form " + formNum
+ );
+ }
+}
+
+/**
+ * Wait for the document to be ready and any existing password fields on
+ * forms to be processed.
+ *
+ * @param existingPasswordFieldsCount the number of password fields
+ * that begin on the test page.
+ */
+function registerRunTests(existingPasswordFieldsCount = 0) {
+ return new Promise(resolve => {
+ function onDOMContentLoaded() {
+ var form = document.createElement("form");
+ form.id = "observerforcer";
+ var username = document.createElement("input");
+ username.name = "testuser";
+ form.appendChild(username);
+ var password = document.createElement("input");
+ password.name = "testpass";
+ password.type = "password";
+ form.appendChild(password);
+
+ let foundForcer = false;
+ var observer = SpecialPowers.wrapCallback(function (
+ subject,
+ topic,
+ data
+ ) {
+ if (data === "observerforcer") {
+ foundForcer = true;
+ } else {
+ existingPasswordFieldsCount--;
+ }
+
+ if (!foundForcer || existingPasswordFieldsCount > 0) {
+ return;
+ }
+
+ SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
+ form.remove();
+ SimpleTest.executeSoon(() => {
+ var runTestEvent = new Event("runTests");
+ window.dispatchEvent(runTestEvent);
+ resolve();
+ });
+ });
+ SpecialPowers.addObserver(observer, "passwordmgr-processed-form");
+
+ document.body.appendChild(form);
+ }
+ // We provide a general mechanism for our tests to know when they can
+ // safely run: we add a final form that we know will be filled in, wait
+ // for the login manager to tell us that it's filled in and then continue
+ // with the rest of the tests.
+ if (
+ document.readyState == "complete" ||
+ document.readyState == "interactive"
+ ) {
+ onDOMContentLoaded();
+ } else {
+ window.addEventListener("DOMContentLoaded", onDOMContentLoaded);
+ }
+ });
+}
+
+function enablePrimaryPassword() {
+ setPrimaryPassword(true);
+}
+
+function disablePrimaryPassword() {
+ setPrimaryPassword(false);
+}
+
+function setPrimaryPassword(enable) {
+ PWMGR_COMMON_PARENT.sendAsyncMessage("setPrimaryPassword", { enable });
+}
+
+function isLoggedIn() {
+ return PWMGR_COMMON_PARENT.sendQuery("isLoggedIn");
+}
+
+function logoutPrimaryPassword() {
+ runInParent(function parent_logoutPrimaryPassword() {
+ var sdr = Cc["@mozilla.org/security/sdr;1"].getService(
+ Ci.nsISecretDecoderRing
+ );
+ sdr.logoutAndTeardown();
+ });
+}
+
+/**
+ * Resolves when a specified number of forms have been processed for (potential) filling.
+ * This relies on the observer service which only notifies observers within the same process.
+ */
+function promiseFormsProcessedInSameProcess(expectedCount = 1) {
+ var processedCount = 0;
+ return new Promise((resolve, reject) => {
+ function onProcessedForm(subject, topic, data) {
+ processedCount++;
+ if (processedCount == expectedCount) {
+ info(`${processedCount} form(s) processed`);
+ SpecialPowers.removeObserver(
+ onProcessedForm,
+ "passwordmgr-processed-form"
+ );
+ resolve(SpecialPowers.Cu.waiveXrays(subject), data);
+ }
+ }
+ SpecialPowers.addObserver(onProcessedForm, "passwordmgr-processed-form");
+ });
+}
+
+/**
+ * Resolves when a form has been processed for (potential) filling.
+ * This works across processes.
+ */
+async function promiseFormsProcessed(expectedCount = 1) {
+ info(`waiting for ${expectedCount} forms to be processed`);
+ var processedCount = 0;
+ return new Promise(resolve => {
+ PWMGR_COMMON_PARENT.addMessageListener(
+ "formProcessed",
+ function formProcessed() {
+ processedCount++;
+ info(`processed form ${processedCount} of ${expectedCount}`);
+ if (processedCount == expectedCount) {
+ info(`processing of ${expectedCount} forms complete`);
+ PWMGR_COMMON_PARENT.removeMessageListener(
+ "formProcessed",
+ formProcessed
+ );
+ resolve();
+ }
+ }
+ );
+ });
+}
+
+async function loadFormIntoWindow(origin, html, win, expectedCount = 1, task) {
+ let loadedPromise = new Promise(resolve => {
+ win.addEventListener(
+ "load",
+ function (event) {
+ if (event.target.location.href.endsWith("blank.html")) {
+ resolve();
+ }
+ },
+ { once: true }
+ );
+ });
+
+ let processedPromise = promiseFormsProcessed(expectedCount);
+ win.location =
+ origin + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html";
+ info(`Waiting for window to load for origin: ${origin}`);
+ await loadedPromise;
+
+ await SpecialPowers.spawn(
+ win,
+ [html, task?.toString()],
+ function (contentHtml, contentTask = null) {
+ // eslint-disable-next-line no-unsanitized/property
+ this.content.document.documentElement.innerHTML = contentHtml;
+ // Similar to the invokeContentTask helper in accessible/tests/browser/shared-head.js
+ if (contentTask) {
+ // eslint-disable-next-line no-eval
+ const runnableTask = eval(`
+ (() => {
+ return (${contentTask});
+ })();`);
+ runnableTask.call(this);
+ }
+ }
+ );
+
+ info("Waiting for the form to be processed");
+ await processedPromise;
+}
+
+function getTelemetryEvents(options) {
+ return new Promise(resolve => {
+ PWMGR_COMMON_PARENT.addMessageListener(
+ "getTelemetryEvents",
+ function gotResult(events) {
+ info(
+ "CONTENT: getTelemetryEvents gotResult: " + JSON.stringify(events)
+ );
+ PWMGR_COMMON_PARENT.removeMessageListener(
+ "getTelemetryEvents",
+ gotResult
+ );
+ resolve(events);
+ }
+ );
+ PWMGR_COMMON_PARENT.sendAsyncMessage("getTelemetryEvents", options);
+ });
+}
+
+function loadRecipes(recipes) {
+ info("Loading recipes");
+ return new Promise(resolve => {
+ PWMGR_COMMON_PARENT.addMessageListener("loadedRecipes", function loaded() {
+ PWMGR_COMMON_PARENT.removeMessageListener("loadedRecipes", loaded);
+ resolve(recipes);
+ });
+ PWMGR_COMMON_PARENT.sendAsyncMessage("loadRecipes", recipes);
+ });
+}
+
+function resetRecipes() {
+ info("Resetting recipes");
+ return new Promise(resolve => {
+ PWMGR_COMMON_PARENT.addMessageListener("recipesReset", function reset() {
+ PWMGR_COMMON_PARENT.removeMessageListener("recipesReset", reset);
+ resolve();
+ });
+ PWMGR_COMMON_PARENT.sendAsyncMessage("resetRecipes");
+ });
+}
+
+function resetWebsitesWithSharedCredential() {
+ info("Resetting the 'websites-with-shared-credential-backend' collection");
+ return new Promise(resolve => {
+ PWMGR_COMMON_PARENT.addMessageListener(
+ "resetWebsitesWithSharedCredential",
+ function reset() {
+ PWMGR_COMMON_PARENT.removeMessageListener(
+ "resetWebsitesWithSharedCredential",
+ reset
+ );
+ resolve();
+ }
+ );
+ PWMGR_COMMON_PARENT.sendAsyncMessage("resetWebsitesWithSharedCredential");
+ });
+}
+
+function promiseStorageChanged(expectedChangeTypes) {
+ return new Promise((resolve, reject) => {
+ function onStorageChanged({ topic, data }) {
+ let changeType = expectedChangeTypes.shift();
+ is(data, changeType, "Check expected passwordmgr-storage-changed type");
+ if (expectedChangeTypes.length === 0) {
+ PWMGR_COMMON_PARENT.removeMessageListener(
+ "storageChanged",
+ onStorageChanged
+ );
+ resolve();
+ }
+ }
+ PWMGR_COMMON_PARENT.addMessageListener("storageChanged", onStorageChanged);
+ });
+}
+
+function promisePromptShown(expectedTopic) {
+ return new Promise((resolve, reject) => {
+ function onPromptShown({ topic, data }) {
+ is(topic, expectedTopic, "Check expected prompt topic");
+ PWMGR_COMMON_PARENT.removeMessageListener("promptShown", onPromptShown);
+ resolve();
+ }
+ PWMGR_COMMON_PARENT.addMessageListener("promptShown", onPromptShown);
+ });
+}
+
+/**
+ * Run a function synchronously in the parent process and destroy it in the test cleanup function.
+ * @param {Function|String} aFunctionOrURL - either a function that will be stringified and run
+ * or the URL to a JS file.
+ * @return {Object} - the return value of loadChromeScript providing message-related methods.
+ * @see loadChromeScript in specialpowersAPI.js
+ */
+function runInParent(aFunctionOrURL) {
+ let chromeScript = SpecialPowers.loadChromeScript(aFunctionOrURL);
+ SimpleTest.registerCleanupFunction(() => {
+ chromeScript.destroy();
+ });
+ return chromeScript;
+}
+
+/** Manage logins in parent chrome process.
+ * */
+function manageLoginsInParent() {
+ return runInParent(function addLoginsInParentInner() {
+ /* eslint-env mozilla/chrome-script */
+ addMessageListener("removeAllUserFacingLogins", () => {
+ Services.logins.removeAllUserFacingLogins();
+ });
+
+ /* eslint-env mozilla/chrome-script */
+ addMessageListener("addLogins", async logins => {
+ let nsLoginInfo = Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo,
+ "init"
+ );
+
+ const loginInfos = logins.map(login => new nsLoginInfo(...login));
+ try {
+ await Services.logins.addLogins(loginInfos);
+ } catch (e) {
+ assert.ok(false, "addLogins threw: " + e);
+ }
+ });
+ });
+}
+
+/** Initialize with a list of logins. The logins are added within the parent chrome process.
+ * @param {array} aLogins - a list of logins to add. Each login is an array of the arguments
+ * that would be passed to nsLoginInfo.init().
+ */
+async function addLoginsInParent(...aLogins) {
+ const script = manageLoginsInParent();
+ await script.sendQuery("addLogins", aLogins);
+ return script;
+}
+
+/** Initialize with a list of logins, after removing all user facing logins.
+ * The logins are added within the parent chrome process.
+ * @param {array} aLogins - a list of logins to add. Each login is an array of the arguments
+ * that would be passed to nsLoginInfo.init().
+ */
+async function setStoredLoginsAsync(...aLogins) {
+ const script = manageLoginsInParent();
+ script.sendQuery("removeAllUserFacingLogins");
+ await script.sendQuery("addLogins", aLogins);
+ return script;
+}
+
+/*
+ * gTestDependsOnDeprecatedLogin Set this global to true if your test relies
+ * on the testuser/testpass login that is created in pwmgr_common.js. New tests
+ * should not rely on this login.
+ */
+var gTestDependsOnDeprecatedLogin = false;
+
+/**
+ * Replace the content innerHTML with the provided form and wait for autofill to fill in the form.
+ *
+ * @param {string} form The form to be appended to the #content element.
+ * @param {string} fieldSelector The CSS selector for the field to-be-filled
+ * @param {string} fieldValue The value expected to be filled
+ * @param {string} formId The ID (excluding the # character) of the form
+ */
+function setFormAndWaitForFieldFilled(
+ form,
+ { fieldSelector, fieldValue, formId }
+) {
+ // eslint-disable-next-line no-unsanitized/property
+ document.querySelector("#content").innerHTML = form;
+ return SimpleTest.promiseWaitForCondition(() => {
+ let ancestor = formId
+ ? document.querySelector("#" + formId)
+ : document.documentElement;
+ return ancestor.querySelector(fieldSelector).value == fieldValue;
+ }, "Wait for password manager to fill form");
+}
+
+/**
+ * Run commonInit synchronously in the parent then run the test function after the runTests event.
+ *
+ * @param {Function} aFunction The test function to run
+ */
+function runChecksAfterCommonInit(aFunction = null) {
+ SimpleTest.waitForExplicitFinish();
+ if (aFunction) {
+ window.addEventListener("runTests", aFunction);
+ PWMGR_COMMON_PARENT.addMessageListener("registerRunTests", () =>
+ registerRunTests()
+ );
+ }
+ PWMGR_COMMON_PARENT.sendAsyncMessage("setupParent", {
+ testDependsOnDeprecatedLogin: gTestDependsOnDeprecatedLogin,
+ });
+ return PWMGR_COMMON_PARENT;
+}
+
+// Begin code that runs immediately for all tests that include this file.
+
+const PWMGR_COMMON_PARENT = runInParent(
+ SimpleTest.getTestFileURL("pwmgr_common_parent.js")
+);
+
+SimpleTest.registerCleanupFunction(() => {
+ SpecialPowers.flushPrefEnv();
+
+ PWMGR_COMMON_PARENT.sendAsyncMessage("cleanup");
+
+ runInParent(function cleanupParent() {
+ /* eslint-env mozilla/chrome-script */
+ // eslint-disable-next-line no-shadow
+ const { LoginManagerParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginManagerParent.sys.mjs"
+ );
+
+ // Remove all logins and disabled hosts
+ Services.logins.removeAllUserFacingLogins();
+
+ let disabledHosts = Services.logins.getAllDisabledHosts();
+ disabledHosts.forEach(host =>
+ Services.logins.setLoginSavingEnabled(host, true)
+ );
+
+ let authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].getService(
+ Ci.nsIHttpAuthManager
+ );
+ authMgr.clearAll();
+
+ // Check that it's not null, instead of truthy to catch it becoming undefined
+ // in a refactoring.
+ if (LoginManagerParent._recipeManager !== null) {
+ LoginManagerParent._recipeManager.reset();
+ }
+
+ // Cleanup PopupNotifications (if on a relevant platform)
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (chromeWin && chromeWin.PopupNotifications) {
+ let notes = chromeWin.PopupNotifications._currentNotifications;
+ if (notes.length) {
+ dump("Removing " + notes.length + " popup notifications.\n");
+ }
+ for (let note of notes) {
+ note.remove();
+ }
+ }
+
+ // Clear events last in case the above cleanup records events.
+ Services.telemetry.clearEvents();
+ });
+});
+
+/**
+ * Proxy for Services.logins (nsILoginManager).
+ * Only supports arguments which support structured clone plus {nsILoginInfo}
+ * Assumes properties are methods.
+ */
+this.LoginManager = new Proxy(
+ {},
+ {
+ get(target, prop, receiver) {
+ return (...args) => {
+ let loginInfoIndices = [];
+ let cloneableArgs = args.map((val, index) => {
+ if (
+ SpecialPowers.call_Instanceof(val, SpecialPowers.Ci.nsILoginInfo)
+ ) {
+ loginInfoIndices.push(index);
+ return LoginHelper.loginToVanillaObject(val);
+ }
+
+ return val;
+ });
+
+ return PWMGR_COMMON_PARENT.sendQuery("proxyLoginManager", {
+ args: cloneableArgs,
+ loginInfoIndices,
+ methodName: prop,
+ });
+ };
+ },
+ }
+);
diff --git a/toolkit/components/passwordmgr/test/mochitest/pwmgr_common_parent.js b/toolkit/components/passwordmgr/test/mochitest/pwmgr_common_parent.js
new file mode 100644
index 0000000000..00173cf323
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/pwmgr_common_parent.js
@@ -0,0 +1,249 @@
+/**
+ * Loaded as a frame script to do privileged things in mochitest-plain tests.
+ * See pwmgr_common.js for the content process companion.
+ */
+
+/* eslint-env mozilla/chrome-script */
+
+"use strict";
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { LoginHelper } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginHelper.sys.mjs"
+);
+var { LoginManagerParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginManagerParent.sys.mjs"
+);
+const { LoginTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/LoginTestUtils.sys.mjs"
+);
+if (LoginHelper.relatedRealmsEnabled) {
+ let rsPromise =
+ LoginTestUtils.remoteSettings.setupWebsitesWithSharedCredentials();
+ async () => {
+ await rsPromise;
+ };
+}
+if (LoginHelper.improvedPasswordRulesEnabled) {
+ let rsPromise = LoginTestUtils.remoteSettings.setupImprovedPasswordRules({
+ rules: "",
+ });
+ async () => {
+ await rsPromise;
+ };
+}
+
+/**
+ * Init with a common login
+ * If selfFilling is true or non-undefined, fires an event at the page so that
+ * the test can start checking filled-in values. Tests that check observer
+ * notifications might be confused by this.
+ */
+async function commonInit(selfFilling, testDependsOnDeprecatedLogin) {
+ var pwmgr = Services.logins;
+ assert.ok(pwmgr != null, "Access LoginManager");
+
+ // Check that initial state has no logins
+ var logins = pwmgr.getAllLogins();
+ assert.equal(logins.length, 0, "Not expecting logins to be present");
+ var disabledHosts = pwmgr.getAllDisabledHosts();
+ if (disabledHosts.length) {
+ assert.ok(false, "Warning: wasn't expecting disabled hosts to be present.");
+ for (var host of disabledHosts) {
+ pwmgr.setLoginSavingEnabled(host, true);
+ }
+ }
+
+ if (testDependsOnDeprecatedLogin) {
+ // Add a login that's used in multiple tests
+ var login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ login.init(
+ "http://mochi.test:8888",
+ "http://mochi.test:8888",
+ null,
+ "testuser",
+ "testpass",
+ "uname",
+ "pword"
+ );
+ await pwmgr.addLoginAsync(login);
+ }
+
+ // Last sanity check
+ logins = pwmgr.getAllLogins();
+ assert.equal(
+ logins.length,
+ testDependsOnDeprecatedLogin ? 1 : 0,
+ "Checking for successful init login"
+ );
+ disabledHosts = pwmgr.getAllDisabledHosts();
+ assert.equal(disabledHosts.length, 0, "Checking for no disabled hosts");
+
+ if (selfFilling) {
+ return;
+ }
+
+ // Notify the content side that initialization is done and tests can start.
+ sendAsyncMessage("registerRunTests");
+}
+
+function dumpLogins() {
+ let logins = Services.logins.getAllLogins();
+ assert.ok(true, "----- dumpLogins: have " + logins.length + " logins. -----");
+ for (var i = 0; i < logins.length; i++) {
+ dumpLogin("login #" + i + " --- ", logins[i]);
+ }
+}
+
+function dumpLogin(label, login) {
+ var loginText = "";
+ loginText += "origin: ";
+ loginText += login.origin;
+ loginText += " / formActionOrigin: ";
+ loginText += login.formActionOrigin;
+ loginText += " / realm: ";
+ loginText += login.httpRealm;
+ loginText += " / user: ";
+ loginText += login.username;
+ loginText += " / pass: ";
+ loginText += login.password;
+ loginText += " / ufield: ";
+ loginText += login.usernameField;
+ loginText += " / pfield: ";
+ loginText += login.passwordField;
+ assert.ok(true, label + loginText);
+}
+
+function onStorageChanged(subject, topic, data) {
+ sendAsyncMessage("storageChanged", {
+ topic,
+ data,
+ });
+}
+Services.obs.addObserver(onStorageChanged, "passwordmgr-storage-changed");
+
+function onPrompt(subject, topic, data) {
+ sendAsyncMessage("promptShown", {
+ topic,
+ data,
+ });
+}
+Services.obs.addObserver(onPrompt, "passwordmgr-prompt-change");
+Services.obs.addObserver(onPrompt, "passwordmgr-prompt-save");
+
+addMessageListener("cleanup", () => {
+ Services.obs.removeObserver(onStorageChanged, "passwordmgr-storage-changed");
+ Services.obs.removeObserver(onPrompt, "passwordmgr-prompt-change");
+ Services.obs.removeObserver(onPrompt, "passwordmgr-prompt-save");
+ Services.logins.removeAllUserFacingLogins();
+});
+
+// Begin message listeners
+
+addMessageListener(
+ "setupParent",
+ async ({
+ selfFilling = false,
+ testDependsOnDeprecatedLogin = false,
+ } = {}) => {
+ await commonInit(selfFilling, testDependsOnDeprecatedLogin);
+ sendAsyncMessage("doneSetup");
+ }
+);
+
+addMessageListener("loadRecipes", async function (recipes) {
+ var recipeParent = await LoginManagerParent.recipeParentPromise;
+ await recipeParent.load(recipes);
+ sendAsyncMessage("loadedRecipes", recipes);
+});
+
+addMessageListener("resetRecipes", async function () {
+ let recipeParent = await LoginManagerParent.recipeParentPromise;
+ await recipeParent.reset();
+ sendAsyncMessage("recipesReset");
+});
+
+addMessageListener("getTelemetryEvents", options => {
+ options = Object.assign(
+ {
+ filterProps: {},
+ clear: false,
+ },
+ options
+ );
+ let snapshots = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ options.clear
+ );
+ let events = options.process in snapshots ? snapshots[options.process] : [];
+
+ // event is array of values like: [22476,"pwmgr","autocomplete_field","generatedpassword"]
+ let keys = ["id", "category", "method", "object", "value"];
+ events = events.filter(entry => {
+ for (let idx = 0; idx < keys.length; idx++) {
+ let key = keys[idx];
+ if (
+ key in options.filterProps &&
+ options.filterProps[key] !== entry[idx]
+ ) {
+ return false;
+ }
+ }
+ return true;
+ });
+ sendAsyncMessage("getTelemetryEvents", events);
+});
+
+addMessageListener("proxyLoginManager", msg => {
+ // Recreate nsILoginInfo objects from vanilla JS objects.
+ let recreatedArgs = msg.args.map((arg, index) => {
+ if (msg.loginInfoIndices.includes(index)) {
+ return LoginHelper.vanillaObjectToLogin(arg);
+ }
+
+ return arg;
+ });
+
+ let rv = Services.logins[msg.methodName](...recreatedArgs);
+ if (rv instanceof Ci.nsILoginInfo) {
+ rv = LoginHelper.loginToVanillaObject(rv);
+ } else if (
+ Array.isArray(rv) &&
+ !!rv.length &&
+ rv[0] instanceof Ci.nsILoginInfo
+ ) {
+ rv = rv.map(login => LoginHelper.loginToVanillaObject(login));
+ }
+ return rv;
+});
+
+addMessageListener("isLoggedIn", () => {
+ // This can't use the LoginManager proxy in pwmgr_common.js since it's not a method.
+ return Services.logins.isLoggedIn;
+});
+
+addMessageListener("setPrimaryPassword", ({ enable }) => {
+ if (enable) {
+ LoginTestUtils.primaryPassword.enable();
+ } else {
+ LoginTestUtils.primaryPassword.disable();
+ }
+});
+
+LoginManagerParent.setListenerForTests((msg, { origin, data }) => {
+ if (msg == "ShowDoorhanger") {
+ sendAsyncMessage("formSubmissionProcessed", { origin, data });
+ } else if (msg == "PasswordEditedOrGenerated") {
+ sendAsyncMessage("passwordEditedOrGenerated", { origin, data });
+ } else if (msg == "FormProcessed") {
+ sendAsyncMessage("formProcessed", {});
+ }
+});
+
+addMessageListener("cleanup", () => {
+ LoginManagerParent.setListenerForTests(null);
+});
diff --git a/toolkit/components/passwordmgr/test/mochitest/slow_image.html b/toolkit/components/passwordmgr/test/mochitest/slow_image.html
new file mode 100644
index 0000000000..172d592633
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/slow_image.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <img src="slow_image.sjs" />
+ </body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/slow_image.sjs b/toolkit/components/passwordmgr/test/mochitest/slow_image.sjs
new file mode 100644
index 0000000000..b955e43f5d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/slow_image.sjs
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// The delay time will not impact test running time, as the test does
+// not wait for the "load" event. We just need to ensure the pwmgr code
+// e.g. autofill happens before the delay time elapses.
+const DELAY_MS = "5000";
+
+let timer;
+
+function handleRequest(req, resp) {
+ resp.processAsync();
+ resp.setHeader("Cache-Control", "no-cache", false);
+ resp.setHeader("Content-Type", "image/png", false);
+
+ timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.init(
+ () => {
+ resp.write("");
+ resp.finish();
+ },
+ DELAY_MS,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+}
diff --git a/toolkit/components/passwordmgr/test/mochitest/subtst_prefilled_form.html b/toolkit/components/passwordmgr/test/mochitest/subtst_prefilled_form.html
new file mode 100644
index 0000000000..8a3e0b0240
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/subtst_prefilled_form.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!-- Form with username and password fields pre-populated.
+ With a couple elements where .defaultValue !== .value -->
+<form id="form-basic">
+ <input id="form-basic-username" name="username" value="user">
+ <input id="form-basic-password" name="password" type="password" value="pass">
+ <select name="picker">
+ <option value="foo">0</option>
+ <option value="bar" selected>1</option>
+ </select>
+ <input id="form-basic-submit" type="submit">
+ <button id="form-basic-reset" type="reset">Reset</button>
+</form>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/subtst_primary_pass.html b/toolkit/components/passwordmgr/test/mochitest/subtst_primary_pass.html
new file mode 100644
index 0000000000..dc892c50e2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/subtst_primary_pass.html
@@ -0,0 +1,8 @@
+<h2>MP subtest</h2>
+This form triggers a MP and gets filled in.<br>
+<form>
+Username: <input type="text" id="userfield" name="u"><br>
+Password: <input type="password" id="passfield" name="p"
+ oninput="parent.postMessage('filled', '*');"><br>
+</form>
+<iframe></iframe>
diff --git a/toolkit/components/passwordmgr/test/mochitest/subtst_prompt_async.html b/toolkit/components/passwordmgr/test/mochitest/subtst_prompt_async.html
new file mode 100644
index 0000000000..39a72add56
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/subtst_prompt_async.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Multiple auth request</title>
+</head>
+<body>
+ <iframe id="iframe1" src="authenticate.sjs?r=1&user=user3name&pass=user3pass&realm=mochirealm3&proxy_user=proxy_user2&proxy_pass=proxy_pass2&proxy_realm=proxy_realm2"></iframe>
+ <iframe id="iframe2" src="authenticate.sjs?r=2&user=user3name&pass=user3pass&realm=mochirealm3&proxy_user=proxy_user2&proxy_pass=proxy_pass2&proxy_realm=proxy_realm2"></iframe>
+ <iframe id="iframe3" src="authenticate.sjs?r=3&user=user3name&pass=user3pass&realm=mochirealm3&proxy_user=proxy_user2&proxy_pass=proxy_pass2&proxy_realm=proxy_realm2"></iframe>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_DOMInputPasswordAdded_fired_between_DOMContentLoaded_and_load_events.html b/toolkit/components/passwordmgr/test/mochitest/test_DOMInputPasswordAdded_fired_between_DOMContentLoaded_and_load_events.html
new file mode 100644
index 0000000000..4f4f0fedc2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_DOMInputPasswordAdded_fired_between_DOMContentLoaded_and_load_events.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test the password manager code called on DOMInputPasswordAdded runs when it occurs between DOMContentLoaded and load events</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <!-- In cases where the "DOMContentLoaded" event for a page has occured but not the "load" event when
+ "DOMInputPasswordAdded" fires, we want to make sure that the Password Manager code (i.e.
+ _fetchLoginsFromParentAndFillForm) still runs on the page.
+ This scenario can happen for example when a page has very little initial HTML, but extensive JS that
+ adds Custom Elements or other HTML later, or when other subresources, like images, take a while to load.
+ In this test, we delay the page load with a delayed response for an image source. -->
+<script type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+
+let DEFAULT_ORIGIN = window.location.origin;
+let FILE_PATH = "/tests/toolkit/components/passwordmgr/test/mochitest/slow_image.html";
+
+// Bug 1655195: Sometimes saved logins are not removed in between mochitests when this test is run as
+// part of a suite
+runInParent(function removeAll() {
+ Services.logins.removeAllUserFacingLogins();
+});
+
+let readyPromise = registerRunTests();
+
+async function openDocumentInWindow(win) {
+ let DOMContentLoadedPromise = new Promise((resolve) => {
+ win.addEventListener("DOMContentLoaded", function() {
+ resolve();
+ }, {once: true});
+ });
+ win.location = DEFAULT_ORIGIN + FILE_PATH;
+ await DOMContentLoadedPromise;
+}
+
+async function test_password_autofilled() {
+ info("Adding one login for the page");
+ await addLoginsInParent([DEFAULT_ORIGIN, DEFAULT_ORIGIN, null, "user", "omgsecret!"]);
+
+ let numLogins = await LoginManager.countLogins(DEFAULT_ORIGIN, DEFAULT_ORIGIN, null);
+ is(numLogins, 1, "Correct number of logins");
+
+ let win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await openDocumentInWindow(win);
+ let processedPromise = promiseFormsProcessed();
+ await SpecialPowers.spawn(win, [], function() {
+ let doc = this.content.document;
+ info("Adding password input field to the page to trigger DOMInputPasswordAdded");
+ let passwordField = doc.createElement("input");
+ passwordField.type = "password";
+ is(doc.readyState, "interactive", "Make sure 'DOMContentLoaded' has fired but not 'load'");
+ doc.body.append(passwordField);
+ });
+ info("Waiting for the password field to be autofilled");
+ await processedPromise;
+ let expectedValue = "omgsecret!";
+ await SpecialPowers.spawn(win, [expectedValue], expectedValueF => {
+ is(this.content.document.querySelector("input[type='password']").value, expectedValueF, "Ensure the password field is autofilled");
+ });
+
+ SimpleTest.finish();
+}
+
+readyPromise.then(async () => test_password_autofilled());
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_LoginManagerContent_passwordEditedOrGenerated.html b/toolkit/components/passwordmgr/test/mochitest/test_LoginManagerContent_passwordEditedOrGenerated.html
new file mode 100644
index 0000000000..3c8c92bd97
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_LoginManagerContent_passwordEditedOrGenerated.html
@@ -0,0 +1,160 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test behavior of unmasking in LMC._passwordEditedOrGenerated</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test"></pre>
+<script>
+const { LoginManagerChild } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/LoginManagerChild.jsm"
+);
+
+function preventDefaultAndStopProgagation(event) {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+}
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["signon.generation.available", true],
+ ["signon.generation.enabled", true],
+ ]});
+ await setStoredLoginsAsync(
+ [location.origin, "https://autofill", null, "user1", "pass1"],
+ [location.origin, "https://autofill", null, "user2", "pass2"]
+ );
+});
+
+add_task(async function prevent_default_and_stop_propagation() {
+ const form = createLoginForm({
+ action: "https://autofill"
+ });
+ await promiseFormsProcessedInSameProcess();
+ form.pword.focus();
+
+ form.pword.addEventListener("focus", preventDefaultAndStopProgagation);
+ form.pword.addEventListener("focus", preventDefaultAndStopProgagation, true);
+ form.pword.addEventListener("blur", preventDefaultAndStopProgagation);
+ form.pword.addEventListener("blur", preventDefaultAndStopProgagation, true);
+
+ SpecialPowers.wrap(form.pword).setUserInput("generatedpass");
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "Before first fill");
+ LoginManagerChild.forWindow(window)._passwordEditedOrGenerated(form.pword, { triggeredByFillingGenerated: true });
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "After first fill");
+ synthesizeKey("KEY_Tab"); // blur
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After blur");
+ synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "After shift-tab to focus again");
+});
+
+add_task(async function fields_masked_after_saved_login_fill() {
+ const form = createLoginForm({
+ action: "https://autofill"
+ });
+ await promiseFormsProcessedInSameProcess();
+ form.pword.focus();
+
+ SpecialPowers.wrap(form.pword).setUserInput("generatedpass");
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "Before first fill");
+ LoginManagerChild.forWindow(window)._passwordEditedOrGenerated(form.pword, { triggeredByFillingGenerated: true });
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "After first fill");
+ synthesizeKey("KEY_Tab", { shiftKey: true }); // blur pw, focus un
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After blur");
+
+ info("Filling username matching saved login");
+ sendString("user1");
+
+ let processedPromise = promiseFormsProcessedInSameProcess();
+ synthesizeKey("KEY_Tab"); // focus again and trigger a fill of the matching password
+ await processedPromise;
+ is(form.pword.value, "pass1", "Saved password was filled")
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After filling a saved login");
+});
+
+add_task(async function fields_masked_after_replacing_whole_value() {
+ const form = createLoginForm({
+ action: "https://autofill"
+ });
+ await promiseFormsProcessedInSameProcess();
+ form.pword.focus();
+
+ SpecialPowers.wrap(form.pword).setUserInput("generatedpass");
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "Before first fill");
+ LoginManagerChild.forWindow(window)._passwordEditedOrGenerated(form.pword, { triggeredByFillingGenerated: true });
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "After first fill");
+ synthesizeKey("KEY_Tab", { shiftKey: true }); // blur pw, focus un
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After blur");
+
+ synthesizeKey("KEY_Tab"); // focus again and replace the whole password value
+ info("Replacing password field value with arbitrary string");
+ sendString("some_other_password");
+ is(form.pword.value, "some_other_password", "Whole password replaced")
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "Replaced password value");
+
+ synthesizeKey("KEY_Tab"); // blur pw
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After blur");
+ synthesizeKey("KEY_Tab", { shiftKey: true }); // focus pw again
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After focus again");
+});
+
+add_task(async function fields_unmasked_after_adding_character() {
+ const form = createLoginForm({
+ action: "https://autofill"
+ });
+ await promiseFormsProcessedInSameProcess();
+ form.pword.focus();
+
+ SpecialPowers.wrap(form.pword).setUserInput("generatedpass");
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "Before first fill");
+ LoginManagerChild.forWindow(window)._passwordEditedOrGenerated(form.pword, { triggeredByFillingGenerated: true });
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "After first fill");
+ synthesizeKey("KEY_Tab", { shiftKey: true }); // blur pw, focus un
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After blur");
+
+ synthesizeKey("KEY_Tab"); // focus again
+ synthesizeKey("KEY_ArrowRight"); // Remove the selection
+ info("Adding a character to the end of the password");
+ sendString("@");
+ is(form.pword.value, "generatedpass@", "Character was added to the value")
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "Added @");
+
+ synthesizeKey("KEY_Tab"); // blur pw
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After blur after @");
+ synthesizeKey("KEY_Tab", { shiftKey: true }); // focus pw again
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "After focus after @");
+});
+
+add_task(async function type_not_password() {
+ const form = createLoginForm({
+ action: "https://autofill"
+ });
+ await promiseFormsProcessedInSameProcess();
+ form.pword.focus();
+
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "Before first fill");
+ SpecialPowers.wrap(form.pword).setUserInput("generatedpass");
+ LoginManagerChild.forWindow(window)._passwordEditedOrGenerated(form.pword, { triggeredByFillingGenerated: true });
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "After first fill");
+
+ // Simulate a website doing their own unmasking and re-masking
+ form.pword.type = "text";
+ await new Promise(resolve => SimpleTest.executeSoon(resolve));
+ form.pword.type = "password";
+
+ synthesizeKey("KEY_Tab"); // blur
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After blur");
+ synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "After shift-tab to focus again");
+});
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_autofill_related_realms_no_dupes.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_autofill_related_realms_no_dupes.html
new file mode 100644
index 0000000000..243ea9f052
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_autofill_related_realms_no_dupes.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test no duplicate logins using autofill/autocomplete with related realms</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+Login Manager test: no duplicate logins when using autofill and autocomplete with related realms
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: related realms autofill/autocomplete. **/
+
+function restoreForm(form) {
+ form.uname.value = "";
+ form.pword.value = "";
+ form.uname.focus();
+}
+
+function sendFakeAutocompleteEvent(element) {
+ const acEvent = document.createEvent("HTMLEvents");
+ acEvent.initEvent("DOMAutoComplete", true, false);
+ element.dispatchEvent(acEvent);
+}
+
+function spinEventLoop() {
+ return Promise.resolve();
+}
+
+async function promiseACPopupClosed() {
+ return SimpleTest.promiseWaitForCondition(async () => {
+ const popupState = await getPopupState();
+ return !popupState.open;
+ }, "Wait for AC popup to be closed");
+}
+
+add_setup(async () => {
+ await addLoginsInParent(
+ // Simple related domain relationship where example.com and other-example.com are in the related domains list
+ ["https://other-example.com", "https://other-example.com", null, "relatedUser1", "relatedPass1", "uname", "pword"],
+
+ // Example.com and example.co.uk are related, so sub.example.co.uk should appear on example.com's autocomplete dropdown
+ // The intent is to cover the ebay.com/ebay.co.uk and all other country TLD cases
+ // where the sign in page is actually signin.ebay.com/signin.ebay.co.uk but credentials could have manually been entered
+ // for ebay.com/ebay.co.uk or automatically stored as signin.ebay.com/sigin.ebay.co.uk
+ ["https://sub.example.co.uk", "https://sub.example.co.uk", null, "subUser1", "subPass1", "uname", "pword"],
+
+ // Ensures there are no duplicates for the exact domain that the user is on
+ ["https://example.com", "https://example.com", null, "exactUser1", "exactPass1", "uname", "pword"],
+ ["https://www.example.com", "https://www.example.com", null, "exactWWWUser1", "exactWWWPass1", "uname", "pword"],
+ );
+ listenForUnexpectedPopupShown();
+});
+
+add_task(async function test_no_duplicates_autocomplete_autofill() {
+ const form = createLoginForm({
+ action: "https://www.example.com"
+ });
+
+ await promiseFormsProcessedInSameProcess();
+ await SimpleTest.promiseFocus(window);
+ const results = await popupBy(() => {
+ checkLoginForm(form.uname, "exactUser1", form.pword, "exactPass1")
+ restoreForm(form);
+ });
+
+ const popupState = await getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entires are selected upon opening");
+ const expectedMenuItems = ["exactUser1", "exactWWWUser1", "relatedUser1", "subUser1"];
+ checkAutoCompleteResults(results, expectedMenuItems, window.location.host, "Check all menuitems are displayed correctly");
+
+ const acEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsAC, clear: true });
+ is(acEvents.length, 1, "One autocomplete event");
+ checkACTelemetryEvent(acEvents[0], form.uname, {
+ "hadPrevious": "0",
+ "login": expectedMenuItems.length + "",
+ "loginsFooter": "1"
+ });
+ restoreForm(form);
+ synthesizeKey("KEY_Escape");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form.html
new file mode 100644
index 0000000000..a061171279
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form.html
@@ -0,0 +1,894 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic login autocomplete</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: multiple login autocomplete
+
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content"></div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: multiple login autocomplete. **/
+
+// Restore the form to the default state.
+function restoreForm(form) {
+ form.uname.value = "";
+ form.pword.value = "";
+ form.uname.focus();
+}
+
+function sendFakeAutocompleteEvent(element) {
+ var acEvent = document.createEvent("HTMLEvents");
+ acEvent.initEvent("DOMAutoComplete", true, false);
+ element.dispatchEvent(acEvent);
+}
+
+function spinEventLoop() {
+ return Promise.resolve();
+}
+
+async function promiseACPopupClosed() {
+ return SimpleTest.promiseWaitForCondition(async () => {
+ let popupState = await getPopupState();
+ return !popupState.open;
+ }, "Wait for AC popup to be closed");
+}
+
+add_setup(async () => {
+ listenForUnexpectedPopupShown();
+});
+
+add_task(async function form1_initial_empty() {
+ const form = createLoginForm({
+ action: "https://autocomplete:8888/formtest.js"
+ });
+ await SimpleTest.promiseFocus(window);
+
+ // Make sure initial form is empty.
+ checkLoginForm(form.uname, "", form.pword, "");
+ let popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+add_task(async function form1_menuitems() {
+ await setStoredLoginsAsync(
+ // login 0 has no username, so should be filtered out from the autocomplete list.
+ [location.origin, "https://autocomplete:8888", null, "", "pass0", "", "pword"],
+
+ [location.origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"],
+ [location.origin, "https://autocomplete:8888", null, "user-3", "pass-3", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete:8888/formtest.js"
+ });
+ await SimpleTest.promiseFocus(window);
+ // Trigger autocomplete popup
+ form.uname.focus();
+ const autocompleteItems = await popupByArrowDown();
+
+ let popupState = await getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ let expectedMenuItems = ["user-1",
+ "user-2",
+ "user-3"];
+ checkAutoCompleteResults(autocompleteItems, expectedMenuItems,
+ window.location.host, "Check all menuitems are displayed correctly.");
+
+ let acEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsAC, clear: true });
+ is(acEvents.length, 1, "One autocomplete event");
+ checkACTelemetryEvent(acEvents[0], form.uname, {
+ "hadPrevious": "0",
+ "login": expectedMenuItems.length + "",
+ "loginsFooter": "1"
+ });
+
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update just by opening
+ synthesizeKey("KEY_Escape");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "", form.pword, "");
+});
+
+add_task(async function form1_first_entry() {
+ await setStoredLoginsAsync(
+ [origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ await SimpleTest.promiseFocus(window);
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+ let popupState = await getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+ synthesizeKey("KEY_ArrowDown"); // first
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update just by selecting
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-1", form.pword, "pass-1");
+});
+
+add_task(async function form1_second_entry() {
+ await setStoredLoginsAsync(
+ [origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_ArrowDown"); // second
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-2", form.pword, "pass-2");
+});
+
+add_task(async function form1_wraparound_first_entry() {
+ await setStoredLoginsAsync(
+ [origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_ArrowDown"); // second
+ synthesizeKey("KEY_ArrowDown"); // footer
+ synthesizeKey("KEY_ArrowDown"); // deselects
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-1", form.pword, "pass-1");
+});
+
+add_task(async function form1_wraparound_up_last_entry() {
+ await setStoredLoginsAsync(
+ [origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+ synthesizeKey("KEY_ArrowUp"); // footer
+ synthesizeKey("KEY_ArrowUp"); // last (fourth)
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-2", form.pword, "pass-2");
+});
+
+add_task(async function form1_wraparound_down_up_up() {
+ await setStoredLoginsAsync(
+ [origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+ synthesizeKey("KEY_ArrowDown"); // select first entry
+ synthesizeKey("KEY_ArrowUp"); // selects nothing!
+ synthesizeKey("KEY_ArrowUp"); // footer
+ synthesizeKey("KEY_ArrowUp"); // select last entry
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-2", form.pword, "pass-2");
+});
+
+add_task(async function form1_wraparound_up_last() {
+ await setStoredLoginsAsync(
+ [origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_ArrowUp"); // deselects
+ synthesizeKey("KEY_ArrowUp"); // footer
+ synthesizeKey("KEY_ArrowUp"); // last entry
+ synthesizeKey("KEY_ArrowUp"); // first entry
+ synthesizeKey("KEY_ArrowUp"); // deselects
+ synthesizeKey("KEY_ArrowUp"); // footer
+ synthesizeKey("KEY_ArrowUp"); // last entry
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-2", form.pword, "pass-2");
+});
+
+add_task(async function form1_fill_username_without_autofill_right() {
+ await setStoredLoginsAsync(
+ [origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+ // Set first entry w/o triggering autocomplete
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_ArrowRight");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-1", form.pword, ""); // empty password
+});
+
+add_task(async function form1_fill_username_without_autofill_left() {
+ await setStoredLoginsAsync(
+ [origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+ // Set first entry w/o triggering autocomplete
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_ArrowLeft");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-1", form.pword, ""); // empty password
+});
+
+add_task(async function form1_pageup_first() {
+ await setStoredLoginsAsync(
+ [origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+ // Check first entry (page up)
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_ArrowDown"); // second
+ synthesizeKey("KEY_PageUp"); // first
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-1", form.pword, "pass-1");
+});
+
+add_task(async function form1_pagedown_last() {
+ await setStoredLoginsAsync(
+ [origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+ /* test 13 */
+ // Check last entry (page down)
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_PageDown"); // footer
+ synthesizeKey("KEY_ArrowUp"); // last
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-2", form.pword, "pass-2");
+});
+
+add_task(async function form1_untrusted_event() {
+ await setStoredLoginsAsync(
+ [origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+ // Send a fake (untrusted) event.
+ checkLoginForm(form.uname, "", form.pword, "");
+ form.uname.value = "user-2";
+ sendFakeAutocompleteEvent(form.uname);
+ checkLoginForm(form.uname, "user-2", form.pword, "");
+});
+
+add_task(async function form1_delete() {
+ await setStoredLoginsAsync(
+ [origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"],
+ [origin, "https://autocomplete:8888", null, "user-3", "pass-3", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+
+ // XXX tried sending character "t" before/during dropdown to test
+ // filtering, but had no luck. Seemed like the character was getting lost.
+ // Setting uname.value didn't seem to work either. This works with a human
+ // driver, so I'm not sure what's up.
+
+ // Delete the first entry (of 3), "user-1"
+ synthesizeKey("KEY_ArrowDown");
+ const numLoginsBeforeDeletion = await LoginManager.countLogins(origin, "https://autocomplete:8888", null);
+ is(numLoginsBeforeDeletion, 3, "Correct number of logins before deleting one");
+
+ const countChangedPromise = notifyMenuChanged(3);
+ const deletionPromise = promiseStorageChanged(["removeLogin"]);
+ // On OS X, shift-backspace and shift-delete work, just delete does not.
+ // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
+ synthesizeKey("KEY_Delete", {shiftKey: true});
+ await deletionPromise;
+
+ checkLoginForm(form.uname, "", form.pword, "");
+ const numLoginsAfterDeletion = await LoginManager.countLogins(origin, "https://autocomplete:8888", null);
+ is(numLoginsAfterDeletion, 2, "Correct number of logins after deleting one");
+ await countChangedPromise;
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-2", form.pword, "pass-2");
+});
+
+add_task(async function form1_delete_second() {
+ await setStoredLoginsAsync(
+ [origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"],
+ [origin, "https://autocomplete:8888", null, "user-3", "pass-3", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+
+ // Delete the second entry (of 3), "user-2"
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_Delete", {shiftKey: true});
+ checkLoginForm(form.uname, "", form.pword, "");
+ const numLoginsAfterDeletion = await LoginManager.countLogins(origin, "https://autocomplete:8888", null);
+ is(numLoginsAfterDeletion, 2, "Correct number of logins after deleting one");
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-3", form.pword, "pass-3");
+});
+
+add_task(async function form1_delete_last() {
+ await setStoredLoginsAsync(
+ [origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"],
+ [origin, "https://autocomplete:8888", null, "user-3", "pass-3", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+
+ /* test 54 */
+ // Delete the last entry (of 3), "user-3"
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_ArrowDown");
+ const numLoginsBeforeDeletion = await LoginManager.countLogins(origin, "https://autocomplete:8888", null);
+ is(numLoginsBeforeDeletion, 3, "Correct number of logins before deleting one");
+ synthesizeKey("KEY_Delete", {shiftKey: true});
+ checkLoginForm(form.uname, "", form.pword, "");
+ const numLoginsAfterDeletion = await LoginManager.countLogins(origin, "https://autocomplete:8888", null);
+ is(numLoginsAfterDeletion, 2, "Correct number of logins after deleting one");
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-1", form.pword, "pass-1");
+});
+
+// Tests for single-user forms for ignoring autocomplete=off */
+
+add_task(async function form_default() {
+ await setStoredLoginsAsync(
+ [location.origin, "https://autocomplete2", null, "user", "pass", "uname", "pword"],
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete2"
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+});
+
+add_task(async function password_autocomplete_off() {
+ await setStoredLoginsAsync(
+ [location.origin, "https://autocomplete2", null, "user", "pass", "uname", "pword"],
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete2",
+ password: {
+ autocomplete: "off"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+
+ // Trigger autocomplete popup
+ restoreForm(form);
+ await popupByArrowDown();
+
+ // Check first entry
+ synthesizeKey("KEY_ArrowDown");
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+});
+
+add_task(async function username_autocomplete_off() {
+ await setStoredLoginsAsync(
+ [location.origin, "https://autocomplete2", null, "user", "pass", "uname", "pword"],
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete2",
+ username: {
+ autocomplete: "off"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+
+ restoreForm(form);
+ await popupByArrowDown();
+
+ // Check first entry
+ synthesizeKey("KEY_ArrowDown");
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+});
+
+add_task(async function form_autocomplete_off() {
+ await setStoredLoginsAsync(
+ [location.origin, "https://autocomplete2", null, "user", "pass", "uname", "pword"],
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete2",
+ autocomplete: "off"
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+ restoreForm(form);
+ await popupByArrowDown();
+
+ // Check first entry
+ synthesizeKey("KEY_ArrowDown");
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+});
+
+add_task(async function username_and_password_autocomplete_off() {
+ await setStoredLoginsAsync(
+ [location.origin, "https://autocomplete2", null, "user", "pass", "uname", "pword"],
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete2",
+ username: {
+ autocomplete: "off"
+ },
+ password: {
+ autocomplete: "off"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+ restoreForm(form);
+ await popupByArrowDown();
+
+ // Check first entry
+ synthesizeKey("KEY_ArrowDown");
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+});
+
+add_task(async function changing_username_does_not_touch_password() {
+ await setStoredLoginsAsync(
+ [location.origin, "https://autocomplete2", null, "user", "pass", "uname", "pword"],
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete2",
+ username: {
+ autocomplete: "off"
+ },
+ password: {
+ autocomplete: "off"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+
+ // Test that the password field remains filled in after changing
+ // the username.
+ form.uname.focus();
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("X", {shiftKey: true});
+ // Trigger the 'blur' event on uname
+ form.pword.focus();
+ await spinEventLoop();
+ checkLoginForm(form.uname, "userX", form.pword, "pass");
+});
+
+add_task(async function form7() {
+ await setStoredLoginsAsync(
+ [location.origin, "https://autocomplete3", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "https://autocomplete3", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete3"
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ checkLoginForm(form.uname, "", form.pword, "");
+
+ // Insert a new username field into the form. We'll then make sure
+ // that invoking the autocomplete doesn't try to fill the form.
+ const newField = document.createElement("input");
+ newField.setAttribute("type", "text");
+ newField.setAttribute("name", "uname2");
+ form.insertBefore(newField, form.pword);
+ await spinEventLoop();
+ is(newField.value, "", "Verifying empty uname2");
+});
+
+add_task(async function form7_2() {
+ await setStoredLoginsAsync(
+ [location.origin, "https://autocomplete3", null, "user", "pass", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete3"
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ // Insert a new username field into the form. We'll then make sure
+ // that invoking the autocomplete doesn't try to fill the form.
+ const newField = document.createElement("input");
+ newField.setAttribute("type", "text");
+ newField.setAttribute("name", "uname2");
+ form.insertBefore(newField, form.pword);
+
+ restoreForm(form);
+ const autocompleteItems = await popupByArrowDown();
+ checkAutoCompleteResults(autocompleteItems,
+ ["user"],
+ window.location.host,
+ "Check dropdown is showing all logins while field is blank");
+
+
+ // Check first entry
+ synthesizeKey("KEY_ArrowDown");
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update
+ synthesizeKey("KEY_Enter");
+ // The form changes, so we expect the old username field to get the
+ // selected autocomplete value, but neither the new username field nor
+ // the password field should have any values filled in.
+ await SimpleTest.promiseWaitForCondition(() => form.uname.value == "user",
+ "Wait for username to get filled");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user", form.pword, "");
+ is(newField.value, "", "Verifying empty uname2");
+});
+
+add_task(async function form8() {
+ await setStoredLoginsAsync(
+ [location.origin, "https://autocomplete", null, "user", "pass", "uname", "pword"]
+ );
+ const form1 = createLoginForm({
+ num: 1,
+ action: "https://autocomplete-other"
+ });
+ const form2 = createLoginForm({
+ num: 2,
+ action: "https://autocomplete"
+ });
+ await promiseFormsProcessedInSameProcess(2);
+
+ checkLoginForm(form2.uname, "user", form2.pword, "pass");
+
+ restoreForm(form2);
+ checkLoginForm(form2.uname, "", form2.pword, "");
+
+ form1.uname.focus();
+ checkLoginForm(form2.uname, "", form2.pword, "");
+});
+
+add_task(async function form9_filtering() {
+ await setStoredLoginsAsync(
+ [location.origin, "https://autocomplete", null, "form9userAB", "pass-1", "uname", "pword"],
+ [location.origin, "https://autocomplete", null, "form9userAAB", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete"
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ let results = await popupBy(() => form.uname.focus());
+ checkAutoCompleteResults(results,
+ ["form9userAAB", "form9userAB"],
+ window.location.host,
+ "Check dropdown is showing all logins while field is blank");
+ synthesizeKey("KEY_Escape"); // Need to close the popup so we can get another popupshown after sending the string below.
+
+ results = await popupBy(() => sendString("form9userAB"));
+ checkAutoCompleteResults(results,
+ ["form9userAB"],
+ window.location.host,
+ "Check dropdown is showing login with only one 'A'");
+
+ checkLoginForm(form.uname, "form9userAB", form.pword, "");
+ form.uname.focus();
+ synthesizeKey("KEY_ArrowLeft");
+ results = await popupBy(() => synthesizeKey("A", {shiftKey: true}));
+
+ checkLoginForm(form.uname, "form9userAAB", form.pword, "");
+ checkAutoCompleteResults(results, ["form9userAAB"],
+ window.location.host, "Check dropdown is updated after inserting 'A'");
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "form9userAAB", form.pword, "pass-2");
+});
+
+add_task(async function form9_autocomplete_cache() {
+ await setStoredLoginsAsync(
+ [location.origin, "https://autocomplete", null, "form9userAB", "pass-1", "uname", "pword"],
+ [location.origin, "https://autocomplete", null, "form9userAAB", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete"
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ await popupBy(() => form.uname.focus());
+
+ await addLoginsInParent(
+ [location.origin, "https://autocomplete", null, "form9userAABzz", "pass-3", "uname", "pword"]
+ );
+
+ const promise1 = notifyMenuChanged(1);
+ sendString("z");
+ const results1 = await promise1;
+ checkAutoCompleteResults(results1, [], window.location.host,
+ "Check popup does not have any login items");
+
+ // check that empty results are cached - bug 496466
+ const promise2 = notifyMenuChanged(1);
+ sendString("z");
+ const results2 = await promise2;
+ checkAutoCompleteResults(results2, [], window.location.host,
+ "Check popup only has the footer when it opens");
+});
+
+add_task(async function form11_formless() {
+ await setStoredLoginsAsync(
+ [location.origin, location.origin, null, "user", "pass", "uname", "pword"]
+ );
+ const form = createLoginForm();
+ await promiseFormsProcessedInSameProcess();
+
+ // Test form-less autocomplete
+ restoreForm(form);
+ checkLoginForm(form.uname, "", form.pword, "");
+ await popupByArrowDown();
+
+ // Trigger autocomplete
+ synthesizeKey("KEY_ArrowDown");
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update
+ const processedPromise = promiseFormsProcessedInSameProcess();
+ synthesizeKey("KEY_Enter");
+ await processedPromise;
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+});
+
+add_task(async function form11_open_on_trusted_focus() {
+ await setStoredLoginsAsync(
+ [location.origin, location.origin, null, "user", "pass", "uname", "pword"]
+ );
+ const form = createLoginForm();
+ await promiseFormsProcessedInSameProcess();
+
+ form.uname.value = "";
+ form.pword.value = "";
+
+ // Move focus to the password field so we can test the first click on the
+ // username field.
+ form.pword.focus();
+ checkLoginForm(form.uname, "", form.pword, "");
+ const firePrivEventPromise = new Promise((resolve) => {
+ form.uname.addEventListener("click", (e) => {
+ ok(e.isTrusted, "Ensure event is trusted");
+ resolve();
+ });
+ });
+ await popupBy(async () => {
+ synthesizeMouseAtCenter(form.uname, {});
+ await firePrivEventPromise;
+ });
+ synthesizeKey("KEY_ArrowDown");
+ const processedPromise = promiseFormsProcessedInSameProcess();
+ synthesizeKey("KEY_Enter");
+ await processedPromise;
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+});
+
+add_task(async function form12_recipes() {
+ await setStoredLoginsAsync(
+ [location.origin, "https://autocomplete", null, "user", "pass", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete",
+ password: {
+ type: "text"
+ }
+ });
+
+ await loadRecipes({
+ siteRecipes: [{
+ "hosts": [window.location.host],
+ "usernameSelector": "input[name='1']",
+ "passwordSelector": "input[name='2']",
+ }],
+ });
+
+ // First test DOMAutocomplete
+ // Switch the password field to type=password so _fillForm marks the username
+ // field for autocomplete.
+ form.pword.type = "password";
+ await promiseFormsProcessedInSameProcess();
+ restoreForm(form);
+ checkLoginForm(form.uname, "", form.pword, "");
+ await popupByArrowDown();
+
+ synthesizeKey("KEY_ArrowDown");
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+
+ // Now test recipes with blur on the username field.
+ restoreForm(form);
+ checkLoginForm(form.uname, "", form.pword, "");
+ form.uname.value = "user";
+ checkLoginForm(form.uname, "user", form.pword, "");
+ synthesizeKey("KEY_Tab");
+ await promiseFormsProcessedInSameProcess();
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+ await resetRecipes();
+});
+
+add_task(async function form13_stays_open_upon_empty_search() {
+ await setStoredLoginsAsync(
+ [location.origin, "https://autocomplete:8888", null, "", "pass", "", "pword"],
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete:8888",
+ username: {
+ value: "prefilled"
+ },
+ password: {
+ value: "prefilled"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ checkLoginForm(form.uname, "prefilled", form.pword, "prefilled");
+
+ form.uname.scrollIntoView();
+ await popupBy(() => synthesizeMouseAtCenter(form.uname, {}));
+ form.uname.select();
+ synthesizeKey("KEY_Delete");
+
+ await spinEventLoop();
+ let popupState = await getPopupState();
+ is(popupState.open, true, "Check popup is still open");
+ checkLoginForm(form.uname, "", form.pword, "prefilled");
+
+ info("testing password field");
+ synthesizeMouseAtCenter(form.pword, {});
+ form.pword.select();
+ popupState = await getPopupState();
+ is(popupState.open, false, "Check popup closed since password field isn't empty");
+ await popupBy(() => synthesizeKey("KEY_Delete"));
+ checkLoginForm(form.uname, "", form.pword, "");
+});
+
+add_task(async function form14_username_only() {
+ await setStoredLoginsAsync(
+ [location.origin, "https://autocomplete", null, "user", "pass", "uname", "pword"],
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete",
+ autocomplete: "off",
+ username: {
+ value: "prefilled",
+ type: "email",
+ autocomplete: "username"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ await SimpleTest.promiseFocus(window);
+
+ const pword = { value: "" };
+ checkLoginForm(form.uname, "prefilled", pword, "");
+ restoreForm(form);
+ await popupByArrowDown();
+
+ // Check first entry
+ synthesizeKey("KEY_ArrowDown");
+ checkLoginForm(form.uname, "", pword, ""); // value shouldn't update
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user", pword, "");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_formActionOrigin.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_formActionOrigin.html
new file mode 100644
index 0000000000..587d57215b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_formActionOrigin.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test that logins with non-matching formActionOrigin appear in autocomplete dropdown</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: logins with non-matching formActionOrigin appear in autocomplete dropdown
+
+<script>
+const chromeScript = runChecksAfterCommonInit();
+</script>
+<p id="display"></p>
+<!-- we presumably can't hide the content for this test. -->
+<div id="content"></div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/** Test for Login Manager: multiple login autocomplete. **/
+
+add_setup(async () => {
+ await addLoginsInParent(
+ [window.location.origin, "https://differentFormSubmitURL", null, "dfsu1", "dfsp1", "uname", "pword"]
+ );
+ listenForUnexpectedPopupShown();
+});
+
+add_task(async function test_form1_initial_empty() {
+ const form = createLoginForm();
+
+ await SimpleTest.promiseFocus(window);
+
+ // Make sure initial form is empty.
+ checkLoginForm(form.uname, "", form.pword, "");
+ const popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+/* For this testcase, the only login that exists for this origin
+ * is one with a different formActionOrigin, so the login will appear
+ * in the autocomplete popup.
+ */
+add_task(async function test_form1_menu_shows_logins_for_different_formActionOrigin() {
+ const form = createLoginForm();
+
+ await SimpleTest.promiseFocus(window);
+
+ // Trigger autocomplete popup
+ form.uname.value = "";
+ form.pword.value = "";
+ form.uname.focus();
+
+ const autocompleteItems = await popupByArrowDown();
+
+ const popupState = await getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ const expectedMenuItems = ["dfsu1"];
+ checkAutoCompleteResults(autocompleteItems, expectedMenuItems, window.location.host, "Check all menuitems are displayed correctly.");
+
+ synthesizeKey("KEY_ArrowDown"); // first item
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update just by selecting
+
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ checkLoginForm(form.uname, "dfsu1", form.pword, "dfsp1");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_insecure.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_insecure.html
new file mode 100644
index 0000000000..13b4e2170b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_insecure.html
@@ -0,0 +1,849 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test insecure form field autocomplete</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content"></div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: multiple login autocomplete. **/
+
+// Restore the form to the default state.
+function restoreForm(form) {
+ form.uname.value = "";
+ form.pword.value = "";
+ form.uname.focus();
+}
+
+function sendFakeAutocompleteEvent(element) {
+ var acEvent = document.createEvent("HTMLEvents");
+ acEvent.initEvent("DOMAutoComplete", true, false);
+ element.dispatchEvent(acEvent);
+}
+
+async function promiseACPopupClosed() {
+ return SimpleTest.promiseWaitForCondition(async () => {
+ let popupState = await getPopupState();
+ return !popupState.open;
+ }, "Wait for AC popup to be closed");
+}
+
+add_setup(async () => {
+ listenForUnexpectedPopupShown();
+});
+
+add_task(async function form1_initial_empty() {
+ const form = createLoginForm({
+ action: "http://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ // Make sure initial form is empty.
+ checkLoginForm(form.uname, "", form.pword, "");
+ const popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+add_task(async function form1_warning_entry() {
+ await setStoredLoginsAsync(
+ // login 0 has no username, so should be filtered out from the autocomplete list.
+ [location.origin, "http://autocomplete:8888", null, "", "pass0", "", "pword"],
+
+ [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-3", "pass-3", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ // Trigger autocomplete popup
+ form.uname.focus();
+ const autocompleteItems = await popupByArrowDown();
+
+ const popupState = await getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ const expectedMenuItems = [
+ "This connection is not secure. Logins entered here could be compromised. Learn More",
+ "user-1",
+ "user-2",
+ "user-3"
+ ];
+ checkAutoCompleteResults(autocompleteItems, expectedMenuItems, "mochi.test", "Check all menuitems are displayed correctly.");
+
+ synthesizeKey("KEY_ArrowDown"); // select insecure warning
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update just by selecting
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "", form.pword, "");
+});
+
+add_task(async function form1_first_entry() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+
+ let popupState = await getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ synthesizeKey("KEY_ArrowDown"); // first
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update just by selecting
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ checkLoginForm(form.uname, "user-1", form.pword, "pass-1");
+});
+
+add_task(async function form1_second_entry() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_ArrowDown"); // second
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ checkLoginForm(form.uname, "user-2", form.pword, "pass-2");
+});
+
+add_task(async function form1_wraparound_first_entry() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_ArrowDown"); // second
+ synthesizeKey("KEY_ArrowDown"); // footer
+ synthesizeKey("KEY_ArrowDown"); // deselects
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-1", form.pword, "pass-1");
+});
+
+add_task(async function form1_wraparound_up_last_entry() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+
+ synthesizeKey("KEY_ArrowUp"); // footer
+ synthesizeKey("KEY_ArrowUp"); // last (fourth)
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-2", form.pword, "pass-2");
+});
+
+add_task(async function form1_wraparound_down_up_up() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+
+ synthesizeKey("KEY_ArrowDown"); // select first entry
+ synthesizeKey("KEY_ArrowUp"); // selects nothing!
+ synthesizeKey("KEY_ArrowUp"); // selects footer
+ synthesizeKey("KEY_ArrowUp"); // select last entry
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-2", form.pword, "pass-2");
+});
+
+add_task(async function form1_wraparound_up_last() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_ArrowUp"); // deselects
+ synthesizeKey("KEY_ArrowUp"); // footer
+ synthesizeKey("KEY_ArrowUp"); // last entry
+ synthesizeKey("KEY_ArrowUp"); // skip insecure warning
+ synthesizeKey("KEY_ArrowUp"); // first entry
+ synthesizeKey("KEY_ArrowUp"); // deselects
+ synthesizeKey("KEY_ArrowUp"); // footer
+ synthesizeKey("KEY_ArrowUp"); // last entry
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-2", form.pword, "pass-2");
+});
+
+add_task(async function form1_fill_username_without_autofill_right() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+
+ // Set first entry w/o triggering autocomplete
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_ArrowRight");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-1", form.pword, ""); // empty password
+});
+
+add_task(async function form1_fill_username_without_autofill_left() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+
+ // Set first entry w/o triggering autocomplete
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_ArrowLeft");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-1", form.pword, ""); // empty password
+});
+
+add_task(async function form1_pageup_first() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+
+ // Check first entry (page up)
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_ArrowDown"); // second
+ synthesizeKey("KEY_PageUp"); // first
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-1", form.pword, "pass-1");
+});
+
+add_task(async function form1_pagedown_last() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+
+ // test 13
+ // Check last entry (page down)
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_PageDown"); // last
+ synthesizeKey("KEY_ArrowUp"); // skip footer
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-2", form.pword, "pass-2");
+});
+
+add_task(async function form1_untrusted_event() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+
+ // Send a fake (untrusted) event.
+ checkLoginForm(form.uname, "", form.pword, "");
+ form.uname.value = "user-2";
+ sendFakeAutocompleteEvent(form.uname);
+ await ensureLoginFormStaysFilledWith(form.uname, "user-2", form.pword, "");
+});
+
+add_task(async function form1_delete() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-3", "pass-3", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+ // should have 5 menu items by now: 3 logins plus insecure warning + footer
+ await notifyMenuChanged(5);
+
+ // XXX tried sending character "t" before/during dropdown to test
+ // filtering, but had no luck. Seemed like the character was getting lost.
+ // Setting uname.value didn't seem to work either. This works with a human
+ // driver, so I'm not sure what's up.
+
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ // Delete the first entry (of 3), "user-1"
+ synthesizeKey("KEY_ArrowDown");
+ const numLoginsBeforeDeletion = await LoginManager.countLogins(location.origin, "http://autocomplete:8888", null);
+ is(numLoginsBeforeDeletion, 3, "Correct number of logins before deleting one");
+
+ const countChangedPromise = notifyMenuChanged(4);
+ const deletionPromise = promiseStorageChanged(["removeLogin"]);
+ // On OS X, shift-backspace and shift-delete work, just delete does not.
+ // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
+ synthesizeKey("KEY_Delete", {shiftKey: true});
+ await deletionPromise;
+
+ checkLoginForm(form.uname, "", form.pword, "");
+ const numLoginsAfterDeletion = await LoginManager.countLogins(location.origin, "http://autocomplete:8888", null);
+ is(numLoginsAfterDeletion, 2, "Correct number of logins after deleting one");
+ await countChangedPromise;
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-2", form.pword, "pass-2");
+});
+
+add_task(async function form1_delete_second() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-3", "pass-3", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ // Delete the second entry (of 3), "user-2"
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_Delete", {shiftKey: true});
+ checkLoginForm(form.uname, "", form.pword, "");
+ const numLoginsAfterDeletion = await LoginManager.countLogins(location.origin, "http://autocomplete:8888", null);
+ is(numLoginsAfterDeletion, 2, "Correct number of logins after deleting one");
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-3", form.pword, "pass-3");
+});
+
+add_task(async function form1_delete_last() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"],
+ [location.origin, "http://autocomplete:8888", null, "user-3", "pass-3", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete:8888/formtest.js"
+ });
+ await promiseFormsProcessedInSameProcess();
+ // Trigger autocomplete popup
+ form.uname.focus();
+ await popupByArrowDown();
+
+ /* test 54 */
+ // Delete the last entry (of 3), "user-3"
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_ArrowDown");
+ const numLoginsBeforeDeletion = await LoginManager.countLogins(location.origin, "http://autocomplete:8888", null);
+ is(numLoginsBeforeDeletion, 3, "Correct number of logins before deleting one");
+ synthesizeKey("KEY_Delete", {shiftKey: true});
+ checkLoginForm(form.uname, "", form.pword, "");
+ const numLoginsAfterDeletion = await LoginManager.countLogins(location.origin, "http://autocomplete:8888", null);
+ is(numLoginsAfterDeletion, 2, "Correct number of logins after deleting one");
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_Enter");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user-1", form.pword, "pass-1");
+});
+
+// Tests for single-user forms for ignoring autocomplete=off
+
+add_task(async function password_autocomplete_off() {
+ await setStoredLoginsAsync(
+ [location.origin, "https://autocomplete2", null, "user", "pass", "uname", "pword"],
+ );
+ const form = createLoginForm({
+ action: "https://autocomplete2",
+ password: {
+ autocomplete: "off"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+
+ restoreForm(form);
+ await popupByArrowDown();
+
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ // Check first entry
+ synthesizeKey("KEY_ArrowDown");
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+});
+
+add_task(async function username_autocomplete_off() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete2", null, "user", "pass", "uname", "pword"],
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete2",
+ username: {
+ autocomplete: "off"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+
+ restoreForm(form);
+ await popupByArrowDown();
+
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ // Check first entry
+ synthesizeKey("KEY_ArrowDown");
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+});
+
+add_task(async function form_autocomplete_off() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete2", null, "user", "pass", "uname", "pword"],
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete2",
+ autocomplete: "off"
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+ restoreForm(form);
+ await popupByArrowDown();
+
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ // Check first entry
+ synthesizeKey("KEY_ArrowDown");
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+});
+
+add_task(async function username_and_password_autocomplete_off() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete2", null, "user", "pass", "uname", "pword"],
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete2",
+ username: {
+ autocomplete: "off"
+ },
+ password: {
+ autocomplete: "off"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+ restoreForm(form);
+ await popupByArrowDown();
+
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ // Check first entry
+ synthesizeKey("KEY_ArrowDown");
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+});
+
+add_task(async function changing_username_does_not_touch_password() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete2", null, "user", "pass", "uname", "pword"],
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete2",
+ username: {
+ autocomplete: "off"
+ },
+ password: {
+ autocomplete: "off"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+
+ // Test that the password field remains filled in after changing
+ // the username.
+ form.uname.focus();
+ synthesizeKey("KEY_ArrowRight");
+ synthesizeKey("X", {shiftKey: true});
+ // Trigger the 'blur' event on uname
+ form.pword.focus();
+ await ensureLoginFormStaysFilledWith(form.uname, "userX", form.pword, "pass");
+});
+
+add_task(async function form7() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete3", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete3", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete3"
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ checkLoginForm(form.uname, "", form.pword, "");
+
+ // Insert a new username field into the form. We'll then make sure
+ // that invoking the autocomplete doesn't try to fill the form.
+ const newField = document.createElement("input");
+ newField.setAttribute("type", "text");
+ newField.setAttribute("name", "uname2");
+ form.insertBefore(newField, form.pword);
+ await ensureLoginFormStaysFilledWith(newField, "", form.pword, "");
+});
+
+add_task(async function form7_2() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete3", null, "user", "pass", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete3"
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ // Insert a new username field into the form. We'll then make sure
+ // that invoking the autocomplete doesn't try to fill the form.
+ const newField = document.createElement("input");
+ newField.setAttribute("type", "text");
+ newField.setAttribute("name", "uname2");
+ form.insertBefore(newField, form.pword);
+
+ restoreForm(form);
+ const autocompleteItems = await popupByArrowDown();
+ checkAutoCompleteResults(autocompleteItems, [
+ "This connection is not secure. Logins entered here could be compromised. Learn More",
+ "user"
+ ],
+ window.location.host,
+ "Check dropdown is showing all logins while field is blank");
+
+
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ // Check first entry
+ synthesizeKey("KEY_ArrowDown");
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update
+ synthesizeKey("KEY_Enter");
+ // The form changes, so we expect the old username field to get the
+ // selected autocomplete value, but neither the new username field nor
+ // the password field should have any values filled in.
+ await SimpleTest.promiseWaitForCondition(() => form.uname.value == "user",
+ "Wait for username to get filled");
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user", form.pword, "");
+ is(newField.value, "", "Verifying empty uname2");
+});
+
+add_task(async function form8() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete", null, "user", "pass", "uname", "pword"]
+ );
+ const form1 = createLoginForm({
+ num: 1,
+ action: "http://autocomplete-other"
+ });
+ const form2 = createLoginForm({
+ num: 2,
+ action: "http://autocomplete"
+ });
+ await promiseFormsProcessedInSameProcess(2);
+
+ checkLoginForm(form2.uname, "user", form2.pword, "pass");
+
+ restoreForm(form2);
+ checkLoginForm(form2.uname, "", form2.pword, "");
+
+ form1.uname.focus();
+ checkLoginForm(form2.uname, "", form2.pword, "");
+});
+
+add_task(async function form9_filtering() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete", null, "form9userAB", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete", null, "form9userAAB", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete"
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ let results = await popupBy(() => form.uname.focus());
+ checkAutoCompleteResults(results, [
+ "This connection is not secure. Logins entered here could be compromised. Learn More",
+ "form9userAAB",
+ "form9userAB"
+ ],
+ window.location.host,
+ "Check dropdown is showing all logins while field is blank");
+ synthesizeKey("KEY_Escape"); // Need to close the popup so we can get another popupshown after sending the string below.
+
+ results = await popupBy(() => sendString("form9userAB"));
+ checkAutoCompleteResults(results, [
+ "This connection is not secure. Logins entered here could be compromised. Learn More",
+ "form9userAB"
+ ],
+ window.location.host,
+ "Check dropdown is showing login with only one 'A'");
+
+ checkLoginForm(form.uname, "form9userAB", form.pword, "");
+ form.uname.focus();
+ synthesizeKey("KEY_ArrowLeft");
+ results = await popupBy(() => synthesizeKey("A", {shiftKey: true}));
+
+ checkLoginForm(form.uname, "form9userAAB", form.pword, "");
+ checkAutoCompleteResults(results, [
+ "This connection is not secure. Logins entered here could be compromised. Learn More",
+ "form9userAAB"
+ ],
+ window.location.host, "Check dropdown is updated after inserting 'A'");
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "form9userAAB", form.pword, "pass-2");
+});
+
+add_task(async function form9_autocomplete_cache() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete", null, "form9userAB", "pass-1", "uname", "pword"],
+ [location.origin, "http://autocomplete", null, "form9userAAB", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete"
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ await popupBy(() => form.uname.focus());
+
+ await addLoginsInParent(
+ [location.origin, "http://autocomplete", null, "form9userAABzz", "pass-3", "uname", "pword"]
+ );
+
+ const promise1 = notifyMenuChanged(2);
+ sendString("z");
+ const results1 = await promise1;
+ checkAutoCompleteResults(results1, [
+ "This connection is not secure. Logins entered here could be compromised. Learn More",
+ ], window.location.host,
+ "Check popup does not have any login items");
+
+ // check that empty results are cached - bug 496466
+ const promise2 = notifyMenuChanged(2);
+ sendString("z");
+ const results2 = await promise2;
+ checkAutoCompleteResults(results2, [
+ "This connection is not secure. Logins entered here could be compromised. Learn More",
+ ], window.location.host,
+ "Check popup only has the footer when it opens");
+});
+
+add_task(async function form12_recipes() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete", null, "user", "pass", "uname", "pword"]
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete",
+ password: {
+ type: "text"
+ }
+ });
+
+ await loadRecipes({
+ siteRecipes: [{
+ "hosts": [window.location.host],
+ "usernameSelector": "input[name='1']",
+ "passwordSelector": "input[name='2']",
+ }],
+ });
+
+ // First test DOMAutocomplete
+ // Switch the password field to type=password so _fillForm marks the username
+ // field for autocomplete.
+ form.pword.type = "password";
+ await promiseFormsProcessedInSameProcess();
+ restoreForm(form);
+ checkLoginForm(form.uname, "", form.pword, "");
+ await popupByArrowDown();
+
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ synthesizeKey("KEY_ArrowDown");
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ await promiseACPopupClosed();
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+
+ // Now test recipes with blur on the username field.
+ restoreForm(form);
+ checkLoginForm(form.uname, "", form.pword, "");
+ form.uname.value = "user";
+ checkLoginForm(form.uname, "user", form.pword, "");
+ synthesizeKey("KEY_Tab");
+ await promiseFormsProcessedInSameProcess();
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+ await resetRecipes();
+});
+
+add_task(async function form11_formless() {
+ await setStoredLoginsAsync(
+ [location.origin, location.origin, null, "user", "pass", "uname", "pword"]
+ );
+ const form = createLoginForm();
+ await promiseFormsProcessedInSameProcess();
+
+ // Test form-less autocomplete
+ restoreForm(form);
+ checkLoginForm(form.uname, "", form.pword, "");
+ await popupByArrowDown();
+
+ synthesizeKey("KEY_ArrowDown"); // skip insecure warning
+ // Trigger autocomplete
+ synthesizeKey("KEY_ArrowDown");
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ checkLoginForm(form.uname, "user", form.pword, "pass");
+});
+
+add_task(async function form13_stays_open_upon_empty_search() {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete:8888", null, "", "pass", "", "pword"],
+ );
+ const form = createLoginForm({
+ action: "http://autocomplete:8888",
+ username: {
+ value: "prefilled"
+ },
+ password: {
+ value: "prefilled"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ checkLoginForm(form.uname, "prefilled", form.pword, "prefilled");
+
+ form.uname.scrollIntoView();
+ await popupBy(() => synthesizeMouseAtCenter(form.uname, {}));
+ form.uname.select();
+ synthesizeKey("KEY_Delete");
+
+ await ensureLoginFormStaysFilledWith(form.uname, "", form.pword, "prefilled");
+ let popupState = await getPopupState();
+ is(popupState.open, true, "Check popup is still open");
+
+ info("testing password field");
+ synthesizeMouseAtCenter(form.pword, {});
+ form.pword.select();
+ popupState = await getPopupState();
+ is(popupState.open, false, "Check popup closed since password field isn't empty");
+ await popupBy(() => synthesizeKey("KEY_Delete"));
+ checkLoginForm(form.uname, "", form.pword, "");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_related_realms.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_related_realms.html
new file mode 100644
index 0000000000..988cd05954
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_related_realms.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test login autocomplete with related realms</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+Login Manager test: related realms autocomplete
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/** Test for Login Manager: related realms autocomplete. **/
+
+function sendFakeAutocompleteEvent(element) {
+ var acEvent = document.createEvent("HTMLEvents");
+ acEvent.initEvent("DOMAutoComplete", true, false);
+ element.dispatchEvent(acEvent);
+}
+
+async function promiseACPopupClosed() {
+ return SimpleTest.promiseWaitForCondition(async () => {
+ const popupState = await getPopupState();
+ return !popupState.open;
+ }, "Wait for AC popup to be closed");
+}
+
+add_setup(async () => {
+ await setStoredLoginsAsync(
+ // Simple related domain relationship where example.com and other-example.com are in the related domains list
+ ["https://other-example.com", "https://other-example.com", null, "relatedUser1", "relatedPass1", "uname", "pword"],
+
+ // Example.com and example.co.uk are related, so sub.example.co.uk should appear on example.com's autocomplete dropdown
+ // The intent is to cover the ebay.com/ebay.co.uk and all other country TLD cases
+ // where the sign in page is actually signin.ebay.com/signin.ebay.co.uk but credentials could have manually been entered
+ // for ebay.com/ebay.co.uk or automatically stored as signin.ebay.com/sigin.ebay.co.uk
+ ["https://sub.example.co.uk", "https://sub.example.co.uk", null, "subUser1", "subPass1", "uname", "pword"],
+ );
+ listenForUnexpectedPopupShown();
+});
+
+add_task(async function test_form1_initial_empty() {
+ const form = createLoginForm();
+ await promiseFormsProcessedInSameProcess();
+
+ // Make sure initial form is empty.
+ checkLoginForm(form.uname, "", form.pword, "");
+ const popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+add_task(async function test_form_related_domain_menuitems() {
+ const form = createLoginForm();
+ await promiseFormsProcessedInSameProcess();
+
+ form.uname.focus();
+
+ const autocompleteItems = await popupByArrowDown();
+ const popupState = await getPopupState();
+
+ is(popupState.selectedIndex, -1, "Check no entires are selected upon opening");
+
+ const expectedMenuItems = ["relatedUser1", "subUser1"];
+ checkAutoCompleteResults(autocompleteItems, expectedMenuItems, window.location.host, "Check all menuitems are displayed correctly");
+
+ const acEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsAC, clear: true });
+ is(acEvents.length, 1, "One autocomplete event");
+ checkACTelemetryEvent(acEvents[0], form.uname, {
+ "hadPrevious": "0",
+ "login": expectedMenuItems.length + "",
+ "loginsFooter": "1"
+ });
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update just by opening
+
+ synthesizeKey("KEY_ArrowDown"); // first item
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update just by selecting
+
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ is(form.pword.value, "relatedPass1", "password should match the login that was selected");
+ checkLoginForm(form.uname, "relatedUser1", form.pword, "relatedPass1");
+
+ form.uname.value = "";
+ form.pword.value = "";
+ form.uname.focus();
+
+ await popupByArrowDown();
+
+ synthesizeKey("KEY_ArrowDown"); // first item
+ synthesizeKey("KEY_ArrowDown"); // second item
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update just by selecting
+
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ is(form.pword.value, "subPass1", "password should match the login that was selected");
+ checkLoginForm(form.uname, "subUser1", form.pword, "subPass1");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_subdomain.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_subdomain.html
new file mode 100644
index 0000000000..685c2044c8
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_subdomain.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test that logins with non-exact match origin appear in autocomplete dropdown</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: logins with non-exact match origin appear in autocomplete dropdown
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content"></div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const setupScript = runInParent(function setup() {
+ addMessageListener("getDateString", () => {
+ const dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined, { dateStyle: "medium" });
+ return dateAndTimeFormatter.format(new Date());
+ });
+});
+
+add_setup(async () => {
+ const origin = window.location.origin;
+ const lastDot = origin.lastIndexOf(".");
+ const suffix = origin.slice(lastDot);
+
+ const baseHost = "http://example" + suffix;
+ const baseSecureHost = "https://example" + suffix;
+ const oldHost = "http://old.example" + suffix;
+ const oldSecureHost = "https://old.example" + suffix;
+ const newHost = "https://new.example" + suffix;
+
+ await addLoginsInParent(
+ // The first two logins should never be visible on https: versions of
+ // *.example.com since the login is for http: and an https: login exists for this username.
+ [oldHost, oldSecureHost, null, "dsdu1", "dsdp1new", "uname", "pword"],
+ [baseHost, baseSecureHost, null, "dsdu1", "dsdp1", "uname", "pword"],
+ [oldSecureHost, oldSecureHost, null, "dsdu1", "dsdp1", "uname", "pword"],
+ [baseSecureHost, baseSecureHost, null, "dsdu1", "dsdp1", "uname", "pword"],
+ [newHost, newHost, null, "dsdu1", "dsdp1prime", "uname", "pword"]
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.includeOtherSubdomainsInLookup", true],
+ ],
+ });
+ listenForUnexpectedPopupShown();
+});
+
+add_task(async function test_form1_initial_empty() {
+ const form = createLoginForm({
+ action: "https://otherexample.com/formtest.js"
+ });
+
+ // Make sure initial form is empty.
+ checkLoginForm(form.uname, "", form.pword, "");
+ const popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+/* For this testcase, there exists two logins for this origin
+ * on different subdomains but with different passwords. Both logins
+ * should appear in the autocomplete popup.
+ */
+add_task(async function test_form1_menu_shows_two_logins_same_usernames_for_different_subdomain() {
+ const form = createLoginForm({
+ action: "https://otherexample.com/formtest.js"
+ });
+
+ // Trigger autocomplete popup
+ form.uname.focus();
+
+ const autocompleteItems = await popupByArrowDown();
+
+ const popupState = await getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ // The logins are added "today" and since they are duplicates, the date that they were last
+ // changed will be appended.
+ const dateString = await setupScript.sendQuery("getDateString");
+ const username = `dsdu1 (${dateString})`;
+
+ checkAutoCompleteResults(autocompleteItems, [username, username], window.location.host, "Check all menuitems are displayed correctly.");
+
+ synthesizeKey("KEY_ArrowDown"); // first item
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update just by selecting
+
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ is(form.pword.value, "dsdp1", "password should match the login that was selected");
+ checkLoginForm(form.uname, "dsdu1", form.pword, "dsdp1");
+
+ form.uname.value = "";
+ form.pword.value = "";
+ form.uname.focus();
+
+ await popupByArrowDown();
+
+ synthesizeKey("KEY_ArrowDown"); // first item
+ synthesizeKey("KEY_ArrowDown"); // second item
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update just by selecting
+
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ is(form.pword.value, "dsdp1prime", "Password should match the login that was selected");
+ checkLoginForm(form.uname, "dsdu1", form.pword, "dsdp1prime");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_hasBeenTypePassword.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_hasBeenTypePassword.html
new file mode 100644
index 0000000000..6e520952b2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_hasBeenTypePassword.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test that passwords are autocompleted into fields that were previously type=password</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: Test that passwords are autocompleted into fields that were previously type=password
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content"></div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// Restore the form to the default state.
+function restoreForm(form) {
+ form.uname.value = "";
+ form.pword.value = "";
+ form.uname.focus();
+}
+
+function spinEventLoop() {
+ return Promise.resolve();
+}
+
+add_setup(async () => {
+ const origin = window.location.origin;
+ await addLoginsInParent(
+ [origin, origin, null, "user1", "pass1"],
+ [origin, origin, null, "user2", "pass2"]
+ );
+ listenForUnexpectedPopupShown();
+});
+
+add_task(async function test_form1_initial_empty() {
+ const form = createLoginForm({
+ action: "https://www.example.com/formtest.js"
+ });
+
+ await SimpleTest.promiseFocus(window);
+
+ // Make sure initial form is empty.
+ checkLoginForm(form.uname, "", form.pword, "");
+ const popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+add_task(async function test_form1_password_to_type_text() {
+ const form = createLoginForm({
+ action: "https://www.example.com/formtest.js"
+ });
+
+ await SimpleTest.promiseFocus(window);
+ info("Setting the password field type to text");
+ // This is similar to a site implementing their own password visibility/unmasking toggle
+ form.pword.type = "text";
+
+ // Trigger autocomplete popup
+ restoreForm(form);
+ const autocompleteItems = await popupByArrowDown();
+
+ const popupState = await getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ checkAutoCompleteResults(autocompleteItems, ["user1", "user2"], window.location.host,
+ "Check all menuitems are displayed correctly.");
+
+ synthesizeKey("KEY_ArrowDown"); // first item
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update just by selecting
+
+ synthesizeKey("KEY_Enter");
+
+ await promiseFormsProcessedInSameProcess();
+ is(form.uname.value, "user1", "username should match the login, not the password");
+ is(form.pword.value, "pass1", "password should match the login, not the username");
+ checkLoginForm(form.uname, "user1", form.pword, "pass1");
+
+ restoreForm(form);
+ info("Focusing the password field");
+ form.pword.focus();
+
+ await popupByArrowDown();
+
+ synthesizeKey("KEY_ArrowDown"); // first item
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update just by selecting
+
+ synthesizeKey("KEY_Enter");
+ await spinEventLoop();
+ is(form.pword.value, "pass1", "Password should match the login that was selected");
+ checkLoginForm(form.uname, "", form.pword, "pass1");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight.html
new file mode 100644
index 0000000000..dfde95e1e4
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form field autofill highlight</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test">
+<script>
+const { ContentTaskUtils } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/ContentTaskUtils.jsm"
+);
+
+add_setup(async () => {
+ const origin = window.location.origin;
+ await addLoginsInParent(
+ [origin, "https://autocomplete", null, "user1", "pass1", "", ""],
+ [origin, "https://autocomplete", null, "user2", "pass2", "", ""]
+ );
+});
+
+add_task(async function test_field_highlight_on_autocomplete() {
+ const form = createLoginForm({
+ action: "https://autocomplete"
+ });
+
+ await openPopupOn(form.uname);
+ synthesizeKey("KEY_ArrowDown");
+ await synthesizeKey("KEY_Enter");
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return form.uname.matches(":autofill")
+ }, "Highlight was successfully applied to the username field on username autocomplete");
+
+ ok(form.pword.matches(":autofill"),
+ "Highlight was successfully applied to the password field on username autocomplete");
+
+ // Clear existing highlight on login fields. We check by pressing the tab key after backspace
+ // (by shifting focus to the next element) because the tab key was known to cause a bug where the
+ // highlight is applied once again. See Bug 1526522.
+ form.uname.focus();
+ synthesizeKey("KEY_Backspace");
+ synthesizeKey("KEY_Tab");
+ ok(!form.uname.matches(":autofill"),
+ "Highlight was successfully removed on the username field");
+
+ synthesizeKey("KEY_Backspace");
+ synthesizeKey("KEY_Tab");
+ ok(!form.pword.matches(":autofill"),
+ "Highlight was successfully removed on the password field");
+
+ // Clear login fields.
+ form.uname.value = "";
+ form.pword.value = "";
+
+ // Test password field autocomplete.
+ await openPopupOn(form.pword);
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_Enter");
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return form.pword.matches(":autofill");
+ }, "Highlight was successfully applied to the password field on password autocomplete");
+
+ // Clear existing highlight on the password field. We check by pressing the tab key after backspace
+ // (by shifting focus to the next element) because the tab key was known to cause a bug where the
+ // highlight is applied once again. See Bug 1526522.
+ synthesizeKey("KEY_Backspace");
+ synthesizeKey("KEY_Tab");
+
+ ok(!form.pword.matches(":autofill"),
+ "Highlight was successfully removed on the password field");
+
+ // Clear login fields.
+ form.uname.value = "";
+ form.pword.value = "";
+});
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight_non_login.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight_non_login.html
new file mode 100644
index 0000000000..eb82e90656
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight_non_login.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form field autofill highlight</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test">
+<script>
+function closeCurrentTab() {
+ runInParent(function cleanUpWindow() {
+ let window = Services.wm.getMostRecentWindow("navigator:browser");
+ window.gBrowser.removeTab(window.gBrowser.selectedTab);
+ });
+}
+
+add_setup(async () => {
+ await setStoredLoginsAsync(
+ [location.origin, "http://autocomplete", null, "user1", "pass1", "", ""],
+ [location.origin, "http://autocomplete", null, "user2", "pass2", "", ""]
+ );
+});
+
+add_task(async function test_field_highlight_on_pw_field_autocomplete_insecureWarning() {
+ const form = createLoginForm({
+ action: "http://autocomplete"
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ // Press enter on insecure warning and check.
+ form.pword.focus();
+ await popupByArrowDown();
+ synthesizeKey("KEY_ArrowDown"); // insecure warning
+ synthesizeKey("KEY_Enter");
+
+ is(document.defaultView.getComputedStyle(form.pword).getPropertyValue("filter"), "none",
+ "Highlight is not applied to the password field if enter key is pressed on the insecure warning item");
+ is(document.defaultView.getComputedStyle(form.uname).getPropertyValue("filter"), "none",
+ "Highlight is not applied to the username field if enter key is pressed on the insecure warning item");
+
+ // Press tab on insecure warning and check.
+ await openPopupOn(form.pword);
+ synthesizeKey("KEY_ArrowDown"); // insecure warning
+ synthesizeKey("KEY_Tab");
+
+ is(document.defaultView.getComputedStyle(form.pword).getPropertyValue("filter"), "none",
+ "Highlight is not applied to the password field if tab key is pressed on the insecure warning item");
+ is(document.defaultView.getComputedStyle(form.uname).getPropertyValue("filter"), "none",
+ "Highlight is not applied to the username field if tab key is pressed on the insecure warning item");
+});
+
+add_task(async function test_field_highlight_on_pw_field_autocomplete_footer() {
+ const form = createLoginForm({
+ action: "http://autocomplete"
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ // Press enter on the footer and check.
+ await openPopupOn(form.pword);
+ synthesizeKey("KEY_ArrowUp"); // footer
+ synthesizeKey("KEY_Enter");
+
+ is(document.defaultView.getComputedStyle(form.pword).getPropertyValue("filter"), "none",
+ "Highlight is not applied to the password field if enter key is pressed on the footer item");
+ is(document.defaultView.getComputedStyle(form.uname).getPropertyValue("filter"), "none",
+ "Highlight is not applied to the username field if enter key is pressed on the footer item");
+
+ closeCurrentTab();
+
+ // Press tab on the footer and check.
+ await openPopupOn(form.pword);
+ synthesizeKey("KEY_ArrowUp"); // footer
+ synthesizeKey("KEY_Tab");
+
+ is(document.defaultView.getComputedStyle(form.pword).getPropertyValue("filter"), "none",
+ "Highlight is not applied to the password field if tab key is pressed on the footer item");
+ is(document.defaultView.getComputedStyle(form.uname).getPropertyValue("filter"), "none",
+ "Highlight is not applied to the username field if tab key is pressed on the insecure warning item");
+
+ closeCurrentTab();
+});
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight_username_only_form.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight_username_only_form.html
new file mode 100644
index 0000000000..884bdc66d3
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight_username_only_form.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form field autofill highlight</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test">
+<script>
+const { ContentTaskUtils } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/ContentTaskUtils.jsm"
+);
+
+add_setup(async () => {
+ await addLoginsInParent(
+ [location.origin, "https://autocomplete", null, "user1", "pass1", "", ""],
+ [location.origin, "https://autocomplete", null, "user2", "pass2", "", ""]
+ );
+});
+
+add_task(async function test_username_field_in_username_only_form_highlight_on_autocomplete() {
+ const form = createLoginForm({
+ action: "https://autocomplete",
+ username: {
+ autocomplete: "username"
+ },
+ password: false
+ });
+
+ await openPopupOn(form.uname);
+ synthesizeKey("KEY_ArrowDown");
+ await synthesizeKey("KEY_Enter");
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return form.uname.matches(":autofill");
+ }, "Highlight was successfully applied to the username field on username autocomplete");
+
+ // Clear existing highlight on login fields. We check by pressing the tab key after backspace
+ // (by shifting focus to the next element) because the tab key was known to cause a bug where the
+ // highlight is applied once again. See Bug 1526522.
+ form.uname.focus();
+ synthesizeKey("KEY_Backspace");
+ synthesizeKey("KEY_Tab");
+ ok(!form.uname.matches(":autofill"),
+ "Highlight was successfully removed on the username field");
+});
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_downgrade.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_downgrade.html
new file mode 100644
index 0000000000..109b3e91c6
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_downgrade.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autocomplete on an HTTPS page using upgraded HTTP logins</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+ <iframe></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const origin = "http://" + window.location.host;
+const secureOrigin = "https://" + window.location.host;
+const iframe = document.getElementsByTagName("iframe")[0];
+let iframeDoc, hostname;
+let uname;
+let pword;
+
+// Restore the form to the default state.
+function restoreForm() {
+ return SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ this.content.document.getElementById("form-basic-password").focus();
+ this.content.document.getElementById("form-basic-username").value = "";
+ this.content.document.getElementById("form-basic-password").value = "";
+ this.content.document.getElementById("form-basic-username").focus();
+ });
+}
+
+const HTTP_FORM_URL = origin + "/tests/toolkit/components/passwordmgr/test/mochitest/form_basic.html";
+
+add_setup(async () => {
+ await setStoredLoginsAsync(
+ // We have two actual HTTPS to avoid autofill before the schemeUpgrades pref flips to true.
+ [secureOrigin, secureOrigin, null, "name", "pass", "uname", "pword"],
+ [secureOrigin, secureOrigin, null, "name1", "pass1", "uname", "pword"],
+ // Same as above but HTTP instead of HTTPS (to test de-duping)
+ [origin, origin, null, "name1", "pass1", "uname", "pword"],
+ // Different HTTP login to upgrade with secure formActionOrigin
+ [origin, secureOrigin, null, "name2", "passHTTPtoHTTPS", "uname", "pword"]
+ );
+});
+
+async function setup(formUrl) {
+ await SpecialPowers.pushPrefEnv({"set": [["signon.schemeUpgrades", true]]});
+
+ let processedPromise = promiseFormsProcessed();
+ iframe.src = formUrl;
+ await new Promise(resolve => {
+ iframe.addEventListener("load", function() {
+ resolve();
+ }, {once: true});
+ });
+
+ await processedPromise;
+
+ hostname = await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ return this.content.document.documentURIObject.host;
+ });
+}
+
+add_task(async function test_autocomplete_https_downgrade() {
+ info("test_autocomplete_http, setup with " + HTTP_FORM_URL);
+ await setup(HTTP_FORM_URL);
+
+ LoginManager.getAllLogins().then(logins => {
+ info("got logins: " + logins.map(l => l.origin));
+ });
+ // from a HTTP page, look for matching logins, we should never offer a login with an HTTPS scheme
+ // we're expecting just login2 as a match
+ let isCrossOrigin = false;
+ try {
+ // If this is a cross-origin test, the parent will be inaccessible. The fields
+ // should not be filled in.
+ window.parent.windowGlobalChild;
+ } catch(ex) {
+ isCrossOrigin = true;
+ }
+
+ await checkLoginFormInFrame(iframe, "form-basic-username", isCrossOrigin ? "" : "name1",
+ "form-basic-password", isCrossOrigin ? "" : "pass1");
+
+ // Trigger autocomplete popup
+ await restoreForm();
+ let popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+ const autocompleteItems = await popupByArrowDown();
+ info("got results: " + autocompleteItems.join(", "));
+ popupState = await getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected");
+ checkAutoCompleteResults(autocompleteItems, ["This connection is not secure. Logins entered here could be compromised. Learn More", "name1", "name2"], hostname, "initial");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html
new file mode 100644
index 0000000000..a0a81a8c88
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html
@@ -0,0 +1,191 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autocomplete on an HTTPS page using upgraded HTTP logins</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+ <iframe></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const originSecure = "https://" + window.location.host;
+const originNonSecure = "http://" + window.location.host;
+const iframe = document.getElementsByTagName("iframe")[0];
+let iframeDoc, hostname;
+
+// Restore the form to the default state.
+function restoreForm() {
+ return SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ this.content.document.getElementById("form-basic-password").focus();
+ this.content.document.getElementById("form-basic-username").value = "";
+ this.content.document.getElementById("form-basic-password").value = "";
+ this.content.document.getElementById("form-basic-username").focus();
+ });
+}
+
+const HTTPS_FORM_URL = originSecure + "/tests/toolkit/components/passwordmgr/test/mochitest/form_basic.html";
+
+async function setup(formUrl = HTTPS_FORM_URL) {
+ await SpecialPowers.pushPrefEnv({"set": [["signon.schemeUpgrades", true]]});
+
+ let processedPromise = promiseFormsProcessed();
+ iframe.src = formUrl;
+ await new Promise(resolve => {
+ iframe.addEventListener("load", function() {
+ resolve();
+ }, {once: true});
+ });
+
+ await processedPromise;
+
+ hostname = await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ return this.content.document.documentURIObject.host;
+ });
+}
+
+add_setup(async () => {
+ await setStoredLoginsAsync(
+ // We have two actual HTTPS to avoid autofill before the schemeUpgrades pref flips to true.
+ [originSecure, originSecure, null, "name", "pass", "uname", "pword"],
+ [originSecure, originSecure, null, "name1", "pass1", "uname", "pword"],
+
+ // Same as above but HTTP instead of HTTPS (to test de-duping)
+ [originNonSecure, originNonSecure, null, "name1", "pass1", "uname", "pword"],
+
+ // Different HTTP login to upgrade with secure formActionOrigin
+ [originNonSecure, originSecure, null, "name2", "passHTTPtoHTTPS", "uname", "pword"]
+ );
+});
+
+add_task(async function setup_https_frame() {
+ await setup(HTTPS_FORM_URL);
+});
+
+add_task(async function test_empty_first_entry() {
+ // Make sure initial form is empty.
+ await checkLoginFormInFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "", "form-basic-password", "");
+ // Trigger autocomplete popup
+ await restoreForm();
+ let popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+ let autocompleteItems = await popupBy();
+ popupState = await getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected");
+ checkAutoCompleteResults(autocompleteItems, ["name", "name1", "name2"], hostname, "initial");
+
+ // Check first entry
+ let index0Promise = notifySelectedIndex(0);
+ synthesizeKey("KEY_ArrowDown");
+ await index0Promise;
+ await checkLoginFormInFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "", "form-basic-password", ""); // value shouldn't update
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ await checkLoginFormInFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "name", "form-basic-password", "pass");
+});
+
+add_task(async function test_empty_second_entry() {
+ await restoreForm();
+ await popupBy();
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_ArrowDown"); // second
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ await checkLoginFormInFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "name1", "form-basic-password", "pass1");
+});
+
+add_task(async function test_search() {
+ await restoreForm();
+ let results = await popupBy(async () => {
+ // We need to blur for the autocomplete controller to notice the forced value below.
+ await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ let uname = this.content.document.getElementById("form-basic-username");
+ uname.blur();
+ uname.value = "name";
+ uname.focus();
+ });
+
+ sendChar("1");
+ synthesizeKey("KEY_ArrowDown"); // open
+ });
+ checkAutoCompleteResults(results, ["name1"], hostname, "check result deduping for 'name1'");
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ await checkLoginFormInFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "name1", "form-basic-password", "pass1");
+
+ let popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is now closed");
+});
+
+add_task(async function test_delete_first_entry() {
+ await restoreForm();
+ await popupBy();
+
+ let index0Promise = notifySelectedIndex(0);
+ synthesizeKey("KEY_ArrowDown");
+ await index0Promise;
+
+ let deletionPromise = promiseStorageChanged(["removeLogin"]);
+ // On OS X, shift-backspace and shift-delete work, just delete does not.
+ // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
+ synthesizeKey("KEY_Delete", {shiftKey: true});
+ await deletionPromise;
+ await checkLoginFormInFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "", "form-basic-password", "");
+
+ let results = await notifyMenuChanged(3, "name1");
+
+ checkAutoCompleteResults(results, ["name1", "name2"], hostname, "two logins should remain after deleting the first");
+ let popupState = await getPopupState();
+ is(popupState.open, true, "Check popup stays open after deleting");
+ synthesizeKey("KEY_Escape");
+ popupState = await getPopupState();
+ is(popupState.open, false, "Check popup closed upon ESC");
+});
+
+add_task(async function test_delete_duplicate_entry() {
+ await restoreForm();
+ await popupBy();
+
+ let index0Promise = notifySelectedIndex(0);
+ synthesizeKey("KEY_ArrowDown");
+ await index0Promise;
+
+ let deletionPromise = promiseStorageChanged(["removeLogin"]);
+ // On OS X, shift-backspace and shift-delete work, just delete does not.
+ // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
+ synthesizeKey("KEY_Delete", {shiftKey: true});
+ await deletionPromise;
+ await checkLoginFormInFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "", "form-basic-password", "");
+
+ is(await LoginManager.countLogins(originNonSecure, originNonSecure, null), 1,
+ "Check that the HTTP login remains");
+ is(await LoginManager.countLogins(originSecure, originSecure, null), 0,
+ "Check that the HTTPS login was deleted");
+
+ // Two menu items should remain as the HTTPS login should have been deleted but
+ // the HTTP would remain.
+ let results = await notifyMenuChanged(2, "name2");
+
+ checkAutoCompleteResults(results, ["name2"], hostname, "one login should remain after deleting the HTTPS name1");
+ let popupState = await getPopupState();
+ is(popupState.open, true, "Check popup stays open after deleting");
+ synthesizeKey("KEY_Escape");
+ popupState = await getPopupState();
+ is(popupState.open, false, "Check popup closed upon ESC");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation.html
new file mode 100644
index 0000000000..3555b9173d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation.html
@@ -0,0 +1,574 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill and autocomplete on autocomplete=new-password fields</title>
+ <!-- This test assumes that telemetry events are not cleared after the `setup` task. -->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <script src="../../../satchel/test/satchel_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: autofill with autocomplete=new-password fields
+
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content"></div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const { ContentTaskUtils } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/ContentTaskUtils.jsm"
+);
+const { TestUtils } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+);
+
+let dateAndTimeFormatter = new SpecialPowers.Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "medium",
+});
+
+const TelemetryFilterPropsUsed = Object.freeze({
+ category: "pwmgr",
+ method: "autocomplete_field",
+ object: "generatedpassword",
+});
+
+const TelemetryFilterPropsShown = Object.freeze({
+ category: "pwmgr",
+ method: "autocomplete_shown",
+ object: "generatedpassword",
+});
+
+async function waitForTelemetryEventsCondition(cond, options = {},
+ errorMsg = "waitForTelemetryEventsCondition timed out", maxTries = 200) {
+ return TestUtils.waitForCondition(async () => {
+ let events = await getTelemetryEvents(options);
+ let result;
+ try {
+ result = cond(events);
+ info("waitForTelemetryEventsCondition, result: " + result);
+ } catch (e) {
+ info("waitForTelemetryEventsCondition caught exception, got events: " + JSON.stringify(events));
+ ok(false, `${e}\n${e.stack}`);
+ }
+ return result ? events : null;
+ }, errorMsg, maxTries);
+}
+
+async function promiseACPopupClosed() {
+ return SimpleTest.promiseWaitForCondition(async () => {
+ let popupState = await getPopupState();
+ return !popupState.open;
+ }, "Wait for AC popup to be closed");
+}
+
+async function showACPopup(formNumber, expectedACLabels) {
+ const autocompleteItems = await popupByArrowDown();
+ checkAutoCompleteResults(autocompleteItems, expectedACLabels,
+ window.location.host, "Check all rows are correct");
+}
+
+async function checkTelemetryEventsPWGenShown(expectedPWGenTelemetryEvents) {
+ info(`showed generated password option, check there are now ${expectedPWGenTelemetryEvents} generatedpassword telemetry events`);
+ await waitForTelemetryEventsCondition(events => {
+ return events.length == expectedPWGenTelemetryEvents;
+ }, { process: "parent", filterProps: TelemetryFilterPropsShown }, `Wait for there to be ${expectedPWGenTelemetryEvents} shown telemetry events`);
+}
+
+async function checkTelemetryEventsPWGenUsed(expectedPWGenTelemetryEvents) {
+ info("filled generated password again, ensure we don't record another generatedpassword autocomplete telemetry event");
+ let telemetryEvents;
+ try {
+ telemetryEvents = await waitForTelemetryEventsCondition(events => events.length == expectedPWGenTelemetryEvents + 1,
+ { process: "parent", filterProps: TelemetryFilterPropsUsed },
+ `Wait for there to be ${expectedPWGenTelemetryEvents + 1} used events`, 50);
+ } catch (ex) {}
+ ok(!telemetryEvents, `Expected to timeout waiting for there to be ${expectedPWGenTelemetryEvents + 1} events`);
+}
+
+function clearGeneratedPasswords() {
+ const { LoginManagerParent } = ChromeUtils.import("resource://gre/modules/LoginManagerParent.jsm");
+ if (LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin()) {
+ LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().clear();
+ }
+}
+
+add_setup(async () => {
+ let useEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsUsed, clear: true });
+ is(useEvents.length, 0, "Expect 0 use events");
+ let showEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsShown, clear: true });
+ is(showEvents.length, 0, "Expect 0 show events");
+ let acEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsAC, clear: true });
+ is(acEvents.length, 0, "Expect 0 autocomplete events");
+
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["signon.generation.available", true],
+ ["signon.generation.enabled", true],
+ ]});
+});
+
+add_task(async function test_autofillAutocompleteUsername_noGeneration() {
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["signon.generation.available", false],
+ ["signon.generation.enabled", false],
+ ]});
+ await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]);
+
+ createLoginForm({
+ num: 1,
+ action: "https://autofill",
+ password: {
+ name: "p"
+ }
+ });
+ const form2 = createLoginForm({
+ num: 2,
+ action: "https://autofill",
+ password: {
+ name: "password",
+ autocomplete: "new-password"
+ }
+ });
+ await promiseFormsProcessedInSameProcess(2);
+
+ // reference form was filled as expected?
+ checkForm(1, "user1", "pass1");
+
+ // 2nd form should not be filled
+ checkForm(2, "", "");
+
+ form2.uname.focus();
+ await showACPopup(2, ["user1"]);
+
+ let acEvents = await waitForTelemetryEventsCondition(events => {
+ return events.length == 1;
+ }, { process: "parent", filterProps: TelemetryFilterPropsAC, clear: true }, `Wait for there to be 1 autocomplete telemetry event`);
+ checkACTelemetryEvent(acEvents[0], form2.uname, {
+ "hadPrevious": "0",
+ "login": "1",
+ "loginsFooter": "1"
+ });
+
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_Enter");
+
+ await promiseFormsProcessedInSameProcess();
+ checkForm(2, "user1", "pass1");
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_autofillAutocompletePassword_noGeneration() {
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["signon.generation.available", false],
+ ["signon.generation.enabled", false],
+ ]});
+ await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]);
+
+ const form = createLoginForm({
+ num: 2,
+ action: "https://autofill",
+ password: {
+ name: "password",
+ autocomplete: "new-password"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ // 2nd form should not be filled
+ checkForm(2, "", "");
+
+ form.password.focus();
+ await showACPopup(2, ["user1"]);
+ let acEvents = await waitForTelemetryEventsCondition(events => {
+ return events.length == 1;
+ }, { process: "parent", filterProps: TelemetryFilterPropsAC, clear: true }, `Wait for there to be 1 autocomplete telemetry event`);
+ checkACTelemetryEvent(acEvents[0], form.password, {
+ "hadPrevious": "0",
+ "login": "1",
+ "loginsFooter": "1"
+ });
+
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_Enter");
+ // Can't use promiseFormsProcessedInSameProcess() when autocomplete fills the field directly.
+ await SimpleTest.promiseWaitForCondition(() => form.password.value == "pass1", "Check pw filled");
+ checkForm(2, "", "pass1");
+
+ // No autocomplete results should appear for non-empty pw fields.
+ await noPopupByArrowDown();
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_autofillAutocompleteUsername_noGeneration2() {
+ await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]);
+
+ const form = createLoginForm({
+ num: 2,
+ action: "https://autofill",
+ password: {
+ name: "password",
+ autocomplete: "new-password"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ // 2nd form should not be filled
+ checkForm(2, "", "");
+
+ form.uname.focus();
+ // No generation option on username fields.
+ await showACPopup(2, ["user1"]);
+ let acEvents = await waitForTelemetryEventsCondition(events => {
+ return events.length == 1;
+ }, { process: "parent", filterProps: TelemetryFilterPropsAC, clear: true }, `Wait for there to be 1 autocomplete telemetry event`);
+ checkACTelemetryEvent(acEvents[0], form.uname, {
+ "hadPrevious": "0",
+ "login": "1",
+ "loginsFooter": "1"
+ });
+
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ checkForm(2, "user1", "pass1");
+});
+
+add_task(async function test_autofillAutocompletePassword_withGeneration() {
+ const formAttributesToTest = [
+ {
+ num: 2,
+ action: "https://autofill",
+ password: {
+ name: "password",
+ autocomplete: "new-password"
+ }
+ },
+ {
+ num: 3,
+ action: "https://autofill",
+ username: {
+ name: "username"
+ },
+ password: {
+ name: "password",
+ label: "New password"
+ }
+ }
+ ];
+
+ // Bug 1616356 and Bug 1548878: Recorded once per origin
+ let expectedPWGenTelemetryEvents = 0;
+ // Bug 1619498: Recorded once every time the autocomplete popup is shown
+ let expectedACShownTelemetryEvents = 0;
+
+ for (const formAttributes of formAttributesToTest) {
+ runInParent(clearGeneratedPasswords);
+ await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]);
+
+ const formNumber = formAttributes.num;
+
+ const form = createLoginForm(formAttributes);
+ await promiseFormsProcessedInSameProcess();
+ form.reset();
+
+ // This form should be filled
+ checkForm(formNumber, "", "");
+
+ form.password.focus();
+
+ await showACPopup(formNumber, [
+ "user1",
+ "Use a Securely Generated Password",
+ ]);
+ expectedPWGenTelemetryEvents++;
+ expectedACShownTelemetryEvents++;
+
+ await checkTelemetryEventsPWGenShown(expectedPWGenTelemetryEvents);
+ let acEvents = await waitForTelemetryEventsCondition(events => {
+ return events.length == expectedACShownTelemetryEvents;
+ }, { process: "parent", filterProps: TelemetryFilterPropsAC }, `Wait for there to be ${expectedACShownTelemetryEvents} autocomplete telemetry event(s)`);
+ checkACTelemetryEvent(acEvents[expectedACShownTelemetryEvents - 1], form.password, {
+ "generatedPasswo": "1",
+ "hadPrevious": "0",
+ "login": "1",
+ "loginsFooter": "1"
+ });
+
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_Enter");
+ // Can't use promiseFormsProcessedInSameProcess() when autocomplete fills the field directly.
+ await SimpleTest.promiseWaitForCondition(() => form.password.value == "pass1", "Check pw filled");
+ checkForm(formNumber, "", "pass1");
+
+ // No autocomplete results should appear for non-empty pw fields.
+ await noPopupByArrowDown();
+
+ info("Removing all logins to test auto-saving of generated passwords");
+ await LoginManager.removeAllUserFacingLogins();
+
+ while (form.password.value) {
+ synthesizeKey("KEY_Backspace");
+ }
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blanked field");
+
+ info("This time select the generated password");
+ await showACPopup(formNumber, [
+ "Use a Securely Generated Password",
+ ]);
+ expectedACShownTelemetryEvents++;
+
+ await checkTelemetryEventsPWGenShown(expectedPWGenTelemetryEvents);
+ acEvents = await waitForTelemetryEventsCondition(events => {
+ return events.length == expectedACShownTelemetryEvents;
+ }, { process: "parent", filterProps: TelemetryFilterPropsAC }, `Wait for there to be ${expectedACShownTelemetryEvents} autocomplete telemetry event(s)`);
+ checkACTelemetryEvent(acEvents[expectedACShownTelemetryEvents - 1], form.password, {
+ "generatedPasswo": "1",
+ "hadPrevious": "0",
+ "loginsFooter": "1"
+ });
+
+ synthesizeKey("KEY_ArrowDown");
+ let storageAddPromise = promiseStorageChanged(["addLogin"]);
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Before first fill of generated pw");
+ synthesizeKey("KEY_Enter");
+
+ info("waiting for the password field to be filled with the generated password");
+ await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check generated pw filled");
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, "After first fill of generated pw");
+ info("Wait for generated password to be added to storage");
+ await storageAddPromise;
+
+ let logins = await LoginManager.getAllLogins();
+ let timePasswordChanged = logins[logins.length - 1].timePasswordChanged;
+ let time = dateAndTimeFormatter.format(new Date(timePasswordChanged));
+ const LABEL_NO_USERNAME = "No username (" + time + ")";
+
+ let generatedPW = form.password.value;
+ is(generatedPW.length, GENERATED_PASSWORD_LENGTH, "Check generated password length");
+ ok(generatedPW.match(GENERATED_PASSWORD_REGEX), "Check generated password format");
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, "After fill");
+
+ info("Check field is masked upon blurring");
+ synthesizeKey("KEY_Tab"); // blur
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "After blur");
+ synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, "After shift-tab to focus again");
+ // Remove selection for OS where the whole value is selected upon focus.
+ synthesizeKey("KEY_ArrowRight");
+
+ while (form.password.value) {
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, form.password.value);
+ synthesizeKey("KEY_Backspace");
+ }
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blanked field");
+
+ info("Blur the empty field to trigger a 'change' event");
+ synthesizeKey("KEY_Tab"); // blur
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blur after blanking");
+ synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Focus again after blanking");
+
+ info("Type a single character after blanking");
+ synthesizeKey("@");
+
+ info("Blur the single-character field to trigger a 'change' event");
+ synthesizeKey("KEY_Tab"); // blur
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blur after backspacing");
+ synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Focus again after backspacing");
+ synthesizeKey("KEY_Backspace"); // Blank the field again
+
+ await showACPopup(formNumber, [
+ LABEL_NO_USERNAME,
+ "Use a Securely Generated Password",
+ ]);
+ expectedACShownTelemetryEvents++;
+
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_Enter");
+ await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check generated pw filled");
+ // Same generated password should be used, even despite the 'change' to @ earlier.
+ checkForm(formNumber, "", generatedPW);
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, "Second fill of the generated pw");
+
+ await checkTelemetryEventsPWGenUsed(expectedPWGenTelemetryEvents);
+
+ logins = await LoginManager.getAllLogins();
+ is(logins.length, 1, "Still 1 login after filling the generated password a 2nd time");
+ is(logins[0].timePasswordChanged, timePasswordChanged, "Saved login wasn't changed");
+ is(logins[0].password, generatedPW, "Password is the same");
+
+ info("filling the saved login to ensure the field is masked again");
+
+ while (form.password.value) {
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, form.password.value);
+ synthesizeKey("KEY_Backspace");
+ }
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blanked field again");
+
+ info("Blur the field to trigger a 'change' event again");
+ synthesizeKey("KEY_Tab"); // blur
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blur after blanking again");
+ synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Focus again after blanking again");
+ // Remove selection for OS where the whole value is selected upon focus.
+ synthesizeKey("KEY_ArrowRight");
+
+ await showACPopup(formNumber, [
+ LABEL_NO_USERNAME,
+ "Use a Securely Generated Password",
+ ]);
+ expectedACShownTelemetryEvents++;
+
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_Enter");
+ await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check saved generated pw filled");
+ // Same generated password should be used but from storage
+ checkForm(formNumber, "", generatedPW);
+ // Passwords from storage should always be masked.
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "after fill from storage");
+ synthesizeKey("KEY_Tab"); // blur
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "after blur");
+ synthesizeKey("KEY_Tab", { shiftKey: true }); // focus
+ LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "after shift-tab to focus again");
+ }
+});
+
+add_task(async function test_autofillAutocompletePassword_saveLoginDisabled() {
+ await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]);
+
+ const form = createLoginForm({
+ num: 2,
+ action: "https://autofill",
+ password: {
+ name: "password",
+ autocomplete: "new-password"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ // form should not be filled
+ checkForm(2, "", "");
+
+ let formOrigin = new URL(document.documentURI).origin;
+ is(formOrigin, location.origin, "Expected form origin");
+
+ await LoginManager.setLoginSavingEnabled(location.origin, false);
+
+ form.password.focus();
+ // when login-saving is disabled for an origin, we expect no generated password row here
+ await showACPopup(2, ["user1"]);
+
+ // close any open menu
+ synthesizeKey("KEY_Escape");
+ await promiseACPopupClosed();
+
+ await LoginManager.setLoginSavingEnabled(location.origin, true);
+});
+
+add_task(async function test_deleteAndReselectGeneratedPassword() {
+ await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]);
+
+ const form = createLoginForm({
+ num: 2,
+ action: "https://autofill",
+ password: {
+ name: "password",
+ autocomplete: "new-password"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ info("Removing all logins to test auto-saving of generated passwords");
+ await LoginManager.removeAllUserFacingLogins();
+
+ // form should not be filled
+ checkForm(2, "", "");
+
+ async function showAndSelectACPopupItem(index) {
+ form.password.focus();
+ if (form.password.value) {
+ form.password.select();
+ synthesizeKey("KEY_Backspace");
+ }
+ const autocompleteItems = await popupByArrowDown();
+ if (index < 0) {
+ index = autocompleteItems.length + index;
+ }
+ for (let i=0; i<=index; i++) {
+ synthesizeKey("KEY_ArrowDown");
+ }
+ await TestUtils.waitForTick();
+ return autocompleteItems[index];
+ }
+
+ let storagePromise, menuLabel, itemIndex, savedLogins;
+
+ // fill the password field with the generated password via auto-complete menu
+ storagePromise = promiseStorageChanged(["addLogin"]);
+ // select last-but-2 item - the one before the footer
+ menuLabel = await showAndSelectACPopupItem(-2);
+ is(menuLabel, "Use a Securely Generated Password", "Check item label");
+ synthesizeKey("KEY_Enter");
+ info("waiting for the password field to be filled with the generated password");
+ await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check generated pw filled");
+ info("Wait for generated password to be added to storage");
+ await storagePromise;
+
+ form.uname.focus();
+ await TestUtils.waitForTick();
+
+ is(form.password.value.length, LoginTestUtils.generation.LENGTH, "Check password looks generated");
+ const GENERATED_PASSWORD = form.password.value;
+
+ savedLogins = await LoginManager.getAllLogins();
+ is(savedLogins.length, 1, "Check saved logins count");
+
+ info("clear the password field and delete the saved login using the AC menu")
+ storagePromise = promiseStorageChanged(["removeLogin"]);
+
+ itemIndex = 0;
+ menuLabel = await showAndSelectACPopupItem(itemIndex);
+ ok(menuLabel.includes("No username"), "Check first item is the auto-saved login");
+ // Send delete to remove the auto-saved login from storage
+ // On OS X, shift-backspace and shift-delete work, just delete does not.
+ // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
+ synthesizeKey("KEY_Delete", {shiftKey: true});
+ await storagePromise;
+
+ form.uname.focus();
+ await TestUtils.waitForTick();
+
+ savedLogins = await LoginManager.getAllLogins();
+ is(savedLogins.length, 0, "Check saved logins count");
+
+ info("Re-fill with the generated password");
+ // select last-but-2 item - the one before the footer
+ menuLabel = await showAndSelectACPopupItem(-2);
+ is(menuLabel, "Use a Securely Generated Password", "Check item label");
+ synthesizeKey("KEY_Enter");
+ info("waiting for the password field to be filled with the generated password");
+ await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check generated pw filled");
+
+ form.uname.focus();
+ await TestUtils.waitForTick();
+ is(form.password.value, GENERATED_PASSWORD, "Generated password has not changed");
+});
+
+// add_task(async function test_passwordGenerationShownTelemetry() {
+// // Should only be recorded once per principal origin per session, but the cache is cleared each time ``initLogins`` is called.
+// await waitForTelemetryEventsCondition(events => {
+// return events.length == 3;
+// }, { process: "parent", filterProps: TelemetryFilterPropsShown }, "Expect 3 shown telemetry events");
+// });
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html
new file mode 100644
index 0000000000..cd5d5952f6
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html
@@ -0,0 +1,518 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test filling generated passwords into confirm password fields</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <script src="../../../satchel/test/satchel_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: filling generated passwords into confirm password fields
+
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+
+ <!-- form1 has new-password followed by confirm-password fields -->
+ <form id="form1" action="https://example.com" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" autocomplete="new-password">
+ <input type="password" name="pword-next">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form2 has 2 new-password fields -->
+ <form id="form2" action="https://example.com" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" autocomplete="new-password">
+ <input type="password" name="pword-between">
+ <input type="password" name="pword-next" autocomplete="new-password">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form3 has lots of junk fields before the confirm-password field -->
+ <form id="form3" action="https://example.com" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" autocomplete="new-password">
+ <input type="text" name="junk0">
+ <input type="text" name="junk1">
+ <input type="text" name="junk2">
+ <input type="text" name="junk3">
+ <input type="text" name="junk4">
+ <input type="password" name="pword-next">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form4 has a password field after the confirm-password field we don't want to fill into -->
+ <form id="form4" action="https://example.com" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" autocomplete="new-password">
+ <input type="password" name="pword-next" autocomplete="new-password">
+ <input type="password" name="pword-extra" autocomplete="new-password">
+ <button type="submit">Submit</button>
+ </form>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const { ContentTaskUtils } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/ContentTaskUtils.jsm"
+);
+const { TestUtils } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+);
+
+const setupScript = runInParent(function parentTestSetup() {
+ const { LoginTestUtils } = ChromeUtils.import(
+ "resource://testing-common/LoginTestUtils.jsm"
+ );
+
+ addMessageListener(
+ "resetLoginsAndGeneratedPasswords", () => {
+ LoginTestUtils.clearData();
+ LoginTestUtils.resetGeneratedPasswordsCache();
+ }
+ );
+});
+
+function testReset() {
+ return setupScript.sendAsyncMessage("resetLoginsAndGeneratedPasswords");
+}
+
+function generateDateString(date) {
+ let dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined,
+ { dateStyle: "medium" });
+ return dateAndTimeFormatter.format(date);
+}
+
+const DATE_NOW_STRING = generateDateString(new Date());
+
+async function promiseACPopupClosed() {
+ return SimpleTest.promiseWaitForCondition(async () => {
+ // FIXME(bug 1721900): previously `promiseWaitForCondition` wouldn't await
+ // on async functions, so the condition would always immediately resolve as
+ // `true`.
+ return true;
+ // let popupState = await getPopupState();
+ // return !popupState.open;
+ }, "Wait for AC popup to be closed");
+}
+
+async function fillWithGeneratedPassword(input, expectedResults = ["Use a Securely Generated Password"]) {
+ info("Opening the AC menu");
+ const { items } = await openPopupOn(input);
+ checkAutoCompleteResults(items, expectedResults,
+ window.location.host, "Check all rows are correct");
+
+ for (let autocompleteItem of items) {
+ synthesizeKey("KEY_ArrowDown");
+ if (autocompleteItem == "Use a Securely Generated Password") {
+ synthesizeKey("KEY_Enter");
+ break;
+ }
+ }
+ await TestUtils.waitForTick();
+}
+
+async function testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ passwordInputName = "pword",
+ confirmInputName = "pword-next",
+ expectedACResults,
+ expectedFilled = [], // the names of the inputs which should be filled
+ // field-name: expected-value for any non-filled inputs which should have values other than their .defaultValue
+ expectedNonDefaultValues = {},
+ expectedMasked = [], // the names of the inputs which should be masked
+ beforeFn,
+ afterFn
+}) {
+ // form should not be initially filled
+ checkForm(formNumber, "", "");
+
+ if (beforeFn) {
+ await beforeFn(document.getElementById(`form${formNumber}`));
+ document.documentElement.scrollTop; // Flush pending reflows which may be caused by beforeFn.
+ }
+ let pword = getFormElementByName(formNumber, passwordInputName || "pword");
+ let pword2 = getFormElementByName(formNumber, confirmInputName || "pword-next");
+
+ await fillWithGeneratedPassword(pword, expectedACResults);
+
+ info("waiting for the password field to be filled with the generated password");
+ await SimpleTest.promiseWaitForCondition(() => !!pword.value, "Check generated pw filled");
+ await TestUtils.waitForTick();
+
+ let generatedPW = pword.value;
+ is(generatedPW.length, GENERATED_PASSWORD_LENGTH, "Check generated password length");
+ ok(generatedPW.match(GENERATED_PASSWORD_REGEX), "Check generated password format");
+ LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, "After fill");
+
+ info("Check the expected password fields are filled");
+ for (let input of pword.form.querySelectorAll("input")) {
+ // check field filling & highlights
+ if (expectedFilled.includes(input.name)) {
+ await ContentTaskUtils.waitForCondition(() => {
+ return input.matches(":autofill");
+ }, `Highlight was successfully applied to the (${input.name}) field`);
+
+ is(input.value, generatedPW, `Field (${input.name}) has the generated password value`);
+ } else {
+ await ContentTaskUtils.waitForCondition(() => {
+ return !input.matches(":autofill");
+ }, `Highlight was not applied to field (${input.name})`);
+
+ let expectedValue = (input.name in expectedNonDefaultValues) ? expectedNonDefaultValues[input.name] : input.defaultValue;
+ is(input.value, expectedValue, `Field (${input.name}) field has the expected value`);
+ }
+
+ if (expectedMasked.includes(input.name)) {
+ LOGIN_FIELD_UTILS.checkPasswordMasked(input, true, `Field (${input.name}) should be masked`);
+ }
+ }
+
+ if (expectedFilled.includes(pword2.name)) {
+ info("Check the 2 field values aren't mirrored");
+ // changing the password field value should result in a message sent to the parent process
+ let messageSentPromise = getPasswordEditedMessage();
+
+ pword.focus();
+ // add a character. We don't expect the confirm password field to get the same change
+ synthesizeKey("KEY_End");
+ synthesizeKey("@");
+ await TestUtils.waitForTick();
+ is(pword.value.length, GENERATED_PASSWORD_LENGTH + 1, "Value of the first password field changed");
+ is(pword2.value, generatedPW, "Value of the confirm field did not change");
+
+ // focusing the 2nd password field blurs the first and results in a "change" event
+ pword2.focus();
+ info("Waiting for edit message");
+ await messageSentPromise;
+
+ LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Generated password is masked when blurred");
+ if (expectedMasked.includes(pword2.name)) {
+ LOGIN_FIELD_UTILS.checkPasswordMasked(pword2, false, "Confirm password should be be unmasked");
+ }
+
+ // remove a character from the confirm field.
+ // We don't expect the filled password field to get the same change
+ synthesizeKey("KEY_End");
+ synthesizeKey("KEY_Backspace");
+ await TestUtils.waitForTick();
+ is(pword2.value, generatedPW.substring(0, GENERATED_PASSWORD_LENGTH - 1), "Value of the confirm field changed");
+ is(pword.value.length, GENERATED_PASSWORD_LENGTH + 1, "Value of the first password field didn't change");
+
+ // the confirm field has its highlight cleared when emptied
+ pword2.focus();
+ pword2.select();
+ synthesizeKey("KEY_Backspace");
+ await TestUtils.waitForTick();
+ await ContentTaskUtils.waitForCondition(() => {
+ return !pword2.matches(":autofill");
+ }, "Highlight was successfully cleared from the confirm-password field");
+
+ // if it got originally masked (i.e. was a password field) verify the focused confirm field now masks
+ // its input like a normal, non-generated password field after being emptied
+ if (expectedMasked.includes(pword2.name)) {
+ pword2.focus();
+ synthesizeKey("a");
+ LOGIN_FIELD_UTILS.checkPasswordMasked(pword2, true, "Confirm password gets masked again when focused");
+ }
+ } else {
+ let expectedValue = (pword2.name in expectedNonDefaultValues) ? expectedNonDefaultValues[pword2.name] : pword2.defaultValue;
+ is(pword2.value, expectedValue, "Value of the confirm-password field is unchanged");
+
+ if (pword2.type == "password" && !pword2.disabled && !pword2.readOnly) {
+ // make sure we didn't change the behavior of a non-generated password field
+ // it should remain masked when it has focus
+ pword2.focus();
+ LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Non-generataed password field remains masked when focused");
+ }
+ }
+
+ if (afterFn) {
+ await afterFn(document.getElementById(`form${formNumber}`));
+ document.documentElement.scrollTop; // Flush pending reflows which may be caused by afterFn.
+ }
+
+ // close any open menu
+ synthesizeKey("KEY_Escape");
+ await promiseACPopupClosed();
+
+ recreateTree(document.getElementById(`form${formNumber}`));
+}
+
+add_task(async function test_fillNextPasswordField() {
+ const formNumber = 1;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ // the next password field should be filled with the generated password
+ expectedFilled: ["pword", "pword-next"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ // the confirm field should be masked, the filled field has focus and should not be masked
+ expectedMasked: ["pword-next"],
+ });
+});
+
+add_task(async function test_fillNextPasswordFieldWasPasswordType() {
+ const formNumber = 1;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ beforeFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ is(pword2.type, "password", "confirm field is of password type");
+ pword2.type = "text";
+ },
+ // the next hasBeenTypePassword field should be filled with the generated password
+ expectedFilled: ["pword", "pword-next"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ // the confirm field should is currently type=text so will not be masked
+ expectedMasked: [],
+ afterFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.type = "password";
+ }
+ });
+});
+
+add_task(async function test_dontFillNonEmptyPasswordField() {
+ const formNumber = 1;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ beforeFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.setAttribute("value", "previous value");
+ },
+ // the would-be confirm field is not empty so will not be filled
+ expectedFilled: ["pword"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ // pword is filled by the test, but has focus so isn't masked. Its masking behavior is tested elsewhere.
+ expectedMasked: [],
+ afterFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.setAttribute("value", "");
+ }
+ });
+});
+
+add_task(async function test_dontFillEditedNewPasswordField() {
+ const formNumber = 2;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ beforeFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.focus()
+ sendString("edited value");
+ },
+ // the would-be confirm field is not empty so will not be filled
+ expectedFilled: ["pword"],
+ expectedNonDefaultValues: {
+ "pword-next": "edited value",
+ },
+ // pword is filled by the test, but has focus so isn't masked. Its masking behavior is tested elsewhere.
+ expectedMasked: [],
+ afterFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.setAttribute("value", "");
+ }
+ });
+});
+
+add_task(async function test_ignoreReadOnlyField() {
+ const formNumber = 1;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ beforeFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.readOnly = true;
+ },
+ // the confirm field candidate is read-only so will not be filled
+ expectedFilled: ["pword"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ // pword is filled by the test, but has focus so isn't masked. Its masking behavior is tested elsewhere.
+ expectedMasked: [],
+ afterFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.readOnly = false;
+ }
+ });
+});
+
+add_task(async function test_ignoreDisabledField() {
+ const formNumber = 1;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ beforeFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.disabled = true;
+ },
+ // the confirm field candidate is read-only so will not be filled
+ expectedFilled: ["pword"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ // pword is filled by the test, but has focus so isn't masked. Its masking behavior is tested elsewhere.
+ expectedMasked: [],
+ afterFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.disabled = false;
+ }
+ });
+});
+
+add_task(async function test_preferMatchingAutoCompleteInfoPasswordField() {
+ const formNumber = 2;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ expectedFilled: ["pword", "pword-next"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ expectedMasked: ["pword-next"],
+ });
+});
+
+add_task(async function test_ignoreDisabledMatchingAutoCompleteInfoPasswordField() {
+ const formNumber = 2;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ beforeFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.disabled = true;
+ },
+ // it should fill the next password field, not the disabled one
+ confirmInputName: "pword-between",
+ expectedFilled: ["pword", "pword-between"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ expectedMasked: ["pword-between"],
+ afterFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.disabled = false;
+ },
+ });
+});
+
+add_task(async function test_ignoreTooDistantPasswordField() {
+ const formNumber = 3;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ expectedFilled: ["pword"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ expectedMasked: [],
+ });
+});
+
+add_task(async function test_tooManyDisabledFields() {
+ // we don't fill into disabled fields,
+ // but they do count towards the distance from the first password field
+ const formNumber = 3;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ beforeFn(form) {
+ for(let inp of form.querySelectorAll("input[name*='junk']")) {
+ inp.disabled = true;
+ }
+ },
+ expectedFilled: ["pword"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ expectedMasked: [],
+ afterFn(form) {
+ for(let inp of form.querySelectorAll("input[name*='junk']")) {
+ inp.disabled = false;
+ }
+ },
+ });
+});
+
+add_task(async function test_skipOverHiddenFields() {
+ // hidden fields do not count towards the distance from the first password field
+ const formNumber = 3;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ beforeFn(form) {
+ for(let inp of form.querySelectorAll("input[name*='junk']")) {
+ inp.type = "hidden";
+ }
+ },
+ expectedFilled: ["pword", "pword-next"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ expectedMasked: ["pword-next"],
+ afterFn(form) {
+ for(let inp of form.querySelectorAll("input[name*='junk']")) {
+ inp.type = "text";
+ }
+ },
+ });
+});
+
+add_task(async function test_dontFill3rdPasswordField() {
+ // if a generated password field was previously filled on a form
+ // don't look for a confirm-password field when filling another field with a generated password
+ const formNumber = 4;
+ await testReset();
+
+ let pword = getFormElementByName(formNumber, "pword");
+ let pword2 = getFormElementByName(formNumber, "pword-next");
+ let pword3 = getFormElementByName(formNumber, "pword-extra");
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ async beforeFn(form) {
+ // disable the following password fields so they dont get filled just yet
+ pword2.disabled = true;
+ pword3.disabled = true;
+
+ info("beforeFn, filling the pword field");
+ await fillWithGeneratedPassword(pword);
+ info("beforeFn: waiting for the password field to be filled with the generated password");
+ await SimpleTest.promiseWaitForCondition(() => !!pword.value, "Check generated pw filled");
+
+ pword2.disabled = false;
+ pword3.disabled = false;
+ is(pword2.value, "", "The pword-next field was not filled");
+ is(pword3.value, "", "The pword-extra field was not filled");
+ },
+ // Fill into the confirm-password field,
+ // we want to confirm this doesnt now try and fill another nearby "confirm" field
+ passwordInputName: "pword-next",
+ confirmInputName: "pword-extra",
+ // we already generated a password in this test, so the AC menu will have the auto-saved login
+ expectedACResults: [
+ "No username (" + DATE_NOW_STRING + ")",
+ "Use a Securely Generated Password",
+ ],
+ // we've manually filled both fields,
+ // we don't expect the third 'pword-extr' field to be filled
+ expectedFilled: ["pword", "pword-next"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ expectedMasked: [
+ "pword", // pword was filled in the beforeFn and is blurred so it should be masked
+ // pword-next was filled by the test and is focused so should not be masked
+ ],
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_open.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_open.html
new file mode 100644
index 0000000000..e792aa19af
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_open.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test password field autocomplete footer with and without logins</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content"></div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: Test password field autocomplete footer with and without logins **/
+
+add_task(async function test_no_autofill() {
+ await setStoredLoginsAsync(
+ [location.origin, "", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm();
+ await promiseFormsProcessedInSameProcess();
+
+ // Make sure initial form is empty as autofill shouldn't happen in the sandboxed frame.
+ checkLoginForm(form.uname, "", form.pword, "");
+ let popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+add_task(async function test_two_logins() {
+ await setStoredLoginsAsync(
+ [location.origin, "", null, "user-1", "pass-1", "uname", "pword"],
+ [location.origin, "", null, "user-2", "pass-2", "uname", "pword"]
+ );
+ const form = createLoginForm();
+ await promiseFormsProcessedInSameProcess();
+
+ await popupBy(() => form.uname.focus());
+
+ // popup on the password field should open upon focus
+ let results = await popupBy(() => synthesizeKey("KEY_Tab"));
+
+ let popupState = await getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ let expectedMenuItems = [
+ "user-1",
+ "user-2",
+ ];
+ checkAutoCompleteResults(results, expectedMenuItems, window.location.host, "Check all menuitems are displayed correctly.");
+
+ checkLoginForm(form.uname, "", form.pword, "");
+});
+
+add_task(async function test_zero_logins() {
+ // no logins stored
+ await setStoredLoginsAsync();
+ const form = createLoginForm();
+ await promiseFormsProcessedInSameProcess();
+
+ form.uname.focus();
+
+ let shownPromise = popupBy().then(() => ok(false, "Should not have shown"));
+ // Popup on the password field should NOT automatically open upon focus when there are no saved logins.
+ synthesizeKey("KEY_Tab"); // focus the password field
+ SimpleTest.requestFlakyTimeout("Giving a chance for the unexpected popup to show");
+ let autocompleteItems = await Promise.race([
+ shownPromise,
+ new Promise(resolve => setTimeout(resolve, 2000)), // Wait 2s for the popup to appear
+ ]);
+
+ let popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is still closed");
+
+ checkLoginForm(form.uname, "", form.pword, "");
+ info("arrow down should still open the popup");
+ autocompleteItems = await popupByArrowDown();
+ checkAutoCompleteResults(autocompleteItems, [], window.location.host, "Check only footer is displayed.");
+ checkLoginForm(form.uname, "", form.pword, "");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_sandboxed.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_sandboxed.html
new file mode 100644
index 0000000000..229791109a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_sandboxed.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form field autocomplete in sandboxed documents (null principal)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+ <iframe id="sandboxed"
+ sandbox=""
+ src="form_basic.html"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: form field autocomplete in sandboxed documents (null principal) **/
+
+let sandboxed = document.getElementById("sandboxed");
+let uname;
+let pword;
+let hostname;
+
+add_setup(async () => {
+ await setStoredLoginsAsync(
+ [window.location.origin, "", null, "tempuser1", "temppass1", "uname", "pword"]
+ );
+ let frameWindow = SpecialPowers.wrap(sandboxed).contentWindow;
+ // Can't use SimpleTest.promiseFocus as it doesn't work with the sandbox.
+ await SimpleTest.promiseWaitForCondition(() => {
+ return frameWindow.document.readyState == "complete" && frameWindow.location.href != "about:blank";
+ }, "Check frame is loaded");
+ let frameDoc = SpecialPowers.wrap(sandboxed).contentDocument;
+ uname = frameDoc.getElementById("form-basic-username");
+ pword = frameDoc.getElementById("form-basic-password");
+ hostname = frameDoc.documentURIObject.host;
+});
+
+add_task(async function test_no_autofill() {
+ // Make sure initial form is empty as autofill shouldn't happen in the sandboxed frame.
+ checkLoginForm(uname, "", pword, "");
+ let popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+add_task(async function test_autocomplete_warning_no_logins() {
+ const { items } = await openPopupOn(pword);
+
+ let popupState = await getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ let expectedMenuItems = [
+ "This connection is not secure. Logins entered here could be compromised. Learn More",
+ ];
+ checkAutoCompleteResults(items, expectedMenuItems, hostname, "Check all menuitems are displayed correctly.");
+
+ checkLoginForm(uname, "", pword, "");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_tab_between_fields.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_tab_between_fields.html
new file mode 100644
index 0000000000..9df3467621
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_tab_between_fields.html
@@ -0,0 +1,167 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autocomplete behavior when tabbing between form fields</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content"></div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const { TestUtils } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+);
+
+const availableLogins = {
+ "exampleUser1": [location.origin, "https://autofill", null, "user1", "pass1", "uname", "pword"],
+ "subdomainUser1": ["https://sub." + location.host, "https://autofill", null, "user1", "pass1", "uname", "pword"],
+ "emptyUsername": [location.origin, "https://autofill", null, "", "pass2", "uname", "pword"],
+}
+
+const tests = [
+ {
+ name: "single_login_exact_origin_no_inputs",
+ logins: ["exampleUser1"],
+ expectedAutofillUsername: "user1",
+ expectedAutofillPassword: "pass1",
+ expectedACLabels: ["user1"],
+ typeUsername: null,
+ expectedTabbedUsername: "",
+ expectedTabbedPassword: "",
+ },
+ {
+ name: "single_login_exact_origin_initial_letter",
+ logins: ["exampleUser1"],
+ expectedAutofillUsername: "user1",
+ expectedAutofillPassword: "pass1",
+ expectedACLabels: ["user1"],
+ typeUsername: "u",
+ expectedTabbedUsername: "u",
+ expectedTabbedPassword: "",
+ },
+ {
+ name: "single_login_exact_origin_type_username",
+ logins: ["exampleUser1"],
+ expectedAutofillUsername: "user1",
+ expectedAutofillPassword: "pass1",
+ expectedACLabels: ["user1"],
+ typeUsername: "user1",
+ expectedTabbedUsername: "user1",
+ expectedTabbedPassword: "pass1",
+ },
+ {
+ name: "single_login_subdomain_no_inputs",
+ logins: ["subdomainUser1"],
+ expectedAutofillUsername: "",
+ expectedAutofillPassword: "",
+ expectedACLabels: ["user1"],
+ typeUsername: null,
+ expectedTabbedUsername: "",
+ expectedTabbedPassword: "",
+ },
+ {
+ name: "single_login_subdomain_type_username",
+ logins: ["subdomainUser1"],
+ expectedAutofillUsername: "",
+ expectedAutofillPassword: "",
+ expectedACLabels: ["user1"],
+ typeUsername: "user1",
+ expectedTabbedUsername: "user1",
+ expectedTabbedPassword: "",
+ },
+ {
+ name: "two_logins_one_with_empty_username",
+ logins: ["exampleUser1", "emptyUsername"],
+ expectedAutofillUsername: "user1",
+ expectedAutofillPassword: "pass1",
+ expectedACLabels: ["user1"],
+ typeUsername: "",
+ expectedTabbedUsername: "",
+ expectedTabbedPassword: "",
+ },
+];
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({"set": [["signon.includeOtherSubdomainsInLookup", true]]});
+});
+
+async function testResultOfTabInteractions(testData) {
+ const logins = testData.logins.map(name => availableLogins[name]);
+ await setStoredLoginsAsync(...logins);
+
+ const form = createLoginForm({
+ action: "https://autofill"
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ await SimpleTest.promiseFocus(window);
+
+ // check autofill results
+ checkForm(1, testData.expectedAutofillUsername, testData.expectedAutofillPassword);
+
+ SpecialPowers.wrap(form.pword).setUserInput("");
+ SpecialPowers.wrap(form.uname).setUserInput("");
+
+ info("Placing focus in the password field");
+ form.pword.focus();
+ await synthesizeKey("KEY_Tab", { shiftKey: true }); // blur pw, focus un
+
+ // moving focus shouldn't change anything
+ await ensureLoginFormStaysFilledWith(form.uname, "", form.pword, "");
+
+ info("waiting for AC results");
+ const results = await popupByArrowDown();
+ info("checking results");
+ checkAutoCompleteResults(results, testData.expectedACLabels,
+ window.location.host, "Check all rows are correct");
+
+ if (testData.typeUsername) {
+ await sendString(testData.typeUsername);
+ }
+
+ // don't select anything from the AC menu
+ await synthesizeKey("KEY_Escape");
+ await TestUtils.waitForCondition(async () => {
+ let popupState = await getPopupState();
+ return !popupState.open;
+ }, "AutoComplete popup should have closed");
+
+ await synthesizeKey("KEY_Tab");
+
+ // wait until username and password are automatically filled in with the
+ // expected values...
+ await TestUtils.waitForCondition(() => {
+ return form.uname.value === testData.expectedTabbedUsername & form.pword.value === testData.expectedTabbedPassword;
+ }, "Username and password field should be filled");
+
+ // ...and if the value is not different from the original value in the form,
+ // make sure that the form keeps its values
+ if (testData.expectedTabbedPassword === "") {
+ await ensureLoginFormStaysFilledWith(form.uname, testData.expectedTabbedUsername, form.pword, testData.expectedTabbedPassword);
+ }
+
+ ok(form.pword.matches("input:focus"), "pword field is focused");
+}
+
+for (const testData of tests) {
+ const tmp = {
+ async [testData.name]() {
+ await testResultOfTabInteractions(testData);
+ },
+ };
+ add_task(tmp[testData.name]);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_autocomplete_types.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_autocomplete_types.html
new file mode 100644
index 0000000000..c44d5a25ef
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_autocomplete_types.html
@@ -0,0 +1,112 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofilling with autocomplete types (username, off, cc-type, etc.)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Test autofilling with autocomplete types (username, off, cc-type, etc.)
+
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/*
+ Test for Login Manager: Skip over inappropriate autcomplete types when finding username field
+ */
+
+const win = window.open("about:blank");
+const loadPromise = loadFormIntoWindow(location.origin, `
+ <form id="form0" action="https://autocomplete">
+ <input type="text" name="uname">
+ <input type="text" autocomplete="">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form1" action="https://autocomplete">
+ <input type="text" name="uname">
+ <input type="text" autocomplete="username">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form2" action="https://autocomplete">
+ <input type="text" name="uname">
+ <input type="text" autocomplete="off" name="acfield">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form3" action="https://autocomplete">
+ <input type="text" name="uname">
+ <input type="text" autocomplete="on" name="acfield">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form4" action="https://autocomplete">
+ <input type="text" name="uname">
+ <input type="text" autocomplete="nosuchtype" name="acfield">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form5" action="https://autocomplete">
+ <input type="text" name="uname">
+ <input type="text" autocomplete="email" name="acfield">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form6" action="https://autocomplete">
+ <input type="text" name="uname">
+ <input type="text" autocomplete="tel" name="acfield">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form7" action="https://autocomplete">
+ <input type="text" name="uname">
+ <input type="text" autocomplete="tel-national" name="acfield">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- Begin forms where the first field is skipped for the username -->
+
+ <form id="form101" action="https://autocomplete">
+ <input type="text" name="uname">
+ <input type="text" autocomplete="cc-number" name="acfield">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>`, win, 8);
+
+add_setup(async () => {
+ await setStoredLoginsAsync(
+ [window.location.origin, "https://autocomplete", null, "testuser@example.com", "testpass1", "", ""]
+ );
+ SimpleTest.registerCleanupFunction(() => win.close());
+});
+
+/* Tests for autofill of single-user forms with various autocomplete types */
+add_task(async function test_autofill_autocomplete_types() {
+ await loadPromise;
+ await checkLoginFormInFrameWithElementValues(win, 0, null, "testuser@example.com", "testpass1");
+ await checkLoginFormInFrameWithElementValues(win, 1, null, "testuser@example.com", "testpass1");
+ await checkLoginFormInFrameWithElementValues(win, 2, null, "testuser@example.com", "testpass1");
+ await checkLoginFormInFrameWithElementValues(win, 3, null, "testuser@example.com", "testpass1");
+ await checkLoginFormInFrameWithElementValues(win, 4, null, "testuser@example.com", "testpass1");
+ await checkLoginFormInFrameWithElementValues(win, 5, null, "testuser@example.com", "testpass1");
+ await checkLoginFormInFrameWithElementValues(win, 6, null, "testuser@example.com", "testpass1");
+ await checkLoginFormInFrameWithElementValues(win, 7, null, "testuser@example.com", "testpass1");
+ await checkLoginFormInFrameWithElementValues(win, 101, "testuser@example.com", null, "testpass1");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_different_formActionOrigin.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_different_formActionOrigin.html
new file mode 100644
index 0000000000..240e250a19
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_different_formActionOrigin.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill on an HTTPS page using upgraded HTTP logins with different formActionOrigin</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+const MISSING_ACTION_PATH = TESTS_DIR + "mochitest/form_basic.html";
+
+const chromeScript = runChecksAfterCommonInit(false);
+
+let nsLoginInfo = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1",
+ SpecialPowers.Ci.nsILoginInfo,
+ "init");
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+let origin = window.location.origin;
+let suborigin = "http://sub." + window.location.host;
+
+let win = window.open("about:blank");
+SimpleTest.registerCleanupFunction(() => win.close());
+
+async function prepareLoginsAndProcessForm(url, logins = []) {
+ await LoginManager.removeAllUserFacingLogins();
+
+ let dates = Date.now();
+ for (let login of logins) {
+ SpecialPowers.do_QueryInterface(login, SpecialPowers.Ci.nsILoginMetaInfo);
+ // Force all dates to be the same so they don't affect things like deduping.
+ login.timeCreated = login.timePasswordChanged = login.timeLastUsed = dates;
+ await LoginManager.addLogin(login);
+ }
+
+ let processedPromise = promiseFormsProcessed();
+ win.location = url;
+ await processedPromise;
+}
+
+add_task(async function test_formActionOrigin_wildcard_should_autofill() {
+ await prepareLoginsAndProcessForm(origin + MISSING_ACTION_PATH, [
+ new nsLoginInfo(origin, "", null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+
+ await checkLoginFormInFrame(win, "form-basic-username", "name2", "form-basic-password", "pass2");
+});
+
+add_task(async function test_formActionOrigin_different_shouldnt_autofill() {
+ await prepareLoginsAndProcessForm(origin + MISSING_ACTION_PATH, [
+ new nsLoginInfo(origin, "https://example.net", null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+
+
+ await checkLoginFormInFrame(win, "form-basic-username", "", "form-basic-password", "");
+});
+
+add_task(async function test_formActionOrigin_subdomain_should_autofill() {
+ await prepareLoginsAndProcessForm(origin + MISSING_ACTION_PATH, [
+ new nsLoginInfo(origin, suborigin, null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+
+ await checkLoginFormInFrame(win, "form-basic-username", "name2", "form-basic-password", "pass2");
+});
+
+add_task(async function test_origin_subdomain_should_not_autofill() {
+ await prepareLoginsAndProcessForm(origin + MISSING_ACTION_PATH, [
+ new nsLoginInfo(suborigin, origin, null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+
+ await checkLoginFormInFrame(win, "form-basic-username", "", "form-basic-password", "");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_different_subdomain.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_different_subdomain.html
new file mode 100644
index 0000000000..b914968d43
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_different_subdomain.html
@@ -0,0 +1,150 @@
+xcod<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill on an HTTPS page using logins with different eTLD+1</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+const MISSING_ACTION_PATH = TESTS_DIR + "mochitest/form_basic.html";
+
+const chromeScript = runChecksAfterCommonInit(false);
+
+let nsLoginInfo = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1",
+ SpecialPowers.Ci.nsILoginInfo,
+ "init");
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+ <iframe></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+let iframe = SpecialPowers.wrap(document.getElementsByTagName("iframe")[0]);
+let win = window.open("about:blank");
+SimpleTest.registerCleanupFunction(() => win.close());
+
+let origin = window.location.origin;
+let otherOrigin = "https://foobar." + window.location.host;
+let oldOrigin = "https://old." + window.location.host;
+
+async function checkWindowLoginForm(expectedUsername, expectedPassword) {
+ return SpecialPowers.spawn(win, [expectedUsername, expectedPassword], function(un, pw) {
+ let doc = this.content.document;
+ Assert.equal(doc.querySelector("#form-basic-username").value, un, "Check username value");
+ Assert.equal(doc.querySelector("#form-basic-password").value, pw, "Check password value");
+ });
+}
+
+async function prepareLogins(logins = []) {
+ await LoginManager.removeAllUserFacingLogins();
+
+ let dates = Date.now();
+ for (let login of logins) {
+ SpecialPowers.do_QueryInterface(login, SpecialPowers.Ci.nsILoginMetaInfo);
+ // Force all dates to be the same so they don't affect things like deduping.
+ login.timeCreated = login.timePasswordChanged = login.timeLastUsed = dates;
+ await LoginManager.addLogin(login);
+ }
+}
+
+async function formReadyInFrame(url) {
+ let processedPromise = promiseFormsProcessed();
+ iframe.src = url;
+ return processedPromise;
+}
+
+async function formReadyInWindow(url) {
+ let processedPromise = promiseFormsProcessedInSameProcess();
+ win.location = url;
+ return processedPromise;
+}
+
+add_task(async function test_login_with_different_subdomain_shouldnt_autofill_wildcard_formActionOrigin() {
+ await prepareLogins([
+ new nsLoginInfo(otherOrigin, "", null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+ await formReadyInWindow(origin + MISSING_ACTION_PATH);
+
+ await checkWindowLoginForm("", "");
+});
+
+add_task(async function test_login_with_different_subdomain_shouldnt_autofill_same_domain_formActionOrigin() {
+ await prepareLogins([
+ new nsLoginInfo(otherOrigin, origin, null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+ await formReadyInWindow(origin + MISSING_ACTION_PATH);
+
+ await checkWindowLoginForm("", "");
+});
+
+add_task(async function test_matching_logins_with_different_subdomain_and_matching_domain_should_autofill() {
+ await prepareLogins([
+ new nsLoginInfo(origin, origin, null,
+ "name2", "pass2", "uname", "pword"),
+ new nsLoginInfo(oldOrigin, origin, null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+ await formReadyInWindow(origin + MISSING_ACTION_PATH);
+
+ await checkWindowLoginForm("name2", "pass2");
+});
+
+add_task(async function test_login_with_different_subdomain_shouldnt_autofill_different_subdomain_formActionOrigin() {
+ await prepareLogins([
+ new nsLoginInfo(otherOrigin, otherOrigin, null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+ await formReadyInWindow(origin + MISSING_ACTION_PATH);
+
+ await checkWindowLoginForm("", "");
+});
+
+add_task(async function test_login_with_different_subdomain_shouldnt_autofill_different_domain_formActionOrigin() {
+ await prepareLogins([
+ new nsLoginInfo(otherOrigin, "https://example.net", null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+ await formReadyInWindow(origin + MISSING_ACTION_PATH);
+
+ await checkWindowLoginForm("", "");
+});
+
+add_task(async function test_login_with_same_origin_shouldnt_autofill_cross_origin_iframe() {
+ await SimpleTest.promiseFocus(window);
+
+ async function checkIframeLoginForm(expectedUsername, expectedPassword) {
+ return SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [expectedUsername, expectedPassword], function(un, pw) {
+ var u = this.content.document.getElementById("form-basic-username");
+ var p = this.content.document.getElementById("form-basic-password");
+ Assert.equal(u.value, un, "Check username value");
+ Assert.equal(p.value, pw, "Check password value");
+ });
+ }
+
+ // We need an origin that is supported by the test framework to be able to load the
+ // cross-origin form into the iframe.
+ let crossOrigin = "https://test1.example.com";
+ info(`Top level frame origin: ${origin}. Iframe and login origin: ${crossOrigin}.`);
+ await prepareLogins([
+ new nsLoginInfo(crossOrigin, crossOrigin, null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+ await formReadyInFrame(crossOrigin + MISSING_ACTION_PATH);
+
+ await checkIframeLoginForm("", "");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_from_bfcache.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_from_bfcache.html
new file mode 100644
index 0000000000..d0fcb16e18
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_from_bfcache.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofilling documents restored from the back/forward cache (bfcache)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="test_crossOriginBfcacheRestore();">
+<p id="display"></p>
+
+<div id="content">
+ <a id="next" href="https://example.org/tests/toolkit/components/passwordmgr/test/mochitest/file_history_back.html" target="loginWin">Next</a>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/*
+ * The test opens a new window and updates login information. Then
+ * a new page is loaded and it goes immediately back. The initial page
+ * should be coming out from the bfcache and the form control values should be
+ * the ones filled during the initial load.
+ */
+async function test_crossOriginBfcacheRestore() {
+ // If Fission is disabled, the pref is no-op.
+ await SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]});
+
+ var bc = new BroadcastChannel("form_basic_bfcache");
+ window.open("form_basic_bfcache.html", "", "noopener");
+ var pageshowCount = 0;
+ bc.onmessage = function(event) {
+ if (event.data.type == "pageshow") {
+ ++pageshowCount;
+ if (pageshowCount == 1) {
+ is(event.data.persisted, false, "Initial load");
+ bc.postMessage("nextpage");
+ } else if (pageshowCount == 2) {
+ is(event.data.persisted, true, "Should have persisted the page.");
+ bc.postMessage("close");
+ }
+ } else if (event.data.type == "is") {
+ is(event.data.value1, event.data.value2, event.data.message);
+ } else if (event.data.type == "ok") {
+ is(event.data.value, event.data.message);
+ } else if (event.data == "closed") {
+ bc.close();
+ SimpleTest.finish();
+ }
+ }
+
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_hasBeenTypePassword.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_hasBeenTypePassword.html
new file mode 100644
index 0000000000..2139d30e61
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_hasBeenTypePassword.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test no autofill into a password field that is no longer type=password</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+Login Manager test: Test no autofill into a password field that is no longer type=password
+
+<script>
+let DEFAULT_ORIGIN = window.location.origin;
+
+/** Test for Login Manager: Test no autofill into a password field that is no longer type=password **/
+
+add_setup(async () => {
+ await setStoredLoginsAsync(
+ [DEFAULT_ORIGIN, "https://autofill", null, "user1", "pass1"]
+ );
+});
+
+
+// As a control, test that autofill is working on this page.
+add_task(async function test_autofill_control() {
+ let win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await loadFormIntoWindow(DEFAULT_ORIGIN, `
+ <form id="form1" action="https://autofill">
+ <p>This is form 1.</p>
+ <input id="username-1" type="text" name="uname">
+ <input id="password-1" type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ </form>`, win);
+ await checkLoginFormInFrame(win, "username-1", "user1", "password-1", "pass1");
+});
+
+add_task(async function test_no_autofill() {
+ let win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ // Synchronously change the password field type to text before the fill happens.
+ await loadFormIntoWindow(DEFAULT_ORIGIN, `
+ <form id="form1" action="https://autofill">
+ <p>This is form 1.</p>
+ <input id="username-1" type="text" name="uname">
+ <input id="password-1" type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ </form>`, win, 1, () => {
+ this.content.document.getElementById("password-1").type = "text";
+ });
+ await checkLoginFormInFrame(win, "username-1", "", "password-1", "");
+});
+</script>
+
+<p id="display"></p>
+
+<div id="content"></div>
+
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight.html
new file mode 100644
index 0000000000..3ca214841c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form field autofill highlight</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<pre id="test">
+<script>
+add_setup(async () => {
+ await setStoredLoginsAsync(
+ [window.location.origin, "https://autofill", null, "user1", "pass1", "", ""]
+ );
+});
+
+add_task(async function test_field_highlight_on_autofill() {
+ let win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await loadFormIntoWindow(window.location.origin, `
+ <form id="form1" action="https://autofill" onsubmit="return false;">
+ <input type="text" id="uname">
+ <input type="password" id="pword">
+ <button type="submit">Submit</button>
+ </form>`, win);
+
+ await SpecialPowers.spawn(win, [], async function() {
+ let EventUtils = ContentTaskUtils.getEventUtils(this.content);
+ let doc = this.content.document;
+ let username = doc.getElementById("uname");
+ let password = doc.getElementById("pword");
+ ok(username.matches(":autofill"),
+ "Highlight was successfully applied to the username field on page load autofill");
+ ok(password.matches(":autofill"),
+ "Highlight was successfully applied to the password field on page load autofill");
+
+ // Test that initiating a change on the input value will remove the highlight. We check by pressing
+ // the tab key after backspace(by shifting focus to the next element) because the tab key is known to
+ // cause a bug where the highlight is applied once again.
+ username.focus();
+ await EventUtils.synthesizeKey("KEY_Backspace", {}, this.content);
+ await EventUtils.synthesizeKey("KEY_Tab", {}, this.content);
+
+ ok(!username.matches(":autofill"), "Highlight was successfully removed on change in value of username input element");
+
+ await EventUtils.synthesizeKey("KEY_Backspace", {}, this.content);
+ await EventUtils.synthesizeKey("KEY_Tab", {}, this.content);
+ ok(!password.matches(":autofill"), "Highlight was successfully removed on change in value of password input element");
+ });
+});
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight_empty_username.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight_empty_username.html
new file mode 100644
index 0000000000..36fffb480b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight_empty_username.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test that filling an empty username into a form does not highlight the username element</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+</script>
+<body>
+<p id="display"></p>
+<div id="content">
+<pre id="test">
+<script>
+add_setup(async () => {
+ await setStoredLoginsAsync(
+ [window.location.origin, "https://autofill", null, "", "pass1", "", ""]
+ );
+});
+
+add_task(async function test_field_highlight_on_autofill() {
+ let win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await loadFormIntoWindow(window.location.origin, `
+ <form id="form1" action="https://autofill" onsubmit="return false;">
+ <input type="text" id="uname">
+ <input type="password" id="pword">
+ <button type="submit">Submit</button>
+ </form>`, win);
+
+ await SpecialPowers.spawn(win, [], async function() {
+ let EventUtils = ContentTaskUtils.getEventUtils(this.content);
+ let doc = this.content.document;
+ let username = doc.getElementById("uname");
+ let password = doc.getElementById("pword");
+ ok(!username.matches(":autofill"),
+ "Highlight was not applied to the username field on page load autofill");
+ ok(password.matches(":autofill"),
+ "Highlight was successfully applied to the password field on page load autofill");
+
+ // Test that initiating a change on the input value will remove the highlight. We check by pressing
+ // the tab key after backspace(by shifting focus to the next element) because the tab key is known to
+ // cause a bug where the highlight is applied once again.
+ username.focus();
+ await EventUtils.synthesizeKey("U", {}, this.content);
+ await EventUtils.synthesizeKey("KEY_Tab", {}, this.content);
+
+ ok(!username.matches(":autofill"), "Highlight is still not present on username element");
+
+ await EventUtils.synthesizeKey("KEY_Backspace", {}, this.content);
+ await EventUtils.synthesizeKey("KEY_Tab", {}, this.content);
+ ok(!password.matches(":autofill"), "Highlight was successfully removed on change in value of password input element");
+ });
+});
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight_username_only_form.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight_username_only_form.html
new file mode 100644
index 0000000000..67892a9ea4
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight_username_only_form.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test that filling a username into a username-only form does highlight the username element</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<pre id="test">
+<script>
+add_setup(async () => {
+ await setStoredLoginsAsync(
+ [window.location.origin, "https://autofill", null, "user1", "pass1", "", ""]
+ );
+});
+
+add_task(async function test_field_highlight_on_autofill() {
+ let win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await loadFormIntoWindow(window.location.origin, `
+ <form id="form1" action="https://autofill" onsubmit="return false;">
+ <input type="text" id="uname" autocomplete="username">
+ <button type="submit">Submit</button>
+ </form>`, win);
+
+ await SpecialPowers.spawn(win, [], async function() {
+ let EventUtils = ContentTaskUtils.getEventUtils(this.content);
+ let doc = this.content.document;
+ let username = doc.getElementById("uname");
+ ok(username.matches(":autofill"),
+ "Highlight was successfully applied to the username field on page load autofill");
+
+ // Test that initiating a change on the input value will remove the highlight. We check by pressing
+ // the tab key after backspace(by shifting focus to the next element) because the tab key is known to
+ // cause a bug where the highlight is applied once again.
+ username.focus();
+ await EventUtils.synthesizeKey("KEY_Backspace", {}, this.content);
+ await EventUtils.synthesizeKey("KEY_Tab", {}, this.content);
+
+ ok(!username.matches(":autofill"), "Highlight was successfully removed on change in value of username input element");
+ });
+});
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_downgrade.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_downgrade.html
new file mode 100644
index 0000000000..cf7c8ca450
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_downgrade.html
@@ -0,0 +1,118 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test we don't autofill on an HTTP page using HTTPS logins</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+const MISSING_ACTION_PATH = TESTS_DIR + "mochitest/form_basic.html";
+const SAME_ORIGIN_ACTION_PATH = TESTS_DIR + "mochitest/form_same_origin_action.html";
+
+const chromeScript = runChecksAfterCommonInit(false);
+
+let nsLoginInfo = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1",
+ SpecialPowers.Ci.nsILoginInfo,
+ "init");
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+let win = window.open("about:blank");
+SimpleTest.registerCleanupFunction(() => win.close());
+
+async function prepareAndProcessForm(url, login) {
+ let processedPromise = promiseFormsProcessed();
+ win.location = url;
+ info("prepareAndProcessForm, assigned window location: " + url);
+ await processedPromise;
+}
+
+async function checkFormsWithLogin(formUrls, login, expectedUsername, expectedPassword) {
+ await LoginManager.removeAllUserFacingLogins();
+ await LoginManager.addLogin(login);
+
+ for (let url of formUrls) {
+ info("start test_checkNoAutofillOnDowngrade w. url: " + url);
+
+ await prepareAndProcessForm(url);
+ info("form was processed");
+
+ await SpecialPowers.spawn(win, [url, expectedUsername, expectedPassword],
+ function(urlContent, expectedUsernameContent, expectedPasswordContent) {
+ let doc = this.content.document;
+ let uname = doc.getElementById("form-basic-username");
+ let pword = doc.getElementById("form-basic-password");
+ Assert.equal(uname.value, expectedUsernameContent, `username ${expectedUsernameContent ? "filled" : "not filled"} on ${urlContent}`);
+ Assert.equal(pword.value, expectedPasswordContent, `password ${expectedPasswordContent ? "filled" : "not filled"} on ${urlContent}`);
+ });
+ }
+}
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["signon.schemeUpgrades", true],
+ ["dom.security.https_first", false],
+ ]});
+});
+
+add_task(async function test_sanityCheckHTTPS() {
+ let login = new nsLoginInfo("https://example.com", "https://example.com", null,
+ "name1", "pass1", "uname", "pword");
+
+ await checkFormsWithLogin([
+ `https://example.com${MISSING_ACTION_PATH}`,
+ `https://example.com${SAME_ORIGIN_ACTION_PATH}`,
+ ], login, "name1", "pass1");
+});
+
+add_task(async function test_checkNoAutofillOnDowngrade() {
+ let login = new nsLoginInfo("https://example.com", "https://example.com", null,
+ "name1", "pass1", "uname", "pword");
+ await checkFormsWithLogin([
+ `http://example.com${MISSING_ACTION_PATH}`,
+ `http://example.com${SAME_ORIGIN_ACTION_PATH}`,
+ ], login, "", "");
+});
+
+add_task(async function test_checkNoAutofillOnDowngradeSubdomain() {
+ let login = new nsLoginInfo("https://sub.example.com", "https://example.com", null,
+ "name1", "pass1", "uname", "pword");
+ todo(false, "await promiseFormsProcessed timesout when test is run with scheme=https");
+ await checkFormsWithLogin([
+ `http://example.com${MISSING_ACTION_PATH}`,
+ `http://example.com${SAME_ORIGIN_ACTION_PATH}`,
+ ], login, "", "");
+});
+
+
+add_task(async function test_checkNoAutofillOnDowngradeDifferentPort() {
+ let login = new nsLoginInfo("https://example.com:8080", "https://example.com", null,
+ "name1", "pass1", "uname", "pword");
+ await checkFormsWithLogin([
+ `http://example.com${MISSING_ACTION_PATH}`,
+ `http://example.com${SAME_ORIGIN_ACTION_PATH}`,
+ ], login, "", "");
+});
+
+add_task(async function test_checkNoAutofillOnDowngradeSubdomainDifferentPort() {
+ let login = new nsLoginInfo("https://sub.example.com:8080", "https://example.com", null,
+ "name1", "pass1", "uname", "pword");
+ await checkFormsWithLogin([
+ `https://example.com${MISSING_ACTION_PATH}`,
+ `https://example.com${SAME_ORIGIN_ACTION_PATH}`,
+ ], login, "", "");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html
new file mode 100644
index 0000000000..104f4ef144
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html
@@ -0,0 +1,148 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill on an HTTPS page using upgraded HTTP logins</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+const MISSING_ACTION_PATH = TESTS_DIR + "mochitest/form_basic.html";
+const CROSS_ORIGIN_SECURE_PATH = TESTS_DIR + "mochitest/form_cross_origin_secure_action.html";
+
+const chromeScript = runChecksAfterCommonInit(false);
+
+let nsLoginInfo = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1",
+ SpecialPowers.Ci.nsILoginInfo,
+ "init");
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+let win = window.open("about:blank");
+SimpleTest.registerCleanupFunction(() => win.close());
+
+async function prepareLoginsAndProcessForm(url, logins = []) {
+ await LoginManager.removeAllUserFacingLogins();
+
+ let dates = Date.now();
+ for (let login of logins) {
+ SpecialPowers.do_QueryInterface(login, SpecialPowers.Ci.nsILoginMetaInfo);
+ // Force all dates to be the same so they don't affect things like deduping.
+ login.timeCreated = login.timePasswordChanged = login.timeLastUsed = dates;
+ await LoginManager.addLogin(login);
+ }
+
+ let processedPromise = promiseFormsProcessed();
+ win.location = url;
+ await processedPromise;
+}
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["signon.schemeUpgrades", true],
+ ["signon.includeOtherSubdomainsInLookup", true],
+ ]});
+});
+
+add_task(async function test_simpleNoDupesNoAction() {
+ await prepareLoginsAndProcessForm("https://example.com" + MISSING_ACTION_PATH, [
+ new nsLoginInfo("http://example.com", "http://example.com", null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+
+ await checkLoginFormInFrame(win,
+ "form-basic-username", "name2",
+ "form-basic-password", "pass2");
+});
+
+add_task(async function test_simpleNoDupesUpgradeOriginAndAction() {
+ await prepareLoginsAndProcessForm("https://example.com" + CROSS_ORIGIN_SECURE_PATH, [
+ new nsLoginInfo("http://example.com", "http://example.org", null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+
+ await checkLoginFormInFrame(win, "form-basic-username", "name2",
+ "form-basic-password", "pass2");
+});
+
+add_task(async function test_simpleNoDupesUpgradeOriginOnly() {
+ await prepareLoginsAndProcessForm("https://example.com" + CROSS_ORIGIN_SECURE_PATH, [
+ new nsLoginInfo("http://example.com", "https://example.org", null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+
+ await checkLoginFormInFrame(win, "form-basic-username", "name2",
+ "form-basic-password", "pass2");
+});
+
+add_task(async function test_simpleNoDupesUpgradeActionOnly() {
+ await prepareLoginsAndProcessForm("https://example.com" + CROSS_ORIGIN_SECURE_PATH, [
+ new nsLoginInfo("https://example.com", "http://example.org", null,
+ "name2", "pass2", "uname", "pword"),
+ ]);
+
+ await checkLoginFormInFrame(win, "form-basic-username", "name2",
+ "form-basic-password", "pass2");
+});
+
+add_task(async function test_dedupe() {
+ await prepareLoginsAndProcessForm("https://example.com" + MISSING_ACTION_PATH, [
+ new nsLoginInfo("https://example.com", "https://example.com", null,
+ "name1", "passHTTPStoHTTPS", "uname", "pword"),
+ new nsLoginInfo("http://example.com", "http://example.com", null,
+ "name1", "passHTTPtoHTTP", "uname", "pword"),
+ new nsLoginInfo("http://example.com", "https://example.com", null,
+ "name1", "passHTTPtoHTTPS", "uname", "pword"),
+ new nsLoginInfo("https://example.com", "http://example.com", null,
+ "name1", "passHTTPStoHTTP", "uname", "pword"),
+ ]);
+
+ await checkLoginFormInFrame(win, "form-basic-username", "name1",
+ "form-basic-password", "passHTTPStoHTTPS");
+});
+
+add_task(async function test_dedupe_subdomain() {
+ // subdomain match (should be autofilled)
+ let loginToFill = new nsLoginInfo("http://test1.example.com", "http://test1.example.com", null,
+ "name1", "pass1");
+ const loginToFillGUID = "subdomain-match"
+ // Assign a GUID to this login so we can ensure this is the login that gets
+ // filled later.
+ loginToFill.QueryInterface(SpecialPowers.Ci.nsILoginMetaInfo).guid = loginToFillGUID;
+
+ await prepareLoginsAndProcessForm("https://test1.example.com" + MISSING_ACTION_PATH, [
+ // All logins have the same username and password:
+ // https: (scheme match)
+ new nsLoginInfo("https://example.com", "https://example.com", null,
+ "name1", "pass1"),
+ loginToFill,
+ // formActionOrigin match
+ new nsLoginInfo("http://example.com", "https://test1.example.com", null,
+ "name1", "pass1"),
+ ]);
+
+ await checkLoginFormInFrame(win, "form-basic-username", "name1",
+ "form-basic-password", "pass1");
+
+ let filledGUID = await SpecialPowers.spawn(win, [], function getFilledGUID() {
+ let LMC = this.content.windowGlobalChild.getActor("LoginManager");
+ let doc = this.content.document;
+ let form = doc.getElementById("form-basic");
+ let { login: filledLogin } = LMC.stateForDocument(doc).fillsByRootElement.get(form);
+ return filledLogin && filledLogin.guid;
+ });
+ is(filledGUID, loginToFillGUID, "Check the correct login was filled");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html
new file mode 100644
index 0000000000..755a1f3f1d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test password-only forms should prefer a password-only login when present</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: Bug 444968
+<script>
+PWMGR_COMMON_PARENT.sendAsyncMessage("setupParent", { selfFilling: true });
+
+SimpleTest.waitForExplicitFinish();
+
+const chromeScript = runInParent(async function chromeSetup() {
+ const login1A = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+ const login1B = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+ const login2A = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+ const login2B = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+ const login2C = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+
+ login1A.init("http://mochi.test:8888", "http://bug444968-1", null, "testuser1A", "testpass1A", "", "");
+ login1B.init("http://mochi.test:8888", "http://bug444968-1", null, "", "testpass1B", "", "");
+
+ login2A.init("http://mochi.test:8888", "http://bug444968-2", null, "testuser2A", "testpass2A", "", "");
+ login2B.init("http://mochi.test:8888", "http://bug444968-2", null, "", "testpass2B", "", "");
+ login2C.init("http://mochi.test:8888", "http://bug444968-2", null, "testuser2C", "testpass2C", "", "");
+
+ await Services.logins.addLogins([
+ login1A,
+ login1B,
+ login2A,
+ login2B,
+ login2C,
+ ]);
+
+ addMessageListener("removeLogins", function removeLogins() {
+ Services.logins.removeLogin(login1A);
+ Services.logins.removeLogin(login1B);
+ Services.logins.removeLogin(login2A);
+ Services.logins.removeLogin(login2B);
+ Services.logins.removeLogin(login2C);
+ });
+});
+
+SimpleTest.registerCleanupFunction(() => chromeScript.sendAsyncMessage("removeLogins"));
+
+registerRunTests();
+</script>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const DEFAULT_ORIGIN = window.location.origin;
+
+/* Test for Login Manager: 444968 (password-only forms should prefer a
+ * password-only login when present )
+ */
+async function startTest() {
+ const win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await loadFormIntoWindow(DEFAULT_ORIGIN, `
+ <!-- first 3 forms have matching user+pass and pass-only logins -->
+
+ <!-- user+pass form. -->
+ <form id="form1" action="http://bug444968-1">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- password-only form. -->
+ <form id="form2" action="http://bug444968-1">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form, username prefilled -->
+ <form id="form3" action="http://bug444968-1">
+ <input type="text" name="uname" value="testuser1A">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+
+ <!-- next 4 forms have matching user+pass (2x) and pass-only (1x) logins -->
+
+ <!-- user+pass form. -->
+ <form id="form4" action="http://bug444968-2">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- password-only form. -->
+ <form id="form5" action="http://bug444968-2">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form, username prefilled -->
+ <form id="form6" action="http://bug444968-2">
+ <input type="text" name="uname" value="testuser2A">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- user+pass form, username prefilled -->
+ <form id="form7" action="http://bug444968-2">
+ <input type="text" name="uname" value="testuser2C">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>`, win, 7);
+
+ await checkLoginFormInFrameWithElementValues(win, 1, "testuser1A", "testpass1A");
+ await checkLoginFormInFrameWithElementValues(win, 2, "testpass1B");
+ await checkLoginFormInFrameWithElementValues(win, 3, "testuser1A", "testpass1A");
+
+ checkUnmodifiedFormInFrame(win, 4); // 2 logins match
+ await checkLoginFormInFrameWithElementValues(win, 5, "testpass2B");
+ await checkLoginFormInFrameWithElementValues(win, 6, "testuser2A", "testpass2A");
+ await checkLoginFormInFrameWithElementValues(win, 7, "testuser2C", "testpass2C");
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("runTests", startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_sandboxed.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_sandboxed.html
new file mode 100644
index 0000000000..8fd6debd5c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_sandboxed.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test form field autofill in sandboxed documents (null principal)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content">
+ <iframe id="sandboxed"
+ sandbox=""></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const { TestUtils } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+);
+
+/** Test for Login Manager: form field autofill in sandboxed documents (null principal) **/
+
+const sandboxed = document.getElementById("sandboxed");
+let uname;
+let pword;
+
+add_setup(async () => {
+ await setStoredLoginsAsync(["https://example.com", "", null, "tempuser1", "temppass1", "uname", "pword"]);
+});
+
+add_task(async function test_no_autofill_in_form() {
+ sandboxed.src = "form_basic.html";
+ const frameWindow = SpecialPowers.wrap(sandboxed).contentWindow;
+ const DOMFormHasPasswordPromise = new Promise(resolve => {
+ SpecialPowers.addChromeEventListener("DOMFormHasPassword", function onDFHP() {
+ SpecialPowers.removeChromeEventListener("DOMFormHasPassword", onDFHP);
+ resolve();
+ });
+ });
+ // Can't use SimpleTest.promiseFocus as it doesn't work with the sandbox.
+ await SimpleTest.promiseWaitForCondition(() => {
+ return frameWindow.document.readyState == "complete" &&
+ frameWindow.location.href.endsWith("form_basic.html");
+ }, "Check frame is loaded");
+ info("frame loaded");
+ await DOMFormHasPasswordPromise;
+ const frameDoc = SpecialPowers.wrap(sandboxed).contentDocument;
+
+ uname = frameDoc.getElementById("form-basic-username");
+ pword = frameDoc.getElementById("form-basic-password");
+
+ // Autofill shouldn't happen in the sandboxed frame but would have happened by
+ // now since DOMFormHasPassword was observed above.
+ await ensureLoginFormStaysFilledWith(uname, "", pword, "");
+
+ info("blurring the username field after typing the username");
+ uname.focus();
+ uname.setUserInput("tempuser1");
+ synthesizeKey("VK_TAB", {}, frameWindow);
+
+ await TestUtils.waitForCondition(() => {
+ return uname.value === "tempuser1" & pword.value === "";
+ }, "Username and password field should be filled");
+});
+
+add_task(async function test_no_autofill_outside_form() {
+ sandboxed.src = "formless_basic.html";
+ const frameWindow = SpecialPowers.wrap(sandboxed).contentWindow;
+ const DOMInputPasswordAddedPromise = new Promise(resolve => {
+ SpecialPowers.addChromeEventListener("DOMInputPasswordAdded", function onDIPA() {
+ SpecialPowers.removeChromeEventListener("DOMInputPasswordAdded", onDIPA);
+ resolve();
+ });
+ });
+ // Can't use SimpleTest.promiseFocus as it doesn't work with the sandbox.
+ await SimpleTest.promiseWaitForCondition(() => {
+ return frameWindow.document.readyState == "complete" &&
+ frameWindow.location.href.endsWith("formless_basic.html");
+ }, "Check frame is loaded");
+ info("frame loaded");
+ await DOMInputPasswordAddedPromise;
+ const frameDoc = SpecialPowers.wrap(sandboxed).contentDocument;
+
+ uname = frameDoc.getElementById("form-basic-username");
+ pword = frameDoc.getElementById("form-basic-password");
+
+ // Autofill shouldn't happen in the sandboxed frame but would have happened by
+ // now since DOMInputPasswordAdded was observed above.
+ await ensureLoginFormStaysFilledWith(uname, "", pword, "");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_tab_between_fields.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_tab_between_fields.html
new file mode 100644
index 0000000000..53eb959d7b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_tab_between_fields.html
@@ -0,0 +1,154 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autocomplete behavior when tabbing between form fields</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+let nsLoginInfo = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1",
+ SpecialPowers.Ci.nsILoginInfo,
+ "init");
+let readyPromise = registerRunTests();
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+let DEFAULT_ORIGIN = window.location.origin;
+let win;
+let html = `
+ <form id="form1" action="https://autofill" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>`;
+
+async function prepareLogins(logins = []) {
+ await LoginManager.removeAllUserFacingLogins();
+
+ for (let login of logins) {
+ let storageAddPromise = promiseStorageChanged(["addLogin"]);
+ await LoginManager.addLogin(login);
+ await storageAddPromise;
+ }
+ let count = (await LoginManager.getAllLogins()).length;
+ is(count, logins.length, "All logins were added");
+}
+
+const availableLogins = {
+ "exampleUser1": new nsLoginInfo(DEFAULT_ORIGIN, "https://autofill", null,
+ "user1", "pass1", "uname", "pword"),
+}
+
+async function recreateTreeInWindow(formNum) {
+ await SpecialPowers.spawn(win, [formNum], (formNumF) => {
+ // eslint-disable-next-line no-unsanitized/property
+ let form = this.content.document.querySelector(`#form${formNumF}`);
+ // eslint-disable-next-line no-unsanitized/property, no-self-assign
+ form.outerHTML = form.outerHTML;
+ });
+}
+
+const tests = [
+ {
+ name: "autofill_disabled_exact_username",
+ autofillEnabled: false,
+ logins: ["exampleUser1"],
+ expectedAutofillUsername: "",
+ expectedAutofillPassword: "",
+ typeUsername: "user1",
+ expectedTabbedUsername: "user1",
+ expectedTabbedPassword: "",
+ },
+ {
+ name: "autofill_enabled_exact_username",
+ autofillEnabled: true,
+ logins: ["exampleUser1"],
+ expectedAutofillUsername: "user1",
+ expectedAutofillPassword: "pass1",
+ typeUsername: "user1",
+ expectedTabbedUsername: "user1",
+ expectedTabbedPassword: "pass1",
+ },
+];
+
+add_setup(async () => {
+ ok(readyPromise, "check promise is available");
+ await readyPromise;
+ win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await loadFormIntoWindow(DEFAULT_ORIGIN, html, win);
+});
+
+async function testResultOfTabInteractions(testData) {
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["signon.autofillForms", testData.autofillEnabled],
+ ]});
+
+ await SimpleTest.promiseFocus(win);
+ let logins = testData.logins.map(name => availableLogins[name]);
+ await prepareLogins(logins);
+
+ info("recreating form");
+ let processed = promiseFormsProcessed();
+ await recreateTreeInWindow(1);
+ info("waiting for form processed");
+ await processed;
+ // check autofill results
+ await checkLoginFormInFrameWithElementValues(win, 1, testData.expectedAutofillUsername, testData.expectedAutofillPassword);
+
+ await SpecialPowers.spawn(win, [testData.typeUsername], async (typeUsername) => {
+ let doc = this.content.document;
+ let pword = doc.querySelector("[name='pword']");
+ let uname = doc.querySelector("[name='uname']");
+
+ pword.setUserInput("");
+ uname.setUserInput("");
+
+ info("Placing focus in the username field");
+ uname.focus();
+
+ if (typeUsername) {
+ info("Filling username field");
+ EventUtils.sendString(typeUsername, this.content);
+ }
+
+ EventUtils.synthesizeKey("KEY_Tab", {}, this.content); // blur un, focus pw
+ await new Promise(resolve => SpecialPowers.executeSoon(resolve));
+
+ ok(pword.matches("input:focus"), "pword field is focused");
+ });
+
+ await checkLoginFormInFrameWithElementValues(win, 1, testData.expectedTabbedUsername, testData.expectedTabbedPassword);
+
+ await recreateTreeInWindow(1);
+ await promiseFormsProcessed();
+
+ await SpecialPowers.spawn(win, [], () => {
+ EventUtils.synthesizeKey("KEY_Escape", {}, this.content);
+ });
+}
+
+for (let testData of tests) {
+ let tmp = {
+ async [testData.name]() {
+ await testResultOfTabInteractions(testData);
+ },
+ };
+ add_task(tmp[testData.name]);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_username-only.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_username-only.html
new file mode 100644
index 0000000000..860c317409
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_username-only.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill on username-form</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Test autofill on username-form
+
+<script>
+add_setup(async () => {
+ await setStoredLoginsAsync(
+ [window.location.origin, "https://autofill", null, "user1", "pass1"]
+ );
+});
+
+add_task(async function test_autofill_username_only_form() {
+ await loadRecipes({
+ siteRecipes: [{
+ hosts: ["mochi.test:8888"],
+ notUsernameSelector: "input[name='shouldnotfill']",
+ }],
+ });
+
+ let win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+
+ // 5 out of the 7 forms should be autofilled
+ await loadFormIntoWindow(window.location.origin, `
+ <!-- no password field, 1 username field -->
+ <form id='form1' action='https://autofill'> 1
+ <input type='text' name='uname' autocomplete='username' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <!-- no password field, 1 username field, with a value set -->
+ <form id='form2' action='https://autofill'> 2
+ <input type='text' name='uname' autocomplete='username' value='someuser'>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <!-- no password field, 2 username fields, should be ignored -->
+ <form id='form3' action='https://autofill'> 3
+ <input type='text' name='uname1' autocomplete='username' value=''>
+ <input type='text' name='uname2' autocomplete='username' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <!-- no password field, 1 username field, too small for the username login -->
+ <form id='form4' action='https://autofill'> 4
+ <input type='text' name='uname' value='' maxlength="4" autocomplete='username'>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <!-- no password field, 1 username field, too small for the username login -->
+ <form id='form5' action='https://autofill'> 5
+ <input type='text' name='uname' value='' maxlength="0" autocomplete='username'>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <!-- no password field, 1 text input field (not a username-only form), should be ignored -->
+ <form id='form6' action='https://autofill'> 6
+ <input type='text' name='uname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <!-- no password field, 1 username field that matches notUsernameSelector recipe -->
+ <form id='form7' action='https://autofill'> 7
+ <input type='text' name='shouldnotfill' autocomplete='username' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>`, win, 5);
+
+ await checkLoginFormInFrameWithElementValues(win, 1, "user1");
+ await checkLoginFormInFrameWithElementValues(win, 2, "someuser");
+ await checkUnmodifiedFormInFrame(win, 3);
+ await checkUnmodifiedFormInFrame(win, 4);
+ await checkUnmodifiedFormInFrame(win, 5);
+ await checkUnmodifiedFormInFrame(win, 6);
+ await checkUnmodifiedFormInFrame(win, 7);
+
+ await resetRecipes();
+});
+</script>
+
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test"></pre>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_username-only_threshold.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_username-only_threshold.html
new file mode 100644
index 0000000000..d9a2e08095
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_username-only_threshold.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill on username-form when the number of form exceeds the lookup threshold</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Test not autofill on username-form when the number of form exceeds the lookup threshold
+
+<script>
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({"set": [["signon.usernameOnlyForm.lookupThreshold", 5]]});
+
+ await setStoredLoginsAsync(
+ [window.location.origin, "https://autofill", null, "user1", "pass1"]
+ );
+});
+
+add_task(async function test_autofill_username_only_form() {
+ const win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+
+ await loadFormIntoWindow(window.location.origin, `
+ <!-- no password field, 1 username field -->
+ <form id='form1' action='https://autofill'> 1
+ <input type='text' name='uname' autocomplete='username' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <form id='form2' action='https://autofill'> 2
+ <input type='text' name='uname' autocomplete='username' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <form id='form3' action='https://autofill'> 3
+ <input type='text' name='uname' autocomplete='username' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <form id='form4' action='https://autofill'> 4
+ <input type='text' name='uname' autocomplete='username' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <form id='form5' action='https://autofill'> 5
+ <input type='text' name='uname' autocomplete='username' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <form id='form6' action='https://autofill'> 6
+ <input type='text' name='uname' autocomplete='username' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>`, win, 5);
+
+ await checkLoginFormInFrameWithElementValues(win, 1, "user1");
+ await checkLoginFormInFrameWithElementValues(win, 2, "user1");
+ await checkLoginFormInFrameWithElementValues(win, 3, "user1");
+ await checkLoginFormInFrameWithElementValues(win, 4, "user1");
+ await checkLoginFormInFrameWithElementValues(win, 5, "user1");
+ await checkUnmodifiedFormInFrame(win, 6);
+});
+</script>
+
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html b/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html
new file mode 100644
index 0000000000..803197c2a2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html
@@ -0,0 +1,114 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test login autocomplete is activated when focused by js on load</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content">
+ <iframe></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const iframe = document.getElementsByTagName("iframe")[0];
+let iframeDoc, hostname;
+
+add_setup(async () => {
+ const origin = window.location.origin;
+ await setStoredLoginsAsync(
+ [origin, origin, null, "name", "pass"],
+ [origin, origin, null, "name1", "pass1"]
+ );
+
+ const processedPromise = promiseFormsProcessed();
+ iframe.src = "/tests/toolkit/components/passwordmgr/test/mochitest/form_autofocus_js.html";
+ await new Promise(resolve => {
+ iframe.addEventListener("load", function() {
+ resolve();
+ }, {once: true});
+ });
+
+ await processedPromise;
+
+ hostname = await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ return this.content.document.documentURIObject.host;
+ });
+
+ SimpleTest.requestFlakyTimeout("Giving a chance for the unexpected popupshown to occur");
+});
+
+add_task(async function test_initial_focus() {
+ let results = await notifyMenuChanged(3, "name");
+ checkAutoCompleteResults(results, ["name", "name1"], hostname, "Two login results");
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ Assert.equal(this.content.document.getElementById("form-basic-password").value, "pass", "Check first password filled");
+ });
+ let popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is now closed");
+});
+
+// This depends on the filling from the previous test.
+add_task(async function test_not_reopened_if_filled() {
+ listenForUnexpectedPopupShown();
+ await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ this.content.document.getElementById("form-basic-username").focus();
+ });
+ info("Waiting to see if a popupshown occurs");
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ // cleanup
+ gPopupShownExpected = true;
+ await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ this.content.document.getElementById("form-basic-submit").focus();
+ });
+});
+
+add_task(async function test_reopened_after_edit_not_matching_saved() {
+ await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ this.content.document.getElementById("form-basic-username").value = "nam";
+ });
+ await popupBy(async () => {
+ await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ this.content.document.getElementById("form-basic-username").focus();
+ });
+ });
+ await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ this.content.document.getElementById("form-basic-submit").focus();
+ });
+});
+
+add_task(async function test_not_reopened_after_selecting() {
+ await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ this.content.document.getElementById("form-basic-username").value = "";
+ this.content.document.getElementById("form-basic-password").value = "";
+ });
+ listenForUnexpectedPopupShown();
+
+ await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ let formFillController = SpecialPowers.getFormFillController();
+ let usernameField = this.content.document.getElementById("form-basic-username");
+ formFillController.markAsLoginManagerField(usernameField);
+ });
+
+ info("Waiting to see if a popupshown occurs");
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ // Cleanup
+ gPopupShownExpected = true;
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form.html
new file mode 100644
index 0000000000..d13fd1369f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic autofill</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: simple form fill
+
+<script>
+gTestDependsOnDeprecatedLogin = true;
+runChecksAfterCommonInit(startTest);
+
+let DEFAULT_ORIGIN = window.location.origin;
+
+/** Test for Login Manager: form fill, multiple forms. **/
+
+async function startTest() {
+ let win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await loadFormIntoWindow(DEFAULT_ORIGIN, `
+ <form id="form1" action="formtest.js">
+ <p>This is form 1.</p>
+ <input id="username-1" type="text" name="uname">
+ <input id="password-1" type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>`, win);
+ await checkLoginFormInFrame(win, "username-1", "testuser", "password-1", "testpass");
+
+ SimpleTest.finish();
+}
+</script>
+
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html
new file mode 100644
index 0000000000..64450300d6
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test forms with no password fields</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms with no password fields
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+ <!-- Form with no user field or password field -->
+ <form id="form1" action="formtest.js">
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Form with no user field or password field, but one other field -->
+ <form id="form2" action="formtest.js">
+ <input type="checkbox">
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Form with no user field or password field, but one other field -->
+ <form id="form3" action="formtest.js">
+ <input type="checkbox" name="uname" value="">
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Form with a text field, but no password field -->
+ <form id="form4" action="formtest.js">
+ <input type="text" name="yyyyy">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Form with a user field, but no password field -->
+ <form id="form5" action="formtest.js">
+ <input type="text" name="uname">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function startTest() {
+ is(getFormElementByName(3, "uname").value, "", "Checking for unfilled checkbox (form 3)");
+ is(getFormElementByName(4, "yyyyy").value, "", "Checking for unfilled text field (form 4)");
+ is(getFormElementByName(5, "uname").value, "", "Checking for unfilled text field (form 5)");
+
+ SimpleTest.finish();
+}
+
+runChecksAfterCommonInit(startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html
new file mode 100644
index 0000000000..bb91d9cf3e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html
@@ -0,0 +1,171 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill for forms with 1 password field</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms with 1 password field
+<script>
+gTestDependsOnDeprecatedLogin = true;
+runChecksAfterCommonInit(() => startTest());
+
+let DEFAULT_ORIGIN = window.location.origin;
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: simple form fill **/
+
+async function startTest() {
+ let win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await loadFormIntoWindow(DEFAULT_ORIGIN, `
+ <!-- no username fields -->
+
+ <form id='form1' action='formtest.js'> 1
+ <!-- Blank, so fill in the password -->
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form2' action='formtest.js'> 2
+ <!-- Already contains the password, so nothing to do. -->
+ <input type='password' name='pname' value='testpass'>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form3' action='formtest.js'> 3
+ <!-- Contains unknown password, so don't change it -->
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+ </form>
+
+
+ <!-- username fields -->
+
+ <form id='form4' action='formtest.js'> 4
+ <!-- Blanks, so fill in login -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form5' action='formtest.js'> 5
+ <!-- Username already set, so fill in password -->
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form6' action='formtest.js'> 6
+ <!-- Unknown username, so don't fill in password -->
+ <input type='text' name='uname' value='xxxxxxxx'>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form7' action='formtest.js'> 7
+ <!-- Password already set, could fill in username but that's weird so we don't -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='testpass'>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form8' action='formtest.js'> 8
+ <!-- Unknown password, so don't fill in a username -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+ </form>
+
+
+
+ <!-- extra text fields -->
+
+ <form id='form9' action='formtest.js'> 9
+ <!-- text field _after_ password should never be treated as a username field -->
+ <input type='password' name='pname' value=''>
+ <input type='text' name='uname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form10' action='formtest.js'> 10
+ <!-- only the first text field before the password should be for username -->
+ <input type='text' name='other' value=''>
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form11' action='formtest.js'> 11
+ <!-- variation just to make sure extra text field is still ignored -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <input type='text' name='other' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+
+
+ <!-- same as last bunch, but with xxxx in the extra field. -->
+
+ <form id='form12' action='formtest.js'> 12
+ <!-- text field _after_ password should never be treated as a username field -->
+ <input type='password' name='pname' value=''>
+ <input type='text' name='uname' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form13' action='formtest.js'> 13
+ <!-- only the first text field before the password should be for username -->
+ <input type='text' name='other' value='xxxxxxxx'>
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form14' action='formtest.js'> 14
+ <!-- variation just to make sure extra text field is still ignored -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <input type='text' name='other' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+ </form>`, win, 14);
+
+ var f = 1;
+
+ // 1-3
+ await checkLoginFormInFrameWithElementValues(win, f++, "testpass");
+ await checkLoginFormInFrameWithElementValues(win, f++, "testpass");
+ await checkLoginFormInFrameWithElementValues(win, f++, "xxxxxxxx");
+
+ // 4-8
+ await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass");
+ await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass");
+ await checkLoginFormInFrameWithElementValues(win, f++, "xxxxxxxx", "");
+ await checkLoginFormInFrameWithElementValues(win, f++, "", "testpass");
+ await checkLoginFormInFrameWithElementValues(win, f++, "", "xxxxxxxx");
+
+ // 9-14
+ await checkLoginFormInFrameWithElementValues(win, f++, "testpass", "");
+ await checkLoginFormInFrameWithElementValues(win, f++, "", "testuser", "testpass");
+ await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass", "");
+ await checkLoginFormInFrameWithElementValues(win, f++, "testpass", "xxxxxxxx");
+ await checkLoginFormInFrameWithElementValues(win, f++, "xxxxxxxx", "testuser", "testpass");
+ await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass", "xxxxxxxx");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html
new file mode 100644
index 0000000000..d7aaadc895
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test forms with 1 password field, part 2</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms with 1 password field, part 2
+<script>
+gTestDependsOnDeprecatedLogin = true;
+runChecksAfterCommonInit(() => startTest());
+
+let DEFAULT_ORIGIN = window.location.origin;
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: simple form fill, part 2 **/
+
+async function startTest() {
+ let win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await loadFormIntoWindow(DEFAULT_ORIGIN, `
+ <form id='form1' action='formtest.js'> 1
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form2' action='formtest.js'> 2
+ <input type='password' name='pname' value='' disabled>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form3' action='formtest.js'> 3
+ <input type='password' name='pname' value='' readonly>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form4' action='formtest.js'> 4
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form5' action='formtest.js'> 5
+ <input type='text' name='uname' value='' disabled>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form6' action='formtest.js'> 6
+ <input type='text' name='uname' value='' readonly>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form7' action='formtest.js'> 7
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='' disabled>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form8' action='formtest.js'> 8
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='' readonly>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form9' action='formtest.js'> 9
+ <input type='text' name='uname' value='TESTUSER'>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form10' action='formtest.js'> 10
+ <input type='text' name='uname' value='TESTUSER' readonly>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form11' action='formtest.js'> 11
+ <input type='text' name='uname' value='TESTUSER' disabled>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+ </form>`, win, 11);
+
+ var f;
+
+ // Test various combinations of disabled/readonly inputs
+ await checkLoginFormInFrameWithElementValues(win, 1, "testpass"); // control
+ await checkUnmodifiedFormInFrame(win, 2);
+ await checkUnmodifiedFormInFrame(win, 3);
+ await checkLoginFormInFrameWithElementValues(win, 4, "testuser", "testpass"); // control
+ for (f = 5; f <= 8; f++) {
+ await checkUnmodifiedFormInFrame(win, f);
+ }
+ // Test case-insensitive comparison of username field
+ await checkLoginFormInFrameWithElementValues(win, 9, "testuser", "testpass");
+ await checkLoginFormInFrameWithElementValues(win, 10, "TESTUSER", "testpass");
+ await checkLoginFormInFrameWithElementValues(win, 11, "TESTUSER", "testpass");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html
new file mode 100644
index 0000000000..1d9224a819
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html
@@ -0,0 +1,190 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill for forms with 2 password fields</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms with 2 password fields
+<script>
+gTestDependsOnDeprecatedLogin = true;
+runChecksAfterCommonInit(() => startTest());
+
+let DEFAULT_ORIGIN = window.location.origin;
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: simple form fill **/
+
+async function startTest() {
+ let win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await loadFormIntoWindow(DEFAULT_ORIGIN, `
+ <!-- no username fields -->
+
+ <form id='form1' action='formtest.js'> 1
+ <!-- simple form, fill in first pw -->
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form2' action='formtest.js'> 2
+ <!-- same but reverse pname and qname, field names are ignored. -->
+ <input type='password' name='qname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form3' action='formtest.js'> 3
+ <!-- text field after password fields should be ignored, no username. -->
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value=''>
+ <input type='text' name='uname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form4' action='formtest.js'> 4
+ <!-- nothing to do, password already present -->
+ <input type='password' name='pname' value='testpass'>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form5' action='formtest.js'> 5
+ <!-- don't clobber an existing unrecognized password -->
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form6' action='formtest.js'> 6
+ <!-- fill in first field, 2nd field shouldn't be touched anyway. -->
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+ </form>
+
+
+
+ <!-- with username fields -->
+
+
+
+ <form id='form7' action='formtest.js'> 7
+ <!-- simple form, should fill in username and first pw -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form8' action='formtest.js'> 8
+ <!-- reverse pname and qname, field names are ignored. -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='qname' value=''>
+ <input type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form9' action='formtest.js'> 9
+ <!-- username already filled, so just fill first password -->
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form10' action='formtest.js'> 10
+ <!-- unknown username, don't fill in a password -->
+ <input type='text' name='uname' value='xxxxxxxx'>
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form11' action='formtest.js'> 11
+ <!-- don't clobber unknown password -->
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form12' action='formtest.js'> 12
+ <!-- fill in 1st pass, don't clobber 2nd pass -->
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value=''>
+ <input type='password' name='qname' value='xxxxxxxx'>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form13' action='formtest.js'> 13
+ <!-- nothing to do, user and pass prefilled. life is easy. -->
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value='testpass'>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form14' action='formtest.js'> 14
+ <!-- shouldn't fill in username because 1st pw field is unknown. -->
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <input type='password' name='qname' value='testpass'>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form15' action='formtest.js'> 15
+ <!-- textfield in the middle of pw fields should be ignored -->
+ <input type='password' name='pname' value=''>
+ <input type='text' name='uname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+ </form>
+
+ <form id='form16' action='formtest.js'> 16
+ <!-- same, and don't clobber existing unknown password -->
+ <input type='password' name='pname' value='xxxxxxxx'>
+ <input type='text' name='uname' value=''>
+ <input type='password' name='qname' value=''>
+ <button type='submit'>Submit</button>
+ </form>`, win, 16);
+
+ var f = 1;
+
+ // 1-6 no username
+ await checkLoginFormInFrameWithElementValues(win, f++, "testpass", "");
+ await checkLoginFormInFrameWithElementValues(win, f++, "testpass", "");
+ await checkLoginFormInFrameWithElementValues(win, f++, "testpass", "", "");
+ await checkLoginFormInFrameWithElementValues(win, f++, "testpass", "");
+ await checkLoginFormInFrameWithElementValues(win, f++, "xxxxxxxx", "");
+ await checkLoginFormInFrameWithElementValues(win, f++, "testpass", "xxxxxxxx");
+
+ // 7-15 with username
+ await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass", "");
+ await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass", "");
+ await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass", "");
+ await checkLoginFormInFrameWithElementValues(win, f++, "xxxxxxxx", "", "");
+ await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "xxxxxxxx", "");
+ await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass", "xxxxxxxx");
+ await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass", "");
+ await checkLoginFormInFrameWithElementValues(win, f++, "", "xxxxxxxx", "testpass");
+ await checkLoginFormInFrameWithElementValues(win, f++, "testpass", "", "");
+ await checkLoginFormInFrameWithElementValues(win, f++, "xxxxxxxx", "", "");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html
new file mode 100644
index 0000000000..a2c60e5964
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html
@@ -0,0 +1,111 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for form fill with 2 password fields</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: form fill, 2 password fields
+<p id="display"></p>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: form fill, 2 password fields **/
+
+/*
+ * If a form has two password fields, other things may be going on....
+ *
+ * 1 - The user might be creating a new login (2nd field for typo checking)
+ * 2 - The user is changing a password (old and new password each have field)
+ *
+ * This test is for case #1.
+ */
+
+var numSubmittedForms = 0;
+var numStartingLogins = 0;
+
+function startTest() {
+ // Check for unfilled forms
+ is(getFormElementByName(1, "uname").value, "", "Checking username 1");
+ is(getFormElementByName(1, "pword").value, "", "Checking password 1A");
+ is(getFormElementByName(1, "qword").value, "", "Checking password 1B");
+
+ // Fill in the username and password fields, for account creation.
+ // Form 1
+ SpecialPowers.wrap(getFormElementByName(1, "uname")).setUserInput("newuser1");
+ SpecialPowers.wrap(getFormElementByName(1, "pword")).setUserInput("newpass1");
+ SpecialPowers.wrap(getFormElementByName(1, "qword")).setUserInput("newpass1");
+
+ // eslint-disable-next-line no-unused-vars
+ var button = getFormSubmitButton(1);
+
+ todo(false, "form submission disabled, can't auto-accept dialog yet");
+ SimpleTest.finish();
+}
+
+
+// Called by each form's onsubmit handler.
+function checkSubmit(formNum) {
+ numSubmittedForms++;
+
+ // End the test at the last form.
+ if (formNum == 999) {
+ is(numSubmittedForms, 999, "Ensuring all forms submitted for testing.");
+
+ (async () => {
+ var numEndingLogins = await LoginManager.countLogins("", "", "");
+
+ ok(numEndingLogins > 0, "counting logins at end");
+ is(numStartingLogins, numEndingLogins + 222, "counting logins at end");
+
+ SimpleTest.finish();
+ })();
+ return false; // return false to cancel current form submission
+ }
+
+ // submit the next form.
+ var button = getFormSubmitButton(formNum + 1);
+ button.click();
+
+ return false; // return false to cancel current form submission
+}
+
+
+function getFormSubmitButton(formNum) {
+ var form = $("form" + formNum); // by id, not name
+ ok(form != null, "getting form " + formNum);
+
+ // we can't just call form.submit(), because that doesn't seem to
+ // invoke the form onsubmit handler.
+ var button = form.firstChild;
+ while (button && button.type != "submit") {
+ button = button.nextSibling;
+ }
+ ok(button != null, "getting form submit button");
+
+ return button;
+}
+
+runChecksAfterCommonInit(startTest);
+
+</script>
+</pre>
+<div id="content" style="display: none">
+ <form id="form1" onsubmit="return checkSubmit(1)" action="http://newuser.com">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <input type="password" name="qword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+</div>
+
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html
new file mode 100644
index 0000000000..59aec20612
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html
@@ -0,0 +1,259 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofill for forms with 3 password fields</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms with 3 password fields (form filling)
+<script>
+gTestDependsOnDeprecatedLogin = true;
+runChecksAfterCommonInit(() => startTest());
+
+let DEFAULT_ORIGIN = window.location.origin;
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: form fill, 3 password fields **/
+
+// Test to make sure 3-password forms are filled properly.
+
+async function startTest() {
+ let win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await loadFormIntoWindow(DEFAULT_ORIGIN, `
+ <p>The next three forms are <b>user/pass/passB/passC</b>, as all-empty, preuser(only), and preuser/pass</p>
+ <form id="form1" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form2" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="pword">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form3" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="pword" value="testpass">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+
+ <p>The next three forms are <b>user/passB/pass/passC</b>, as all-empty, preuser(only), and preuser/pass</p>
+ <form id="form4" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="qword">
+ <input type="password" name="pword">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form5" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="qword">
+ <input type="password" name="pword">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form6" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="qword">
+ <input type="password" name="pword" value="testpass">
+ <input type="password" name="rword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <p>The next three forms are <b>user/passB/passC/pass</b>, as all-empty, preuser(only), and preuser/pass</p>
+ <form id="form7" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form8" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <form id="form9" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="qword">
+ <input type="password" name="rword">
+ <input type="password" name="pword" value="testpass">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>`, win, 9);
+
+ let TESTCASES = [
+ // Check form 1
+ {
+ formNum: 1,
+ isAssertionTuples: [
+ ["uname", "testuser", "Checking username 1"],
+ ["pword", "testpass", "Checking password 1"],
+ ["qword", "", "Checking password 1 (q)"],
+ ["rword", "", "Checking password 1 (r)"],
+ ],
+ },
+ // Check form 2
+ {
+ formNum: 2,
+ isAssertionTuples: [
+ ["uname", "testuser", "Checking username 2"],
+ ["pword", "testpass", "Checking password 2"],
+ ["qword", "", "Checking password 2 (q)"],
+ ["rword", "", "Checking password 2 (r)"],
+ ],
+ },
+ // Check form 3
+ {
+ formNum: 3,
+ isAssertionTuples: [
+ ["uname", "testuser", "Checking username 3"],
+ ["pword", "testpass", "Checking password 3"],
+ ["qword", "", "Checking password 3 (q)"],
+ ["rword", "", "Checking password 3 (r)"],
+ ],
+ },
+ // Check form 4
+ {
+ formNum: 4,
+ isAssertionTuples: [
+ ["uname", "testuser", "Checking username 4"],
+ ["rword", "", "Checking password 4 (r)"],
+ ],
+ todoIsAssertionTuples: [
+ ["qword", "", "Checking password 4 (q)"],
+ ["pword", "testpass", "Checking password 4"],
+ ],
+ },
+ // Check form 5
+ {
+ formNum: 5,
+ isAssertionTuples: [
+ ["uname", "testuser", "Checking username 5"],
+ ["rword", "", "Checking password 5 (r)"],
+ ],
+ todoIsAssertionTuples: [
+ ["qword", "", "Checking password 5 (q)"],
+ ["pword", "testpass", "Checking password 5"],
+ ],
+ },
+ // Check form 6
+ {
+ formNum: 6,
+ isAssertionTuples: [
+ ["uname", "testuser", "Checking username 6"],
+ ["pword", "testpass", "Checking password 6"],
+ ["rword", "", "Checking password 6 (r)"],
+ ],
+ todoIsAssertionTuples: [
+ ["qword", "", "Checking password 6 (q)"],
+ ],
+ },
+ // Check form 7
+ {
+ formNum: 7,
+ isAssertionTuples: [
+ ["uname", "testuser", "Checking username 7"],
+ ["rword", "", "Checking password 7 (r)"],
+ ],
+ todoIsAssertionTuples: [
+ ["qword", "", "Checking password 7 (q)"],
+ ["pword", "testpass", "Checking password 7"],
+ ],
+ },
+ // Check form 8
+ {
+ formNum: 8,
+ isAssertionTuples: [
+ ["uname", "testuser", "Checking username 8"],
+ ["rword", "", "Checking password 8 (r)"],
+ ],
+ todoIsAssertionTuples: [
+ ["qword", "", "Checking password 8 (q)"],
+ ["pword", "testpass", "Checking password 8"],
+ ],
+ },
+ // Check form 9
+ {
+ formNum: 9,
+ isAssertionTuples: [
+ ["uname", "testuser", "Checking username 9"],
+ ["rword", "", "Checking password 9 (r)"],
+ ["pword", "testpass", "Checking password 9"],
+ ],
+ todoIsAssertionTuples: [
+ ["qword", "", "Checking password 9 (q)"],
+ ],
+ },
+ ];
+
+ await SpecialPowers.spawn(win, [TESTCASES], (testcasesF) => {
+ let doc = this.content.document;
+ for (let testcase of testcasesF) {
+ let { formNum } = testcase;
+ for (let tuple of testcase.isAssertionTuples) {
+ let [name, value, message] = tuple;
+ is(doc.querySelector(`#form${formNum} input[name=${name}]`).value, value, message);
+ }
+ if (!testcase.todoIsAssertionTuples) {
+ continue;
+ }
+ // TODO: Bug 1669614
+ // for (let tuple of testcase.todoIsAssertionTuples) {
+ // let [name, value, message] = tuple;
+ // todo_is(doc.querySelector(`#form${formNum} input[name=${name}]`).value, value, message);
+ // }
+ }
+ });
+
+ // TODO: as with the 2-password cases, add tests to check for creating new
+ // logins and changing passwords.
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_honor_autocomplete_off.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_honor_autocomplete_off.html
new file mode 100644
index 0000000000..746c6cc923
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_honor_autocomplete_off.html
@@ -0,0 +1,153 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test login autofill autocomplete when signon.autofillForms.autocompleteOff is false</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: autofilling when autocomplete=off
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content"></div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/** Test for Login Manager: multiple login autocomplete. **/
+
+let { ContentTaskUtils } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/ContentTaskUtils.jsm"
+);
+
+// Check for expected username/password in form.
+function checkFormValues(form, expectedUsername, expectedPassword) {
+ let uname = form.querySelector("[name='uname']");
+ let pword = form.querySelector("[name='pword']");
+ is(uname.value, expectedUsername, `Checking ${form.id} username is: ${expectedUsername}`);
+ is(pword.value, expectedPassword, `Checking ${form.id} password is: ${expectedPassword}`);
+}
+
+async function autoCompleteFieldsFromFirstMatch(form) {
+ // trigger autocomplete from the username field
+ await SimpleTest.promiseFocus(form.ownerGlobal);
+ let uname = form.querySelector("[name='uname']");
+ await popupBy(() => uname.focus());
+
+ let formFilled = promiseFormsProcessedInSameProcess();
+ await synthesizeKey("KEY_ArrowDown"); // open
+ await synthesizeKey("KEY_Enter");
+ await formFilled;
+ await Promise.resolve();
+}
+
+add_setup(async () => {
+ // Set the pref before the document loads.
+ SpecialPowers.setBoolPref("signon.autofillForms.autocompleteOff", false);
+ SimpleTest.registerCleanupFunction(() => {
+ SpecialPowers.clearUserPref("signon.autofillForms.autocompleteOff");
+ });
+
+ await setStoredLoginsAsync(
+ [window.location.origin, "https://autocomplete", null, "singleuser", "singlepass", "uname", "pword"]
+ );
+ listenForUnexpectedPopupShown();
+});
+
+/* Tests for autofill of single-user forms for when we honor autocomplete=off on password fields */
+add_task(async function honor_password_autocomplete_off() {
+ const form = createLoginForm({
+ action: "https://autocomplete",
+ password: {
+ autocomplete: "off"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+ await SimpleTest.promiseFocus(window);
+ // With the pref toggled off, and with autocomplete=off on the password field,
+ // we expect not to have autofilled this form
+ checkFormValues(form, "", "");
+ // ..but it should autocomplete just fine
+ await autoCompleteFieldsFromFirstMatch(form);
+ checkFormValues(form, "singleuser", "singlepass");
+});
+
+add_task(async function honor_username_autocomplete_off() {
+ const form = createLoginForm({
+ action: "https://autocomplete",
+ username: {
+ autocomplete: "off"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+ await SimpleTest.promiseFocus(window);
+ // With the pref toggled off, and with autocomplete=off on the username field,
+ // we expect to have autofilled this form
+ checkFormValues(form, "singleuser", "singlepass");
+});
+
+add_task(async function honor_form_autocomplete_off() {
+ const form = createLoginForm({
+ action: "https://autocomplete",
+ autocomplete: "off"
+ });
+ await promiseFormsProcessedInSameProcess();
+ await SimpleTest.promiseFocus(window);
+ // With the pref toggled off, and with autocomplete=off on the form,
+ // we expect to have autofilled this form
+ checkFormValues(form, "singleuser", "singlepass");
+});
+
+add_task(async function honor_username_and_password_autocomplete_off() {
+ const form = createLoginForm({
+ action: "https://autocomplete",
+ username: {
+ autocomplete: "off"
+ },
+ password: {
+ autocomplete: "off"
+ }
+ });
+ await promiseFormsProcessedInSameProcess();
+ await SimpleTest.promiseFocus(window);
+ // With the pref toggled off, and autocomplete=off on the username and password field,
+ // we expect not to have autofilled this form
+ checkFormValues(form, "", "");
+ // ..but it should autocomplete just fine
+ await autoCompleteFieldsFromFirstMatch(form);
+ checkFormValues(form, "singleuser", "singlepass");
+});
+
+add_task(async function reference_form() {
+ const form = createLoginForm({
+ action: "https://autocomplete"
+ });
+ await promiseFormsProcessedInSameProcess();
+ await SimpleTest.promiseFocus(window);
+ // (this is a control, w/o autocomplete=off, to ensure the login
+ // that was being suppressed would have been filled in otherwise)
+ checkFormValues(form, "singleuser", "singlepass");
+});
+
+add_task(async function honor_username_autocomplete_off_without_password() {
+ const form = createLoginForm({
+ action: "https://autocomplete",
+ username: {
+ id: "username",
+ autocomplete: "off"
+ },
+ password: false
+ });
+ await promiseFormsProcessedInSameProcess();
+ await SimpleTest.promiseFocus(window);
+ // With the pref toggled off, and with autocomplete=off on the username field
+ // in a username-only form, we expect to have autofilled this form
+ is(form.uname.value, "singleuser", `Checking form6 username is: singleuser`);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html
new file mode 100644
index 0000000000..12aeb0bab3
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html
@@ -0,0 +1,165 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for html5 input types (email, tel, url, etc.)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: html5 input types (email, tel, url, etc.)
+<script>
+runChecksAfterCommonInit(() => startTest());
+
+const DEFAULT_ORIGIN = window.location.origin;
+
+runInParent(async function setup() {
+ const login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+ const login2 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+ const login3 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+ const login4 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+ const login5 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+
+ login1.init("http://mochi.test:8888", "http://bug600551-1", null,
+ "testuser@example.com", "testpass1", "", "");
+ login2.init("http://mochi.test:8888", "http://bug600551-2", null,
+ "555-555-5555", "testpass2", "", "");
+ login3.init("http://mochi.test:8888", "http://bug600551-3", null,
+ "http://mozilla.org", "testpass3", "", "");
+ login4.init("http://mochi.test:8888", "http://bug600551-4", null,
+ "123456789", "testpass4", "", "");
+ login5.init("http://mochi.test:8888", "http://bug600551-5", null,
+ "test", "test", "", "");
+
+ await Services.logins.addLogins([
+ login1,
+ login2,
+ login3,
+ login4,
+ login5,
+ ]);
+});
+</script>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/* Test for Login Manager: 600551
+ (Password manager not working with input type=email)
+ */
+async function startTest() {
+ const win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await loadFormIntoWindow(DEFAULT_ORIGIN, `
+ <form id="form1" action="http://bug600551-1">
+ <input type="email" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form2" action="http://bug600551-2">
+ <input type="tel" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form3" action="http://bug600551-3">
+ <input type="url" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form4" action="http://bug600551-4">
+ <input type="number" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form5" action="http://bug600551-5">
+ <input type="search" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- The following forms should not be filled with usernames -->
+ <form id="form6" action="formtest.js">
+ <input type="datetime" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form7" action="formtest.js">
+ <input type="date" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form8" action="formtest.js">
+ <input type="month" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form9" action="formtest.js">
+ <input type="week" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form10" action="formtest.js">
+ <input type="time" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form11" action="formtest.js">
+ <input type="datetime-local" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form12" action="formtest.js">
+ <input type="range" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form13" action="formtest.js">
+ <input type="color" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>`, win, 13);
+
+ await checkLoginFormInFrameWithElementValues(win, 1, "testuser@example.com", "testpass1");
+ await checkLoginFormInFrameWithElementValues(win, 2, "555-555-5555", "testpass2");
+ await checkLoginFormInFrameWithElementValues(win, 3, "http://mozilla.org", "testpass3");
+ await checkLoginFormInFrameWithElementValues(win, 4, "123456789", "testpass4");
+ await checkLoginFormInFrameWithElementValues(win, 5, "test", "test");
+
+ info("type=datetime should not be considered a username");
+ await checkLoginFormInFrameWithElementValues(win, 6, "");
+ info("type=date should not be considered a username");
+ await checkLoginFormInFrameWithElementValues(win, 7, "");
+ info("type=month should not be considered a username");
+ await checkLoginFormInFrameWithElementValues(win, 8, "");
+ info("type=week should not be considered a username");
+ await checkLoginFormInFrameWithElementValues(win, 9, "");
+ info("type=time should not be considered a username");
+ await checkLoginFormInFrameWithElementValues(win, 10, "");
+ info("type=datetime-local should not be considered a username");
+ await checkLoginFormInFrameWithElementValues(win, 11, "");
+ info("type=range should not be considered a username");
+ await checkLoginFormInFrameWithElementValues(win, 12, "50");
+ info("type=color should not be considered a username");
+ await checkLoginFormInFrameWithElementValues(win, 13, "#000000");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html
new file mode 100644
index 0000000000..4a202a7c05
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=355063
+-->
+<head>
+ <meta charset="utf-8"/>
+ <title>Test for Bug 355063</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="application/javascript">
+ /** Test for Bug 355063 **/
+ gTestDependsOnDeprecatedLogin = true;
+ runChecksAfterCommonInit(async function startTest() {
+ info("startTest");
+ // Password Manager's own listener should always have been added first, so
+ // the test's listener should be called after the pwmgr's listener fills in
+ // a login.
+ //
+ SpecialPowers.addChromeEventListener("DOMFormHasPassword", function eventFired() {
+ SpecialPowers.removeChromeEventListener("DOMFormHasPassword", eventFired);
+ var passField = $("p1");
+ passField.addEventListener("input", checkForm);
+ });
+ await setFormAndWaitForFieldFilled("<form id=form1>form1: <input id=u1><input type=password id=p1></form><br>",
+ {fieldSelector: "#u1", fieldValue: "testuser"});
+ });
+
+ function checkForm() {
+ info("checkForm");
+ var userField = document.getElementById("u1");
+ var passField = document.getElementById("p1");
+ is(userField.value, "testuser", "checking filled username");
+ is(passField.value, "testpass", "checking filled password");
+
+ SimpleTest.finish();
+ }
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=355063">Mozilla Bug 355063</a>
+<p id="display"></p>
+<div id="content">
+forms go here!
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html
new file mode 100644
index 0000000000..05e07983f1
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html
@@ -0,0 +1,212 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test forms and logins without a username</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: forms and logins without a username.
+<script>
+gTestDependsOnDeprecatedLogin = true;
+runChecksAfterCommonInit(async () => {
+ // The first login uses a unique formActionOrigin, to check forms where no other logins
+ // will apply. The second login uses the normal formActionOrigin, so that we can test
+ // forms with a mix of username and non-username logins that might apply.
+ //
+ // Note: the second login is deleted at the end of the test.
+ await addLoginsInParent(
+ ["http://mochi.test:8888", "http://mochi.test:1111", null, "", "1234"],
+ ["http://mochi.test:8888", "http://mochi.test:8888", null, "", "1234"]
+ );
+ await startTest();
+});
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const DEFAULT_ORIGIN = window.location.origin;
+
+/** Test for Login Manager: password-only logins **/
+async function startTest() {
+ const win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await loadFormIntoWindow(DEFAULT_ORIGIN, `
+ <!-- simple form: no username field, 1 password field -->
+ <form id='form1' action='http://mochi.test:1111/formtest.js'> 1
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <!-- simple form: no username field, 2 password fields -->
+ <form id='form2' action='http://mochi.test:1111/formtest.js'> 2
+ <input type='password' name='pname1' value=''>
+ <input type='password' name='pname2' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <!-- simple form: no username field, 3 password fields -->
+ <form id='form3' action='http://mochi.test:1111/formtest.js'> 3
+ <input type='password' name='pname1' value=''>
+ <input type='password' name='pname2' value=''>
+ <input type='password' name='pname3' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <!-- simple form: no username field, 5 password fields -->
+ <form id='form4' action='http://mochi.test:1111/formtest.js'> 4
+ <input type='password' name='pname1' value=''>
+ <input type='password' name='pname2' value=''>
+ <input type='password' name='pname3' value=''>
+ <input type='password' name='pname4' value=''>
+ <input type='password' name='pname5' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <!-- 6 password fields, should be ignored. -->
+ <form id='form5' action='http://mochi.test:1111/formtest.js'> 5
+ <input type='password' name='pname1' value=''>
+ <input type='password' name='pname2' value=''>
+ <input type='password' name='pname3' value=''>
+ <input type='password' name='pname4' value=''>
+ <input type='password' name='pname5' value=''>
+ <input type='password' name='pname6' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+
+
+ <!-- 1 username field -->
+ <form id='form6' action='http://mochi.test:1111/formtest.js'> 6
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+
+ <!-- 1 username field, with a value set -->
+ <form id='form7' action='http://mochi.test:1111/formtest.js'> 7
+ <input type='text' name='uname' value='someuser'>
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+
+
+ <!--
+ (The following forms have 2 potentially-matching logins, on is
+ password-only, the other is username+password)
+ -->
+
+
+
+ <!-- 1 username field, with value set. Fill in the matching U+P login -->
+ <form id='form8' action='formtest.js'> 8
+ <input type='text' name='uname' value='testuser'>
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <!-- 1 username field, with value set. Don't fill in U+P login-->
+ <form id='form9' action='formtest.js'> 9
+ <input type='text' name='uname' value='someuser'>
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+
+
+ <!-- 1 username field, too small for U+P login -->
+ <form id='form10' action='formtest.js'> 10
+ <input type='text' name='uname' value='' maxlength="4">
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <!-- 1 username field, too small for U+P login -->
+ <form id='form11' action='formtest.js'> 11
+ <input type='text' name='uname' value='' maxlength="0">
+ <input type='password' name='pname' value=''>
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <!-- 1 username field, too small for U+P login -->
+ <form id='form12' action='formtest.js'> 12
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='' maxlength="4">
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <!-- 1 username field, too small for either login -->
+ <form id='form13' action='formtest.js'> 13
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='' maxlength="1">
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>
+
+ <!-- 1 username field, too small for either login -->
+ <form id='form14' action='formtest.js'> 14
+ <input type='text' name='uname' value=''>
+ <input type='password' name='pname' value='' maxlength="0">
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>`, win, 14);
+
+ await checkLoginFormInFrameWithElementValues(win, 1, "1234");
+ await checkLoginFormInFrameWithElementValues(win, 2, "1234", "");
+ await checkLoginFormInFrameWithElementValues(win, 3, "1234", "", "");
+ await checkLoginFormInFrameWithElementValues(win, 4, "1234");
+ await checkUnmodifiedFormInFrame(win, 5);
+
+ await checkLoginFormInFrameWithElementValues(win, 6, "testuser", "testpass");
+ await checkLoginFormInFrameWithElementValues(win, 7, "someuser", "");
+
+ await checkLoginFormInFrameWithElementValues(win, 8, "testuser", "testpass");
+ await checkLoginFormInFrameWithElementValues(win, 9, "someuser", "");
+
+ await checkLoginFormInFrameWithElementValues(win, 10, "", "1234");
+ await checkLoginFormInFrameWithElementValues(win, 11, "", "1234");
+ await checkLoginFormInFrameWithElementValues(win, 12, "", "1234");
+
+ await checkUnmodifiedFormInFrame(win, 13);
+ await checkUnmodifiedFormInFrame(win, 14);
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html b/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html
new file mode 100644
index 0000000000..63ff775aab
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html
@@ -0,0 +1,161 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test bug 627616 related to proxy authentication</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ var Ci = SpecialPowers.Ci;
+
+ function makeXHR(expectedStatus, expectedText, extra) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "authenticate.sjs?" +
+ "proxy_user=proxy_user&" +
+ "proxy_pass=proxy_pass&" +
+ "proxy_realm=proxy_realm&" +
+ "user=user1name&" +
+ "pass=user1pass&" +
+ "realm=mochirealm&" +
+ extra || "");
+ xhr.onloadend = function() {
+ is(xhr.status, expectedStatus, "xhr.status");
+ is(xhr.statusText, expectedText, "xhr.statusText");
+ runNextTest();
+ };
+ return xhr;
+ }
+
+ function testNonAnonymousCredentials() {
+ var xhr = makeXHR(200, "OK");
+ xhr.send();
+ }
+
+ function testAnonymousCredentials() {
+ // Test that an anonymous request correctly performs proxy authentication
+ var xhr = makeXHR(401, "Authentication required");
+ SpecialPowers.wrap(xhr).channel.loadFlags |= Ci.nsIChannel.LOAD_ANONYMOUS;
+ xhr.send();
+ }
+
+ function testAnonymousNoAuth() {
+ // Next, test that an anonymous request still does not include any non-proxy
+ // authentication headers.
+ var xhr = makeXHR(200, "Authorization header not found", "anonymous=1");
+ SpecialPowers.wrap(xhr).channel.loadFlags |= Ci.nsIChannel.LOAD_ANONYMOUS;
+ xhr.send();
+ }
+
+ var gExpectedDialogs = 0;
+ var gCurrentTest;
+ function runNextTest() {
+ is(gExpectedDialogs, 0, "received expected number of auth dialogs");
+ mm.sendAsyncMessage("prepareForNextTest");
+ mm.addMessageListener("prepareForNextTestDone", function prepared(msg) {
+ mm.removeMessageListener("prepareForNextTestDone", prepared);
+ if (pendingTests.length) {
+ ({expectedDialogs: gExpectedDialogs,
+ test: gCurrentTest} = pendingTests.shift());
+ gCurrentTest.call(this);
+ } else {
+ mm.sendAsyncMessage("cleanup");
+ mm.addMessageListener("cleanupDone", () => {
+ // mm.destroy() is called as a cleanup function by runInParent(), no
+ // need to do it here.
+ SimpleTest.finish();
+ });
+ }
+ });
+ }
+
+ var pendingTests = [{expectedDialogs: 2, test: testNonAnonymousCredentials},
+ {expectedDialogs: 1, test: testAnonymousCredentials},
+ {expectedDialogs: 0, test: testAnonymousNoAuth}];
+
+ const mm = runInParent(() => {
+ const { classes: parentCc, interfaces: parentCi } = Components;
+
+ const {NetUtil} = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+ const channel = NetUtil.newChannel({
+ uri: "http://example.com",
+ loadUsingSystemPrincipal: true,
+ });
+
+ const pps = parentCc["@mozilla.org/network/protocol-proxy-service;1"].
+ getService(parentCi.nsIProtocolProxyService);
+ pps.asyncResolve(channel, 0, {
+ async onProxyAvailable(req, uri, pi, status) {
+ const mozproxy = "moz-proxy://" + pi.host + ":" + pi.port;
+ const login1 = parentCc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(parentCi.nsILoginInfo);
+ login1.init(mozproxy, null, "proxy_realm", "proxy_user", "proxy_pass",
+ "", "");
+
+ const login2 = parentCc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(parentCi.nsILoginInfo);
+ login2.init("http://mochi.test:8888", null, "mochirealm", "user1name",
+ "user1pass", "", "");
+ await Services.logins.addLogins([login1, login2]);
+
+ sendAsyncMessage("setupDone");
+ },
+ QueryInterface: ChromeUtils.generateQI([parentCi.nsIProtocolProxyCallback]),
+ });
+
+ addMessageListener("prepareForNextTest", message => {
+ parentCc["@mozilla.org/network/http-auth-manager;1"].
+ getService(parentCi.nsIHttpAuthManager).
+ clearAll();
+ sendAsyncMessage("prepareForNextTestDone");
+ });
+
+ const modalType = Services.prefs.getIntPref(
+ "prompts.modalType.httpAuth"
+ );
+ const authPromptIsCommonDialog =
+ modalType === Services.prompt.MODAL_TYPE_WINDOW
+ || (modalType === Services.prompt.MODAL_TYPE_TAB
+ && Services.prefs.getBoolPref(
+ "prompts.tabChromePromptSubDialog",
+ false
+ ));
+
+ const dialogObserverTopic = authPromptIsCommonDialog
+ ? "common-dialog-loaded" : "tabmodal-dialog-loaded";
+
+ function dialogObserver(subj, topic, data) {
+ if (authPromptIsCommonDialog) {
+ subj.Dialog.ui.prompt.document
+ .getElementById("commonDialog")
+ .acceptDialog();
+ } else {
+ const prompt = subj.ownerGlobal.gBrowser.selectedBrowser
+ .tabModalPromptBox.getPrompt(subj);
+ prompt.Dialog.ui.button0.click(); // Accept button
+ }
+ sendAsyncMessage("promptAccepted");
+ }
+
+ Services.obs.addObserver(dialogObserver, dialogObserverTopic);
+
+ addMessageListener("cleanup", message => {
+ Services.obs.removeObserver(dialogObserver, dialogObserverTopic);
+ sendAsyncMessage("cleanupDone");
+ });
+ });
+
+ mm.addMessageListener("promptAccepted", msg => {
+ gExpectedDialogs--;
+ });
+ mm.addMessageListener("setupDone", msg => {
+ runNextTest();
+ });
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html b/toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html
new file mode 100644
index 0000000000..f7de66a01d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=776171
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 776171 related to HTTP auth</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="startTest()">
+<script class="testbody" type="text/javascript">
+
+/**
+ * This test checks we correctly ignore authentication entry
+ * for a subpath and use creds from the URL when provided when XHR
+ * is used with filled user name and password.
+ *
+ * 1. connect auth2/authenticate.sjs that expects user1:pass1 password
+ * 2. connect a dummy URL at the same path
+ * 3. connect authenticate.sjs that again expects user1:pass1 password
+ * in this case, however, we have an entry without an identity
+ * for this path (that is a parent for auth2 path in the first step)
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+function doxhr(URL, user, pass, next) {
+ var xhr = new XMLHttpRequest();
+ if (user && pass) {
+ xhr.open("POST", URL, true, user, pass);
+ } else {
+ xhr.open("POST", URL, true);
+ }
+ xhr.onload = function() {
+ is(xhr.status, 200, "Got status 200");
+ next();
+ };
+ xhr.onerror = function() {
+ ok(false, "request passed");
+ SimpleTest.finish();
+ };
+ xhr.send();
+}
+
+function startTest() {
+ doxhr("auth2/authenticate.sjs?user=user1&pass=pass1&realm=realm1", "user1", "pass1", function() {
+ doxhr("auth2", null, null, function() {
+ doxhr("authenticate.sjs?user=user1&pass=pass1&realm=realm1", "user1", "pass1", SimpleTest.finish);
+ });
+ });
+}
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_case_differences.html b/toolkit/components/passwordmgr/test/mochitest/test_case_differences.html
new file mode 100644
index 0000000000..fd0d5b39f8
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_case_differences.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autocomplete due to multiple matching logins</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: autocomplete due to multiple matching logins
+<p id="display"></p>
+<!-- we presumably can't hide the content for this test. -->
+<div id="content"></div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/** Test for Login Manager: autocomplete due to multiple matching logins **/
+
+add_setup(async () => {
+ await addLoginsInParent(
+ [location.origin, "https://autocomplete:8888", null, "name", "pass", "uname", "pword"],
+ [location.origin, "https://autocomplete:8888", null, "Name", "Pass", "uname", "pword"],
+ [location.origin, "https://autocomplete:8888", null, "USER", "PASS", "uname", "pword"]
+ );
+})
+
+add_task(async function test_empty_first_entry() {
+ const form = createLoginForm({
+ action: "https://autocomplete:8888"
+ });
+
+ // Make sure initial form is empty.
+ checkLoginForm(form.uname, "", form.pword, "");
+
+ // Trigger autocomplete popup
+ form.uname.focus();
+
+ let popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+ const { items } = await openPopupOn(form.uname);
+ popupState = await getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected");
+ checkAutoCompleteResults(items, ["name", "Name", "USER"], "example.com", "initial");
+
+ // Check first entry
+ const index0Promise = notifySelectedIndex(0);
+ synthesizeKey("KEY_ArrowDown");
+ await index0Promise;
+ checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ checkLoginForm(form.uname, "name", form.pword, "pass");
+});
+
+add_task(async function test_empty_second_entry() {
+ const form = createLoginForm({
+ action: "https://autocomplete:8888"
+ });
+
+ await openPopupOn(form.uname);
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_ArrowDown"); // second
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ checkLoginForm(form.uname, "Name", form.pword, "Pass");
+});
+
+add_task(async function test_empty_third_entry() {
+ const form = createLoginForm({
+ action: "https://autocomplete:8888"
+ });
+
+ await openPopupOn(form.uname);
+ synthesizeKey("KEY_ArrowDown"); // first
+ synthesizeKey("KEY_ArrowDown"); // second
+ synthesizeKey("KEY_ArrowDown"); // third
+ synthesizeKey("KEY_Enter");
+ await promiseFormsProcessedInSameProcess();
+ checkLoginForm(form.uname, "USER", form.pword, "PASS");
+});
+
+add_task(async function test_preserve_matching_username_case() {
+ const form = createLoginForm({
+ action: "https://autocomplete:8888"
+ });
+ await promiseFormsProcessedInSameProcess();
+
+ await openPopupOn(form.uname, { inputValue: "user" });
+ // Check that we don't clobber user-entered text when tabbing away
+ // (even with no autocomplete entry selected)
+ synthesizeKey("KEY_Tab");
+ await promiseFormsProcessedInSameProcess();
+ checkLoginForm(form.uname, "user", form.pword, "PASS");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_dismissed_doorhanger_in_shadow_DOM.html b/toolkit/components/passwordmgr/test/mochitest/test_dismissed_doorhanger_in_shadow_DOM.html
new file mode 100644
index 0000000000..37ddbaae42
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_dismissed_doorhanger_in_shadow_DOM.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test the password manager dismissed doorhanger can detect username and password fields in a Shadow DOM.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<iframe></iframe>
+
+<script type="application/javascript">
+const { LoginManagerChild } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/LoginManagerChild.jsm"
+);
+
+add_setup(async () => {
+ const readyPromise = registerRunTests();
+ info("Waiting for setup and page load");
+ await readyPromise;
+
+ // assert that there are no logins
+ const allLogins = await LoginManager.getAllLogins();
+ is(allLogins.length, 0, "There are no logins");
+});
+
+const IFRAME = document.querySelector("iframe");
+const PASSWORD_VALUE = "!@$*";
+const TESTCASES = [
+ // Check that the Shadow DOM version of form_basic.html works
+ {
+ name: "test_form_each_field_in_its_own_shadow_root",
+ filename: "form_basic_shadow_DOM_each_field_in_its_own_shadow_root.html",
+ },
+ // Check that the Shadow DOM version of formless_basic.html works
+ {
+ name: "test_formless_each_field_in_its_own_shadow_root",
+ filename: "formless_basic_shadow_DOM_each_field_in_its_own_shadow_root.html",
+ },
+ // Check that the nested Shadow DOM version of form_basic.html works
+ {
+ name: "test_form_nested_each_field_in_its_own_shadow_root",
+ filename: "form_nested_shadow_DOM_each_field_in_its_own_shadow_root.html",
+ }
+];
+
+async function editPasswordFieldInShadowDOM() {
+ info("Editing the input field in the form with a Shadow DOM");
+ await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [PASSWORD_VALUE], function(val) {
+ const doc = this.content.document;
+ // Grab the wrapper element to get the shadow root containing the password field
+ let wrapper = doc.getElementById("wrapper-password");
+ if (!wrapper) {
+ // This is a nested Shadow DOM test case
+ const outerWrapper = doc.getElementById("outer-wrapper-password");
+ const outerShadowRoot = outerWrapper.openOrClosedShadowRoot;
+ wrapper = outerShadowRoot.querySelector("#wrapper-password");
+ }
+ // If the ShadowRoot's mode is "closed", it can only be accessed from a chrome-privileged
+ // (Bug 1421568) or addon context (Bug 1439153)
+ const shadowRoot = wrapper.openOrClosedShadowRoot;
+ const passwordField = shadowRoot.querySelector("[name='password']");
+ Assert.equal(passwordField.value, "", "Check password didn't get autofilled");
+ passwordField.setUserInput(val);
+ Assert.equal(passwordField.value, val, "Checking for filled password");
+ }
+ );
+}
+
+async function testForm(testcase) {
+ const iframeLoaded = new Promise(resolve => {
+ IFRAME.addEventListener(
+ "load",
+ function(e) {
+ resolve(true);
+ },
+ { once: true }
+ );
+ });
+
+ // This could complete before the page finishes loading.
+ const formsProcessed = promiseFormsProcessed();
+
+ IFRAME.src = testcase.filename;
+ info("Waiting for test page to load in the iframe");
+ await iframeLoaded;
+
+ info("Waiting for 'input' event listener to be added to the form before editing");
+ await formsProcessed;
+
+ const passwordEditProcessed = getPasswordEditedMessage();
+
+ await editPasswordFieldInShadowDOM();
+
+ info("Waiting for parent process to receive input field edit message from content");
+ await passwordEditProcessed;
+}
+
+for (let testcase of TESTCASES) {
+ const taskName = testcase.name;
+ const tmp = {
+ async [taskName]() {
+ await testForm(testcase);
+ }
+ }
+ add_task(tmp[taskName]);
+}
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formLike_rootElement_with_Shadow_DOM.html b/toolkit/components/passwordmgr/test/mochitest/test_formLike_rootElement_with_Shadow_DOM.html
new file mode 100644
index 0000000000..2e9b0039ca
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formLike_rootElement_with_Shadow_DOM.html
@@ -0,0 +1,151 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test that FormLike.rootElement points to right element when the page has Shadow DOM</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<iframe></iframe>
+
+<script type="application/javascript">
+const { LoginFormFactory } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/LoginFormFactory.jsm"
+);
+
+add_setup(async () => {
+ const readyPromise = registerRunTests();
+ info("Waiting for setup and page load");
+ await readyPromise;
+
+ // assert that there are no logins
+ const allLogins = await LoginManager.getAllLogins();
+ is(allLogins.length, 0, "There are no logins");
+});
+
+const IFRAME = document.querySelector("iframe");
+const TESTCASES = [
+ // Check that the Shadow DOM version of form_basic.html works
+ {
+ name: "test_form_each_field_in_its_own_shadow_root",
+ filename: "form_basic_shadow_DOM_each_field_in_its_own_shadow_root.html",
+ hostAndRootElementSelectorTuples: [["span#wrapper-password", "form"]],
+ },
+ {
+ name: "test_form_both_fields_together_in_a_shadow_root",
+ filename: "form_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html",
+ hostAndRootElementSelectorTuples: [["span#wrapper-un-and-pw", "form"]],
+ },
+ {
+ name: "test_form_form_and_fields_together_in_a_shadow_root",
+ filename: "form_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html",
+ hostAndRootElementSelectorTuples: [["span#wrapper", "form"]],
+ },
+ // Check that the Shadow DOM version of formless_basic.html works
+ {
+ name: "test_formless_each_field_in_its_own_shadow_root",
+ filename: "formless_basic_shadow_DOM_each_field_in_its_own_shadow_root.html",
+ hostAndRootElementSelectorTuples: [["span#wrapper-password", "html"]],
+ },
+ {
+ name: "test_formless_both_fields_together_in_a_shadow_root",
+ filename: "formless_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html",
+ hostAndRootElementSelectorTuples: [["span#wrapper-un-and-pw", "html"]],
+ },
+ {
+ name: "test_formless_form_and_fields_together_in_a_shadow_root.html",
+ filename: "formless_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html",
+ hostAndRootElementSelectorTuples: [["span#wrapper", "html"]],
+ },
+ // Check that the nested Shadow DOM version of form_basic.html works
+ {
+ name: "test_form_nested_each_field_in_its_own_shadow_root",
+ filename: "form_nested_shadow_DOM_each_field_in_its_own_shadow_root.html",
+ hostAndRootElementSelectorTuples: [["span#wrapper-password", "form"]],
+ outerHostElementSelector: "span#outer-wrapper-password",
+ },
+ {
+ name: "test_form_nested_both_fields_together_in_a_shadow_root",
+ filename: "form_nested_shadow_DOM_both_fields_together_in_a_shadow_root.html",
+ hostAndRootElementSelectorTuples: [["span#inner-wrapper", "form"]],
+ outerHostElementSelector: "span#outer-wrapper",
+ },
+ {
+ name: "test_form_nested_form_and_fields_together_in_a_shadow_root",
+ filename: "form_nested_shadow_DOM_form_and_fields_together_in_a_shadow_root.html",
+ hostAndRootElementSelectorTuples: [["span#inner-wrapper", "form"]],
+ outerHostElementSelector: "span#outer-wrapper",
+ },
+ {
+ name: "test_multiple_forms_shadow_DOM_all_known_variants",
+ filename: "multiple_forms_shadow_DOM_all_known_variants.html",
+ hostAndRootElementSelectorTuples: [
+ ["span#outer-wrapper", "html"],
+ ["span#wrapper-password-form-case-1", "form#form-case-1"],
+ ["span#wrapper-form-case-2", "form#form-case-2"],
+ ["span#wrapper-form-case-3", "form#form-case-3"],
+ ],
+ outerHostElementSelector: "span#outer-wrapper",
+ }
+];
+
+async function testForm(testcase) {
+ const iframeLoaded = new Promise(resolve => {
+ IFRAME.addEventListener(
+ "load",
+ function(e) {
+ resolve(true);
+ },
+ { once: true }
+ );
+ });
+
+ // This could complete before the page finishes loading.
+ const numForms = testcase.hostAndRootElementSelectorTuples.length;
+ const formsProcessed = promiseFormsProcessedInSameProcess(numForms);
+
+ IFRAME.src = testcase.filename;
+ info("Waiting for test page to load in the iframe");
+ await iframeLoaded;
+
+ info(`Wait for ${numForms} form(s) to be processed.`);
+ await formsProcessed;
+
+ const iframeDoc = SpecialPowers.wrap(IFRAME.contentWindow).document;
+ for (let [hostElementSelector, rootElementSelector] of testcase.hostAndRootElementSelectorTuples) {
+ info("Get the expected rootElement from the document");
+ let hostElement = iframeDoc.querySelector(hostElementSelector);
+ let outerShadowRoot = null;
+ if (!hostElement) {
+ // Nested Shadow DOM testcase
+ const outerHostElement = iframeDoc.querySelector(testcase.outerHostElementSelector);
+ outerShadowRoot = outerHostElement.openOrClosedShadowRoot;
+ hostElement = outerShadowRoot.querySelector(hostElementSelector);
+ }
+ const shadowRoot = hostElement.openOrClosedShadowRoot;
+ let expectedRootElement = iframeDoc.querySelector(rootElementSelector);
+ if (!expectedRootElement) {
+ // The form itself is inside a ShadowRoot and/or there is a ShadowRoot in between the field and form
+ expectedRootElement =
+ shadowRoot.querySelector(rootElementSelector) ||
+ outerShadowRoot.querySelector(rootElementSelector);
+ }
+ ok(LoginFormFactory.getRootElementsWeakSetForDocument(iframeDoc).has(expectedRootElement), "Ensure formLike.rootElement has the expected value");
+ }
+}
+
+for (let testcase of TESTCASES) {
+ const taskName = testcase.name;
+ const tmp = {
+ async [taskName]() {
+ await testForm(testcase);
+ }
+ }
+ add_task(tmp[taskName]);
+}
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html b/toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html
new file mode 100644
index 0000000000..21f5f18904
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html
@@ -0,0 +1,140 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for considering form action</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: Bug 360493
+<script>
+gTestDependsOnDeprecatedLogin = true;
+runChecksAfterCommonInit(() => startTest());
+
+let DEFAULT_ORIGIN = window.location.origin;
+</script>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: 360493 (Cross-Site Forms + Password
+ Manager = Security Failure) **/
+
+// This test is designed to make sure variations on the form's |action|
+// and |method| continue to work with the fix for 360493.
+
+async function startTest() {
+ let win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await loadFormIntoWindow(DEFAULT_ORIGIN, `
+ <!-- normal form with normal relative action. -->
+ <form id="form1" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- fully specify the action URL -->
+ <form id="form2" action="http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- fully specify the action URL, and change the path -->
+ <form id="form3" action="http://mochi.test:8888/zomg/wtf/bbq/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- fully specify the action URL, and change the path and filename -->
+ <form id="form4" action="http://mochi.test:8888/zomg/wtf/bbq/passwordmgr/test/not_a_test.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- specify the action URL relative to the current document-->
+ <form id="form5" action="./formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- specify the action URL relative to the current server -->
+ <form id="form6" action="/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Change the method from get to post -->
+ <form id="form7" action="formtest.js" method="POST">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Blank action URL specified -->
+ <form id="form8" action="">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- |action| attribute entirely missing -->
+ <form id="form9" >
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- action url as javascript -->
+ <form id="form10" action="javascript:alert('this form is not submitted so this alert should not be invoked');">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>`, win, 10);
+
+ // TODO: action=IP.ADDRESS instead of HOSTNAME?
+ // TODO: test with |base href="http://othersite//"| ?
+
+ for (var i = 1; i <= 9; i++) {
+ // Check form i
+ await checkLoginFormInFrameWithElementValues(win, i, "testuser", "testpass");
+ }
+
+ // The login's formActionOrigin isn't "javascript:", so don't fill it in.
+ await checkLoginFormInFrameWithElementValues(win, 10, "", "");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html b/toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html
new file mode 100644
index 0000000000..2eae0958ca
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html
@@ -0,0 +1,173 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for considering form action</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: Bug 360493
+<script>
+gTestDependsOnDeprecatedLogin = true;
+runChecksAfterCommonInit(() => startTest());
+
+let DEFAULT_ORIGIN = window.location.origin;
+</script>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: 360493 (Cross-Site Forms + Password Manager = Security Failure) **/
+
+async function startTest() {
+ let win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await loadFormIntoWindow(DEFAULT_ORIGIN, `
+ <!-- The tests in this page exercise things that shouldn't work. -->
+
+ <!-- Change port # of action URL from 8888 to 7777 -->
+ <form id="form1" action="http://localhost:7777/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- No port # in action URL -->
+ <form id="form2" action="http://localhost/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Change protocol from http:// to ftp://, include the expected 8888 port # -->
+ <form id="form3" action="ftp://localhost:8888/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Change protocol from http:// to ftp://, no port # specified -->
+ <form id="form4" action="ftp://localhost/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Try a weird URL. -->
+ <form id="form5" action="about:blank">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Try a weird URL. (If the normal embedded action URL doesn't work, that should mean other URLs won't either) -->
+ <form id="form6" action="view-source:http://localhost:8888/tests/toolkit/components/passwordmgr/test/formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Try a weird URL. -->
+ <form id="form7" action="view-source:formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Action URL points to a different host (this is the archetypical exploit) -->
+ <form id="form8" action="http://www.cnn.com/">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Action URL points to a different host, user field prefilled -->
+ <form id="form9" action="http://www.cnn.com/">
+ <input type="text" name="uname" value="testuser">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- Try wrapping a evil form around a good form, to see if we can confuse the parser. -->
+ <form id="form10-A" action="http://www.cnn.com/">
+ <form id="form10-B" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit (inner)</button>
+ <button type="reset"> Reset (inner)</button>
+ </form>
+ <button type="submit" id="neutered_submit10">Submit (outer)</button>
+ <button type="reset">Reset (outer)</button>
+ </form>
+
+ <!-- Try wrapping a good form around an evil form, to see if we can confuse the parser. -->
+ <form id="form11-A" action="formtest.js">
+ <form id="form11-B" action="http://www.cnn.com/">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit (inner)</button>
+ <button type="reset"> Reset (inner)</button>
+ </form>
+ <button type="submit" id="neutered_submit11">Submit (outer)</button>
+ <button type="reset">Reset (outer)</button>
+ </form>`, win, 11);
+
+ // TODO: probably should have some accounts which have no port # in the action url.
+ // JS too. And different host/proto.
+ // TODO: www.site.com vs. site.com?
+ // TODO: foo.site.com vs. bar.site.com?
+
+ for (var i = 1; i <= 8; i++) {
+ // Check form i
+ await checkLoginFormInFrameWithElementValues(win, i, "", "");
+ }
+
+ await checkLoginFormInFrameWithElementValues(win, 9, "testuser", "");
+
+ await checkLoginFormInFrameWithElementValues(win, "10-A", "", "");
+
+ // The DOM indicates this form could be filled, as the evil inner form
+ // is discarded. And yet pwmgr seems not to fill it. Not sure why.
+ todo(false, "Mangled form combo not being filled when maybe it could be?");
+ await checkLoginFormInFrameWithElementValues(win, "11-A", "testuser", "testpass");
+
+ // Verify this by making sure there are no extra forms in the document, and
+ // that the submit button for the neutered forms don't do anything.
+ // If the test finds extra forms the submit() causes the test to timeout, then
+ // there may be a security issue.
+ await SpecialPowers.spawn(win, [], async function submitForms() {
+ is(this.content.document.forms.length, 11, "Checking for unexpected forms");
+ this.content.document.getElementById("neutered_submit10").click();
+ this.content.document.getElementById("neutered_submit11").click();
+ });
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html b/toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html
new file mode 100644
index 0000000000..3bb52ef8df
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test forms with a JS submit action</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: form with JS submit action
+<script>
+runChecksAfterCommonInit(() => startTest());
+
+const origin = window.location.origin;
+
+/** Test for Login Manager: JS action URL **/
+
+async function startTest() {
+ await addLoginsInParent(
+ [origin, "javascript:", null, "jsuser", "jspass123", "uname", "pword"]
+ );
+ const win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await loadFormIntoWindow(origin, `
+ <form id='form1' action='javascript:alert("never shows")'> 1
+ <input name="uname">
+ <input name="pword" type="password">
+
+ <button type='submit'>Submit</button>
+ <button type='reset'> Reset </button>
+ </form>`, win);
+
+ await checkLoginFormInFrameWithElementValues(win, 1, "jsuser", "jspass123");
+
+ SimpleTest.finish();
+}
+</script>
+
+<p id="display"></p>
+
+<div id="content" style="display: none">
+</div>
+
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html
new file mode 100644
index 0000000000..3422fa893a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html
@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test autofilling of fields outside of a form</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript">
+document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("loginFrame").addEventListener("load", (evt) => {
+ // Tell the parent to setup test logins.
+ PWMGR_COMMON_PARENT.sendAsyncMessage("setupParent", { selfFilling: true, testDependsOnDeprecatedLogin: true });
+ });
+});
+
+let doneSetupPromise = new Promise(resolve => {
+ // When the setup is done, load a recipe for this test.
+ PWMGR_COMMON_PARENT.addMessageListener("doneSetup", function doneSetup() {
+ resolve();
+ });
+});
+
+add_setup(async () => {
+ info("Waiting for loads and setup");
+ await doneSetupPromise;
+
+ await loadRecipes({
+ siteRecipes: [{
+ hosts: ["mochi.test:8888"],
+ usernameSelector: "input[name='recipeuname']",
+ passwordSelector: "input[name='recipepword']",
+ }],
+ });
+});
+
+
+const DEFAULT_ORIGIN = "http://mochi.test:8888";
+const TESTCASES = [
+ {
+ // Inputs
+ document: `<input type=password>`,
+
+ // Expected outputs
+ expectedInputValues: ["testpass"],
+ },
+ {
+ document: `<input>
+ <input type=password>`,
+ expectedInputValues: ["testuser", "testpass"],
+ },
+ {
+ document: `<input>
+ <input type=password>
+ <input type=password>`,
+ expectedInputValues: ["testuser", "testpass", ""],
+ },
+ {
+ document: `<input>
+ <input type=password>
+ <input type=password>
+ <input type=password>`,
+ expectedInputValues: ["testuser", "testpass", "", ""],
+ },
+ {
+ document: `<input>
+ <input type=password form="form1">
+ <input type=password>
+ <form id="form1">
+ <input>
+ <input type=password>
+ </form>`,
+ expectedFormCount: 2,
+ expectedInputValues: ["testuser", "testpass", "testpass", "", ""],
+ },
+ {
+ document: `<!-- formless password field selector recipe test -->
+ <input>
+ <input type=password>
+ <input>
+ <input type=password name="recipepword">`,
+ expectedInputValues: ["", "", "testuser", "testpass"],
+ },
+ {
+ document: `<!-- formless username and password field selector recipe test -->
+ <input name="recipeuname">
+ <input>
+ <input type=password>
+ <input type=password name="recipepword">`,
+ expectedInputValues: ["testuser", "", "", "testpass"],
+ },
+ {
+ document: `<!-- form and formless recipe field selector test -->
+ <input name="recipeuname">
+ <input>
+ <input type=password form="form1"> <!-- not filled since recipe affects both FormLikes -->
+ <input type=password>
+ <input type=password name="recipepword">
+ <form id="form1">
+ <input>
+ <input type=password>
+ </form>`,
+ expectedFormCount: 2,
+ expectedInputValues: ["testuser", "", "", "", "testpass", "", ""],
+ },
+];
+
+add_task(async function test() {
+ let loginFrame = document.getElementById("loginFrame");
+ let frameDoc = loginFrame.contentWindow.document;
+
+ for (let tc of TESTCASES) {
+ info("Starting testcase: " + JSON.stringify(tc));
+
+ let numFormLikesExpected = tc.expectedFormCount || 1;
+
+ let processedFormPromise = promiseFormsProcessedInSameProcess(numFormLikesExpected);
+
+ // eslint-disable-next-line no-unsanitized/property
+ frameDoc.documentElement.innerHTML = tc.document;
+ info("waiting for " + numFormLikesExpected + " processed form(s)");
+ await processedFormPromise;
+
+ let testInputs = frameDoc.documentElement.querySelectorAll("input");
+ is(testInputs.length, tc.expectedInputValues.length, "Check number of inputs");
+ for (let i = 0; i < tc.expectedInputValues.length; i++) {
+ let expectedValue = tc.expectedInputValues[i];
+ is(testInputs[i].value, expectedValue, `Check expected input value ${i} : ${expectedValue}`);
+ }
+ }
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ <iframe id="loginFrame" src="http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/blank.html"></iframe>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html
new file mode 100644
index 0000000000..cf1e82a85e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html
@@ -0,0 +1,243 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test capturing of fields outside of a form</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript">
+const { LoginFormFactory } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/LoginFormFactory.jsm"
+);
+const { LoginManagerChild } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/LoginManagerChild.jsm"
+);
+
+function loadFrame() {
+ return new Promise(resolve => {
+ document.getElementById("loginFrame").addEventListener("load", (evt) => {
+ if (evt.target.contentWindow.location.href.includes("blank.html")) {
+ resolve();
+ }
+ });
+ });
+}
+
+let loadPromise = new Promise(resolve => {
+ document.addEventListener("DOMContentLoaded", () => {
+ resolve(loadFrame());
+ });
+});
+
+add_setup(async () => {
+ info("Waiting for page and frame loads");
+ await loadPromise;
+
+ await loadRecipes({
+ siteRecipes: [{
+ hosts: ["mochi.test:8888"],
+ usernameSelector: "input[name='recipeuname']",
+ passwordSelector: "input[name='recipepword']",
+ }],
+ });
+});
+
+const DEFAULT_ORIGIN = "http://mochi.test:8888";
+const TESTCASES = [
+ {
+ // Inputs
+ document: `<input type=password value="">`,
+ selectorValues: {
+ "[type=password]": "pass1",
+ },
+ inputIndexForFormLike: 0,
+ expectedFormsCount: 1,
+
+ // Expected outputs similar to PasswordManager:onFormSubmit
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: null,
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<input id="u1" value="">
+ <input type=password value="">`,
+ selectorValues: {
+ "#u1": "user1",
+ "[type=password]": "pass1",
+ },
+ inputIndexForFormLike: 0,
+ expectedFormsCount: 1,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<input id="u1" value="">
+ <input type=password value="">`,
+ selectorValues: {
+ "#u1": "user1",
+ "[type=password]": "pass1",
+ },
+ inputIndexForFormLike: 1,
+ expectedFormsCount: 1,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<input id="u1" value="">
+ <input id="p1" type=password value="">
+ <input id="p2" type=password value="">`,
+ selectorValues: {
+ "#u1": "user1",
+ "#p1": "pass1",
+ "#p2": "pass2",
+ },
+ inputIndexForFormLike: 2,
+ expectedFormsCount: 1,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: "pass1",
+ },
+ {
+ document: `<input id="u1" value="">
+ <input id="p1" type=password value="">
+ <input id="p2" type=password value="">
+ <input id="p3" type=password value="">`,
+ selectorValues: {
+ "#u1": "user1",
+ "#p1": "pass1",
+ "#p2": "pass2",
+ "#p3": "pass2",
+ },
+ inputIndexForFormLike: 3,
+ expectedFormsCount: 1,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: "pass1",
+ },
+ {
+ document: `<input id="u1" value="">
+ <input id="p1" type=password value="" form="form1">
+ <input id="p2" type=password value="">
+ <form id="form1">
+ <input id="u2" value="">
+ <input id="p3" type=password value="">
+ </form>`,
+ selectorValues: {
+ "#u1": "user1",
+ "#p1": "user2",
+ "#p2": "pass1",
+ "#u2": "user3",
+ "#p3": "pass2",
+ },
+ inputIndexForFormLike: 2,
+ expectedFormsCount: 2,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<!-- recipe field override -->
+ <input name="recipeuname" value="">
+ <input id="u1" value="">
+ <input id="p1" type=password value="">
+ <input name="recipepword" type=password value="">`,
+ selectorValues: {
+ "[name='recipeuname']": "username from recipe",
+ "#u1": "default field username",
+ "#p1": "pass1",
+ "[name='recipepword']": "pass2",
+ },
+ inputIndexForFormLike: 2,
+ expectedFormsCount: 1,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: "username from recipe",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: null,
+ },
+];
+
+let count = 0;
+async function testFormlessSubmit(tc) {
+ let loginFrame = document.getElementById("loginFrame");
+
+ let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document;
+ info("Starting testcase: " + JSON.stringify(tc));
+
+ let formsProcessed = promiseFormsProcessedInSameProcess(tc.expectedFormsCount);
+ // eslint-disable-next-line no-unsanitized/property
+ frameDoc.documentElement.innerHTML = tc.document;
+ await formsProcessed;
+ // We eliminate no user input as a reason for not capturing by modifying the value
+ setUserInputValues(frameDoc.documentElement, tc.selectorValues);
+
+ let inputForFormLike = frameDoc.querySelectorAll("input")[tc.inputIndexForFormLike];
+
+ let formLike = LoginFormFactory.createFromField(inputForFormLike);
+
+ info("Calling _onFormSubmit with FormLike");
+ let submitProcessed = getSubmitMessage();
+ LoginManagerChild.forWindow(frameDoc.defaultView)._onFormSubmit(formLike);
+
+ let { origin, data } = await submitProcessed;
+
+ // Check data sent via PasswordManager:onFormSubmit
+ is(origin, tc.origin, "Check origin");
+ is(data.formActionOrigin, tc.formActionOrigin, "Check formActionOrigin");
+
+ if (tc.usernameFieldValue === null) {
+ is(data.usernameField, tc.usernameFieldValue, "Check usernameField");
+ } else {
+ is(data.usernameField.value, tc.usernameFieldValue, "Check usernameField");
+ }
+
+ is(data.newPasswordField.value, tc.newPasswordFieldValue, "Check newPasswordFieldValue");
+
+ if (tc.oldPasswordFieldValue === null) {
+ is(data.oldPasswordField, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue");
+ } else {
+ is(data.oldPasswordField.value, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue");
+ }
+
+ loadPromise = loadFrame();
+ loginFrame.contentWindow.location =
+ "http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html?" + count++;
+ await loadPromise;
+}
+
+for (let tc of TESTCASES) {
+ let taskName = "testcase-" + count++;
+ let tmp = {
+ async [taskName]() {
+ await testFormlessSubmit(tc);
+ },
+ };
+ add_task(tmp[taskName]);
+}
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ <iframe id="loginFrame" src="http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal.html
new file mode 100644
index 0000000000..56fc428ec3
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal.html
@@ -0,0 +1,292 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test capturing of fields due to form removal</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript">
+const { LoginManagerChild } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/LoginManagerChild.jsm"
+);
+
+let loadPromise = new Promise(resolve => {
+ document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("loginFrame").addEventListener("load", (evt) => {
+ resolve();
+ });
+ });
+});
+
+add_setup(async () => {
+ info("Waiting for page and frame loads");
+ await loadPromise;
+
+ await loadRecipes({
+ siteRecipes: [{
+ hosts: ["test1.mochi.test:8888"],
+ usernameSelector: "input[name='recipeuname']",
+ passwordSelector: "input[name='recipepword']",
+ }],
+ });
+});
+
+const DEFAULT_ORIGIN = "http://test1.mochi.test:8888";
+const SCRIPTS = {
+ // Test removing the form(or form-less password field), their parent node, and the top-level node.
+ REMOVE_FORM: `let e = document.querySelector("form"); e.parentNode.removeChild(e);`,
+ REMOVE_PASSWORD: `let e = document.querySelector("[type=password]"); e.parentNode.removeChild(e);`,
+ REMOVE_FORM_PARENT: `let e = document.querySelector("form").parentNode; e.parentNode.removeChild(e);`,
+ REMOVE_PASSWORD_PARENT: `let e = document.querySelector("[type=password]").parentNode; e.parentNode.removeChild(e);`,
+ REMOVE_TOP: `let e = document.querySelector("html"); e.parentNode.removeChild(e);`,
+
+ // Add testcases related to page navigation here to ensure these cases still work
+ // when we have set up form removal observer.
+ PUSHSTATE: `history.pushState({}, "Pushed state", "?pushed");`,
+ WINDOW_LOCATION: `window.location = "data:text/html;charset=utf-8,window.location";`,
+};
+const TESTCASES = [
+ {
+ // Inputs
+ document: `<input type=password value="">`,
+ selectorValues: {
+ "[type=password]": "pass1",
+ },
+ expectedFormsCount: 1,
+
+ // Expected outputs similar to PasswordManager:onFormSubmit
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: null,
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<input id="u1" value="">
+ <input type=password value="">`,
+ selectorValues: {
+ "#u1": "user1",
+ "[type=password]": "pass1",
+ },
+
+ expectedFormsCount: 1,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<input id="u1" value="">
+ <input id="p1" type=password value="">
+ <input id="p2" type=password value="">`,
+ selectorValues: {
+ "#u1": "user1",
+ "#p1": "pass1",
+ "#p2": "pass2",
+ },
+
+ expectedFormsCount: 1,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: "pass1",
+ },
+ {
+ document: `<input id="u1" value="">
+ <input id="p1" type=password value="">
+ <input id="p2" type=password value="">
+ <input id="p3" type=password value="">`,
+ selectorValues: {
+ "#u1": "user1",
+ "#p1": "pass1",
+ "#p2": "pass2",
+ "#p3": "pass2",
+ },
+
+ expectedFormsCount: 1,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: "pass1",
+ },
+ {
+ // Since there are two FormLikes to auto-submit in this case we mark
+ // one FormLike's password fields with a magic "ignore-form-submission"
+ // value so we can just focus on the other form. We then repeat the testcase
+ // below with the other FormLike ignored.
+ document: `<input id="u1" value="">
+ <input type=password id="p1" value="" form="form1">
+ <input type=password id="p2" value="">
+ <form id="form1">
+ <input id="u2" value="">
+ <input id="p3" type=password value="">
+ </form>`,
+ selectorValues: {
+ "#u1": "user1",
+ "#p1": "ignore-form-submission",
+ "#p2": "pass1",
+ "#u2": "user3",
+ "#p3": "ignore-form-submission",
+ },
+ expectedFormsCount: 2,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ removePassword: true,
+ },
+ { // Same as above but with the other form ignored.
+ document: `<input id="u1" value="">
+ <input id="p1" type=password value="" form="form1">
+ <input id="p2" type=password value="">
+ <form id="form1">
+ <input id="u2" value="">
+ <input id="p3" type=password value="">
+ </form>`,
+ selectorValues: {
+ "#u1": "user1",
+ "#p1": "pass2",
+ "#p2": "ignore-form-submission",
+ "#u2": "user3",
+ "#p3": "pass2",
+ },
+
+ expectedFormsCount: 2,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: null,
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: null,
+ },
+ /*
+ XXX - Bug 1698498 :
+ This test case fails because when we call querySelector in LoginRecipes.jsm
+ after the form is removed, querySelector can't find the element.
+ {
+ document: `<!-- recipe field override -->
+ <input name="recipeuname" value="">
+ <input id="u1" value="">
+ <input id="p1" type=password value="">
+ <input name="recipepword" type=password value="">`,
+ selectorValues: {
+ "[name='recipeuname']": "username from recipe",
+ "#u1": "default field username",
+ "#p1": "pass1",
+ "[name='recipepword']": "pass2",
+ },
+
+ expectedFormsCount: 1,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: "username from recipe",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: null,
+ },*/
+];
+
+function filterFormSubmissions({ origin, data }) {
+ return data.newPasswordField.value != "ignore-form-submission";
+}
+
+async function testFormlesSubmitFormRemoval(tc, testDoc, scriptName) {
+ let loginFrame = document.getElementById("loginFrame");
+ let loadedPromise = new Promise((resolve) => {
+ loginFrame.addEventListener("load", function() {
+ resolve();
+ }, {once: true});
+ });
+ loginFrame.src = DEFAULT_ORIGIN + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html";
+ await loadedPromise;
+
+ let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document;
+
+ let formsProcessed = promiseFormsProcessed(tc.expectedFormsCount);
+ // eslint-disable-next-line no-unsanitized/property
+ frameDoc.documentElement.innerHTML = testDoc;
+ await formsProcessed;
+ // We eliminate no user input as a reason for not capturing by modifying the value
+ setUserInputValues(frameDoc.documentElement, tc.selectorValues)
+
+ await SpecialPowers.spawn(frameDoc.defaultView, [], async () => {
+ await content.fetch("http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html");
+ });
+
+ let submitProcessed = getSubmitMessage(filterFormSubmissions);
+ info("Running " + scriptName + " script to cause a submission");
+ frameDoc.defaultView.eval(SCRIPTS[scriptName]);
+
+ info("Waiting for formSubmissionProcsssed message");
+ let { origin, data } = await submitProcessed;
+ info("Got for formSubmissionProcsssed message");
+
+ // Check data sent via PasswordManager:onFormSubmit
+ is(origin, tc.origin, "Check origin");
+ is(data.formActionOrigin, tc.formActionOrigin, "Check formActionOrigin");
+
+ if (tc.usernameFieldValue === null) {
+ is(data.usernameField, tc.usernameFieldValue, "Check usernameField");
+ } else {
+ is(data.usernameField.value, tc.usernameFieldValue, "Check usernameField");
+ }
+
+ is(data.newPasswordField.value, tc.newPasswordFieldValue, "Check newPasswordFieldValue");
+
+ if (tc.oldPasswordFieldValue === null) {
+ is(data.oldPasswordField, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue");
+ } else {
+ is(data.oldPasswordField.value, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue");
+ }
+};
+
+let count = 0;
+for (let tc of TESTCASES) {
+ for (let scriptName of Object.keys(SCRIPTS)) {
+ for (let surroundDocumentWithForm of [false, true]) {
+ let testDoc = tc.document;
+ if (surroundDocumentWithForm) {
+ if (testDoc.includes("<form")) {
+ info("Skipping surroundDocumentWithForm case since document already contains a <form>");
+ continue;
+ }
+ testDoc = "<form>" + testDoc + "</form>";
+ }
+
+ if (["REMOVE_FORM", "REMOVE_FORM_PARENT"].includes(scriptName) &&
+ (!testDoc.includes("<form") || tc.removePassword)) {
+ continue;
+ } else if (["REMOVE_PASSWORD","REMOVE_PASSWORD_PARENT"].includes(scriptName) &&
+ testDoc.includes("<form")) {
+ continue;
+ }
+
+ let taskName = `testcase-${count}-${scriptName}${surroundDocumentWithForm ? '-formWrapped' : ''}`;
+ let tmp = {
+ async [taskName]() {
+ info("Starting testcase with script " + scriptName + " and " +
+ (surroundDocumentWithForm ? "a" : "no") + " form wrapper: " + JSON.stringify(tc));
+ await testFormlesSubmitFormRemoval(tc, testDoc, scriptName);
+ },
+ };
+ add_task(tmp[taskName]);
+ }
+ }
+ count++;
+}
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ <iframe id="loginFrame" src="http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal_negative.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal_negative.html
new file mode 100644
index 0000000000..8a91553c85
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal_negative.html
@@ -0,0 +1,205 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test no capturing of fields outside of a form due to navigation</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript">
+SimpleTest.requestFlakyTimeout("Testing that a message doesn't arrive");
+
+let loadPromise = new Promise(resolve => {
+ document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("loginFrame").addEventListener("load", (evt) => {
+ resolve();
+ });
+ });
+});
+
+function submissionProcessed(...args) {
+ ok(false, "No formSubmissionProcessed should occur in this test");
+ info("got: " + JSON.stringify(args));
+}
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.formRemovalCapture.enabled", true],
+ ],
+ });
+
+ info("Waiting for page and frame loads");
+ await loadPromise;
+
+ PWMGR_COMMON_PARENT.addMessageListener("formSubmissionProcessed", submissionProcessed);
+
+ SimpleTest.registerCleanupFunction(() => {
+ PWMGR_COMMON_PARENT.removeMessageListener("formSubmissionProcessed", submissionProcessed);
+ });
+});
+
+const DEFAULT_ORIGIN = "http://test1.mochi.test:8888";
+const SCRIPTS = {
+ // Test form submission is not triggered when there is no user interaction and
+ // no ajax request fired previously.
+ REMOVE_TOP: `let e = document.querySelector("html"); e.parentNode.removeChild(e);`,
+
+ // Test the following scripts don't trigger form submissions because of the
+ // form removal heuristics
+ PUSHSTATE: `history.pushState({}, "Pushed state", "?pushed");`,
+ WINDOW_LOCATION: `window.location = "data:text/html;charset=utf-8,window.location";`,
+ WINDOW_LOCATION_RELOAD: `window.location.reload();`,
+ HISTORY_BACK: `history.back();`,
+ HISTORY_GO_MINUS1: `history.go(-1);`,
+};
+
+const HEURISTICS = [
+ {
+ userInput: true,
+ ajaxSuccess: true,
+ },
+ {
+ userInput: false,
+ ajaxSuccess: true,
+ },
+ {
+ userInput: true,
+ ajaxSuccess: false,
+ },
+ {
+ userInput: false,
+ ajaxSuccess: false,
+ },
+];
+
+const TESTCASES = [
+ // Begin test cases that shouldn't trigger capture.
+ {
+ // Empty password field in a form
+ document: `<form><input type=password value="xxx"></form>`,
+ selectorValues: {
+ "[type=password]": "",
+ },
+ },
+ {
+ // Empty password field
+ document: `<input type=password value="">`,
+ selectorValues: {
+ "[type=password]": "",
+ },
+ },
+ {
+ // Test with an input that would normally be captured but with SCRIPTS that
+ // shouldn't trigger capture.
+ document: `<input type=password value="">`,
+ selectorValues: {
+ "[type=password]": "pass2",
+ },
+ wouldCapture: true,
+ },
+ {
+ // Test with an input that would normally be captured but with SCRIPTS that
+ // shouldn't trigger capture.
+ document: `<form><input type=password value=""></form>`,
+ selectorValues: {
+ "[type=password]": "pass2",
+ },
+ wouldCapture: true,
+ },
+];
+
+async function testFormlesSubmitNavigationNegative(tc, scriptName, heuristic) {
+ let loginFrame = document.getElementById("loginFrame");
+ let waitTime;
+ let android = navigator.appVersion.includes("Android");
+ if (android) {
+ // intermittent failures on Android Debug at 5 seconds
+ waitTime = 10000;
+ } else {
+ waitTime = 5000;
+ }
+
+ let loadedPromise = new Promise((resolve) => {
+ loginFrame.addEventListener("load", function() {
+ resolve();
+ }, {once: true});
+ });
+
+ loginFrame.src = DEFAULT_ORIGIN + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html";
+ await loadedPromise;
+
+ let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document;
+ let formsProcessed = promiseFormsProcessed();
+ // eslint-disable-next-line no-unsanitized/property
+ frameDoc.documentElement.innerHTML = tc.document;
+ await formsProcessed;
+
+ // We eliminate no user input as a reason for not capturing by modifying the value
+ setUserInputValues(frameDoc.documentElement, tc.selectorValues, heuristic.userInput);
+
+ if (heuristic.ajaxSuccess) {
+ await SpecialPowers.spawn(frameDoc.defaultView, [], async () => {
+ await content.fetch("http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html");
+ });
+ }
+
+ info("Running " + scriptName + " script to check for a submission");
+ frameDoc.defaultView.eval(SCRIPTS[scriptName]);
+
+ info("Running " + scriptName + " script to check for a submission 1");
+ // Wait to see if the promise above resolves.
+ await new Promise(resolve => setTimeout(resolve, waitTime));
+ info("Running " + scriptName + " script to check for a submission 2");
+ ok(true, "Done waiting for captures");
+
+}
+
+let count = 0;
+for (let tc of TESTCASES) {
+ for (let scriptName of Object.keys(SCRIPTS)) {
+ for (let heuristic of HEURISTICS) {
+ let shouldCaptureAFormRemoval = heuristic.ajaxSuccess && heuristic.userInput;
+ // Only run the following scripts when we are going to observeform removal change
+ // to save some time running this whole test.
+ if (["PUSHSTATE", "WINDOW_LOCATION", "WINDOW_LOCATION_RELOAD", "HISTORY_BACK", "HISTORY_GO_MINUS1"].includes(scriptName)) {
+ if(!shouldCaptureAFormRemoval) {
+ continue;
+ }
+
+ if (tc.wouldCapture && ["PUSHSTATE", "WINDOW_LOCATION"].includes(scriptName)) {
+ // Don't run scripts that should actually capture for this testcase.
+ continue;
+ }
+ } else if (["REMOVE_TOP"].includes(scriptName)) {
+ if (shouldCaptureAFormRemoval) {
+ // Don't run scripts that should actually capture for this testcase.
+ continue;
+ }
+ }
+
+ let taskName = `testcase-${count}-${scriptName}`;
+ let tmp = {
+ async [taskName]() {
+ info("Starting testcase with script " + scriptName + ": " + JSON.stringify(tc) + ": " + JSON.stringify(heuristic));
+ await testFormlesSubmitNavigationNegative(tc, scriptName, heuristic);
+ },
+ };
+ add_task(tmp[taskName]);
+ }
+ }
+}
+
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ <iframe id="loginFrame" src="http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html
new file mode 100644
index 0000000000..22fce4561e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html
@@ -0,0 +1,272 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test capturing of fields outside of a form due to navigation</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript">
+const { LoginManagerChild } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/LoginManagerChild.jsm"
+);
+
+let loadPromise = new Promise(resolve => {
+ document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("loginFrame").addEventListener("load", (evt) => {
+ resolve();
+ });
+ });
+});
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.formlessCapture.enabled", true],
+ ],
+ });
+
+ info("Waiting for page and frame loads");
+ await loadPromise;
+
+ await loadRecipes({
+ siteRecipes: [{
+ hosts: ["test1.mochi.test:8888"],
+ usernameSelector: "input[name='recipeuname']",
+ passwordSelector: "input[name='recipepword']",
+ }],
+ });
+});
+
+const DEFAULT_ORIGIN = "http://test1.mochi.test:8888";
+const SCRIPTS = {
+ PUSHSTATE: `history.pushState({}, "Pushed state", "?pushed");`,
+ WINDOW_LOCATION: `window.location = "data:text/html;charset=utf-8,window.location";`,
+};
+const TESTCASES = [
+ {
+ // Inputs
+ document: `<input type=password value="">`,
+ selectorValues: {
+ "[type=password]": "pass1",
+ },
+ expectedFormsCount: 1,
+
+ // Expected outputs similar to PasswordManager:onFormSubmit
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: null,
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<input id="u1" value="">
+ <input type=password value="">`,
+ selectorValues: {
+ "#u1": "user1",
+ "[type=password]": "pass1",
+ },
+
+ expectedFormsCount: 1,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<input id="u1" value="">
+ <input id="p1" type=password value="">
+ <input id="p2" type=password value="">`,
+ selectorValues: {
+ "#u1": "user1",
+ "#p1": "pass1",
+ "#p2": "pass2",
+ },
+
+ expectedFormsCount: 1,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: "pass1",
+ },
+ {
+ document: `<input id="u1" value="">
+ <input id="p1" type=password value="">
+ <input id="p2" type=password value="">
+ <input id="p3" type=password value="">`,
+ selectorValues: {
+ "#u1": "user1",
+ "#p1": "pass1",
+ "#p2": "pass2",
+ "#p3": "pass2",
+ },
+
+ expectedFormsCount: 1,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: "pass1",
+ },
+ {
+ // Since there are two FormLikes to auto-submit in this case we mark
+ // one FormLike's password fields with a magic "ignore-form-submission"
+ // value so we can just focus on the other form. We then repeat the testcase
+ // below with the other FormLike ignored.
+ document: `<input id="u1" value="">
+ <input type=password id="p1" value="" form="form1">
+ <input type=password id="p2" value="">
+ <form id="form1">
+ <input id="u2" value="">
+ <input id="p3" type=password value="">
+ </form>`,
+ selectorValues: {
+ "#u1": "user1",
+ "#p1": "ignore-form-submission",
+ "#p2": "pass1",
+ "#u2": "user3",
+ "#p3": "ignore-form-submission",
+ },
+ expectedFormsCount: 2,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: "user1",
+ newPasswordFieldValue: "pass1",
+ oldPasswordFieldValue: null,
+ },
+ { // Same as above but with the other form ignored.
+ document: `<input id="u1" value="">
+ <input id="p1" type=password value="" form="form1">
+ <input id="p2" type=password value="">
+ <form id="form1">
+ <input id="u2" value="">
+ <input id="p3" type=password value="">
+ </form>`,
+ selectorValues: {
+ "#u1": "user1",
+ "#p1": "pass2",
+ "#p2": "ignore-form-submission",
+ "#u2": "user3",
+ "#p3": "pass2",
+ },
+
+ expectedFormsCount: 2,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: null,
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: null,
+ },
+ {
+ document: `<!-- recipe field override -->
+ <input name="recipeuname" value="">
+ <input id="u1" value="">
+ <input id="p1" type=password value="">
+ <input name="recipepword" type=password value="">`,
+ selectorValues: {
+ "[name='recipeuname']": "username from recipe",
+ "#u1": "default field username",
+ "#p1": "pass1",
+ "[name='recipepword']": "pass2",
+ },
+
+ expectedFormsCount: 1,
+ origin: DEFAULT_ORIGIN,
+ formActionOrigin: DEFAULT_ORIGIN,
+ usernameFieldValue: "username from recipe",
+ newPasswordFieldValue: "pass2",
+ oldPasswordFieldValue: null,
+ },
+];
+
+function filterFormSubmissions({ origin, data }) {
+ return data.newPasswordField.value != "ignore-form-submission";
+}
+
+async function testFormlesSubmitNavigation(tc, testDoc, scriptName) {
+
+ let loginFrame = document.getElementById("loginFrame");
+ let loadedPromise = new Promise((resolve) => {
+ loginFrame.addEventListener("load", function() {
+ resolve();
+ }, {once: true});
+ });
+ loginFrame.src = DEFAULT_ORIGIN + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html";
+ await loadedPromise;
+
+ let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document;
+
+ let formsProcessed = promiseFormsProcessed(tc.expectedFormsCount);
+ // eslint-disable-next-line no-unsanitized/property
+ frameDoc.documentElement.innerHTML = testDoc;
+ await formsProcessed;
+ // We eliminate no user input as a reason for not capturing by modifying the value
+ setUserInputValues(frameDoc.documentElement, tc.selectorValues)
+
+ let submitProcessed = getSubmitMessage(filterFormSubmissions);
+ info("Running " + scriptName + " script to cause a submission");
+ frameDoc.defaultView.eval(SCRIPTS[scriptName]);
+
+ info("Waiting for formSubmissionProcsssed message");
+ let { origin, data } = await submitProcessed;
+ info("Got for formSubmissionProcsssed message");
+
+ // Check data sent via PasswordManager:onFormSubmit
+ is(origin, tc.origin, "Check origin");
+ is(data.formActionOrigin, tc.formActionOrigin, "Check formActionOrigin");
+
+ if (tc.usernameFieldValue === null) {
+ is(data.usernameField, tc.usernameFieldValue, "Check usernameField");
+ } else {
+ is(data.usernameField.value, tc.usernameFieldValue, "Check usernameField");
+ }
+
+ is(data.newPasswordField.value, tc.newPasswordFieldValue, "Check newPasswordFieldValue");
+
+ if (tc.oldPasswordFieldValue === null) {
+ is(data.oldPasswordField, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue");
+ } else {
+ is(data.oldPasswordField.value, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue");
+ }
+};
+
+let count = 0;
+for (let tc of TESTCASES) {
+ for (let scriptName of Object.keys(SCRIPTS)) {
+ for (let surroundDocumentWithForm of [false, true]) {
+ let testDoc = tc.document;
+ if (surroundDocumentWithForm) {
+ if (testDoc.includes("<form")) {
+ info("Skipping surroundDocumentWithForm case since document already contains a <form>");
+ continue;
+ }
+ testDoc = "<form>" + testDoc + "</form>";
+ }
+ let taskName = `testcase-${count}-${scriptName}${surroundDocumentWithForm ? '-formWrapped' : ''}`;
+ let tmp = {
+ async [taskName]() {
+ info("Starting testcase with script " + scriptName + " and " +
+ (surroundDocumentWithForm ? "a" : "no") + " form wrapper: " + JSON.stringify(tc));
+ await testFormlesSubmitNavigation(tc, testDoc, scriptName);
+ },
+ };
+ add_task(tmp[taskName]);
+ }
+ }
+ count++;
+}
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ <iframe id="loginFrame" src="http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html
new file mode 100644
index 0000000000..4b437ced1b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test no capturing of fields outside of a form due to navigation</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript">
+SimpleTest.requestFlakyTimeout("Testing that a message doesn't arrive");
+
+let loadPromise = new Promise(resolve => {
+ document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("loginFrame").addEventListener("load", (evt) => {
+ resolve();
+ });
+ });
+});
+
+function submissionProcessed(...args) {
+ ok(false, "No formSubmissionProcessed should occur in this test");
+ info("got: " + JSON.stringify(args));
+}
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.formlessCapture.enabled", true],
+ ],
+ });
+
+ info("Waiting for page and frame loads");
+ await loadPromise;
+
+ PWMGR_COMMON_PARENT.addMessageListener("formSubmissionProcessed", submissionProcessed);
+
+ SimpleTest.registerCleanupFunction(() => {
+ PWMGR_COMMON_PARENT.removeMessageListener("formSubmissionProcessed", submissionProcessed);
+ });
+});
+
+const DEFAULT_ORIGIN = "http://test1.mochi.test:8888";
+const SCRIPTS = {
+ PUSHSTATE: `history.pushState({}, "Pushed state", "?pushed");`,
+ WINDOW_LOCATION: `window.location = "data:text/html;charset=utf-8,window.location";`,
+ WINDOW_LOCATION_RELOAD: `window.location.reload();`,
+ HISTORY_BACK: `history.back();`,
+ HISTORY_GO_MINUS1: `history.go(-1);`,
+};
+const TESTCASES = [
+ // Begin test cases that shouldn't trigger capture.
+ {
+ // Empty password field in a form
+ document: `<form><input type=password value="xxx"></form>`,
+ selectorValues: {
+ "[type=password]": "",
+ },
+ },
+ {
+ // Empty password field
+ document: `<input type=password value="">`,
+ selectorValues: {
+ "[type=password]": "",
+ },
+ },
+ {
+ // Test with an input that would normally be captured but with SCRIPTS that
+ // shouldn't trigger capture.
+ document: `<input type=password value="">`,
+ selectorValues: {
+ "[type=password]": "pass2",
+ },
+ wouldCapture: true,
+ },
+ {
+ // Test with an input that would normally be captured but with SCRIPTS that
+ // shouldn't trigger capture.
+ document: `<form><input type=password value=""></form>`,
+ selectorValues: {
+ "[type=password]": "pass2",
+ },
+ wouldCapture: true,
+ },
+];
+
+async function testFormlesSubmitNavigationNegative(tc, scriptName) {
+ let loginFrame = document.getElementById("loginFrame");
+ let waitTime;
+ let android = navigator.appVersion.includes("Android");
+ if (android) {
+ // intermittent failures on Android Debug at 5 seconds
+ waitTime = 10000;
+ } else {
+ waitTime = 5000;
+ }
+
+ let loadedPromise = new Promise((resolve) => {
+ loginFrame.addEventListener("load", function() {
+ resolve();
+ }, {once: true});
+ });
+ loginFrame.src = DEFAULT_ORIGIN + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html";
+ await loadedPromise;
+
+ let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document;
+ // eslint-disable-next-line no-unsanitized/property
+ frameDoc.documentElement.innerHTML = tc.document;
+ // We eliminate no user input as a reason for not capturing by modifying the value
+ setUserInputValues(frameDoc.documentElement, tc.selectorValues);
+
+ info("Running " + scriptName + " script to check for a submission");
+ frameDoc.defaultView.eval(SCRIPTS[scriptName]);
+
+ // Wait to see if the promise above resolves.
+ await new Promise(resolve => setTimeout(resolve, waitTime));
+ ok(true, "Done waiting for captures");
+}
+
+let count = 0;
+for (let tc of TESTCASES) {
+ for (let scriptName of Object.keys(SCRIPTS)) {
+ if (tc.wouldCapture && ["PUSHSTATE", "WINDOW_LOCATION"].includes(scriptName)) {
+ // Don't run scripts that should actually capture for this testcase.
+ continue;
+ }
+ let taskName = `testcase-${count}-${scriptName}`;
+ let tmp = {
+ async [taskName]() {
+ info("Starting testcase with script " + scriptName + ": " + JSON.stringify(tc));
+ await testFormlesSubmitNavigationNegative(tc, scriptName);
+ },
+ };
+ add_task(tmp[taskName]);
+ }
+}
+
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ <iframe id="loginFrame" src="http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_input_events.html b/toolkit/components/passwordmgr/test/mochitest/test_input_events.html
new file mode 100644
index 0000000000..2560c212d8
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_input_events.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for input events in Login Manager</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: input events should fire.
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script>
+ add_setup(async () => {
+ await setStoredLoginsAsync(
+ [location.origin, location.origin, null, "testuser", "testpass", "uname", "pword"]
+ );
+ })
+
+ add_task(async function username_events() {
+ return new Promise(resolve => {
+ let inputFired = false;
+ const form = createLoginForm();
+ form.uname.oninput = e => {
+ is(e.target.value, "testuser", "Should get 'testuser' as value in input event");
+ inputFired = true;
+ };
+ form.uname.onchange = e => {
+ ok(inputFired, "Should get input event before change event for username field.");
+ is(e.target.value, "testuser", "Should get 'testuser' as value in change event");
+ resolve();
+ };
+ })
+ })
+
+ add_task(async function password_events() {
+ return new Promise(resolve => {
+ let inputFired = false;
+ const form = createLoginForm();
+ form.pword.oninput = e => {
+ is(e.target.value, "testpass", "Should get 'testpass' as value in input event");
+ inputFired = true;
+ };
+ form.pword.onchange = e => {
+ ok(inputFired, "Should get input event before change event for password field.");
+ is(e.target.value, "testpass", "Should get 'testpass' as value in change event");
+ resolve();
+ };
+ })
+ })
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html b/toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html
new file mode 100644
index 0000000000..c6e378a516
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for input events in Login Manager when username/password are filled in already</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="onNewEvent(event)">
+Login Manager test: input events should fire.
+
+<script>
+runChecksAfterCommonInit();
+
+SimpleTest.requestFlakyTimeout("untriaged");
+
+/** Test for Login Manager: form fill when form is already filled, should not get input events. **/
+
+var onloadFired = false;
+
+function onNewEvent(e) {
+ console.error("Got " + e.type + " event.");
+ if (e.type == "load") {
+ onloadFired = true;
+ getFormElementByName(1, "uname").focus();
+ sendKey("Tab");
+ } else {
+ ok(false, "Got an input event for " + e.target.name + " field, which shouldn't happen.");
+ }
+}
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+
+ <form id="form1" action="formtest.js">
+ <p>This is form 1.</p>
+ <input type="text" name="uname" oninput="onNewEvent(event)" value="testuser">
+ <input type="password" name="pword" oninput="onNewEvent(event)" onfocus="setTimeout(function() { SimpleTest.finish() }, 1000);" value="testpass">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html
new file mode 100644
index 0000000000..8fcb9df6f6
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic login, contextual inscure password warning without saved logins</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: contextual inscure password warning without saved logins
+
+<script>
+let chromeScript = runChecksAfterCommonInit();
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+
+ <form id="form1" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: contextual insecure password warning without saved logins. **/
+
+let uname = getFormElementByName(1, "uname");
+let pword = getFormElementByName(1, "pword");
+
+// Restore the form to the default state.
+function restoreForm() {
+ uname.value = "";
+ pword.value = "";
+ uname.focus();
+}
+
+function spinEventLoop() {
+ return Promise.resolve();
+}
+
+add_setup(async () => {
+ listenForUnexpectedPopupShown();
+});
+
+add_task(async function test_form1_initial_empty() {
+ await SimpleTest.promiseFocus(window);
+
+ // Make sure initial form is empty.
+ checkLoginForm(uname, "", pword, "");
+ let popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+add_task(async function test_form1_warning_entry() {
+ await SimpleTest.promiseFocus(window);
+ // Trigger autocomplete popup
+ restoreForm();
+ await popupBy();
+
+ let popupState = await getPopupState();
+ is(popupState.open, true, "Check popup is opened");
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ let acEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsAC, clear: true });
+ is(acEvents.length, 1, "One autocomplete event");
+ checkACTelemetryEvent(acEvents[0], uname, {
+ "hadPrevious": "0",
+ "insecureWarning": "1",
+ "loginsFooter": "1"
+ });
+
+ synthesizeKey("KEY_ArrowDown"); // select insecure warning
+ checkLoginForm(uname, "", pword, ""); // value shouldn't update just by selecting
+ synthesizeKey("KEY_Enter");
+ await spinEventLoop(); // let focus happen
+ checkLoginForm(uname, "", pword, "");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_maxlength.html b/toolkit/components/passwordmgr/test/mochitest/test_maxlength.html
new file mode 100644
index 0000000000..a61812f6d3
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_maxlength.html
@@ -0,0 +1,144 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for maxlength attributes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: Bug 391514
+<script>
+gTestDependsOnDeprecatedLogin = true;
+runChecksAfterCommonInit(() => startTest());
+
+let DEFAULT_ORIGIN = window.location.origin;
+</script>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/* Test for Login Manager: 391514 (Login Manager gets confused with
+ * password/PIN on usaa.com)
+ */
+
+async function startTest() {
+ let win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+ await loadFormIntoWindow(DEFAULT_ORIGIN, `
+ <!-- normal form. -->
+ <form id="form1" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- limited username -->
+ <form id="form2" action="formtest.js">
+ <input type="text" name="uname" maxlength="4">
+ <input type="password" name="pword">
+ </form>
+
+ <!-- limited password -->
+ <form id="form3" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword" maxlength="4">
+ </form>
+
+ <!-- limited username and password -->
+ <form id="form4" action="formtest.js">
+ <input type="text" name="uname" maxlength="4">
+ <input type="password" name="pword" maxlength="4">
+ </form>
+
+
+ <!-- limited username -->
+ <form id="form5" action="formtest.js">
+ <input type="text" name="uname" maxlength="0">
+ <input type="password" name="pword">
+ </form>
+
+ <!-- limited password -->
+ <form id="form6" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword" maxlength="0">
+ </form>
+
+ <!-- limited username and password -->
+ <form id="form7" action="formtest.js">
+ <input type="text" name="uname" maxlength="0">
+ <input type="password" name="pword" maxlength="0">
+ </form>
+
+
+ <!-- limited, but ok, username -->
+ <form id="form8" action="formtest.js">
+ <input type="text" name="uname" maxlength="999">
+ <input type="password" name="pword">
+ </form>
+
+ <!-- limited, but ok, password -->
+ <form id="form9" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword" maxlength="999">
+ </form>
+
+ <!-- limited, but ok, username and password -->
+ <form id="form10" action="formtest.js">
+ <input type="text" name="uname" maxlength="999">
+ <input type="password" name="pword" maxlength="999">
+ </form>
+
+
+ <!-- limited, but ok, username -->
+ <!-- (note that filled values are exactly 8 characters) -->
+ <form id="form11" action="formtest.js">
+ <input type="text" name="uname" maxlength="8">
+ <input type="password" name="pword">
+ </form>
+
+ <!-- limited, but ok, password -->
+ <!-- (note that filled values are exactly 8 characters) -->
+ <form id="form12" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword" maxlength="8">
+ </form>
+
+ <!-- limited, but ok, username and password -->
+ <!-- (note that filled values are exactly 8 characters) -->
+ <form id="form13" action="formtest.js">
+ <input type="text" name="uname" maxlength="8">
+ <input type="password" name="pword" maxlength="8">
+ </form>`, win, 13);
+
+ var i;
+
+ await checkLoginFormInFrameWithElementValues(win, 1, "testuser", "testpass");
+
+ for (i = 2; i < 8; i++) {
+ await checkLoginFormInFrameWithElementValues(win, i, "", "");
+ }
+
+ for (i = 8; i < 14; i++) {
+ await checkLoginFormInFrameWithElementValues(win, i, "testuser", "testpass");
+ }
+
+ // Note that tests 11-13 are limited to exactly the expected value.
+ // Assert this lest someone change the login we're testing with.
+ await SpecialPowers.spawn(win, [11, 8], (formNum, length) => {
+ let form = this.content.document.getElementById(`form${formNum}`);
+ let field = form.querySelector("[name='uname']");
+ is(field.value.length, length, "asserting test assumption is valid.");
+ });
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_munged_values.html b/toolkit/components/passwordmgr/test/mochitest/test_munged_values.html
new file mode 100644
index 0000000000..5afac7348b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_munged_values.html
@@ -0,0 +1,364 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test handling of possibly-manipulated username values</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript">
+const readyPromise = registerRunTests();
+
+const DEFAULT_ORIGIN = window.location.origin;
+
+function removeAllUserFacingLoginsInParent() {
+ runInParent(function removeAllUserFacingLogins() {
+ Services.logins.removeAllUserFacingLogins();
+ });
+}
+
+async function add2logins() {
+ removeAllUserFacingLoginsInParent();
+ await addLoginsInParent([DEFAULT_ORIGIN, DEFAULT_ORIGIN, null, "real••••user", "pass1", "", ""], [DEFAULT_ORIGIN, DEFAULT_ORIGIN, null, "user2", "pass2", "", ""]);
+}
+
+async function addSingleLogin() {
+ removeAllUserFacingLoginsInParent();
+ await addLoginsInParent([DEFAULT_ORIGIN, DEFAULT_ORIGIN, null, "real••••user", "pass1", "", ""])
+}
+
+/**
+ * For any test including the character "!", generate a version of that test for every known munge
+ * character.
+ **/
+ function generateTestCases(test) {
+ const MUNGE_CHARS = ["*", ".", "•"];
+
+ const nothingToReplace = Object.values(test).every(value => typeof value !== "string" || !value.includes("!"));
+ if (nothingToReplace) {
+ return test;
+ };
+
+ return MUNGE_CHARS.map(char => {
+ const newTest = {};
+ for (const [propName, val] of Object.entries(test)) {
+ if (typeof val === "string") {
+ newTest[propName] = val.replace(/!/g, char);
+ } else {
+ newTest[propName] = val;
+ }
+ };
+ return newTest;
+})};
+
+const loadPromise = new Promise(resolve => {
+ document.addEventListener("DOMContentLoaded", () => {
+ resolve();
+ });
+});
+
+add_setup(async () => {
+ info("Waiting for setup and page and window loads");
+ await readyPromise;
+ await loadPromise;
+});
+
+add_task(async function test_new_logins() {
+ const TEST_CASES = [
+ // ! is replaced with characters commonly used for munging
+ {
+ testName: "test_middle!MaskedUsername",
+ username: "so!!!ne",
+ expected: null,
+ },
+ {
+ testName: "test_start!MaskedUsername",
+ username: "!!!eone",
+ expected: null,
+ },
+ {
+ testName: "test_end!MaskedUsername",
+ username: "some!!!",
+ expected: null,
+ },
+ {
+ testName: "test_ok!Username",
+ username: "obelixand!",
+ expected: "obelixand!",
+ },
+ {
+ testName: "test_ok!Username2",
+ username: "!!username!!",
+ expected: "!!username!!",
+ },
+ {
+ // We should only consider a username munged if it repeats of the same character
+ testName: "test_combinedMungeCharacters",
+ username: "*.•*.•*.•*.•*.•*.•",
+ expected: "*.•*.•*.•*.•*.•*.•",
+ },
+].flatMap(generateTestCases);
+ for (const tc of TEST_CASES) {
+ info("Starting testcase: " + JSON.stringify(tc));
+ // Create a new window for each test case, because if we instead try to use
+ // the same window and change the page using window.location, that will trigger
+ // an onLocationChange event, which can trigger unwanted FormSubmit outside of
+ // clicking the submit button in each test document.
+ const win = window.open("about:blank");
+ const html = `
+ <form id="form1" onsubmit="return false;">
+ <input type="text" name="uname" value="${tc.username}">
+ <input type="password" name="pword" value="thepassword">
+ <button type="submit" id="submitBtn">Submit</button>
+ </form>`;
+ await loadFormIntoWindow(DEFAULT_ORIGIN, html, win);
+ await SpecialPowers.spawn(win, [html], function(contentHtml) {
+ const doc = this.content.document;
+ for (const field of doc.querySelectorAll("input")) {
+ const actualValue = field.value;
+ field.value = "";
+ SpecialPowers.wrap(field).setUserInput(actualValue);
+ }
+ });
+ await SpecialPowers.spawn(win, [tc], function(testcase) {
+ const doc = this.content.document;
+ Assert.equal(doc.querySelector("[name='uname']").value, testcase.username, "Checking for filled username");
+ });
+
+ // Check data sent via PasswordManager:onFormSubmit
+ const processedPromise = getSubmitMessage();
+ await SpecialPowers.spawn(win, [], function() {
+ this.content.document.getElementById("submitBtn").click();
+ });
+
+ const { data } = await processedPromise;
+ info("Got submitted result: " + JSON.stringify(data));
+
+ if (tc.expected === null) {
+ is(data.usernameField, tc.expected, "Check usernameField");
+ } else {
+ is(data.usernameField.value, tc.expected, "Check usernameField");
+ }
+
+ win.close();
+ await SimpleTest.promiseFocus(window);
+ }
+});
+
+add_task(async function test_no_save_dialog_when_password_is_fully_munged() {
+ const TEST_CASES = [
+ {
+ testName: "test_passFullyMungedBy!",
+ password: "!!!!!!!!!",
+ shouldShowPrompt: false,
+ },
+ {
+ testName: "test_passStartsMungedBy!",
+ password: "!!!!!!!butThenAPassword",
+ shouldShowPrompt: true,
+ },
+ {
+ testName: "test_passEndsMungedBy!",
+ password: "aRealPasswordAndThen!!!!!!!",
+ shouldShowPrompt: true,
+ },
+ {
+ testName: "test_passMostlyMungedBy!",
+ password: "!!!a!!!!",
+ shouldShowPrompt: true,
+ },
+ {
+ testName: "test_combinedMungedCharacters",
+ password: "*.•*.•*.•*.•",
+ shouldShowPrompt: true,
+ },
+ ].flatMap(generateTestCases);
+
+ for (const tc of TEST_CASES) {
+ info("Starting testcase: " + tc.testName)
+ // Create a new window for each test case, because if we instead try to use
+ // the same window and change the page using window.location, that will trigger
+ // an onLocationChange event, which can trigger unwanted FormSubmit outside of
+ // clicking the submit button in each test document.
+ const win = window.open("about:blank");
+ const html = `
+ <form id="form1" onsubmit="return false;">
+ <input type="text" name="uname" value="username">
+ <input type="password" name="pword" value="${tc.password}">
+ <button type="submit" id="submitBtn">Submit</button>
+ </form>`;
+ await loadFormIntoWindow(DEFAULT_ORIGIN, html, win);
+ await SpecialPowers.spawn(win, [html], function(contentHtml) {
+ const doc = this.content.document;
+ for (const field of doc.querySelectorAll("input")) {
+ const actualValue = field.value;
+ field.value = "";
+ SpecialPowers.wrap(field).setUserInput(actualValue);
+ }
+ });
+ await SpecialPowers.spawn(win, [tc], function(testcase) {
+ const doc = this.content.document;
+ Assert.equal(doc.querySelector("[name='pword']").value, testcase.password, "Checking for filled password");
+ });
+
+ const formSubmitListener = SpecialPowers.spawn(win, [], function() {
+ return new Promise(resolve => {
+ this.content.windowRoot.addEventListener(
+ "PasswordManager:ShowDoorhanger",
+ event => {
+ info(`PasswordManager:ShowDoorhanger called. Event: ${JSON.stringify(event)}`);
+ resolve(event.detail.messageSent);
+ }
+ );
+ });
+ });
+
+ await SpecialPowers.spawn(win, [], function() {
+ this.content.document.getElementById("submitBtn").click();
+ });
+
+ const dialogRequested = await formSubmitListener;
+
+ is(dialogRequested, tc.shouldShowPrompt, "Verify 'show save/update prompt' message sent to parent process");
+
+ win.close();
+ await SimpleTest.promiseFocus(window);
+ }
+});
+
+add_task(async function test_no_autofill_munged_username_matching_password() {
+ // run this test with 2 matching logins from this origin so we don't autofill
+ await add2logins();
+ const allLogins = await LoginManager.getAllLogins();
+ const matchingLogins = Array.prototype.filter.call(allLogins, l => l.origin == DEFAULT_ORIGIN);
+ is(matchingLogins.length, 2, "Expected number of matching logins");
+
+ const bulletLogin = matchingLogins.find(l => l.username == "real••••user");
+ ok(bulletLogin, "Found the real••••user login");
+
+ const timesUsed = bulletLogin.timesUsed;
+ const guid = bulletLogin.guid;
+
+ const win = window.open("about:blank");
+ const html =
+ `<form id="form1" onsubmit="return false;">
+ <input type="text" name="uname" value="">
+ <input type="password" name="pword" value="">
+ <button type="submit" id="submitBtn">Submit</button>
+ </form>`;
+ await loadFormIntoWindow(DEFAULT_ORIGIN, html, win);
+ await SpecialPowers.spawn(win, [html], function(contentHtml) {
+ const doc = this.content.document;
+ for (const field of doc.querySelectorAll("input")) {
+ const actualValue = field.value;
+ field.value = "";
+ SpecialPowers.wrap(field).setUserInput(actualValue);
+ }
+ });
+ await SpecialPowers.spawn(win, [], function() {
+ const doc = this.content.document;
+ Assert.equal(doc.querySelector("[name='uname']").value, "", "Check username didn't get autofilled");
+ SpecialPowers.wrap(doc.querySelector("[name='uname']")).setUserInput("real••••user");
+ SpecialPowers.wrap(doc.querySelector("[name='pword']")).setUserInput("pass1");
+ });
+
+ // we shouldn't get the save password doorhanger...
+ const popupShownPromise = noPopupBy();
+
+ // Check data sent via PasswordManager:onFormSubmit
+ const processedPromise = getSubmitMessage();
+ await SpecialPowers.spawn(win, [], function() {
+ this.content.document.getElementById("submitBtn").click();
+ });
+
+ const { data } = await processedPromise;
+ info("Got submitted result: " + JSON.stringify(data));
+
+ is(data.usernameField, null, "Check usernameField");
+
+ const updatedLogins = await LoginManager.getAllLogins();
+ const updatedLogin = Array.prototype.find.call(updatedLogins, l => l.guid == guid);
+ ok(updatedLogin, "Got the login via guid");
+ is(updatedLogin.timesUsed, timesUsed + 1, "timesUsed was incremented");
+
+ await popupShownPromise;
+
+ win.close();
+ await SimpleTest.promiseFocus(window);
+});
+
+
+add_task(async function test_autofill_munged_username_matching_password() {
+ // only a single matching login so we autofill the username
+ await addSingleLogin();
+
+ const allLogins = await LoginManager.getAllLogins();
+ const matchingLogins = Array.prototype.filter.call(allLogins, l => l.origin == DEFAULT_ORIGIN);
+ is(matchingLogins.length, 1, "Expected number of matching logins");
+
+ info("matched login: " + matchingLogins[0].username);
+ const bulletLogin = matchingLogins.find(l => l.username == "real••••user");
+ ok(bulletLogin, "Found the real••••user login");
+
+ const timesUsed = bulletLogin.timesUsed;
+ const guid = bulletLogin.guid;
+
+ const win = window.open("about:blank");
+ const html =
+ `<form id="form1" onsubmit="return false;">
+ <input type="text" name="uname" value="">
+ <input type="password" name="pword" value="">
+ <button type="submit" id="submitBtn">Submit</button>
+ </form>`;
+ await loadFormIntoWindow(DEFAULT_ORIGIN, html, win);
+ await SpecialPowers.spawn(win, [html], function(contentHtml) {
+ const doc = this.content.document;
+ for (const field of doc.querySelectorAll("input")) {
+ const actualValue = field.value;
+ field.value = "";
+ SpecialPowers.wrap(field).setUserInput(actualValue);
+ }
+ });
+ await SpecialPowers.spawn(win, [], function() {
+ const doc = this.content.document;
+ Assert.equal(doc.querySelector("[name='uname']").value, "real••••user", "Check username did get autofilled");
+ doc.querySelector("[name='pword']").setUserInput("pass1");
+ });
+
+ // we shouldn't get the save/update password doorhanger as it didn't change
+ const popupShownPromise = noPopupBy();
+
+ // Check data sent via PasswordManager:onFormSubmit
+ const processedPromise = getSubmitMessage();
+ await SpecialPowers.spawn(win, [], function() {
+ this.content.document.getElementById("submitBtn").click();
+ });
+
+ const { data } = await processedPromise;
+ info("Got submitted result: " + JSON.stringify(data));
+
+ is(data.usernameField, null, "Check usernameField");
+
+ const updatedLogins = await LoginManager.getAllLogins();
+ const updatedLogin = Array.prototype.find.call(updatedLogins, l => l.guid == guid);
+ ok(updatedLogin, "Got the login via guid");
+ is(updatedLogin.timesUsed, timesUsed + 1, "timesUsed was incremented");
+
+ await popupShownPromise;
+
+ win.close();
+ await SimpleTest.promiseFocus(window);
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_one_doorhanger_per_un_pw.html b/toolkit/components/passwordmgr/test/mochitest/test_one_doorhanger_per_un_pw.html
new file mode 100644
index 0000000000..4d8dfd1fee
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_one_doorhanger_per_un_pw.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Don't repeatedly prompt to save the same username and password
+ combination in the same document</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+ let chromeScript = runChecksAfterCommonInit();
+
+ SimpleTest.requestFlakyTimeout("Giving a chance for the unexpected popupshown to occur");
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <form id="form1" onsubmit="return false;">
+ <input type="text" name="uname" id="ufield">
+ <input type="password" name="pword" id="pfield">
+ <button type="submit" id="submitBtn">Submit</button>
+ </form>
+</div>
+
+<pre id="test"></pre>
+<script>
+ /** Test for Login Manager: Don't repeatedly prompt to save the
+ same username and password combination in the same document **/
+
+ add_task(async function test_prompt_does_not_reappear() {
+ let username = document.getElementById("ufield");
+ let password = document.getElementById("pfield");
+ let submitButton = document.getElementById("submitBtn");
+
+ SpecialPowers.wrap(username).setUserInput("user");
+ SpecialPowers.wrap(password).setUserInput("pass");
+
+ let processedPromise = getSubmitMessage();
+ let promptShownPromise = promisePromptShown("passwordmgr-prompt-save");
+ submitButton.click();
+ await processedPromise;
+ await promptShownPromise;
+
+ is(username.value, "user", "Checking for filled username");
+ is(password.value, "pass", "Checking for filled password");
+
+ let promptShown = false;
+ promptShownPromise = promisePromptShown("passwordmgr-prompt-save").then(value => {
+ promptShown = true;
+ });
+ submitButton.click();
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ ok(!promptShown, "Prompt is not shown for the same login values a second time");
+ });
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_onsubmit_value_change.html b/toolkit/components/passwordmgr/test/mochitest/test_onsubmit_value_change.html
new file mode 100644
index 0000000000..f23940d34b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_onsubmit_value_change.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test input value change right after onsubmit event</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: input value change right after onsubmit event
+
+<script>
+ let chromeScript = runChecksAfterCommonInit();
+
+ function getSubmitMessage() {
+ info("getSubmitMessage");
+ return new Promise((resolve, reject) => {
+ chromeScript.addMessageListener("formSubmissionProcessed", function processed(...args) {
+ info("got formSubmissionProcessed");
+ chromeScript.removeMessageListener("formSubmissionProcessed", processed);
+ resolve(args[0]);
+ });
+ });
+ }
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+ <form id="form1" action="formTest.js" onsubmit="return false;">
+ <input type="text" name="uname" id="ufield">
+ <input type="password" name="pword" id="pfield">
+ <button type="submit" id="submitBtn">Submit</button>
+ </form>
+
+</div>
+
+<pre id="test"></pre>
+<script>
+ /** Test for Login Manager: input value change right after onsubmit event **/
+ add_task(async function checkFormValues() {
+ SpecialPowers.wrap(document.getElementById("ufield")).setUserInput("testuser");
+ SpecialPowers.wrap(document.getElementById("pfield")).setUserInput("testpass");
+ is(getFormElementByName(1, "uname").value, "testuser", "Checking for filled username");
+ is(getFormElementByName(1, "pword").value, "testpass", "Checking for filled password");
+
+ document.getElementById("form1").addEventListener("submit", () => {
+ // deliberately assign to .value rather than setUserInput:
+ // the scenario under test here is that script is changing/populating
+ // fields after the user has clicked the submit button
+ document.getElementById("ufield").value = "newuser";
+ document.getElementById("pfield").value = "newpass";
+ }, true);
+
+ document.getElementById("form1").addEventListener("submit", (e) => e.preventDefault());
+
+ let processedPromise = getSubmitMessage();
+
+ let button = document.getElementById("submitBtn");
+ button.click();
+
+ let { data } = await processedPromise;
+ is(data.usernameField.value, "testuser", "Should have registered \"testuser\" for username");
+ is(data.newPasswordField.value, "testpass", "Should have registered \"testpass\" for username");
+ });
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html b/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html
new file mode 100644
index 0000000000..2db691b1bf
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html
@@ -0,0 +1,198 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test basic login autocomplete</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: multiple login autocomplete
+
+<script>
+var chromeScript = runChecksAfterCommonInit();
+
+const INSECURE_WARNING_TEXT = "This connection is not secure. Logins entered here could be compromised. Learn More";
+
+let origin = window.location.origin;
+addLoginsInParent(
+ // login0 has no username, so should be filtered out from the autocomplete list.
+ [origin, "http://autocomplete:8888", null, "", "user0pass", "", "pword"],
+ [origin, "http://autocomplete:8888", null, "tempuser1", "temppass1", "uname", "pword"],
+ [origin, "http://autocomplete:8888", null, "testuser2", "testpass2", "uname", "pword"],
+ [origin, "http://autocomplete:8888", null, "testuser3", "testpass3", "uname", "pword"],
+ [origin, "http://autocomplete:8888", null, "zzzuser4", "zzzpass4", "uname", "pword"]);
+</script>
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+
+ <!-- form1 tests multiple matching logins -->
+ <form id="form1" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <h1>Sign in</h1>
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form2" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" readonly="true">
+ <button type="submit">Submit</button>
+ </form>
+
+ <form id="form3" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" disabled="true">
+ <button type="submit">Submit</button>
+ </form>
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/** Test for Login Manager: multiple login autocomplete. **/
+
+let uname = getFormElementByName(1, "uname");
+let pword = getFormElementByName(1, "pword");
+
+// Restore the form to the default state.
+async function reinitializeForm(index) {
+ // Using innerHTML is for creating the autocomplete popup again, so the
+ // preference value will be applied to the constructor of
+ // LoginAutoCompleteResult.
+ let form = document.getElementById("form" + index);
+ let temp = form.innerHTML;
+ form.innerHTML = "";
+ // eslint-disable-next-line no-unsanitized/property
+ form.innerHTML = temp;
+
+ await new Promise(resolve => {
+ let observer = SpecialPowers.wrapCallback(() => {
+ SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
+ resolve();
+ });
+ SpecialPowers.addObserver(observer, "passwordmgr-processed-form");
+ });
+
+ await SimpleTest.promiseFocus(window);
+
+ uname = getFormElementByName(index, "uname");
+ pword = getFormElementByName(index, "pword");
+ uname.value = "";
+ pword.value = "";
+ pword.focus();
+}
+
+function generateDateString(date) {
+ let dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined,
+ { dateStyle: "medium" });
+ return dateAndTimeFormatter.format(date);
+}
+
+const DATE_NOW_STRING = generateDateString(new Date());
+
+// Check for expected username/password in form.
+function checkACFormPasswordField(expectedPassword) {
+ var formID = uname.parentNode.id;
+ is(pword.value, expectedPassword, "Checking " + formID + " password is: " + JSON.stringify(expectedPassword));
+}
+
+async function userOpenAutocompleteOnForm1(autoFillInsecureForms) {
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["signon.autofillForms.http", autoFillInsecureForms],
+ ]});
+ await reinitializeForm(1);
+ const autocompleteItems = await popupBy();
+
+ const popupState = await getPopupState();
+ is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+ const expectedMenuItems = [INSECURE_WARNING_TEXT,
+ "No username (" + DATE_NOW_STRING + ")",
+ "tempuser1",
+ "testuser2",
+ "testuser3",
+ "zzzuser4"];
+ checkAutoCompleteResults(autocompleteItems, expectedMenuItems, "mochi.test", "Check all menuitems are displayed correctly.");
+}
+
+async function userPressedDown_passwordIs(value) {
+ synthesizeKey("KEY_ArrowDown");
+ await Promise.resolve(); // let focus happen
+ checkACFormPasswordField(value);
+}
+
+async function userPressedEnter_passwordIs(value) {
+ synthesizeKey("KEY_Enter");
+ await Promise.resolve(); // let focus happen
+ checkACFormPasswordField(value);
+}
+
+async function noPopupOnForm(formIndex, reason) {
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["signon.autofillForms.http", true],
+ ]});
+ await reinitializeForm(formIndex);
+
+ // Trigger autocomplete popup
+ synthesizeKey("KEY_ArrowDown"); // open
+ let popupState = await getPopupState();
+ is(popupState.open, false, reason);
+}
+
+add_setup(async () => {
+ listenForUnexpectedPopupShown();
+});
+
+add_task(async function form1_initial_empty() {
+ await SimpleTest.promiseFocus(window);
+
+ // Make sure initial form is empty.
+ checkACFormPasswordField("");
+ let popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is initially closed");
+});
+
+add_task(async function noAutocompleteForReadonlyField() {
+ await noPopupOnForm(2, "Check popup is closed for a readonly field.");
+});
+
+add_task(async function noAutocompleteForDisabledField() {
+ await noPopupOnForm(3, "Check popup is closed for a disabled field.");
+});
+
+add_task(async function insecureAutoFill_EnterOnWarning() {
+ await userOpenAutocompleteOnForm1(true);
+ await userPressedDown_passwordIs("");
+ await userPressedEnter_passwordIs("");
+});
+
+add_task(async function insecureAutoFill_EnterOnLogin() {
+ await userOpenAutocompleteOnForm1(true);
+ await userPressedDown_passwordIs(""); // select insecure warning
+ await userPressedDown_passwordIs(""); // select login
+ await userPressedEnter_passwordIs("user0pass");
+});
+
+add_task(async function noInsecureAutoFill_EnterOnWarning() {
+ await userOpenAutocompleteOnForm1(false);
+ await userPressedDown_passwordIs(""); // select insecure warning
+ await userPressedEnter_passwordIs("");
+});
+
+add_task(async function noInsecureAutoFill_EnterOnLogin() {
+ await userOpenAutocompleteOnForm1(false);
+ await userPressedDown_passwordIs(""); // select insecure warning
+ await userPressedDown_passwordIs(""); // select login
+ await userPressedEnter_passwordIs("user0pass");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_password_length.html b/toolkit/components/passwordmgr/test/mochitest/test_password_length.html
new file mode 100644
index 0000000000..3edfc1a00a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_password_length.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test handling of different password length</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="application/javascript">
+const { LoginManagerChild } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/LoginManagerChild.jsm"
+);
+
+let readyPromise = registerRunTests();
+
+let loadPromise = new Promise(resolve => {
+ document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("loginFrame").addEventListener("load", (evt) => {
+ resolve();
+ });
+ });
+});
+
+async function loadFormIntoIframe(origin, html) {
+ let loginFrame = document.getElementById("loginFrame");
+ let loadedPromise = new Promise((resolve) => {
+ loginFrame.addEventListener("load", function() {
+ resolve();
+ }, {once: true});
+ });
+ let processedPromise = promiseFormsProcessed();
+ loginFrame.src = origin + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html";
+ await loadedPromise;
+
+ await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [html], function(contentHtml) {
+ // eslint-disable-next-line no-unsanitized/property
+ this.content.document.documentElement.innerHTML = contentHtml;
+ });
+
+ // Wait for the form to be processed before trying to submit.
+ await processedPromise;
+}
+
+add_setup(async () => {
+ info("Waiting for setup and page and frame loads");
+ await readyPromise;
+ await loadPromise;
+});
+
+const DEFAULT_ORIGIN = window.location.origin;
+const TESTCASES = [
+ {
+ testName: "test_control2PasswordFields",
+ pword1: "pass1",
+ pword2: "pass2",
+ expectedNewPassword: { value: "pass2" },
+ expectedOldPassword: { value: "pass1" },
+ },
+ {
+ testName: "test_1characterPassword",
+ pword1: "x",
+ pword2: "pass2",
+ expectedNewPassword: { value: "pass2" },
+ expectedOldPassword: null,
+ },
+ {
+ testName: "test_2characterPassword",
+ pword1: "xy",
+ pword2: "pass2",
+ expectedNewPassword: { value: "pass2" },
+ expectedOldPassword: { value: "xy" },
+ },
+ {
+ testName: "test_1characterNewPassword",
+ pword1: "pass1",
+ pword2: "x",
+ expectedNewPassword: { value: "pass1" },
+ expectedOldPassword: null,
+ },
+];
+
+/**
+ * @return {Promise} resolving when form submission was processed.
+ */
+function getSubmitMessage() {
+ return new Promise((resolve, reject) => {
+ PWMGR_COMMON_PARENT.addMessageListener("formSubmissionProcessed", function processed(...args) {
+ info("got formSubmissionProcessed");
+ PWMGR_COMMON_PARENT.removeMessageListener("formSubmissionProcessed", processed);
+ resolve(args[0]);
+ });
+ });
+}
+
+add_task(async function test_password_lengths() {
+ for (let tc of TESTCASES) {
+ info("Starting testcase: " + tc.testName + ", " + JSON.stringify([tc.pword1, tc.pword2]));
+ await loadFormIntoIframe(DEFAULT_ORIGIN, `<form id="form1" onsubmit="return false;">
+ <input type="text" name="uname" value="myname">
+ <input type="password" name="pword1" value="">
+ <input type="password" name="pword2" value="">
+ <button type="submit" id="submitBtn">Submit</button>
+ </form>`);
+
+ await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [tc], function(testcase) {
+ let doc = this.content.document;
+ Assert.equal(doc.querySelector("[name='uname']").value, "myname", "Checking for filled username");
+ doc.querySelector("[name='pword1']").setUserInput(testcase.pword1);
+ doc.querySelector("[name='pword2']").setUserInput(testcase.pword2);
+ });
+
+ // Check data sent via PasswordManager:onFormSubmit
+ let processedPromise = getSubmitMessage();
+ await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
+ this.content.document.getElementById("submitBtn").click();
+ });
+
+ let { data } = await processedPromise;
+ info("Got submitted result: " + JSON.stringify(data));
+
+ if (tc.expectedNewPassword === null) {
+ is(data.newPasswordField,
+ tc.expectedNewPassword, "Check expectedNewPassword is null");
+ } else {
+ is(data.newPasswordField.value,
+ tc.expectedNewPassword.value,
+ "Check that newPasswordField.value matches the expectedNewPassword.value");
+ }
+ if (tc.expectedOldPassword === null) {
+ is(data.oldPasswordField,
+ tc.expectedOldPassword, "Check expectedOldPassword is null");
+ } else {
+ is(data.oldPasswordField.value,
+ tc.expectedOldPassword.value,
+ "Check that oldPasswordField.value matches expectedOldPassword.value");
+ }
+ }
+});
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ <iframe id="loginFrame" src="/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html b/toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html
new file mode 100644
index 0000000000..fcaeb0e455
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html
@@ -0,0 +1,114 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test that passwords only get filled in type=password</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: Bug 242956
+<script>
+gTestDependsOnDeprecatedLogin = true;
+runChecksAfterCommonInit(() => startTest());
+
+let DEFAULT_ORIGIN = window.location.origin;
+</script>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: 242956 (Stored password is inserted into a
+ readable text input on a second page) **/
+
+// Make sure that pwmgr only puts passwords into type=password <input>s.
+// Might as well test the converse, too (username in password field).
+
+async function startTest() {
+ let win = window.open("about:blank");
+ SimpleTest.registerCleanupFunction(() => win.close());
+
+ // only 4 out of 7 forms are to be autofilled
+ await loadFormIntoWindow(DEFAULT_ORIGIN, `
+ <!-- pword is not a type=password input -->
+ <form id="form1" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="text" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- uname is not a type=text input -->
+ <form id="form2" action="formtest.js">
+ <input type="password" name="uname">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- two "pword" inputs, (text + password) -->
+ <form id="form3" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="text" name="pword">
+ <input type="password" name="qword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- same thing, different order -->
+ <form id="form4" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="password" name="pword">
+ <input type="text" name="qword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- uname is not a type=text input (try a checkbox just for variety) -->
+ <form id="form5" action="formtest.js">
+ <input type="checkbox" name="uname" value="">
+ <input type="password" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- pword is not a type=password input (try a checkbox just for variety) -->
+ <form id="form6" action="formtest.js">
+ <input type="text" name="uname">
+ <input type="checkbox" name="pword" value="">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>
+
+ <!-- pword is not a type=password input -->
+ <form id="form7" action="formtest.js">
+ <input type="text" name="uname" value="testuser">
+ <input type="text" name="pword">
+
+ <button type="submit">Submit</button>
+ <button type="reset"> Reset </button>
+ </form>`, win, 4);
+
+ await checkLoginFormInFrameWithElementValues(win, 1, "", "");
+ await checkLoginFormInFrameWithElementValues(win, 2, "testpass", "");
+ await checkLoginFormInFrameWithElementValues(win, 3, "", "testuser", "testpass");
+ await checkLoginFormInFrameWithElementValues(win, 4, "testuser", "testpass", "");
+ await checkLoginFormInFrameWithElementValues(win, 5, "", "testpass");
+ await checkLoginFormInFrameWithElementValues(win, 6, "", "");
+ await checkLoginFormInFrameWithElementValues(win, 7, "testuser", "");
+
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_primary_password.html b/toolkit/components/passwordmgr/test/mochitest/test_primary_password.html
new file mode 100644
index 0000000000..f8ffec57d0
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_primary_password.html
@@ -0,0 +1,298 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for primary password</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: primary password.
+
+<p id="display"></p>
+
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+"use strict";
+
+// Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
+modalType = Ci.nsIPrompt.MODAL_TYPE_WINDOW;
+
+const exampleCom = "https://example.com/tests/toolkit/components/passwordmgr/test/mochitest/";
+const exampleOrg = "https://example.org/tests/toolkit/components/passwordmgr/test/mochitest/";
+
+gTestDependsOnDeprecatedLogin = true;
+const chromeScript = runChecksAfterCommonInit();
+
+const win = window.open("about:blank");
+SimpleTest.registerCleanupFunction(() => win.close());
+
+add_setup(async () => {
+ await addLoginsInParent(
+ ["https://example.com", "https://example.com", null, "user1", "pass1", "uname", "pword"],
+ ["https://example.org", "https://example.org", null, "user2", "pass2", "uname", "pword"]
+ );
+ ok(await isLoggedIn(), "should be initially logged in (no PP)");
+ enablePrimaryPassword();
+ ok(!await isLoggedIn(), "should be logged out after setting PP");
+});
+
+add_task(async function test_1() {
+ // Trigger a MP prompt via the API
+ const state = {
+ msg: "Please enter your Primary Password.",
+ title: "the title",
+ textValue: "",
+ passValue: "",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "passField",
+ defButton: "button0",
+ };
+ const action = {
+ buttonClick: "ok",
+ passField: LoginTestUtils.primaryPassword.primaryPassword,
+ };
+ const promptDone = handlePrompt(state, action);
+
+ const logins = await LoginManager.getAllLogins();
+
+ await promptDone;
+ is(logins.length, 3, "expected number of logins");
+
+ ok(await isLoggedIn(), "should be logged in after MP prompt");
+ logoutPrimaryPassword();
+ ok(!await isLoggedIn(), "should be logged out");
+});
+
+add_task(async function test_2() {
+ // Try again but click cancel.
+ const state = {
+ msg: "Please enter your Primary Password.",
+ title: "the title",
+ textValue: "",
+ passValue: "",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "passField",
+ defButton: "button0",
+ };
+ const action = {
+ buttonClick: "cancel",
+ };
+ const promptDone = handlePrompt(state, action);
+
+ const logins = await LoginManager.getAllLogins().catch(() => {});
+ await promptDone;
+ is(logins, undefined, "shouldn't have gotten logins");
+ ok(!await isLoggedIn(), "should still be logged out");
+});
+
+add_task(async function test_3() {
+ const state = {
+ msg: "Please enter your Primary Password.",
+ title: "the title",
+ textValue: "",
+ passValue: "",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "passField",
+ defButton: "button0",
+ };
+ const action = {
+ buttonClick: "ok",
+ passField: LoginTestUtils.primaryPassword.primaryPassword,
+ };
+ const promptDone = handlePrompt(state, action);
+
+ const fillPromise = promiseFormsProcessed();
+
+ info("Load a single window to trigger a MP");
+ await SimpleTest.promiseFocus(win, true);
+ win.location = exampleCom + "subtst_primary_pass.html";
+
+ await promptDone;
+ info("promptDone");
+ await fillPromise;
+ info("filled");
+
+ // check contents of win fields
+
+ await SpecialPowers.spawn(win, [], function() {
+ const u = this.content.document.getElementById("userfield");
+ const p = this.content.document.getElementById("passfield");
+ Assert.equal(u.value, "user1", "checking expected user to have been filled in");
+ Assert.equal(p.value, "pass1", "checking expected pass to have been filled in");
+ u.value = "";
+ p.value = "";
+ });
+
+ ok(await isLoggedIn(), "should be logged in");
+ logoutPrimaryPassword();
+ ok(!await isLoggedIn(), "should be logged out");
+});
+
+add_task(async function test_4() {
+ const state = {
+ msg: "Please enter your Primary Password.",
+ title: "the title",
+ textValue: "",
+ passValue: "",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "passField",
+ defButton: "button0",
+ };
+ let action = {
+ buttonClick: "none",
+ };
+ const promptDone = handlePrompt(state, action);
+
+ // first part of loading 2 MP-triggering windows
+ await SimpleTest.promiseFocus(win);
+ win.location = exampleOrg + "subtst_primary_pass.html";
+ // The MP prompt is open but don't take any action yet.
+ await promptDone;
+
+ // check contents of win fields
+ await SpecialPowers.spawn(win, [], function() {
+ const u = this.content.document.getElementById("userfield");
+ const p = this.content.document.getElementById("passfield");
+ Assert.equal(u.value, "", "checking expected empty user");
+ Assert.equal(p.value, "", "checking expected empty pass");
+ });
+
+ ok(!await isLoggedIn(), "should be logged out");
+
+ // XXX check that there's 1 MP window open
+
+ // Load a second login form in an iframe
+ // This should detect that there's already a pending MP prompt, and not
+ // put up a second one.
+
+ // Since the Primary Password prompt is open, we can't focus another tab
+ // to load the second form. Instead, we load the same form into an iframe.
+ const url = exampleOrg + "subtst_primary_pass.html";
+ await SpecialPowers.spawn(win, [url], async function(urlF) {
+ const iframe = this.content.document.querySelector("iframe");
+ const loadPromise = new Promise(resolve => {
+ iframe.addEventListener("load", function onload() {
+ resolve();
+ }, { once: true });
+ });
+ // Use the same origin as the top level to ensure we would autofill
+ // if we could (we don't fill in cross-origin iframes).
+ iframe.src = urlF;
+ await loadPromise;
+ });
+
+ // We can't use promiseFormsProcessed* here, because _fillForm doesn't
+ // run if Primary Password is locked.
+ await new Promise(resolve => {
+ // Testing a negative, wait a little to give the login manager a chance to
+ // (incorrectly) fill in the form. Note, we cannot use setTimeout()
+ // here because the modal window suspends all window timers. Instead we
+ // must use a chrome script to use nsITimer directly.
+ const chromeURL = SimpleTest.getTestFileURL("chrome_timeout.js");
+ const script = SpecialPowers.loadChromeScript(chromeURL);
+ script.addMessageListener("ready", _ => {
+ script.sendAsyncMessage("setTimeout", { delay: 500 });
+ });
+ script.addMessageListener("timeout", resolve);
+ });
+
+ // iframe should load without having triggered a MP prompt (because one
+ // is already waiting)
+
+ // check contents of iframe fields
+ await SpecialPowers.spawn(win, [], function() {
+ const iframe = this.content.document.querySelector("iframe");
+ const frameDoc = iframe.contentDocument;
+ const u = frameDoc.getElementById("userfield");
+ const p = frameDoc.getElementById("passfield");
+ Assert.equal(u.value, "", "checking expected empty user");
+ Assert.equal(p.value, "", "checking expected empty pass");
+ });
+
+ // XXX check that there's 1 MP window open
+ ok(!await isLoggedIn(), "should be logged out");
+
+ // Ok, now enter the MP. The MP prompt is already up.
+ const fillPromise = promiseFormsProcessed(2);
+
+ // fill existing MP dialog with MP.
+ action = {
+ buttonClick: "ok",
+ passField: LoginTestUtils.primaryPassword.primaryPassword,
+ };
+ await handlePrompt(state, action);
+ await fillPromise;
+
+ // We shouldn't have to worry about win's load event racing with
+ // filling of the iframe's data. We notify observers synchronously, so
+ // the iframe's observer will process the iframe before win even finishes
+ // processing the form.
+ ok(await isLoggedIn(), "should be logged in");
+
+ // check contents of win fields
+ await SpecialPowers.spawn(win, [], function() {
+ const u = this.content.document.getElementById("userfield");
+ const p = this.content.document.getElementById("passfield");
+ Assert.equal(u.value, "user2", "checking expected user to have been filled in");
+ Assert.equal(p.value, "pass2", "checking expected pass to have been filled in");
+
+ // clearing fields to not cause a submission when the next document is loaded
+ u.value = "";
+ p.value = "";
+ });
+
+ // check contents of iframe fields
+ await SpecialPowers.spawn(win, [], function() {
+ const iframe = this.content.document.querySelector("iframe");
+ const frameDoc = iframe.contentDocument;
+ const u = frameDoc.getElementById("userfield");
+ const p = frameDoc.getElementById("passfield");
+ Assert.equal(u.value, "user2", "checking expected user to have been filled in");
+ Assert.equal(p.value, "pass2", "checking expected pass to have been filled in");
+
+ // clearing fields to not cause a submission when the next document is loaded
+ u.value = "";
+ p.value = "";
+ });
+});
+
+// XXX do a test5ABC with clicking cancel?
+
+SimpleTest.registerCleanupFunction(function finishTest() {
+ disablePrimaryPassword();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt.html
new file mode 100644
index 0000000000..115039c706
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt.html
@@ -0,0 +1,669 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test prompter.{prompt,asyncPromptPassword,asyncPromptUsernameAndPassword}</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var state, action;
+var uname = { value: null };
+var pword = { value: null };
+var result = { value: null };
+var isOk;
+
+// Force parent to not look for tab-modal prompts, as they're not used for auth prompts.
+modalType = Ci.nsIPrompt.MODAL_TYPE_WINDOW;
+
+let prompterParent = runInParent(() => {
+ const promptFac = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"].
+ getService(Ci.nsIPromptFactory);
+
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let prompter1 = promptFac.getPrompt(chromeWin, Ci.nsIAuthPrompt);
+
+ addMessageListener("proxyPrompter", async function onMessage(msg) {
+ let rv = await prompter1[msg.methodName](...msg.args);
+ return {
+ rv,
+ // Send the args back to content so out/inout args can be checked.
+ args: msg.args,
+ };
+ });
+});
+
+let prompter1 = new PrompterProxy(prompterParent);
+
+const defaultTitle = "the title";
+const defaultMsg = "the message";
+
+add_setup(async () => {
+ await addLoginsInParent(
+ ["http://example.com", null, "http://example.com", "", "examplepass", "", ""],
+ ["http://example2.com", null, "http://example2.com", "user1name", "user1pass", "", ""],
+ ["http://example2.com", null, "http://example2.com", "user2name", "user2pass", "", ""],
+ ["http://example2.com", null, "http://example2.com", "user3.name@host", "user3pass", "", ""],
+ ["http://example2.com", null, "http://example2.com", "100@beef", "user3pass", "", ""],
+ ["http://example2.com", null, "http://example2.com", "100%beef", "user3pass", "", ""]
+ );
+});
+
+add_task(async function test_prompt_accept() {
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "abc",
+ passValue: "",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: true,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ textField: "xyz",
+ };
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.prompt(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, "abc", result);
+ await promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(result.value, "xyz", "Checking prompt() returned value");
+});
+
+add_task(async function test_prompt_cancel() {
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "abc",
+ passValue: "",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: true,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "cancel",
+ };
+ promptDone = handlePrompt(state, action);
+ isOk = prompter1.prompt(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, "abc", result);
+ await promptDone;
+ ok(!isOk, "Checking dialog return value (cancel)");
+});
+
+add_task(async function test_promptPassword_defaultAccept() {
+ // Default password provided, existing logins are ignored.
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "",
+ passValue: "inputpw",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "passField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ passField: "secret",
+ };
+ pword.value = "inputpw";
+ promptDone = handlePrompt(state, action);
+ isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ await promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "secret", "Checking returned password");
+});
+
+add_task(async function test_promptPassword_defaultCancel() {
+ // Default password provided, existing logins are ignored.
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "",
+ passValue: "inputpw",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "passField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "cancel",
+ };
+ pword.value = "inputpw";
+ promptDone = handlePrompt(state, action);
+ isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ await promptDone;
+ ok(!isOk, "Checking dialog return value (cancel)");
+});
+
+add_task(async function test_promptPassword_emptyAccept() {
+ // No default password provided, realm does not match existing login.
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "",
+ passValue: "",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "passField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ passField: "secret",
+ };
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://nonexample.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ await promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "secret", "Checking returned password");
+});
+
+add_task(async function test_promptPassword_saved() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "examplepass", "Checking returned password");
+});
+
+add_task(async function test_promptPassword_noMatchingPasswordForEmptyUN() {
+ // No default password provided, none of the logins from this host are
+ // password-only so the user is prompted.
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "",
+ passValue: "",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "passField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ passField: "secret",
+ };
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ await promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "secret", "Checking returned password");
+});
+
+add_task(async function test_promptPassword_matchingPWForUN() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://user1name@example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "user1pass", "Checking returned password");
+});
+
+add_task(async function test_promptPassword_matchingPWForUN2() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://user2name@example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "user2pass", "Checking returned password");
+});
+
+add_task(async function test_promptPassword_matchingPWForUN3() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://user3%2Ename%40host@example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "user3pass", "Checking returned password");
+});
+
+add_task(async function test_promptPassword_extraAt() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://100@beef@example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "user3pass", "Checking returned password");
+});
+
+add_task(async function test_promptPassword_usernameEncoding() {
+ // No default password provided, matching login is returned w/o prompting.
+ pword.value = null;
+ isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://100%25beef@example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "user3pass", "Checking returned password");
+
+ // XXX test saving a password with Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY
+});
+
+add_task(async function test_promptPassword_realm() {
+ // We don't pre-fill or save for NS_GetAuthKey-generated realms, but we should still prompt
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "",
+ passValue: "",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "passField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ passField: "fill2pass",
+ };
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "example2.com:80 (somerealm)",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword);
+ await promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "fill2pass", "Checking returned password");
+});
+
+add_task(async function test_promptPassword_realm2() {
+ // We don't pre-fill or save for NS_GetAuthKey-generated realms, but we should still prompt
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "",
+ passValue: "",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "passField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ passField: "fill2pass",
+ };
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "example2.com:80 (somerealm)",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, pword);
+ await promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(pword.value, "fill2pass", "Checking returned password");
+});
+
+add_task(async function test_promptUsernameAndPassword_accept() {
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "inuser",
+ passValue: "inpass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ textField: "outuser",
+ passField: "outpass",
+ };
+ uname.value = "inuser";
+ pword.value = "inpass";
+ promptDone = handlePrompt(state, action);
+ isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "http://nonexample.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, uname, pword);
+ await promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "outuser", "Checking returned username");
+ is(pword.value, "outpass", "Checking returned password");
+});
+
+add_task(async function test_promptUsernameAndPassword_cancel() {
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "inuser",
+ passValue: "inpass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "cancel",
+ };
+ uname.value = "inuser";
+ pword.value = "inpass";
+ promptDone = handlePrompt(state, action);
+ isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "http://nonexample.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, uname, pword);
+ await promptDone;
+ ok(!isOk, "Checking dialog return value (cancel)");
+});
+
+add_task(async function test_promptUsernameAndPassword_autofill() {
+ // test filling in existing password-only login
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "",
+ passValue: "examplepass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+ uname.value = null;
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "http://example.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ await promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "", "Checking returned username");
+ is(pword.value, "examplepass", "Checking returned password");
+});
+
+add_task(async function test_promptUsernameAndPassword_multipleExisting() {
+ // test filling in existing login (undetermined from multiple selection)
+ // user2name/user2pass would also be valid to fill here.
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "user1name",
+ passValue: "user1pass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+ uname.value = null;
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ await promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ ok(uname.value == "user1name" || uname.value == "user2name", "Checking returned username");
+ ok(pword.value == "user1pass" || uname.value == "user2pass", "Checking returned password");
+});
+
+add_task(async function test_promptUsernameAndPassword_multipleExisting1() {
+ // test filling in existing login (user1 from multiple selection)
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "user1name",
+ passValue: "user1pass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+ uname.value = "user1name";
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ await promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "user1name", "Checking returned username");
+ is(pword.value, "user1pass", "Checking returned password");
+});
+
+add_task(async function test_promptUsernameAndPassword_multipleExisting2() {
+ // test filling in existing login (user2 from multiple selection)
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "user2name",
+ passValue: "user2pass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+ uname.value = "user2name";
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ await promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "user2name", "Checking returned username");
+ is(pword.value, "user2pass", "Checking returned password");
+});
+
+add_task(async function test_promptUsernameAndPassword_passwordChange() {
+ // test changing password
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "user2name",
+ passValue: "user2pass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ passField: "NEWuser2pass",
+ };
+ uname.value = "user2name";
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ await promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "user2name", "Checking returned username");
+ is(pword.value, "NEWuser2pass", "Checking returned password");
+});
+
+add_task(async function test_promptUsernameAndPassword_changePasswordBack() {
+ // test changing password (back to original value)
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "user2name",
+ passValue: "NEWuser2pass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ passField: "user2pass",
+ };
+ uname.value = "user2name";
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ await promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "user2name", "Checking returned username");
+ is(pword.value, "user2pass", "Checking returned password");
+});
+
+add_task(async function test_promptUsernameAndPassword_realm() {
+ // We don't pre-fill or save for NS_GetAuthKey-generated realms, but we should still prompt
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "",
+ passValue: "",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ textField: "fill2user",
+ passField: "fill2pass",
+ };
+ uname.value = null;
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "example2.com:80 (somerealm)",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, uname, pword);
+ await promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "fill2user", "Checking returned username");
+ is(pword.value, "fill2pass", "Checking returned password");
+});
+
+add_task(async function test_promptUsernameAndPassword_realm2() {
+ // We don't pre-fill or save for NS_GetAuthKey-generated realms, but we should still prompt
+ state = {
+ msg: "the message",
+ title: "the title",
+ textValue: "",
+ passValue: "",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ textField: "fill2user",
+ passField: "fill2pass",
+ };
+ uname.value = null;
+ pword.value = null;
+ promptDone = handlePrompt(state, action);
+ isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "example2.com:80 (somerealm)",
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword);
+ await promptDone;
+ ok(isOk, "Checking dialog return value (accept)");
+ is(uname.value, "fill2user", "Checking returned username");
+ is(pword.value, "fill2pass", "Checking returned password");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_async.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_async.html
new file mode 100644
index 0000000000..d9934a3d28
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_async.html
@@ -0,0 +1,621 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Async Auth Prompt</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+ <script class="testbody" type="text/javascript">
+ const { NetUtil } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/NetUtil.jsm"
+ );
+ const { TestUtils } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+ );
+ const EXAMPLE_COM = "http://example.com/tests/toolkit/components/passwordmgr/test/mochitest/";
+ const EXAMPLE_ORG = "http://example.org/tests/toolkit/components/passwordmgr/test/mochitest/";
+ let mozproxyOrigin;
+
+ // Let prompt_common know what kind of modal type is enabled for auth prompts.
+ modalType = authPromptModalType;
+
+ // These are magically defined on the window due to the iframe IDs
+ /* global iframe1, iframe2a, iframe2b */
+
+ /**
+ * Add a listener to add some logins to be autofilled in the HTTP/proxy auth. prompts later.
+ */
+ let pwmgrParent = runInParent(() => {
+ Services.prefs.setIntPref("network.auth.subresource-http-auth-allow", 2);
+ Services.prefs.setIntPref("prompts.authentication_dialog_abuse_limit", -1);
+
+ addMessageListener("initLogins", async function onMessage(msg) {
+ const loginsData = [
+ [msg.mozproxyOrigin, "proxy_realm", "proxy_user", "proxy_pass"],
+ [msg.mozproxyOrigin, "proxy_realm2", "proxy_user2", "proxy_pass2"],
+ [msg.mozproxyOrigin, "proxy_realm3", "proxy_user3", "proxy_pass3"],
+ [msg.mozproxyOrigin, "proxy_realm4", "proxy_user4", "proxy_pass4"],
+ [msg.mozproxyOrigin, "proxy_realm5", "proxy_user5", "proxy_pass5"],
+ ["http://example.com", "mochirealm", "user1name", "user1pass"],
+ ["http://example.org", "mochirealm2", "user2name", "user2pass"],
+ ["http://example.com", "mochirealm3", "user3name", "user3pass"],
+ ["http://example.com", "mochirealm4", "user4name", "user4pass"],
+ ["http://example.com", "mochirealm5", "user5name", "user5pass"],
+ ["http://example.com", "mochirealm6", "user6name", "user6pass"]
+ ];
+ const logins = loginsData.map(([host, realm, user, pass]) => {
+ const login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+ login.init(host, null, realm, user, pass, "", "");
+ return login
+ })
+ await Services.logins.addLogins(logins);
+ });
+ }); // end runInParent
+
+ function promiseLoadedContentDoc(frame) {
+ return new Promise(resolve => {
+ frame.addEventListener("load", function onLoad(evt) {
+ resolve(SpecialPowers.wrap(frame).contentDocument);
+ }, { once: true });
+ });
+ }
+
+ function promiseProxyErrorLoad(frame) {
+ return TestUtils.waitForCondition(async function checkForProxyConnectFailure() {
+ try {
+ return await SpecialPowers.spawn(frame, [], function() {
+ return this.content.document.documentURI.includes("proxyConnectFailure");
+ })
+ } catch (e) {
+ // The frame may not be ready for the 'spawn' task right after setting
+ // iframe.src, which will throw an exception when that happens.
+ // Since this test is testing error load, we can't wait until the iframe
+ // is 'loaded' either. So we simply catch the exception here and retry the task
+ // later since we are in the waitForCondition loop.
+ return false;
+ }
+ }, "Waiting for proxyConnectFailure documentURI");
+ }
+
+ /**
+ * Make a channel to get the ProxyInfo used by the test harness so that we
+ * can add logins for the correct proxy origin.
+ */
+ add_task(async function setup_getProxyInfoForHarness() {
+ await new Promise(resolve => {
+ let resolveCallback = SpecialPowers.wrapCallbackObject({
+ // eslint-disable-next-line mozilla/use-chromeutils-generateqi
+ QueryInterface(iid) {
+ const interfaces = [Ci.nsIProtocolProxyCallback, Ci.nsISupports];
+
+ if (!interfaces.some(v => iid.equals(v))) {
+ throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE;
+ }
+ return this;
+ },
+
+ onProxyAvailable(req, uri, pi, status) {
+ // Add logins using the proxy host and port used by the mochitest harness.
+ mozproxyOrigin = "moz-proxy://" + SpecialPowers.wrap(pi).host + ":" +
+ SpecialPowers.wrap(pi).port;
+
+ pwmgrParent.sendQuery("initLogins", {mozproxyOrigin}).then(resolve)
+ },
+ });
+
+ // Need to allow for arbitrary network servers defined in PAC instead of a hardcoded moz-proxy.
+ let channel = NetUtil.newChannel({
+ uri: "http://example.com",
+ loadUsingSystemPrincipal: true,
+ });
+
+ let pps = SpecialPowers.Cc["@mozilla.org/network/protocol-proxy-service;1"]
+ .getService();
+
+ pps.asyncResolve(channel, 0, resolveCallback);
+ });
+ });
+
+ add_task(async function test_proxyAuthThenTwoHTTPAuth() {
+ // Load through a single proxy with authentication required 3 different
+ // pages, first with one login, other two with their own different login.
+ // We expect to show just a single dialog for proxy authentication and
+ // then two dialogs to authenticate to login 1 and then login 2.
+
+ let iframe1DocPromise = promiseLoadedContentDoc(iframe1);
+ let iframe2aDocPromise = promiseLoadedContentDoc(iframe2a);
+ let iframe2bDocPromise = promiseLoadedContentDoc(iframe2b);
+
+ iframe1.src = EXAMPLE_COM + "authenticate.sjs?" +
+ "r=1&" +
+ "user=user1name&" +
+ "pass=user1pass&" +
+ "realm=mochirealm&" +
+ "proxy_user=proxy_user&" +
+ "proxy_pass=proxy_pass&" +
+ "proxy_realm=proxy_realm";
+ iframe2a.src = EXAMPLE_ORG + "authenticate.sjs?" +
+ "r=2&" +
+ "user=user2name&" +
+ "pass=user2pass&" +
+ "realm=mochirealm2&" +
+ "proxy_user=proxy_user&" +
+ "proxy_pass=proxy_pass&" +
+ "proxy_realm=proxy_realm";
+ iframe2b.src = EXAMPLE_ORG + "authenticate.sjs?" +
+ "r=3&" +
+ "user=user2name&" +
+ "pass=user2pass&" +
+ "realm=mochirealm2&" +
+ "proxy_user=proxy_user&" +
+ "proxy_pass=proxy_pass&" +
+ "proxy_realm=proxy_realm";
+
+ let state = {
+ msg: `The proxy ${mozproxyOrigin} is requesting a username and password. The site says: “proxy_realm”`,
+ title: "Authentication Required",
+ textValue: "proxy_user",
+ passValue: "proxy_pass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ let action = {
+ buttonClick: "ok",
+ };
+ await handlePrompt(state, action);
+
+ // We don't know what order these prompts appear in so get both states and check them.
+ // We can't use Promise.all here since we can't start the 2nd timer in chromeScript.js until
+ // the first timer is done since the timer variable gets clobbered, plus we don't want
+ // different actions racing each other.
+ let promptStates = [
+ await handlePromptWithoutChecks(action),
+ await handlePromptWithoutChecks(action),
+ ];
+
+ let expected1 = Object.assign({}, state, {
+ msg: "This site is asking you to sign in. Warning: Your login information will be shared with example.com, not the website you are currently visiting.",
+ textValue: "user1name",
+ passValue: "user1pass",
+ });
+
+ let expected2 = Object.assign({}, state, {
+ msg: "This site is asking you to sign in. Warning: Your login information will be shared with example.org, not the website you are currently visiting.",
+ textValue: "user2name",
+ passValue: "user2pass",
+ });
+
+ // The order isn't important.
+ let expectedPromptStates = [
+ expected1,
+ expected2,
+ ];
+
+ is(promptStates.length, expectedPromptStates.length,
+ "Check we handled the right number of prompts");
+ for (let promptState of promptStates) {
+ let expectedStateIndexForMessage = expectedPromptStates.findIndex(eps => {
+ return eps.msg == promptState.msg;
+ });
+ isnot(expectedStateIndexForMessage, -1, "Check state message was found in expected array");
+ let expectedPromptState = expectedPromptStates.splice(expectedStateIndexForMessage, 1)[0];
+ checkPromptState(promptState, expectedPromptState);
+ }
+
+ await iframe1DocPromise;
+ await iframe2aDocPromise;
+ await iframe2bDocPromise;
+
+ await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
+ let doc = this.content.document;
+ let authok1 = doc.getElementById("ok").textContent;
+ let proxyok1 = doc.getElementById("proxy").textContent;
+ Assert.equal(authok1, "PASS", "WWW Authorization OK, frame1");
+ Assert.equal(proxyok1, "PASS", "Proxy Authorization OK, frame1");
+ });
+
+ await SpecialPowers.spawn(getIframeBrowsingContext(window, 1), [], function() {
+ let doc = this.content.document;
+ let authok2a = doc.getElementById("ok").textContent;
+ let proxyok2a = doc.getElementById("proxy").textContent;
+ Assert.equal(authok2a, "PASS", "WWW Authorization OK, frame2a");
+ Assert.equal(proxyok2a, "PASS", "Proxy Authorization OK, frame2a");
+ });
+
+ await SpecialPowers.spawn(getIframeBrowsingContext(window, 2), [], function() {
+ let doc = this.content.document;
+ let authok2b = doc.getElementById("ok").textContent;
+ let proxyok2b = doc.getElementById("proxy").textContent;
+ Assert.equal(authok2b, "PASS", "WWW Authorization OK, frame2b");
+ Assert.equal(proxyok2b, "PASS", "Proxy Authorization OK, frame2b");
+ });
+ });
+
+ add_task(async function test_threeSubframesWithSameProxyAndHTTPAuth() {
+ // Load an iframe with 3 subpages all requiring the same login through
+ // an authenticated proxy. We expect 2 dialogs, proxy authentication
+ // and web authentication.
+
+ let iframe1DocPromise = promiseLoadedContentDoc(iframe1);
+
+ iframe1.src = EXAMPLE_COM + "subtst_prompt_async.html";
+ iframe2a.src = "about:blank";
+ iframe2b.src = "about:blank";
+
+ let state = {
+ msg: `The proxy ${mozproxyOrigin} is requesting a username and password. The site says: “proxy_realm2”`,
+ title: "Authentication Required",
+ textValue: "proxy_user2",
+ passValue: "proxy_pass2",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ let action = {
+ buttonClick: "ok",
+ };
+ await handlePrompt(state, action);
+
+ Object.assign(state, {
+ msg: "This site is asking you to sign in. Warning: Your login information will be shared with example.com, not the website you are currently visiting.",
+ textValue: "user3name",
+ passValue: "user3pass",
+ });
+ await handlePrompt(state, action);
+
+ await iframe1DocPromise;
+
+ function checkIframe(frameid) {
+ let doc = this.content.document;
+ let authok = doc.getElementById("ok").textContent;
+ let proxyok = doc.getElementById("proxy").textContent;
+
+ Assert.equal(authok, "PASS", "WWW Authorization OK, " + frameid);
+ Assert.equal(proxyok, "PASS", "Proxy Authorization OK, " + frameid);
+ }
+
+ let parentIFrameBC = SpecialPowers.wrap(window).windowGlobalChild
+ .browsingContext.children[0];
+
+ let childIFrame = SpecialPowers.unwrap(parentIFrameBC.children[0]);
+ await SpecialPowers.spawn(childIFrame, ["iframe1"], checkIframe);
+ childIFrame = SpecialPowers.unwrap(parentIFrameBC.children[1]);
+ await SpecialPowers.spawn(childIFrame, ["iframe2"], checkIframe);
+ childIFrame = SpecialPowers.unwrap(parentIFrameBC.children[2]);
+ await SpecialPowers.spawn(childIFrame, ["iframe3"], checkIframe);
+ });
+
+ add_task(async function test_oneFrameWithUnauthenticatedProxy() {
+ // Load in the iframe page through unauthenticated proxy
+ // and discard the proxy authentication. We expect to see
+ // unauthenticated page content and just a single dialog.
+
+ iframe1.src = EXAMPLE_COM + "authenticate.sjs?" +
+ "user=user4name&" +
+ "pass=user4pass&" +
+ "realm=mochirealm4&" +
+ "proxy_user=proxy_user3&" +
+ "proxy_pass=proxy_pass3&" +
+ "proxy_realm=proxy_realm3";
+
+ let state = {
+ msg: `The proxy ${mozproxyOrigin} is requesting a username and password. The site says: “proxy_realm3”`,
+ title: "Authentication Required",
+ textValue: "proxy_user3",
+ passValue: "proxy_pass3",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ let action = {
+ buttonClick: "cancel",
+ };
+ await handlePrompt(state, action);
+
+ await promiseProxyErrorLoad(iframe1);
+ });
+
+ add_task(async function test_reloadReusingProxyAuthButCancellingHTTPAuth() {
+ // Reload the frame from previous step and pass the proxy authentication
+ // but cancel the WWW authentication. We should get the proxy=ok and WWW=fail
+ // content as a result.
+ let iframe1DocPromise = promiseLoadedContentDoc(iframe1);
+
+ iframe1.src = EXAMPLE_COM + "authenticate.sjs?" +
+ "user=user4name&" +
+ "pass=user4pass&" +
+ "realm=mochirealm4&" +
+ "proxy_user=proxy_user3&" +
+ "proxy_pass=proxy_pass3&" +
+ "proxy_realm=proxy_realm3";
+
+ let state = {
+ msg: `The proxy ${mozproxyOrigin} is requesting a username and password. The site says: “proxy_realm3”`,
+ title: "Authentication Required",
+ textValue: "proxy_user3",
+ passValue: "proxy_pass3",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ let action = {
+ buttonClick: "ok",
+ };
+ await handlePrompt(state, action);
+
+ Object.assign(state, {
+ msg: "This site is asking you to sign in. Warning: Your login information will be shared with example.com, not the website you are currently visiting.",
+ textValue: "user4name",
+ passValue: "user4pass",
+ });
+ action = {
+ buttonClick: "cancel",
+ };
+ await handlePrompt(state, action);
+
+ await iframe1DocPromise;
+
+ await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
+ let doc = this.content.document;
+ let authok1 = doc.getElementById("ok").textContent;
+ let proxyok1 = doc.getElementById("proxy").textContent;
+
+ Assert.equal(authok1, "FAIL", "WWW Authorization FAILED, frame1");
+ Assert.equal(proxyok1, "PASS", "Proxy Authorization OK, frame1");
+ });
+ });
+
+ add_task(async function test_hugePayloadCancelled() {
+ // Same as the previous two steps but let the server generate
+ // huge content load to check http channel is capable to handle
+ // case when auth dialog is canceled or accepted before unauthenticated
+ // content data is load from the server. (This would be better to
+ // implement using delay of server response).
+ iframe1.src = EXAMPLE_COM + "authenticate.sjs?" +
+ "user=user5name&" +
+ "pass=user5pass&" +
+ "realm=mochirealm5&" +
+ "proxy_user=proxy_user4&" +
+ "proxy_pass=proxy_pass4&" +
+ "proxy_realm=proxy_realm4&" +
+ "huge=1";
+
+ let state = {
+ msg: `The proxy ${mozproxyOrigin} is requesting a username and password. The site says: “proxy_realm4”`,
+ title: "Authentication Required",
+ textValue: "proxy_user4",
+ passValue: "proxy_pass4",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ let action = {
+ buttonClick: "cancel",
+ };
+ await handlePrompt(state, action);
+
+ await promiseProxyErrorLoad(iframe1);
+ });
+
+ add_task(async function test_hugeProxySuccessWWWFail() {
+ // Reload the frame from the previous step and let the proxy
+ // authentication pass but WWW fail. We expect two dialogs
+ // and an unauthenticated page content load.
+
+ let iframe1DocPromise = promiseLoadedContentDoc(iframe1);
+ iframe1.src = EXAMPLE_COM + "authenticate.sjs?" +
+ "user=user5name&" +
+ "pass=user5pass&" +
+ "realm=mochirealm5&" +
+ "proxy_user=proxy_user4&" +
+ "proxy_pass=proxy_pass4&" +
+ "proxy_realm=proxy_realm4&" +
+ "huge=1";
+
+ let state = {
+ msg: `The proxy ${mozproxyOrigin} is requesting a username and password. The site says: “proxy_realm4”`,
+ title: "Authentication Required",
+ textValue: "proxy_user4",
+ passValue: "proxy_pass4",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ let action = {
+ buttonClick: "ok",
+ };
+ await handlePrompt(state, action);
+
+ Object.assign(state, {
+ msg: "This site is asking you to sign in. Warning: Your login information will be shared with example.com, not the website you are currently visiting.",
+ textValue: "user5name",
+ passValue: "user5pass",
+ });
+ action = {
+ buttonClick: "cancel",
+ };
+ await handlePrompt(state, action);
+
+ await iframe1DocPromise;
+
+ await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
+ let doc = this.content.document;
+ let authok1 = doc.getElementById("ok").textContent;
+ let proxyok1 = doc.getElementById("proxy").textContent;
+ let footnote = doc.getElementById("footnote").textContent;
+
+ Assert.equal(authok1, "FAIL", "WWW Authorization FAILED, frame1");
+ Assert.equal(proxyok1, "PASS", "Proxy Authorization OK, frame1");
+ Assert.equal(footnote, "This is a footnote after the huge content fill",
+ "Footnote present and loaded completely");
+ });
+ });
+
+ add_task(async function test_hugeProxySuccessWWWSuccess() {
+ // Reload again and let pass all authentication dialogs.
+ // Check we get the authenticated content not broken by
+ // the unauthenticated content.
+
+ let iframe1DocPromise = promiseLoadedContentDoc(iframe1);
+ await SpecialPowers.spawn(iframe1, [], function() {
+ this.content.document.location.reload();
+ });
+
+ let state = {
+ msg: "This site is asking you to sign in. Warning: Your login information will be shared with example.com, not the website you are currently visiting.",
+ title: "Authentication Required",
+ textValue: "user5name",
+ passValue: "user5pass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ let action = {
+ buttonClick: "ok",
+ };
+ await handlePrompt(state, action);
+
+ await iframe1DocPromise;
+
+ await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
+ let doc = this.content.document;
+ let authok1 = doc.getElementById("ok").textContent;
+ let proxyok1 = doc.getElementById("proxy").textContent;
+ let footnote = doc.getElementById("footnote").textContent;
+
+ Assert.equal(authok1, "PASS", "WWW Authorization OK, frame1");
+ Assert.equal(proxyok1, "PASS", "Proxy Authorization OK, frame1");
+ Assert.equal(footnote, "This is a footnote after the huge content fill",
+ "Footnote present and loaded completely");
+ });
+ });
+
+ add_task(async function test_cancelSome() {
+ // Check we process all challenges sent by server when
+ // user cancels prompts
+ let iframe1DocPromise = promiseLoadedContentDoc(iframe1);
+ iframe1.src = EXAMPLE_COM + "authenticate.sjs?" +
+ "user=user6name&" +
+ "pass=user6pass&" +
+ "realm=mochirealm6&" +
+ "proxy_user=proxy_user5&" +
+ "proxy_pass=proxy_pass5&" +
+ "proxy_realm=proxy_realm5&" +
+ "huge=1&" +
+ "multiple=3";
+
+ let state = {
+ msg: `The proxy ${mozproxyOrigin} is requesting a username and password. The site says: “proxy_realm5”`,
+ title: "Authentication Required",
+ textValue: "proxy_user5",
+ passValue: "proxy_pass5",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ let action = {
+ buttonClick: "cancel",
+ };
+ await handlePrompt(state, action);
+
+ action = {
+ buttonClick: "cancel",
+ };
+ await handlePrompt(state, action);
+
+ action = {
+ buttonClick: "ok",
+ };
+ await handlePrompt(state, action);
+
+ Object.assign(state, {
+ msg: "This site is asking you to sign in. Warning: Your login information will be shared with example.com, not the website you are currently visiting.",
+ textValue: "user6name",
+ passValue: "user6pass",
+ });
+
+ action = {
+ buttonClick: "cancel",
+ };
+ await handlePrompt(state, action);
+
+ action = {
+ buttonClick: "ok",
+ };
+ await handlePrompt(state, action);
+
+ await iframe1DocPromise;
+ await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
+ let doc = this.content.document;
+ let authok1 = doc.getElementById("ok").textContent;
+ let proxyok1 = doc.getElementById("proxy").textContent;
+ let footnote = doc.getElementById("footnote").textContent;
+
+ Assert.equal(authok1, "PASS", "WWW Authorization OK, frame1");
+ Assert.equal(proxyok1, "PASS", "Proxy Authorization OK, frame1");
+ Assert.equal(footnote, "This is a footnote after the huge content fill",
+ "Footnote present and loaded completely");
+ });
+
+ });
+ </script>
+</head>
+<body>
+ <iframe id="iframe1"></iframe>
+ <iframe id="iframe2a"></iframe>
+ <iframe id="iframe2b"></iframe>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html
new file mode 100644
index 0000000000..effe438d1c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html
@@ -0,0 +1,319 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test HTTP auth prompts by loading authenticate.sjs</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <iframe id="iframe"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var iframe = document.getElementById("iframe");
+
+// Let prompt_common know what kind of modal type is enabled for auth prompts.
+modalType = authPromptModalType;
+
+const AUTHENTICATE_PATH = new URL("authenticate.sjs", window.location.href).pathname;
+
+add_setup(async () => {
+ await addLoginsInParent(
+ ["http://mochi.test:8888", null, "mochitest", "mochiuser1", "mochipass1", "", ""],
+ ["http://mochi.test:8888", null, "mochitest2", "mochiuser2", "mochipass2", "", ""],
+ ["http://mochi.test:8888", null, "mochitest3", "mochiuser3", "mochipass3-old", "", ""],
+ // Logins to test scheme upgrades (allowed) and downgrades (disallowed)
+ ["http://example.com", null, "schemeUpgrade", "httpUser", "httpPass", "", ""],
+ ["https://example.com", null, "schemeDowngrade", "httpsUser", "httpsPass", "", ""],
+ // HTTP and HTTPS version of the same domain and realm but with different passwords.
+ ["http://example.org", null, "schemeUpgradeDedupe", "dedupeUser", "httpPass", "", ""],
+ ["https://example.org", null, "schemeUpgradeDedupe", "dedupeUser", "httpsPass", "", ""]
+ );
+});
+
+add_task(async function test_iframe() {
+ let state = {
+ msg: "This site is asking you to sign in.",
+ title: "Authentication Required",
+ textValue: "mochiuser1",
+ passValue: "mochipass1",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ let action = {
+ buttonClick: "ok",
+ };
+ promptDone = handlePrompt(state, action);
+
+ // The following tests are driven by iframe loads
+
+ var iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1";
+ await promptDone;
+ await iframeLoaded;
+ await checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1"},
+ iframe);
+
+ state = {
+ msg: "This site is asking you to sign in.",
+ title: "Authentication Required",
+ textValue: "mochiuser2",
+ passValue: "mochipass2",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+ promptDone = handlePrompt(state, action);
+ // We've already authenticated to this host:port. For this next
+ // request, the existing auth should be sent, we'll get a 401 reply,
+ // and we should prompt for new auth.
+ iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "authenticate.sjs?user=mochiuser2&pass=mochipass2&realm=mochitest2";
+ await promptDone;
+ await iframeLoaded;
+ await checkEchoedAuthInfo({user: "mochiuser2", pass: "mochipass2"},
+ iframe);
+
+ // Now make a load that requests the realm from test 1000. It was
+ // already provided there, so auth will *not* be prompted for -- the
+ // networking layer already knows it!
+ iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1";
+ await iframeLoaded;
+ await checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1"},
+ iframe);
+
+ // Same realm we've already authenticated to, but with a different
+ // expected password (to trigger an auth prompt, and change-password
+ // popup notification).
+ state = {
+ msg: "This site is asking you to sign in.",
+ title: "Authentication Required",
+ textValue: "mochiuser1",
+ passValue: "mochipass1",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ passField: "mochipass1-new",
+ };
+ promptDone = handlePrompt(state, action);
+ iframeLoaded = onloadPromiseFor("iframe");
+ let promptShownPromise = promisePromptShown("passwordmgr-prompt-change");
+ iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1-new";
+ await promptDone;
+ await iframeLoaded;
+ await checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1-new"},
+ iframe);
+ await promptShownPromise;
+
+ // Same as last test, but for a realm we haven't already authenticated
+ // to (but have an existing saved login for, so that we'll trigger
+ // a change-password popup notification.
+ state = {
+ msg: "This site is asking you to sign in.",
+ title: "Authentication Required",
+ textValue: "mochiuser3",
+ passValue: "mochipass3-old",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ passField: "mochipass3-new",
+ };
+ promptDone = handlePrompt(state, action);
+ iframeLoaded = onloadPromiseFor("iframe");
+ promptShownPromise = promisePromptShown("passwordmgr-prompt-change");
+ iframe.src = "authenticate.sjs?user=mochiuser3&pass=mochipass3-new&realm=mochitest3";
+ await promptDone;
+ await iframeLoaded;
+ await checkEchoedAuthInfo({user: "mochiuser3", pass: "mochipass3-new"},
+ iframe);
+ await promptShownPromise;
+
+ // Housekeeping: Delete login4 to test the save prompt in the next test.
+ runInParent(() => {
+ var tmpLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+ createInstance(Ci.nsILoginInfo);
+ tmpLogin.init("http://mochi.test:8888", null, "mochitest3",
+ "mochiuser3", "mochipass3-old", "", "");
+ Services.logins.removeLogin(tmpLogin);
+
+ // Clear cached auth from this subtest, and avoid leaking due to bug 459620.
+ var authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].
+ getService(Ci.nsIHttpAuthManager);
+ authMgr.clearAll();
+ });
+
+ state = {
+ msg: "This site is asking you to sign in.",
+ title: "Authentication Required",
+ textValue: "",
+ passValue: "",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ textField: "mochiuser3",
+ passField: "mochipass3-old",
+ };
+ // Trigger a new prompt, so we can test adding a new login.
+ promptDone = handlePrompt(state, action);
+
+ iframeLoaded = onloadPromiseFor("iframe");
+ promptShownPromise = promisePromptShown("passwordmgr-prompt-save");
+ iframe.src = "authenticate.sjs?user=mochiuser3&pass=mochipass3-old&realm=mochitest3";
+ await promptDone;
+ await iframeLoaded;
+ await checkEchoedAuthInfo({user: "mochiuser3", pass: "mochipass3-old"},
+ iframe);
+ await promptShownPromise;
+});
+
+add_task(async function test_schemeUpgrade() {
+ let state = {
+ msg: "This site is asking you to sign in. Warning: Your login information " +
+ "will be shared with example.com, not the website you are currently visiting.",
+ title: "Authentication Required",
+ textValue: "httpUser",
+ passValue: "httpPass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ let action = {
+ buttonClick: "ok",
+ };
+ let promptDone = handlePrompt(state, action);
+
+ // The following tests are driven by iframe loads
+
+ let iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "https://example.com" + AUTHENTICATE_PATH +
+ "?user=httpUser&pass=httpPass&realm=schemeUpgrade";
+ await promptDone;
+ await iframeLoaded;
+ await checkEchoedAuthInfo({user: "httpUser", pass: "httpPass"},
+ iframe);
+});
+
+add_task(async function test_schemeDowngrade() {
+ const state = {
+ msg: "This site is asking you to sign in. Warning: Your login information " +
+ "will be shared with example.com, not the website you are currently visiting.",
+ title: "Authentication Required",
+ textValue: "", // empty because we shouldn't downgrade
+ passValue: "",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ const action = {
+ buttonClick: "cancel",
+ };
+ const promptDone = handlePrompt(state, action);
+
+ // The following tests are driven by iframe loads
+
+ const iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "http://example.com" + AUTHENTICATE_PATH +
+ "?user=unused&pass=unused&realm=schemeDowngrade";
+ await promptDone;
+ await iframeLoaded;
+});
+
+add_task(async function test_schemeUpgrade_dedupe() {
+ const state = {
+ msg: "This site is asking you to sign in. Warning: Your login information " +
+ "will be shared with example.org, not the website you are currently visiting.",
+ title: "Authentication Required",
+ textValue: "dedupeUser",
+ passValue: "httpsPass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ const action = {
+ buttonClick: "ok",
+ };
+ const promptDone = handlePrompt(state, action);
+
+ // The following tests are driven by iframe loads
+
+ const iframeLoaded = onloadPromiseFor("iframe");
+ iframe.src = "https://example.org" + AUTHENTICATE_PATH +
+ "?user=dedupeUser&pass=httpsPass&realm=schemeUpgradeDedupe";
+ await promptDone;
+ await iframeLoaded;
+ await checkEchoedAuthInfo({user: "dedupeUser", pass: "httpsPass"},
+ iframe);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html
new file mode 100644
index 0000000000..5b7584e4fa
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test HTTP auth prompts by loading authenticate.sjs with no window</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// Let prompt_common know what kind of modal type is enabled for auth prompts.
+modalType = authPromptModalType;
+
+add_setup(async () => {
+ await addLoginsInParent(
+ ["http://mochi.test:8888", null, "mochitest", "mochiuser1", "mochipass1", "", ""]
+ );
+});
+
+add_task(async function test_sandbox_xhr() {
+ const state = {
+ msg: "This site is asking you to sign in.",
+ title: "Authentication Required",
+ textValue: "mochiuser1",
+ passValue: "mochipass1",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ const action = {
+ buttonClick: "ok",
+ };
+ const promptDone = handlePrompt(state, action);
+
+ const url = new URL("authenticate.sjs?user=mochiuser1&pass=mochipass1", window.location.href);
+ const sandboxConstructor = SpecialPowers.Cu.Sandbox;
+ const sandbox = new sandboxConstructor(this, {wantXrays: true});
+ function sandboxedRequest(sandboxedUrl) {
+ const req = new XMLHttpRequest();
+ req.open("GET", sandboxedUrl, true);
+ req.send(null);
+ }
+
+ const loginModifiedPromise = promiseStorageChanged(["modifyLogin"]);
+ sandbox.sandboxedRequest = sandboxedRequest(url);
+ info("send the XHR request in the sandbox");
+ SpecialPowers.Cu.evalInSandbox("sandboxedRequest;", sandbox);
+
+ await promptDone;
+ info("prompt shown, waiting for metadata updates");
+ // Ensure the timeLastUsed and timesUsed metadata are updated.
+ await loginModifiedPromise;
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html
new file mode 100644
index 0000000000..08cbedab88
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html
@@ -0,0 +1,370 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test promptAuth prompts</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const level = Ci.nsIAuthPrompt2.LEVEL_NONE;
+const authinfo = {
+ username: "",
+ password: "",
+ domain: "",
+
+ flags: Ci.nsIAuthInformation.AUTH_HOST,
+ authenticationScheme: "basic",
+ realm: "",
+};
+
+// Let prompt_common know what kind of modal type is enabled for auth prompts.
+modalType = authPromptModalType;
+
+const prompterParent = runInParent(() => {
+ const promptFac = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"].
+ getService(Ci.nsIPromptFactory);
+
+ const chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ const prompter2 = promptFac.getPrompt(chromeWin, Ci.nsIAuthPrompt2);
+ prompter2.QueryInterface(Ci.nsILoginManagerAuthPrompter).browser = chromeWin.gBrowser.selectedBrowser;
+
+ const channels = {};
+ channels.channel1 = Services.io.newChannel("http://example.com",
+ null,
+ null,
+ null, // aLoadingNode
+ Services.
+ scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+
+ channels.channel2 = Services.io.newChannel("http://example2.com",
+ null,
+ null,
+ null, // aLoadingNode
+ Services.
+ scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+
+ addMessageListener("proxyPrompter", function onMessage(msg) {
+ const args = [...msg.args];
+ const channelName = args.shift();
+ // Replace the channel name string (arg. 0) with the channel by that name.
+ args.unshift(channels[channelName]);
+
+ const rv = prompter2[msg.methodName](...args);
+ return {
+ rv,
+ // Send the args back to content so out/inout args can be checked.
+ args: msg.args,
+ };
+ });
+});
+
+const prompter2 = new PrompterProxy(prompterParent);
+
+add_setup(async () => {
+ await addLoginsInParent(
+ ["http://example.com", null, "http://example.com", "", "examplepass", "", ""],
+ ["http://example2.com", null, "http://example2.com", "user1name", "user1pass", "", ""],
+ ["http://example2.com", null, "http://example2.com", "user2name", "user2pass", "", ""],
+ ["http://example2.com", null, "http://example2.com", "user3.name@host", "user3pass", "", ""],
+ ["http://example2.com", null, "http://example2.com", "100@beef", "user3pass", "", ""],
+ ["http://example2.com", null, "http://example2.com", "100%beef", "user3pass", "", ""]
+ );
+});
+
+add_task(async function test_accept() {
+ const state = {
+ msg: "This site is asking you to sign in.",
+ title: "Authentication Required",
+ textValue: "inuser",
+ passValue: "inpass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ const action = {
+ buttonClick: "ok",
+ textField: "outuser",
+ passField: "outpass",
+ };
+ authinfo.username = "inuser";
+ authinfo.password = "inpass";
+ authinfo.realm = "some realm";
+
+ promptDone = handlePrompt(state, action);
+ // Since prompter2 is actually a proxy to send a message to a chrome script and
+ // we can't send a channel in a message, we instead send the channel name that
+ // already exists in the chromeScript.
+ const isOk = prompter2.promptAuth("channel1", level, authinfo);
+ await promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "outuser", "Checking returned username");
+ is(authinfo.password, "outpass", "Checking returned password");
+});
+
+add_task(async function test_cancel() {
+ const state = {
+ msg: "This site is asking you to sign in.",
+ title: "Authentication Required",
+ textValue: "outuser",
+ passValue: "outpass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ const action = {
+ buttonClick: "cancel",
+ };
+ promptDone = handlePrompt(state, action);
+ const isOk = prompter2.promptAuth("channel1", level, authinfo);
+ await promptDone;
+
+ ok(!isOk, "Checking dialog return value (cancel)");
+});
+
+add_task(async function test_pwonly() {
+ // test filling in password-only login
+ const state = {
+ msg: "This site is asking you to sign in.",
+ title: "Authentication Required",
+ textValue: "",
+ passValue: "examplepass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ const action = {
+ buttonClick: "ok",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example.com";
+
+ promptDone = handlePrompt(state, action);
+ const isOk = prompter2.promptAuth("channel1", level, authinfo);
+ await promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "", "Checking returned username");
+ is(authinfo.password, "examplepass", "Checking returned password");
+});
+
+add_task(async function test_multipleExisting() {
+ // test filling in existing login (undetermined from multiple selection)
+ // user2name/user2pass would also be valid to fill here.
+ const state = {
+ msg: "This site is asking you to sign in.",
+ title: "Authentication Required",
+ textValue: "user1name",
+ passValue: "user1pass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ const action = {
+ buttonClick: "ok",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example2.com";
+
+ promptDone = handlePrompt(state, action);
+ const isOk = prompter2.promptAuth("channel2", level, authinfo);
+ await promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ ok(authinfo.username == "user1name" || authinfo.username == "user2name", "Checking returned username");
+ ok(authinfo.password == "user1pass" || authinfo.password == "user2pass", "Checking returned password");
+});
+
+add_task(async function test_multipleExisting2() {
+ // test filling in existing login (undetermined --> user1)
+ // user2name/user2pass would also be valid to fill here.
+ const state = {
+ msg: "This site is asking you to sign in.",
+ title: "Authentication Required",
+ textValue: "user1name",
+ passValue: "user1pass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ // enter one of the known logins, test 504+505 exercise the two possible states.
+ const action = {
+ buttonClick: "ok",
+ textField: "user1name",
+ passField: "user1pass",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example2.com";
+
+ promptDone = handlePrompt(state, action);
+ const isOk = prompter2.promptAuth("channel2", level, authinfo);
+ await promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "user1name", "Checking returned username");
+ is(authinfo.password, "user1pass", "Checking returned password");
+});
+
+add_task(async function test_multipleExisting3() {
+ // test filling in existing login (undetermined --> user2)
+ // user2name/user2pass would also be valid to fill here.
+ const state = {
+ msg: "This site is asking you to sign in.",
+ title: "Authentication Required",
+ textValue: "user1name",
+ passValue: "user1pass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ // enter one of the known logins, test 504+505 exercise the two possible states.
+ const action = {
+ buttonClick: "ok",
+ textField: "user2name",
+ passField: "user2pass",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example2.com";
+
+ promptDone = handlePrompt(state, action);
+ const isOk = prompter2.promptAuth("channel2", level, authinfo);
+ await promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "user2name", "Checking returned username");
+ is(authinfo.password, "user2pass", "Checking returned password");
+});
+
+add_task(async function test_changingMultiple() {
+ // test changing a password (undetermined --> user2 w/ newpass)
+ // user2name/user2pass would also be valid to fill here.
+ const state = {
+ msg: "This site is asking you to sign in.",
+ title: "Authentication Required",
+ textValue: "user1name",
+ passValue: "user1pass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ // force to user2, and change the password
+ const action = {
+ buttonClick: "ok",
+ textField: "user2name",
+ passField: "NEWuser2pass",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example2.com";
+
+ promptDone = handlePrompt(state, action);
+ const isOk = prompter2.promptAuth("channel2", level, authinfo);
+ await promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "user2name", "Checking returned username");
+ is(authinfo.password, "NEWuser2pass", "Checking returned password");
+});
+
+add_task(async function test_changingMultiple2() {
+ // test changing a password (undetermined --> user2 w/ origpass)
+ // user2name/user2pass would also be valid to fill here.
+ const state = {
+ msg: "This site is asking you to sign in.",
+ title: "Authentication Required",
+ textValue: "user1name",
+ passValue: "user1pass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ // force to user2, and change the password back
+ const action = {
+ buttonClick: "ok",
+ textField: "user2name",
+ passField: "user2pass",
+ };
+ authinfo.username = "";
+ authinfo.password = "";
+ authinfo.realm = "http://example2.com";
+
+ promptDone = handlePrompt(state, action);
+ const isOk = prompter2.promptAuth("channel2", level, authinfo);
+ await promptDone;
+
+ ok(isOk, "Checking dialog return value (accept)");
+ is(authinfo.username, "user2name", "Checking returned username");
+ is(authinfo.password, "user2pass", "Checking returned password");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html
new file mode 100644
index 0000000000..a7cec2d0b9
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html
@@ -0,0 +1,269 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test promptAuth proxy prompts</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <iframe id="iframe"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/* eslint-disable mozilla/use-chromeutils-generateqi */
+
+const LEVEL = Ci.nsIAuthPrompt2.LEVEL_NONE;
+
+let proxyAuthinfo = {
+ username: "",
+ password: "",
+ domain: "",
+
+ flags: Ci.nsIAuthInformation.AUTH_PROXY,
+ authenticationScheme: "basic",
+ realm: "",
+};
+
+// Let prompt_common know what kind of modal type is enabled for auth prompts.
+modalType = authPromptModalType;
+
+let chromeScript = runInParent(() => {
+ const promptFac = Cc[
+ "@mozilla.org/passwordmanager/authpromptfactory;1"
+ ].getService(Ci.nsIPromptFactory);
+
+ let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let prompter2 = promptFac.getPrompt(chromeWin, Ci.nsIAuthPrompt2);
+ prompter2.QueryInterface(Ci.nsILoginManagerAuthPrompter).browser =
+ chromeWin.gBrowser.selectedBrowser;
+
+ let mozproxyURL;
+ let proxyChannel;
+
+ addMessageListener("init", () => init());
+
+ addMessageListener("proxyPrompter", function onMessage(msg) {
+ let args = [...msg.args];
+
+ args[0] = proxyChannel;
+ let rv = prompter2[msg.methodName](...args);
+ return {
+ rv,
+ // Send the args back to content so out/inout args can be checked.
+ args: msg.args,
+ };
+ });
+
+ addMessageListener("getTimeLastUsed", () => {
+ let logins = Services.logins.findLogins(mozproxyURL, null, "Proxy Realm");
+ return logins[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
+ });
+
+ function init() {
+ // Need to allow for arbitrary network servers defined in PAC instead of a hardcoded moz-proxy.
+ let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+
+ let channel = Services.io.newChannel(
+ "http://example.com",
+ null,
+ null,
+ null, // aLoadingNode
+ systemPrincipal,
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ pps.asyncResolve(channel, 0, resolveCallback);
+ }
+
+ class ProxyChannelListener {
+ onStartRequest(request) {
+ sendAsyncMessage("initDone");
+ }
+ onStopRequest(request, status) {}
+ }
+
+ async function initLogins(pi) {
+ mozproxyURL = `moz-proxy://${pi.host}:${pi.port}`;
+
+ let proxyLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+
+ proxyLogin.init(
+ mozproxyURL,
+ null,
+ "Proxy Realm",
+ "proxuser",
+ "proxpass",
+ "",
+ ""
+ );
+
+ await Services.logins.addLoginAsync(proxyLogin);
+ }
+
+ let resolveCallback = {
+ QueryInterface(iid) {
+ const interfaces = [Ci.nsIProtocolProxyCallback, Ci.nsISupports];
+
+ if (
+ !interfaces.some(function (v) {
+ return iid.equals(v);
+ })
+ ) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ }
+ return this;
+ },
+
+ async onProxyAvailable(req, uri, pi, status) {
+ await initLogins(pi);
+
+ // I'm cheating a bit here... We should probably do some magic foo to get
+ // something implementing nsIProxiedProtocolHandler and then call
+ // NewProxiedChannel(), so we have something that's definately a proxied
+ // channel. But Mochitests use a proxy for a number of hosts, so just
+ // requesting a normal channel will give us a channel that's proxied.
+ // The proxyChannel needs to move to at least on-modify-request to
+ // have valid ProxyInfo, but we use OnStartRequest during startup()
+ // for simplicity.
+ proxyChannel = Services.io.newChannel(
+ "http://mochi.test:8888",
+ null,
+ null,
+ null, // aLoadingNode
+ systemPrincipal,
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ proxyChannel.asyncOpen(new ProxyChannelListener());
+ },
+ };
+});
+
+let prompter2 = new PrompterProxy(chromeScript);
+
+add_setup(async () => {
+ let initComplete = new Promise((resolve) =>
+ chromeScript.addMessageListener("initDone", resolve)
+ );
+ chromeScript.sendAsyncMessage("init");
+ info("Waiting for startup to complete...");
+ await initComplete;
+});
+
+add_task(async function test_noAutologin() {
+ // test proxy login (default = no autologin), make sure it prompts.
+ let state = {
+ msg:
+ "The proxy moz-proxy://127.0.0.1:8888 is requesting a username and password. The site says: “Proxy Realm”",
+ title: "Authentication Required",
+ textValue: "proxuser",
+ passValue: "proxpass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ let action = {
+ buttonClick: "ok",
+ };
+ proxyAuthinfo.username = "";
+ proxyAuthinfo.password = "";
+ proxyAuthinfo.realm = "Proxy Realm";
+ proxyAuthinfo.flags = Ci.nsIAuthInformation.AUTH_PROXY;
+
+ let time1 = await chromeScript.sendQuery("getTimeLastUsed");
+ promptDone = handlePrompt(state, action);
+ let isOk = prompter2.promptAuth(null, LEVEL, proxyAuthinfo);
+ await promptDone;
+ let time2 = await chromeScript.sendQuery("getTimeLastUsed");
+
+ ok(isOk, "Checking dialog return value (accept)");
+ isnot(time1, time2, "Checking that timeLastUsed was updated");
+ is(proxyAuthinfo.username, "proxuser", "Checking returned username");
+ is(proxyAuthinfo.password, "proxpass", "Checking returned password");
+});
+
+add_task(async function test_autologin() {
+ // test proxy login (with autologin)
+
+ // Enable the autologin pref.
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.autologin.proxy", true]],
+ });
+
+ proxyAuthinfo.username = "";
+ proxyAuthinfo.password = "";
+ proxyAuthinfo.realm = "Proxy Realm";
+ proxyAuthinfo.flags = Ci.nsIAuthInformation.AUTH_PROXY;
+
+ let time1 = await chromeScript.sendQuery("getTimeLastUsed");
+ let isOk = prompter2.promptAuth(null, LEVEL, proxyAuthinfo);
+ let time2 = await chromeScript.sendQuery("getTimeLastUsed");
+
+ ok(isOk, "Checking dialog return value (accept)");
+ isnot(time1, time2, "Checking that timeLastUsed was updated");
+ is(proxyAuthinfo.username, "proxuser", "Checking returned username");
+ is(proxyAuthinfo.password, "proxpass", "Checking returned password");
+});
+
+add_task(async function test_autologin_incorrect() {
+ // test proxy login (with autologin), ensure it prompts after a failed auth.
+ let state = {
+ msg:
+ "The proxy moz-proxy://127.0.0.1:8888 is requesting a username and password. The site says: “Proxy Realm”",
+ title: "Authentication Required",
+ textValue: "proxuser",
+ passValue: "proxpass",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ let action = {
+ buttonClick: "ok",
+ };
+
+ proxyAuthinfo.username = "";
+ proxyAuthinfo.password = "";
+ proxyAuthinfo.realm = "Proxy Realm";
+ proxyAuthinfo.flags =
+ Ci.nsIAuthInformation.AUTH_PROXY | Ci.nsIAuthInformation.PREVIOUS_FAILED;
+
+ let time1 = await chromeScript.sendQuery("getTimeLastUsed");
+ promptDone = handlePrompt(state, action);
+ let isOk = prompter2.promptAuth(null, LEVEL, proxyAuthinfo);
+ await promptDone;
+ let time2 = await chromeScript.sendQuery("getTimeLastUsed");
+
+ ok(isOk, "Checking dialog return value (accept)");
+ isnot(time1, time2, "Checking that timeLastUsed was updated");
+ is(proxyAuthinfo.username, "proxuser", "Checking returned username");
+ is(proxyAuthinfo.password, "proxpass", "Checking returned password");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html b/toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html
new file mode 100644
index 0000000000..818a4e15bf
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for recipes overriding login fields</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+gTestDependsOnDeprecatedLogin = true;
+var chromeScript = runChecksAfterCommonInit();
+
+let fillPromiseResolvers = [];
+
+function waitForFills(fillCount) {
+ let promises = [];
+ while (fillCount--) {
+ let promise = new Promise(resolve => fillPromiseResolvers.push(resolve));
+ promises.push(promise);
+ }
+
+ return Promise.all(promises);
+}
+
+add_setup(async () => {
+ // This test should run without any existing loaded recipes interfering.
+ await resetRecipes();
+
+ if (document.readyState !== "complete") {
+ await new Promise((resolve) => {
+ document.onreadystatechange = () => {
+ if (document.readyState !== "complete") {
+ return;
+ }
+ document.onreadystatechange = null;
+ resolve();
+ };
+ });
+ }
+
+ document.getElementById("content")
+ .addEventListener("input", function handleInputEvent(evt) {
+ let resolve = fillPromiseResolvers.shift();
+ if (!resolve) {
+ ok(false, "Too many fills");
+ return;
+ }
+
+ resolve(evt.target);
+ });
+});
+
+add_task(async function loadUsernamePasswordSelectorRecipes() {
+ await loadRecipes({
+ siteRecipes: [{
+ hosts: ["mochi.test:8888"],
+ usernameSelector: "input[name='uname1']",
+ passwordSelector: "input[name='pword2']",
+ }],
+ });
+});
+
+add_task(async function testOverriddingFields() {
+ // Insert the form dynamically so autofill is triggered after setup above.
+ document.getElementById("content").innerHTML = `
+ <!-- form with recipe for the username and password -->
+ <form id="form1">
+ <input type="text" name="uname1" data-expected="true">
+ <input type="text" name="uname2" data-expected="false">
+ <input type="password" name="pword1" data-expected="false">
+ <input type="password" name="pword2" data-expected="true">
+ </form>`;
+
+ let elements = await waitForFills(2);
+ for (let element of elements) {
+ is(element.dataset.expected, "true", `${element.name} was filled`);
+ }
+});
+
+add_task(async function testDefaultHeuristics() {
+ // Insert the form dynamically so autofill is triggered after setup above.
+ document.getElementById("content").innerHTML = `
+ <!-- Fallback to the default heuristics since the selectors don't match -->
+ <form id="form2">
+ <input type="text" name="uname3" data-expected="false">
+ <input type="text" name="uname4" data-expected="true">
+ <input type="password" name="pword3" data-expected="true">
+ <input type="password" name="pword4" data-expected="false">
+ </form>`;
+
+ let elements = await waitForFills(2);
+ for (let element of elements) {
+ is(element.dataset.expected, "true", `${element.name} was filled`);
+ }
+});
+
+add_task(async function loadNotUsernameSelectorRecipes() {
+ await resetRecipes();
+ await loadRecipes({
+ siteRecipes: [{
+ hosts: ["mochi.test:8888"],
+ notUsernameSelector: "input[name='not_uname1']",
+ }],
+ });
+});
+
+add_task(async function testNotUsernameField() {
+ document.getElementById("content").innerHTML = `
+ <!-- The field matching notUsernameSelector should be skipped -->
+ <form id="form3">
+ <input type="text" name="uname5" data-expected="true">
+ <input type="text" name="not_uname1" data-expected="false">
+ <input type="password" name="pword5" data-expected="true">
+ </form>`;
+
+ let elements = await waitForFills(2);
+ for (let element of elements) {
+ is(element.dataset.expected, "true", `${element.name} was filled`);
+ }
+});
+
+add_task(async function testNotUsernameFieldNoUsername() {
+ document.getElementById("content").innerHTML = `
+ <!-- The field matching notUsernameSelector should be skipped.
+ No username field should be found and filled in this case -->
+ <form id="form4">
+ <input type="text" name="not_uname1" data-expected="false">
+ <input type="password" name="pword6" data-expected="true">
+ </form>`;
+
+ let elements = await waitForFills(1);
+ for (let element of elements) {
+ is(element.dataset.expected, "true", `${element.name} was filled`);
+ }
+});
+
+add_task(async function loadNotPasswordSelectorRecipes() {
+ await resetRecipes();
+ await loadRecipes({
+ siteRecipes: [{
+ hosts: ["mochi.test:8888"],
+ notPasswordSelector: "input[name='not_pword'], input[name='not_pword2']",
+ }],
+ });
+});
+
+add_task(async function testNotPasswordField() {
+ document.getElementById("content").innerHTML = `
+ <!-- The field matching notPasswordSelector should be skipped -->
+ <form id="form5">
+ <input type="text" name="uname7" data-expected="true">
+ <input type="password" name="not_pword" data-expected="false">
+ <input type="password" name="pword7" data-expected="true">
+ </form>`;
+
+ let elements = await waitForFills(2);
+ for (let element of elements) {
+ is(element.dataset.expected, "true", `${element.name} was filled`);
+ }
+});
+
+add_task(async function testNotPasswordFieldNoPassword() {
+ document.getElementById("content").innerHTML = `
+ <!-- The field matching notPasswordSelector should be skipped.
+ No username or password field should be found and filled in this case.
+ A dummy form7 is added after so we know when the login manager is done
+ considering filling form6. -->
+ <form id="form6">
+ <input type="text" name="uname8" data-expected="false">
+ <input type="password" name="not_pword" data-expected="false">
+ </form>
+ <form id="form7">
+ <input type="password" name="pword9" data-expected="true">
+ </form>`;
+
+ let elements = await waitForFills(1);
+ for (let element of elements) {
+ is(element.dataset.expected, "true", `${element.name} was filled`);
+ }
+});
+
+add_task(async function testNotPasswordField_tooManyToOkay() {
+ document.getElementById("content").innerHTML = `
+ <!-- The field matching notPasswordSelector should be skipped so we won't
+ have too many pw fields to handle (3). -->
+ <form id="form8">
+ <input type="text" name="uname9" data-expected="true">
+ <input type="password" name="not_pword2" data-expected="false">
+ <input type="password" name="not_pword" data-expected="false">
+ <input type="password" name="pword10" data-expected="true">
+ <input type="password" name="pword11" data-expected="false">
+ <input type="password" name="pword12" data-expected="false">
+ </form>`;
+
+ let elements = await waitForFills(2);
+ for (let element of elements) {
+ is(element.dataset.expected, "true", `${element.name} was filled`);
+ }
+});
+
+</script>
+
+<p id="display"></p>
+
+<div id="content">
+ // Forms are inserted dynamically
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_submit_without_field_modifications.html b/toolkit/components/passwordmgr/test/mochitest/test_submit_without_field_modifications.html
new file mode 100644
index 0000000000..db06561b9d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_submit_without_field_modifications.html
@@ -0,0 +1,313 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Don't send onFormSubmit message on navigation if the user did not interact
+ with the login fields</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+
+<div id="content">
+ <iframe id="loginFrame">
+ </iframe>
+</div>
+
+<pre id="test"></pre>
+<script>
+const { TestUtils } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+);
+SimpleTest.requestFlakyTimeout("Giving a chance for the unexpected popup to show");
+
+const EXAMPLE_COM = window.location.origin + "/tests/toolkit/components/passwordmgr/test/mochitest/";
+const PREFILLED_FORM_URL = EXAMPLE_COM + "subtst_prefilled_form.html"
+
+let iframe = document.getElementById("loginFrame");
+
+function waitForLoad() {
+ return new Promise(resolve => {
+ function handleLoad() {
+ iframe.removeEventListener("load", handleLoad);
+ resolve();
+ }
+ iframe.addEventListener("load", handleLoad);
+ });
+}
+
+async function setupWithOneLogin(pageUrl) {
+ let origin = window.location.origin;
+ addLoginsInParent([origin, origin, null, "user1", "pass1"]);
+
+ let chromeScript = runInParent(function testSetup() {
+ for (let l of Services.logins.getAllLogins()) {
+ info("Got login: " + l.username + ", " + l.password);
+ }
+ });
+
+ await setup(pageUrl);
+ return chromeScript;
+}
+
+function resetSavedLogins() {
+ let chromeScript = runInParent(function testTeardown() {
+ Services.logins.removeAllUserFacingLogins();
+ });
+ chromeScript.destroy();
+}
+
+async function setup(pageUrl) {
+ let loadPromise = waitForLoad();
+ let processedFormPromise = promiseFormsProcessed();
+ iframe.src = pageUrl;
+
+ await processedFormPromise;
+ info("initial form processed");
+ await loadPromise;
+ await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ let doc = this.content.document;
+ let link = doc.createElement("a");
+ link.setAttribute("href", "http://mochi.test:8888");
+ doc.body.appendChild(link);
+ });
+}
+
+async function navigateWithoutUserInteraction() {
+ let loadPromise = waitForLoad();
+ await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ let doc = this.content.document;
+ let hadInteracted = doc.userHasInteracted;
+ let target = doc.querySelector("a[href]");
+ if (target) {
+ target.click();
+ } else {
+ target = doc.querySelector("form");
+ target.submit();
+ }
+ is(doc.userHasInteracted, hadInteracted, "document.userHasInteracted shouldn't have changed");
+ });
+ await loadPromise;
+}
+
+async function userInput(selector, value) {
+ await SpecialPowers.spawn(getIframeBrowsingContext(window), [selector, value], async function(sel, val) {
+ // use "real" synthesized events rather than setUserInput to ensure
+ // document.userHasInteracted is flipped true
+ let EventUtils = ContentTaskUtils.getEventUtils(content);
+ let target = this.content.document.querySelector(sel);
+ target.focus();
+ target.select();
+ await EventUtils.synthesizeKey("KEY_Backspace", {}, this.content);
+ await EventUtils.sendString(val, this.content);
+ info(
+ `userInput: new target.value: ${target.value}`
+ );
+ target.blur();
+ return Promise.resolve();
+ });
+}
+
+function checkDocumentUserHasInteracted() {
+ return SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ return this.content.document.userHasInteracted;
+ });
+}
+
+add_task(async function test_init() {
+ // For this test, we'll be testing with & without user document interaction.
+ // So we'll reset the pref which dictates the behavior of
+ // LoginFormState._formHasModifiedFields in automation
+ // and ensure all interactions are properly emulated
+ ok(SpecialPowers.getBoolPref("signon.testOnlyUserHasInteractedByPrefValue"), "signon.testOnlyUserHasInteractedByPrefValue should default to true");
+ info("test_init, flipping the signon.testOnlyUserHasInteractedByPrefValue pref");
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["signon.testOnlyUserHasInteractedByPrefValue", false],
+ ]});
+ SimpleTest.registerCleanupFunction(async function cleanup_pref() {
+ await SpecialPowers.popPrefEnv();
+ });
+
+ await SimpleTest.promiseWaitForCondition(() => LoginHelper.testOnlyUserHasInteractedWithDocument === null);
+ is(LoginHelper.testOnlyUserHasInteractedWithDocument, null,
+ "LoginHelper.testOnlyUserHasInteractedWithDocument should be null for this set of tests");
+});
+
+add_task(async function test_no_message_on_navigation() {
+ // If login field values were set by the website, we don't message to save the
+ // login values if the user did not interact with the fields before submiting.
+ await setup(PREFILLED_FORM_URL);
+
+ let submitMessageSent = false;
+ getSubmitMessage().then(value => {
+ submitMessageSent = true;
+ });
+ await navigateWithoutUserInteraction();
+
+ // allow time to pass before concluding no onFormSubmit message was sent
+ await new Promise(res => setTimeout(res, 1000));
+ ok(!submitMessageSent, "onFormSubmit message is not sent on navigation since the login fields were not modified");
+});
+
+add_task(async function test_prefd_off_message_on_navigation() {
+ // Confirm the pref controls capture behavior with non-user-set field values.
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["signon.userInputRequiredToCapture.enabled", false],
+ ]});
+ await setup(PREFILLED_FORM_URL);
+
+ let promiseSubmitMessage = getSubmitMessage();
+ await navigateWithoutUserInteraction();
+ await promiseSubmitMessage;
+ info("onFormSubmit message was sent as expected after navigation");
+
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_message_with_user_interaction_on_navigation() {
+ await setup(PREFILLED_FORM_URL);
+ await userInput("#form-basic-username", "foo");
+
+ let promiseSubmitMessage = getSubmitMessage();
+ await navigateWithoutUserInteraction();
+ await promiseSubmitMessage;
+ info("onFormSubmit message was sent as expected after user interaction");
+});
+
+add_task(async function test_empty_form_with_input_handler() {
+ await setup(EXAMPLE_COM + "formless_basic.html");
+ await userInput("#form-basic-username", "user");
+ await userInput("#form-basic-password", "pass");
+
+ let promiseSubmitMessage = getSubmitMessage();
+ await navigateWithoutUserInteraction();
+ await promiseSubmitMessage;
+ info("onFormSubmit message was sent as expected after user interaction");
+});
+
+add_task(async function test_no_message_on_autofill_without_user_interaction() {
+ let chromeScript = await setupWithOneLogin(EXAMPLE_COM + "form_basic.html");
+ // Check for autofilled values.
+ await checkLoginFormInFrame(getIframeBrowsingContext(window, 0),
+ "form-basic-username", "user1",
+ "form-basic-password", "pass1");
+
+ info("LoginHelper.testOnlyUserHasInteractedWithDocument:" +
+ LoginHelper.testOnlyUserHasInteractedWithDocument
+ );
+ ok(!(await checkDocumentUserHasInteracted()), "document.userHasInteracted should be initially false");
+ let submitMessageSent = false;
+ getSubmitMessage().then(value => {
+ submitMessageSent = true;
+ });
+ info("Navigating the page")
+ await navigateWithoutUserInteraction();
+
+ // allow time to pass before concluding no onFormSubmit message was sent
+ await new Promise(res => setTimeout(res, 1000));
+
+ chromeScript.destroy();
+ resetSavedLogins();
+
+ ok(!submitMessageSent, "onFormSubmit message is not sent on navigation since the document had no user interaction");
+});
+
+add_task(async function test_message_on_autofill_with_document_interaction() {
+ // We expect that as long as the form values !== their defaultValues,
+ // any document interaction allows the submit message to be sent
+
+ let chromeScript = await setupWithOneLogin(EXAMPLE_COM + "form_basic.html");
+ // Check for autofilled values.
+ await checkLoginFormInFrame(getIframeBrowsingContext(window, 0),
+ "form-basic-username", "user1",
+ "form-basic-password", "pass1");
+
+ let userInteracted = await checkDocumentUserHasInteracted();
+ ok(!userInteracted, "document.userHasInteracted should be initially false");
+
+ await SpecialPowers.spawn(getIframeBrowsingContext(window), ["#form-basic-username"], async function(sel) {
+ // Click somewhere in the document to ensure document.userHasInteracted is flipped to true
+ let EventUtils = ContentTaskUtils.getEventUtils(content);
+ let target = this.content.document.querySelector(sel);
+
+ await EventUtils.synthesizeMouseAtCenter(target, {}, this.content);
+ });
+
+ userInteracted = await checkDocumentUserHasInteracted();
+ ok(userInteracted, "After synthesizeMouseAtCenter, document.userHasInteracted should be true");
+
+ let promiseSubmitMessage = getSubmitMessage();
+ await navigateWithoutUserInteraction();
+
+ let { data } = await promiseSubmitMessage;
+ ok(data.autoFilledLoginGuid, "Message was sent with autoFilledLoginGuid");
+ info("Message was sent as expected after document user interaction");
+
+ chromeScript.destroy();
+ resetSavedLogins();
+});
+
+add_task(async function test_message_on_autofill_with_user_interaction() {
+ // Editing a field value causes the submit message to be sent as
+ // there is both document interaction and field modification
+ let chromeScript = await setupWithOneLogin(EXAMPLE_COM + "form_basic.html");
+ // Check for autofilled values.
+ await checkLoginFormInFrame(getIframeBrowsingContext(window, 0),
+ "form-basic-username", "user1",
+ "form-basic-password", "pass1");
+
+ userInput("#form-basic-username", "newuser");
+ let promiseSubmitMessage = getSubmitMessage();
+ await navigateWithoutUserInteraction();
+
+ let { data } = await promiseSubmitMessage;
+ ok(data.autoFilledLoginGuid, "Message was sent with autoFilledLoginGuid");
+ is(data.usernameField.value, "newuser", "Message was sent with correct usernameField.value");
+ info("Message was sent as expected after user form interaction");
+
+ chromeScript.destroy();
+ resetSavedLogins();
+});
+
+add_task(async function test_no_message_on_user_input_from_other_form() {
+ // ensure input into unrelated fields on the page don't change login form modified-ness
+ await setup(PREFILLED_FORM_URL);
+
+ // Add a form which will not be submitted and an input associated with that form
+ await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
+ let doc = this.content.document;
+ let loginForm = doc.querySelector("form");
+ let fragment = doc.createDocumentFragment();
+ let otherForm = doc.createElement("form");
+ otherForm.id ="otherForm";
+ fragment.appendChild(otherForm);
+
+ let alienField = doc.createElement("input");
+ alienField.id = "alienField";
+ alienField.type = "text"; // not a password field
+ alienField.setAttribute("form", "otherForm");
+ // new field is child of the login, but a member of different non-login form via its .form property
+ loginForm.appendChild(alienField);
+ doc.body.appendChild(fragment);
+ });
+ await userInput("#alienField", "something");
+
+ let submitMessageSent = false;
+ getSubmitMessage().then(data => {
+ info("submit mesage data: " + JSON.stringify(data));
+ submitMessageSent = true;
+ });
+
+ info("submitting the form");
+ await navigateWithoutUserInteraction();
+
+ // allow time to pass before concluding no onFormSubmit message was sent
+ await new Promise(res => setTimeout(res, 1000));
+ ok(!submitMessageSent, "onFormSubmit message is not sent on navigation since no login fields were modified");
+});
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html b/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html
new file mode 100644
index 0000000000..510cb2e1f1
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html
@@ -0,0 +1,166 @@
+
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test interaction between autocomplete and focus on username fields</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const action1 = "http://username-focus-1";
+const action2 = "http://username-focus-2";
+
+add_setup(async () => {
+ await addLoginsInParent(
+ [location.origin, action1, null, "testuser1A", "testpass1A", "", ""],
+ [location.origin, action2, null, "testuser2A", "testpass2A", "", ""],
+ [location.origin, action2, null, "testuser2B", "testpass2B", "", ""]
+ );
+});
+
+add_task(async function autofilled() {
+ const form = createLoginForm({ action: action1 });
+
+ info("Username and password already filled so don't show autocomplete");
+ await noPopupBy(() => form.uname.focus());
+
+ form.submit.focus();
+ form.uname.value = "testuser";
+ info("Focus when we don't have an exact match");
+ await popupBy(() => form.uname.focus());
+});
+
+add_task(async function autofilled_prefilled_un() {
+ const form = createLoginForm({
+ action: action1,
+ username: {
+ value: "testuser1A"
+ }
+ });
+
+ info("Username and password already filled so don't show autocomplete");
+ await noPopupBy(() => form.uname.focus());
+
+ form.submit.focus();
+ form.uname.value = "testuser";
+ info("Focus when we don't have an exact match");
+ await popupBy(() => form.uname.focus());
+});
+
+add_task(async function autofilled_focused_dynamic() {
+ const form = createLoginForm({
+ action: action1,
+ password: {
+ type: "not-yet-password"
+ }
+ });
+
+ info("Username and password will be filled while username focused");
+ await noPopupBy(() => form.uname.focus());
+
+ info("triggering autofill");
+ await noPopupBy(() => form.pword.type = "password");
+
+ const popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is closed");
+
+ form.submit.focus();
+ form.pword.value = "test";
+ info("Focus when we don't have an exact match");
+ await popupBy(() => form.uname.focus());
+});
+
+// Begin testing forms that have multiple saved logins
+
+add_task(async function multiple() {
+ const form = createLoginForm({ action: action2 });
+
+ info("Fields not filled due to multiple so autocomplete upon focus");
+ await popupBy(() => form.uname.focus());
+});
+
+add_task(async function multiple_dynamic() {
+ const form = createLoginForm({
+ action: action2,
+ password: {
+ type: "not-yet-password"
+ }
+ });
+
+ info("Fields not filled but username is focused upon marking so open");
+ await noPopupBy(() => form.uname.focus());
+
+ info("triggering _fillForm code");
+ await popupBy(() => form.pword.type = "password");
+});
+
+add_task(async function multiple_prefilled_un1() {
+ const form = createLoginForm({
+ action: action2,
+ username: {
+ value: "testuser2A"
+ }
+ });
+
+ info("Username and password already filled so don't show autocomplete");
+ await noPopupBy(() => form.uname.focus());
+
+ form.submit.focus();
+ form.uname.value = "testuser";
+ info("Focus when we don't have an exact match");
+ await popupBy(() => form.uname.focus());
+});
+
+add_task(async function multiple_prefilled_un2() {
+ const form = createLoginForm({
+ action: action2,
+ username: {
+ value: "testuser2B"
+ }
+ });
+
+ info("Username and password already filled so don't show autocomplete");
+ await noPopupBy(() => form.uname.focus());
+
+ form.submit.focus();
+ form.uname.value = "testuser";
+ info("Focus when we don't have an exact match");
+ await popupBy(() => form.uname.focus());
+});
+
+add_task(async function multiple_prefilled_focused_dynamic() {
+ const form = createLoginForm({
+ action: action2,
+ username: {
+ value: "testuser2B"
+ },
+ password: {
+ type: "not-yet-password"
+ }
+ });
+
+ info("Username and password will be filled while username focused");
+ await noPopupBy(() => form.uname.focus());
+ info("triggering autofill");
+ await noPopupBy(() => form.pword.type = "password");
+
+ let popupState = await getPopupState();
+ is(popupState.open, false, "Check popup is closed");
+
+ form.submit.focus();
+ form.pword.value = "test";
+ info("Focus when we don't have an exact match");
+ await popupBy(() => form.uname.focus());
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_xhr.html b/toolkit/components/passwordmgr/test/mochitest/test_xhr.html
new file mode 100644
index 0000000000..ac573d2b02
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_xhr.html
@@ -0,0 +1,164 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for XHR prompts</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: XHR prompt
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <iframe id="iframe"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Login Manager: XHR prompts. **/
+function makeRequest(uri) {
+ return new Promise((resolve, reject) => {
+ let request = new XMLHttpRequest();
+ request.open("GET", uri, true);
+ request.addEventListener("loadend", function onLoadEnd() {
+ let result = xhrLoad(request.responseXML);
+ resolve(result);
+ });
+ request.send(null);
+ });
+}
+
+function xhrLoad(xmlDoc) {
+ // The server echos back the user/pass it received.
+ var username = xmlDoc.getElementById("user").textContent;
+ var password = xmlDoc.getElementById("pass").textContent;
+ var authok = xmlDoc.getElementById("ok").textContent;
+ return {username, password, authok};
+}
+
+// Let prompt_common know what kind of modal type is enabled for auth prompts.
+modalType = authPromptModalType;
+
+let prompterParent = runInParent(() => {
+ const promptFac = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"].
+ getService(Ci.nsIPromptFactory);
+
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let prompt = promptFac.getPrompt(chromeWin, Ci.nsIAuthPrompt);
+
+ addMessageListener("proxyPrompter", function onMessage(msg) {
+ let rv = prompt[msg.methodName](...msg.args);
+ return {
+ rv,
+ // Send the args back to content so out/inout args can be checked.
+ args: msg.args,
+ };
+ });
+});
+
+let prompter1 = new PrompterProxy(prompterParent);
+
+add_setup(async () => {
+ await addLoginsInParent(
+ ["http://mochi.test:8888", null, "xhr", "xhruser1", "xhrpass1"],
+ ["http://mochi.test:8888", null, "xhr2", "xhruser2", "xhrpass2"]
+ );
+});
+
+add_task(async function test1() {
+ let state = {
+ msg: "This site is asking you to sign in.",
+ title: "Authentication Required",
+ textValue: "xhruser1",
+ passValue: "xhrpass1",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ let action = {
+ buttonClick: "ok",
+ };
+ let promptDone = handlePrompt(state, action);
+ let requestPromise = makeRequest("authenticate.sjs?user=xhruser1&pass=xhrpass1&realm=xhr");
+ await promptDone;
+ let result = await requestPromise;
+
+ is(result.authok, "PASS", "Checking for successful authentication");
+ is(result.username, "xhruser1", "Checking for username");
+ is(result.password, "xhrpass1", "Checking for password");
+});
+
+add_task(async function test2() {
+ // Test correct parenting, by opening another tab in the foreground,
+ // and making sure the prompt re-focuses the original tab when shown:
+ let newWin = window.open();
+ newWin.focus();
+
+ let state = {
+ msg: "This site is asking you to sign in.",
+ title: "Authentication Required",
+ textValue: "xhruser2",
+ passValue: "xhrpass2",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+
+ // For window prompts check that the dialog is modal, chrome and dependent;
+ // We can't just check window.opener because that'll be
+ // a content window, which therefore isn't exposed (it'll lie and
+ // be null).
+ if (authPromptModalType === SpecialPowers.Services.prompt.MODAL_TYPE_WINDOW) {
+ state.chrome = true;
+ state.dialog = true;
+ state.chromeDependent = true;
+ state.isWindowModal = true;
+ }
+
+ let action = {
+ buttonClick: "ok",
+ };
+ let promptDone = handlePrompt(state, action);
+ let requestPromise = makeRequest("authenticate.sjs?user=xhruser2&pass=xhrpass2&realm=xhr2");
+ await promptDone;
+ let result = await requestPromise;
+
+ runInParent(() => {
+ // Check that the right tab is focused:
+ let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let spec = browserWin.gBrowser.selectedBrowser.currentURI.spec;
+ assert.ok(spec.startsWith(window.location.origin),
+ `Tab with remote URI (rather than about:blank)
+ should be focused (${spec})`);
+ });
+
+ is(result.authok, "PASS", "Checking for successful authentication");
+ is(result.username, "xhruser2", "Checking for username");
+ is(result.password, "xhrpass2", "Checking for password");
+
+ // Wait for the assert from the parent script to run and send back its reply,
+ // so it's processed before the test ends.
+ await SpecialPowers.executeAfterFlushingMessageQueue();
+
+ newWin.close();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html b/toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html
new file mode 100644
index 0000000000..16d5e786e0
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=654348
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test XHR auth with user and pass arguments</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="pwmgr_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="startTest()">
+<script class="testbody" type="text/javascript">
+
+/**
+ * This test checks we correctly ignore authentication entry
+ * for a subpath and use creds from the URL when provided when XHR
+ * is used with filled user name and password.
+ *
+ * 1. connect authenticate.sjs that excepts user1:pass1 password
+ * 2. connect authenticate.sjs that this time expects differentuser2:pass2 password
+ * we must use the creds that are provided to the xhr witch are different and expected
+ */
+
+function doxhr(URL, user, pass, code, next) {
+ var xhr = new XMLHttpRequest();
+ if (user && pass) {
+ xhr.open("POST", URL, true, user, pass);
+ } else {
+ xhr.open("POST", URL, true);
+ }
+ xhr.onload = function() {
+ is(xhr.status, code, "expected response code " + code);
+ next();
+ };
+ xhr.onerror = function() {
+ ok(false, "request passed");
+ finishTest();
+ };
+ xhr.send();
+}
+
+function startTest() {
+ doxhr("authenticate.sjs?user=dummy&pass=pass1&realm=realm1&formauth=1", "dummy", "dummy", 403, function() {
+ doxhr("authenticate.sjs?user=dummy&pass=pass1&realm=realm1&formauth=1", "dummy", "pass1", 200, finishTest);
+ });
+}
+
+function finishTest() {
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+</html>