summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/test/browser
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/test/browser')
-rw-r--r--toolkit/components/passwordmgr/test/browser/.eslintrc.js7
-rw-r--r--toolkit/components/passwordmgr/test/browser/authenticate.sjs84
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser.ini162
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPassword.js152
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPossibleUsername.js254
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_DOMInputPasswordAdded.js101
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_autocomplete_autofocus_with_frame.js48
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_autocomplete_disabled_readonly_passwordField.js138
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_autocomplete_footer.js125
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_autocomplete_generated_password_private_window.js111
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_autocomplete_import.js259
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js44
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_autocomplete_primary_password.js121
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_autofill_hidden_document.js205
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_autofill_http.js135
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_autofill_track_filled_logins.js111
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_basicAuth_multiTab.js158
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_basicAuth_rateLimit.js146
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_basicAuth_switchTab.js34
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_context_menu.js678
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js120
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_context_menu_generated_password.js482
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_context_menu_iframe.js223
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_crossOriginSubmissionUsesCorrectOrigin.js53
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_deleteLoginsBackup.js282
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_autocomplete_values.js274
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_autofill_then_save_password.js181
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_crossframe.js236
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_dismissed_for_ccnumber.js202
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_empty_password.js42
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_form_password_edit.js562
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_generated_password.js1845
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_httpsUpgrade.js303
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_multipage_form.js182
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_password_edits.js220
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_promptToChangePassword.js685
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_remembering.js1275
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_replace_dismissed_with_visible_while_opening.js65
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_save_password.js159
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_submit_telemetry.js387
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_target_blank.js94
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_toggles.js478
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_username_edits.js192
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_doorhanger_window_open.js201
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_entry_point_telemetry.js103
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_exceptions_dialog.js141
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_fileURIOrigin.js51
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_focus_before_first_DOMContentLoaded.js103
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_form_history_fallback.js65
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js161
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_insecurePasswordConsoleWarning.js131
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_isProbablyASignUpForm.js42
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_localip_frame.js86
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_message_onFormSubmit.js82
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_openPasswordManager.js161
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_preselect_login.js183
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_private_window.js954
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_proxyAuth_prompt.js182
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_relay_telemetry.js514
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_telemetry_SignUpFormRuleset.js57
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_test_changeContentInputValue.js129
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_username_only_form_telemetry.js198
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_username_select_dialog.js177
-rw-r--r--toolkit/components/passwordmgr/test/browser/empty.html8
-rw-r--r--toolkit/components/passwordmgr/test/browser/file_focus_before_DOMContentLoaded.sjs35
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_autofocus_frame.html10
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_autofocus_js.html10
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_basic.html12
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_basic_iframe.html23
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_basic_login.html12
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_basic_no_username.html11
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_basic_signup.html10
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_basic_with_confirm_field.html13
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_cross_origin_insecure_action.html12
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_cross_origin_secure_action.html12
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_crossframe.html13
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_crossframe_inner.html13
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_disabled_readonly_passwordField.html12
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_expanded.html16
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_multipage.html32
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_password_change.html17
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_same_origin_action.html12
-rw-r--r--toolkit/components/passwordmgr/test/browser/form_signup_detection.html31
-rw-r--r--toolkit/components/passwordmgr/test/browser/formless_basic.html18
-rw-r--r--toolkit/components/passwordmgr/test/browser/head.js965
-rw-r--r--toolkit/components/passwordmgr/test/browser/insecure_test.html9
-rw-r--r--toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html16
-rw-r--r--toolkit/components/passwordmgr/test/browser/multiple_forms.html145
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_1.html29
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_10.html27
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html26
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html32
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_12_target_blank.html31
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_2.html30
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_0un.html27
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_1un_1text.html31
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_3.html30
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_4.html30
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_5.html26
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_6.html27
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_8.html29
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_9.html29
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_notifications_change_p.html32
-rw-r--r--toolkit/components/passwordmgr/test/browser/subtst_privbrowsing_1.html16
104 files changed, 17255 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/test/browser/.eslintrc.js b/toolkit/components/passwordmgr/test/browser/.eslintrc.js
new file mode 100644
index 0000000000..000a981cbe
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ rules: {
+ "no-var": "off",
+ },
+};
diff --git a/toolkit/components/passwordmgr/test/browser/authenticate.sjs b/toolkit/components/passwordmgr/test/browser/authenticate.sjs
new file mode 100644
index 0000000000..8f463dda6f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/authenticate.sjs
@@ -0,0 +1,84 @@
+function handleRequest(request, response) {
+ var match;
+ var requestAuth = 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.
+ var query = "?" + request.queryString;
+
+ var expected_user = "test",
+ expected_pass = "testpass",
+ realm = "mochitest";
+
+ // 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];
+ }
+
+ // 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.
+
+ var actual_user = "",
+ actual_pass = "",
+ authHeader;
+
+ if (request.hasHeader("Authorization")) {
+ authHeader = request.getHeader("Authorization");
+ match = /Basic (.+)/.exec(authHeader);
+ if (match.length != 2) {
+ throw new Error("Couldn't parse auth header: " + authHeader);
+ }
+
+ var 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];
+ }
+
+ // 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 (requestAuth) {
+ response.setStatusLine("1.0", 401, "Authentication required");
+ 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>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");
+ response.write("</html>");
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser.ini b/toolkit/components/passwordmgr/test/browser/browser.ini
new file mode 100644
index 0000000000..ae41ef2c47
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser.ini
@@ -0,0 +1,162 @@
+[DEFAULT]
+support-files =
+ ../formsubmit.sjs
+ authenticate.sjs
+ empty.html
+ form_basic.html
+ form_basic_iframe.html
+ form_basic_login.html
+ form_basic_signup.html
+ form_basic_no_username.html
+ formless_basic.html
+ form_multipage.html
+ form_same_origin_action.html
+ form_cross_origin_secure_action.html
+ form_cross_origin_insecure_action.html
+ form_expanded.html
+ insecure_test_subframe.html
+ head.js
+ multiple_forms.html
+ ../../../../../browser/components/aboutlogins/tests/browser/head.js
+
+
+[browser_DOMFormHasPassword.js]
+[browser_DOMFormHasPossibleUsername.js]
+[browser_DOMInputPasswordAdded.js]
+skip-if = (os == "linux") || (os == "mac") # Bug 1337606
+[browser_autocomplete_autofocus_with_frame.js]
+support-files =
+ form_autofocus_frame.html
+[browser_autocomplete_disabled_readonly_passwordField.js]
+support-files =
+ form_disabled_readonly_passwordField.html
+[browser_autocomplete_footer.js]
+skip-if =
+ !debug && os == "linux" && bits == 64 && os_version == "18.04" # Bug 1591126
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_autocomplete_generated_password_private_window.js]
+[browser_autocomplete_import.js]
+https_first_disabled = true
+skip-if =
+ os == "mac" # Bug 1775902
+ os == "win" && !debug # Bug 1775902
+[browser_autocomplete_insecure_warning.js]
+[browser_autocomplete_primary_password.js]
+[browser_autofill_hidden_document.js]
+skip-if =
+ (os == "win" && os_version == "10.0" && debug) # bug 1530935
+ apple_catalina && fission && !debug # high frequency intermittent, Bug 1716486
+
+[browser_autofill_http.js]
+https_first_disabled = true
+skip-if = verify
+[browser_autofill_track_filled_logins.js]
+[browser_basicAuth_multiTab.js]
+skip-if = os == "android"
+[browser_basicAuth_rateLimit.js]
+[browser_basicAuth_switchTab.js]
+skip-if = (debug && os == "mac") # Bug 1530566
+[browser_context_menu.js]
+[browser_context_menu_autocomplete_interaction.js]
+skip-if =
+ verify
+[browser_context_menu_generated_password.js]
+[browser_context_menu_iframe.js]
+[browser_crossOriginSubmissionUsesCorrectOrigin.js]
+support-files =
+ form_cross_origin_secure_action.html
+[browser_deleteLoginsBackup.js]
+skip-if = os == "android"
+[browser_doorhanger_autocomplete_values.js]
+[browser_doorhanger_autofill_then_save_password.js]
+[browser_doorhanger_crossframe.js]
+support-files =
+ form_crossframe.html
+ form_crossframe_inner.html
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_doorhanger_dismissed_for_ccnumber.js]
+[browser_doorhanger_empty_password.js]
+[browser_doorhanger_form_password_edit.js]
+[browser_doorhanger_generated_password.js]
+support-files =
+ form_basic_with_confirm_field.html
+ form_password_change.html
+[browser_doorhanger_httpsUpgrade.js]
+support-files =
+ subtst_notifications_1.html
+ subtst_notifications_8.html
+[browser_doorhanger_multipage_form.js]
+[browser_doorhanger_password_edits.js]
+[browser_doorhanger_promptToChangePassword.js]
+[browser_doorhanger_remembering.js]
+[browser_doorhanger_replace_dismissed_with_visible_while_opening.js]
+[browser_doorhanger_save_password.js]
+[browser_doorhanger_submit_telemetry.js]
+skip-if =
+ tsan # Bug 1661305
+ os == "linux" && (debug || asan) # Bug 1658056, asan: 1695395
+[browser_doorhanger_target_blank.js]
+support-files =
+ subtst_notifications_12_target_blank.html
+[browser_doorhanger_toggles.js]
+[browser_doorhanger_username_edits.js]
+[browser_doorhanger_window_open.js]
+support-files =
+ subtst_notifications_11.html
+ subtst_notifications_11_popup.html
+skip-if = os == "linux" # Bug 1312981, bug 1313136
+[browser_entry_point_telemetry.js]
+[browser_exceptions_dialog.js]
+[browser_fileURIOrigin.js]
+[browser_focus_before_first_DOMContentLoaded.js]
+support-files =
+ file_focus_before_DOMContentLoaded.sjs
+[browser_form_history_fallback.js]
+https_first_disabled = true # TODO remove that line and move test to HTTPS, see Bug 1776350
+skip-if = os == "linux" && debug # Bug 1334336
+support-files =
+ subtst_notifications_1.html
+ subtst_notifications_2.html
+ subtst_notifications_2pw_0un.html
+ subtst_notifications_2pw_1un_1text.html
+ subtst_notifications_3.html
+ subtst_notifications_4.html
+ subtst_notifications_5.html
+ subtst_notifications_6.html
+ subtst_notifications_8.html
+ subtst_notifications_9.html
+ subtst_notifications_10.html
+ subtst_notifications_change_p.html
+[browser_formless_submit_chrome.js]
+skip-if = tsan # Bug 1683730
+[browser_insecurePasswordConsoleWarning.js]
+https_first_disabled = true
+skip-if = verify
+[browser_isProbablyASignUpForm.js]
+support-files =
+ form_signup_detection.html
+[browser_localip_frame.js]
+skip-if =
+ os == 'mac' && bits == 64 # Bug 1683848
+ os == 'linux' && !debug && bits == 64 # Bug 1683848
+ win10_2004 && !fission # Bug 1723573
+[browser_message_onFormSubmit.js]
+[browser_openPasswordManager.js]
+[browser_preselect_login.js]
+[browser_private_window.js]
+support-files =
+ subtst_privbrowsing_1.html
+ form_password_change.html
+skip-if =
+ os == 'linux' && bits == 64 && os_version == '18.04' && !debug # Bug 1744976
+ os == 'win' && os_version == '10.0' && debug # Bug 1782656
+[browser_proxyAuth_prompt.js]
+skip-if = os == "android"
+[browser_relay_telemetry.js]
+[browser_telemetry_SignUpFormRuleset.js]
+[browser_test_changeContentInputValue.js]
+[browser_username_only_form_telemetry.js]
+[browser_username_select_dialog.js]
+support-files =
+ subtst_notifications_change_p.html \ No newline at end of file
diff --git a/toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPassword.js b/toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPassword.js
new file mode 100644
index 0000000000..6d4d333369
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPassword.js
@@ -0,0 +1,152 @@
+const ids = {
+ INPUT_ID: "input1",
+ FORM1_ID: "form1",
+ FORM2_ID: "form2",
+ CHANGE_INPUT_ID: "input2",
+};
+
+function task(contentIds) {
+ let resolve;
+ let promise = new Promise(r => {
+ resolve = r;
+ });
+
+ function unexpectedContentEvent(evt) {
+ Assert.ok(false, "Received a " + evt.type + " event on content");
+ }
+
+ var gDoc = null;
+
+ addEventListener("load", tabLoad, true);
+
+ function tabLoad() {
+ if (content.location.href == "about:blank") {
+ return;
+ }
+ removeEventListener("load", tabLoad, true);
+
+ gDoc = content.document;
+ gDoc.addEventListener("DOMFormHasPassword", unexpectedContentEvent);
+ addEventListener("DOMFormHasPassword", unexpectedContentEvent);
+ gDoc.defaultView.setTimeout(test_inputAdd, 0);
+ }
+
+ function test_inputAdd() {
+ addEventListener("DOMFormHasPassword", test_inputAddHandler, {
+ once: true,
+ capture: true,
+ });
+ let input = gDoc.createElementNS("http://www.w3.org/1999/xhtml", "input");
+ input.setAttribute("type", "password");
+ input.setAttribute("id", contentIds.INPUT_ID);
+ input.setAttribute("data-test", "unique-attribute");
+ gDoc.getElementById(contentIds.FORM1_ID).appendChild(input);
+ }
+
+ function test_inputAddHandler(evt) {
+ evt.stopPropagation();
+ Assert.equal(
+ evt.target.id,
+ contentIds.FORM1_ID,
+ evt.type + " event targets correct form element (added password element)"
+ );
+ gDoc.defaultView.setTimeout(test_inputChangeForm, 0);
+ }
+
+ function test_inputChangeForm() {
+ addEventListener("DOMFormHasPassword", test_inputChangeFormHandler, {
+ once: true,
+ capture: true,
+ });
+ let input = gDoc.getElementById(contentIds.INPUT_ID);
+ input.setAttribute("form", contentIds.FORM2_ID);
+ }
+
+ function test_inputChangeFormHandler(evt) {
+ evt.stopPropagation();
+ Assert.equal(
+ evt.target.id,
+ contentIds.FORM2_ID,
+ evt.type + " event targets correct form element (changed form)"
+ );
+ gDoc.defaultView.setTimeout(test_inputChangesType, 0);
+ }
+
+ function test_inputChangesType() {
+ addEventListener("DOMFormHasPassword", test_inputChangesTypeHandler, {
+ once: true,
+ capture: true,
+ });
+ let input = gDoc.getElementById(contentIds.CHANGE_INPUT_ID);
+ input.setAttribute("type", "password");
+ }
+
+ function test_inputChangesTypeHandler(evt) {
+ evt.stopPropagation();
+ Assert.equal(
+ evt.target.id,
+ contentIds.FORM1_ID,
+ evt.type + " event targets correct form element (changed type)"
+ );
+ gDoc.defaultView.setTimeout(finish, 0);
+ }
+
+ function finish() {
+ removeEventListener("DOMFormHasPassword", unexpectedContentEvent);
+ gDoc.removeEventListener("DOMFormHasPassword", unexpectedContentEvent);
+ resolve();
+ }
+
+ return promise;
+}
+
+add_task(async function test_disconnectedInputs() {
+ const tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+ await ContentTask.spawn(tab.linkedBrowser, [], async () => {
+ const unexpectedEvent = evt => {
+ Assert.ok(
+ false,
+ `${evt.type} should not be fired for disconnected forms.`
+ );
+ };
+
+ addEventListener("DOMFormHasPassword", unexpectedEvent);
+
+ const form = content.document.createElement("form");
+ const passwordInput = content.document.createElement("input");
+ passwordInput.setAttribute("type", "password");
+ form.appendChild(passwordInput);
+
+ // Delay the execution for a bit to allow time for any asynchronously
+ // dispatched 'DOMFormHasPassword' events to be processed.
+ // This is necessary because such events might not be triggered immediately,
+ // and we want to ensure that if they are dispatched, they are captured
+ // before we remove the event listener.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 50));
+ removeEventListener("DOMFormHasPassword", unexpectedEvent);
+ });
+
+ Assert.ok(true, "Test completed");
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function () {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+
+ let promise = ContentTask.spawn(tab.linkedBrowser, ids, task);
+ BrowserTestUtils.loadURIString(
+ tab.linkedBrowser,
+ `data:text/html;charset=utf-8,
+ <html><body>
+ <form id="${ids.FORM1_ID}">
+ <input id="${ids.CHANGE_INPUT_ID}">
+ </form>
+ <form id="${ids.FORM2_ID}"></form>
+ </body></html>`
+ );
+ await promise;
+
+ Assert.ok(true, "Test completed");
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPossibleUsername.js b/toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPossibleUsername.js
new file mode 100644
index 0000000000..7f39395587
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPossibleUsername.js
@@ -0,0 +1,254 @@
+const ids = {
+ INPUT_ID: "input1",
+ FORM1_ID: "form1",
+ FORM2_ID: "form2",
+ CHANGE_INPUT_ID: "input2",
+ INPUT_TYPE: "",
+};
+
+function task({ contentIds, expected }) {
+ let resolve;
+ let promise = new Promise(r => {
+ resolve = r;
+ });
+
+ function unexpectedContentEvent(evt) {
+ Assert.ok(false, "Received a " + evt.type + " event on content");
+ }
+
+ var gDoc = null;
+
+ addEventListener("load", tabLoad, true);
+
+ function tabLoad() {
+ if (content.location.href == "about:blank") {
+ return;
+ }
+ removeEventListener("load", tabLoad, true);
+
+ gDoc = content.document;
+ gDoc.addEventListener("DOMFormHasPossibleUsername", unexpectedContentEvent);
+ addEventListener("DOMFormHasPossibleUsername", unexpectedContentEvent);
+ gDoc.defaultView.setTimeout(test_inputAdd, 0);
+ }
+
+ function test_inputAdd() {
+ if (expected) {
+ addEventListener("DOMFormHasPossibleUsername", test_inputAddHandler, {
+ once: true,
+ capture: true,
+ });
+ } else {
+ gDoc.defaultView.setTimeout(test_inputAddHandler, 0);
+ }
+ let input = gDoc.createElementNS("http://www.w3.org/1999/xhtml", "input");
+ input.setAttribute("type", contentIds.INPUT_TYPE);
+ input.setAttribute("id", contentIds.INPUT_ID);
+ input.setAttribute("data-test", "unique-attribute");
+ gDoc.getElementById(contentIds.FORM1_ID).appendChild(input);
+ }
+
+ function test_inputAddHandler(evt) {
+ if (expected) {
+ evt.stopPropagation();
+ Assert.equal(
+ evt.target.id,
+ contentIds.FORM1_ID,
+ evt.type +
+ " event targets correct form element (added possible username element)"
+ );
+ }
+ gDoc.defaultView.setTimeout(test_inputChangeForm, 0);
+ }
+
+ function test_inputChangeForm() {
+ if (expected) {
+ addEventListener(
+ "DOMFormHasPossibleUsername",
+ test_inputChangeFormHandler,
+ { once: true, capture: true }
+ );
+ } else {
+ gDoc.defaultView.setTimeout(test_inputChangeFormHandler, 0);
+ }
+ let input = gDoc.getElementById(contentIds.INPUT_ID);
+ input.setAttribute("form", contentIds.FORM2_ID);
+ }
+
+ function test_inputChangeFormHandler(evt) {
+ if (expected) {
+ evt.stopPropagation();
+ Assert.equal(
+ evt.target.id,
+ contentIds.FORM2_ID,
+ evt.type + " event targets correct form element (changed form)"
+ );
+ }
+ // TODO(Bug 1864405): Refactor this test to not expect a DOM event
+ // when the type is set to the same value
+ const nextTask =
+ expected && contentIds.INPUT_TYPE === "text"
+ ? finish
+ : test_inputChangesType;
+ gDoc.defaultView.setTimeout(nextTask, 0);
+ }
+
+ function test_inputChangesType() {
+ if (expected) {
+ addEventListener(
+ "DOMFormHasPossibleUsername",
+ test_inputChangesTypeHandler,
+ { once: true, capture: true }
+ );
+ } else {
+ gDoc.defaultView.setTimeout(test_inputChangesTypeHandler, 0);
+ }
+ let input = gDoc.getElementById(contentIds.CHANGE_INPUT_ID);
+ input.setAttribute("type", contentIds.INPUT_TYPE);
+ }
+
+ function test_inputChangesTypeHandler(evt) {
+ if (expected) {
+ evt.stopPropagation();
+ Assert.equal(
+ evt.target.id,
+ contentIds.FORM1_ID,
+ evt.type + " event targets correct form element (changed type)"
+ );
+ }
+ gDoc.defaultView.setTimeout(finish, 0);
+ }
+
+ function finish() {
+ removeEventListener("DOMFormHasPossibleUsername", unexpectedContentEvent);
+ gDoc.removeEventListener(
+ "DOMFormHasPossibleUsername",
+ unexpectedContentEvent
+ );
+ resolve();
+ }
+
+ return promise;
+}
+
+add_setup(async function () {
+ Services.prefs.setBoolPref("signon.usernameOnlyForm.enabled", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("signon.usernameOnlyForm.enabled");
+ });
+});
+
+add_task(async function test_disconnectedInputs() {
+ const tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+ await ContentTask.spawn(tab.linkedBrowser, [], async () => {
+ const unexpectedEvent = evt => {
+ Assert.ok(
+ false,
+ `${evt.type} should not be fired for disconnected forms.`
+ );
+ };
+
+ addEventListener("DOMFormHasPossibleUsername", unexpectedEvent);
+ const form = content.document.createElement("form");
+ const textInput = content.document.createElement("input");
+ textInput.setAttribute("type", "text");
+ form.appendChild(textInput);
+
+ // Delay the execution for a bit to allow time for any asynchronously
+ // dispatched 'DOMFormHasPossibleUsername' events to be processed.
+ // This is necessary because such events might not be triggered immediately,
+ // and we want to ensure that if they are dispatched, they are captured
+ // before we remove the event listener.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 50));
+ removeEventListener("DOMFormHasPossibleUsername", unexpectedEvent);
+ });
+
+ Assert.ok(true, "Test completed");
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function test_usernameOnlyForm() {
+ for (let type of ["text", "email"]) {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+
+ ids.INPUT_TYPE = type;
+ let promise = ContentTask.spawn(
+ tab.linkedBrowser,
+ { contentIds: ids, expected: true },
+ task
+ );
+ BrowserTestUtils.loadURIString(
+ tab.linkedBrowser,
+ `data:text/html;charset=utf-8,
+ <html><body>
+ <form id="${ids.FORM1_ID}">
+ <input id="${ids.CHANGE_INPUT_ID}">
+ </form>
+ <form id="${ids.FORM2_ID}"></form>
+ </body></html>`
+ );
+ await promise;
+
+ Assert.ok(true, "Test completed");
+ gBrowser.removeCurrentTab();
+ }
+});
+
+add_task(async function test_nonSupportedInputType() {
+ for (let type of ["url", "tel", "number"]) {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+
+ ids.INPUT_TYPE = type;
+ let promise = ContentTask.spawn(
+ tab.linkedBrowser,
+ { contentIds: ids, expected: false },
+ task
+ );
+ BrowserTestUtils.loadURIString(
+ tab.linkedBrowser,
+ `data:text/html;charset=utf-8,
+ <html><body>
+ <form id="${ids.FORM1_ID}">
+ <input id="${ids.CHANGE_INPUT_ID}">
+ </form>
+ <form id="${ids.FORM2_ID}"></form>
+ </body></html>`
+ );
+ await promise;
+
+ Assert.ok(true, "Test completed");
+ gBrowser.removeCurrentTab();
+ }
+});
+
+add_task(async function test_usernameOnlyFormPrefOff() {
+ Services.prefs.setBoolPref("signon.usernameOnlyForm.enabled", false);
+
+ for (let type of ["text", "email"]) {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+
+ ids.INPUT_TYPE = type;
+ let promise = ContentTask.spawn(
+ tab.linkedBrowser,
+ { contentIds: ids, expected: false },
+ task
+ );
+ BrowserTestUtils.loadURIString(
+ tab.linkedBrowser,
+ `data:text/html;charset=utf-8,
+ <html><body>
+ <form id="${ids.FORM1_ID}">
+ <input id="${ids.CHANGE_INPUT_ID}">
+ </form>
+ <form id="${ids.FORM2_ID}"></form>
+ </body></html>`
+ );
+ await promise;
+
+ Assert.ok(true, "Test completed");
+ gBrowser.removeCurrentTab();
+ }
+
+ Services.prefs.clearUserPref("signon.usernameOnlyForm.enabled");
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_DOMInputPasswordAdded.js b/toolkit/components/passwordmgr/test/browser/browser_DOMInputPasswordAdded.js
new file mode 100644
index 0000000000..11ca2ac1cd
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_DOMInputPasswordAdded.js
@@ -0,0 +1,101 @@
+const consts = {
+ HTML_NS: "http://www.w3.org/1999/xhtml",
+
+ INPUT_ID: "input1",
+ FORM1_ID: "form1",
+ FORM2_ID: "form2",
+ CHANGE_INPUT_ID: "input2",
+ BODY_INPUT_ID: "input3",
+};
+
+function task(contentConsts) {
+ let resolve;
+ let promise = new Promise(r => {
+ resolve = r;
+ });
+
+ function unexpectedContentEvent(evt) {
+ Assert.ok(false, "Received a " + evt.type + " event on content");
+ }
+
+ var gDoc = null;
+
+ addEventListener("load", tabLoad, true);
+
+ function tabLoad() {
+ removeEventListener("load", tabLoad, true);
+ gDoc = content.document;
+ // These events shouldn't escape to content.
+ gDoc.addEventListener("DOMInputPasswordAdded", unexpectedContentEvent);
+ gDoc.defaultView.setTimeout(test_inputAddOutsideForm, 0);
+ }
+
+ function test_inputAddOutsideForm() {
+ addEventListener(
+ "DOMInputPasswordAdded",
+ test_inputAddOutsideFormHandler,
+ false
+ );
+ let input = gDoc.createElementNS(contentConsts.HTML_NS, "input");
+ input.setAttribute("type", "password");
+ input.setAttribute("id", contentConsts.BODY_INPUT_ID);
+ input.setAttribute("data-test", "unique-attribute");
+ gDoc.body.appendChild(input);
+ info("Done appending the input element to the body");
+ }
+
+ function test_inputAddOutsideFormHandler(evt) {
+ removeEventListener(evt.type, test_inputAddOutsideFormHandler, false);
+ Assert.equal(
+ evt.target.id,
+ contentConsts.BODY_INPUT_ID,
+ evt.type +
+ " event targets correct input element (added password element outside form)"
+ );
+ gDoc.defaultView.setTimeout(test_inputChangesType, 0);
+ }
+
+ function test_inputChangesType() {
+ addEventListener(
+ "DOMInputPasswordAdded",
+ test_inputChangesTypeHandler,
+ false
+ );
+ let input = gDoc.getElementById(contentConsts.CHANGE_INPUT_ID);
+ input.setAttribute("type", "password");
+ }
+
+ function test_inputChangesTypeHandler(evt) {
+ removeEventListener(evt.type, test_inputChangesTypeHandler, false);
+ Assert.equal(
+ evt.target.id,
+ contentConsts.CHANGE_INPUT_ID,
+ evt.type + " event targets correct input element (changed type)"
+ );
+ gDoc.defaultView.setTimeout(completeTest, 0);
+ }
+
+ function completeTest() {
+ Assert.ok(true, "Test completed");
+ gDoc.removeEventListener("DOMInputPasswordAdded", unexpectedContentEvent);
+ resolve();
+ }
+
+ return promise;
+}
+
+add_task(async function () {
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+ let promise = ContentTask.spawn(tab.linkedBrowser, consts, task);
+ BrowserTestUtils.loadURIString(
+ tab.linkedBrowser,
+ `data:text/html;charset=utf-8,
+ <html><body>
+ <input id="${consts.CHANGE_INPUT_ID}" />
+ </body>
+ </html>
+ `
+ );
+ await promise;
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_autocomplete_autofocus_with_frame.js b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_autofocus_with_frame.js
new file mode 100644
index 0000000000..551e1b939a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_autofocus_with_frame.js
@@ -0,0 +1,48 @@
+const TEST_URL_PATH = "https://example.org" + DIRECTORY_PATH;
+
+add_setup(async function () {
+ let login = LoginTestUtils.testData.formLogin({
+ origin: "https://example.org",
+ formActionOrigin: "https://example.org",
+ username: "username1",
+ password: "password1",
+ });
+ await Services.logins.addLoginAsync(login);
+ login = LoginTestUtils.testData.formLogin({
+ origin: "https://example.org",
+ formActionOrigin: "https://example.org",
+ username: "username2",
+ password: "password2",
+ });
+ await Services.logins.addLoginAsync(login);
+});
+
+// Verify that the autocomplete popup opens when the username field in autofocused.
+add_task(async function test_autofocus_autocomplete() {
+ let popup = document.getElementById("PopupAutoComplete");
+ let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+
+ let formFilled = listenForTestNotification("FormProcessed");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_URL_PATH + "form_autofocus_frame.html"
+ );
+
+ await formFilled;
+ await popupShown;
+
+ Assert.ok(true, "popup opened");
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ popup.firstChild.getItemAtIndex(0).click();
+ await promiseHidden;
+
+ Assert.ok(true, "popup closed");
+
+ let password = await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ return content.document.getElementById("form-basic-password").value;
+ });
+ Assert.equal(password, "password1", "password filled in");
+
+ gBrowser.removeTab(tab);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_autocomplete_disabled_readonly_passwordField.js b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_disabled_readonly_passwordField.js
new file mode 100644
index 0000000000..f6e0a62678
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_disabled_readonly_passwordField.js
@@ -0,0 +1,138 @@
+const TEST_URL_PATH =
+ "https://example.org" +
+ DIRECTORY_PATH +
+ "form_disabled_readonly_passwordField.html";
+const FIRST_ITEM = 0;
+
+/**
+ * Add two logins to prevent autofilling, but the AutocompletePopup will be displayed
+ */
+add_setup(async () => {
+ let login1 = LoginTestUtils.testData.formLogin({
+ origin: "https://example.org",
+ formActionOrigin: "https://example.org",
+ username: "username1",
+ password: "password1",
+ });
+
+ let login2 = LoginTestUtils.testData.formLogin({
+ origin: "https://example.org",
+ formActionOrigin: "https://example.org",
+ username: "username2",
+ password: "password2",
+ });
+
+ await Services.logins.addLogins([login1, login2]);
+});
+
+add_task(
+ async function test_autocomplete_for_usernameField_with_disabled_passwordField() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ let popup = document.getElementById("PopupAutoComplete");
+
+ Assert.ok(popup, "Got Popup");
+
+ await openACPopup(
+ popup,
+ browser,
+ "#login_form_disabled_password input[name=username]"
+ );
+
+ info("Popup opened");
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ popup.firstChild.getItemAtIndex(FIRST_ITEM).click();
+ await promiseHidden;
+
+ info("Popup closed");
+
+ let [username, password] = await SpecialPowers.spawn(
+ browser,
+ [],
+ async () => {
+ let doc = content.document;
+ let contentUsername = doc.querySelector(
+ "#login_form_disabled_password input[name=username]"
+ ).value;
+ let contentPassword = doc.querySelector(
+ "#login_form_disabled_password input[name=password]"
+ ).value;
+ return [contentUsername, contentPassword];
+ }
+ );
+ Assert.equal(
+ username,
+ "username1",
+ "Username was autocompleted with correct value."
+ );
+ Assert.equal(
+ password,
+ "",
+ "Password was not autocompleted, because field is disabled."
+ );
+ }
+ );
+ }
+);
+
+add_task(
+ async function test_autocomplete_for_usernameField_with_readonly_passwordField() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ let popup = document.getElementById("PopupAutoComplete");
+
+ Assert.ok(popup, "Got Popup");
+
+ await openACPopup(
+ popup,
+ browser,
+ "#login_form_readonly_password input[name=username]"
+ );
+
+ info("Popup opened");
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ popup.firstChild.getItemAtIndex(FIRST_ITEM).click();
+ await promiseHidden;
+
+ info("Popup closed");
+
+ let [username, password] = await SpecialPowers.spawn(
+ browser,
+ [],
+ async () => {
+ let doc = content.document;
+ let contentUsername = doc.querySelector(
+ "#login_form_readonly_password input[name=username]"
+ ).value;
+ info(contentUsername);
+ let contentPassword = doc.querySelector(
+ "#login_form_readonly_password input[name=password]"
+ ).value;
+ info(contentPassword);
+ return [contentUsername, contentPassword];
+ }
+ );
+ Assert.equal(
+ username,
+ "username1",
+ "Username was autocompleted with correct value."
+ );
+ Assert.equal(
+ password,
+ "",
+ "Password was not autocompleted, because field is readonly."
+ );
+ }
+ );
+ }
+);
diff --git a/toolkit/components/passwordmgr/test/browser/browser_autocomplete_footer.js b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_footer.js
new file mode 100644
index 0000000000..6660daec99
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_footer.js
@@ -0,0 +1,125 @@
+"use strict";
+
+const TEST_ORIGIN = "https://example.com";
+const BASIC_FORM_PAGE_PATH = DIRECTORY_PATH + "form_basic.html";
+
+function loginList() {
+ return [
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: "username",
+ password: "password",
+ }),
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: "username2",
+ password: "password2",
+ }),
+ ];
+}
+
+/**
+ * Initialize logins and set prefs needed for the test.
+ */
+add_task(async function test_initialize() {
+ Services.prefs.setBoolPref("signon.showAutoCompleteFooter", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("signon.showAutoCompleteFooter");
+ });
+
+ await Services.logins.addLogins(loginList());
+});
+
+add_task(async function test_autocomplete_footer_onclick() {
+ let url = TEST_ORIGIN + BASIC_FORM_PAGE_PATH;
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function footer_onclick(browser) {
+ let popup = document.getElementById("PopupAutoComplete");
+ Assert.ok(popup, "Got popup");
+
+ await openACPopup(popup, browser, "#form-basic-username");
+
+ let footer = popup.querySelector(`[originaltype="loginsFooter"]`);
+ Assert.ok(footer, "Got footer richlistitem");
+
+ await TestUtils.waitForCondition(() => {
+ return !EventUtils.isHidden(footer);
+ }, "Waiting for footer to become visible");
+
+ let openingFunc = () => EventUtils.synthesizeMouseAtCenter(footer, {});
+ let passwordManager = await openPasswordManager(openingFunc, false);
+
+ info("Password Manager was opened");
+
+ Assert.ok(
+ !passwordManager.filterValue,
+ "Search string should not be set to filter logins"
+ );
+
+ // open_management
+ await LoginTestUtils.telemetry.waitForEventCount(1);
+
+ // Check event telemetry recorded when opening management UI
+ TelemetryTestUtils.assertEvents(
+ [["pwmgr", "open_management", "autocomplete"]],
+ { category: "pwmgr", method: "open_management" },
+ { clear: true, process: "content" }
+ );
+
+ await passwordManager.close();
+ await closePopup(popup);
+ }
+ );
+});
+
+add_task(async function test_autocomplete_footer_keydown() {
+ let url = TEST_ORIGIN + BASIC_FORM_PAGE_PATH;
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function footer_enter_keydown(browser) {
+ let popup = document.getElementById("PopupAutoComplete");
+ Assert.ok(popup, "Got popup");
+
+ await openACPopup(popup, browser, "#form-basic-username");
+
+ let footer = popup.querySelector(`[originaltype="loginsFooter"]`);
+ Assert.ok(footer, "Got footer richlistitem");
+
+ await TestUtils.waitForCondition(() => {
+ return !EventUtils.isHidden(footer);
+ }, "Waiting for footer to become visible");
+
+ await EventUtils.synthesizeKey("KEY_ArrowDown");
+ await EventUtils.synthesizeKey("KEY_ArrowDown");
+ await EventUtils.synthesizeKey("KEY_ArrowDown");
+ let openingFunc = () => EventUtils.synthesizeKey("KEY_Enter");
+
+ let passwordManager = await openPasswordManager(openingFunc, false);
+ info("Login dialog was opened");
+
+ Assert.ok(
+ !passwordManager.filterValue,
+ "Search string should not be set to filter logins"
+ );
+
+ // Check event telemetry recorded when opening management UI
+ TelemetryTestUtils.assertEvents(
+ [["pwmgr", "open_management", "autocomplete"]],
+ { category: "pwmgr", method: "open_management" },
+ { clear: true, process: "content" }
+ );
+
+ await passwordManager.close();
+ await closePopup(popup);
+ }
+ );
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_autocomplete_generated_password_private_window.js b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_generated_password_private_window.js
new file mode 100644
index 0000000000..f28a7a1d52
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_generated_password_private_window.js
@@ -0,0 +1,111 @@
+// The origin for the test URIs.
+const TEST_ORIGIN = "https://example.com";
+const FORM_PAGE_PATH =
+ "/browser/toolkit/components/passwordmgr/test/browser/form_basic.html";
+const passwordInputSelector = "#form-basic-password";
+
+add_setup(async function () {
+ Services.telemetry.clearEvents();
+ TelemetryTestUtils.assertEvents([], {
+ category: "pwmgr",
+ method: "autocomplete_shown",
+ });
+});
+
+add_task(async function test_autocomplete_new_password_popup_item_visible() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN,
+ },
+ async function (browser) {
+ info("Generate and cache a password for a non-private context");
+ let lmp =
+ browser.browsingContext.currentWindowGlobal.getActor("LoginManager");
+ await lmp.getGeneratedPassword();
+ Assert.equal(
+ LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().size,
+ 1,
+ "Non-Private password should be cached"
+ );
+ }
+ );
+
+ await LoginTestUtils.addLogin({ username: "username", password: "pass1" });
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ const doc = win.document;
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: win.gBrowser,
+ url: TEST_ORIGIN + FORM_PAGE_PATH,
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+ await ContentTask.spawn(
+ browser,
+ [passwordInputSelector],
+ function openAutocomplete(sel) {
+ content.document.querySelector(sel).autocomplete = "new-password";
+ }
+ );
+
+ let popup = doc.getElementById("PopupAutoComplete");
+ Assert.ok(popup, "Got popup");
+ await openACPopup(popup, browser, passwordInputSelector);
+
+ let item = popup.querySelector(`[originaltype="generatedPassword"]`);
+ Assert.ok(item, "Should get 'Generate password' richlistitem");
+
+ let onPopupClosed = BrowserTestUtils.waitForCondition(
+ () => !popup.popupOpen,
+ "Popup should get closed"
+ );
+
+ await TestUtils.waitForTick();
+
+ TelemetryTestUtils.assertEvents(
+ [["pwmgr", "autocomplete_shown", "generatedpassword"]],
+ { category: "pwmgr", method: "autocomplete_shown" }
+ );
+
+ await closePopup(popup);
+ await onPopupClosed;
+ }
+ );
+
+ let lastPBContextExitedPromise = TestUtils.topicObserved(
+ "last-pb-context-exited"
+ ).then(() => TestUtils.waitForTick());
+ await BrowserTestUtils.closeWindow(win);
+ await lastPBContextExitedPromise;
+ Assert.equal(
+ LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().size,
+ 1,
+ "Only private-context passwords should be cleared"
+ );
+});
+
+add_task(async function test_autocomplete_menu_item_enabled() {
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ const doc = win.document;
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: win.gBrowser,
+ url: TEST_ORIGIN + FORM_PAGE_PATH,
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser);
+ await openPasswordContextMenu(browser, passwordInputSelector);
+ let generatedPasswordItem = doc.getElementById(
+ "fill-login-generated-password"
+ );
+ Assert.equal(
+ generatedPasswordItem.disabled,
+ false,
+ "Generate password context menu item should be enabled in PB mode"
+ );
+ await closePopup(document.getElementById("contentAreaContextMenu"));
+ }
+ );
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_autocomplete_import.js b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_import.js
new file mode 100644
index 0000000000..0be137d88d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_import.js
@@ -0,0 +1,259 @@
+const { ChromeMigrationUtils } = ChromeUtils.importESModule(
+ "resource:///modules/ChromeMigrationUtils.sys.mjs"
+);
+const { ExperimentAPI } = ChromeUtils.importESModule(
+ "resource://nimbus/ExperimentAPI.sys.mjs"
+);
+const { ExperimentFakes } = ChromeUtils.importESModule(
+ "resource://testing-common/NimbusTestUtils.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+// Dummy migrator to change and detect importable behavior.
+const gTestMigrator = {
+ profiles: [],
+
+ getSourceProfiles() {
+ return this.profiles;
+ },
+
+ migrate: sinon
+ .stub()
+ .callsFake(() =>
+ LoginTestUtils.addLogin({ username: "import", password: "pass" })
+ ),
+};
+
+// Showing importables updates counts delayed, so adjust and cleanup.
+add_setup(async function setup() {
+ const debounce = sinon
+ .stub(LoginManagerParent, "SUGGEST_IMPORT_DEBOUNCE_MS")
+ .value(0);
+ const importable = sinon
+ .stub(ChromeMigrationUtils, "getImportableLogins")
+ .resolves(["chrome"]);
+ const migrator = sinon
+ .stub(MigrationUtils, "getMigrator")
+ .resolves(gTestMigrator);
+
+ const doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "password-autocomplete",
+ value: { directMigrateSingleProfile: true },
+ });
+
+ // This makes the last autocomplete test *not* show import suggestions.
+ Services.prefs.setIntPref("signon.suggestImportCount", 3);
+
+ registerCleanupFunction(async () => {
+ await doExperimentCleanup();
+ debounce.restore();
+ importable.restore();
+ migrator.restore();
+ Services.prefs.clearUserPref("signon.suggestImportCount");
+ });
+});
+
+add_task(async function check_fluent_ids() {
+ await document.l10n.ready;
+ MozXULElement.insertFTLIfNeeded("toolkit/main-window/autocomplete.ftl");
+
+ const host = "testhost.com";
+ for (const browser of ChromeMigrationUtils.CONTEXTUAL_LOGIN_IMPORT_BROWSERS) {
+ const id = `autocomplete-import-logins-${browser}`;
+ const message = await document.l10n.formatValue(id, { host });
+ Assert.ok(
+ message.includes(`data-l10n-name="line1"`),
+ `${id} included line1`
+ );
+ Assert.ok(
+ message.includes(`data-l10n-name="line2"`),
+ `${id} included line2`
+ );
+ Assert.ok(message.includes(host), `${id} replaced host`);
+ }
+});
+
+/**
+ * Tests that if the user selects the password import suggestion from
+ * the autocomplete popup, and there is more than one profile available
+ * to import from, that the migration wizard opens to guide the user
+ * through importing those logins.
+ */
+add_task(async function import_suggestion_wizard() {
+ let wizard;
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "https://example.com" + DIRECTORY_PATH + "form_basic.html",
+ },
+ async function (browser) {
+ const popup = document.getElementById("PopupAutoComplete");
+ Assert.ok(popup, "Got popup");
+ await openACPopup(popup, browser, "#form-basic-username");
+
+ const importableItem = popup.querySelector(
+ `[originaltype="importableLogins"]`
+ );
+ Assert.ok(importableItem, "Got importable suggestion richlistitem");
+
+ await BrowserTestUtils.waitForCondition(
+ () => !importableItem.collapsed,
+ "Wait for importable suggestion to show"
+ );
+
+ // Pretend there's 2+ profiles to trigger the wizard.
+ gTestMigrator.profiles.length = 2;
+
+ info("Clicking on importable suggestion");
+ const wizardPromise = BrowserTestUtils.waitForMigrationWizard(window);
+
+ // The modal window blocks execution, so avoid calling directly.
+ executeSoon(() => EventUtils.synthesizeMouseAtCenter(importableItem, {}));
+
+ wizard = await wizardPromise;
+ Assert.ok(wizard, "Wizard opened");
+ Assert.equal(
+ gTestMigrator.migrate.callCount,
+ 0,
+ "Direct migrate not used"
+ );
+
+ await closePopup(popup);
+ }
+ );
+
+ // Close the wizard in the end of the test. If we close the wizard when the tab
+ // is still opened, the username field will be focused again, which triggers another
+ // importable suggestion.
+ await BrowserTestUtils.closeMigrationWizard(wizard);
+});
+
+add_task(async function import_suggestion_learn_more() {
+ let supportTab;
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "https://example.com" + DIRECTORY_PATH + "form_basic.html",
+ },
+ async function (browser) {
+ const popup = document.getElementById("PopupAutoComplete");
+ Assert.ok(popup, "Got popup");
+ await openACPopup(popup, browser, "#form-basic-username");
+
+ const learnMoreItem = popup.querySelector(`[type="importableLearnMore"]`);
+ Assert.ok(learnMoreItem, "Got importable learn more richlistitem");
+
+ await BrowserTestUtils.waitForCondition(
+ () => !learnMoreItem.collapsed,
+ "Wait for importable learn more to show"
+ );
+
+ info("Clicking on importable learn more");
+ const supportTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "password-import"
+ );
+ EventUtils.synthesizeMouseAtCenter(learnMoreItem, {});
+ supportTab = await supportTabPromise;
+ Assert.ok(supportTab, "Support tab opened");
+
+ await closePopup(popup);
+ }
+ );
+
+ // Close the tab in the end of the test to avoid the username field being
+ // focused again.
+ await BrowserTestUtils.removeTab(supportTab);
+});
+
+add_task(async function import_suggestion_migrate() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "https://example.com" + DIRECTORY_PATH + "form_basic.html",
+ },
+ async function (browser) {
+ const popup = document.getElementById("PopupAutoComplete");
+ Assert.ok(popup, "Got popup");
+ await openACPopup(popup, browser, "#form-basic-username");
+
+ const importableItem = popup.querySelector(
+ `[originaltype="importableLogins"]`
+ );
+ Assert.ok(importableItem, "Got importable suggestion richlistitem");
+
+ await BrowserTestUtils.waitForCondition(
+ () => !importableItem.collapsed,
+ "Wait for importable suggestion to show"
+ );
+
+ // Pretend there's 1 profile to trigger migrate.
+ gTestMigrator.profiles.length = 1;
+
+ info("Clicking on importable suggestion");
+ const migratePromise = BrowserTestUtils.waitForCondition(
+ () => gTestMigrator.migrate.callCount,
+ "Wait for direct migration attempt"
+ );
+ EventUtils.synthesizeMouseAtCenter(importableItem, {});
+
+ const callCount = await migratePromise;
+ Assert.equal(callCount, 1, "Direct migrate used once");
+
+ const importedItem = await BrowserTestUtils.waitForCondition(
+ () => popup.querySelector(`[originaltype="loginWithOrigin"]`),
+ "Wait for imported login to show"
+ );
+ EventUtils.synthesizeMouseAtCenter(importedItem, {});
+
+ const username = await SpecialPowers.spawn(
+ browser,
+ [],
+ () => content.document.getElementById("form-basic-username").value
+ );
+ Assert.equal(username, "import", "username from import filled in");
+
+ LoginTestUtils.clearData();
+ }
+ );
+});
+
+add_task(async function import_suggestion_not_shown() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "https://example.com" + DIRECTORY_PATH + "form_basic.html",
+ },
+ async function (browser) {
+ const popup = document.getElementById("PopupAutoComplete");
+ Assert.ok(popup, "Got popup");
+ let opened = false;
+ openACPopup(popup, browser, "#form-basic-password").then(
+ () => (opened = true)
+ );
+
+ await TestUtils.waitForCondition(() => {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ return opened;
+ });
+
+ const footer = popup.querySelector(`[originaltype="loginsFooter"]`);
+ Assert.ok(footer, "Got footer richlistitem");
+
+ await TestUtils.waitForCondition(() => {
+ return !EventUtils.isHidden(footer);
+ }, "Waiting for footer to become visible");
+
+ Assert.ok(
+ !popup.querySelector(`[originaltype="importableLogins"]`),
+ "No importable suggestion shown"
+ );
+
+ await closePopup(popup);
+ }
+ );
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js
new file mode 100644
index 0000000000..f00ea80937
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js
@@ -0,0 +1,44 @@
+"use strict";
+
+const EXPECTED_SUPPORT_URL =
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "insecure-password";
+
+add_task(async function test_clickInsecureFieldWarning() {
+ let url =
+ "https://example.com" +
+ DIRECTORY_PATH +
+ "form_cross_origin_insecure_action.html";
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function (browser) {
+ let popup = document.getElementById("PopupAutoComplete");
+ Assert.ok(popup, "Got popup");
+ await openACPopup(popup, browser, "#form-basic-username");
+ await new Promise(requestAnimationFrame);
+
+ let warningItem = popup.querySelector(`[type="insecureWarning"]`);
+ Assert.ok(warningItem, "Got warning richlistitem");
+
+ await BrowserTestUtils.waitForCondition(
+ () => !warningItem.collapsed,
+ "Wait for warning to show"
+ );
+
+ info("Clicking on warning");
+ let supportTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ EXPECTED_SUPPORT_URL
+ );
+ EventUtils.synthesizeMouseAtCenter(warningItem, {});
+ let supportTab = await supportTabPromise;
+ Assert.ok(supportTab, "Support tab opened");
+ await closePopup(popup);
+ BrowserTestUtils.removeTab(supportTab);
+ }
+ );
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_autocomplete_primary_password.js b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_primary_password.js
new file mode 100644
index 0000000000..c3152740cd
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_primary_password.js
@@ -0,0 +1,121 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+const HOST = "https://example.com";
+const URL =
+ HOST + "/browser/toolkit/components/passwordmgr/test/browser/form_basic.html";
+const TIMEOUT_PREF = "signon.masterPasswordReprompt.timeout_ms";
+
+const BRAND_BUNDLE = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+);
+const BRAND_FULL_NAME = BRAND_BUNDLE.GetStringFromName("brandFullName");
+
+// Waits for the primary password prompt and cancels it when close() is called on the return value.
+async function waitForDialog() {
+ let [subject] = await TestUtils.topicObserved("common-dialog-loaded");
+ let dialog = subject.Dialog;
+ let expected = "Password Required - " + BRAND_FULL_NAME;
+ Assert.equal(dialog.args.title, expected, "Check common dialog title");
+ return {
+ async close(win = window) {
+ dialog.ui.button1.click();
+ return BrowserTestUtils.waitForEvent(win, "DOMModalDialogClosed");
+ },
+ };
+}
+
+add_setup(async function () {
+ let login = LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: "username",
+ password: "password",
+ });
+ await Services.logins.addLoginAsync(login);
+ LoginTestUtils.primaryPassword.enable();
+
+ registerCleanupFunction(function () {
+ LoginTestUtils.primaryPassword.disable();
+ });
+
+ // Set primary password prompt timeout to 3s.
+ // If this test goes intermittent, you likely have to increase this value.
+ await SpecialPowers.pushPrefEnv({ set: [[TIMEOUT_PREF, 3000]] });
+});
+
+// Test that autocomplete does not trigger a primary password prompt
+// for a certain time after it was cancelled.
+add_task(async function test_mpAutocompleteTimeout() {
+ // Wait for initial primary password dialog after opening the tab.
+ let dialogShown = waitForDialog();
+
+ await BrowserTestUtils.withNewTab(URL, async function (browser) {
+ (await dialogShown).close();
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Focus the password field to trigger autocompletion.
+ content.document.getElementById("form-basic-password").focus();
+ });
+
+ // Wait 4s, dialog should not have been shown
+ // (otherwise the code below will not work).
+ await new Promise(c => setTimeout(c, 4000));
+
+ dialogShown = waitForDialog();
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Re-focus the password field to trigger autocompletion.
+ content.document.getElementById("form-basic-username").focus();
+ content.document.getElementById("form-basic-password").focus();
+ });
+ (await dialogShown).close();
+ closePopup(document.getElementById("PopupAutoComplete"));
+ });
+
+ // Wait 4s for the timer to pass again and not interfere with the next test.
+ await new Promise(c => setTimeout(c, 4000));
+});
+
+// Test that autocomplete does not trigger a primary password prompt
+// if one is already showing.
+add_task(async function test_mpAutocompleteUIBusy() {
+ // Wait for initial primary password dialog after adding the login.
+ let dialogShown = waitForDialog();
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ Services.tm.dispatchToMainThread(async () => {
+ try {
+ // Trigger a MP prompt in the new window by saving a login
+ await Services.logins.addLoginAsync(LoginTestUtils.testData.formLogin());
+ } catch (e) {
+ // Handle throwing from MP cancellation
+ }
+ });
+ let { close } = await dialogShown;
+
+ let windowGlobal =
+ gBrowser.selectedBrowser.browsingContext.currentWindowGlobal;
+ let loginManagerParent = windowGlobal.getActor("LoginManager");
+ let origin = "https://www.example.com";
+ let data = {
+ actionOrigin: "",
+ searchString: "",
+ previousResult: null,
+ hasBeenTypePassword: true,
+ isSecure: false,
+ isProbablyANewPasswordField: true,
+ };
+
+ function dialogObserver(subject, topic, data) {
+ Assert.ok(false, "A second dialog shouldn't have been shown");
+ Services.obs.removeObserver(dialogObserver, topic);
+ }
+ Services.obs.addObserver(dialogObserver, "common-dialog-loaded");
+
+ let results = await loginManagerParent.doAutocompleteSearch(origin, data);
+ Assert.equal(results.logins.length, 0, "No results since uiBusy is true");
+ await close(win);
+
+ await BrowserTestUtils.closeWindow(win);
+
+ Services.obs.removeObserver(dialogObserver, "common-dialog-loaded");
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_autofill_hidden_document.js b/toolkit/components/passwordmgr/test/browser/browser_autofill_hidden_document.js
new file mode 100644
index 0000000000..e7af2f8b84
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_autofill_hidden_document.js
@@ -0,0 +1,205 @@
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/aboutlogins/tests/browser/head.js",
+ this
+);
+
+const TEST_URL_PATH = "/browser/toolkit/components/passwordmgr/test/browser/";
+const INITIAL_URL = `about:blank`;
+const FORM_URL = `https://example.org${TEST_URL_PATH}form_basic.html`;
+const FORMLESS_URL = `https://example.org${TEST_URL_PATH}formless_basic.html`;
+const FORM_MULTIPAGE_URL = `https://example.org${TEST_URL_PATH}form_multipage.html`;
+const testUrls = [FORM_URL, FORMLESS_URL, FORM_MULTIPAGE_URL];
+const testUrlsWithForm = [FORM_URL, FORM_MULTIPAGE_URL];
+const BRAND_BUNDLE = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+);
+const BRAND_FULL_NAME = BRAND_BUNDLE.GetStringFromName("brandFullName");
+
+async function getDocumentVisibilityState(browser) {
+ let visibility = await SpecialPowers.spawn(browser, [], async function () {
+ return content.document.visibilityState;
+ });
+ return visibility;
+}
+
+add_setup(async function () {
+ Services.prefs.setBoolPref("signon.usernameOnlyForm.enabled", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("signon.usernameOnlyForm.enabled");
+ });
+
+ Services.logins.removeAllUserFacingLogins();
+ let login = LoginTestUtils.testData.formLogin({
+ origin: "https://example.org",
+ formActionOrigin: "https://example.org",
+ username: "user1",
+ password: "pass1",
+ });
+ await Services.logins.addLoginAsync(login);
+});
+
+testUrlsWithForm.forEach(testUrl => {
+ add_task(async function test_processed_form_fired() {
+ // Sanity check. If this doesnt work any results for the subsequent tasks are suspect
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ INITIAL_URL
+ );
+ let tab1Visibility = await getDocumentVisibilityState(tab1.linkedBrowser);
+ Assert.equal(
+ tab1Visibility,
+ "visible",
+ "The first tab should be foreground"
+ );
+
+ let formProcessedPromise = listenForTestNotification("FormProcessed");
+ BrowserTestUtils.loadURIString(tab1.linkedBrowser, testUrl);
+ await formProcessedPromise;
+ gBrowser.removeTab(tab1);
+ });
+});
+
+testUrls.forEach(testUrl => {
+ add_task(async function test_defer_autofill_until_visible() {
+ let result, tab1Visibility;
+ // open 2 tabs
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ INITIAL_URL
+ );
+ const tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ INITIAL_URL
+ );
+
+ // confirm document is hidden
+ tab1Visibility = await getDocumentVisibilityState(tab1.linkedBrowser);
+ Assert.equal(
+ tab1Visibility,
+ "hidden",
+ "The first tab should be backgrounded"
+ );
+
+ // we shouldn't even try to autofill while hidden, so wait for the document to be in the
+ // non-visible pending queue instead.
+ let formFilled = false;
+ listenForTestNotification("FormProcessed").then(() => {
+ formFilled = true;
+ });
+ BrowserTestUtils.loadURIString(tab1.linkedBrowser, testUrl);
+
+ await TestUtils.waitForCondition(() => {
+ let windowGlobal = tab1.linkedBrowser.browsingContext.currentWindowGlobal;
+ if (!windowGlobal || windowGlobal.documentURI.spec == "about:blank") {
+ return false;
+ }
+
+ let actor = windowGlobal.getActor("LoginManager");
+ return actor.sendQuery("PasswordManager:formIsPending");
+ });
+
+ Assert.ok(
+ !formFilled,
+ "Observer should not be notified when form is loaded into a hidden document"
+ );
+
+ // Add the observer before switching tab
+ let formProcessedPromise = listenForTestNotification("FormProcessed");
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ result = await formProcessedPromise;
+ tab1Visibility = await getDocumentVisibilityState(tab1.linkedBrowser);
+ Assert.equal(
+ tab1Visibility,
+ "visible",
+ "The first tab should be foreground"
+ );
+ Assert.ok(
+ result,
+ "Observer should be notified when input's document becomes visible"
+ );
+
+ // the form should have been autofilled with the login
+ let fieldValues = await SpecialPowers.spawn(
+ tab1.linkedBrowser,
+ [],
+ function () {
+ let doc = content.document;
+ return {
+ username: doc.getElementById("form-basic-username").value,
+ password: doc.getElementById("form-basic-password")?.value,
+ };
+ }
+ );
+ Assert.equal(fieldValues.username, "user1", "Checking filled username");
+
+ // skip password test for a username-only form
+ if (![FORM_MULTIPAGE_URL].includes(testUrl)) {
+ Assert.equal(fieldValues.password, "pass1", "Checking filled password");
+ }
+
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+ });
+});
+
+testUrlsWithForm.forEach(testUrl => {
+ add_task(async function test_immediate_autofill_with_primarypassword() {
+ LoginTestUtils.primaryPassword.enable();
+ await LoginTestUtils.reloadData();
+ info(
+ `Have enabled primaryPassword, now isLoggedIn? ${Services.logins.isLoggedIn}`
+ );
+
+ registerCleanupFunction(async function () {
+ LoginTestUtils.primaryPassword.disable();
+ await LoginTestUtils.reloadData();
+ });
+
+ // open 2 tabs
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ INITIAL_URL
+ );
+ const tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ INITIAL_URL
+ );
+
+ info(
+ "load a background login form tab with a matching saved login " +
+ "and wait to see if the primary password dialog is shown"
+ );
+ Assert.equal(
+ await getDocumentVisibilityState(tab2.linkedBrowser),
+ "visible",
+ "The second tab should be visible"
+ );
+
+ const tab1Visibility = await getDocumentVisibilityState(tab1.linkedBrowser);
+ Assert.equal(
+ tab1Visibility,
+ "hidden",
+ "The first tab should be backgrounded"
+ );
+
+ const dialogObserved = waitForMPDialog("authenticate", tab1.ownerGlobal);
+
+ // In this case we will try to autofill while hidden, so look for the passwordmgr-processed-form
+ // to be observed
+ let formProcessedPromise = listenForTestNotification("FormProcessed");
+ BrowserTestUtils.loadURIString(tab1.linkedBrowser, testUrl);
+ await Promise.all([formProcessedPromise, dialogObserved]);
+
+ Assert.ok(
+ formProcessedPromise,
+ "Observer should be notified when form is loaded into a hidden document"
+ );
+ Assert.ok(
+ dialogObserved,
+ "MP Dialog should be shown when form is loaded into a hidden document"
+ );
+
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+ });
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_autofill_http.js b/toolkit/components/passwordmgr/test/browser/browser_autofill_http.js
new file mode 100644
index 0000000000..df80693673
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_autofill_http.js
@@ -0,0 +1,135 @@
+const TEST_URL_PATH =
+ "://example.org/browser/toolkit/components/passwordmgr/test/browser/";
+
+add_setup(async function () {
+ const login1 = LoginTestUtils.testData.formLogin({
+ origin: "http://example.org",
+ formActionOrigin: "http://example.org",
+ username: "username",
+ password: "password",
+ });
+ const login2 = LoginTestUtils.testData.formLogin({
+ origin: "http://example.org",
+ formActionOrigin: "http://example.com",
+ username: "username",
+ password: "password",
+ });
+ await Services.logins.addLogins([login1, login2]);
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.autofillForms.http", false]],
+ });
+});
+
+add_task(async function test_http_autofill() {
+ for (let scheme of ["http", "https"]) {
+ let formFilled = listenForTestNotification("FormProcessed");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ `${scheme}${TEST_URL_PATH}form_basic.html`
+ );
+
+ await formFilled;
+
+ let [username, password] = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let doc = content.document;
+ let contentUsername = doc.getElementById("form-basic-username").value;
+ let contentPassword = doc.getElementById("form-basic-password").value;
+ return [contentUsername, contentPassword];
+ }
+ );
+
+ Assert.equal(
+ username,
+ scheme == "http" ? "" : "username",
+ "Username filled correctly"
+ );
+ Assert.equal(
+ password,
+ scheme == "http" ? "" : "password",
+ "Password filled correctly"
+ );
+
+ gBrowser.removeTab(tab);
+ }
+});
+
+add_task(async function test_iframe_in_http_autofill() {
+ for (let scheme of ["http", "https"]) {
+ // Wait for parent and child iframe to be processed.
+ let formFilled = listenForTestNotification("FormProcessed", 2);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ `${scheme}${TEST_URL_PATH}form_basic_iframe.html`
+ );
+
+ await formFilled;
+
+ let [username, password] = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser.browsingContext.children[0],
+ [],
+ async function () {
+ let doc = this.content.document;
+ return [
+ doc.getElementById("form-basic-username").value,
+ doc.getElementById("form-basic-password").value,
+ ];
+ }
+ );
+
+ Assert.equal(
+ username,
+ scheme == "http" ? "" : "username",
+ "Username filled correctly"
+ );
+ Assert.equal(
+ password,
+ scheme == "http" ? "" : "password",
+ "Password filled correctly"
+ );
+
+ gBrowser.removeTab(tab);
+ }
+});
+
+add_task(async function test_http_action_autofill() {
+ for (let type of ["insecure", "secure"]) {
+ let formFilled = listenForTestNotification("FormProcessed");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ `https${TEST_URL_PATH}form_cross_origin_${type}_action.html`
+ );
+
+ await formFilled;
+
+ let [username, password] = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ let doc = this.content.document;
+ return [
+ doc.getElementById("form-basic-username").value,
+ doc.getElementById("form-basic-password").value,
+ ];
+ }
+ );
+
+ Assert.equal(
+ username,
+ type == "insecure" ? "" : "username",
+ "Username filled correctly"
+ );
+ Assert.equal(
+ password,
+ type == "insecure" ? "" : "password",
+ "Password filled correctly"
+ );
+
+ gBrowser.removeTab(tab);
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_autofill_track_filled_logins.js b/toolkit/components/passwordmgr/test/browser/browser_autofill_track_filled_logins.js
new file mode 100644
index 0000000000..df9580828d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_autofill_track_filled_logins.js
@@ -0,0 +1,111 @@
+"use strict";
+
+const TEST_HOSTNAME = "https://example.com";
+const BASIC_FORM_PAGE_PATH = DIRECTORY_PATH + "form_basic.html";
+const BASIC_FORM_NO_USERNAME_PAGE_PATH =
+ DIRECTORY_PATH + "form_basic_no_username.html";
+
+add_task(async function test() {
+ let nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo,
+ "init"
+ );
+ for (let usernameRequested of [true, false]) {
+ info(
+ "Testing page with " +
+ (usernameRequested ? "" : "no ") +
+ "username requested"
+ );
+ let url = usernameRequested
+ ? TEST_HOSTNAME + BASIC_FORM_PAGE_PATH
+ : TEST_HOSTNAME + BASIC_FORM_NO_USERNAME_PAGE_PATH;
+
+ // The login here must be a different domain than the page for this testcase.
+ let login = new nsLoginInfo(
+ "https://example.org",
+ "https://example.org",
+ null,
+ "bob",
+ "mypassword",
+ "form-basic-username",
+ "form-basic-password"
+ );
+ login = await Services.logins.addLoginAsync(login);
+ Assert.equal(
+ login.timesUsed,
+ 1,
+ "The timesUsed should be 1 after creation"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url,
+ });
+
+ // Convert the login object to a plain JS object for passing across process boundaries.
+ login = LoginHelper.loginToVanillaObject(login);
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [{ login, usernameRequested }],
+ async ({ login: addedLogin, usernameRequested: aUsernameRequested }) => {
+ const { LoginFormFactory } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginFormFactory.sys.mjs"
+ );
+ const { LoginManagerChild } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginManagerChild.sys.mjs"
+ );
+ const { LoginHelper } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginHelper.sys.mjs"
+ );
+
+ let password = content.document.querySelector("#form-basic-password");
+ let formLike = LoginFormFactory.createFromField(password);
+ info("Calling _fillForm with FormLike");
+ addedLogin = LoginHelper.vanillaObjectToLogin(addedLogin);
+ LoginManagerChild.forWindow(content)._fillForm(
+ formLike,
+ [addedLogin],
+ null,
+ {
+ autofillForm: true,
+ clobberUsername: true,
+ clobberPassword: true,
+ userTriggered: true,
+ }
+ );
+
+ if (aUsernameRequested) {
+ let username = content.document.querySelector("#form-basic-username");
+ Assert.equal(username.value, "bob", "Filled username should match");
+ }
+ Assert.equal(
+ password.value,
+ "mypassword",
+ "Filled password should match"
+ );
+ }
+ );
+
+ let processedPromise = listenForTestNotification("ShowDoorhanger");
+ SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.document.getElementById("form-basic").submit();
+ });
+ await processedPromise;
+
+ let logins = Services.logins.getAllLogins();
+
+ Assert.equal(logins.length, 1, "There should only be one login saved");
+ Assert.equal(
+ logins[0].guid,
+ login.guid,
+ "The saved login should match the one added and used above"
+ );
+ checkOnlyLoginWasUsedTwice({ justChanged: false });
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Reset all passwords before next iteration.
+ Services.logins.removeAllUserFacingLogins();
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_basicAuth_multiTab.js b/toolkit/components/passwordmgr/test/browser/browser_basicAuth_multiTab.js
new file mode 100644
index 0000000000..68f21d0ea4
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_basicAuth_multiTab.js
@@ -0,0 +1,158 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Tests that can show multiple auth prompts in different tabs and handle them
+ * correctly.
+ */
+
+const ORIGIN1 = "https://example.com";
+const ORIGIN2 = "https://example.org";
+
+const AUTH_PATH =
+ "/browser/toolkit/components/passwordmgr/test/browser/authenticate.sjs";
+
+/**
+ * Opens a tab and navigates to the auth test path.
+ * @param {String} origin - Origin to open with test path.
+ * @param {Object} authOptions - Authentication options to pass to server and
+ * test for.
+ * @param {String} authOptions.user - Expected username.
+ * @param {String} authOptions.pass - Expected password.
+ * @param {String} authOptions.realm - Realm to return on auth request.
+ * @returns {Object} - An object containing passed origin and authOptions,
+ * opened tab, a promise which resolves once the tab loads, a promise which
+ * resolves once the prompt has been opened.
+ */
+async function openTabWithAuthPrompt(origin, authOptions) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+
+ let promptPromise = PromptTestUtils.waitForPrompt(tab.linkedBrowser, {
+ modalType: Services.prompt.MODAL_TYPE_TAB,
+ promptType: "promptUserAndPass",
+ });
+ let url = new URL(origin + AUTH_PATH);
+ Object.entries(authOptions).forEach(([key, value]) =>
+ url.searchParams.append(key, value)
+ );
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ url.toString()
+ );
+ info("Loading " + url.toString());
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, url.toString());
+ return { origin, tab, authOptions, loadPromise, promptPromise };
+}
+
+/**
+ * Waits for tab to load and tests for expected auth state.
+ * @param {boolean} expectAuthed - true: auth success, false otherwise.
+ * @param {Object} tabInfo - Information about the tab as generated by
+ * openTabWithAuthPrompt.
+ */
+async function testTabAuthed(expectAuthed, { tab, loadPromise, authOptions }) {
+ // Wait for tab to load after auth.
+ await loadPromise;
+ Assert.ok(true, "Tab loads after auth");
+
+ // Fetch auth results from body (set by authenticate.sjs).
+ let { loginResult, user, pass } = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ () => {
+ let result = {};
+ result.loginResult = content.document.getElementById("ok").innerText;
+ result.user = content.document.getElementById("user").innerText;
+ result.pass = content.document.getElementById("pass").innerText;
+ return result;
+ }
+ );
+
+ Assert.equal(
+ loginResult == "PASS",
+ expectAuthed,
+ "Site has expected auth state"
+ );
+ Assert.equal(user, expectAuthed ? authOptions.user : "", "Sent correct user");
+ Assert.equal(
+ pass,
+ expectAuthed ? authOptions.pass : "",
+ "Sent correct password"
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ // This test relies on tab auth prompts.
+ set: [["prompts.modalType.httpAuth", Services.prompt.MODAL_TYPE_TAB]],
+ });
+});
+
+add_task(async function test() {
+ let tabA = await openTabWithAuthPrompt(ORIGIN1, {
+ user: "userA",
+ pass: "passA",
+ realm: "realmA",
+ });
+ // Tab B and C share realm and credentials.
+ // However, since the auth happens in separate tabs we should get two prompts.
+ let tabB = await openTabWithAuthPrompt(ORIGIN2, {
+ user: "userB",
+ pass: "passB",
+ realm: "realmB",
+ });
+ let tabC = await openTabWithAuthPrompt(ORIGIN2, {
+ user: "userB",
+ pass: "passB",
+ realm: "realmB",
+ });
+ let tabs = [tabA, tabB, tabC];
+
+ info(`Opening ${tabs.length} tabs with auth prompts`);
+ let prompts = await Promise.all(tabs.map(tab => tab.promptPromise));
+
+ Assert.equal(prompts.length, tabs.length, "Should have one prompt per tab");
+
+ for (let i = 0; i < prompts.length; i++) {
+ let titleEl = prompts[i].ui.prompt.document.querySelector("#titleText");
+ Assert.equal(
+ titleEl.textContent,
+ new URL(tabs[i].origin).host,
+ "Prompt matches the tab's host"
+ );
+ }
+
+ // Interact with the prompts. This is deliberately done out of order
+ // (no FIFO, LIFO).
+ let [promptA, promptB, promptC] = prompts;
+
+ // Accept prompt B with correct login details.
+ await PromptTestUtils.handlePrompt(promptB, {
+ loginInput: tabB.authOptions.user,
+ passwordInput: tabB.authOptions.pass,
+ });
+ await testTabAuthed(true, tabB);
+
+ // Accept prompt A with correct login details
+ await PromptTestUtils.handlePrompt(promptA, {
+ loginInput: tabA.authOptions.user,
+ passwordInput: tabA.authOptions.pass,
+ });
+ await testTabAuthed(true, tabA);
+
+ // Cancel prompt C
+ await PromptTestUtils.handlePrompt(promptC, {
+ buttonNumClick: 1,
+ });
+ await testTabAuthed(false, tabC);
+
+ // Cleanup tabs
+ tabs.forEach(({ tab }) => BrowserTestUtils.removeTab(tab));
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_basicAuth_rateLimit.js b/toolkit/components/passwordmgr/test/browser/browser_basicAuth_rateLimit.js
new file mode 100644
index 0000000000..1da16090c9
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_basicAuth_rateLimit.js
@@ -0,0 +1,146 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This tests that the basic auth dialog can not be used for DOS attacks
+// and that the protections are reset on user-initiated navigation/reload.
+
+let promptModalType = Services.prefs.getIntPref("prompts.modalType.httpAuth");
+
+function promiseAuthWindowShown() {
+ return PromptTestUtils.handleNextPrompt(
+ window,
+ { modalType: promptModalType, promptType: "promptUserAndPass" },
+ { buttonNumClick: 1 }
+ );
+}
+
+add_task(async function test() {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com",
+ async function (browser) {
+ let cancelDialogLimit = Services.prefs.getIntPref(
+ "prompts.authentication_dialog_abuse_limit"
+ );
+
+ let authShown = promiseAuthWindowShown();
+ let browserLoaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.loadURIString(
+ browser,
+ "https://example.com/browser/toolkit/components/passwordmgr/test/browser/authenticate.sjs"
+ );
+ await authShown;
+ Assert.ok(true, "Seen dialog number 1");
+ await browserLoaded;
+ Assert.ok(true, "Loaded document number 1");
+
+ // Reload the document a bit more often than should be allowed.
+ // As long as we're in the acceptable range we should receive
+ // auth prompts, otherwise we should not receive them and the
+ // page should just load.
+ // We've already seen the dialog once, hence we start the loop at 1.
+ for (let i = 1; i < cancelDialogLimit + 2; i++) {
+ if (i < cancelDialogLimit) {
+ authShown = promiseAuthWindowShown();
+ }
+ browserLoaded = BrowserTestUtils.browserLoaded(browser);
+ SpecialPowers.spawn(browser, [], function () {
+ content.document.location.reload();
+ });
+ if (i < cancelDialogLimit) {
+ await authShown;
+ Assert.ok(true, `Seen dialog number ${i + 1}`);
+ }
+ await browserLoaded;
+ Assert.ok(true, `Loaded document number ${i + 1}`);
+ }
+
+ let reloadButton = document.getElementById("reload-button");
+ await TestUtils.waitForCondition(
+ () => !reloadButton.hasAttribute("disabled")
+ );
+
+ // Verify that we can click the reload button to reset the counter.
+ authShown = promiseAuthWindowShown();
+ browserLoaded = BrowserTestUtils.browserLoaded(browser);
+ reloadButton.click();
+ await authShown;
+ Assert.ok(true, "Seen dialog number 1");
+ await browserLoaded;
+ Assert.ok(true, "Loaded document number 1");
+
+ // Now check loading subresources with auth on the page.
+ browserLoaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.loadURIString(browser, "https://example.com");
+ await browserLoaded;
+
+ // We've already seen the dialog once, hence we start the loop at 1.
+ for (let i = 1; i < cancelDialogLimit + 2; i++) {
+ if (i < cancelDialogLimit) {
+ authShown = promiseAuthWindowShown();
+ }
+
+ let iframeLoaded = SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ let iframe = doc.createElement("iframe");
+ doc.body.appendChild(iframe);
+ let loaded = new Promise(resolve => {
+ iframe.addEventListener(
+ "load",
+ function (e) {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ iframe.src =
+ "https://example.com/browser/toolkit/components/passwordmgr/test/browser/authenticate.sjs";
+ await loaded;
+ });
+
+ if (i < cancelDialogLimit) {
+ await authShown;
+ Assert.ok(true, `Seen dialog number ${i + 1}`);
+ }
+
+ await iframeLoaded;
+ Assert.ok(true, `Loaded iframe number ${i + 1}`);
+ }
+
+ // Verify that third party subresources can not spawn new auth dialogs.
+ let iframeLoaded = SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ let iframe = doc.createElement("iframe");
+ doc.body.appendChild(iframe);
+ let loaded = new Promise(resolve => {
+ iframe.addEventListener(
+ "load",
+ function (e) {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ iframe.src =
+ "https://example.org/browser/toolkit/components/passwordmgr/test/browser/authenticate.sjs";
+ await loaded;
+ });
+
+ await iframeLoaded;
+ Assert.ok(
+ true,
+ "Loaded a third party iframe without showing the auth dialog"
+ );
+
+ // Verify that pressing enter in the urlbar also resets the counter.
+ authShown = promiseAuthWindowShown();
+ browserLoaded = BrowserTestUtils.browserLoaded(browser);
+ gURLBar.value =
+ "https://example.com/browser/toolkit/components/passwordmgr/test/browser/authenticate.sjs";
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+ await authShown;
+ await browserLoaded;
+ }
+ );
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_basicAuth_switchTab.js b/toolkit/components/passwordmgr/test/browser/browser_basicAuth_switchTab.js
new file mode 100644
index 0000000000..32bcd13ae4
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_basicAuth_switchTab.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let modalType = Services.prefs.getIntPref("prompts.modalType.httpAuth");
+
+add_task(async function test() {
+ let tab = BrowserTestUtils.addTab(gBrowser);
+ isnot(tab, gBrowser.selectedTab, "New tab shouldn't be selected");
+
+ let authPromptShown = PromptTestUtils.waitForPrompt(tab.linkedBrowser, {
+ modalType,
+ promptType: "promptUserAndPass",
+ });
+
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ BrowserTestUtils.loadURIString(
+ tab.linkedBrowser,
+ "https://example.com/browser/toolkit/components/passwordmgr/test/browser/authenticate.sjs"
+ );
+
+ // Wait for the basic auth prompt
+ let dialog = await authPromptShown;
+
+ Assert.equal(gBrowser.selectedTab, tab, "Should have selected the new tab");
+
+ // Cancel the auth prompt
+ PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 1 });
+
+ // After closing the prompt the load should finish
+ await loadPromise;
+
+ gBrowser.removeTab(tab);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_context_menu.js b/toolkit/components/passwordmgr/test/browser/browser_context_menu.js
new file mode 100644
index 0000000000..63e161c003
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu.js
@@ -0,0 +1,678 @@
+/**
+ * Test the password manager context menu.
+ */
+
+/* eslint no-shadow:"off" */
+
+"use strict";
+
+// The origin for the test URIs.
+const TEST_ORIGIN = "https://example.com";
+const MULTIPLE_FORMS_PAGE_PATH =
+ "/browser/toolkit/components/passwordmgr/test/browser/multiple_forms.html";
+
+const CONTEXT_MENU = document.getElementById("contentAreaContextMenu");
+const POPUP_HEADER = document.getElementById("fill-login");
+
+/**
+ * Initialize logins needed for the tests and disable autofill
+ * for login forms for easier testing of manual fill.
+ */
+add_task(async function test_initialize() {
+ Services.prefs.setBoolPref("signon.autofillForms", false);
+ Services.prefs.setBoolPref("signon.usernameOnlyForm.enabled", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("signon.autofillForms");
+ Services.prefs.clearUserPref("signon.schemeUpgrades");
+ Services.prefs.clearUserPref("signon.usernameOnlyForm.enabled");
+ });
+ await Services.logins.addLogins(loginList());
+});
+
+/**
+ * Check if the context menu is populated with the right
+ * menuitems for the target password input field.
+ */
+add_task(async function test_context_menu_populate_password_noSchemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", false);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + MULTIPLE_FORMS_PAGE_PATH,
+ },
+ async function (browser) {
+ await openPasswordContextMenu(browser, "#test-password-1");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 2);
+
+ await closePopup(CONTEXT_MENU);
+ }
+ );
+});
+
+/**
+ * Check if the context menu is populated with the right
+ * menuitems for the target password input field.
+ */
+add_task(async function test_context_menu_populate_password_schemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + MULTIPLE_FORMS_PAGE_PATH,
+ },
+ async function (browser) {
+ await openPasswordContextMenu(browser, "#test-password-1");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 3);
+
+ await closePopup(CONTEXT_MENU);
+ }
+ );
+});
+
+/**
+ * Check if the context menu is populated with the right menuitems
+ * for the target username field with a password field present.
+ */
+add_task(
+ async function test_context_menu_populate_username_with_password_noSchemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", false);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ TEST_ORIGIN +
+ "/browser/toolkit/components/" +
+ "passwordmgr/test/browser/multiple_forms.html",
+ },
+ async function (browser) {
+ await openPasswordContextMenu(browser, "#test-username-3");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 2);
+
+ await closePopup(CONTEXT_MENU);
+ }
+ );
+ }
+);
+/**
+ * Check if the context menu is populated with the right menuitems
+ * for the target username field with a password field present.
+ */
+add_task(
+ async function test_context_menu_populate_username_with_password_schemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ TEST_ORIGIN +
+ "/browser/toolkit/components/" +
+ "passwordmgr/test/browser/multiple_forms.html",
+ },
+ async function (browser) {
+ await openPasswordContextMenu(browser, "#test-username-3");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 3);
+
+ await closePopup(CONTEXT_MENU);
+ }
+ );
+ }
+);
+
+/**
+ * Check if the context menu is populated with the right menuitems
+ * for the target username field without a password field present.
+ */
+add_task(
+ async function test_context_menu_populate_username_with_password_noSchemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", false);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ TEST_ORIGIN +
+ "/browser/toolkit/components/" +
+ "passwordmgr/test/browser/multiple_forms.html",
+ },
+ async function (browser) {
+ await openPasswordContextMenu(browser, "#test-username-1");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 2);
+
+ await closePopup(CONTEXT_MENU);
+ }
+ );
+ }
+);
+/**
+ * Check if the context menu is populated with the right menuitems
+ * for the target username field without a password field present.
+ */
+add_task(
+ async function test_context_menu_populate_username_with_password_schemeUpgrades() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ TEST_ORIGIN +
+ "/browser/toolkit/components/" +
+ "passwordmgr/test/browser/multiple_forms.html",
+ },
+ async function (browser) {
+ await openPasswordContextMenu(browser, "#test-username-1");
+
+ // Check the content of the password manager popup
+ let popupMenu = document.getElementById("fill-login-popup");
+ checkMenu(popupMenu, 3);
+
+ await closePopup(CONTEXT_MENU);
+ }
+ );
+ }
+);
+
+/**
+ * Check if the password field is correctly filled when one
+ * login menuitem is clicked.
+ */
+add_task(async function test_context_menu_password_fill() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + MULTIPLE_FORMS_PAGE_PATH,
+ },
+ async function (browser) {
+ let formDescriptions = await SpecialPowers.spawn(
+ browser,
+ [],
+ async function () {
+ let forms = Array.from(
+ content.document.getElementsByClassName("test-form")
+ );
+ return forms.map(f => f.getAttribute("description"));
+ }
+ );
+
+ for (let description of formDescriptions) {
+ info("Testing form: " + description);
+
+ let passwordInputIds = await SpecialPowers.spawn(
+ browser,
+ [{ description }],
+ async function ({ description }) {
+ let formElement = content.document.querySelector(
+ `[description="${description}"]`
+ );
+ let passwords = Array.from(
+ formElement.querySelectorAll(
+ "input[type='password'], input[data-type='password']"
+ )
+ );
+ return passwords.map(p => p.id);
+ }
+ );
+
+ for (let inputId of passwordInputIds) {
+ info("Testing password field: " + inputId);
+
+ // Synthesize a right mouse click over the password input element.
+ await openPasswordContextMenu(
+ browser,
+ "#" + inputId,
+ async function () {
+ let inputDisabled = await SpecialPowers.spawn(
+ browser,
+ [{ inputId }],
+ async function ({ inputId }) {
+ let input = content.document.getElementById(inputId);
+ return input.disabled || input.readOnly;
+ }
+ );
+
+ // If the password field is disabled or read-only, we want to see
+ // the disabled Fill Password popup header.
+ if (inputDisabled) {
+ Assert.ok(!POPUP_HEADER.hidden, "Popup menu is not hidden.");
+ Assert.ok(POPUP_HEADER.disabled, "Popup menu is disabled.");
+ await closePopup(CONTEXT_MENU);
+ }
+ Assert.equal(
+ POPUP_HEADER.getAttribute("data-l10n-id"),
+ "main-context-menu-use-saved-password",
+ "top-level label is correct"
+ );
+
+ return !inputDisabled;
+ }
+ );
+
+ if (CONTEXT_MENU.state != "open") {
+ continue;
+ }
+
+ // The only field affected by the password fill
+ // should be the target password field itself.
+ await assertContextMenuFill(browser, description, null, inputId, 1);
+ await SpecialPowers.spawn(
+ browser,
+ [{ inputId }],
+ async function ({ inputId }) {
+ let passwordField = content.document.getElementById(inputId);
+ Assert.equal(
+ passwordField.value,
+ "password1",
+ "Check upgraded login was actually used"
+ );
+ }
+ );
+
+ await closePopup(CONTEXT_MENU);
+ }
+ }
+ }
+ );
+});
+
+/**
+ * Check if the form is correctly filled when one
+ * username context menu login menuitem is clicked.
+ */
+add_task(async function test_context_menu_username_login_fill() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + MULTIPLE_FORMS_PAGE_PATH,
+ },
+ async function (browser) {
+ let formDescriptions = await SpecialPowers.spawn(
+ browser,
+ [],
+ async function () {
+ let forms = Array.from(
+ content.document.getElementsByClassName("test-form")
+ );
+ return forms.map(f => f.getAttribute("description"));
+ }
+ );
+
+ for (let description of formDescriptions) {
+ info("Testing form: " + description);
+ let usernameInputIds = await SpecialPowers.spawn(
+ browser,
+ [{ description }],
+ async function ({ description }) {
+ let formElement = content.document.querySelector(
+ `[description="${description}"]`
+ );
+ let inputs = Array.from(
+ formElement.querySelectorAll(
+ "input[type='text']:not([data-type='password'])"
+ )
+ );
+ return inputs.map(p => p.id);
+ }
+ );
+
+ for (let inputId of usernameInputIds) {
+ info("Testing username field: " + inputId);
+
+ // Synthesize a right mouse click over the username input element.
+ await openPasswordContextMenu(
+ browser,
+ "#" + inputId,
+ async function () {
+ let headerHidden = POPUP_HEADER.hidden;
+ let headerDisabled = POPUP_HEADER.disabled;
+ let headerLabelID = POPUP_HEADER.getAttribute("data-l10n-id");
+
+ let data = {
+ description,
+ inputId,
+ headerHidden,
+ headerDisabled,
+ headerLabelID,
+ };
+ let shouldContinue = await SpecialPowers.spawn(
+ browser,
+ [data],
+ async function (data) {
+ let {
+ description,
+ inputId,
+ headerHidden,
+ headerDisabled,
+ headerLabelID,
+ } = data;
+ let formElement = content.document.querySelector(
+ `[description="${description}"]`
+ );
+ let usernameField = content.document.getElementById(inputId);
+ // We always want to check if the first password field is filled,
+ // since this is the current behavior from the _fillForm function.
+ let passwordField = formElement.querySelector(
+ "input[type='password'], input[data-type='password']"
+ );
+
+ // If we don't want to see the actual popup menu,
+ // check if the popup is hidden or disabled.
+ if (
+ !passwordField ||
+ usernameField.disabled ||
+ usernameField.readOnly ||
+ passwordField.disabled ||
+ passwordField.readOnly
+ ) {
+ if (!passwordField) {
+ // Should show popup for a username-only form.
+ if (usernameField.autocomplete == "username") {
+ Assert.ok(!headerHidden, "Popup menu is not hidden.");
+ } else {
+ Assert.ok(headerHidden, "Popup menu is hidden.");
+ }
+ } else {
+ Assert.ok(!headerHidden, "Popup menu is not hidden.");
+ Assert.ok(headerDisabled, "Popup menu is disabled.");
+ }
+ return false;
+ }
+ Assert.equal(
+ headerLabelID,
+ "main-context-menu-use-saved-login",
+ "top-level label is correct"
+ );
+ return true;
+ }
+ );
+
+ if (!shouldContinue) {
+ await closePopup(CONTEXT_MENU);
+ }
+
+ return shouldContinue;
+ }
+ );
+
+ if (CONTEXT_MENU.state != "open") {
+ continue;
+ }
+
+ let passwordFieldId = await SpecialPowers.spawn(
+ browser,
+ [{ description }],
+ async function ({ description }) {
+ let formElement = content.document.querySelector(
+ `[description="${description}"]`
+ );
+ return formElement.querySelector(
+ "input[type='password'], input[data-type='password']"
+ ).id;
+ }
+ );
+
+ // We shouldn't change any field that's not the target username field or the first password field
+ await assertContextMenuFill(
+ browser,
+ description,
+ inputId,
+ passwordFieldId,
+ 1
+ );
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ passwordFieldId }],
+ async function ({ passwordFieldId }) {
+ let passwordField =
+ content.document.getElementById(passwordFieldId);
+ if (!passwordField.hasAttribute("expectedFail")) {
+ Assert.equal(
+ passwordField.value,
+ "password1",
+ "Check upgraded login was actually used"
+ );
+ }
+ }
+ );
+
+ await closePopup(CONTEXT_MENU);
+ }
+ }
+ }
+ );
+});
+
+/**
+ * Check event telemetry is correctly recorded when opening the saved logins / management UI
+ * from the context menu
+ */
+add_task(async function test_context_menu_open_management() {
+ Services.prefs.setBoolPref("signon.schemeUpgrades", false);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + MULTIPLE_FORMS_PAGE_PATH,
+ },
+ async function (browser) {
+ await openPasswordContextMenu(browser, "#test-password-1");
+
+ let openingFunc = () => gContextMenu.openPasswordManager();
+ // wait until the management UI opens
+ let passwordManager = await openPasswordManager(openingFunc);
+ info("Management UI dialog was opened");
+
+ TelemetryTestUtils.assertEvents(
+ [["pwmgr", "open_management", "contextmenu"]],
+ { category: "pwmgr", method: "open_management" },
+ { clear: true, process: "content" }
+ );
+
+ await passwordManager.close();
+ await closePopup(CONTEXT_MENU);
+ }
+ );
+});
+
+/**
+ * Verify that only the expected form fields are filled.
+ */
+async function assertContextMenuFill(
+ browser,
+ formId,
+ usernameFieldId,
+ passwordFieldId,
+ loginIndex
+) {
+ let popupMenu = document.getElementById("fill-login-popup");
+ let unchangedSelector = `[description="${formId}"] input:not(#${passwordFieldId})`;
+
+ if (usernameFieldId) {
+ unchangedSelector += `:not(#${usernameFieldId})`;
+ }
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ unchangedSelector }],
+ async function ({ unchangedSelector }) {
+ let unchangedFields =
+ content.document.querySelectorAll(unchangedSelector);
+
+ // Store the value of fields that should remain unchanged.
+ if (unchangedFields.length) {
+ for (let field of unchangedFields) {
+ field.setAttribute("original-value", field.value);
+ }
+ }
+ }
+ );
+
+ // Execute the default command of the specified login menuitem found in the context menu.
+ let loginItem =
+ popupMenu.getElementsByClassName("context-login-item")[loginIndex];
+
+ // Find the used login by it's username (Use only unique usernames in this test).
+ let { username, password } = getLoginFromUsername(loginItem.label);
+
+ let data = {
+ username,
+ password,
+ usernameFieldId,
+ passwordFieldId,
+ formId,
+ unchangedSelector,
+ };
+ let continuePromise = ContentTask.spawn(browser, data, async function (data) {
+ let {
+ username,
+ password,
+ usernameFieldId,
+ passwordFieldId,
+ formId,
+ unchangedSelector,
+ } = data;
+ let form = content.document.querySelector(`[description="${formId}"]`);
+ await ContentTaskUtils.waitForEvent(
+ form,
+ "input",
+ "Username input value changed"
+ );
+
+ if (usernameFieldId) {
+ let usernameField = content.document.getElementById(usernameFieldId);
+
+ // If we have an username field, check if it's correctly filled
+ if (usernameField.getAttribute("expectedFail") == null) {
+ Assert.equal(
+ username,
+ usernameField.value,
+ "Username filled and correct."
+ );
+ }
+ }
+
+ if (passwordFieldId) {
+ let passwordField = content.document.getElementById(passwordFieldId);
+
+ // If we have a password field, check if it's correctly filled
+ if (passwordField && passwordField.getAttribute("expectedFail") == null) {
+ Assert.equal(
+ password,
+ passwordField.value,
+ "Password filled and correct."
+ );
+ }
+ }
+
+ let unchangedFields = content.document.querySelectorAll(unchangedSelector);
+
+ // Check that all fields that should not change have the same value as before.
+ if (unchangedFields.length) {
+ Assert.ok(() => {
+ for (let field of unchangedFields) {
+ if (field.value != field.getAttribute("original-value")) {
+ return false;
+ }
+ }
+ return true;
+ }, "Other fields were not changed.");
+ }
+ });
+
+ loginItem.doCommand();
+
+ return continuePromise;
+}
+
+/**
+ * Check if every login that matches the page origin are available at the context menu.
+ * @param {Element} contextMenu
+ * @param {Number} expectedCount - Number of logins expected in the context menu. Used to ensure
+ * we continue testing something useful.
+ */
+function checkMenu(contextMenu, expectedCount) {
+ let logins = loginList().filter(login => {
+ return LoginHelper.isOriginMatching(login.origin, TEST_ORIGIN, {
+ schemeUpgrades: Services.prefs.getBoolPref("signon.schemeUpgrades"),
+ });
+ });
+ // Make an array of menuitems for easier comparison.
+ let menuitems = [
+ ...CONTEXT_MENU.getElementsByClassName("context-login-item"),
+ ];
+ Assert.equal(
+ menuitems.length,
+ expectedCount,
+ "Expected number of menu items"
+ );
+ Assert.ok(
+ logins.every(l => menuitems.some(m => l.username == m.label)),
+ "Every login have an item at the menu."
+ );
+}
+
+/**
+ * Search for a login by it's username.
+ *
+ * Only unique login/origin combinations should be used at this test.
+ */
+function getLoginFromUsername(username) {
+ return loginList().find(login => login.username == username);
+}
+
+/**
+ * List of logins used for the test.
+ *
+ * We should only use unique usernames in this test,
+ * because we need to search logins by username. There is one duplicate u+p combo
+ * in order to test de-duping in the menu.
+ */
+function loginList() {
+ return [
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: "username",
+ password: "password",
+ }),
+ // Same as above but HTTP in order to test de-duping.
+ LoginTestUtils.testData.formLogin({
+ origin: "http://example.com",
+ formActionOrigin: "http://example.com",
+ username: "username",
+ password: "password",
+ }),
+ LoginTestUtils.testData.formLogin({
+ origin: "http://example.com",
+ formActionOrigin: "http://example.com",
+ username: "username1",
+ password: "password1",
+ }),
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: "username2",
+ password: "password2",
+ }),
+ LoginTestUtils.testData.formLogin({
+ origin: "http://example.org",
+ formActionOrigin: "http://example.org",
+ username: "username-cross-origin",
+ password: "password-cross-origin",
+ }),
+ ];
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js b/toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js
new file mode 100644
index 0000000000..8ffe07a673
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js
@@ -0,0 +1,120 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+/*
+ * Test the password manager context menu interaction with autocomplete.
+ */
+
+"use strict";
+
+const TEST_ORIGIN = "https://example.com";
+const BASIC_FORM_PAGE_PATH = DIRECTORY_PATH + "form_basic.html";
+
+/**
+ * Initialize logins needed for the tests and disable autofill
+ * for login forms for easier testing of manual fill.
+ */
+add_task(async function test_initialize() {
+ let autocompletePopup = document.getElementById("PopupAutoComplete");
+ Services.prefs.setBoolPref("signon.autofillForms", false);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("signon.autofillForms");
+ autocompletePopup.removeEventListener(
+ "popupshowing",
+ autocompleteUnexpectedPopupShowing
+ );
+ });
+ await Services.logins.addLogins(loginList());
+ autocompletePopup.addEventListener(
+ "popupshowing",
+ autocompleteUnexpectedPopupShowing
+ );
+});
+
+add_task(async function test_context_menu_username() {
+ let formFilled = listenForTestNotification("FormProcessed");
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + BASIC_FORM_PAGE_PATH,
+ },
+ async function (browser) {
+ await formFilled;
+ await openContextMenu(browser, "#form-basic-username");
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ Assert.equal(contextMenu.state, "open", "Context menu opened");
+ contextMenu.hidePopup();
+ }
+ );
+});
+
+add_task(async function test_context_menu_password() {
+ let formFilled = listenForTestNotification("FormProcessed");
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + BASIC_FORM_PAGE_PATH,
+ },
+ async function (browser) {
+ await formFilled;
+ await openContextMenu(browser, "#form-basic-password");
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ Assert.equal(contextMenu.state, "open", "Context menu opened");
+ contextMenu.hidePopup();
+ }
+ );
+});
+
+function autocompleteUnexpectedPopupShowing(event) {
+ Assert.ok(false, "Autocomplete shouldn't appear");
+ event.target.hidePopup();
+}
+
+/**
+ * Synthesize mouse clicks to open the context menu popup
+ * for a target login input element.
+ */
+async function openContextMenu(browser, loginInput) {
+ // First synthesize a mousedown. We need this to get the focus event with the "contextmenu" event.
+ let eventDetails1 = { type: "mousedown", button: 2 };
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ loginInput,
+ eventDetails1,
+ browser
+ );
+
+ // Then synthesize the contextmenu click over the input element.
+ let contextMenuShownPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown"
+ );
+ let eventDetails = { type: "contextmenu", button: 2 };
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ loginInput,
+ eventDetails,
+ browser
+ );
+ await contextMenuShownPromise;
+
+ // Wait to see which popups are shown.
+ await new Promise(resolve => setTimeout(resolve, 1000));
+}
+
+function loginList() {
+ return [
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: "username",
+ password: "password",
+ }),
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: "username2",
+ password: "password2",
+ }),
+ ];
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_context_menu_generated_password.js b/toolkit/components/passwordmgr/test/browser/browser_context_menu_generated_password.js
new file mode 100644
index 0000000000..4e4edb7b14
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu_generated_password.js
@@ -0,0 +1,482 @@
+/**
+ * Test the password manager context menu item can fill password fields with a generated password.
+ */
+
+/* eslint no-shadow:"off" */
+
+"use strict";
+
+// The origin for the test URIs.
+const TEST_ORIGIN = "https://example.com";
+const FORM_PAGE_PATH =
+ "/browser/toolkit/components/passwordmgr/test/browser/form_basic_login.html";
+const CONTEXT_MENU = document.getElementById("contentAreaContextMenu");
+
+const passwordInputSelector = "#form-basic-password";
+
+registerCleanupFunction(async function cleanup_resetPrefs() {
+ await SpecialPowers.popPrefEnv();
+});
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.generation.available", true],
+ ["signon.generation.enabled", true],
+ ],
+ });
+ // assert that there are no logins
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 0, "There are no logins");
+});
+
+add_task(async function test_hidden_by_prefs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.generation.available", true],
+ ["signon.generation.enabled", false],
+ ],
+ });
+
+ // test that the generated password option is not present when the feature is not enabled
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + FORM_PAGE_PATH,
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+
+ await openPasswordContextMenu(browser, passwordInputSelector);
+ let generatedPasswordItem = document.getElementById(
+ "fill-login-generated-password"
+ );
+ Assert.ok(
+ !BrowserTestUtils.is_visible(generatedPasswordItem),
+ "generated password item is hidden"
+ );
+
+ CONTEXT_MENU.hidePopup();
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_fill_hidden_by_login_saving_disabled() {
+ // test that the generated password option is not present when the user
+ // disabled password saving for the site.
+ Services.logins.setLoginSavingEnabled(TEST_ORIGIN, false);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + FORM_PAGE_PATH,
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+
+ await openPasswordContextMenu(browser, passwordInputSelector);
+ let generatedPasswordItem = document.getElementById(
+ "fill-login-generated-password"
+ );
+ Assert.ok(
+ !BrowserTestUtils.is_visible(generatedPasswordItem),
+ "generated password item is hidden"
+ );
+
+ CONTEXT_MENU.hidePopup();
+ }
+ );
+
+ Services.logins.setLoginSavingEnabled(TEST_ORIGIN, true);
+});
+
+add_task(async function test_fill_hidden_by_locked_primary_password() {
+ // test that the generated password option is not present when the user
+ // didn't unlock the primary password.
+ LoginTestUtils.primaryPassword.enable();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + FORM_PAGE_PATH,
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+
+ await openPasswordContextMenu(
+ browser,
+ passwordInputSelector,
+ () => false
+ );
+ let generatedPasswordItem = document.getElementById(
+ "fill-login-generated-password"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(generatedPasswordItem),
+ "generated password item is visible"
+ );
+ Assert.ok(
+ generatedPasswordItem.disabled,
+ "generated password item is disabled"
+ );
+
+ CONTEXT_MENU.hidePopup();
+ }
+ );
+
+ LoginTestUtils.primaryPassword.disable();
+});
+
+add_task(async function fill_generated_password_empty_field() {
+ // test that we can fill with generated password into an empty password field
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + FORM_PAGE_PATH,
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+ await SpecialPowers.spawn(
+ browser,
+ [[passwordInputSelector]],
+ function checkInitialFieldValue(inputSelector) {
+ const input = content.document.querySelector(inputSelector);
+ Assert.equal(input.value.length, 0, "Password field is empty");
+ Assert.ok(
+ !input.matches(":autofill"),
+ "Password field should not be highlighted"
+ );
+ }
+ );
+
+ await doFillGeneratedPasswordContextMenuItem(
+ browser,
+ passwordInputSelector
+ );
+ await SpecialPowers.spawn(
+ browser,
+ [[passwordInputSelector]],
+ function checkFinalFieldValue(inputSelector) {
+ let { LoginTestUtils: LTU } = ChromeUtils.importESModule(
+ "resource://testing-common/LoginTestUtils.sys.mjs"
+ );
+ const input = content.document.querySelector(inputSelector);
+ Assert.equal(
+ input.value.length,
+ LTU.generation.LENGTH,
+ "Password field was filled with generated password"
+ );
+ Assert.ok(
+ input.matches(":autofill"),
+ "Password field should be highlighted"
+ );
+ LTU.loginField.checkPasswordMasked(input, false, "after fill");
+
+ info("cleaing the field");
+ input.setUserInput("");
+ }
+ );
+
+ let acPopup = document.getElementById("PopupAutoComplete");
+ await openACPopup(acPopup, browser, passwordInputSelector);
+
+ let pwgenItem = acPopup.querySelector(
+ `[originaltype="generatedPassword"]`
+ );
+ Assert.ok(
+ !pwgenItem || EventUtils.isHidden(pwgenItem),
+ "pwgen item should no longer be shown"
+ );
+
+ await closePopup(acPopup);
+ }
+ );
+});
+
+add_task(async function fill_generated_password_nonempty_field() {
+ // test that we can fill with generated password into an non-empty password field
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + FORM_PAGE_PATH,
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+ await changeContentFormValues(browser, {
+ [passwordInputSelector]: "aa",
+ });
+ await SpecialPowers.spawn(
+ browser,
+ [[passwordInputSelector]],
+ function checkInitialFieldValue(inputSelector) {
+ const input = content.document.querySelector(inputSelector);
+ Assert.ok(
+ !input.matches(":autofill"),
+ "Password field should not be highlighted"
+ );
+ }
+ );
+
+ await doFillGeneratedPasswordContextMenuItem(
+ browser,
+ passwordInputSelector
+ );
+ await SpecialPowers.spawn(
+ browser,
+ [[passwordInputSelector]],
+ function checkFinalFieldValue(inputSelector) {
+ let { LoginTestUtils: LTU } = ChromeUtils.importESModule(
+ "resource://testing-common/LoginTestUtils.sys.mjs"
+ );
+ const input = content.document.querySelector(inputSelector);
+ Assert.equal(
+ input.value.length,
+ LTU.generation.LENGTH,
+ "Password field was filled with generated password"
+ );
+ Assert.ok(
+ input.matches(":autofill"),
+ "Password field should be highlighted"
+ );
+ LTU.loginField.checkPasswordMasked(input, false, "after fill");
+ }
+ );
+ }
+ );
+ LoginTestUtils.clearData();
+ LoginTestUtils.resetGeneratedPasswordsCache();
+});
+
+add_task(async function fill_generated_password_with_matching_logins() {
+ // test that we can fill a generated password when there are matching logins
+ let login = LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: "username",
+ password: "pass1",
+ });
+ let storageChangedPromised = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "addLogin"
+ );
+ await Services.logins.addLoginAsync(login);
+ await storageChangedPromised;
+
+ let formFilled = listenForTestNotification("FormProcessed");
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + FORM_PAGE_PATH,
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+ await formFilled;
+ await SpecialPowers.spawn(
+ browser,
+ [[passwordInputSelector]],
+ function checkInitialFieldValue(inputSelector) {
+ Assert.equal(
+ content.document.querySelector(inputSelector).value,
+ "pass1",
+ "Password field has initial value"
+ );
+ }
+ );
+
+ await doFillGeneratedPasswordContextMenuItem(
+ browser,
+ passwordInputSelector
+ );
+ await SpecialPowers.spawn(
+ browser,
+ [[passwordInputSelector]],
+ function checkFinalFieldValue(inputSelector) {
+ let { LoginTestUtils: LTU } = ChromeUtils.importESModule(
+ "resource://testing-common/LoginTestUtils.sys.mjs"
+ );
+ const input = content.document.querySelector(inputSelector);
+ Assert.equal(
+ input.value.length,
+ LTU.generation.LENGTH,
+ "Password field was filled with generated password"
+ );
+ Assert.ok(
+ input.matches(":autofill"),
+ "Password field should be highlighted"
+ );
+ LTU.loginField.checkPasswordMasked(input, false, "after fill");
+ }
+ );
+
+ await openPasswordContextMenu(browser, passwordInputSelector);
+
+ // Execute the command of the first login menuitem found at the context menu.
+ let passwordChangedPromise = ContentTask.spawn(
+ browser,
+ null,
+ async function () {
+ let passwordInput = content.document.getElementById(
+ "form-basic-password"
+ );
+ await ContentTaskUtils.waitForEvent(passwordInput, "input");
+ }
+ );
+
+ let popupMenu = document.getElementById("fill-login-popup");
+ let firstLoginItem =
+ popupMenu.getElementsByClassName("context-login-item")[0];
+ firstLoginItem.doCommand();
+
+ await passwordChangedPromise;
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ contextMenu.hidePopup();
+
+ // Blur the field to trigger a 'change' event.
+ await BrowserTestUtils.synthesizeKey("KEY_Tab", undefined, browser);
+ await BrowserTestUtils.synthesizeKey(
+ "KEY_Tab",
+ { shiftKey: true },
+ browser
+ );
+
+ await SpecialPowers.spawn(
+ browser,
+ [[passwordInputSelector]],
+ function checkFieldNotGeneratedPassword(inputSelector) {
+ let { LoginTestUtils: LTU } = ChromeUtils.importESModule(
+ "resource://testing-common/LoginTestUtils.sys.mjs"
+ );
+ const input = content.document.querySelector(inputSelector);
+ Assert.equal(
+ input.value,
+ "pass1",
+ "Password field was filled with the saved password"
+ );
+ LTU.loginField.checkPasswordMasked(
+ input,
+ true,
+ "after fill of a saved login"
+ );
+ }
+ );
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 2, "Check 2 logins");
+ isnot(
+ logins[0].password,
+ logins[1].password,
+ "Generated password shouldn't have changed to match the filled password"
+ );
+
+ Services.logins.removeAllUserFacingLogins();
+ LoginTestUtils.resetGeneratedPasswordsCache();
+});
+
+add_task(async function test_edited_generated_password_in_new_tab() {
+ // test that we can fill the generated password into an empty password field,
+ // edit it, and then fill the edited password.
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + FORM_PAGE_PATH,
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+ await SpecialPowers.spawn(
+ browser,
+ [[passwordInputSelector]],
+ function checkInitialFieldValue(inputSelector) {
+ const input = content.document.querySelector(inputSelector);
+ Assert.equal(input.value.length, 0, "Password field is empty");
+ Assert.ok(
+ !input.matches(":autofill"),
+ "Password field should not be highlighted"
+ );
+ }
+ );
+
+ await doFillGeneratedPasswordContextMenuItem(
+ browser,
+ passwordInputSelector
+ );
+ await SpecialPowers.spawn(
+ browser,
+ [[passwordInputSelector]],
+ function checkAndEditFieldValue(inputSelector) {
+ let { LoginTestUtils: LTU } = ChromeUtils.importESModule(
+ "resource://testing-common/LoginTestUtils.sys.mjs"
+ );
+ const input = content.document.querySelector(inputSelector);
+ Assert.equal(
+ input.value.length,
+ LTU.generation.LENGTH,
+ "Password field was filled with generated password"
+ );
+ Assert.ok(
+ input.matches(":autofill"),
+ "Password field should be highlighted"
+ );
+ LTU.loginField.checkPasswordMasked(input, false, "after fill");
+ }
+ );
+
+ await BrowserTestUtils.sendChar("!", browser);
+ await BrowserTestUtils.sendChar("@", browser);
+ let storageChangedPromised = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "modifyLogin"
+ );
+ await BrowserTestUtils.synthesizeKey("KEY_Tab", undefined, browser);
+ info("Waiting for storage update");
+ await storageChangedPromised;
+ }
+ );
+
+ info("Now fill again in a new tab and ensure the edited password is used");
+
+ // Disable autofill in the new tab
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.autofillForms", false]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + FORM_PAGE_PATH,
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+
+ await doFillGeneratedPasswordContextMenuItem(
+ browser,
+ passwordInputSelector
+ );
+
+ await SpecialPowers.spawn(
+ browser,
+ [[passwordInputSelector]],
+ function checkAndEditFieldValue(inputSelector) {
+ let { LoginTestUtils: LTU } = ChromeUtils.importESModule(
+ "resource://testing-common/LoginTestUtils.sys.mjs"
+ );
+ const input = content.document.querySelector(inputSelector);
+ Assert.equal(
+ input.value.length,
+ LTU.generation.LENGTH + 2,
+ "Password field was filled with edited generated password"
+ );
+ LTU.loginField.checkPasswordMasked(input, false, "after fill");
+ }
+ );
+ }
+ );
+
+ LoginTestUtils.clearData();
+ LoginTestUtils.resetGeneratedPasswordsCache();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_context_menu_iframe.js b/toolkit/components/passwordmgr/test/browser/browser_context_menu_iframe.js
new file mode 100644
index 0000000000..2545cdfebe
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu_iframe.js
@@ -0,0 +1,223 @@
+/*
+ * Test the password manager context menu.
+ */
+
+"use strict";
+
+const TEST_ORIGIN = "https://example.com";
+
+// Test with a page that only has a form within an iframe, not in the top-level document
+const IFRAME_PAGE_PATH =
+ "/browser/toolkit/components/passwordmgr/test/browser/form_basic_iframe.html";
+
+/**
+ * Initialize logins needed for the tests and disable autofill
+ * for login forms for easier testing of manual fill.
+ */
+add_task(async function test_initialize() {
+ Services.prefs.setBoolPref("signon.autofillForms", false);
+ Services.prefs.setBoolPref("signon.schemeUpgrades", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("signon.autofillForms");
+ Services.prefs.clearUserPref("signon.schemeUpgrades");
+ });
+ await Services.logins.addLogins(loginList());
+});
+
+/**
+ * Check if the password field is correctly filled when it's in an iframe.
+ */
+add_task(async function test_context_menu_iframe_fill() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + IFRAME_PAGE_PATH,
+ },
+ async function (browser) {
+ await openPasswordContextMenu(
+ browser,
+ "#form-basic-password",
+ () => true,
+ browser.browsingContext.children[0],
+ true
+ );
+
+ let popupMenu = document.getElementById("fill-login-popup");
+
+ // Stores the original value of username
+ function promiseFrameInputValue(name) {
+ return SpecialPowers.spawn(
+ browser.browsingContext.children[0],
+ [name],
+ function (inputname) {
+ return content.document.getElementById(inputname).value;
+ }
+ );
+ }
+ let usernameOriginalValue = await promiseFrameInputValue(
+ "form-basic-username"
+ );
+
+ // Execute the command of the first login menuitem found at the context menu.
+ let firstLoginItem =
+ popupMenu.getElementsByClassName("context-login-item")[0];
+ Assert.ok(firstLoginItem, "Found the first login item");
+
+ await TestUtils.waitForTick();
+
+ Assert.ok(
+ BrowserTestUtils.is_visible(firstLoginItem),
+ "First login menuitem is visible"
+ );
+
+ info("Clicking on the firstLoginItem");
+ // click on the login item to fill the password field, triggering an "input" event
+ popupMenu.activateItem(firstLoginItem);
+
+ let passwordValue = await TestUtils.waitForCondition(async () => {
+ let value = await promiseFrameInputValue("form-basic-password");
+ return value;
+ });
+
+ // Find the used login by it's username.
+ let login = getLoginFromUsername(firstLoginItem.label);
+ Assert.equal(
+ login.password,
+ passwordValue,
+ "Password filled and correct."
+ );
+
+ let usernameNewValue = await promiseFrameInputValue(
+ "form-basic-username"
+ );
+ Assert.equal(
+ usernameOriginalValue,
+ usernameNewValue,
+ "Username value was not changed."
+ );
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ contextMenu.hidePopup();
+
+ await cleanupDoorhanger();
+ await cleanupPasswordNotifications();
+ }
+ );
+});
+
+/**
+ * Check that the login context menu items don't appear on an opaque origin.
+ */
+add_task(async function test_context_menu_iframe_sandbox() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + IFRAME_PAGE_PATH,
+ },
+ async function (browser) {
+ info("Opening context menu for test_context_menu_iframe_sandbox");
+ await openPasswordContextMenu(
+ browser,
+ "#form-basic-password",
+ function checkDisabled() {
+ info("checkDisabled for test_context_menu_iframe_sandbox");
+ let popupHeader = document.getElementById("fill-login");
+ Assert.ok(
+ popupHeader.hidden,
+ "Check that the Fill Login menu item is hidden"
+ );
+ return false;
+ },
+ browser.browsingContext.children[1]
+ );
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ contextMenu.hidePopup();
+ }
+ );
+});
+
+/**
+ * Check that the login context menu item appears for sandbox="allow-same-origin"
+ */
+add_task(async function test_context_menu_iframe_sandbox_same_origin() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN + IFRAME_PAGE_PATH,
+ },
+ async function (browser) {
+ await openPasswordContextMenu(
+ browser,
+ "#form-basic-password",
+ function checkDisabled() {
+ let popupHeader = document.getElementById("fill-login");
+ Assert.ok(
+ !popupHeader.hidden,
+ "Check that the Fill Login menu item is visible"
+ );
+ Assert.ok(
+ !popupHeader.disabled,
+ "Check that the Fill Login menu item is disabled"
+ );
+ return false;
+ },
+ browser.browsingContext.children[2]
+ );
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ contextMenu.hidePopup();
+ }
+ );
+});
+
+/**
+ * Search for a login by it's username.
+ *
+ * Only unique login/origin combinations should be used at this test.
+ */
+function getLoginFromUsername(username) {
+ return loginList().find(login => login.username == username);
+}
+
+/**
+ * List of logins used for the test.
+ *
+ * We should only use unique usernames in this test,
+ * because we need to search logins by username. There is one duplicate u+p combo
+ * in order to test de-duping in the menu.
+ */
+function loginList() {
+ return [
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: "username",
+ password: "password",
+ }),
+ // Same as above but HTTP in order to test de-duping.
+ LoginTestUtils.testData.formLogin({
+ origin: "http://example.com",
+ formActionOrigin: "http://example.com",
+ username: "username",
+ password: "password",
+ }),
+ LoginTestUtils.testData.formLogin({
+ origin: "http://example.com",
+ formActionOrigin: "http://example.com",
+ username: "username1",
+ password: "password1",
+ }),
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: "username2",
+ password: "password2",
+ }),
+ LoginTestUtils.testData.formLogin({
+ origin: "http://example.org",
+ formActionOrigin: "http://example.org",
+ username: "username-cross-origin",
+ password: "password-cross-origin",
+ }),
+ ];
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_crossOriginSubmissionUsesCorrectOrigin.js b/toolkit/components/passwordmgr/test/browser/browser_crossOriginSubmissionUsesCorrectOrigin.js
new file mode 100644
index 0000000000..0aecba63a6
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_crossOriginSubmissionUsesCorrectOrigin.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function getDataFromNextSubmitMessage() {
+ return new Promise(resolve => {
+ LoginManagerParent.setListenerForTests((msg, data) => {
+ if (msg == "ShowDoorhanger") {
+ resolve(data);
+ }
+ });
+ });
+}
+
+add_task(async function testCrossOriginFormUsesCorrectOrigin() {
+ let dataPromise = getDataFromNextSubmitMessage();
+
+ let url =
+ "https://example.com" +
+ DIRECTORY_PATH +
+ "form_cross_origin_secure_action.html";
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function (browser) {
+ await SpecialPowers.spawn(browser.browsingContext, [], () => {
+ let doc = content.document;
+ doc.getElementById("form-basic-username").setUserInput("username");
+ doc.getElementById("form-basic-password").setUserInput("password");
+ doc.getElementById("form-basic").submit();
+ info("Submitting form");
+ });
+ }
+ );
+
+ let data = await dataPromise;
+ info("Origin retrieved from message listener");
+
+ Assert.equal(
+ data.origin,
+ "https://example.com",
+ "Message origin should match form origin"
+ );
+ isnot(
+ data.origin,
+ data.data.actionOrigin,
+ "If origin and actionOrigin match, this test will false positive"
+ );
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_deleteLoginsBackup.js b/toolkit/components/passwordmgr/test/browser/browser_deleteLoginsBackup.js
new file mode 100644
index 0000000000..608329f482
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_deleteLoginsBackup.js
@@ -0,0 +1,282 @@
+/**
+ * Test that logins backup is deleted as expected when logins are deleted.
+ */
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ FXA_PWDMGR_HOST: "resource://gre/modules/FxAccountsCommon.js",
+ FXA_PWDMGR_REALM: "resource://gre/modules/FxAccountsCommon.js",
+});
+
+const nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo,
+ "init"
+);
+
+const login1 = new nsLoginInfo(
+ "https://example.com",
+ "https://example.com",
+ null,
+ "notifyu1",
+ "notifyp1",
+ "user",
+ "pass"
+);
+const login2 = new nsLoginInfo(
+ "https://example.com",
+ "https://example.com",
+ null,
+ "",
+ "notifyp1",
+ "",
+ "pass"
+);
+
+const fxaKey = new nsLoginInfo(
+ FXA_PWDMGR_HOST,
+ null,
+ FXA_PWDMGR_REALM,
+ "foo@bar.com",
+ "pass2",
+ "",
+ ""
+);
+
+const loginStorePath = PathUtils.join(PathUtils.profileDir, "logins.json");
+const loginBackupPath = PathUtils.join(
+ PathUtils.profileDir,
+ "logins-backup.json"
+);
+
+async function waitForBackupUpdate() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(subject, topic) {
+ Services.obs.removeObserver(observer, "logins-backup-updated");
+ resolve();
+ }, "logins-backup-updated");
+ });
+}
+
+async function loginStoreExists() {
+ return TestUtils.waitForCondition(() => IOUtils.exists(loginStorePath));
+}
+
+async function loginBackupExists() {
+ return TestUtils.waitForCondition(() => IOUtils.exists(loginBackupPath));
+}
+
+async function loginBackupDeleted() {
+ return TestUtils.waitForCondition(
+ async () => !(await IOUtils.exists(loginBackupPath))
+ );
+}
+
+// If a fxa key is stored as a login, test that logins backup is updated to only store
+// the fxa key when the last user facing login is deleted.
+add_task(
+ async function test_deleteLoginsBackup_removeAllUserFacingLogins_fxaKey() {
+ info(
+ "Testing removeAllUserFacingLogins() case when there is a saved fxa key"
+ );
+ info("Adding two logins: fxa key and one user facing login");
+ let storageUpdatePromise = TestUtils.topicObserved(
+ "password-storage-updated"
+ );
+ await Services.logins.addLoginAsync(login1);
+ Assert.ok(true, "added login1");
+ await loginStoreExists();
+ await Services.logins.addLoginAsync(fxaKey);
+ Assert.ok(true, "added fxaKey");
+ await loginBackupExists();
+ Assert.ok(true, "logins-backup.json now exists");
+ await storageUpdatePromise;
+ info("Writes to storage are complete for addLogin calls");
+
+ storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
+ info("Removing all user facing logins");
+ Services.logins.removeAllUserFacingLogins();
+ await storageUpdatePromise;
+ info("Writes to storage are complete after removeAllUserFacingLogins call");
+ await waitForBackupUpdate();
+ Assert.ok(
+ true,
+ "logins-backup.json was updated to only store the fxa key, as expected"
+ );
+
+ // Clean up.
+ // Since there is a fxa key left, we need to call removeAllLogins() or removeLogin(fxaKey)
+ // to remove the fxa key. Otherwise the test will fail in verify mode when trying to add login1
+ Services.logins.removeAllLogins();
+ await IOUtils.remove(loginStorePath);
+ }
+);
+
+// Test that logins backup is deleted when Services.logins.removeAllUserFacingLogins() is called.
+add_task(async function test_deleteLoginsBackup_removeAllUserFacingLogins() {
+ // Remove logins.json and logins-backup.json before starting.
+ info("Testing the removeAllUserFacingLogins() case");
+
+ await IOUtils.remove(loginStorePath, { ignoreAbsent: true });
+ await IOUtils.remove(loginBackupPath, { ignoreAbsent: true });
+
+ let storageUpdatePromise = TestUtils.topicObserved(
+ "password-storage-updated"
+ );
+ info("Add a login to create logins.json");
+ await Services.logins.addLoginAsync(login1);
+ await loginStoreExists();
+ Assert.ok(true, "logins.json now exists");
+
+ info("Add a second login to create logins-backup.json");
+ await Services.logins.addLoginAsync(login2);
+ await loginBackupExists();
+ info("logins-backup.json now exists");
+
+ await storageUpdatePromise;
+ info("Writes to storage are complete for addLogin calls");
+
+ storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
+ info("Removing all user facing logins");
+ Services.logins.removeAllUserFacingLogins();
+
+ await storageUpdatePromise;
+ info(
+ "Writes to storage are complete when removeAllUserFacingLogins() is called"
+ );
+ await loginBackupDeleted();
+ info(
+ "logins-backup.json was deleted as expected when all logins were removed"
+ );
+
+ // Clean up.
+ await IOUtils.remove(loginStorePath);
+});
+
+// 1. Test that logins backup is deleted when Services.logins.removeAllLogins() is called
+// 2. If a FxA key is stored as a login, test that logins backup is deleted when
+// Services.logins.removeAllLogins() is called
+add_task(async function test_deleteLoginsBackup_removeAllLogins() {
+ // Remove logins.json and logins-backup.json before starting.
+ info("Testing the removeAllLogins() case");
+
+ await IOUtils.remove(loginStorePath, { ignoreAbsent: true });
+ await IOUtils.remove(loginBackupPath, { ignoreAbsent: true });
+
+ let storageUpdatePromise = TestUtils.topicObserved(
+ "password-storage-updated"
+ );
+ info("Add a login to create logins.json");
+ await Services.logins.addLoginAsync(login1);
+ Assert.ok(true, "added login1");
+ await loginStoreExists();
+ Assert.ok(true, "logins.json now exists");
+ await Services.logins.addLoginAsync(login2);
+ Assert.ok(true, "added login2");
+ await loginBackupExists();
+ info("logins-backup.json now exists");
+
+ await storageUpdatePromise;
+ info("Writes to storage are complete for addLogin calls");
+
+ storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
+ info("Removing all logins");
+ Services.logins.removeAllLogins();
+
+ await storageUpdatePromise;
+ info("Writes to storage are complete when removeAllLogins() is called");
+
+ await loginBackupDeleted();
+ info(
+ "logins-backup.json was deleted as expected when all logins were removed"
+ );
+ await IOUtils.remove(loginStorePath);
+
+ info("Testing the removeAllLogins() case when FxA key is present");
+ storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
+ await Services.logins.addLoginAsync(login1);
+ await loginStoreExists();
+ await Services.logins.addLoginAsync(fxaKey);
+ await loginBackupExists();
+ info("logins-backup.json now exists");
+ await storageUpdatePromise;
+ info("Write to storage are complete for addLogin calls");
+
+ storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
+ info("Removing all logins, including FxA key");
+ Services.logins.removeAllLogins();
+ await storageUpdatePromise;
+ info("Writes to storage are complete after the last removeAllLogins call");
+ await loginBackupDeleted();
+ info(
+ "logins-backup.json was deleted when the last logins were removed, as expected"
+ );
+
+ // Clean up.
+ await IOUtils.remove(loginStorePath);
+});
+
+// 1. Test that logins backup is deleted when the last saved login is removed using
+// Services.logins.removeLogin() when no fxa key is saved.
+// 2. Test that logins backup is updated when the last saved login is removed using
+// Services.logins.removeLogin() when a fxa key is present.
+add_task(async function test_deleteLoginsBackup_removeLogin() {
+ info("Testing the removeLogin() case when there is no saved fxa key");
+ info("Adding two logins");
+ let storageUpdatePromise = TestUtils.topicObserved(
+ "password-storage-updated"
+ );
+ await Services.logins.addLoginAsync(login1);
+ await loginStoreExists();
+ await Services.logins.addLoginAsync(login2);
+ await loginBackupExists();
+ info("logins-backup.json now exists");
+
+ await storageUpdatePromise;
+ info("Writes to storage are complete for addLogin calls");
+
+ storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
+ info("Removing one login");
+ Services.logins.removeLogin(login1);
+ await storageUpdatePromise;
+ info("Writes to storage are complete after one removeLogin call");
+ await loginBackupExists();
+
+ storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
+ info("Removing the last login");
+ Services.logins.removeLogin(login2);
+ await storageUpdatePromise;
+ info("Writes to storage are complete after the last removeLogin call");
+ await loginBackupDeleted();
+ info(
+ "logins-backup.json was deleted as expected when the last saved login was removed"
+ );
+ await IOUtils.remove(loginStorePath);
+
+ info("Testing the removeLogin() case when there is a saved fxa key");
+ info("Adding two logins: one user facing and the fxa key");
+ storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
+ await Services.logins.addLoginAsync(login1);
+ await loginStoreExists();
+ await Services.logins.addLoginAsync(fxaKey);
+ await loginBackupExists();
+ info("logins-backup.json now exists");
+
+ await storageUpdatePromise;
+ info("Writes to storage are complete for addLogin calls");
+
+ storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
+ let backupUpdate = waitForBackupUpdate();
+ Services.logins.removeLogin(login1);
+ await storageUpdatePromise;
+ info("Writes to storage are complete after one removeLogin call");
+ await backupUpdate;
+
+ await loginBackupExists();
+ info("logins-backup.json was updated to contain only the fxa key");
+
+ // Clean up.
+ // Since there is a fxa key left, we need to call removeAllLogins() or removeLogin(fxaKey)
+ // to remove the fxa key. Otherwise the test will fail in verify mode when trying to add login1
+ Services.logins.removeAllLogins();
+ await IOUtils.remove(loginStorePath);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_autocomplete_values.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_autocomplete_values.js
new file mode 100644
index 0000000000..3f8bfddaf4
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_autocomplete_values.js
@@ -0,0 +1,274 @@
+/**
+ * Modify page elements and verify that they are found as options in the save/update doorhanger.
+ */
+
+const USERNAME_SELECTOR = "#form-expanded-username";
+const PASSWORD_SELECTOR = "#form-expanded-password";
+const SEARCH_SELECTOR = "#form-expanded-search";
+const CAPTCHA_SELECTOR = "#form-expanded-captcha";
+const NON_FORM_SELECTOR = "#form-expanded-non-form-input";
+
+const AUTOCOMPLETE_POPUP_SELECTOR = "#PopupAutoComplete";
+const USERNAME_DROPMARKER_SELECTOR =
+ "#password-notification-username-dropmarker";
+
+const TEST_CASES = [
+ {
+ description: "a modified username should be included in the popup",
+ modifiedFields: [
+ { [USERNAME_SELECTOR]: "new_username" },
+ { [PASSWORD_SELECTOR]: "myPassword" },
+ ],
+ expectUsernameDropmarker: true,
+ expectedValues: ["new_username"],
+ },
+ {
+ description:
+ "if no non-password fields are modified, no popup should be available",
+ modifiedFields: [{ [PASSWORD_SELECTOR]: "myPassword" }],
+ expectUsernameDropmarker: false,
+ expectedValues: [],
+ },
+ {
+ description: "all modified username fields should be included in the popup",
+ modifiedFields: [
+ { [USERNAME_SELECTOR]: "new_username" },
+ { [SEARCH_SELECTOR]: "unrelated search query" },
+ { [CAPTCHA_SELECTOR]: "someCaptcha" },
+ { [PASSWORD_SELECTOR]: "myPassword" },
+ ],
+ expectUsernameDropmarker: true,
+ expectedValues: ["new_username", "unrelated search query", "someCaptcha"],
+ },
+ {
+ description:
+ "any modified fields that don't look like usernames or passwords should not be included in the popup",
+ modifiedFields: [
+ { [PASSWORD_SELECTOR]: "myPassword" },
+ { [NON_FORM_SELECTOR]: "I dont even know what this one is" },
+ ],
+ expectUsernameDropmarker: false,
+ expectedValues: [],
+ },
+ {
+ description:
+ "when a field is modified multiple times, all CHANGE event values should be included in the popup",
+ modifiedFields: [
+ { [USERNAME_SELECTOR]: "new_username1" },
+ { [USERNAME_SELECTOR]: "new_username2" },
+ { [USERNAME_SELECTOR]: "new_username3" },
+ { [PASSWORD_SELECTOR]: "myPassword" },
+ ],
+ expectUsernameDropmarker: true,
+ expectedValues: ["new_username1", "new_username2", "new_username3"],
+ },
+ {
+ description: "empty strings should not be displayed in popup",
+ modifiedFields: [
+ { [PASSWORD_SELECTOR]: "myPassword" },
+ { [USERNAME_SELECTOR]: "new_username" },
+ { [USERNAME_SELECTOR]: "" },
+ ],
+ expectUsernameDropmarker: true,
+ expectedValues: ["new_username"],
+ },
+ {
+ description: "saved logins should be displayed in popup",
+ modifiedFields: [
+ { [USERNAME_SELECTOR]: "new_username" },
+ { [PASSWORD_SELECTOR]: "myPassword" },
+ ],
+ savedLogins: [
+ {
+ username: "savedUn1",
+ password: "somePass",
+ },
+ {
+ username: "savedUn2",
+ password: "otherPass",
+ },
+ ],
+ expectUsernameDropmarker: true,
+ expectedValues: ["new_username", "savedUn1", "savedUn2"],
+ },
+ {
+ description: "duplicated page usernames should only be displayed once",
+ modifiedFields: [
+ { [PASSWORD_SELECTOR]: "myPassword" },
+ { [USERNAME_SELECTOR]: "new_username1" },
+ { [USERNAME_SELECTOR]: "new_username2" },
+ { [USERNAME_SELECTOR]: "new_username1" },
+ ],
+ expectUsernameDropmarker: true,
+ expectedValues: ["new_username1", "new_username2"],
+ },
+ {
+ description: "non-un/pw fields also prompt doorhanger updates",
+ modifiedFields: [
+ { [PASSWORD_SELECTOR]: "myPassword" },
+ { [USERNAME_SELECTOR]: "new_username1" },
+ { [SEARCH_SELECTOR]: "search" },
+ { [CAPTCHA_SELECTOR]: "captcha" },
+ ],
+ expectUsernameDropmarker: true,
+ expectedValues: ["new_username1", "search", "captcha"],
+ },
+ // {
+ // description: "duplicated saved/page usernames should TODO https://mozilla.invisionapp.com/share/XGXL6WZVKFJ#/screens/420547613/comments",
+ // },
+];
+
+function _validateTestCase(tc) {
+ if (tc.expectUsernameDropmarker) {
+ Assert.ok(
+ !!tc.expectedValues.length,
+ "Validate test case. A visible dropmarker implies expected values"
+ );
+ } else {
+ Assert.ok(
+ !tc.expectedValues.length,
+ "Validate test case. A hidden dropmarker implies no expected values"
+ );
+ }
+}
+
+async function _setPrefs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.capture.inputChanges.enabled", true]],
+ });
+}
+
+async function _addSavedLogins(logins) {
+ let loginsData = logins.map(({ username }) =>
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username,
+ password: "Saved login passwords not used in this test",
+ })
+ );
+ await Services.logins.addLogins(loginsData);
+}
+
+async function _clickDropmarker(document, notificationElement) {
+ let acPopup = document.querySelector(AUTOCOMPLETE_POPUP_SELECTOR);
+ let acPopupShown = BrowserTestUtils.waitForEvent(acPopup, "popupshown");
+
+ notificationElement.querySelector(USERNAME_DROPMARKER_SELECTOR).click();
+ await acPopupShown;
+}
+
+function _getSuggestedValues(document) {
+ let suggestedValues = [];
+ let autocompletePopup = document.querySelector(AUTOCOMPLETE_POPUP_SELECTOR);
+ let numRows = autocompletePopup.view.matchCount;
+ for (let i = 0; i < numRows; i++) {
+ suggestedValues.push(autocompletePopup.view.getValueAt(i));
+ }
+ return suggestedValues;
+}
+
+add_task(async function test_edit_password() {
+ await _setPrefs();
+ for (let testCase of TEST_CASES) {
+ info("Test case: " + JSON.stringify(testCase));
+ _validateTestCase(testCase);
+
+ // Clean state before the test case is executed.
+ await LoginTestUtils.clearData();
+ await cleanupDoorhanger();
+ await cleanupPasswordNotifications();
+ Services.logins.removeAllUserFacingLogins();
+
+ // Create the pre-existing logins when needed.
+ if (testCase.savedLogins) {
+ info("Adding logins " + JSON.stringify(testCase.savedLogins));
+ await _addSavedLogins(testCase.savedLogins);
+ }
+
+ info("Opening tab");
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_expanded.html",
+ },
+ async function (browser) {
+ info("Editing the form");
+ for (const change of testCase.modifiedFields) {
+ for (const selector in change) {
+ let newValue = change[selector];
+ info(`Setting field '${selector}' to '${newValue}'`);
+ await changeContentFormValues(browser, change);
+ }
+ }
+
+ let notif = getCaptureDoorhanger("any");
+
+ let { panel } = PopupNotifications;
+
+ let promiseShown = BrowserTestUtils.waitForEvent(panel, "popupshown");
+
+ EventUtils.synthesizeMouseAtCenter(notif.anchorElement, {});
+
+ await promiseShown;
+
+ let notificationElement = panel.childNodes[0];
+
+ let usernameDropmarker = notificationElement.querySelector(
+ USERNAME_DROPMARKER_SELECTOR
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(usernameDropmarker) ==
+ testCase.expectUsernameDropmarker,
+ "Confirm dropmarker visibility"
+ );
+
+ if (testCase.expectUsernameDropmarker) {
+ info("Opening autocomplete popup");
+ await _clickDropmarker(document, notificationElement);
+ }
+
+ let suggestedValues = _getSuggestedValues(document);
+
+ let expectedNotFound = testCase.expectedValues.filter(
+ expected => !suggestedValues.includes(expected)
+ );
+ let foundNotExpected = suggestedValues.filter(
+ actual => !testCase.expectedValues.includes(actual)
+ );
+
+ // Log expected/actual inconsistencies
+ Assert.ok(
+ !expectedNotFound.length,
+ `All expected values should be found\nCase: "${
+ testCase.description
+ }"\nExpected: ${JSON.stringify(
+ testCase.expectedValues
+ )}\nActual: ${JSON.stringify(
+ suggestedValues
+ )}\nExpected not found: ${JSON.stringify(expectedNotFound)}
+ `
+ );
+ Assert.ok(
+ !foundNotExpected.length,
+ `All actual values should be expected\nCase: "${
+ testCase.description
+ }"\nExpected: ${JSON.stringify(
+ testCase.expectedValues
+ )}\nActual: ${JSON.stringify(
+ suggestedValues
+ )}\nFound not expected: ${JSON.stringify(foundNotExpected)}
+ `
+ );
+
+ // Clean up state
+ await cleanupDoorhanger();
+ await cleanupPasswordNotifications();
+ await clearMessageCache(browser);
+ Services.logins.removeAllUserFacingLogins();
+ }
+ );
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_autofill_then_save_password.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_autofill_then_save_password.js
new file mode 100644
index 0000000000..1e62dacc13
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_autofill_then_save_password.js
@@ -0,0 +1,181 @@
+/**
+ * Test that after we autofill, the site makes changes to the login, and then the
+ * user modifies their login, a save/update doorhanger is shown.
+ *
+ * This is a regression test for Bug 1632405.
+ */
+
+const testCases = [
+ {
+ name: "autofill, then delete u/p, then fill new u/p should show 'save'",
+ oldUsername: "oldUsername",
+ oldPassword: "oldPassword",
+ actions: [
+ {
+ setUsername: "newUsername",
+ },
+ {
+ setPassword: "newPassword",
+ },
+ ],
+ expectedNotification: "addLogin",
+ expectedDoorhanger: "password-save",
+ },
+ {
+ name: "autofill, then delete password, then fill new password should show 'update'",
+ oldUsername: "oldUsername",
+ oldPassword: "oldPassword",
+ actions: [
+ {
+ setPassword: "newPassword",
+ },
+ ],
+ expectedNotification: "modifyLogin",
+ expectedDoorhanger: "password-change",
+ },
+];
+
+for (let testData of testCases) {
+ let tmp = {
+ async [testData.name]() {
+ info("testing with: " + JSON.stringify(testData));
+ await test_save_change(testData);
+ },
+ };
+ add_task(tmp[testData.name]);
+}
+
+async function test_save_change({
+ name,
+ oldUsername,
+ oldPassword,
+ actions,
+ expectedNotification,
+ expectedDoorhanger,
+}) {
+ let originalPrefValue = await Services.prefs.getBoolPref(
+ "signon.testOnlyUserHasInteractedByPrefValue"
+ );
+ Services.prefs.setBoolPref(
+ "signon.testOnlyUserHasInteractedByPrefValue",
+ false
+ );
+
+ info("Starting test: " + name);
+
+ await LoginTestUtils.addLogin({
+ username: oldUsername,
+ password: oldPassword,
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+
+ await ContentTask.spawn(
+ browser,
+ { oldUsername, oldPassword },
+ async function awaitAutofill({ oldUsername, oldPassword }) {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.querySelector("#form-basic-username").value ==
+ oldUsername &&
+ content.document.querySelector("#form-basic-password").value ==
+ oldPassword,
+ "Await and verify autofill"
+ );
+
+ info(
+ "Triggering a page navigation that is not initiated by the user"
+ );
+ content.history.replaceState({}, "", "");
+ }
+ );
+
+ Services.prefs.setBoolPref(
+ "signon.testOnlyUserHasInteractedByPrefValue",
+ true
+ );
+
+ for (let action of actions) {
+ info(`As the user, update form with action: ${JSON.stringify(action)}`);
+ if (typeof action.setUsername !== "undefined") {
+ await changeContentFormValues(browser, {
+ "#form-basic-username": action.setUsername,
+ });
+ }
+ if (typeof action.setPassword !== "undefined") {
+ await changeContentFormValues(browser, {
+ "#form-basic-password": action.setPassword,
+ });
+ }
+ }
+
+ let expectedUsername =
+ [...actions]
+ .reverse()
+ .map(action => action.setUsername)
+ .find(username => !!username) ?? oldUsername;
+ let expectedPassword =
+ [...actions]
+ .reverse()
+ .map(action => action.setPassword)
+ .find(username => !!username) ?? oldPassword;
+
+ await ContentTask.spawn(
+ browser,
+ { expectedUsername, expectedPassword },
+ async function awaitAutofill({ expectedUsername, expectedPassword }) {
+ info("Validating updated fields");
+ Assert.equal(
+ expectedUsername,
+ content.document.querySelector("#form-basic-username").value,
+ "Verify username field updated"
+ );
+ Assert.equal(
+ expectedPassword,
+ content.document.querySelector("#form-basic-password").value,
+ "Verify password field updated"
+ );
+ }
+ );
+
+ let formSubmittedPromise = listenForTestNotification("ShowDoorhanger");
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = this.content.document;
+ doc.getElementById("form-basic").submit();
+ });
+ await formSubmittedPromise;
+
+ info("Waiting for doorhanger of type: " + expectedDoorhanger);
+ let notif = await waitForDoorhanger(browser, expectedDoorhanger);
+
+ await checkDoorhangerUsernamePassword(expectedUsername, expectedPassword);
+
+ let promiseLogin = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == expectedNotification
+ );
+
+ await clickDoorhangerButton(notif, REMEMBER_BUTTON);
+ await promiseLogin;
+ await cleanupDoorhanger(notif); // clean slate for the next test
+
+ Services.prefs.setBoolPref(
+ "signon.testOnlyUserHasInteractedByPrefValue",
+ originalPrefValue
+ );
+ }
+ );
+
+ // Clean up the database before the next test case is executed.
+ Services.logins.removeAllUserFacingLogins();
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_crossframe.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_crossframe.js
new file mode 100644
index 0000000000..8c4770d510
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_crossframe.js
@@ -0,0 +1,236 @@
+const OUTER_URL =
+ "https://test1.example.com:443" + DIRECTORY_PATH + "form_crossframe.html";
+
+requestLongerTimeout(2);
+
+async function acceptPasswordSave() {
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ let promiseNewSavedPassword = TestUtils.topicObserved(
+ "LoginStats:NewSavedPassword",
+ (subject, data) => subject == gBrowser.selectedBrowser
+ );
+ clickDoorhangerButton(notif, REMEMBER_BUTTON);
+ await promiseNewSavedPassword;
+}
+
+function checkFormFields(browsingContext, prefix, username, password) {
+ return SpecialPowers.spawn(
+ browsingContext,
+ [prefix, username, password],
+ (formPrefix, expectedUsername, expectedPassword) => {
+ let doc = content.document;
+ Assert.equal(
+ doc.getElementById(formPrefix + "-username").value,
+ expectedUsername,
+ "username matches"
+ );
+ Assert.equal(
+ doc.getElementById(formPrefix + "-password").value,
+ expectedPassword,
+ "password matches"
+ );
+ }
+ );
+}
+
+function listenForNotifications(count, expectedFormOrigin) {
+ return new Promise(resolve => {
+ let notifications = [];
+ LoginManagerParent.setListenerForTests((msg, data) => {
+ if (msg == "FormProcessed") {
+ notifications.push("FormProcessed: " + data.browsingContext.id);
+ } else if (msg == "ShowDoorhanger") {
+ Assert.equal(
+ data.origin,
+ expectedFormOrigin,
+ "Message origin should match expected"
+ );
+ notifications.push("FormSubmit: " + data.data.usernameField.name);
+ }
+ if (notifications.length == count) {
+ resolve(notifications);
+ }
+ });
+ });
+}
+
+async function verifyNotifications(notifyPromise, expected) {
+ let actual = await notifyPromise;
+
+ Assert.equal(actual.length, expected.length, "Extra notification(s) sent");
+ let expectedItem;
+ while ((expectedItem = expected.pop())) {
+ let index = actual.indexOf(expectedItem);
+ if (index >= 0) {
+ actual.splice(index, 1);
+ } else {
+ Assert.ok(false, "Expected notification '" + expectedItem + "' not sent");
+ }
+ }
+}
+
+// Make sure there is an autocomplete result for the frame's saved login and select it.
+async function autocompleteLoginInIFrame(
+ browser,
+ iframeBrowsingContext,
+ selector
+) {
+ let popup = document.getElementById("PopupAutoComplete");
+ Assert.ok(popup, "Got popup");
+
+ await openACPopup(popup, browser, selector, iframeBrowsingContext);
+
+ let autocompleteLoginResult = popup.querySelector(
+ `[originaltype="loginWithOrigin"]`
+ );
+ Assert.ok(autocompleteLoginResult, "Got login richlistitem");
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+
+ await EventUtils.synthesizeKey("KEY_ArrowDown");
+ await EventUtils.synthesizeKey("KEY_Enter");
+
+ await promiseHidden;
+}
+
+/*
+ * In this test, a frame is loaded with a document that contains a username
+ * and password field. This frame also contains another child iframe that
+ * itself contains a username and password field. This inner frame is loaded
+ * from a different domain than the first.
+ *
+ * locationMode should be false to submit forms, or true to click a button
+ * which changes the location instead. The latter should still save the
+ * username and password.
+ */
+async function submitSomeCrossSiteFrames(locationMode) {
+ info("Check with location mode " + locationMode);
+ let notifyPromise = listenForNotifications(2);
+
+ let firsttab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ OUTER_URL
+ );
+
+ let outerFrameBC = firsttab.linkedBrowser.browsingContext;
+ let innerFrameBC = outerFrameBC.children[0];
+
+ await verifyNotifications(notifyPromise, [
+ "FormProcessed: " + outerFrameBC.id,
+ "FormProcessed: " + innerFrameBC.id,
+ ]);
+
+ // Fill in the username and password for both the outer and inner frame
+ // and submit the inner frame.
+ notifyPromise = listenForNotifications(1, "https://test2.example.org");
+ info("submit page after changing inner form");
+
+ await SpecialPowers.spawn(outerFrameBC, [], () => {
+ let doc = content.document;
+ doc.getElementById("outer-username").setUserInput("outer");
+ doc.getElementById("outer-password").setUserInput("outerpass");
+ });
+
+ await SpecialPowers.spawn(innerFrameBC, [locationMode], doClick => {
+ let doc = content.document;
+ doc.getElementById("inner-username").setUserInput("inner");
+ doc.getElementById("inner-password").setUserInput("innerpass");
+ if (doClick) {
+ doc.getElementById("inner-gobutton").click();
+ } else {
+ doc.getElementById("inner-form").submit();
+ }
+ });
+
+ await acceptPasswordSave();
+
+ await verifyNotifications(notifyPromise, ["FormSubmit: username"]);
+
+ // Next, open a second tab with the same page in it to verify that the data gets filled properly.
+ notifyPromise = listenForNotifications(2);
+ let secondtab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ OUTER_URL
+ );
+
+ let outerFrameBC2 = secondtab.linkedBrowser.browsingContext;
+ let innerFrameBC2 = outerFrameBC2.children[0];
+ await verifyNotifications(notifyPromise, [
+ "FormProcessed: " + outerFrameBC2.id,
+ "FormProcessed: " + innerFrameBC2.id,
+ ]);
+
+ // We don't expect the innerFrame to be autofilled with the saved login, since
+ // it is cross-origin with the top level frame, so we autocomplete instead.
+ info("Autocompleting saved login into inner form");
+ await autocompleteLoginInIFrame(
+ secondtab.linkedBrowser,
+ innerFrameBC2,
+ "#inner-username"
+ );
+
+ await checkFormFields(outerFrameBC2, "outer", "", "");
+ await checkFormFields(innerFrameBC2, "inner", "inner", "innerpass");
+
+ // Next, change the username and password fields in the outer frame and submit.
+ notifyPromise = listenForNotifications(1, "https://test1.example.com");
+ info("submit page after changing outer form");
+
+ await SpecialPowers.spawn(outerFrameBC2, [locationMode], doClick => {
+ let doc = content.document;
+ doc.getElementById("outer-username").setUserInput("outer2");
+ doc.getElementById("outer-password").setUserInput("outerpass2");
+ if (doClick) {
+ doc.getElementById("outer-gobutton").click();
+ } else {
+ doc.getElementById("outer-form").submit();
+ }
+
+ doc.getElementById("outer-form").submit();
+ });
+
+ await acceptPasswordSave();
+ await verifyNotifications(notifyPromise, ["FormSubmit: outer-username"]);
+
+ // Finally, open a third tab with the same page in it to verify that the data gets filled properly.
+ notifyPromise = listenForNotifications(2);
+ let thirdtab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ OUTER_URL
+ );
+
+ let outerFrameBC3 = thirdtab.linkedBrowser.browsingContext;
+ let innerFrameBC3 = outerFrameBC3.children[0];
+ await verifyNotifications(notifyPromise, [
+ "FormProcessed: " + outerFrameBC3.id,
+ "FormProcessed: " + innerFrameBC3.id,
+ ]);
+
+ // We don't expect the innerFrame to be autofilled with the saved login, since
+ // it is cross-origin with the top level frame, so we autocomplete instead.
+ info("Autocompleting saved login into inner form");
+ await autocompleteLoginInIFrame(
+ thirdtab.linkedBrowser,
+ innerFrameBC3,
+ "#inner-username"
+ );
+
+ await checkFormFields(outerFrameBC3, "outer", "outer2", "outerpass2");
+ await checkFormFields(innerFrameBC3, "inner", "inner", "innerpass");
+
+ LoginManagerParent.setListenerForTests(null);
+
+ await BrowserTestUtils.removeTab(firsttab);
+ await BrowserTestUtils.removeTab(secondtab);
+ await BrowserTestUtils.removeTab(thirdtab);
+
+ LoginTestUtils.clearData();
+}
+
+add_task(async function cross_site_frames_submit() {
+ await submitSomeCrossSiteFrames(false);
+});
+
+add_task(async function cross_site_frames_changelocation() {
+ await submitSomeCrossSiteFrames(true);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_dismissed_for_ccnumber.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_dismissed_for_ccnumber.js
new file mode 100644
index 0000000000..eeaa211da8
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_dismissed_for_ccnumber.js
@@ -0,0 +1,202 @@
+"use strict";
+
+const TEST_ORIGIN = "https://example.com";
+const BASIC_FORM_PAGE_PATH = DIRECTORY_PATH + "form_basic.html";
+
+add_task(async function test_doorhanger_dismissal_un() {
+ let url = TEST_ORIGIN + BASIC_FORM_PAGE_PATH;
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function test_un_value_as_ccnumber(browser) {
+ // If the username field has a credit card number and if
+ // the password field is a three digit numberic value,
+ // we automatically dismiss the save logins prompt on submission.
+
+ let passwordFilledPromise = listenForTestNotification(
+ "PasswordEditedOrGenerated"
+ );
+ await changeContentFormValues(browser, {
+ "#form-basic-password": "123",
+ // We are interested in the state of the doorhanger created and don't want a
+ // false positive from the password-edited handling
+ "#form-basic-username": "4111111111111111",
+ });
+ info("Waiting for passwordFilledPromise");
+ await passwordFilledPromise;
+ // reset doorhanger/notifications, we're only interested in the submit outcome
+ await cleanupDoorhanger();
+ await cleanupPasswordNotifications();
+ // reset message cache so we can disambiguate between dismissed doorhanger from
+ // password edited vs form submitted w. cc number as username
+ await clearMessageCache(browser);
+
+ let processedPromise = listenForTestNotification("ShowDoorhanger");
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.document.getElementById("form-basic-submit").click();
+ });
+ info("Waiting for FormSubmit");
+ await processedPromise;
+
+ let notif = getCaptureDoorhanger("password-save");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(
+ notif.dismissed,
+ "notification popup was automatically dismissed"
+ );
+ await cleanupDoorhanger(notif);
+ }
+ );
+});
+
+add_task(async function test_doorhanger_dismissal_pw() {
+ let url = TEST_ORIGIN + BASIC_FORM_PAGE_PATH;
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function test_pw_value_as_ccnumber(browser) {
+ // If the password field has a credit card number and if
+ // the password field is also tagged autocomplete="cc-number",
+ // we automatically dismiss the save logins prompt on submission.
+
+ let passwordFilledPromise = listenForTestNotification(
+ "PasswordEditedOrGenerated"
+ );
+ await changeContentFormValues(browser, {
+ "#form-basic-password": "4111111111111111",
+ "#form-basic-username": "aaa",
+ });
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.document
+ .getElementById("form-basic-password")
+ .setAttribute("autocomplete", "cc-number");
+ });
+ await passwordFilledPromise;
+ // reset doorhanger/notifications, we're only interested in the submit outcome
+ await cleanupDoorhanger();
+ await cleanupPasswordNotifications();
+ // reset message cache so we can disambiguate between dismissed doorhanger from
+ // password edited vs form submitted w. cc number as password
+ await clearMessageCache(browser);
+
+ let processedPromise = listenForTestNotification("ShowDoorhanger");
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.document.getElementById("form-basic-submit").click();
+ });
+ await processedPromise;
+
+ let notif = getCaptureDoorhanger("password-save");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(
+ notif.dismissed,
+ "notification popup was automatically dismissed"
+ );
+ await cleanupDoorhanger(notif);
+ }
+ );
+});
+
+add_task(async function test_doorhanger_shown_on_un_with_invalid_ccnumber() {
+ let url = TEST_ORIGIN + BASIC_FORM_PAGE_PATH;
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function test_un_with_invalid_cc_number(browser) {
+ // If the username field has a CC number that is invalid,
+ // we show the doorhanger to save logins like we usually do.
+
+ let passwordFilledPromise = listenForTestNotification(
+ "PasswordEditedOrGenerated"
+ );
+ await changeContentFormValues(browser, {
+ "#form-basic-password": "411",
+ "#form-basic-username": "1234123412341234",
+ });
+
+ await passwordFilledPromise;
+ // reset doorhanger/notifications, we're only interested in the submit outcome
+ await cleanupDoorhanger();
+ await cleanupPasswordNotifications();
+ // reset message cache so we can disambiguate between dismissed doorhanger from
+ // password edited vs form submitted w. cc number as password
+ await clearMessageCache(browser);
+
+ let processedPromise = listenForTestNotification("ShowDoorhanger");
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.document.getElementById("form-basic-submit").click();
+ });
+ await processedPromise;
+
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(
+ !notif.dismissed,
+ "notification popup was not automatically dismissed"
+ );
+ await cleanupDoorhanger(notif);
+ }
+ );
+});
+
+add_task(async function test_doorhanger_dismissal_on_change() {
+ let url = TEST_ORIGIN + BASIC_FORM_PAGE_PATH;
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function test_change_in_pw(browser) {
+ let nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo,
+ "init"
+ );
+ let login = new nsLoginInfo(
+ TEST_ORIGIN,
+ TEST_ORIGIN,
+ null,
+ "4111111111111111",
+ "111", // password looks like a card security code
+ "form-basic-username",
+ "form-basic-password"
+ );
+ await Services.logins.addLoginAsync(login);
+
+ let passwordFilledPromise = listenForTestNotification(
+ "PasswordEditedOrGenerated"
+ );
+
+ await changeContentFormValues(browser, {
+ "#form-basic-password": "222", // password looks like a card security code
+ "#form-basic-username": "4111111111111111",
+ });
+ await passwordFilledPromise;
+ // reset doorhanger/notifications, we're only interested in the submit outcome
+ await cleanupDoorhanger();
+ await cleanupPasswordNotifications();
+ // reset message cache so we can disambiguate between dismissed doorhanger from
+ // password edited vs form submitted w. cc number as username
+ await clearMessageCache(browser);
+
+ let processedPromise = listenForTestNotification("ShowDoorhanger");
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.document.getElementById("form-basic-submit").click();
+ });
+ await processedPromise;
+
+ let notif = getCaptureDoorhanger("password-change");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(
+ notif.dismissed,
+ "notification popup was automatically dismissed"
+ );
+ await cleanupDoorhanger(notif);
+ }
+ );
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_empty_password.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_empty_password.js
new file mode 100644
index 0000000000..15c3d52263
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_empty_password.js
@@ -0,0 +1,42 @@
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.rememberSignons.visibilityToggle", true]],
+ });
+});
+
+/**
+ * Test that the doorhanger main action button is disabled
+ * when the password field is empty.
+ *
+ * Also checks that submiting an empty password throws an error.
+ */
+add_task(async function test_empty_password() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "https://example.com/browser/toolkit/components/passwordmgr/test/browser/form_basic.html",
+ },
+ async function (browser) {
+ // Submit the form in the content page with the credentials from the test
+ // case. This will cause the doorhanger notification to be displayed.
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = content.document;
+ doc.getElementById("form-basic-username").setUserInput("username");
+ doc.getElementById("form-basic-password").setUserInput("pw");
+ doc.getElementById("form-basic").submit();
+ });
+
+ await waitForDoorhanger(browser, "password-save");
+ // Synthesize input to empty the field
+ await updateDoorhangerInputValues({
+ password: "",
+ });
+
+ let notificationElement = PopupNotifications.panel.childNodes[0];
+ let mainActionButton = notificationElement.button;
+
+ Assert.ok(mainActionButton.disabled, "Main action button is disabled");
+ await hideDoorhangerPopup();
+ }
+ );
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_form_password_edit.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_form_password_edit.js
new file mode 100644
index 0000000000..75690a25f2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_form_password_edit.js
@@ -0,0 +1,562 @@
+/**
+ * Test changed (not submitted) passwords produce the right doorhangers/notifications
+ */
+
+/* eslint no-shadow:"off" */
+
+"use strict";
+
+// The origin for the test URIs.
+const TEST_ORIGIN = "https://example.com";
+const BASIC_FORM_PAGE_PATH = DIRECTORY_PATH + "form_basic.html";
+const passwordInputSelector = "#form-basic-password";
+const usernameInputSelector = "#form-basic-username";
+
+let testCases = [
+ {
+ name: "Enter password",
+ prefEnabled: true,
+ isLoggedIn: true,
+ logins: [],
+ formDefaults: {},
+ formChanges: {
+ [passwordInputSelector]: "abcXYZ",
+ },
+ expected: {
+ initialForm: {
+ username: "",
+ password: "",
+ },
+ doorhanger: {
+ type: "password-save",
+ dismissed: true,
+ anchorExtraAttr: "",
+ username: "",
+ password: "abcXYZ",
+ toggle: "visible",
+ },
+ },
+ },
+ {
+ name: "Change password",
+ prefEnabled: true,
+ isLoggedIn: true,
+ logins: [],
+ formDefaults: {
+ [passwordInputSelector]: "pass1",
+ },
+ formChanges: {
+ [passwordInputSelector]: "pass-changed",
+ },
+ expected: {
+ initialForm: {
+ username: "",
+ password: "pass1",
+ },
+ doorhanger: {
+ type: "password-save",
+ dismissed: true,
+ anchorExtraAttr: "",
+ username: "",
+ password: "pass-changed",
+ toggle: "visible",
+ },
+ },
+ },
+ {
+ name: "Change autofilled password",
+ prefEnabled: true,
+ isLoggedIn: true,
+ logins: [{ username: "user1", password: "autopass" }],
+ formDefaults: {},
+ formChanges: {
+ [passwordInputSelector]: "autopass-changed",
+ },
+ expected: {
+ initialForm: {
+ username: "user1",
+ password: "autopass",
+ },
+ doorhanger: {
+ type: "password-change",
+ dismissed: true,
+ anchorExtraAttr: "",
+ username: "user1",
+ password: "autopass-changed",
+ },
+ },
+ },
+ {
+ name: "Change autofilled username and password",
+ prefEnabled: true,
+ isLoggedIn: true,
+ logins: [{ username: "user1", password: "pass1" }],
+ formDefaults: {},
+ formChanges: {
+ [usernameInputSelector]: "user2",
+ [passwordInputSelector]: "pass2",
+ },
+ expected: {
+ initialForm: {
+ username: "user1",
+ password: "pass1",
+ },
+ doorhanger: {
+ type: "password-save",
+ dismissed: true,
+ anchorExtraAttr: "",
+ username: "user2",
+ password: "pass2",
+ toggle: "visible",
+ },
+ },
+ },
+ {
+ name: "Change password pref disabled",
+ prefEnabled: false,
+ isLoggedIn: true,
+ logins: [],
+ formDefaults: {
+ [passwordInputSelector]: "pass1",
+ },
+ formChanges: {
+ [passwordInputSelector]: "pass-changed",
+ },
+ expected: {
+ initialForm: {
+ username: "",
+ password: "pass1",
+ },
+ doorhanger: null,
+ },
+ },
+ {
+ name: "Change to new username",
+ prefEnabled: true,
+ isLoggedIn: true,
+ logins: [{ username: "user1", password: "pass1" }],
+ formDefaults: {},
+ formChanges: {
+ [usernameInputSelector]: "user2",
+ },
+ expected: {
+ initialForm: {
+ username: "user1",
+ password: "pass1",
+ },
+ doorhanger: {
+ type: "password-save",
+ dismissed: true,
+ anchorExtraAttr: "",
+ username: "user2",
+ password: "pass1",
+ toggle: "visible",
+ },
+ },
+ },
+ {
+ name: "Change to existing username, different password",
+ prefEnabled: true,
+ isLoggedIn: true,
+ logins: [{ username: "user-saved", password: "pass1" }],
+ formDefaults: {
+ [usernameInputSelector]: "user-prefilled",
+ [passwordInputSelector]: "pass2",
+ },
+ formChanges: {
+ [usernameInputSelector]: "user-saved",
+ },
+ expected: {
+ initialForm: {
+ username: "user-prefilled",
+ password: "pass2",
+ },
+ doorhanger: {
+ type: "password-change",
+ dismissed: true,
+ anchorExtraAttr: "",
+ username: "user-saved",
+ password: "pass2",
+ toggle: "visible",
+ },
+ },
+ },
+ {
+ name: "Add username to existing password",
+ prefEnabled: true,
+ isLoggedIn: true,
+ logins: [{ username: "", password: "pass1" }],
+ formDefaults: {},
+ formChanges: {
+ [usernameInputSelector]: "user1",
+ },
+ expected: {
+ initialForm: {
+ username: "",
+ password: "pass1",
+ },
+ doorhanger: {
+ type: "password-change",
+ dismissed: true,
+ anchorExtraAttr: "",
+ username: "user1",
+ password: "pass1",
+ toggle: "visible",
+ },
+ },
+ },
+ {
+ name: "Change to existing username, password",
+ prefEnabled: true,
+ isLoggedIn: true,
+ logins: [{ username: "user1", password: "pass1" }],
+ formDefaults: {
+ [usernameInputSelector]: "user",
+ [passwordInputSelector]: "pass",
+ },
+ formChanges: {
+ [passwordInputSelector]: "pass1",
+ [usernameInputSelector]: "user1",
+ },
+ expected: {
+ initialForm: {
+ username: "user",
+ password: "pass",
+ },
+ doorhanger: null,
+ },
+ },
+ {
+ name: "Ensure a dismissed password-save doorhanger appears on an input event when editing an unsaved password",
+ prefEnabled: true,
+ isLoggedIn: true,
+ logins: [],
+ formDefaults: {},
+ formChanges: {
+ [passwordInputSelector]: "a",
+ },
+ shouldBlur: false,
+ expected: {
+ initialForm: {
+ username: "",
+ password: "",
+ },
+ doorhanger: {
+ type: "password-save",
+ dismissed: true,
+ anchorExtraAttr: "",
+ username: "",
+ password: "a",
+ toggle: "visible",
+ },
+ },
+ },
+ {
+ name: "Ensure a dismissed password-save doorhanger appears with the latest input value upon editing an unsaved password",
+ prefEnabled: true,
+ isLoggedIn: true,
+ logins: [],
+ formDefaults: {},
+ formChanges: {
+ [passwordInputSelector]: "a",
+ [passwordInputSelector]: "ab",
+ [passwordInputSelector]: "abc",
+ },
+ shouldBlur: false,
+ expected: {
+ initialForm: {
+ username: "",
+ password: "",
+ },
+ doorhanger: {
+ type: "password-save",
+ dismissed: true,
+ anchorExtraAttr: "",
+ username: "",
+ password: "abc",
+ toggle: "visible",
+ },
+ },
+ },
+ {
+ name: "Ensure a dismissed password-change doorhanger appears on an input event when editing a saved password",
+ prefEnabled: true,
+ isLoggedIn: true,
+ logins: [{ username: "", password: "pass1" }],
+ formDefaults: {},
+ formChanges: {
+ [passwordInputSelector]: "pass",
+ },
+ shouldBlur: false,
+ expected: {
+ initialForm: {
+ username: "",
+ password: "pass1",
+ },
+ doorhanger: {
+ type: "password-change",
+ dismissed: true,
+ anchorExtraAttr: "",
+ username: "",
+ password: "pass",
+ toggle: "visible",
+ },
+ },
+ },
+ {
+ name: "Ensure no dismissed doorhanger is shown on 'input' when Primary Password is locked",
+ prefEnabled: true,
+ isLoggedIn: false,
+ logins: [],
+ formDefaults: {},
+ formChanges: {
+ [passwordInputSelector]: "pass",
+ },
+ shouldBlur: false,
+ expected: {
+ initialForm: {
+ username: "",
+ password: "",
+ },
+ doorhanger: null,
+ },
+ },
+ {
+ name: "Ensure no dismissed doorhanger is shown on 'change' when Primary Password is locked",
+ prefEnabled: true,
+ isLoggedIn: false,
+ logins: [],
+ formDefaults: {},
+ formChanges: {
+ [passwordInputSelector]: "pass",
+ },
+ shouldBlur: true,
+ expected: {
+ initialForm: {
+ username: "",
+ password: "",
+ },
+ doorhanger: null,
+ },
+ },
+];
+
+requestLongerTimeout(2);
+SimpleTest.requestCompleteLog();
+
+for (let testData of testCases) {
+ let tmp = {
+ async [testData.name]() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.passwordEditCapture.enabled", testData.prefEnabled]],
+ });
+ if (!testData.isLoggedIn) {
+ // Enable Primary Password
+ LoginTestUtils.primaryPassword.enable();
+ }
+ for (let passwordFieldType of ["password", "text"]) {
+ info(
+ "testing with type=" +
+ passwordFieldType +
+ ": " +
+ JSON.stringify(testData)
+ );
+ await testPasswordChange(testData, { passwordFieldType });
+ }
+ if (!testData.isLoggedIn) {
+ LoginTestUtils.primaryPassword.disable();
+ }
+ await SpecialPowers.popPrefEnv();
+ },
+ };
+ add_task(tmp[testData.name]);
+}
+
+async function testPasswordChange(
+ {
+ logins = [],
+ formDefaults = {},
+ formChanges = {},
+ expected,
+ isLoggedIn,
+ shouldBlur = true,
+ },
+ { passwordFieldType }
+) {
+ await LoginTestUtils.clearData();
+ await cleanupDoorhanger();
+
+ let url = TEST_ORIGIN + BASIC_FORM_PAGE_PATH;
+ for (let login of logins) {
+ await LoginTestUtils.addLogin(login);
+ }
+
+ for (let login of Services.logins.getAllLogins()) {
+ info(`Saved login: ${login.username}, ${login.password}, ${login.origin}`);
+ }
+
+ let formProcessedPromise = listenForTestNotification("FormProcessed");
+ info("Opening tab with url: " + url);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function (browser) {
+ info(`Opened tab with url: ${url}, waiting for focus`);
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+ info("Waiting for form-processed message");
+ await formProcessedPromise;
+ await initForm(browser, formDefaults, { passwordFieldType });
+ await checkForm(browser, expected.initialForm);
+ info("form checked");
+
+ // A message is still sent to the parent process when Primary Password is enabled
+ let notificationMessage =
+ expected.doorhanger || !isLoggedIn
+ ? "PasswordEditedOrGenerated"
+ : "PasswordIgnoreEdit";
+ let passwordTestNotification =
+ listenForTestNotification(notificationMessage);
+
+ await changeContentFormValues(browser, formChanges, shouldBlur);
+
+ info(
+ `form edited, waiting for test notification of ${notificationMessage}`
+ );
+
+ await passwordTestNotification;
+ info("Resolved passwordTestNotification promise");
+
+ if (!expected.doorhanger) {
+ let notif;
+ try {
+ await TestUtils.waitForCondition(
+ () => {
+ return (notif = PopupNotifications.getNotification(
+ "password",
+ browser
+ ));
+ },
+ `Waiting to ensure no notification`,
+ undefined,
+ 25
+ );
+ } catch (ex) {}
+ Assert.ok(!notif, "No doorhanger expected");
+ // the remainder of the test is for doorhanger-expected cases
+ return;
+ }
+
+ let notificationType = expected.doorhanger.type;
+ Assert.ok(
+ /^password-save|password-change$/.test(notificationType),
+ "test provided an expected notification type: " + notificationType
+ );
+ info("waiting for doorhanger");
+ await waitForDoorhanger(browser, notificationType);
+
+ info("verifying doorhanger");
+ let notif = await openAndVerifyDoorhanger(
+ browser,
+ notificationType,
+ expected.doorhanger
+ );
+ Assert.ok(notif, "Doorhanger was shown");
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
+ await promiseHidden;
+
+ info("cleanup doorhanger");
+ await cleanupDoorhanger(notif);
+ }
+ );
+}
+
+async function initForm(browser, formDefaults, passwordFieldType) {
+ await ContentTask.spawn(
+ browser,
+ { passwordInputSelector, passwordFieldType },
+ async function ({ passwordInputSelector, passwordFieldType }) {
+ content.document.querySelector(passwordInputSelector).type =
+ passwordFieldType;
+ }
+ );
+ await ContentTask.spawn(
+ browser,
+ formDefaults,
+ async function (selectorValues) {
+ for (let [sel, value] of Object.entries(selectorValues)) {
+ content.document.querySelector(sel).value = value;
+ }
+ }
+ );
+}
+
+async function checkForm(browser, expected) {
+ await ContentTask.spawn(
+ browser,
+ {
+ [passwordInputSelector]: expected.password,
+ [usernameInputSelector]: expected.username,
+ },
+ async function contentCheckForm(selectorValues) {
+ for (let [sel, value] of Object.entries(selectorValues)) {
+ let field = content.document.querySelector(sel);
+ Assert.equal(
+ field.value,
+ value,
+ sel + " has the expected initial value"
+ );
+ }
+ }
+ );
+}
+
+async function openAndVerifyDoorhanger(browser, type, expected) {
+ // check a dismissed prompt was shown with extraAttr attribute
+ let notif = getCaptureDoorhanger(type);
+ Assert.ok(notif, `${type} doorhanger was created`);
+ Assert.equal(
+ notif.dismissed,
+ expected.dismissed,
+ "Check notification dismissed property"
+ );
+ Assert.equal(
+ notif.anchorElement.getAttribute("extraAttr"),
+ expected.anchorExtraAttr,
+ "Check icon extraAttr attribute"
+ );
+ let { panel } = PopupNotifications;
+ // if the doorhanged is dimissed, we will open it to check panel contents
+ Assert.equal(panel.state, "closed", "Panel is initially closed");
+ let promiseShown = BrowserTestUtils.waitForEvent(panel, "popupshown");
+ // synthesize click on anchor as this also blurs the form field triggering
+ // a change event
+ EventUtils.synthesizeMouseAtCenter(notif.anchorElement, {});
+ await promiseShown;
+ await Promise.resolve();
+ await checkDoorhangerUsernamePassword(expected.username, expected.password);
+
+ let notificationElement = PopupNotifications.panel.childNodes[0];
+ let checkbox = notificationElement.querySelector(
+ "#password-notification-visibilityToggle"
+ );
+
+ if (expected.toggle == "visible") {
+ // Bug 1692284
+ // Assert.ok(BrowserTestUtils.is_visible(checkbox), "Toggle checkbox visible as expected");
+ } else if (expected.toggle == "hidden") {
+ Assert.ok(
+ BrowserTestUtils.is_hidden(checkbox),
+ "Toggle checkbox hidden as expected"
+ );
+ } else {
+ info("Not checking toggle checkbox visibility");
+ }
+ return notif;
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_generated_password.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_generated_password.js
new file mode 100644
index 0000000000..bbcab81854
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_generated_password.js
@@ -0,0 +1,1845 @@
+/**
+ * Test using the generated passwords produces the right doorhangers/notifications
+ */
+
+/* eslint no-shadow:"off" */
+
+"use strict";
+
+// The origin for the test URIs.
+const TEST_ORIGIN = "https://example.com";
+const FORM_PAGE_PATH =
+ "/browser/toolkit/components/passwordmgr/test/browser/form_basic.html";
+const passwordInputSelector = "#form-basic-password";
+const usernameInputSelector = "#form-basic-username";
+
+requestLongerTimeout(2);
+
+async function task_setup() {
+ Services.logins.removeAllUserFacingLogins();
+ LoginTestUtils.resetGeneratedPasswordsCache();
+ await cleanupPasswordNotifications();
+ await LoginTestUtils.remoteSettings.setupImprovedPasswordRules();
+}
+
+async function setup_withOneLogin(username = "username", password = "pass1") {
+ // Reset to a single, known login
+ await task_setup();
+ let login = await LoginTestUtils.addLogin({ username, password });
+ return login;
+}
+
+async function setup_withNoLogins() {
+ // Reset to a single, known login
+ await task_setup();
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 0,
+ "0 logins at the start of the test"
+ );
+}
+
+async function fillGeneratedPasswordFromACPopup(
+ browser,
+ passwordInputSelector
+) {
+ let popup = document.getElementById("PopupAutoComplete");
+ Assert.ok(popup, "Got popup");
+ await openACPopup(popup, browser, passwordInputSelector);
+ await fillGeneratedPasswordFromOpenACPopup(browser, passwordInputSelector);
+}
+
+async function checkPromptContents(
+ anchorElement,
+ browser,
+ expectedPasswordLength = 0
+) {
+ let { panel } = PopupNotifications;
+ Assert.ok(PopupNotifications.isPanelOpen, "Confirm popup is open");
+ let notificationElement = panel.childNodes[0];
+ if (expectedPasswordLength) {
+ info(
+ `Waiting for password value to be ${expectedPasswordLength} chars long`
+ );
+ await BrowserTestUtils.waitForCondition(() => {
+ return (
+ notificationElement.querySelector("#password-notification-password")
+ .value.length == expectedPasswordLength
+ );
+ }, "Wait for nsLoginManagerPrompter writeDataToUI()");
+ }
+
+ return {
+ passwordValue: notificationElement.querySelector(
+ "#password-notification-password"
+ ).value,
+ usernameValue: notificationElement.querySelector(
+ "#password-notification-username"
+ ).value,
+ };
+}
+
+async function verifyGeneratedPasswordWasFilled(
+ browser,
+ passwordInputSelector
+) {
+ await SpecialPowers.spawn(
+ browser,
+ [[passwordInputSelector]],
+ function checkFinalFieldValue(inputSelector) {
+ let { LoginTestUtils: LTU } = ChromeUtils.importESModule(
+ "resource://testing-common/LoginTestUtils.sys.mjs"
+ );
+ let passwordInput = content.document.querySelector(inputSelector);
+ Assert.equal(
+ passwordInput.value.length,
+ LTU.generation.LENGTH,
+ "Password field was filled with generated password"
+ );
+ }
+ );
+}
+
+async function openFormInNewTab(url, formValues, taskFn) {
+ let formFilled = listenForTestNotification("FormProcessed");
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+ await formFilled;
+
+ await SpecialPowers.spawn(
+ browser,
+ [formValues],
+ async function prepareAndCheckForm({
+ password: passwordProps,
+ username: usernameProps,
+ }) {
+ let doc = content.document;
+ // give the form an action so we can know when submit is complete
+ doc.querySelector("form").action = "/";
+
+ let props = passwordProps;
+ if (props) {
+ // We'll reuse the form_basic.html, but ensure we'll get the generated password autocomplete option
+ let field = doc.querySelector(props.selector);
+ if (props.type) {
+ // Change the type from 'password' to something else.
+ field.type = props.type;
+ }
+
+ field.setAttribute("autocomplete", "new-password");
+ if (props.hasOwnProperty("expectedValue")) {
+ Assert.equal(
+ field.value,
+ props.expectedValue,
+ "Check autofilled password value"
+ );
+ }
+ }
+ props = usernameProps;
+ if (props) {
+ let field = doc.querySelector(props.selector);
+ if (props.hasOwnProperty("expectedValue")) {
+ Assert.equal(
+ field.value,
+ props.expectedValue,
+ "Check autofilled username value"
+ );
+ }
+ }
+ }
+ );
+
+ if (formValues.password && formValues.password.setValue !== undefined) {
+ info(
+ "Editing the password, expectedMessage? " +
+ formValues.password.expectedMessage
+ );
+ let messagePromise = formValues.password.expectedMessage
+ ? listenForTestNotification(formValues.password.expectedMessage)
+ : Promise.resolve();
+ await changeContentInputValue(
+ browser,
+ formValues.password.selector,
+ formValues.password.setValue
+ );
+ await messagePromise;
+ info("messagePromise resolved");
+ }
+
+ if (formValues.username && formValues.username.setValue !== undefined) {
+ info(
+ "Editing the username, expectedMessage? " +
+ formValues.username.expectedMessage
+ );
+ let messagePromise = formValues.username.expectedMessage
+ ? listenForTestNotification(formValues.username.expectedMessage)
+ : Promise.resolve();
+ await changeContentInputValue(
+ browser,
+ formValues.username.selector,
+ formValues.username.setValue
+ );
+ await messagePromise;
+ info("messagePromise resolved");
+ }
+
+ await taskFn(browser);
+ await closePopup(
+ browser.ownerDocument.getElementById("confirmation-hint")
+ );
+ }
+ );
+}
+
+async function openAndVerifyDoorhanger(browser, type, expected) {
+ // check a dismissed prompt was shown with extraAttr attribute
+ let notif = getCaptureDoorhanger(type);
+ Assert.ok(notif, `${type} doorhanger was created`);
+ Assert.equal(
+ notif.dismissed,
+ expected.dismissed,
+ "Check notification dismissed property"
+ );
+ Assert.equal(
+ notif.anchorElement.getAttribute("extraAttr"),
+ expected.anchorExtraAttr,
+ "Check icon extraAttr attribute"
+ );
+ let { panel } = PopupNotifications;
+ // if the doorhanged is dimissed, we will open it to check panel contents
+ if (panel.state !== "open") {
+ let promiseShown = BrowserTestUtils.waitForEvent(panel, "popupshown");
+ if (panel.state !== "showing") {
+ // synthesize click on anchor as this also blurs the form field triggering
+ // a change event
+ EventUtils.synthesizeMouseAtCenter(notif.anchorElement, {});
+ }
+ await promiseShown;
+ }
+ let { passwordValue, usernameValue } = await checkPromptContents(
+ notif.anchorElement,
+ browser,
+ expected.passwordLength
+ );
+ Assert.equal(
+ passwordValue.length,
+ expected.passwordLength || LoginTestUtils.generation.LENGTH,
+ "Doorhanger password field has generated 15-char value"
+ );
+ Assert.equal(
+ usernameValue,
+ expected.usernameValue,
+ "Doorhanger username field was popuplated"
+ );
+ return notif;
+}
+
+async function appendContentInputvalue(browser, selector, str) {
+ await ContentTask.spawn(
+ browser,
+ { selector, str },
+ async function ({ selector, str }) {
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ let input = content.document.querySelector(selector);
+ input.focus();
+ input.select();
+ await EventUtils.synthesizeKey("KEY_ArrowRight", {}, content);
+ let changedPromise = ContentTaskUtils.waitForEvent(input, "change");
+ if (str) {
+ await EventUtils.sendString(str, content);
+ }
+ input.blur();
+ await changedPromise;
+ }
+ );
+ info("Input value changed");
+ await TestUtils.waitForTick();
+}
+
+async function submitForm(browser) {
+ // Submit the form
+ info("Now submit the form");
+ let correctPathNamePromise = BrowserTestUtils.browserLoaded(browser);
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.document.querySelector("form").submit();
+ });
+ await correctPathNamePromise;
+ await SpecialPowers.spawn(browser, [], async () => {
+ let win = content;
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ win.location.pathname == "/" && win.document.readyState == "complete"
+ );
+ }, "Wait for form submission load");
+ });
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.generation.available", true],
+ ["signon.generation.enabled", true],
+ ],
+ });
+ // assert that there are no logins
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 0, "There are no logins");
+});
+
+add_task(async function autocomplete_generated_password_auto_saved() {
+ // confirm behavior when filling a generated password via autocomplete
+ // when there are no other logins
+ await setup_withNoLogins();
+ await openFormInNewTab(
+ TEST_ORIGIN + FORM_PAGE_PATH,
+ {
+ password: { selector: passwordInputSelector, expectedValue: "" },
+ username: { selector: usernameInputSelector, expectedValue: "" },
+ },
+ async function taskFn(browser) {
+ let storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "addLogin"
+ );
+ // Let the hint hide itself this first time
+ let forceClosePopup = false;
+ let hintShownAndVerified = verifyConfirmationHint(
+ browser,
+ forceClosePopup
+ );
+
+ await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector);
+ let [{ username, password }] = await storageChangedPromise;
+ await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);
+
+ // Make sure confirmation hint was shown
+ info("waiting for verifyConfirmationHint");
+ await hintShownAndVerified;
+
+ // Check properties of the newly auto-saved login
+ Assert.equal(username, "", "Saved login should have no username");
+ Assert.equal(
+ password.length,
+ LoginTestUtils.generation.LENGTH,
+ "Saved login should have generated password"
+ );
+
+ let notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: true,
+ anchorExtraAttr: "attention",
+ usernameValue: "",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ });
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
+ await promiseHidden;
+
+ // confirm the extraAttr attribute is removed after opening & dismissing the doorhanger
+ Assert.ok(
+ !notif.anchorElement.hasAttribute("extraAttr"),
+ "Check if the extraAttr attribute was removed"
+ );
+ await cleanupDoorhanger(notif);
+
+ storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "modifyLogin"
+ );
+ let [autoSavedLogin] = Services.logins.getAllLogins();
+ info("waiting for submitForm");
+ await submitForm(browser);
+ await storageChangedPromise;
+ verifyLogins([
+ {
+ timesUsed: autoSavedLogin.timesUsed + 1,
+ username: "",
+ },
+ ]);
+ }
+ );
+});
+
+add_task(
+ async function autocomplete_generated_password_with_confirm_field_auto_saved() {
+ // confirm behavior when filling a generated password via autocomplete
+ // when there are no other logins and the form has a confirm password field
+ const FORM_WITH_CONFIRM_FIELD_PAGE_PATH =
+ "/browser/toolkit/components/passwordmgr/test/browser/form_basic_with_confirm_field.html";
+ const confirmPasswordInputSelector = "#form-basic-confirm-password";
+ await setup_withNoLogins();
+ await openFormInNewTab(
+ TEST_ORIGIN + FORM_WITH_CONFIRM_FIELD_PAGE_PATH,
+ {
+ password: { selector: passwordInputSelector, expectedValue: "" },
+ username: { selector: usernameInputSelector, expectedValue: "" },
+ },
+ async function taskFn(browser) {
+ let storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "addLogin"
+ );
+ // Let the hint hide itself this first time
+ let forceClosePopup = false;
+ let hintShownAndVerified = verifyConfirmationHint(
+ browser,
+ forceClosePopup
+ );
+
+ await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector);
+ let [{ username, password }] = await storageChangedPromise;
+ await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);
+ await verifyGeneratedPasswordWasFilled(
+ browser,
+ confirmPasswordInputSelector
+ );
+
+ // Make sure confirmation hint was shown
+ info("waiting for verifyConfirmationHint");
+ await hintShownAndVerified;
+
+ // Check properties of the newly auto-saved login
+ Assert.equal(username, "", "Saved login should have no username");
+ Assert.equal(
+ password.length,
+ LoginTestUtils.generation.LENGTH,
+ "Saved login should have generated password"
+ );
+
+ let notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: true,
+ anchorExtraAttr: "attention",
+ usernameValue: "",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ });
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
+ await promiseHidden;
+
+ // confirm the extraAttr attribute is removed after opening & dismissing the doorhanger
+ Assert.ok(
+ !notif.anchorElement.hasAttribute("extraAttr"),
+ "Check if the extraAttr attribute was removed"
+ );
+ await cleanupDoorhanger(notif);
+
+ storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "modifyLogin"
+ );
+ let [autoSavedLogin] = Services.logins.getAllLogins();
+ info("waiting for submitForm");
+ await submitForm(browser);
+ await storageChangedPromise;
+ verifyLogins([
+ {
+ timesUsed: autoSavedLogin.timesUsed + 1,
+ username: "",
+ },
+ ]);
+ }
+ );
+ }
+);
+
+add_task(async function autocomplete_generated_password_saved_empty_username() {
+ // confirm behavior when filling a generated password via autocomplete
+ // when there is an existing saved login with a "" username
+ await setup_withOneLogin("", "xyzpassword");
+ await openFormInNewTab(
+ TEST_ORIGIN + FORM_PAGE_PATH,
+ {
+ password: {
+ selector: passwordInputSelector,
+ expectedValue: "xyzpassword",
+ setValue: "",
+ expectedMessage: "PasswordEditedOrGenerated",
+ },
+ username: { selector: usernameInputSelector, expectedValue: "" },
+ },
+ async function taskFn(browser) {
+ let [savedLogin] = Services.logins.getAllLogins();
+ let storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "modifyLogin"
+ );
+ await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector);
+ await waitForDoorhanger(browser, "password-change");
+ info("Waiting to openAndVerifyDoorhanger");
+ await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: true,
+ anchorExtraAttr: "",
+ usernameValue: "",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ });
+ await hideDoorhangerPopup();
+ info("Waiting to verifyGeneratedPasswordWasFilled");
+ await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);
+
+ info("waiting for submitForm");
+ await submitForm(browser);
+ let notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: false,
+ anchorExtraAttr: "",
+ usernameValue: "",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ });
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+ await promiseHidden;
+
+ info("Waiting for modifyLogin");
+ await storageChangedPromise;
+ verifyLogins([
+ {
+ timesUsed: savedLogin.timesUsed + 1,
+ username: "",
+ },
+ ]);
+ await cleanupDoorhanger(notif); // cleanup the doorhanger for next test
+ }
+ );
+});
+
+add_task(async function autocomplete_generated_password_saved_username() {
+ // confirm behavior when filling a generated password via autocomplete
+ // into a form with username matching an existing saved login
+ await setup_withOneLogin("user1", "xyzpassword");
+ await openFormInNewTab(
+ TEST_ORIGIN + FORM_PAGE_PATH,
+ {
+ password: {
+ selector: passwordInputSelector,
+ expectedValue: "xyzpassword",
+ setValue: "",
+ expectedMessage: "PasswordEditedOrGenerated",
+ },
+ username: {
+ selector: usernameInputSelector,
+ expectedValue: "user1",
+ },
+ },
+ async function taskFn(browser) {
+ let storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "addLogin"
+ );
+ // We don't need to wait to confirm the hint hides itelf every time
+ let forceClosePopup = true;
+ let hintShownAndVerified = verifyConfirmationHint(
+ browser,
+ forceClosePopup
+ );
+
+ await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector);
+
+ // Make sure confirmation hint was shown
+ info("waiting for verifyConfirmationHint");
+ await hintShownAndVerified;
+
+ info("waiting for addLogin");
+ await storageChangedPromise;
+ await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);
+
+ // Check properties of the newly auto-saved login
+ let [user1LoginSnapshot, autoSavedLogin] = verifyLogins([
+ {
+ username: "user1",
+ password: "xyzpassword", // user1 is unchanged
+ },
+ {
+ timesUsed: 1,
+ username: "",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ },
+ ]);
+
+ let notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: true,
+ anchorExtraAttr: "attention",
+ usernameValue: "user1",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ });
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
+ await promiseHidden;
+
+ // confirm the extraAttr attribute is removed after opening & dismissing the doorhanger
+ Assert.ok(
+ !notif.anchorElement.hasAttribute("extraAttr"),
+ "Check if the extraAttr attribute was removed"
+ );
+ await cleanupDoorhanger(notif);
+
+ storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "modifyLogin"
+ );
+ info("waiting for submitForm");
+ await submitForm(browser);
+ promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+ await promiseHidden;
+ await storageChangedPromise;
+ verifyLogins([
+ {
+ timesUsed: user1LoginSnapshot.timesUsed + 1,
+ username: "user1",
+ password: autoSavedLogin.password,
+ },
+ ]);
+ }
+ );
+});
+
+add_task(async function ac_gen_pw_saved_empty_un_stored_non_empty_un_in_form() {
+ // confirm behavior when when the form's username field has a non-empty value
+ // and there is an existing saved login with a "" username
+ await setup_withOneLogin("", "xyzpassword");
+ await openFormInNewTab(
+ TEST_ORIGIN + FORM_PAGE_PATH,
+ {
+ password: {
+ selector: passwordInputSelector,
+ expectedValue: "xyzpassword",
+ setValue: "",
+ expectedMessage: "PasswordEditedOrGenerated",
+ },
+ username: {
+ selector: usernameInputSelector,
+ expectedValue: "",
+ setValue: "myusername",
+ // with an empty password value, no message is sent for a username change
+ expectedMessage: "",
+ },
+ },
+ async function taskFn(browser) {
+ let [savedLogin] = Services.logins.getAllLogins();
+ let storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "addLogin"
+ );
+ await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector);
+ await waitForDoorhanger(browser, "password-save");
+ info("Waiting to openAndVerifyDoorhanger");
+ await openAndVerifyDoorhanger(browser, "password-save", {
+ dismissed: true,
+ anchorExtraAttr: "",
+ usernameValue: "myusername",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ });
+ await hideDoorhangerPopup();
+ info("Waiting to verifyGeneratedPasswordWasFilled");
+ await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);
+
+ info("waiting for submitForm");
+ await submitForm(browser);
+ let notif = await openAndVerifyDoorhanger(browser, "password-save", {
+ dismissed: false,
+ anchorExtraAttr: "",
+ usernameValue: "myusername",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ });
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ clickDoorhangerButton(notif, REMEMBER_BUTTON);
+ await promiseHidden;
+
+ info("Waiting for addLogin");
+ await storageChangedPromise;
+ verifyLogins([
+ {
+ timesUsed: savedLogin.timesUsed,
+ username: "",
+ password: "xyzpassword",
+ },
+ {
+ timesUsed: 1,
+ username: "myusername",
+ },
+ ]);
+ await cleanupDoorhanger(notif); // cleanup the doorhanger for next test
+ }
+ );
+});
+
+add_task(async function contextfill_generated_password_saved_empty_username() {
+ // confirm behavior when filling a generated password via context menu
+ // when there is an existing saved login with a "" username
+ await setup_withOneLogin("", "xyzpassword");
+ await openFormInNewTab(
+ TEST_ORIGIN + FORM_PAGE_PATH,
+ {
+ password: {
+ selector: passwordInputSelector,
+ expectedValue: "xyzpassword",
+ setValue: "",
+ expectedMessage: "PasswordEditedOrGenerated",
+ },
+ username: { selector: usernameInputSelector, expectedValue: "" },
+ },
+ async function taskFn(browser) {
+ let [savedLogin] = Services.logins.getAllLogins();
+ let storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "modifyLogin"
+ );
+ await doFillGeneratedPasswordContextMenuItem(
+ browser,
+ passwordInputSelector
+ );
+ await waitForDoorhanger(browser, "password-change");
+ info("Waiting to openAndVerifyDoorhanger");
+ await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: true,
+ anchorExtraAttr: "",
+ usernameValue: "",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ });
+ await hideDoorhangerPopup();
+ info("Waiting to verifyGeneratedPasswordWasFilled");
+ await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);
+
+ info("waiting for submitForm");
+ await submitForm(browser);
+ let notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: false,
+ anchorExtraAttr: "",
+ usernameValue: "",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ });
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+ await promiseHidden;
+
+ info("Waiting for modifyLogin");
+ await storageChangedPromise;
+ verifyLogins([
+ {
+ timesUsed: savedLogin.timesUsed + 1,
+ username: "",
+ },
+ ]);
+ await cleanupDoorhanger(notif); // cleanup the doorhanger for next test
+ }
+ );
+});
+
+async function autocomplete_generated_password_edited_no_auto_save(
+ passwordType = "password"
+) {
+ // confirm behavior when filling a generated password via autocomplete
+ // when there is an existing saved login with a "" username and then editing
+ // the password and autocompleting again.
+ await setup_withOneLogin("", "xyzpassword");
+ await openFormInNewTab(
+ TEST_ORIGIN + FORM_PAGE_PATH,
+ {
+ password: {
+ selector: passwordInputSelector,
+ expectedValue: "xyzpassword",
+ setValue: "",
+ type: passwordType,
+ expectedMessage: "PasswordEditedOrGenerated",
+ },
+ username: { selector: usernameInputSelector, expectedValue: "" },
+ },
+ async function taskFn(browser) {
+ let [savedLogin] = Services.logins.getAllLogins();
+ let storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "modifyLogin"
+ );
+ await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector);
+ info(
+ "Filled generated password, waiting for dismissed password-change doorhanger"
+ );
+ await waitForDoorhanger(browser, "password-change");
+ info("Waiting to openAndVerifyDoorhanger");
+ let notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: true,
+ anchorExtraAttr: "",
+ usernameValue: "",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ });
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
+ await promiseHidden;
+
+ info("Waiting to verifyGeneratedPasswordWasFilled");
+ await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);
+
+ await BrowserTestUtils.sendChar("!", browser);
+ await BrowserTestUtils.sendChar("@", browser);
+ await BrowserTestUtils.synthesizeKey("KEY_Tab", undefined, browser);
+
+ await waitForDoorhanger(browser, "password-change");
+ info("Waiting to openAndVerifyDoorhanger");
+ notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: true,
+ anchorExtraAttr: "",
+ usernameValue: "",
+ passwordLength: LoginTestUtils.generation.LENGTH + 2,
+ });
+
+ promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
+ await promiseHidden;
+
+ verifyLogins([
+ {
+ timesUsed: savedLogin.timesUsed,
+ username: "",
+ password: "xyzpassword",
+ },
+ ]);
+
+ info("waiting for submitForm");
+ await submitForm(browser);
+ notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: false,
+ anchorExtraAttr: "",
+ usernameValue: "",
+ passwordLength: LoginTestUtils.generation.LENGTH + 2,
+ });
+
+ promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+ await promiseHidden;
+
+ info("Waiting for modifyLogin");
+ await storageChangedPromise;
+ verifyLogins([
+ {
+ timesUsed: savedLogin.timesUsed + 1,
+ username: "",
+ },
+ ]);
+ await cleanupDoorhanger(notif); // cleanup the doorhanger for next test
+ }
+ );
+
+ LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().clear();
+}
+
+add_task(autocomplete_generated_password_edited_no_auto_save);
+
+add_task(
+ async function autocomplete_generated_password_edited_no_auto_save_type_text() {
+ await autocomplete_generated_password_edited_no_auto_save("text");
+ }
+);
+
+add_task(async function contextmenu_fill_generated_password_and_set_username() {
+ // test when filling with a generated password and editing the username in the form
+ // * the prompt should display the form's username
+ // * the auto-saved login should have "" for username
+ // * confirming the prompt should edit the "" login and add the username
+ await setup_withOneLogin("olduser", "xyzpassword");
+ await openFormInNewTab(
+ TEST_ORIGIN + FORM_PAGE_PATH,
+ {
+ password: {
+ selector: passwordInputSelector,
+ expectedValue: "xyzpassword",
+ setValue: "",
+ expectedMessage: "PasswordEditedOrGenerated",
+ },
+ username: {
+ selector: usernameInputSelector,
+ expectedValue: "olduser",
+ setValue: "differentuser",
+ // with an empty password value, no message is sent for a username change
+ expectedMessage: "",
+ },
+ },
+ async function taskFn(browser) {
+ let storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "addLogin"
+ );
+ await SpecialPowers.spawn(
+ browser,
+ [[passwordInputSelector, usernameInputSelector]],
+ function checkEmptyPasswordField([passwordSelector, usernameSelector]) {
+ Assert.equal(
+ content.document.querySelector(passwordSelector).value,
+ "",
+ "Password field is empty"
+ );
+ }
+ );
+
+ // Let the hint hide itself this first time
+ let forceClosePopup = false;
+ let hintShownAndVerified = verifyConfirmationHint(
+ browser,
+ forceClosePopup
+ );
+
+ info("waiting to fill generated password using context menu");
+ await doFillGeneratedPasswordContextMenuItem(
+ browser,
+ passwordInputSelector
+ );
+
+ info("waiting for verifyConfirmationHint");
+ await hintShownAndVerified;
+ info("waiting for dismissed password-change notification");
+ await waitForDoorhanger(browser, "password-change");
+
+ info("waiting for addLogin");
+ await storageChangedPromise;
+
+ // Check properties of the newly auto-saved login
+ verifyLogins([
+ null, // ignore the first one
+ {
+ timesUsed: 1,
+ username: "",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ },
+ ]);
+
+ info("Waiting to openAndVerifyDoorhanger");
+ await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: true,
+ anchorExtraAttr: "attention",
+ usernameValue: "differentuser",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ });
+ await hideDoorhangerPopup();
+ info("Waiting to verifyGeneratedPasswordWasFilled");
+ await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);
+
+ info("waiting for submitForm");
+ await submitForm(browser);
+ let notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: false,
+ anchorExtraAttr: "",
+ usernameValue: "differentuser",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ });
+
+ storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "modifyLogin"
+ );
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+ await promiseHidden;
+
+ info("Waiting for modifyLogin");
+ await storageChangedPromise;
+ verifyLogins([
+ null,
+ {
+ username: "differentuser",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ timesUsed: 2,
+ },
+ ]);
+ await cleanupDoorhanger(notif); // cleanup the doorhanger for next test
+ }
+ );
+});
+
+add_task(async function contextmenu_password_change_form_without_username() {
+ // test doorhanger behavior when a generated password is filled into a change-password
+ // form with no username
+ await setup_withOneLogin("user1", "xyzpassword");
+ await LoginTestUtils.addLogin({ username: "username2", password: "pass2" });
+ const passwordInputSelector = "#newpass";
+
+ const CHANGE_FORM_PATH =
+ "/browser/toolkit/components/passwordmgr/test/browser/form_password_change.html";
+ await openFormInNewTab(
+ TEST_ORIGIN + CHANGE_FORM_PATH,
+ {
+ password: {
+ selector: passwordInputSelector,
+ expectedValue: "",
+ },
+ },
+ async function taskFn(browser) {
+ let storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "addLogin"
+ );
+
+ // We don't need to wait to confirm the hint hides itelf every time
+ let forceClosePopup = true;
+ let hintShownAndVerified = verifyConfirmationHint(
+ browser,
+ forceClosePopup
+ );
+
+ // Make the 2nd field use a generated password
+ info("Using contextmenu to fill with a generated password");
+ await doFillGeneratedPasswordContextMenuItem(
+ browser,
+ passwordInputSelector
+ );
+
+ info("waiting for dismissed password-change notification");
+ await waitForDoorhanger(browser, "password-change");
+
+ // Make sure confirmation hint was shown
+ info("waiting for verifyConfirmationHint");
+ await hintShownAndVerified;
+
+ info("waiting for addLogin");
+ await storageChangedPromise;
+ // Check properties of the newly auto-saved login
+ verifyLogins([
+ null, // ignore the first one
+ null, // ignore the 2nd one
+ {
+ timesUsed: 1,
+ username: "",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ },
+ ]);
+
+ info("Waiting to openAndVerifyDoorhanger");
+ let notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: true,
+ anchorExtraAttr: "attention",
+ usernameValue: "",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ });
+ // remove notification so we can unambiguously check no new notification gets created later
+ await cleanupDoorhanger(notif);
+
+ info("Waiting to verifyGeneratedPasswordWasFilled");
+ await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);
+
+ storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "modifyLogin"
+ );
+ let { timeLastUsed } = Services.logins.getAllLogins()[2];
+
+ info("waiting for submitForm");
+ await submitForm(browser);
+
+ info("Waiting for modifyLogin");
+ await storageChangedPromise;
+ verifyLogins([
+ null, // ignore the first one
+ null, // ignore the 2nd one
+ {
+ timesUsed: 2,
+ usedSince: timeLastUsed,
+ },
+ ]);
+ // Check no new doorhanger was shown
+ notif = getCaptureDoorhanger("password-change");
+ Assert.ok(!notif, "No new doorhanger should be shown");
+ await cleanupDoorhanger(); // cleanup for next test
+ }
+ );
+});
+
+add_task(
+ async function autosaved_login_updated_to_existing_login_via_doorhanger() {
+ // test when filling with a generated password and editing the username in the
+ // doorhanger to match an existing login:
+ // * the matching login should be updated
+ // * the auto-saved login should be deleted
+ // * the metadata for the matching login should be updated
+ // * the by-origin cache for the password should point at the updated login
+ await setup_withOneLogin("user1", "xyzpassword");
+ await LoginTestUtils.addLogin({
+ username: "user2",
+ password: "abcpassword",
+ });
+ await openFormInNewTab(
+ TEST_ORIGIN + FORM_PAGE_PATH,
+ {
+ password: {
+ selector: passwordInputSelector,
+ expectedValue: "",
+ },
+ username: {
+ selector: usernameInputSelector,
+ expectedValue: "",
+ },
+ },
+ async function taskFn(browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+
+ let storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "addLogin"
+ );
+ // We don't need to wait to confirm the hint hides itelf every time
+ let forceClosePopup = true;
+ let hintShownAndVerified = verifyConfirmationHint(
+ browser,
+ forceClosePopup
+ );
+
+ info("waiting to fill generated password using context menu");
+ await doFillGeneratedPasswordContextMenuItem(
+ browser,
+ passwordInputSelector
+ );
+
+ info("waiting for dismissed password-change notification");
+ await waitForDoorhanger(browser, "password-change");
+
+ // Make sure confirmation hint was shown
+ info("waiting for verifyConfirmationHint");
+ await hintShownAndVerified;
+
+ info("waiting for addLogin");
+ await storageChangedPromise;
+ info("addLogin promise resolved");
+ // Check properties of the newly auto-saved login
+ let [user1LoginSnapshot, unused, autoSavedLogin] = verifyLogins([
+ null, // ignore the first one
+ null, // ignore the 2nd one
+ {
+ timesUsed: 1,
+ username: "",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ },
+ ]);
+ info("user1LoginSnapshot, guid: " + user1LoginSnapshot.guid);
+ info("unused, guid: " + unused.guid);
+ info("autoSavedLogin, guid: " + autoSavedLogin.guid);
+
+ info("verifyLogins ok");
+ let passwordCacheEntry =
+ LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get(
+ "https://example.com"
+ );
+
+ Assert.ok(
+ passwordCacheEntry,
+ "Got the cached generated password entry for https://example.com"
+ );
+ Assert.equal(
+ passwordCacheEntry.value,
+ autoSavedLogin.password,
+ "Cached password matches the auto-saved login password"
+ );
+ Assert.equal(
+ passwordCacheEntry.storageGUID,
+ autoSavedLogin.guid,
+ "Cached password guid matches the auto-saved login guid"
+ );
+
+ info("Waiting to openAndVerifyDoorhanger");
+ let notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: true,
+ anchorExtraAttr: "attention",
+ usernameValue: "",
+ password: autoSavedLogin.password,
+ });
+ Assert.ok(notif, "Got password-change notification");
+
+ info("Calling updateDoorhangerInputValues");
+ await updateDoorhangerInputValues({
+ username: "user1",
+ });
+ info("doorhanger inputs updated");
+
+ let loginModifiedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (subject, data) => {
+ if (data == "modifyLogin") {
+ info("passwordmgr-storage-changed, action: " + data);
+ info("subject: " + JSON.stringify(subject));
+ return true;
+ }
+ return false;
+ }
+ );
+ let loginRemovedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (subject, data) => {
+ if (data == "removeLogin") {
+ info("passwordmgr-storage-changed, action: " + data);
+ info("subject: " + JSON.stringify(subject));
+ return true;
+ }
+ return false;
+ }
+ );
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ info("clicking change button");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+ await promiseHidden;
+
+ info("Waiting for modifyLogin promise");
+ await loginModifiedPromise;
+
+ info("Waiting for removeLogin promise");
+ await loginRemovedPromise;
+
+ info("storage-change promises resolved");
+ // Check the auto-saved login was removed and the original login updated
+ verifyLogins([
+ {
+ username: "user1",
+ password: autoSavedLogin.password,
+ timeCreated: user1LoginSnapshot.timeCreated,
+ timeLastUsed: user1LoginSnapshot.timeLastUsed,
+ passwordChangedSince: autoSavedLogin.timePasswordChanged,
+ },
+ null, // ignore user2
+ ]);
+
+ // Check we have no notifications at this point
+ Assert.ok(!PopupNotifications.isPanelOpen, "No doorhanger is open");
+ Assert.ok(
+ !PopupNotifications.getNotification("password", browser),
+ "No notifications"
+ );
+
+ // make sure the cache entry is unchanged with the removal of the auto-saved login
+ Assert.equal(
+ autoSavedLogin.password,
+ LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get(
+ "https://example.com"
+ ).value,
+ "Generated password cache entry has the expected password value"
+ );
+ }
+ );
+ }
+);
+
+add_task(async function autosaved_login_updated_to_existing_login_onsubmit() {
+ // test when selecting auto-saved generated password in a form filled with an
+ // existing login and submitting the form:
+ // * the matching login should be updated
+ // * the auto-saved login should be deleted
+ // * the metadata for the matching login should be updated
+ // * the by-origin cache for the password should point at the updated login
+
+ // clear both fields which should be autofilled with our single login
+ await setup_withOneLogin("user1", "xyzpassword");
+ await openFormInNewTab(
+ TEST_ORIGIN + FORM_PAGE_PATH,
+ {
+ password: {
+ selector: passwordInputSelector,
+ expectedValue: "xyzpassword",
+ setValue: "",
+ expectedMessage: "PasswordEditedOrGenerated",
+ },
+ username: {
+ selector: usernameInputSelector,
+ expectedValue: "user1",
+ setValue: "",
+ // with an empty password value, no message is sent for a username change
+ expectedMessage: "",
+ },
+ },
+ async function taskFn(browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+
+ // first, create an auto-saved login with generated password
+ let storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "addLogin"
+ );
+ // We don't need to wait to confirm the hint hides itelf every time
+ let forceClosePopup = true;
+ let hintShownAndVerified = verifyConfirmationHint(
+ browser,
+ forceClosePopup
+ );
+
+ info("waiting to fill generated password using context menu");
+ await doFillGeneratedPasswordContextMenuItem(
+ browser,
+ passwordInputSelector
+ );
+
+ info("waiting for dismissed password-change notification");
+ await waitForDoorhanger(browser, "password-change");
+
+ // Make sure confirmation hint was shown
+ info("waiting for verifyConfirmationHint");
+ await hintShownAndVerified;
+
+ info("waiting for addLogin");
+ await storageChangedPromise;
+ info("addLogin promise resolved");
+ // Check properties of the newly auto-saved login
+ let [user1LoginSnapshot, autoSavedLogin] = verifyLogins([
+ null, // ignore the first one
+ {
+ timesUsed: 1,
+ username: "",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ },
+ ]);
+ info("user1LoginSnapshot, guid: " + user1LoginSnapshot.guid);
+ info("autoSavedLogin, guid: " + autoSavedLogin.guid);
+
+ info("verifyLogins ok");
+ let passwordCacheEntry =
+ LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get(
+ "https://example.com"
+ );
+
+ Assert.ok(
+ passwordCacheEntry,
+ "Got the cached generated password entry for https://example.com"
+ );
+ Assert.equal(
+ passwordCacheEntry.value,
+ autoSavedLogin.password,
+ "Cached password matches the auto-saved login password"
+ );
+ Assert.equal(
+ passwordCacheEntry.storageGUID,
+ autoSavedLogin.guid,
+ "Cached password guid matches the auto-saved login guid"
+ );
+
+ let notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: true,
+ anchorExtraAttr: "attention",
+ usernameValue: "",
+ password: autoSavedLogin.password,
+ });
+ await cleanupDoorhanger(notif);
+
+ // now update and submit the form with the user1 username and the generated password
+ info(`submitting form`);
+ let submitResults = await submitFormAndGetResults(
+ browser,
+ "formsubmit.sjs",
+ {
+ "#form-basic-username": "user1",
+ }
+ );
+ Assert.equal(
+ submitResults.username,
+ "user1",
+ "Form submitted with expected username"
+ );
+ Assert.equal(
+ submitResults.password,
+ autoSavedLogin.password,
+ "Form submitted with expected password"
+ );
+ info(
+ `form was submitted, got username/password ${submitResults.username}/${submitResults.password}`
+ );
+
+ await waitForDoorhanger(browser, "password-change");
+ notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: false,
+ anchorExtraAttr: "",
+ usernameValue: "user1",
+ password: autoSavedLogin.password,
+ });
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ let loginModifiedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => {
+ if (data == "modifyLogin") {
+ info("passwordmgr-storage-changed, action: " + data);
+ info("subject: " + JSON.stringify(_));
+ return true;
+ }
+ return false;
+ }
+ );
+ let loginRemovedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => {
+ if (data == "removeLogin") {
+ info("passwordmgr-storage-changed, action: " + data);
+ info("subject: " + JSON.stringify(_));
+ return true;
+ }
+ return false;
+ }
+ );
+
+ info("clicking change button");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+ await promiseHidden;
+
+ info("Waiting for modifyLogin promise");
+ await loginModifiedPromise;
+
+ info("Waiting for removeLogin promise");
+ await loginRemovedPromise;
+
+ info("storage-change promises resolved");
+ // Check the auto-saved login was removed and the original login updated
+ verifyLogins([
+ {
+ username: "user1",
+ password: autoSavedLogin.password,
+ timeCreated: user1LoginSnapshot.timeCreated,
+ timeLastUsed: user1LoginSnapshot.timeLastUsed,
+ passwordChangedSince: autoSavedLogin.timePasswordChanged,
+ },
+ ]);
+
+ // Check we have no notifications at this point
+ Assert.ok(!PopupNotifications.isPanelOpen, "No doorhanger is open");
+ Assert.ok(
+ !PopupNotifications.getNotification("password", browser),
+ "No notifications"
+ );
+
+ // make sure the cache entry is unchanged with the removal of the auto-saved login
+ Assert.equal(
+ autoSavedLogin.password,
+ LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get(
+ "https://example.com"
+ ).value,
+ "Generated password cache entry has the expected password value"
+ );
+ }
+ );
+});
+
+add_task(async function form_change_from_autosaved_login_to_existing_login() {
+ // test when changing from a generated password in a form to an existing saved login
+ // * the auto-saved login should not be deleted
+ // * the metadata for the matching login should be updated
+ // * the by-origin cache for the password should point at the autosaved login
+
+ await setup_withOneLogin("user1", "xyzpassword");
+ await openFormInNewTab(
+ TEST_ORIGIN + FORM_PAGE_PATH,
+ {
+ password: {
+ selector: passwordInputSelector,
+ expectedValue: "xyzpassword",
+ setValue: "",
+ expectedMessage: "PasswordEditedOrGenerated",
+ },
+ username: {
+ selector: usernameInputSelector,
+ expectedValue: "user1",
+ setValue: "",
+ // with an empty password value, no message is sent for a username change
+ expectedMessage: "",
+ },
+ },
+ async function taskFn(browser) {
+ await SimpleTest.promiseFocus(browser);
+
+ // first, create an auto-saved login with generated password
+ let storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "addLogin"
+ );
+ // We don't need to wait to confirm the hint hides itelf every time
+ let forceClosePopup = true;
+ let hintShownAndVerified = verifyConfirmationHint(
+ browser,
+ forceClosePopup
+ );
+
+ info("Filling generated password from AC menu");
+ await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector);
+
+ info("waiting for dismissed password-change notification");
+ await waitForDoorhanger(browser, "password-change");
+
+ // Make sure confirmation hint was shown
+ info("waiting for verifyConfirmationHint");
+ await hintShownAndVerified;
+
+ info("waiting for addLogin");
+ await storageChangedPromise;
+ info("addLogin promise resolved");
+ // Check properties of the newly auto-saved login
+ let [user1LoginSnapshot, autoSavedLogin] = verifyLogins([
+ null, // ignore the first one
+ {
+ timesUsed: 1,
+ username: "",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ },
+ ]);
+ info("user1LoginSnapshot, guid: " + user1LoginSnapshot.guid);
+ info("autoSavedLogin, guid: " + autoSavedLogin.guid);
+
+ info("verifyLogins ok");
+ let passwordCacheEntry =
+ LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get(
+ "https://example.com"
+ );
+
+ Assert.ok(
+ passwordCacheEntry,
+ "Got the cached generated password entry for https://example.com"
+ );
+ Assert.equal(
+ passwordCacheEntry.value,
+ autoSavedLogin.password,
+ "Cached password matches the auto-saved login password"
+ );
+ Assert.equal(
+ passwordCacheEntry.storageGUID,
+ autoSavedLogin.guid,
+ "Cached password guid matches the auto-saved login guid"
+ );
+
+ let notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: true,
+ anchorExtraAttr: "attention",
+ usernameValue: "",
+ password: autoSavedLogin.password,
+ });
+
+ // close but don't remove the doorhanger, we want to ensure it is updated/replaced on further form edits
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ let PN = notif.owner;
+ PN.panel.hidePopup();
+ await promiseHidden;
+ await TestUtils.waitForTick();
+
+ // now update the form with the user1 username and password
+ info(`updating form`);
+ let passwordEditedMessages = listenForTestNotification(
+ "PasswordEditedOrGenerated",
+ 2
+ );
+ let passwordChangeDoorhangerPromise = waitForDoorhanger(
+ browser,
+ "password-change"
+ );
+ let hintDidShow = false;
+ let hintPromiseShown = BrowserTestUtils.waitForPopupEvent(
+ document.getElementById("confirmation-hint"),
+ "shown"
+ );
+ hintPromiseShown.then(() => (hintDidShow = true));
+
+ info("Entering username and password for the previously saved login");
+
+ await changeContentFormValues(browser, {
+ [passwordInputSelector]: user1LoginSnapshot.password,
+ [usernameInputSelector]: user1LoginSnapshot.username,
+ });
+ info(
+ "form edited, waiting for test notification of PasswordEditedOrGenerated"
+ );
+
+ await passwordEditedMessages;
+ info("Resolved listenForTestNotification promise");
+
+ await passwordChangeDoorhangerPromise;
+ // wait to ensure there's no confirmation hint
+ try {
+ await TestUtils.waitForCondition(
+ () => {
+ return hintDidShow;
+ },
+ `Waiting for confirmationHint popup`,
+ undefined,
+ 25
+ );
+ } catch (ex) {
+ info("Got expected timeout from the waitForCondition: ", ex);
+ } finally {
+ Assert.ok(!hintDidShow, "No confirmation hint shown");
+ }
+
+ // the previous doorhanger would have old values, verify it was updated/replaced with new values from the form
+ notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: true,
+ anchorExtraAttr: "",
+ usernameValue: user1LoginSnapshot.username,
+ passwordLength: user1LoginSnapshot.password.length,
+ });
+ await cleanupDoorhanger(notif);
+
+ storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "modifyLogin"
+ );
+
+ // submit the form to ensure the correct updates are made
+ await submitForm(browser);
+ info("form submitted, waiting for storage changed");
+ await storageChangedPromise;
+
+ // Check the auto-saved login has not changed and only metadata on the original login updated
+ verifyLogins([
+ {
+ username: "user1",
+ password: "xyzpassword",
+ timeCreated: user1LoginSnapshot.timeCreated,
+ usedSince: user1LoginSnapshot.timeLastUsed,
+ },
+ {
+ username: "",
+ password: autoSavedLogin.password,
+ timeCreated: autoSavedLogin.timeCreated,
+ timeLastUsed: autoSavedLogin.timeLastUsed,
+ },
+ ]);
+
+ // Check we have no notifications at this point
+ Assert.ok(!PopupNotifications.isPanelOpen, "No doorhanger is open");
+ Assert.ok(
+ !PopupNotifications.getNotification("password", browser),
+ "No notifications"
+ );
+
+ // make sure the cache entry is unchanged with the removal of the auto-saved login
+ Assert.equal(
+ autoSavedLogin.password,
+ LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get(
+ "https://example.com"
+ ).value,
+ "Generated password cache entry has the expected password value"
+ );
+ }
+ );
+});
+
+add_task(async function form_edit_username_and_password_of_generated_login() {
+ // test when changing the username and then the password in a form with a generated password (bug 1625242)
+ // * the toast is not shown for the username change as the auto-saved login is not modified
+ // * the dismissed doorhanger for the username change has the correct username and password
+ // * the toast is shown for the change to the generated password
+ // * the dismissed doorhanger for the password change has the correct username and password
+
+ await setup_withNoLogins();
+ await openFormInNewTab(
+ TEST_ORIGIN + FORM_PAGE_PATH,
+ {},
+ async function taskFn(browser) {
+ await SimpleTest.promiseFocus(browser);
+
+ // first, create an auto-saved login with generated password
+ let storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "addLogin"
+ );
+ // We don't need to wait to confirm the hint hides itelf every time
+ let forceClosePopup = true;
+ let hintShownAndVerified = verifyConfirmationHint(
+ browser,
+ forceClosePopup
+ );
+
+ info("Filling generated password from context menu");
+ // there's no new-password field in this form so we'll use the context menu
+ await doFillGeneratedPasswordContextMenuItem(
+ browser,
+ passwordInputSelector
+ );
+
+ info("waiting for dismissed password-change notification");
+ await waitForDoorhanger(browser, "password-change");
+
+ // Make sure confirmation hint was shown
+ info("waiting for verifyConfirmationHint");
+ await hintShownAndVerified;
+
+ info("waiting for addLogin");
+ await storageChangedPromise;
+ info("addLogin promise resolved");
+ // Check properties of the newly auto-saved login
+ let [autoSavedLoginSnapshot] = verifyLogins([
+ {
+ timesUsed: 1,
+ username: "",
+ passwordLength: LoginTestUtils.generation.LENGTH,
+ },
+ ]);
+
+ let notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: true,
+ anchorExtraAttr: "attention",
+ usernameValue: "",
+ password: autoSavedLoginSnapshot.password,
+ });
+
+ // close but don't remove the doorhanger, we want to ensure it is updated/replaced on further form edits
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ let PN = notif.owner;
+ PN.panel.hidePopup();
+ await promiseHidden;
+ await TestUtils.waitForTick();
+
+ // change the username then the password in the form
+ for (let {
+ fieldSelector,
+ fieldValue,
+ expectedConfirmation,
+ expectedDoorhangerUsername,
+ expectedDoorhangerPassword,
+ expectedDoorhangerType,
+ } of [
+ {
+ fieldSelector: usernameInputSelector,
+ fieldValue: "someuser",
+ expectedConfirmation: false,
+ expectedDoorhangerUsername: "someuser",
+ expectedDoorhangerPassword: autoSavedLoginSnapshot.password,
+ expectedDoorhangerType: "password-change",
+ },
+ {
+ fieldSelector: passwordInputSelector,
+ fieldValue: "!!",
+ expectedConfirmation: true,
+ expectedDoorhangerUsername: "someuser",
+ expectedDoorhangerPassword: autoSavedLoginSnapshot.password + "!!",
+ expectedDoorhangerType: "password-change",
+ },
+ ]) {
+ let loginModifiedPromise = expectedConfirmation
+ ? TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "modifyLogin"
+ )
+ : Promise.resolve();
+
+ // now edit the field value
+ let passwordEditedMessage = listenForTestNotification(
+ "PasswordEditedOrGenerated"
+ );
+ let passwordChangeDoorhangerPromise = waitForDoorhanger(
+ browser,
+ expectedDoorhangerType
+ );
+ let hintDidShow = false;
+ let hintPromiseShown = BrowserTestUtils.waitForPopupEvent(
+ document.getElementById("confirmation-hint"),
+ "shown"
+ );
+ hintPromiseShown.then(() => (hintDidShow = true));
+
+ info(`updating form: ${fieldSelector}: ${fieldValue}`);
+ await appendContentInputvalue(browser, fieldSelector, fieldValue);
+ info(
+ "form edited, waiting for test notification of PasswordEditedOrGenerated"
+ );
+ await passwordEditedMessage;
+ info(
+ "Resolved listenForTestNotification promise, waiting for doorhanger"
+ );
+ await passwordChangeDoorhangerPromise;
+ // wait for possible confirmation hint
+ try {
+ info("Waiting for hintDidShow");
+ await TestUtils.waitForCondition(
+ () => hintDidShow,
+ `Waiting for confirmationHint popup`,
+ undefined,
+ 25
+ );
+ } catch (ex) {
+ info("Got expected timeout from the waitForCondition: " + ex);
+ } finally {
+ info("confirmationHint check done, assert on hintDidShow");
+ Assert.equal(
+ hintDidShow,
+ expectedConfirmation,
+ "Confirmation hint shown"
+ );
+ }
+ info(
+ "Waiting for loginModifiedPromise, expectedConfirmation? " +
+ expectedConfirmation
+ );
+ await loginModifiedPromise;
+
+ // the previous doorhanger would have old values, verify it was updated/replaced with new values from the form
+ info("Verifying the doorhanger");
+ notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: true,
+ anchorExtraAttr: expectedConfirmation ? "attention" : "",
+ usernameValue: expectedDoorhangerUsername,
+ passwordLength: expectedDoorhangerPassword.length,
+ });
+ await cleanupDoorhanger(notif);
+ }
+
+ // submit the form to verify we still get the right doorhanger values
+ let passwordChangeDoorhangerPromise = waitForDoorhanger(
+ browser,
+ "password-change"
+ );
+ await submitForm(browser);
+ info("form submitted, waiting for doorhanger");
+ await passwordChangeDoorhangerPromise;
+ notif = await openAndVerifyDoorhanger(browser, "password-change", {
+ dismissed: false,
+ anchorExtraAttr: "",
+ usernameValue: "someuser",
+ passwordLength: LoginTestUtils.generation.LENGTH + 2,
+ });
+ await cleanupDoorhanger(notif);
+ }
+ );
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_httpsUpgrade.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_httpsUpgrade.js
new file mode 100644
index 0000000000..acf3b64e08
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_httpsUpgrade.js
@@ -0,0 +1,303 @@
+/*
+ * Test capture popup notifications with HTTPS upgrades
+ */
+
+let nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo,
+ "init"
+);
+let login1 = new nsLoginInfo(
+ "http://example.com",
+ "http://example.com",
+ null,
+ "notifyu1",
+ "notifyp1",
+ "user",
+ "pass"
+);
+let login1HTTPS = new nsLoginInfo(
+ "https://example.com",
+ "https://example.com",
+ null,
+ "notifyu1",
+ "notifyp1",
+ "user",
+ "pass"
+);
+
+add_task(async function test_httpsUpgradeCaptureFields_noChange() {
+ info(
+ "Check that we don't prompt to remember when capturing an upgraded login with no change"
+ );
+ await Services.logins.addLoginAsync(login1);
+ // Sanity check the HTTP login exists.
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should have the HTTP login");
+
+ await testSubmittingLoginForm(
+ "subtst_notifications_1.html",
+ function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = getCaptureDoorhanger("password-save");
+ Assert.ok(!notif, "checking for no notification popup");
+ },
+ "https://example.com"
+ ); // This is HTTPS whereas the saved login is HTTP
+
+ logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login still");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(
+ login.origin,
+ "http://example.com",
+ "Check the origin is unchanged"
+ );
+ Assert.equal(login.username, "notifyu1", "Check the username is unchanged");
+ Assert.equal(login.password, "notifyp1", "Check the password is unchanged");
+ Assert.equal(login.timesUsed, 2, "Check times used increased");
+
+ Services.logins.removeLogin(login1);
+});
+
+add_task(async function test_httpsUpgradeCaptureFields_changePW() {
+ info(
+ "Check that we prompt to change when capturing an upgraded login with a new PW"
+ );
+ await Services.logins.addLoginAsync(login1);
+ // Sanity check the HTTP login exists.
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should have the HTTP login");
+
+ await testSubmittingLoginForm(
+ "subtst_notifications_8.html",
+ async function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "pass2",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-change");
+ Assert.ok(notif, "checking for a change popup");
+
+ await checkDoorhangerUsernamePassword("notifyu1", "pass2");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+
+ Assert.ok(
+ !getCaptureDoorhanger("password-change"),
+ "popup should be gone"
+ );
+ },
+ "https://example.com"
+ ); // This is HTTPS whereas the saved login is HTTP
+
+ checkOnlyLoginWasUsedTwice({ justChanged: true });
+ logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login still");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(
+ login.origin,
+ "https://example.com",
+ "Check the origin is upgraded"
+ );
+ Assert.equal(
+ login.formActionOrigin,
+ "https://example.com",
+ "Check the formActionOrigin is upgraded"
+ );
+ Assert.equal(login.username, "notifyu1", "Check the username is unchanged");
+ Assert.equal(login.password, "pass2", "Check the password changed");
+ Assert.equal(login.timesUsed, 2, "Check times used increased");
+
+ Services.logins.removeAllUserFacingLogins();
+});
+
+add_task(
+ async function test_httpsUpgradeCaptureFields_changePWWithBothSchemesSaved() {
+ info(
+ "Check that we prompt to change and properly save when capturing an upgraded login with a new PW when an http login also exists for that username"
+ );
+ await Services.logins.addLogins([login1, login1HTTPS]);
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 2, "Should have both HTTP and HTTPS logins");
+
+ await testSubmittingLoginForm(
+ "subtst_notifications_8.html",
+ async function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "pass2",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-change");
+ Assert.ok(notif, "checking for a change popup");
+
+ await checkDoorhangerUsernamePassword("notifyu1", "pass2");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+
+ Assert.ok(
+ !getCaptureDoorhanger("password-change"),
+ "popup should be gone"
+ );
+ },
+ "https://example.com"
+ );
+
+ logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 2, "Should have 2 logins still");
+ let loginHTTP = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ let loginHTTPS = logins[1].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.ok(
+ LoginHelper.doLoginsMatch(login1, loginHTTP, { ignorePassword: true }),
+ "Check HTTP login is equal"
+ );
+ Assert.equal(loginHTTP.timesUsed, 1, "Check times used stayed the same");
+ Assert.equal(
+ loginHTTP.timeCreated,
+ loginHTTP.timePasswordChanged,
+ "login.timeCreated == login.timePasswordChanged"
+ );
+ Assert.equal(
+ loginHTTP.timeLastUsed,
+ loginHTTP.timePasswordChanged,
+ "timeLastUsed == timePasswordChanged"
+ );
+
+ Assert.ok(
+ LoginHelper.doLoginsMatch(login1HTTPS, loginHTTPS, {
+ ignorePassword: true,
+ }),
+ "Check HTTPS login is equal"
+ );
+ Assert.equal(
+ loginHTTPS.username,
+ "notifyu1",
+ "Check the username is unchanged"
+ );
+ Assert.equal(loginHTTPS.password, "pass2", "Check the password changed");
+ Assert.equal(loginHTTPS.timesUsed, 2, "Check times used increased");
+ Assert.ok(
+ loginHTTPS.timeCreated < loginHTTPS.timePasswordChanged,
+ "login.timeCreated < login.timePasswordChanged"
+ );
+ Assert.equal(
+ loginHTTPS.timeLastUsed,
+ loginHTTPS.timePasswordChanged,
+ "timeLastUsed == timePasswordChanged"
+ );
+
+ Services.logins.removeAllUserFacingLogins();
+ }
+);
+
+add_task(async function test_httpsUpgradeCaptureFields_captureMatchingHTTP() {
+ info("Capture a new HTTP login which matches a stored HTTPS one.");
+ await Services.logins.addLoginAsync(login1HTTPS);
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_1.html",
+ async function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(notif, "got notification popup");
+
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 1,
+ "Should only have the HTTPS login"
+ );
+
+ await checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ clickDoorhangerButton(notif, REMEMBER_BUTTON);
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 2, "Should have both HTTP and HTTPS logins");
+ for (let login of logins) {
+ login = login.QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(
+ login.username,
+ "notifyu1",
+ "Check the username used on the new entry"
+ );
+ Assert.equal(
+ login.password,
+ "notifyp1",
+ "Check the password used on the new entry"
+ );
+ Assert.equal(login.timesUsed, 1, "Check times used on entry");
+ }
+
+ info(
+ "Make sure Remember took effect and we don't prompt for an existing HTTP login"
+ );
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_1.html",
+ function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = getCaptureDoorhanger("password-save");
+ Assert.ok(!notif, "checking for no notification popup");
+ }
+ );
+
+ logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 2, "Should have both HTTP and HTTPS still");
+
+ let httpsLogins = LoginHelper.searchLoginsWithObject({
+ origin: "https://example.com",
+ });
+ Assert.equal(httpsLogins.length, 1, "Check https logins count");
+ let httpsLogin = httpsLogins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.ok(httpsLogin.equals(login1HTTPS), "Check HTTPS login didn't change");
+ Assert.equal(httpsLogin.timesUsed, 1, "Check times used");
+
+ let httpLogins = LoginHelper.searchLoginsWithObject({
+ origin: "http://example.com",
+ });
+ Assert.equal(httpLogins.length, 1, "Check http logins count");
+ let httpLogin = httpLogins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.ok(httpLogin.equals(login1), "Check HTTP login is as expected");
+ Assert.equal(httpLogin.timesUsed, 2, "Check times used increased");
+
+ Services.logins.removeLogin(login1);
+ Services.logins.removeLogin(login1HTTPS);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_multipage_form.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_multipage_form.js
new file mode 100644
index 0000000000..ff7bffcb44
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_multipage_form.js
@@ -0,0 +1,182 @@
+/**
+ * Test that the doorhanger notification for password saving is populated with
+ * the correct values in various password capture cases (multipage login form).
+ */
+
+const testCases = [
+ {
+ name: "No saved logins, username and password",
+ username: "username",
+ password: "password",
+ expectOutcome: [
+ {
+ username: "username",
+ password: "password",
+ },
+ ],
+ },
+ {
+ name: "No saved logins, password with empty username",
+ username: "",
+ password: "password",
+ expectOutcome: [
+ {
+ username: "",
+ password: "password",
+ },
+ ],
+ },
+ {
+ name: "Saved login with username, update password",
+ username: "username",
+ oldPassword: "password",
+ password: "newPassword",
+ expectOutcome: [
+ {
+ username: "username",
+ password: "newPassword",
+ },
+ ],
+ },
+ {
+ name: "Saved login with username, add username",
+ oldUsername: "username",
+ username: "newUsername",
+ password: "password",
+ expectOutcome: [
+ {
+ username: "newUsername",
+ password: "password",
+ },
+ ],
+ },
+ {
+ name: "Saved login with no username, add username and different password",
+ oldUsername: "",
+ username: "username",
+ oldPassword: "password",
+ password: "newPassword",
+ expectOutcome: [
+ {
+ username: "",
+ password: "password",
+ },
+ {
+ username: "username",
+ password: "newPassword",
+ },
+ ],
+ },
+];
+
+add_task(async function test_initialize() {
+ Services.prefs.setBoolPref("signon.usernameOnlyForm.enabled", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("signon.usernameOnlyForm.enabled");
+ });
+});
+
+for (let testData of testCases) {
+ let tmp = {
+ async [testData.name]() {
+ info("testing with: " + JSON.stringify(testData));
+ await test_save_change(testData);
+ },
+ };
+ add_task(tmp[testData.name]);
+}
+
+async function test_save_change(testData) {
+ let { oldUsername, username, oldPassword, password, expectOutcome } =
+ testData;
+ // Add a login for the origin of the form if testing a change notification.
+ if (oldPassword) {
+ await Services.logins.addLoginAsync(
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: typeof oldUsername !== "undefined" ? oldUsername : username,
+ password: oldPassword,
+ })
+ );
+ }
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_multipage.html",
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+
+ // Update the username filed from the test case.
+ info(`update form with username: ${username}`);
+ await changeContentFormValues(browser, {
+ "#form-basic-username": username,
+ });
+
+ // Submit the username-only form, which then advance to the password-only
+ // form.
+ info(`submit the username-only form`);
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = this.content.document;
+ doc.getElementById("form-basic-submit").click();
+ await ContentTaskUtils.waitForCondition(() => {
+ return doc.getElementById("form-basic-password");
+ }, "Wait for the username field");
+ });
+
+ // Update the password filed from the test case.
+ info(`update form with password: ${password}`);
+ await changeContentFormValues(browser, {
+ "#form-basic-password": password,
+ });
+
+ // Submit the form.
+ info(`submit the password-only form`);
+ let formSubmittedPromise = listenForTestNotification("ShowDoorhanger");
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = this.content.document;
+ doc.getElementById("form-basic-submit").click();
+ });
+ await formSubmittedPromise;
+
+ // Simulate the action on the notification to request the login to be
+ // saved, and wait for the data to be updated or saved based on the type
+ // of operation we expect.
+ let expectedNotification, expectedDoorhanger;
+ if (oldPassword !== undefined && oldUsername !== undefined) {
+ expectedNotification = "addLogin";
+ expectedDoorhanger = "password-save";
+ } else if (oldPassword !== undefined) {
+ expectedNotification = "modifyLogin";
+ expectedDoorhanger = "password-change";
+ } else {
+ expectedNotification = "addLogin";
+ expectedDoorhanger = "password-save";
+ }
+
+ info("Waiting for doorhanger of type: " + expectedDoorhanger);
+ let notif = await waitForDoorhanger(browser, expectedDoorhanger);
+
+ // Check the actual content of the popup notification.
+ await checkDoorhangerUsernamePassword(username, password);
+
+ let promiseLogin = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == expectedNotification
+ );
+ await clickDoorhangerButton(notif, REMEMBER_BUTTON);
+ await promiseLogin;
+ await cleanupDoorhanger(notif); // clean slate for the next test
+
+ // Check that the values in the database match the expected values.
+ verifyLogins(expectOutcome);
+ }
+ );
+
+ // Clean up the database before the next test case is executed.
+ Services.logins.removeAllUserFacingLogins();
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_password_edits.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_password_edits.js
new file mode 100644
index 0000000000..804612cf53
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_password_edits.js
@@ -0,0 +1,220 @@
+/**
+ * Test changing the password inside the doorhanger notification for passwords.
+ *
+ * We check the following cases:
+ * - Editing the password of a new login.
+ * - Editing the password of an existing login.
+ * - Changing both username and password to an existing login.
+ * - Changing the username to an existing login.
+ * - Editing username to an empty one and a new password.
+ *
+ * If both the username and password matches an already existing login, we should not
+ * update it's password, but only it's usage timestamp and count.
+ */
+add_task(async function test_edit_password() {
+ let testCases = [
+ {
+ description: "No saved logins, update password in doorhanger",
+ usernameInPage: "username",
+ passwordInPage: "password",
+ passwordChangedTo: "newPassword",
+ timesUsed: 1,
+ },
+ {
+ description: "Login is saved, update password in doorhanger",
+ usernameInPage: "username",
+ usernameInPageExists: true,
+ passwordInPage: "password",
+ passwordInStorage: "oldPassword",
+ passwordChangedTo: "newPassword",
+ timesUsed: 2,
+ },
+ {
+ description:
+ "Change username in doorhanger to match saved login, update password in doorhanger",
+ usernameInPage: "username",
+ usernameChangedTo: "newUsername",
+ usernameChangedToExists: true,
+ passwordInPage: "password",
+ passwordChangedTo: "newPassword",
+ timesUsed: 2,
+ },
+ {
+ description:
+ "Change username in doorhanger to match saved login, dont update password in doorhanger",
+ usernameInPage: "username",
+ usernameChangedTo: "newUsername",
+ usernameChangedToExists: true,
+ passwordInPage: "password",
+ passwordChangedTo: "password",
+ timesUsed: 2,
+ checkPasswordNotUpdated: true,
+ },
+ {
+ description:
+ "Change username and password in doorhanger to match saved empty-username login",
+ usernameInPage: "newUsername",
+ usernameChangedTo: "",
+ usernameChangedToExists: true,
+ passwordInPage: "password",
+ passwordChangedTo: "newPassword",
+ timesUsed: 2,
+ },
+ ];
+
+ for (let testCase of testCases) {
+ info("Test case: " + JSON.stringify(testCase));
+ // Clean state before the test case is executed.
+ await LoginTestUtils.clearData();
+ await cleanupDoorhanger();
+ await cleanupPasswordNotifications();
+
+ // Create the pre-existing logins when needed.
+ if (testCase.usernameInPageExists) {
+ await Services.logins.addLoginAsync(
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: testCase.usernameInPage,
+ password: testCase.passwordInStorage,
+ })
+ );
+ }
+
+ if (testCase.usernameChangedToExists) {
+ await Services.logins.addLoginAsync(
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: testCase.usernameChangedTo,
+ password: testCase.passwordChangedTo,
+ })
+ );
+ }
+
+ let formFilledPromise = listenForTestNotification("FormProcessed");
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ },
+ async function (browser) {
+ await formFilledPromise;
+
+ // Set the form to a known state so we can expect a single PasswordEditedOrGenerated message
+ await initForm(browser, {
+ "#form-basic-username": testCase.usernameInPage,
+ "#form-basic-password": "",
+ });
+
+ let passwordEditedPromise = listenForTestNotification(
+ "PasswordEditedOrGenerated"
+ );
+ info("Editing the form");
+ await changeContentFormValues(browser, {
+ "#form-basic-password": testCase.passwordInPage,
+ });
+ info("Waiting for passwordEditedPromise");
+ await passwordEditedPromise;
+
+ // reset doorhanger/notifications, we're only interested in the submit outcome
+ await cleanupDoorhanger();
+ await cleanupPasswordNotifications();
+ // reset message cache, we're only interested in the submit outcome
+ await clearMessageCache(browser);
+
+ // Submit the form in the content page with the credentials from the test
+ // case. This will cause the doorhanger notification to be displayed.
+ info("Submitting the form");
+ let formSubmittedPromise = listenForTestNotification("ShowDoorhanger");
+ let promiseShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown",
+ event => event.target == PopupNotifications.panel
+ );
+ await SpecialPowers.spawn(browser, [], function () {
+ content.document.getElementById("form-basic").submit();
+ });
+ await formSubmittedPromise;
+
+ let notif = await waitForDoorhanger(browser, "any");
+ Assert.ok(!notif.dismissed, "Doorhanger is not dismissed");
+ await promiseShown;
+
+ // Modify the username & password in the dialog if requested.
+ await updateDoorhangerInputValues({
+ username: testCase.usernameChangedTo,
+ password: testCase.passwordChangedTo,
+ });
+
+ // We expect a modifyLogin notification if the final username used by the
+ // dialog exists in the logins database, otherwise an addLogin one.
+ let expectModifyLogin =
+ typeof testCase.usernameChangedTo !== "undefined"
+ ? testCase.usernameChangedToExists
+ : testCase.usernameInPageExists;
+
+ // Simulate the action on the notification to request the login to be
+ // saved, and wait for the data to be updated or saved based on the type
+ // of operation we expect.
+ let expectedNotification = expectModifyLogin
+ ? "modifyLogin"
+ : "addLogin";
+ let promiseLogin = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == expectedNotification
+ );
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+ await promiseHidden;
+ info("Waiting for storage changed");
+ let [result] = await promiseLogin;
+
+ // Check that the values in the database match the expected values.
+ let login = expectModifyLogin
+ ? result
+ .QueryInterface(Ci.nsIArray)
+ .queryElementAt(1, Ci.nsILoginInfo)
+ : result.QueryInterface(Ci.nsILoginInfo);
+ let meta = login.QueryInterface(Ci.nsILoginMetaInfo);
+
+ let expectedLogin = {
+ username:
+ "usernameChangedTo" in testCase
+ ? testCase.usernameChangedTo
+ : testCase.usernameInPage,
+ password:
+ "passwordChangedTo" in testCase
+ ? testCase.passwordChangedTo
+ : testCase.passwordInPage,
+ timesUsed: testCase.timesUsed,
+ };
+ // Check that the password was not updated if the user is empty
+ if (testCase.checkPasswordNotUpdated) {
+ expectedLogin.usedSince = meta.timeCreated;
+ expectedLogin.timeCreated = meta.timePasswordChanged;
+ }
+ verifyLogins([expectedLogin]);
+ }
+ );
+ }
+});
+
+async function initForm(browser, formDefaults = {}) {
+ await ContentTask.spawn(
+ browser,
+ formDefaults,
+ async function (selectorValues) {
+ for (let [sel, value] of Object.entries(selectorValues)) {
+ content.document.querySelector(sel).value = value;
+ }
+ }
+ );
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_promptToChangePassword.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_promptToChangePassword.js
new file mode 100644
index 0000000000..84c241e020
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_promptToChangePassword.js
@@ -0,0 +1,685 @@
+/**
+ * Test result of different input to the promptToChangePassword doorhanger
+ */
+
+"use strict";
+
+// The origin for the test URIs.
+const TEST_ORIGIN = "https://example.com";
+const passwordInputSelector = "#form-basic-password";
+const usernameInputSelector = "#form-basic-username";
+
+const availLoginsByValue = new Map();
+let savedLoginsByName;
+const finalLoginsByGuid = new Map();
+let finalLogins;
+
+const availLogins = {
+ emptyXYZ: LoginTestUtils.testData.formLogin({
+ username: "",
+ password: "xyz",
+ }),
+ bobXYZ: LoginTestUtils.testData.formLogin({
+ username: "bob",
+ password: "xyz",
+ }),
+ bobABC: LoginTestUtils.testData.formLogin({
+ username: "bob",
+ password: "abc",
+ }),
+};
+availLoginsByValue.set(availLogins.emptyXYZ, "emptyXYZ");
+availLoginsByValue.set(availLogins.bobXYZ, "bobXYZ");
+availLoginsByValue.set(availLogins.bobABC, "bobABC");
+
+async function showChangePasswordDoorhanger(
+ browser,
+ oldLogin,
+ formLogin,
+ { notificationType = "password-change", autoSavedLoginGuid = "" } = {}
+) {
+ let windowGlobal = browser.browsingContext.currentWindowGlobal;
+ let loginManagerActor = windowGlobal.getActor("LoginManager");
+ let prompter = loginManagerActor._getPrompter(browser, null);
+ Assert.ok(
+ !PopupNotifications.isPanelOpen,
+ "Check the doorhanger isn't already open"
+ );
+
+ let promiseShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ prompter.promptToChangePassword(
+ browser,
+ oldLogin,
+ formLogin,
+ false, // dimissed prompt
+ false, // notifySaved
+ autoSavedLoginGuid
+ );
+ await promiseShown;
+
+ let notif = getCaptureDoorhanger(notificationType);
+ Assert.ok(notif, `${notificationType} notification exists`);
+
+ let { panel } = PopupNotifications;
+ let notificationElement = panel.childNodes[0];
+ await BrowserTestUtils.waitForCondition(() => {
+ return (
+ notificationElement.querySelector("#password-notification-password")
+ .value == formLogin.password &&
+ notificationElement.querySelector("#password-notification-username")
+ .value == formLogin.username
+ );
+ }, "Wait for the notification panel to be populated");
+ return notif;
+}
+
+async function setupLogins(...logins) {
+ Services.logins.removeAllUserFacingLogins();
+ let savedLogins = {};
+ let timesCreated = new Set();
+ for (let login of logins) {
+ let loginName = availLoginsByValue.get(login);
+ let savedLogin = await LoginTestUtils.addLogin(login);
+ // we rely on sorting by timeCreated so ensure none are identical
+ Assert.ok(
+ !timesCreated.has(savedLogin.timeCreated),
+ "Each login has a different timeCreated"
+ );
+ timesCreated.add(savedLogin.timeCreated);
+ savedLogins[loginName || savedLogin.guid] = savedLogin.clone();
+ }
+ return savedLogins;
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.autofillForms", false]],
+ });
+ Assert.ok(!PopupNotifications.isPanelOpen, "No notifications panel open");
+});
+
+async function promptToChangePasswordTest(testData) {
+ info("Starting: " + testData.name);
+ savedLoginsByName = await setupLogins(...testData.initialSavedLogins);
+ await SimpleTest.promiseFocus();
+ info("got focus");
+
+ let oldLogin = savedLoginsByName[testData.promptArgs.oldLogin];
+ let changeLogin = LoginTestUtils.testData.formLogin(
+ testData.promptArgs.changeLogin
+ );
+ let options;
+ if (testData.autoSavedLoginName) {
+ options = {
+ autoSavedLoginGuid: savedLoginsByName[testData.autoSavedLoginName].guid,
+ };
+ }
+ info(
+ "Waiting for showChangePasswordDoorhanger, username: " +
+ changeLogin.username
+ );
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ TEST_ORIGIN,
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+ let notif = await showChangePasswordDoorhanger(
+ browser,
+ oldLogin,
+ changeLogin,
+ options
+ );
+
+ await updateDoorhangerInputValues(testData.promptTextboxValues);
+
+ let mainActionButton = getDoorhangerButton(notif, CHANGE_BUTTON);
+ Assert.equal(
+ mainActionButton.label,
+ testData.expectedButtonLabel,
+ "Check button label"
+ );
+
+ let { panel } = PopupNotifications;
+ let promiseHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ let storagePromise;
+ if (testData.expectedStorageChange) {
+ storagePromise = TestUtils.topicObserved("passwordmgr-storage-changed");
+ }
+
+ info("Clicking mainActionButton");
+ mainActionButton.doCommand();
+ info("Waiting for promiseHidden");
+ await promiseHidden;
+ info("Waiting for storagePromise");
+ await storagePromise;
+
+ // ensure the notification was removed to keep clean state for next run
+ await cleanupDoorhanger(notif);
+
+ info(testData.resultDescription);
+
+ finalLoginsByGuid.clear();
+ finalLogins = Services.logins.getAllLogins();
+ finalLogins.sort((a, b) => a.timeCreated > b.timeCreated);
+
+ for (let l of finalLogins) {
+ info(`saved login: ${l.guid}: ${l.username}/${l.password}`);
+ finalLoginsByGuid.set(l.guid, l);
+ }
+ info("verifyLogins next");
+ verifyLogins(testData.expectedResultLogins);
+ if (testData.resultCheck) {
+ testData.resultCheck();
+ }
+ }
+ );
+}
+
+let tests = [
+ {
+ name: "Add username to sole login",
+ initialSavedLogins: [availLogins.emptyXYZ],
+ promptArgs: {
+ oldLogin: "emptyXYZ",
+ changeLogin: {
+ username: "zaphod",
+ password: "xyz",
+ },
+ },
+ promptTextboxValues: {},
+ expectedButtonLabel: "Update",
+ resultDescription: "The existing login just gets a new password",
+ expectedStorageChange: true,
+ expectedResultLogins: [
+ {
+ username: "zaphod",
+ password: "xyz",
+ },
+ ],
+ resultCheck() {
+ Assert.equal(
+ finalLogins[0].guid,
+ savedLoginsByName.emptyXYZ.guid,
+ "Check guid"
+ );
+ },
+ },
+ {
+ name: "Change password of the sole login",
+ initialSavedLogins: [availLogins.bobXYZ],
+ promptArgs: {
+ oldLogin: "bobXYZ",
+ changeLogin: {
+ username: "bob",
+ password: "&*$",
+ },
+ },
+ promptTextboxValues: {},
+ expectedButtonLabel: "Update",
+ resultDescription: "The existing login just gets a new password",
+ expectedStorageChange: true,
+ expectedResultLogins: [
+ {
+ username: "bob",
+ password: "&*$",
+ },
+ ],
+ resultCheck() {
+ Assert.equal(
+ finalLogins[0].guid,
+ savedLoginsByName.bobXYZ.guid,
+ "Check guid"
+ );
+ },
+ },
+ {
+ name: "Change password of the sole empty-username login",
+ initialSavedLogins: [availLogins.emptyXYZ],
+ promptArgs: {
+ oldLogin: "emptyXYZ",
+ changeLogin: {
+ username: "",
+ password: "&*$",
+ },
+ },
+ promptTextboxValues: {},
+ expectedButtonLabel: "Update",
+ resultDescription: "The existing login just gets a new password",
+ expectedStorageChange: true,
+ expectedResultLogins: [
+ {
+ username: "",
+ password: "&*$",
+ },
+ ],
+ resultCheck() {
+ Assert.equal(
+ finalLogins[0].guid,
+ savedLoginsByName.emptyXYZ.guid,
+ "Check guid"
+ );
+ },
+ },
+ {
+ name: "Add different username to empty-usernamed login",
+ initialSavedLogins: [availLogins.emptyXYZ, availLogins.bobABC],
+ promptArgs: {
+ oldLogin: "emptyXYZ",
+ changeLogin: {
+ username: "alice",
+ password: "xyz",
+ },
+ },
+ promptTextboxValues: {},
+ expectedButtonLabel: "Update",
+ resultDescription: "The existing login just gets a new username",
+ expectedStorageChange: true,
+ expectedResultLogins: [
+ {
+ username: "alice",
+ password: "xyz",
+ },
+ {
+ username: "bob",
+ password: "abc",
+ },
+ ],
+ resultCheck() {
+ Assert.equal(
+ finalLogins[0].guid,
+ savedLoginsByName.emptyXYZ.guid,
+ "Check guid"
+ );
+ Assert.ok(
+ finalLogins[0].timeLastUsed > savedLoginsByName.emptyXYZ.timeLastUsed,
+ "Check timeLastUsed of 0th login"
+ );
+ },
+ },
+ {
+ name: "Add username to autosaved login to match an existing usernamed login",
+ initialSavedLogins: [availLogins.emptyXYZ, availLogins.bobABC],
+ autoSavedLoginName: "emptyXYZ",
+ promptArgs: {
+ oldLogin: "emptyXYZ",
+ changeLogin: {
+ username: "bob",
+ password: availLogins.emptyXYZ.password,
+ },
+ },
+ promptTextboxValues: {},
+ expectedButtonLabel: "Update",
+ resultDescription:
+ "Empty-username login is removed, other login gets the empty-login's password",
+ expectedStorageChange: true,
+ expectedResultLogins: [
+ {
+ username: "bob",
+ password: "xyz",
+ },
+ ],
+ resultCheck() {
+ Assert.equal(
+ finalLogins[0].guid,
+ savedLoginsByName.bobABC.guid,
+ "Check guid"
+ );
+ Assert.ok(
+ finalLogins[0].timeLastUsed > savedLoginsByName.bobABC.timeLastUsed,
+ "Check timeLastUsed changed"
+ );
+ },
+ },
+ {
+ name: "Add username to non-autosaved login to match an existing usernamed login",
+ initialSavedLogins: [availLogins.emptyXYZ, availLogins.bobABC],
+ autoSavedLoginName: "",
+ promptArgs: {
+ oldLogin: "emptyXYZ",
+ changeLogin: {
+ username: "bob",
+ password: availLogins.emptyXYZ.password,
+ },
+ },
+ promptTextboxValues: {},
+ expectedButtonLabel: "Update",
+ // We can't end up with duplicates (bob:xyz and bob:ABC) so the following seems reasonable.
+ // We could delete the emptyXYZ but we would want to intelligently merge metadata.
+ resultDescription:
+ "Multiple login matches but user indicated they want bob:xyz in the prompt so modify bob to give that",
+ expectedStorageChange: true,
+ expectedResultLogins: [
+ {
+ username: "",
+ password: "xyz",
+ },
+ {
+ username: "bob",
+ password: "xyz",
+ },
+ ],
+ resultCheck() {
+ Assert.equal(
+ finalLogins[0].guid,
+ savedLoginsByName.emptyXYZ.guid,
+ "Check guid"
+ );
+ Assert.equal(
+ finalLogins[0].timeLastUsed,
+ savedLoginsByName.emptyXYZ.timeLastUsed,
+ "Check timeLastUsed didn't change"
+ );
+ Assert.equal(
+ finalLogins[0].timePasswordChanged,
+ savedLoginsByName.emptyXYZ.timePasswordChanged,
+ "Check timePasswordChanged didn't change"
+ );
+
+ Assert.equal(
+ finalLogins[1].guid,
+ savedLoginsByName.bobABC.guid,
+ "Check guid"
+ );
+ Assert.ok(
+ finalLogins[1].timeLastUsed > savedLoginsByName.bobABC.timeLastUsed,
+ "Check timeLastUsed did change"
+ );
+ Assert.ok(
+ finalLogins[1].timePasswordChanged >
+ savedLoginsByName.bobABC.timePasswordChanged,
+ "Check timePasswordChanged did change"
+ );
+ },
+ },
+ {
+ name: "Username & password changes to an auto-saved login apply to matching usernamed-login",
+ // when we update an auto-saved login - changing both username & password, is
+ // the matching login updated and empty-username login removed?
+ initialSavedLogins: [availLogins.emptyXYZ, availLogins.bobABC],
+ autoSavedLoginName: "emptyXYZ",
+ promptArgs: {
+ oldLogin: "emptyXYZ",
+ changeLogin: {
+ username: "bob",
+ password: "xyz",
+ },
+ },
+ promptTextboxValues: {
+ // type a new password in the doorhanger
+ password: "newpassword",
+ },
+ expectedButtonLabel: "Update",
+ resultDescription:
+ "The empty-username login is removed, other login gets the new password",
+ expectedStorageChange: true,
+ expectedResultLogins: [
+ {
+ username: "bob",
+ password: "newpassword",
+ },
+ ],
+ resultCheck() {
+ Assert.equal(
+ finalLogins[0].guid,
+ savedLoginsByName.bobABC.guid,
+ "Check guid"
+ );
+ Assert.ok(
+ finalLogins[0].timeLastUsed > savedLoginsByName.bobABC.timeLastUsed,
+ "Check timeLastUsed did change"
+ );
+ },
+ },
+ {
+ name: "Username & password changes to a non-auto-saved login matching usernamed-login",
+ // when we update a non-auto-saved login - changing both username & password, is
+ // the matching login updated and empty-username login unchanged?
+ initialSavedLogins: [availLogins.emptyXYZ, availLogins.bobABC],
+ autoSavedLoginName: "", // no auto-saved logins for this session
+ promptArgs: {
+ oldLogin: "emptyXYZ",
+ changeLogin: {
+ username: "bob",
+ password: "xyz",
+ },
+ },
+ promptTextboxValues: {
+ // type a new password in the doorhanger
+ password: "newpassword",
+ },
+ expectedButtonLabel: "Update",
+ resultDescription:
+ "The empty-username login is not changed, other login gets the new password",
+ expectedStorageChange: true,
+ expectedResultLogins: [
+ {
+ username: "",
+ password: "xyz",
+ },
+ {
+ username: "bob",
+ password: "newpassword",
+ },
+ ],
+ resultCheck() {
+ Assert.equal(
+ finalLogins[0].guid,
+ savedLoginsByName.emptyXYZ.guid,
+ "Check guid"
+ );
+ Assert.equal(
+ finalLogins[0].timeLastUsed,
+ savedLoginsByName.emptyXYZ.timeLastUsed,
+ "Check timeLastUsed didn't change"
+ );
+ Assert.equal(
+ finalLogins[0].timePasswordChanged,
+ savedLoginsByName.emptyXYZ.timePasswordChanged,
+ "Check timePasswordChanged didn't change"
+ );
+ Assert.equal(
+ finalLogins[1].guid,
+ savedLoginsByName.bobABC.guid,
+ "Check guid"
+ );
+ Assert.ok(
+ finalLogins[1].timeLastUsed > savedLoginsByName.bobABC.timeLastUsed,
+ "Check timeLastUsed did change"
+ );
+ Assert.ok(
+ finalLogins[1].timePasswordChanged >
+ savedLoginsByName.bobABC.timePasswordChanged,
+ "Check timePasswordChanged did change"
+ );
+ },
+ },
+ {
+ name: "Remove the username and change password of autosaved login",
+ initialSavedLogins: [availLogins.bobABC],
+ autoSavedLoginName: "bobABC",
+ promptArgs: {
+ oldLogin: "bobABC",
+ changeLogin: {
+ username: "bob",
+ password: "abc!", // trigger change prompt with a password change
+ },
+ },
+ promptTextboxValues: {
+ username: "",
+ },
+ expectedButtonLabel: "Update",
+ resultDescription:
+ "The auto-saved login is updated with new empty-username login and new password",
+ expectedStorageChange: true,
+ expectedResultLogins: [
+ {
+ username: "",
+ password: "abc!",
+ },
+ ],
+ resultCheck() {
+ Assert.equal(
+ finalLogins[0].guid,
+ savedLoginsByName.bobABC.guid,
+ "Check guid"
+ );
+ Assert.ok(
+ finalLogins[0].timeLastUsed > savedLoginsByName.bobABC.timeLastUsed,
+ "Check timeLastUsed did change"
+ );
+ Assert.ok(
+ finalLogins[0].timePasswordChanged >
+ savedLoginsByName.bobABC.timePasswordChanged,
+ "Check timePasswordChanged did change"
+ );
+ },
+ },
+ {
+ name: "Remove the username and change password of non-autosaved login",
+ initialSavedLogins: [availLogins.bobABC],
+ // no autosaved guid
+ promptArgs: {
+ oldLogin: "bobABC",
+ changeLogin: {
+ username: "bob",
+ password: "abc!", // trigger change prompt with a password change
+ },
+ },
+ promptTextboxValues: {
+ username: "",
+ },
+ expectedButtonLabel: "Save",
+ resultDescription:
+ "A new empty-username login is created with the new password",
+ expectedStorageChange: true,
+ expectedResultLogins: [
+ {
+ username: "bob",
+ password: "abc",
+ },
+ {
+ username: "",
+ password: "abc!",
+ },
+ ],
+ resultCheck() {
+ Assert.equal(
+ finalLogins[0].guid,
+ savedLoginsByName.bobABC.guid,
+ "Check guid"
+ );
+ Assert.equal(
+ finalLogins[0].timeLastUsed,
+ savedLoginsByName.bobABC.timeLastUsed,
+ "Check timeLastUsed didn't change"
+ );
+ Assert.equal(
+ finalLogins[0].timePasswordChanged,
+ savedLoginsByName.bobABC.timePasswordChanged,
+ "Check timePasswordChanged didn't change"
+ );
+ },
+ },
+ {
+ name: "Remove username from the auto-saved sole login",
+ initialSavedLogins: [availLogins.bobABC],
+ autoSavedLoginName: "bobABC",
+ promptArgs: {
+ oldLogin: "bobABC",
+ changeLogin: {
+ username: "bob",
+ password: "abc!", // trigger change prompt with a password change
+ },
+ },
+ promptTextboxValues: {
+ username: "",
+ password: "abc", // put password back to what it was
+ },
+ expectedButtonLabel: "Update",
+ resultDescription: "The existing login is updated",
+ expectedStorageChange: true,
+ expectedResultLogins: [
+ {
+ username: "",
+ password: "abc",
+ },
+ ],
+ resultCheck() {
+ Assert.equal(
+ finalLogins[0].guid,
+ savedLoginsByName.bobABC.guid,
+ "Check guid"
+ );
+ Assert.ok(
+ finalLogins[0].timeLastUsed > savedLoginsByName.bobABC.timeLastUsed,
+ "Check timeLastUsed did change"
+ );
+ todo_is(
+ finalLogins[0].timePasswordChanged,
+ savedLoginsByName.bobABC.timePasswordChanged,
+ "Check timePasswordChanged didn't change"
+ );
+ },
+ },
+ {
+ name: "Remove username from the non-auto-saved sole login",
+ initialSavedLogins: [availLogins.bobABC],
+ // no autoSavedLoginGuid
+ promptArgs: {
+ oldLogin: "bobABC",
+ changeLogin: {
+ username: "bob",
+ password: "abc!", // trigger change prompt with a password change
+ },
+ },
+ promptTextboxValues: {
+ username: "",
+ password: "abc", // put password back to what it was
+ },
+ expectedButtonLabel: "Save",
+ resultDescription: "A new empty-username login is created",
+ expectedStorageChange: true,
+ expectedResultLogins: [
+ {
+ username: "bob",
+ password: "abc",
+ },
+ {
+ username: "",
+ password: "abc",
+ },
+ ],
+ resultCheck() {
+ Assert.equal(
+ finalLogins[0].guid,
+ savedLoginsByName.bobABC.guid,
+ "Check guid"
+ );
+ Assert.equal(
+ finalLogins[0].timeLastUsed,
+ savedLoginsByName.bobABC.timeLastUsed,
+ "Check timeLastUsed didn't change"
+ );
+ Assert.equal(
+ finalLogins[0].timePasswordChanged,
+ savedLoginsByName.bobABC.timePasswordChanged,
+ "Check timePasswordChanged didn't change"
+ );
+ },
+ },
+];
+
+for (let testData of tests) {
+ let tmp = {
+ async [testData.name]() {
+ await promptToChangePasswordTest(testData);
+ },
+ };
+ add_task(tmp[testData.name]);
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_remembering.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_remembering.js
new file mode 100644
index 0000000000..bc0b245e4d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_remembering.js
@@ -0,0 +1,1275 @@
+/*
+ * Test capture popup notifications
+ */
+
+const BRAND_BUNDLE = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+);
+const BRAND_SHORT_NAME = BRAND_BUNDLE.GetStringFromName("brandShortName");
+
+let nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo,
+ "init"
+);
+let login1 = new nsLoginInfo(
+ "http://example.com",
+ "http://example.com",
+ null,
+ "notifyu1",
+ "notifyp1",
+ "user",
+ "pass"
+);
+let login2 = new nsLoginInfo(
+ "http://example.com",
+ "http://example.com",
+ null,
+ "",
+ "notifyp1",
+ "",
+ "pass"
+);
+let login1B = new nsLoginInfo(
+ "http://example.com",
+ "http://example.com",
+ null,
+ "notifyu1B",
+ "notifyp1B",
+ "user",
+ "pass"
+);
+let login2B = new nsLoginInfo(
+ "http://example.com",
+ "http://example.com",
+ null,
+ "",
+ "notifyp1B",
+ "",
+ "pass"
+);
+
+requestLongerTimeout(2);
+
+add_setup(async function () {
+ // Load recipes for this test.
+ let recipeParent = await LoginManagerParent.recipeParentPromise;
+ await recipeParent.load({
+ siteRecipes: [
+ {
+ hosts: ["example.org"],
+ usernameSelector: "#user",
+ passwordSelector: "#pass",
+ },
+ ],
+ });
+});
+
+add_task(async function test_remember_opens() {
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_1.html",
+ async function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ await cleanupDoorhanger(notif);
+ }
+ );
+});
+
+add_task(async function test_clickNever() {
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_1.html",
+ async function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ Assert.ok(notif, "got notification popup");
+ Assert.equal(
+ true,
+ Services.logins.getLoginSavingEnabled("http://example.com"),
+ "Checking for login saving enabled"
+ );
+
+ await checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ clickDoorhangerButton(notif, NEVER_MENUITEM);
+ await cleanupDoorhanger(notif);
+ }
+ );
+
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 0,
+ "Should not have any logins yet"
+ );
+
+ info("Make sure Never took effect");
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_1.html",
+ function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = getCaptureDoorhanger("password-save");
+ Assert.ok(!notif, "checking for no notification popup");
+ Assert.equal(
+ false,
+ Services.logins.getLoginSavingEnabled("http://example.com"),
+ "Checking for login saving disabled"
+ );
+ Services.logins.setLoginSavingEnabled("http://example.com", true);
+ }
+ );
+
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 0,
+ "Should not have any logins yet"
+ );
+});
+
+add_task(async function test_clickRemember() {
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_1.html",
+ async function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 0,
+ "Should not have any logins yet"
+ );
+
+ await checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ let promiseNewSavedPassword = TestUtils.topicObserved(
+ "LoginStats:NewSavedPassword",
+ (subject, data) => subject == gBrowser.selectedBrowser
+ );
+ clickDoorhangerButton(notif, REMEMBER_BUTTON);
+ await promiseNewSavedPassword;
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(
+ login.username,
+ "notifyu1",
+ "Check the username used on the new entry"
+ );
+ Assert.equal(
+ login.password,
+ "notifyp1",
+ "Check the password used on the new entry"
+ );
+ Assert.equal(login.timesUsed, 1, "Check times used on new entry");
+
+ info(
+ "Make sure Remember took effect and we don't prompt for an existing login"
+ );
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_1.html",
+ function (fieldValues) {
+ // form login matches a saved login, we don't expect a notification on change or submit
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = getCaptureDoorhanger("password-save");
+ Assert.ok(!notif, "checking for no notification popup");
+ }
+ );
+
+ logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "notifyu1", "Check the username used");
+ Assert.equal(login.password, "notifyp1", "Check the password used");
+ Assert.equal(login.timesUsed, 2, "Check times used incremented");
+
+ checkOnlyLoginWasUsedTwice({ justChanged: false });
+
+ // remove that login
+ Services.logins.removeLogin(login1);
+ await cleanupDoorhanger();
+});
+
+/* signons.rememberSignons pref tests... */
+
+add_task(async function test_rememberSignonsFalse() {
+ info("Make sure we don't prompt with rememberSignons=false");
+ Services.prefs.setBoolPref("signon.rememberSignons", false);
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_1.html",
+ function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = getCaptureDoorhanger("password-save");
+ Assert.ok(!notif, "checking for no notification popup");
+ }
+ );
+
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 0,
+ "Should not have any logins yet"
+ );
+});
+
+add_task(async function test_rememberSignonsTrue() {
+ info("Make sure we prompt with rememberSignons=true");
+ Services.prefs.setBoolPref("signon.rememberSignons", true);
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_1.html",
+ async function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ await cleanupDoorhanger(notif);
+ }
+ );
+
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 0,
+ "Should not have any logins yet"
+ );
+});
+
+/* autocomplete=off tests... */
+
+add_task(async function test_autocompleteOffUsername() {
+ info(
+ "Check for notification popup when autocomplete=off present on username"
+ );
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_2.html",
+ async function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(notif, "checking for notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ await cleanupDoorhanger(notif);
+ }
+ );
+
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 0,
+ "Should not have any logins yet"
+ );
+});
+
+add_task(async function test_autocompleteOffPassword() {
+ info(
+ "Check for notification popup when autocomplete=off present on password"
+ );
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_3.html",
+ async function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(notif, "checking for notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ await cleanupDoorhanger(notif);
+ }
+ );
+
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 0,
+ "Should not have any logins yet"
+ );
+});
+
+add_task(async function test_autocompleteOffForm() {
+ info("Check for notification popup when autocomplete=off present on form");
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_4.html",
+ async function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(notif, "checking for notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ await cleanupDoorhanger(notif);
+ }
+ );
+
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 0,
+ "Should not have any logins yet"
+ );
+});
+
+add_task(async function test_noPasswordField() {
+ info("Check for no notification popup when no password field present");
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_5.html",
+ function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(fieldValues.password, "null", "Checking submitted password");
+ let notif = getCaptureDoorhanger("password-save");
+ Assert.ok(!notif, "checking for no notification popup");
+ }
+ );
+
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 0,
+ "Should not have any logins yet"
+ );
+});
+
+add_task(async function test_pwOnlyNewLoginMatchesUPForm() {
+ info("Check for update popup when new existing pw-only login matches form.");
+ await Services.logins.addLoginAsync(login2);
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_1.html",
+ async function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-change");
+ Assert.ok(notif, "checking for notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ Assert.equal(
+ notif.message,
+ "Add username to saved password?",
+ "Check message"
+ );
+
+ let { panel } = PopupNotifications;
+ let passwordVisiblityToggle = panel.querySelector(
+ "#password-notification-visibilityToggle"
+ );
+ Assert.ok(
+ !passwordVisiblityToggle.hidden,
+ "Toggle visible for a recently saved pw"
+ );
+
+ await checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+
+ Assert.ok(
+ !getCaptureDoorhanger("password-change"),
+ "popup should be gone"
+ );
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "notifyu1", "Check the username");
+ Assert.equal(login.password, "notifyp1", "Check the password");
+ Assert.equal(login.timesUsed, 2, "Check times used");
+
+ Services.logins.removeLogin(login);
+});
+
+add_task(async function test_pwOnlyOldLoginMatchesUPForm() {
+ info("Check for update popup when old existing pw-only login matches form.");
+ await Services.logins.addLoginAsync(login2);
+
+ // Change the timePasswordChanged to be old so that the password won't be
+ // revealed in the doorhanger.
+ let oldTimeMS = new Date("2009-11-15").getTime();
+ Services.logins.modifyLogin(
+ login2,
+ LoginHelper.newPropertyBag({
+ timeCreated: oldTimeMS,
+ timeLastUsed: oldTimeMS + 1,
+ timePasswordChanged: oldTimeMS,
+ })
+ );
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_1.html",
+ async function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-change");
+ Assert.ok(notif, "checking for notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ Assert.equal(
+ notif.message,
+ "Add username to saved password?",
+ "Check message"
+ );
+
+ let { panel } = PopupNotifications;
+ let passwordVisiblityToggle = panel.querySelector(
+ "#password-notification-visibilityToggle"
+ );
+ Assert.ok(
+ passwordVisiblityToggle.hidden,
+ "Toggle hidden for an old saved pw"
+ );
+
+ await checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+
+ Assert.ok(
+ !getCaptureDoorhanger("password-change"),
+ "popup should be gone"
+ );
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "notifyu1", "Check the username");
+ Assert.equal(login.password, "notifyp1", "Check the password");
+ Assert.equal(login.timesUsed, 2, "Check times used");
+
+ Services.logins.removeLogin(login);
+});
+
+add_task(async function test_pwOnlyFormMatchesLogin() {
+ info(
+ "Check for no notification popup when pw-only form matches existing login."
+ );
+ await Services.logins.addLoginAsync(login1);
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_6.html",
+ function (fieldValues) {
+ Assert.equal(fieldValues.username, "null", "Checking submitted username");
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = getCaptureDoorhanger("password-save");
+ Assert.ok(!notif, "checking for no notification popup");
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "notifyu1", "Check the username");
+ Assert.equal(login.password, "notifyp1", "Check the password");
+ Assert.equal(login.timesUsed, 2, "Check times used");
+
+ Services.logins.removeLogin(login1);
+});
+
+add_task(async function test_pwOnlyFormDoesntMatchExisting() {
+ info(
+ "Check for notification popup when pw-only form doesn't match existing login."
+ );
+ await Services.logins.addLoginAsync(login1B);
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_6.html",
+ async function (fieldValues) {
+ Assert.equal(fieldValues.username, "null", "Checking submitted username");
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ await cleanupDoorhanger(notif);
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "notifyu1B", "Check the username unchanged");
+ Assert.equal(login.password, "notifyp1B", "Check the password unchanged");
+ Assert.equal(login.timesUsed, 1, "Check times used");
+
+ Services.logins.removeLogin(login1B);
+});
+
+add_task(async function test_changeUPLoginOnUPForm_dont() {
+ info("Check for change-password popup, u+p login on u+p form. (not changed)");
+ await Services.logins.addLoginAsync(login1);
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_8.html",
+ async function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "pass2",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-change");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ Assert.equal(
+ notif.message,
+ "Update login for example.com?",
+ "Check message"
+ );
+
+ await checkDoorhangerUsernamePassword("notifyu1", "pass2");
+ clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "notifyu1", "Check the username unchanged");
+ Assert.equal(login.password, "notifyp1", "Check the password unchanged");
+ Assert.equal(login.timesUsed, 1, "Check times used");
+
+ Services.logins.removeLogin(login1);
+});
+
+add_task(async function test_changeUPLoginOnUPForm_remove() {
+ info("Check for change-password popup, u+p login on u+p form. (remove)");
+ await Services.logins.addLoginAsync(login1);
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_8.html",
+ async function (fieldValues, browser) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "pass2",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-change");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ Assert.equal(
+ notif.message,
+ "Update login for example.com?",
+ "Check message"
+ );
+
+ await checkDoorhangerUsernamePassword("notifyu1", "pass2");
+ clickDoorhangerButton(notif, REMOVE_LOGIN_MENUITEM);
+
+ // Let the hint hide itself
+ const forceClosePopup = false;
+ // Make sure confirmation hint was shown
+ info("waiting for verifyConfirmationHint");
+ await verifyConfirmationHint(browser, forceClosePopup, "identity-icon");
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 0, "Should have 0 logins");
+});
+
+add_task(async function test_changeUPLoginOnUPForm_change() {
+ info("Check for change-password popup, u+p login on u+p form.");
+ await Services.logins.addLoginAsync(login1);
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_8.html",
+ async function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "pass2",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-change");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ Assert.equal(
+ notif.message,
+ "Update login for example.com?",
+ "Check message"
+ );
+
+ await checkDoorhangerUsernamePassword("notifyu1", "pass2");
+ let promiseLoginUpdateSaved = TestUtils.topicObserved(
+ "LoginStats:LoginUpdateSaved",
+ (subject, data) => subject == gBrowser.selectedBrowser
+ );
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+ await promiseLoginUpdateSaved;
+
+ Assert.ok(
+ !getCaptureDoorhanger("password-change"),
+ "popup should be gone"
+ );
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "notifyu1", "Check the username unchanged");
+ Assert.equal(login.password, "pass2", "Check the password changed");
+ Assert.equal(login.timesUsed, 2, "Check times used");
+
+ checkOnlyLoginWasUsedTwice({ justChanged: true });
+
+ // cleanup
+ login1.password = "pass2";
+ Services.logins.removeLogin(login1);
+ login1.password = "notifyp1";
+});
+
+add_task(async function test_changePLoginOnUPForm() {
+ info("Check for change-password popup, p-only login on u+p form (empty u).");
+ await Services.logins.addLoginAsync(login2);
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_9.html",
+ async function (fieldValues) {
+ Assert.equal(fieldValues.username, "", "Checking submitted username");
+ Assert.equal(
+ fieldValues.password,
+ "pass2",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-change");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ Assert.equal(
+ notif.message,
+ "Update password for example.com?",
+ "Check msg"
+ );
+
+ await checkDoorhangerUsernamePassword("", "pass2");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+
+ Assert.ok(
+ !getCaptureDoorhanger("password-change"),
+ "popup should be gone"
+ );
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "", "Check the username unchanged");
+ Assert.equal(login.password, "pass2", "Check the password changed");
+ Assert.equal(login.timesUsed, 2, "Check times used");
+
+ // no cleanup -- saved password to be used in the next test.
+});
+
+add_task(async function test_changePLoginOnPForm() {
+ info("Check for change-password popup, p-only login on p-only form.");
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_10.html",
+ async function (fieldValues) {
+ Assert.equal(fieldValues.username, "null", "Checking submitted username");
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-change");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ Assert.equal(
+ notif.message,
+ "Update password for example.com?",
+ "Check msg"
+ );
+
+ await checkDoorhangerUsernamePassword("", "notifyp1");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+
+ Assert.ok(
+ !getCaptureDoorhanger("password-change"),
+ "popup should be gone"
+ );
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "", "Check the username unchanged");
+ Assert.equal(login.password, "notifyp1", "Check the password changed");
+ Assert.equal(login.timesUsed, 3, "Check times used");
+
+ Services.logins.removeLogin(login2);
+});
+
+add_task(async function test_checkUPSaveText() {
+ info("Check text on a user+pass notification popup");
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_1.html",
+ async function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ Assert.ok(notif, "got notification popup");
+ // Check the text, which comes from the localized saveLoginMsg string.
+ let notificationText = notif.message;
+ let expectedText = "Save login for example.com?";
+ Assert.equal(
+ notificationText,
+ expectedText,
+ "Checking text: " + notificationText
+ );
+ await cleanupDoorhanger(notif);
+ }
+ );
+
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 0,
+ "Should not have any logins yet"
+ );
+});
+
+add_task(async function test_checkPSaveText() {
+ info("Check text on a pass-only notification popup");
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_6.html",
+ async function (fieldValues) {
+ Assert.equal(fieldValues.username, "null", "Checking submitted username");
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ Assert.ok(notif, "got notification popup");
+ // Check the text, which comes from the localized saveLoginMsgNoUser string.
+ let notificationText = notif.message;
+ let expectedText = "Save password for example.com?";
+ Assert.equal(
+ notificationText,
+ expectedText,
+ "Checking text: " + notificationText
+ );
+ await cleanupDoorhanger(notif);
+ }
+ );
+
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 0,
+ "Should not have any logins yet"
+ );
+});
+
+add_task(async function test_capture2pw0un() {
+ info(
+ "Check for notification popup when a form with 2 password fields (no username) " +
+ "is submitted and there are no saved logins."
+ );
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_2pw_0un.html",
+ async function (fieldValues) {
+ Assert.equal(fieldValues.username, "null", "Checking submitted username");
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ Assert.ok(notif, "got notification popup");
+ await cleanupDoorhanger(notif);
+ }
+ );
+
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 0,
+ "Should not have any logins yet"
+ );
+});
+
+add_task(async function test_change2pw0unExistingDifferentUP() {
+ info(
+ "Check for notification popup when a form with 2 password fields (no username) " +
+ "is submitted and there is a saved login with a username and different password."
+ );
+
+ await Services.logins.addLoginAsync(login1B);
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_2pw_0un.html",
+ async function (fieldValues) {
+ Assert.equal(fieldValues.username, "null", "Checking submitted username");
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-change");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ await cleanupDoorhanger(notif);
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "notifyu1B", "Check the username unchanged");
+ Assert.equal(login.password, "notifyp1B", "Check the password unchanged");
+ Assert.equal(login.timesUsed, 1, "Check times used");
+
+ Services.logins.removeLogin(login1B);
+});
+
+add_task(async function test_change2pw0unExistingDifferentP() {
+ info(
+ "Check for notification popup when a form with 2 password fields (no username) " +
+ "is submitted and there is a saved login with no username and different password."
+ );
+
+ await Services.logins.addLoginAsync(login2B);
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_2pw_0un.html",
+ async function (fieldValues) {
+ Assert.equal(fieldValues.username, "null", "Checking submitted username");
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-change");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ await cleanupDoorhanger(notif);
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "", "Check the username unchanged");
+ Assert.equal(login.password, "notifyp1B", "Check the password unchanged");
+ Assert.equal(login.timesUsed, 1, "Check times used");
+
+ Services.logins.removeLogin(login2B);
+});
+
+add_task(async function test_change2pw0unExistingWithSameP() {
+ info(
+ "Check for no notification popup when a form with 2 password fields (no username) " +
+ "is submitted and there is a saved login with a username and the same password."
+ );
+
+ await Services.logins.addLoginAsync(login2);
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_2pw_0un.html",
+ function (fieldValues) {
+ Assert.equal(fieldValues.username, "null", "Checking submitted username");
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = getCaptureDoorhanger("password-change");
+ Assert.ok(!notif, "checking for no notification popup");
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "", "Check the username unchanged");
+ Assert.equal(login.password, "notifyp1", "Check the password unchanged");
+ Assert.equal(login.timesUsed, 2, "Check times used incremented");
+
+ checkOnlyLoginWasUsedTwice({ justChanged: false });
+
+ Services.logins.removeLogin(login2);
+});
+
+add_task(async function test_changeUPLoginOnPUpdateForm() {
+ info("Check for change-password popup, u+p login on password update form.");
+ await Services.logins.addLoginAsync(login1);
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_change_p.html",
+ async function (fieldValues) {
+ Assert.equal(fieldValues.username, "null", "Checking submitted username");
+ Assert.equal(
+ fieldValues.password,
+ "pass2",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-change");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+
+ await checkDoorhangerUsernamePassword("notifyu1", "pass2");
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+
+ Assert.ok(
+ !getCaptureDoorhanger("password-change"),
+ "popup should be gone"
+ );
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "notifyu1", "Check the username unchanged");
+ Assert.equal(login.password, "pass2", "Check the password changed");
+ Assert.equal(login.timesUsed, 2, "Check times used");
+
+ checkOnlyLoginWasUsedTwice({ justChanged: true });
+
+ // cleanup
+ login1.password = "pass2";
+ Services.logins.removeLogin(login1);
+ login1.password = "notifyp1";
+});
+
+add_task(async function test_recipeCaptureFields_NewLogin() {
+ info(
+ "Check that we capture the proper fields when a field recipe is in use."
+ );
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_2pw_1un_1text.html",
+ async function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+
+ // Sanity check, no logins should exist yet.
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 0, "Should not have any logins yet");
+
+ await checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ clickDoorhangerButton(notif, REMEMBER_BUTTON);
+ },
+ "http://example.org"
+ ); // The recipe is for example.org
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "notifyu1", "Check the username unchanged");
+ Assert.equal(login.password, "notifyp1", "Check the password unchanged");
+ Assert.equal(login.timesUsed, 1, "Check times used");
+});
+
+add_task(async function test_recipeCaptureFields_ExistingLogin() {
+ info(
+ "Check that we capture the proper fields when a field recipe is in use " +
+ "and there is a matching login"
+ );
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_2pw_1un_1text.html",
+ function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = getCaptureDoorhanger("password-save");
+ Assert.ok(!notif, "checking for no notification popup");
+ },
+ "http://example.org"
+ );
+
+ checkOnlyLoginWasUsedTwice({ justChanged: false });
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "notifyu1", "Check the username unchanged");
+ Assert.equal(login.password, "notifyp1", "Check the password unchanged");
+ Assert.equal(login.timesUsed, 2, "Check times used incremented");
+
+ Services.logins.removeAllUserFacingLogins();
+});
+
+add_task(async function test_saveUsingEnter() {
+ async function testWithTextboxSelector(fieldSelector) {
+ let storageChangedPromise = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == "addLogin"
+ );
+
+ info("Waiting for form submit and doorhanger interaction");
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_1.html",
+ async function (fieldValues) {
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(notif, "got notification popup");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 0,
+ "Should not have any logins yet"
+ );
+ await checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ let notificationElement = PopupNotifications.panel.childNodes[0];
+ let textbox = notificationElement.querySelector(fieldSelector);
+ textbox.focus();
+ await EventUtils.synthesizeKey("KEY_Enter");
+ }
+ );
+ await storageChangedPromise;
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(
+ login.username,
+ "notifyu1",
+ "Check the username used on the new entry"
+ );
+ Assert.equal(
+ login.password,
+ "notifyp1",
+ "Check the password used on the new entry"
+ );
+ Assert.equal(login.timesUsed, 1, "Check times used on new entry");
+
+ Services.logins.removeAllUserFacingLogins();
+ }
+
+ await testWithTextboxSelector("#password-notification-password");
+ await testWithTextboxSelector("#password-notification-username");
+});
+
+add_task(async function test_noShowPasswordOnDismissal() {
+ info("Check for no Show Password field when the doorhanger is dismissed");
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_1.html",
+ async function (fieldValues) {
+ info("Opening popup");
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ let { panel } = PopupNotifications;
+
+ info("Hiding popup.");
+ let promiseHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ panel.hidePopup();
+ await promiseHidden;
+
+ info("Clicking on anchor to reshow popup.");
+ let promiseShown = BrowserTestUtils.waitForEvent(panel, "popupshown");
+ notif.anchorElement.click();
+ await promiseShown;
+
+ let passwordVisiblityToggle = panel.querySelector(
+ "#password-notification-visibilityToggle"
+ );
+ Assert.equal(
+ passwordVisiblityToggle.hidden,
+ true,
+ "Check that the Show Password field is Hidden"
+ );
+ await cleanupDoorhanger(notif);
+ }
+ );
+});
+
+add_task(async function test_showPasswordOn1stOpenOfDismissedByDefault() {
+ info("Show Password toggle when the doorhanger is dismissed by default");
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_1.html",
+ async function (fieldValues) {
+ info("Opening popup");
+ let notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(!notif.dismissed, "doorhanger is not dismissed");
+ let { panel } = PopupNotifications;
+
+ info("Hiding popup.");
+ let promiseHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ panel.hidePopup();
+ await promiseHidden;
+
+ info("Clicking on anchor to reshow popup.");
+ let promiseShown = BrowserTestUtils.waitForEvent(panel, "popupshown");
+ notif.anchorElement.click();
+ await promiseShown;
+
+ let passwordVisiblityToggle = panel.querySelector(
+ "#password-notification-visibilityToggle"
+ );
+ Assert.equal(
+ passwordVisiblityToggle.hidden,
+ true,
+ "Check that the Show Password field is Hidden"
+ );
+ await cleanupDoorhanger(notif);
+ }
+ );
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_replace_dismissed_with_visible_while_opening.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_replace_dismissed_with_visible_while_opening.js
new file mode 100644
index 0000000000..401e0add1b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_replace_dismissed_with_visible_while_opening.js
@@ -0,0 +1,65 @@
+/**
+ * Replacing a dismissed doorhanger with a visible one while it's opening.
+ *
+ * There are various races between popup notification callbacks to catch with this.
+ * This can happen in the real world by blurring an edited login field by clicking on the login doorhanger.
+ */
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "prompterSvc",
+ "@mozilla.org/login-manager/prompter;1",
+ Ci.nsILoginManagerPrompter
+);
+
+add_task(async function test_replaceDismissedWithVisibleWhileOpening() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "https://example.com",
+ },
+ async function load(browser) {
+ info("Show a dismissed save doorhanger");
+ prompterSvc.promptToSavePassword(
+ browser,
+ LoginTestUtils.testData.formLogin({}),
+ true,
+ false,
+ null
+ );
+ let doorhanger = await waitForDoorhanger(browser, "password-save");
+ Assert.ok(doorhanger, "Got doorhanger");
+ EventUtils.synthesizeMouseAtCenter(doorhanger.anchorElement, {});
+ await BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshowing"
+ );
+ await checkDoorhangerUsernamePassword("the username", "the password");
+ info(
+ "Replace the doorhanger with a non-dismissed one immediately after clicking to open"
+ );
+ prompterSvc.promptToSavePassword(
+ browser,
+ LoginTestUtils.testData.formLogin({}),
+ true,
+ false,
+ null
+ );
+ await Promise.race([
+ BrowserTestUtils.waitForCondition(() => {
+ if (
+ document.getElementById("password-notification-username").value !=
+ "the username" ||
+ document.getElementById("password-notification-password").value !=
+ "the password"
+ ) {
+ return Promise.reject("Field changed to incorrect values");
+ }
+ return false;
+ }, "See if username/password values change to incorrect values"),
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ new Promise(resolve => setTimeout(resolve, 1000)),
+ ]);
+ }
+ );
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_save_password.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_save_password.js
new file mode 100644
index 0000000000..5401ff31af
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_save_password.js
@@ -0,0 +1,159 @@
+/**
+ * Test that the doorhanger notification for password saving is populated with
+ * the correct values in various password capture cases.
+ */
+
+const testCases = [
+ {
+ name: "No saved logins, username and password",
+ username: "username",
+ password: "password",
+ expectOutcome: [
+ {
+ username: "username",
+ password: "password",
+ },
+ ],
+ },
+ {
+ name: "No saved logins, password with empty username",
+ username: "",
+ password: "password",
+ expectOutcome: [
+ {
+ username: "",
+ password: "password",
+ },
+ ],
+ },
+ {
+ name: "Saved login with username, update password",
+ username: "username",
+ oldPassword: "password",
+ password: "newPassword",
+ expectOutcome: [
+ {
+ username: "username",
+ password: "newPassword",
+ },
+ ],
+ },
+ {
+ name: "Saved login with no username, update password",
+ username: "",
+ oldPassword: "password",
+ password: "newPassword",
+ expectOutcome: [
+ {
+ username: "",
+ password: "newPassword",
+ },
+ ],
+ },
+ {
+ name: "Saved login with no username, add username and different password",
+ oldUsername: "",
+ username: "username",
+ oldPassword: "password",
+ password: "newPassword",
+ expectOutcome: [
+ {
+ username: "",
+ password: "password",
+ },
+ {
+ username: "username",
+ password: "newPassword",
+ },
+ ],
+ },
+];
+
+for (let testData of testCases) {
+ let tmp = {
+ async [testData.name]() {
+ info("testing with: " + JSON.stringify(testData));
+ await test_save_change(testData);
+ },
+ };
+ add_task(tmp[testData.name]);
+}
+
+async function test_save_change(testData) {
+ let { oldUsername, username, oldPassword, password, expectOutcome } =
+ testData;
+ // Add a login for the origin of the form if testing a change notification.
+ if (oldPassword) {
+ await Services.logins.addLoginAsync(
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: typeof oldUsername !== "undefined" ? oldUsername : username,
+ password: oldPassword,
+ })
+ );
+ }
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+
+ // Update the form with credentials from the test case.
+ info(`update form with username: ${username}, password: ${password}`);
+ await changeContentFormValues(browser, {
+ "#form-basic-username": username,
+ "#form-basic-password": password,
+ });
+
+ // Submit the form with the new credentials. This will cause the doorhanger
+ // notification to be displayed.
+ let formSubmittedPromise = listenForTestNotification("ShowDoorhanger");
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = this.content.document;
+ doc.getElementById("form-basic").submit();
+ });
+ await formSubmittedPromise;
+
+ // Simulate the action on the notification to request the login to be
+ // saved, and wait for the data to be updated or saved based on the type
+ // of operation we expect.
+ let expectedNotification, expectedDoorhanger;
+ if (oldPassword !== undefined && oldUsername !== undefined) {
+ expectedNotification = "addLogin";
+ expectedDoorhanger = "password-save";
+ } else if (oldPassword !== undefined) {
+ expectedNotification = "modifyLogin";
+ expectedDoorhanger = "password-change";
+ } else {
+ expectedNotification = "addLogin";
+ expectedDoorhanger = "password-save";
+ }
+
+ info("Waiting for doorhanger of type: " + expectedDoorhanger);
+ let notif = await waitForDoorhanger(browser, expectedDoorhanger);
+
+ // Check the actual content of the popup notification.
+ await checkDoorhangerUsernamePassword(username, password);
+
+ let promiseLogin = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == expectedNotification
+ );
+ await clickDoorhangerButton(notif, REMEMBER_BUTTON);
+ await promiseLogin;
+ await cleanupDoorhanger(notif); // clean slate for the next test
+
+ // Check that the values in the database match the expected values.
+ verifyLogins(expectOutcome);
+ }
+ );
+
+ // Clean up the database before the next test case is executed.
+ Services.logins.removeAllUserFacingLogins();
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_submit_telemetry.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_submit_telemetry.js
new file mode 100644
index 0000000000..97353007ac
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_submit_telemetry.js
@@ -0,0 +1,387 @@
+/**
+ * Test that doorhanger submit telemetry is sent when the user saves/updates.
+ */
+
+add_setup(function () {
+ // This test used to rely on the initial timer of
+ // TestUtils.waitForCondition. See bug 1695395.
+ // The test is perma-fail on Linux asan opt without this.
+ let originalWaitForCondition = TestUtils.waitForCondition;
+ TestUtils.waitForCondition = async function (
+ condition,
+ msg,
+ interval = 100,
+ maxTries = 50
+ ) {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ return originalWaitForCondition(condition, msg, interval, maxTries);
+ };
+ registerCleanupFunction(function () {
+ TestUtils.waitForCondition = originalWaitForCondition;
+ });
+});
+
+const PAGE_USERNAME_SELECTOR = "#form-basic-username";
+const PAGE_PASSWORD_SELECTOR = "#form-basic-password";
+
+const TEST_CASES = [
+ {
+ description:
+ "Saving a new login from page values without modification sends a 'no modification' event",
+ savedLogin: undefined,
+ userActions: [
+ {
+ pageChanges: {
+ username: "pageUn",
+ password: "pagePw",
+ },
+ doorhangerChanges: [],
+ },
+ ],
+ expectedEvents: [
+ {
+ type: "save",
+ ping: {
+ did_edit_un: "false",
+ did_select_un: "false",
+ did_edit_pw: "false",
+ did_select_pw: "false",
+ },
+ },
+ ],
+ },
+ /////////////////
+ {
+ description: "Saving two logins sends two events",
+ userActions: [
+ {
+ pageChanges: { password: "pagePw" },
+ doorhangerChanges: [
+ {
+ typedUsername: "doorhangerUn",
+ },
+ ],
+ },
+ {
+ pageChanges: { password: "pagePw2" },
+ doorhangerChanges: [
+ {
+ typedPassword: "doorhangerPw",
+ },
+ ],
+ },
+ ],
+ expectedEvents: [
+ {
+ type: "save",
+ ping: {
+ did_edit_un: "true",
+ did_select_un: "false",
+ did_edit_pw: "false",
+ did_select_pw: "false",
+ },
+ },
+ {
+ type: "update",
+ ping: {
+ did_edit_un: "false",
+ did_select_un: "false",
+ did_edit_pw: "true",
+ did_select_pw: "false",
+ },
+ },
+ ],
+ },
+ /////////////////
+ {
+ description: "Updating a doorhanger password sends a 'pw updated' event",
+ savedLogin: {
+ username: "savedUn",
+ password: "savedPw",
+ },
+ userActions: [
+ {
+ pageChanges: { password: "pagePw" },
+ doorhangerChanges: [
+ {
+ typedPassword: "doorhangerPw",
+ },
+ ],
+ },
+ ],
+ expectedEvents: [
+ {
+ type: "update",
+ ping: {
+ did_edit_un: "false",
+ did_select_un: "false",
+ did_edit_pw: "true",
+ did_select_pw: "false",
+ },
+ },
+ ],
+ },
+ /////////////////
+ {
+ description:
+ "Saving a new username with an existing password sends a 'un updated' event",
+ savedLogin: {
+ username: "savedUn",
+ password: "savedPw",
+ },
+ userActions: [
+ {
+ pageChanges: { password: "pagePw" },
+ doorhangerChanges: [
+ {
+ typedUsername: "doorhangerUn",
+ },
+ ],
+ },
+ ],
+ expectedEvents: [
+ {
+ type: "update",
+ ping: {
+ did_edit_un: "true",
+ did_select_un: "false",
+ did_edit_pw: "false",
+ did_select_pw: "false",
+ },
+ },
+ ],
+ },
+ ///////////////
+ {
+ description: "selecting a saved username sends a 'not edited' event",
+ savedLogin: {
+ username: "savedUn",
+ password: "savedPw",
+ },
+ userActions: [
+ {
+ pageChanges: { password: "pagePw" },
+ doorhangerChanges: [
+ {
+ selectUsername: "savedUn",
+ },
+ ],
+ },
+ ],
+ expectedEvents: [
+ {
+ type: "update",
+ ping: {
+ did_edit_un: "false",
+ did_select_un: "true",
+ did_edit_pw: "false",
+ did_select_pw: "false",
+ },
+ },
+ ],
+ },
+ /////////////////
+ {
+ description:
+ "typing a new username then selecting a saved username sends a 'not edited' event",
+ savedLogin: {
+ username: "savedUn",
+ password: "savedPw",
+ },
+ userActions: [
+ {
+ pageChanges: { password: "pagePw" },
+ doorhangerChanges: [
+ {
+ typedUsername: "doorhangerTypedUn",
+ },
+ {
+ selectUsername: "savedUn",
+ },
+ ],
+ },
+ ],
+ expectedEvents: [
+ {
+ type: "update",
+ ping: {
+ did_edit_un: "false",
+ did_select_un: "true",
+ did_edit_pw: "false",
+ did_select_pw: "false",
+ },
+ },
+ ],
+ },
+ /////////////////
+ {
+ description:
+ "selecting a saved username then typing a new username sends an 'edited' event",
+ savedLogin: {
+ username: "savedUn",
+ password: "savedPw",
+ },
+ userActions: [
+ {
+ pageChanges: { password: "pagePw" },
+ doorhangerChanges: [
+ {
+ selectUsername: "savedUn",
+ },
+ {
+ typedUsername: "doorhangerTypedUn",
+ },
+ ],
+ },
+ ],
+ expectedEvents: [
+ {
+ type: "update",
+ ping: {
+ did_edit_un: "true",
+ did_select_un: "false",
+ did_edit_pw: "false",
+ did_select_pw: "false",
+ },
+ },
+ ],
+ },
+ /////////////////
+];
+
+for (let testData of TEST_CASES) {
+ let tmp = {
+ async [testData.description]() {
+ info("testing with: " + JSON.stringify(testData));
+ await test_submit_telemetry(testData);
+ },
+ };
+ add_task(tmp[testData.description]);
+}
+
+function _validateTestCase(tc) {
+ for (let event of tc.expectedEvents) {
+ Assert.ok(
+ !(event.ping.did_edit_un && event.ping.did_select_un),
+ "'did_edit_un' and 'did_select_un' can never be true at the same time"
+ );
+ Assert.ok(
+ !(event.ping.did_edit_pw && event.ping.did_select_pw),
+ "'did_edit_pw' and 'did_select_pw' can never be true at the same time"
+ );
+ }
+}
+
+async function test_submit_telemetry(tc) {
+ if (tc.savedLogin) {
+ await Services.logins.addLoginAsync(
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: tc.savedLogin.username,
+ password: tc.savedLogin.password,
+ })
+ );
+ }
+
+ let notif;
+ for (let userAction of tc.userActions) {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+
+ if (userAction.pageChanges) {
+ info(
+ `Update form with changes: ${JSON.stringify(
+ userAction.pageChanges
+ )}`
+ );
+ let changeTo = {};
+ if (userAction.pageChanges.username) {
+ changeTo[PAGE_USERNAME_SELECTOR] = userAction.pageChanges.username;
+ }
+ if (userAction.pageChanges.password) {
+ changeTo[PAGE_PASSWORD_SELECTOR] = userAction.pageChanges.password;
+ }
+
+ await changeContentFormValues(browser, changeTo);
+ }
+
+ info("Submitting form");
+ let formSubmittedPromise = listenForTestNotification("ShowDoorhanger");
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = this.content.document;
+ doc.getElementById("form-basic").submit();
+ });
+ await formSubmittedPromise;
+
+ let saveDoorhanger = waitForDoorhanger(browser, "password-save");
+ let updateDoorhanger = waitForDoorhanger(browser, "password-change");
+ notif = await Promise.race([saveDoorhanger, updateDoorhanger]);
+
+ if (PopupNotifications.panel.state !== "open") {
+ await BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ }
+
+ if (userAction.doorhangerChanges) {
+ for (let doorhangerChange of userAction.doorhangerChanges) {
+ if (
+ doorhangerChange.typedUsername ||
+ doorhangerChange.typedPassword
+ ) {
+ await updateDoorhangerInputValues({
+ username: doorhangerChange.typedUsername,
+ password: doorhangerChange.typedPassword,
+ });
+ }
+
+ if (doorhangerChange.selectUsername) {
+ await selectDoorhangerUsername(doorhangerChange.selectUsername);
+ }
+ if (doorhangerChange.selectPassword) {
+ await selectDoorhangerPassword(doorhangerChange.selectPassword);
+ }
+ }
+ }
+
+ info("Waiting for doorhanger");
+ await clickDoorhangerButton(notif, REMEMBER_BUTTON);
+ }
+ );
+ }
+
+ let expectedEvents = tc.expectedEvents.map(expectedEvent => [
+ "pwmgr",
+ "doorhanger_submitted",
+ expectedEvent.type,
+ null,
+ expectedEvent.ping,
+ ]);
+
+ await LoginTestUtils.telemetry.waitForEventCount(
+ expectedEvents.length,
+ "parent",
+ "pwmgr",
+ "doorhanger_submitted"
+ );
+ TelemetryTestUtils.assertEvents(
+ expectedEvents,
+ { category: "pwmgr", method: "doorhanger_submitted" },
+ { clear: true }
+ );
+
+ // Clean up the database before the next test case is executed.
+ await cleanupDoorhanger(notif);
+ Services.logins.removeAllUserFacingLogins();
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_target_blank.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_target_blank.js
new file mode 100644
index 0000000000..63ba1fa2a7
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_target_blank.js
@@ -0,0 +1,94 @@
+/**
+ * Test capture popup notifications when the login form uses target="_blank"
+ */
+
+add_setup(async function () {
+ await SimpleTest.promiseFocus(window);
+});
+
+add_task(async function test_saveTargetBlank() {
+ // This test submits the form to a new tab using target="_blank".
+ let url = "subtst_notifications_12_target_blank.html?notifyu3|notifyp3||";
+ let notifShownPromise = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ let submissionTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ url => {
+ info(url);
+ return url.includes("formsubmit.sjs");
+ },
+ false,
+ true
+ );
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "http://mochi.test:8888" + DIRECTORY_PATH + url,
+ },
+ async function () {
+ // For now the doorhanger appears in the previous tab but it should maybe
+ // appear in the new tab from target="_blank"?
+ BrowserTestUtils.removeTab(await submissionTabPromise);
+
+ let notif = await TestUtils.waitForCondition(
+ () =>
+ getCaptureDoorhangerThatMayOpen(
+ "password-save",
+ PopupNotifications,
+ gBrowser.selectedBrowser
+ ),
+ "Waiting for doorhanger"
+ );
+ Assert.ok(notif, "got notification popup");
+
+ EventUtils.synthesizeMouseAtCenter(notif.anchorElement, {});
+ await notifShownPromise;
+ await checkDoorhangerUsernamePassword("notifyu3", "notifyp3");
+ let storageChangedPromised = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (subject, data) => data != "removeLogin"
+ );
+
+ clickDoorhangerButton(notif, REMEMBER_BUTTON);
+ await storageChangedPromised;
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ );
+
+ // Check result of clicking Remember
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login now");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(
+ login.username,
+ "notifyu3",
+ "Check the username used on the new entry"
+ );
+ Assert.equal(
+ login.password,
+ "notifyp3",
+ "Check the password used on the new entry"
+ );
+ Assert.equal(login.timesUsed, 1, "Check times used on new entry");
+
+ // Check for stale values in the doorhanger <input> after closing.
+ let usernameField = document.getElementById("password-notification-username");
+ todo_is(
+ usernameField.value,
+ "",
+ "Check the username field doesn't have a stale value"
+ );
+ let passwordField = document.getElementById("password-notification-password");
+ todo_is(
+ passwordField.value,
+ "",
+ "Check the password field doesn't have a stale value"
+ );
+
+ // Cleanup
+ Services.logins.removeLogin(login);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_toggles.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_toggles.js
new file mode 100644
index 0000000000..f529369522
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_toggles.js
@@ -0,0 +1,478 @@
+/* eslint no-shadow:"off" */
+
+const passwordInputSelector = "#form-basic-password";
+const usernameInputSelector = "#form-basic-username";
+const FORM_URL =
+ "https://example.com/browser/toolkit/components/passwordmgr/test/browser/form_basic.html";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.rememberSignons.visibilityToggle", true]],
+ });
+});
+
+let testCases = [
+ {
+ /* Test that the doorhanger password field shows plain or * text
+ * when the checkbox is checked.
+ */
+ name: "test_toggle_password",
+ logins: [],
+ enabledPrimaryPassword: false,
+ formDefaults: {},
+ formChanges: {
+ [passwordInputSelector]: "pw",
+ [usernameInputSelector]: "username",
+ },
+ expected: {
+ initialForm: {
+ username: "",
+ password: "",
+ },
+ passwordChangedDoorhanger: null,
+ submitDoorhanger: {
+ type: "password-save",
+ dismissed: false,
+ username: "username",
+ password: "pw",
+ toggleVisible: true,
+ initialToggleState: {
+ inputType: "password",
+ toggleChecked: false,
+ },
+ afterToggleClick0: {
+ inputType: "text",
+ toggleChecked: true,
+ },
+ afterToggleClick1: {
+ inputType: "password",
+ toggleChecked: false,
+ },
+ },
+ },
+ },
+ {
+ /* Test that the doorhanger password toggle checkbox is disabled
+ * when the primary password is set.
+ */
+ name: "test_checkbox_disabled_if_has_primary_password",
+ logins: [],
+ enabledPrimaryPassword: true,
+ formDefaults: {},
+ formChanges: {
+ [passwordInputSelector]: "pass",
+ [usernameInputSelector]: "username",
+ },
+ expected: {
+ initialForm: {
+ username: "",
+ password: "",
+ },
+ passwordChangedDoorhanger: null,
+ submitDoorhanger: {
+ type: "password-save",
+ dismissed: false,
+ username: "username",
+ password: "pass",
+ toggleVisible: false,
+ initialToggleState: {
+ inputType: "password",
+ toggleChecked: false,
+ },
+ },
+ },
+ },
+ {
+ /* Test that the reveal password checkbox is hidden when editing the
+ * password of an autofilled login
+ */
+ name: "test_edit_autofilled_password",
+ logins: [{ username: "username1", password: "password" }],
+ formDefaults: {},
+ formChanges: {
+ [passwordInputSelector]: "password!",
+ },
+ expected: {
+ initialForm: {
+ username: "username1",
+ password: "password",
+ },
+ passwordChangedDoorhanger: {
+ type: "password-change",
+ dismissed: true,
+ username: "username1",
+ password: "password!",
+ toggleVisible: false,
+ initialToggleState: {
+ inputType: "password",
+ toggleChecked: false,
+ },
+ },
+ submitDoorhanger: {
+ type: "password-change",
+ dismissed: false,
+ username: "username1",
+ password: "password!",
+ toggleVisible: false,
+ initialToggleState: {
+ inputType: "password",
+ toggleChecked: false,
+ },
+ },
+ },
+ },
+ {
+ /* Test that the reveal password checkbox is shown when editing the
+ * password of a login that has been autofilled and then deleted
+ */
+ name: "test_autofilled_cleared_then_updated_password",
+ logins: [{ username: "username1", password: "password" }],
+ formDefaults: {},
+ formChanges: [
+ {
+ [passwordInputSelector]: "",
+ },
+ {
+ [passwordInputSelector]: "password!",
+ },
+ ],
+ expected: {
+ initialForm: {
+ username: "username1",
+ password: "password",
+ },
+ passwordChangedDoorhanger: {
+ type: "password-change",
+ dismissed: true,
+ username: "username1",
+ password: "password!",
+ toggleVisible: true,
+ initialToggleState: {
+ inputType: "password",
+ toggleChecked: false,
+ },
+ },
+ submitDoorhanger: {
+ type: "password-change",
+ dismissed: false,
+ username: "username1",
+ password: "password!",
+ toggleVisible: true,
+ initialToggleState: {
+ inputType: "password",
+ toggleChecked: false,
+ },
+ },
+ },
+ },
+ {
+ /* Test that the reveal password checkbox is hidden when editing the
+ * username of an autofilled login
+ */
+ name: "test_edit_autofilled_username",
+ logins: [{ username: "username1", password: "password" }],
+ formDefaults: {},
+ formChanges: {
+ [usernameInputSelector]: "username2",
+ },
+ expected: {
+ initialForm: {
+ username: "username1",
+ password: "password",
+ },
+ passwordChangedDoorhanger: {
+ type: "password-save",
+ dismissed: true,
+ username: "username2",
+ password: "password",
+ toggleVisible: false,
+ initialToggleState: {
+ inputType: "password",
+ toggleChecked: false,
+ },
+ },
+ submitDoorhanger: {
+ type: "password-save",
+ dismissed: false,
+ username: "username2",
+ password: "password",
+ toggleVisible: false,
+ initialToggleState: {
+ inputType: "password",
+ toggleChecked: false,
+ },
+ },
+ },
+ },
+];
+
+for (let testData of testCases) {
+ if (testData.skip) {
+ info("Skipping test:", testData.name);
+ continue;
+ }
+ let tmp = {
+ async [testData.name]() {
+ await testDoorhangerToggles(testData);
+ },
+ };
+ add_task(tmp[testData.name]);
+}
+
+/**
+ * Set initial test conditions,
+ * Load and populate the form,
+ * Submit it and verify doorhanger toggle behavior
+ */
+async function testDoorhangerToggles({
+ logins = [],
+ formDefaults = {},
+ formChanges = {},
+ expected,
+ enabledPrimaryPassword,
+}) {
+ formChanges = Array.isArray(formChanges) ? formChanges : [formChanges];
+
+ for (let login of logins) {
+ await LoginTestUtils.addLogin(login);
+ }
+ if (enabledPrimaryPassword) {
+ LoginTestUtils.primaryPassword.enable();
+ }
+ let formProcessedPromise = listenForTestNotification("FormProcessed");
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: FORM_URL,
+ },
+ async function (browser) {
+ info(`Opened tab with url: ${FORM_URL}, waiting for focus`);
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+ info("Waiting for form-processed message");
+ await formProcessedPromise;
+ await initForm(browser, formDefaults);
+ await checkForm(browser, expected.initialForm);
+ info("form checked");
+
+ // some tests check the dismissed doorhanger from editing the password
+ let formChanged = expected.passwordChangedDoorhanger
+ ? listenForTestNotification("PasswordEditedOrGenerated")
+ : Promise.resolve();
+ for (let change of formChanges) {
+ await changeContentFormValues(browser, change, {
+ method: "paste_text",
+ });
+ }
+
+ await formChanged;
+
+ if (expected.passwordChangedDoorhanger) {
+ let expectedDoorhanger = expected.passwordChangedDoorhanger;
+ info("Verifying dismissed doorhanger from password change");
+ let notif = await waitForDoorhanger(browser, expectedDoorhanger.type);
+ Assert.ok(notif, "got notification popup");
+ Assert.equal(
+ notif.dismissed,
+ expectedDoorhanger.dismissed,
+ "Check notification dismissed property"
+ );
+ let { panel } = browser.ownerGlobal.PopupNotifications;
+ // we will open dismissed doorhanger to check panel contents
+ Assert.equal(panel.state, "closed", "Panel is initially closed");
+ let promiseShown = BrowserTestUtils.waitForEvent(panel, "popupshown");
+ info("Opening the doorhanger popup");
+ // synthesize click on anchor as this also blurs the form field triggering
+ // a change event
+ EventUtils.synthesizeMouseAtCenter(notif.anchorElement, {});
+ await promiseShown;
+ await TestUtils.waitForTick();
+ Assert.ok(
+ panel.children.length,
+ `Check the popup has at least one notification (${panel.children.length})`
+ );
+
+ // Check the password-changed-capture doorhanger contents & behaviour
+ info("Verifying the doorhanger");
+ await verifyDoorhangerToggles(browser, notif, expectedDoorhanger);
+ await hideDoorhangerPopup(notif);
+ }
+
+ if (expected.submitDoorhanger) {
+ let expectedDoorhanger = expected.submitDoorhanger;
+ let { panel } = browser.ownerGlobal.PopupNotifications;
+ // submit the form and wait for the doorhanger
+ info("Submitting the form");
+ let submittedPromise = listenForTestNotification("ShowDoorhanger");
+ let promiseShown = BrowserTestUtils.waitForEvent(panel, "popupshown");
+ await submitForm(browser, "/");
+ await submittedPromise;
+ info("Waiting for doorhanger popup to open");
+ await promiseShown;
+ let notif = await getCaptureDoorhanger(expectedDoorhanger.type);
+ Assert.ok(notif, "got notification popup");
+ Assert.equal(
+ notif.dismissed,
+ expectedDoorhanger.dismissed,
+ "Check notification dismissed property"
+ );
+ Assert.ok(
+ panel.children.length,
+ `Check the popup has at least one notification (${panel.children.length})`
+ );
+
+ // Check the submit-capture doorhanger contents & behaviour
+ info("Verifying the submit doorhanger");
+ await verifyDoorhangerToggles(browser, notif, expectedDoorhanger);
+ await cleanupDoorhanger(notif);
+ }
+ }
+ );
+ await LoginTestUtils.clearData();
+ if (enabledPrimaryPassword) {
+ LoginTestUtils.primaryPassword.disable();
+ }
+ await cleanupPasswordNotifications();
+}
+
+// --------------------------------------------------------------------
+// Helpers
+
+async function verifyDoorhangerToggles(browser, notif, expected) {
+ let { initialToggleState, afterToggleClick0, afterToggleClick1 } = expected;
+
+ let { panel } = browser.ownerGlobal.PopupNotifications;
+ let notificationElement = panel.childNodes[0];
+ let passwordTextbox = notificationElement.querySelector(
+ "#password-notification-password"
+ );
+ let toggleCheckbox = notificationElement.querySelector(
+ "#password-notification-visibilityToggle"
+ );
+ Assert.equal(panel.state, "open", "Panel is open");
+ Assert.ok(
+ BrowserTestUtils.is_visible(passwordTextbox),
+ "The doorhanger password field is visible"
+ );
+
+ await checkDoorhangerUsernamePassword(expected.username, expected.password);
+ if (expected.toggleVisible) {
+ Assert.ok(
+ BrowserTestUtils.is_visible(toggleCheckbox),
+ "The visibility checkbox is shown"
+ );
+ } else {
+ Assert.ok(
+ BrowserTestUtils.is_hidden(toggleCheckbox),
+ "The visibility checkbox is hidden"
+ );
+ }
+
+ if (initialToggleState) {
+ Assert.equal(
+ toggleCheckbox.checked,
+ initialToggleState.toggleChecked,
+ `Initially, toggle is ${
+ initialToggleState.toggleChecked ? "checked" : "unchecked"
+ }`
+ );
+ Assert.equal(
+ passwordTextbox.type,
+ initialToggleState.inputType,
+ `Initially, password input has type: ${initialToggleState.inputType}`
+ );
+ }
+ if (afterToggleClick0) {
+ Assert.ok(
+ !toggleCheckbox.hidden,
+ "The checkbox shouldnt be hidden when clicking on it"
+ );
+ info("Clicking on the visibility toggle");
+ await EventUtils.synthesizeMouseAtCenter(toggleCheckbox, {});
+ await TestUtils.waitForTick();
+ Assert.equal(
+ toggleCheckbox.checked,
+ afterToggleClick0.toggleChecked,
+ `After 1st click, expect toggle to be checked? ${afterToggleClick0.toggleChecked}, actual: ${toggleCheckbox.checked}`
+ );
+ Assert.equal(
+ passwordTextbox.type,
+ afterToggleClick0.inputType,
+ `After 1st click, expect password input to have type: ${afterToggleClick0.inputType}`
+ );
+ }
+ if (afterToggleClick1) {
+ Assert.ok(
+ !toggleCheckbox.hidden,
+ "The checkbox shouldnt be hidden when clicking on it"
+ );
+ info("Clicking on the visibility toggle again");
+ await EventUtils.synthesizeMouseAtCenter(toggleCheckbox, {});
+ await TestUtils.waitForTick();
+ Assert.equal(
+ toggleCheckbox.checked,
+ afterToggleClick1.toggleChecked,
+ `After 2nd click, expect toggle to be checked? ${afterToggleClick0.toggleChecked}, actual: ${toggleCheckbox.checked}`
+ );
+ Assert.equal(
+ passwordTextbox.type,
+ afterToggleClick1.inputType,
+ `After 2nd click, expect password input to have type: ${afterToggleClick1.inputType}`
+ );
+ }
+}
+
+async function initForm(browser, formDefaults) {
+ await ContentTask.spawn(
+ browser,
+ formDefaults,
+ async function (selectorValues = {}) {
+ for (let [sel, value] of Object.entries(selectorValues)) {
+ content.document.querySelector(sel).value = value;
+ }
+ }
+ );
+}
+
+async function checkForm(browser, expected) {
+ await ContentTask.spawn(
+ browser,
+ {
+ [passwordInputSelector]: expected.password,
+ [usernameInputSelector]: expected.username,
+ },
+ async function contentCheckForm(selectorValues) {
+ for (let [sel, value] of Object.entries(selectorValues)) {
+ let field = content.document.querySelector(sel);
+ Assert.equal(
+ field.value,
+ value,
+ sel + " has the expected initial value"
+ );
+ }
+ }
+ );
+}
+
+async function submitForm(browser, action = "") {
+ // Submit the form
+ let correctPathNamePromise = BrowserTestUtils.browserLoaded(browser);
+ await SpecialPowers.spawn(browser, [action], async function (actionPathname) {
+ let form = content.document.querySelector("form");
+ if (actionPathname) {
+ form.action = actionPathname;
+ }
+ info("Submitting form to:" + form.action);
+ form.submit();
+ info("Submitted the form");
+ });
+ await correctPathNamePromise;
+ await SpecialPowers.spawn(browser, [action], async actionPathname => {
+ let win = content;
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ win.location.pathname == actionPathname &&
+ win.document.readyState == "complete"
+ );
+ }, "Wait for form submission load");
+ });
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_username_edits.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_username_edits.js
new file mode 100644
index 0000000000..79dd92db12
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_username_edits.js
@@ -0,0 +1,192 @@
+/**
+ * Test changing the username inside the doorhanger notification for passwords.
+ *
+ * We have to test combination of existing and non-existing logins both for
+ * the original one from the webpage and the final one used by the dialog.
+ *
+ * We also check switching to and from empty usernames.
+ */
+add_task(async function test_edit_username() {
+ let testCases = [
+ {
+ usernameInPage: "username",
+ usernameChangedTo: "newUsername",
+ },
+ {
+ usernameInPage: "username",
+ usernameInPageExists: true,
+ usernameChangedTo: "newUsername",
+ },
+ {
+ usernameInPage: "username",
+ usernameChangedTo: "newUsername",
+ usernameChangedToExists: true,
+ },
+ {
+ usernameInPage: "username",
+ usernameInPageExists: true,
+ usernameChangedTo: "newUsername",
+ usernameChangedToExists: true,
+ },
+ {
+ usernameInPage: "",
+ usernameChangedTo: "newUsername",
+ },
+ {
+ usernameInPage: "newUsername",
+ usernameChangedTo: "",
+ },
+ {
+ usernameInPage: "",
+ usernameChangedTo: "newUsername",
+ usernameChangedToExists: true,
+ },
+ {
+ usernameInPage: "newUsername",
+ usernameChangedTo: "",
+ usernameChangedToExists: true,
+ },
+ ];
+
+ for (let testCase of testCases) {
+ info("Test case: " + JSON.stringify(testCase));
+ // Clean state before the test case is executed.
+ await LoginTestUtils.clearData();
+ await cleanupDoorhanger();
+ await cleanupPasswordNotifications();
+
+ // Create the pre-existing logins when needed.
+ if (testCase.usernameInPageExists) {
+ await Services.logins.addLoginAsync(
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: testCase.usernameInPage,
+ password: "old password",
+ })
+ );
+ }
+
+ if (testCase.usernameChangedToExists) {
+ await Services.logins.addLoginAsync(
+ LoginTestUtils.testData.formLogin({
+ origin: "https://example.com",
+ formActionOrigin: "https://example.com",
+ username: testCase.usernameChangedTo,
+ password: "old password",
+ })
+ );
+ }
+
+ let formFilledPromise = listenForTestNotification("FormProcessed");
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ },
+ async function (browser) {
+ await formFilledPromise;
+ await initForm(browser, {
+ "#form-basic-username": testCase.usernameInPage,
+ });
+
+ let passwordEditedPromise = listenForTestNotification(
+ "PasswordEditedOrGenerated"
+ );
+ info("Editing the form");
+ await changeContentFormValues(browser, {
+ "#form-basic-password": "password",
+ });
+ info("Waiting for passwordEditedPromise");
+ await passwordEditedPromise;
+
+ // reset doorhanger/notifications, we're only interested in the submit outcome
+ await cleanupDoorhanger();
+ await cleanupPasswordNotifications();
+ // reset message cache, we're only interested in the submit outcome
+ await clearMessageCache(browser);
+
+ // Submit the form in the content page with the credentials from the test
+ // case. This will cause the doorhanger notification to be displayed.
+ info("Submitting the form");
+ let formSubmittedPromise = listenForTestNotification("ShowDoorhanger");
+ let promiseShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown",
+ event => event.target == PopupNotifications.panel
+ );
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.document.getElementById("form-basic").submit();
+ });
+ info("Waiting for the submit message");
+ await formSubmittedPromise;
+
+ info("Waiting for the doorhanger");
+ let notif = await waitForDoorhanger(browser, "any");
+ Assert.ok(!notif.dismissed, "Doorhanger is not dismissed");
+ await promiseShown;
+
+ // Modify the username in the dialog if requested.
+ if (testCase.usernameChangedTo !== undefined) {
+ await updateDoorhangerInputValues({
+ username: testCase.usernameChangedTo,
+ });
+ }
+
+ // We expect a modifyLogin notification if the final username used by the
+ // dialog exists in the logins database, otherwise an addLogin one.
+ let expectModifyLogin =
+ testCase.usernameChangedTo !== undefined
+ ? testCase.usernameChangedToExists
+ : testCase.usernameInPageExists;
+ // Simulate the action on the notification to request the login to be
+ // saved, and wait for the data to be updated or saved based on the type
+ // of operation we expect.
+ let expectedNotification = expectModifyLogin
+ ? "modifyLogin"
+ : "addLogin";
+ let promiseLogin = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ (_, data) => data == expectedNotification
+ );
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ clickDoorhangerButton(notif, CHANGE_BUTTON);
+ await promiseHidden;
+ info("Waiting for storage changed");
+ let [result] = await promiseLogin;
+
+ // Check that the values in the database match the expected values.
+ let login = expectModifyLogin
+ ? result
+ .QueryInterface(Ci.nsIArray)
+ .queryElementAt(1, Ci.nsILoginInfo)
+ : result.QueryInterface(Ci.nsILoginInfo);
+ Assert.equal(
+ login.username,
+ testCase.usernameChangedTo !== undefined
+ ? testCase.usernameChangedTo
+ : testCase.usernameInPage
+ );
+ Assert.equal(login.password, "password");
+ }
+ );
+ }
+});
+
+async function initForm(browser, formDefaults = {}) {
+ await ContentTask.spawn(
+ browser,
+ formDefaults,
+ async function (selectorValues) {
+ for (let [sel, value] of Object.entries(selectorValues)) {
+ content.document.querySelector(sel).value = value;
+ }
+ }
+ );
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_doorhanger_window_open.js b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_window_open.js
new file mode 100644
index 0000000000..2375a3c53a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_doorhanger_window_open.js
@@ -0,0 +1,201 @@
+/*
+ * Test capture popup notifications in content opened by window.open
+ */
+
+let nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo,
+ "init"
+);
+let login1 = new nsLoginInfo(
+ "http://mochi.test:8888",
+ "http://mochi.test:8888",
+ null,
+ "notifyu1",
+ "notifyp1",
+ "user",
+ "pass"
+);
+let login2 = new nsLoginInfo(
+ "http://mochi.test:8888",
+ "http://mochi.test:8888",
+ null,
+ "notifyu2",
+ "notifyp2",
+ "user",
+ "pass"
+);
+
+function withTestTabUntilStorageChange(aPageFile, aTaskFn) {
+ function storageChangedObserved(subject, data) {
+ // Watch for actions triggered from a doorhanger (not cleanup tasks with removeLogin)
+ if (data == "removeLogin") {
+ return false;
+ }
+ return true;
+ }
+
+ let storageChangedPromised = TestUtils.topicObserved(
+ "passwordmgr-storage-changed",
+ storageChangedObserved
+ );
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "http://mochi.test:8888" + DIRECTORY_PATH + aPageFile,
+ },
+ async function (browser) {
+ Assert.ok(true, "loaded " + aPageFile);
+ info("running test case task");
+ await aTaskFn();
+ info("waiting for storage change");
+ await storageChangedPromised;
+ }
+ );
+}
+
+add_setup(async function () {
+ await SimpleTest.promiseFocus(window);
+});
+
+add_task(async function test_saveChromeHiddenAutoClose() {
+ let notifShownPromise = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ // query arguments are: username, password, features, auto-close (delimited by '|')
+ let url =
+ "subtst_notifications_11.html?notifyu1|notifyp1|" +
+ "menubar=no,toolbar=no,location=no|autoclose";
+ await withTestTabUntilStorageChange(url, async function () {
+ info("waiting for popupshown");
+ await notifShownPromise;
+ // the popup closes and the doorhanger should appear in the opener
+ let popup = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(popup, "got notification popup");
+ await checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
+ // Sanity check, no logins should exist yet.
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 0, "Should not have any logins yet");
+
+ clickDoorhangerButton(popup, REMEMBER_BUTTON);
+ });
+ // Check result of clicking Remember
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.timesUsed, 1, "Check times used on new entry");
+ Assert.equal(
+ login.username,
+ "notifyu1",
+ "Check the username used on the new entry"
+ );
+ Assert.equal(
+ login.password,
+ "notifyp1",
+ "Check the password used on the new entry"
+ );
+});
+
+add_task(async function test_changeChromeHiddenAutoClose() {
+ let notifShownPromise = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ let url =
+ "subtst_notifications_11.html?notifyu1|pass2|menubar=no,toolbar=no,location=no|autoclose";
+ await withTestTabUntilStorageChange(url, async function () {
+ info("waiting for popupshown");
+ await notifShownPromise;
+ let popup = await getCaptureDoorhangerThatMayOpen("password-change");
+ Assert.ok(popup, "got notification popup");
+ await checkDoorhangerUsernamePassword("notifyu1", "pass2");
+ clickDoorhangerButton(popup, CHANGE_BUTTON);
+ });
+
+ // Check to make sure we updated the password, timestamps and use count for
+ // the login being changed with this form.
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "notifyu1", "Check the username");
+ Assert.equal(login.password, "pass2", "Check password changed");
+ Assert.equal(login.timesUsed, 2, "check .timesUsed incremented on change");
+ Assert.ok(login.timeCreated < login.timeLastUsed, "timeLastUsed bumped");
+ Assert.ok(
+ login.timeLastUsed == login.timePasswordChanged,
+ "timeUsed == timeChanged"
+ );
+
+ login1.password = "pass2";
+ Services.logins.removeLogin(login1);
+ login1.password = "notifyp1";
+});
+
+add_task(async function test_saveChromeVisibleSameWindow() {
+ // This test actually opens a new tab in the same window with default browser settings.
+ let url = "subtst_notifications_11.html?notifyu2|notifyp2||";
+ let notifShownPromise = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ await withTestTabUntilStorageChange(url, async function () {
+ await notifShownPromise;
+ let popup = await getCaptureDoorhangerThatMayOpen("password-save");
+ Assert.ok(popup, "got notification popup");
+ await checkDoorhangerUsernamePassword("notifyu2", "notifyp2");
+ clickDoorhangerButton(popup, REMEMBER_BUTTON);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+
+ // Check result of clicking Remember
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login now");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(
+ login.username,
+ "notifyu2",
+ "Check the username used on the new entry"
+ );
+ Assert.equal(
+ login.password,
+ "notifyp2",
+ "Check the password used on the new entry"
+ );
+ Assert.equal(login.timesUsed, 1, "Check times used on new entry");
+});
+
+add_task(async function test_changeChromeVisibleSameWindow() {
+ let url = "subtst_notifications_11.html?notifyu2|pass2||";
+ let notifShownPromise = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ await withTestTabUntilStorageChange(url, async function () {
+ await notifShownPromise;
+ let popup = await getCaptureDoorhangerThatMayOpen("password-change");
+ Assert.ok(popup, "got notification popup");
+ await checkDoorhangerUsernamePassword("notifyu2", "pass2");
+ clickDoorhangerButton(popup, CHANGE_BUTTON);
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+
+ // Check to make sure we updated the password, timestamps and use count for
+ // the login being changed with this form.
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should have 1 login");
+ let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "notifyu2", "Check the username");
+ Assert.equal(login.password, "pass2", "Check password changed");
+ Assert.equal(login.timesUsed, 2, "check .timesUsed incremented on change");
+ Assert.ok(login.timeCreated < login.timeLastUsed, "timeLastUsed bumped");
+ Assert.ok(
+ login.timeLastUsed == login.timePasswordChanged,
+ "timeUsed == timeChanged"
+ );
+
+ // cleanup
+ login2.password = "pass2";
+ Services.logins.removeLogin(login2);
+ login2.password = "notifyp2";
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_entry_point_telemetry.js b/toolkit/components/passwordmgr/test/browser/browser_entry_point_telemetry.js
new file mode 100644
index 0000000000..aabf2cf63a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_entry_point_telemetry.js
@@ -0,0 +1,103 @@
+const TEST_ORIGIN = "https://example.com";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.rememberSignons.visibilityToggle", true]],
+ });
+ Services.telemetry.clearEvents();
+});
+
+add_task(async function mainMenu_entryPoint() {
+ await SimpleTest.promiseFocus();
+ info("mainMenu_entryPoint, got focus");
+
+ let mainMenu = document.getElementById("appMenu-popup");
+ let target = document.getElementById("PanelUI-menu-button");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(target),
+ "Main menu button should be visible."
+ );
+ info("mainMenu_entryPoint, Main menu button is visible");
+ Assert.equal(
+ mainMenu.state,
+ "closed",
+ `Menu panel (${mainMenu.id}) is initally closed.`
+ );
+
+ info("mainMenu_entryPoint, clicking target and waiting for popup");
+ let popupshown = BrowserTestUtils.waitForEvent(mainMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(target, {});
+ await popupshown;
+
+ info("mainMenu_entryPoint, main menu popup is shown");
+ Assert.equal(mainMenu.state, "open", `Menu panel (${mainMenu.id}) is open.`);
+
+ let loginsButtonID = "appMenu-passwords-button";
+
+ let item = document.getElementById(loginsButtonID);
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(item),
+ "Logins and passwords button is visible."
+ );
+
+ info("mainMenu_entryPoint, clicking on Logins and passwords button");
+ let openingFunc = () => EventUtils.synthesizeMouseAtCenter(item, {});
+ let passwordManager = await openPasswordManager(openingFunc);
+ info("mainMenu_entryPoint, password manager dialog shown");
+
+ await LoginTestUtils.telemetry.waitForEventCount(1);
+ TelemetryTestUtils.assertEvents(
+ [["pwmgr", "open_management", "mainmenu"]],
+ {
+ category: "pwmgr",
+ method: "open_management",
+ },
+ { clear: true, process: "content" }
+ );
+
+ info("mainMenu_entryPoint, close dialog and main menu");
+ await passwordManager.close();
+ mainMenu.hidePopup();
+});
+
+add_task(async function pageInfo_entryPoint() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_ORIGIN,
+ },
+ async function (browser) {
+ info("pageInfo_entryPoint, opening pageinfo");
+ let pageInfo = BrowserPageInfo(TEST_ORIGIN, "securityTab", {});
+ await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init");
+ info(
+ "pageInfo_entryPoint, got pageinfo, wait until password button is visible"
+ );
+ let passwordsButton = pageInfo.document.getElementById(
+ "security-view-password"
+ );
+
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(passwordsButton),
+ "Show passwords button should be visible."
+ );
+ info("pageInfo_entryPoint, clicking the show passwords button...");
+ await SimpleTest.promiseFocus(pageInfo);
+ let openingFunc = () =>
+ EventUtils.synthesizeMouseAtCenter(passwordsButton, {}, pageInfo);
+
+ info("pageInfo_entryPoint, waiting for the passwords manager dialog");
+ let passwordManager = await openPasswordManager(openingFunc);
+
+ TelemetryTestUtils.assertEvents(
+ [["pwmgr", "open_management", "pageinfo"]],
+ { category: "pwmgr", method: "open_management" },
+ { clear: true, process: "content" }
+ );
+
+ info("pageInfo_entryPoint, close dialog and pageInfo");
+ await passwordManager.close();
+ pageInfo.close();
+ }
+ );
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_exceptions_dialog.js b/toolkit/components/passwordmgr/test/browser/browser_exceptions_dialog.js
new file mode 100644
index 0000000000..854f0656b8
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_exceptions_dialog.js
@@ -0,0 +1,141 @@
+"use strict";
+
+const LOGIN_HOST = "https://example.com";
+
+function openExceptionsDialog() {
+ return window.openDialog(
+ "chrome://browser/content/preferences/dialogs/permissions.xhtml",
+ "Toolkit:PasswordManagerExceptions",
+ "",
+ {
+ blockVisible: true,
+ sessionVisible: false,
+ allowVisible: false,
+ hideStatusColumn: true,
+ prefilledHost: "",
+ permissionType: "login-saving",
+ }
+ );
+}
+
+function countDisabledHosts(dialog) {
+ return dialog.document.getElementById("permissionsBox").itemCount;
+}
+
+function promiseStorageChanged(expectedData) {
+ function observer(subject, data) {
+ return (
+ data == expectedData &&
+ subject.QueryInterface(Ci.nsISupportsString).data == LOGIN_HOST
+ );
+ }
+
+ return TestUtils.topicObserved("passwordmgr-storage-changed", observer);
+}
+
+add_task(async function test_disable() {
+ let dialog = openExceptionsDialog();
+ let promiseChanged = promiseStorageChanged("hostSavingDisabled");
+
+ await BrowserTestUtils.waitForEvent(dialog, "load");
+ await new Promise(resolve => {
+ waitForFocus(resolve, dialog);
+ });
+ Services.logins.setLoginSavingEnabled(LOGIN_HOST, false);
+ await promiseChanged;
+ Assert.equal(countDisabledHosts(dialog), 1, "Verify disabled host added");
+ await BrowserTestUtils.closeWindow(dialog);
+});
+
+add_task(async function test_enable() {
+ let dialog = openExceptionsDialog();
+ let promiseChanged = promiseStorageChanged("hostSavingEnabled");
+
+ await BrowserTestUtils.waitForEvent(dialog, "load");
+ await new Promise(resolve => {
+ waitForFocus(resolve, dialog);
+ });
+ Services.logins.setLoginSavingEnabled(LOGIN_HOST, true);
+ await promiseChanged;
+ Assert.equal(countDisabledHosts(dialog), 0, "Verify disabled host removed");
+ await BrowserTestUtils.closeWindow(dialog);
+});
+
+add_task(async function test_block_button_with_enter_key() {
+ // Test ensures that the Enter/Return key does not activate the "Allow" button
+ // in the "Saved Logins" exceptions dialog
+
+ let dialog = openExceptionsDialog();
+
+ await BrowserTestUtils.waitForEvent(dialog, "load");
+ await new Promise(resolve => waitForFocus(resolve, dialog));
+ let btnBlock = dialog.document.getElementById("btnBlock");
+ let btnCookieSession = dialog.document.getElementById("btnCookieSession");
+ let btnHttpsOnlyOff = dialog.document.getElementById("btnHttpsOnlyOff");
+ let btnHttpsOnlyOffTmp = dialog.document.getElementById("btnHttpsOnlyOffTmp");
+ let btnAllow = dialog.document.getElementById("btnAllow");
+
+ Assert.ok(!btnBlock.hidden, "Block button is visible");
+ Assert.ok(btnCookieSession.hidden, "Cookie session button is not visible");
+ Assert.ok(btnAllow.hidden, "Allow button is not visible");
+ Assert.ok(btnHttpsOnlyOff.hidden, "HTTPS-Only session button is not visible");
+ Assert.ok(
+ btnHttpsOnlyOffTmp.hidden,
+ "HTTPS-Only session button is not visible"
+ );
+ Assert.ok(btnBlock.disabled, "Block button is initially disabled");
+ Assert.ok(
+ btnCookieSession.disabled,
+ "Cookie session button is initially disabled"
+ );
+ Assert.ok(btnAllow.disabled, "Allow button is initially disabled");
+ Assert.ok(
+ btnHttpsOnlyOff.disabled,
+ "HTTPS-Only off-button is initially disabled"
+ );
+ Assert.ok(
+ btnHttpsOnlyOffTmp.disabled,
+ "HTTPS-Only temporary off-button is initially disabled"
+ );
+
+ EventUtils.sendString(LOGIN_HOST, dialog);
+
+ Assert.ok(
+ !btnBlock.disabled,
+ "Block button is enabled after entering text in the URL input"
+ );
+ Assert.ok(
+ btnCookieSession.disabled,
+ "Cookie session button is still disabled after entering text in the URL input"
+ );
+ Assert.ok(
+ btnAllow.disabled,
+ "Allow button is still disabled after entering text in the URL input"
+ );
+ Assert.ok(
+ btnHttpsOnlyOff.disabled,
+ "HTTPS-Only off-button is still disabled after entering text in the URL input"
+ );
+ Assert.ok(
+ btnHttpsOnlyOffTmp.disabled,
+ "HTTPS-Only session off-button is still disabled after entering text in the URL input"
+ );
+
+ Assert.equal(
+ countDisabledHosts(dialog),
+ 0,
+ "No blocked hosts should be present before hitting the Enter/Return key"
+ );
+ EventUtils.sendKey("return", dialog);
+
+ Assert.equal(
+ countDisabledHosts(dialog),
+ 1,
+ "Verify the blocked host was added"
+ );
+ Assert.ok(
+ btnBlock.disabled,
+ "Block button is disabled after submitting to the list"
+ );
+ await BrowserTestUtils.closeWindow(dialog);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_fileURIOrigin.js b/toolkit/components/passwordmgr/test/browser/browser_fileURIOrigin.js
new file mode 100644
index 0000000000..ae8c860792
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_fileURIOrigin.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function getDataFromNextSubmitMessage() {
+ return new Promise(resolve => {
+ LoginManagerParent.setListenerForTests((msg, data) => {
+ if (msg == "ShowDoorhanger") {
+ resolve(data);
+ }
+ });
+ });
+}
+
+add_task(async function testCrossOriginFormUsesCorrectOrigin() {
+ const registry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+ Ci.nsIChromeRegistry
+ );
+
+ let dataPromise = getDataFromNextSubmitMessage();
+
+ let url =
+ registry.convertChromeURL(Services.io.newURI(getRootDirectory(gTestPath)))
+ .asciiSpec + "form_basic.html";
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function (browser) {
+ await SpecialPowers.spawn(browser.browsingContext, [], () => {
+ let doc = content.document;
+ doc.getElementById("form-basic-username").setUserInput("username");
+ doc.getElementById("form-basic-password").setUserInput("password");
+ doc.getElementById("form-basic").submit();
+ info("Submitting form");
+ });
+ }
+ );
+
+ let data = await dataPromise;
+ info("Origin retrieved from message listener");
+
+ Assert.equal(
+ data.origin,
+ "file://",
+ "Message origin should match form origin"
+ );
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_focus_before_first_DOMContentLoaded.js b/toolkit/components/passwordmgr/test/browser/browser_focus_before_first_DOMContentLoaded.js
new file mode 100644
index 0000000000..783f8ea3d8
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_focus_before_first_DOMContentLoaded.js
@@ -0,0 +1,103 @@
+/**
+ * Test that autocomplete is properly attached to a username field which gets
+ * focused before DOMContentLoaded in a new browser and process.
+ */
+
+"use strict";
+
+add_setup(async () => {
+ let nsLoginInfo = Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo,
+ "init"
+ );
+ Assert.ok(nsLoginInfo != null, "nsLoginInfo constructor");
+
+ info("Adding two logins to get autocomplete instead of autofill");
+ let login1 = new nsLoginInfo(
+ "https://example.com",
+ "https://autocomplete:8888",
+ null,
+ "tempuser1",
+ "temppass1"
+ );
+
+ let login2 = new nsLoginInfo(
+ "https://example.com",
+ "https://autocomplete:8888",
+ null,
+ "testuser2",
+ "testpass2"
+ );
+
+ await Services.logins.addLogins([login1, login2]);
+});
+
+add_task(async function test_autocompleteFromUsername() {
+ let autocompletePopup = document.getElementById("PopupAutoComplete");
+ let autocompletePopupShown = BrowserTestUtils.waitForEvent(
+ autocompletePopup,
+ "popupshown"
+ );
+
+ const URL = `https://example.com${DIRECTORY_PATH}file_focus_before_DOMContentLoaded.sjs`;
+
+ let newTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: URL,
+ forceNewProcess: true,
+ });
+
+ await SpecialPowers.spawn(
+ newTab.linkedBrowser,
+ [],
+ function checkInitialValues() {
+ let doc = content.document;
+ let uname = doc.querySelector("#uname");
+ let pword = doc.querySelector("#pword");
+
+ Assert.ok(uname, "Username field found");
+ Assert.ok(pword, "Password field found");
+
+ Assert.equal(
+ doc.activeElement,
+ uname,
+ "#uname element should be focused"
+ );
+ Assert.equal(uname.value, "", "Checking username is empty");
+ Assert.equal(pword.value, "", "Checking password is empty");
+ }
+ );
+
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, newTab.linkedBrowser);
+ await autocompletePopupShown;
+
+ let richlistbox = autocompletePopup.richlistbox;
+ Assert.equal(
+ richlistbox.localName,
+ "richlistbox",
+ "The richlistbox should be the first anonymous node"
+ );
+ for (let i = 0; i < autocompletePopup.view.matchCount; i++) {
+ if (
+ richlistbox.selectedItem &&
+ richlistbox.selectedItem.textContent.includes("tempuser1")
+ ) {
+ break;
+ }
+ await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, newTab.linkedBrowser);
+ }
+
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, newTab.linkedBrowser);
+
+ await SpecialPowers.spawn(newTab.linkedBrowser, [], function checkFill() {
+ let doc = content.document;
+ let uname = doc.querySelector("#uname");
+ let pword = doc.querySelector("#pword");
+
+ Assert.equal(uname.value, "tempuser1", "Checking username is filled");
+ Assert.equal(pword.value, "temppass1", "Checking password is filled");
+ });
+
+ BrowserTestUtils.removeTab(newTab);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_form_history_fallback.js b/toolkit/components/passwordmgr/test/browser/browser_form_history_fallback.js
new file mode 100644
index 0000000000..ad76e5479c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_form_history_fallback.js
@@ -0,0 +1,65 @@
+const { FormHistoryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/FormHistoryTestUtils.sys.mjs"
+);
+
+const usernameFieldName = "user";
+
+async function cleanup() {
+ Services.prefs.clearUserPref("signon.rememberSignons");
+ Services.logins.removeAllLogins();
+ await FormHistoryTestUtils.clear(usernameFieldName);
+}
+
+add_setup(async function () {
+ await cleanup();
+});
+
+add_task(
+ async function test_username_not_saved_in_form_history_when_password_manager_enabled() {
+ Services.prefs.setBoolPref("signon.rememberSignons", true);
+
+ await testSubmittingLoginFormHTTP(
+ "subtst_notifications_1.html",
+ async () => {
+ const notif = await getCaptureDoorhangerThatMayOpen("password-save");
+ await clickDoorhangerButton(notif, REMEMBER_BUTTON);
+ }
+ );
+
+ const loginEntries = Services.logins.getAllLogins().length;
+ const historyEntries = await FormHistoryTestUtils.count(usernameFieldName);
+
+ Assert.equal(
+ loginEntries,
+ 1,
+ "Username should be saved in password manager"
+ );
+
+ Assert.equal(
+ historyEntries,
+ 0,
+ "Username should not be saved in form history"
+ );
+ await cleanup();
+ }
+);
+
+add_task(
+ async function test_username_saved_in_form_history_when_password_manager_disabled() {
+ Services.prefs.setBoolPref("signon.rememberSignons", false);
+
+ await testSubmittingLoginFormHTTP("subtst_notifications_1.html");
+
+ const loginEntries = Services.logins.getAllLogins().length;
+ const historyEntries = await FormHistoryTestUtils.count(usernameFieldName);
+
+ Assert.equal(
+ loginEntries,
+ 0,
+ "Username should not be saved in password manager"
+ );
+
+ Assert.equal(historyEntries, 1, "Username should be saved in form history");
+ await cleanup();
+ }
+);
diff --git a/toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js b/toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js
new file mode 100644
index 0000000000..212b5f79d8
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js
@@ -0,0 +1,161 @@
+/*
+ * Test that browser chrome UI interactions don't trigger a capture doorhanger.
+ */
+
+"use strict";
+
+async function fillTestPage(
+ aBrowser,
+ username = "my_username",
+ password = "my_password"
+) {
+ let notif = getCaptureDoorhanger("any", undefined, aBrowser);
+ Assert.ok(!notif, "No doorhangers should be present before filling the form");
+
+ await changeContentFormValues(aBrowser, {
+ "#form-basic-username": username,
+ "#form-basic-password": password,
+ });
+ if (LoginHelper.passwordEditCaptureEnabled) {
+ // Filling the password will generate a dismissed doorhanger.
+ // Check and remove that before running the rest of the task
+ notif = await waitForDoorhanger(aBrowser, "any");
+ Assert.ok(notif.dismissed, "Only a dismissed doorhanger should be present");
+ await cleanupDoorhanger(notif);
+ }
+}
+
+function withTestPage(aTaskFn) {
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "https://example.com" + DIRECTORY_PATH + "formless_basic.html",
+ },
+ async function (aBrowser) {
+ info("tab opened");
+ await fillTestPage(aBrowser);
+ await aTaskFn(aBrowser);
+
+ // Give a chance for the doorhanger to appear
+ await new Promise(resolve => SimpleTest.executeSoon(resolve));
+ let notif = getCaptureDoorhanger("any");
+ Assert.ok(!notif, "No doorhanger should be present");
+ await cleanupDoorhanger(notif);
+ }
+ );
+}
+
+add_setup(async function () {
+ await SimpleTest.promiseFocus(window);
+});
+
+add_task(async function test_urlbar_new_URL() {
+ await withTestPage(async function (aBrowser) {
+ gURLBar.value = "";
+ let focusPromise = BrowserTestUtils.waitForEvent(gURLBar, "focus");
+ gURLBar.focus();
+ await focusPromise;
+ info("focused");
+ EventUtils.sendString("http://mochi.test:8888/");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await BrowserTestUtils.browserLoaded(
+ aBrowser,
+ false,
+ "http://mochi.test:8888/"
+ );
+ });
+});
+
+add_task(async function test_urlbar_fragment_enter() {
+ await withTestPage(function (aBrowser) {
+ gURLBar.focus();
+ gURLBar.select();
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ EventUtils.sendString("#fragment");
+ EventUtils.synthesizeKey("KEY_Enter");
+ });
+});
+
+add_task(async function test_backButton_forwardButton() {
+ await withTestPage(async function (aBrowser) {
+ info("Loading formless_basic.html?second");
+ // Load a new page in the tab so we can test going back
+ BrowserTestUtils.loadURIString(
+ aBrowser,
+ "https://example.com" + DIRECTORY_PATH + "formless_basic.html?second"
+ );
+ await BrowserTestUtils.browserLoaded(
+ aBrowser,
+ false,
+ "https://example.com" + DIRECTORY_PATH + "formless_basic.html?second"
+ );
+ info("Loaded formless_basic.html?second");
+ await fillTestPage(aBrowser, "my_username", "password_2");
+
+ info("formless_basic.html?second form is filled, clicking back");
+ let backPromise = BrowserTestUtils.browserStopped(aBrowser);
+ EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("back-button"),
+ {}
+ );
+ await backPromise;
+
+ // Give a chance for the doorhanger to appear
+ await new Promise(resolve => SimpleTest.executeSoon(resolve));
+ Assert.ok(!getCaptureDoorhanger("any"), "No doorhanger should be present");
+
+ // Now go forward again after filling
+ await fillTestPage(aBrowser, "my_username", "password_3");
+
+ let forwardButton = document.getElementById("forward-button");
+ await BrowserTestUtils.waitForCondition(() => {
+ return !forwardButton.disabled;
+ });
+ let forwardPromise = BrowserTestUtils.browserStopped(aBrowser);
+ info("click the forward button");
+ EventUtils.synthesizeMouseAtCenter(forwardButton, {});
+ await forwardPromise;
+ info("done");
+ });
+});
+
+add_task(async function test_reloadButton() {
+ await withTestPage(async function (aBrowser) {
+ let reloadButton = document.getElementById("reload-button");
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ aBrowser,
+ false,
+ "https://example.com" + DIRECTORY_PATH + "formless_basic.html"
+ );
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return !reloadButton.disabled;
+ });
+ EventUtils.synthesizeMouseAtCenter(reloadButton, {});
+ await loadPromise;
+ });
+});
+
+add_task(async function test_back_keyboard_shortcut() {
+ await withTestPage(async function (aBrowser) {
+ // Load a new page in the tab so we can test going back
+ BrowserTestUtils.loadURIString(
+ aBrowser,
+ "https://example.com" + DIRECTORY_PATH + "formless_basic.html?second"
+ );
+ await BrowserTestUtils.browserLoaded(
+ aBrowser,
+ false,
+ "https://example.com" + DIRECTORY_PATH + "formless_basic.html?second"
+ );
+ await fillTestPage(aBrowser);
+
+ let backPromise = BrowserTestUtils.browserStopped(aBrowser);
+
+ const goBackKeyModifier =
+ AppConstants.platform == "macosx" ? { metaKey: true } : { altKey: true };
+ EventUtils.synthesizeKey("KEY_ArrowLeft", goBackKeyModifier);
+
+ await backPromise;
+ });
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_insecurePasswordConsoleWarning.js b/toolkit/components/passwordmgr/test/browser/browser_insecurePasswordConsoleWarning.js
new file mode 100644
index 0000000000..68d5663258
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_insecurePasswordConsoleWarning.js
@@ -0,0 +1,131 @@
+"use strict";
+
+const WARNING_PATTERN = [
+ {
+ key: "INSECURE_FORM_ACTION",
+ msg: 'JavaScript Warning: "Password fields present in a form with an insecure (http://) form action. This is a security risk that allows user login credentials to be stolen."',
+ },
+ {
+ key: "INSECURE_PAGE",
+ msg: 'JavaScript Warning: "Password fields present on an insecure (http://) page. This is a security risk that allows user login credentials to be stolen."',
+ },
+];
+
+add_task(async function testInsecurePasswordWarning() {
+ // By default, proxies don't apply to 127.0.0.1. We need them to for this test, though:
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.proxy.allow_hijacking_localhost", true]],
+ });
+ let warningPatternHandler;
+
+ function messageHandler(msgObj) {
+ function findWarningPattern(msg) {
+ return WARNING_PATTERN.find(patternPair => {
+ return msg.includes(patternPair.msg);
+ });
+ }
+
+ let warning = findWarningPattern(msgObj.message);
+
+ // Only handle the insecure password related warning messages.
+ if (warning) {
+ // Prevent any unexpected or redundant matched warning message coming after
+ // the test case is ended.
+ Assert.ok(
+ warningPatternHandler,
+ "Invoke a valid warning message handler"
+ );
+ warningPatternHandler(warning, msgObj.message);
+ }
+ }
+ Services.console.registerListener(messageHandler);
+ registerCleanupFunction(function () {
+ Services.console.unregisterListener(messageHandler);
+ });
+
+ for (let [origin, testFile, expectWarnings] of [
+ ["http://127.0.0.1", "form_basic.html", []],
+ ["http://127.0.0.1", "formless_basic.html", []],
+ ["http://example.com", "form_basic.html", ["INSECURE_PAGE"]],
+ ["http://example.com", "formless_basic.html", ["INSECURE_PAGE"]],
+ ["https://example.com", "form_basic.html", []],
+ ["https://example.com", "formless_basic.html", []],
+
+ // For a form with customized action link in the same origin.
+ ["http://127.0.0.1", "form_same_origin_action.html", []],
+ ["http://example.com", "form_same_origin_action.html", ["INSECURE_PAGE"]],
+ ["https://example.com", "form_same_origin_action.html", []],
+
+ // For a form with an insecure (http) customized action link.
+ [
+ "http://127.0.0.1",
+ "form_cross_origin_insecure_action.html",
+ ["INSECURE_FORM_ACTION"],
+ ],
+ [
+ "http://example.com",
+ "form_cross_origin_insecure_action.html",
+ ["INSECURE_PAGE"],
+ ],
+ [
+ "https://example.com",
+ "form_cross_origin_insecure_action.html",
+ ["INSECURE_FORM_ACTION"],
+ ],
+
+ // For a form with a secure (https) customized action link.
+ ["http://127.0.0.1", "form_cross_origin_secure_action.html", []],
+ [
+ "http://example.com",
+ "form_cross_origin_secure_action.html",
+ ["INSECURE_PAGE"],
+ ],
+ ["https://example.com", "form_cross_origin_secure_action.html", []],
+ ]) {
+ let testURL = origin + DIRECTORY_PATH + testFile;
+ let promiseConsoleMessages = new Promise(resolve => {
+ warningPatternHandler = function (warning, originMessage) {
+ Assert.ok(warning, "Handling a warning pattern");
+ let fullMessage = `[${warning.msg} {file: "${testURL}" line: 0 column: 0 source: "0"}]`;
+ Assert.equal(
+ originMessage,
+ fullMessage,
+ "Message full matched:" + originMessage
+ );
+
+ let index = expectWarnings.indexOf(warning.key);
+ isnot(
+ index,
+ -1,
+ "Found warning: " + warning.key + " for URL:" + testURL
+ );
+ if (index !== -1) {
+ // Remove the shown message.
+ expectWarnings.splice(index, 1);
+ }
+ if (expectWarnings.length === 0) {
+ info("All warnings are shown for URL:" + testURL);
+ resolve();
+ }
+ };
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: testURL,
+ },
+ function () {
+ if (expectWarnings.length === 0) {
+ info("All warnings are shown for URL:" + testURL);
+ return Promise.resolve();
+ }
+ return promiseConsoleMessages;
+ }
+ );
+
+ // Remove warningPatternHandler to stop handling the matched warning pattern
+ // and the task should not get any warning anymore.
+ warningPatternHandler = null;
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_isProbablyASignUpForm.js b/toolkit/components/passwordmgr/test/browser/browser_isProbablyASignUpForm.js
new file mode 100644
index 0000000000..0f256d74a1
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_isProbablyASignUpForm.js
@@ -0,0 +1,42 @@
+"use strict";
+
+const TEST_URL = `https://example.com${DIRECTORY_PATH}form_signup_detection.html`;
+
+add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL,
+ },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [], async () => {
+ const doc = content.document;
+ const { LoginManagerChild } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginManagerChild.sys.mjs"
+ );
+ const loginManagerChild = new LoginManagerChild();
+ const docState = loginManagerChild.stateForDocument(doc);
+ let isSignUpForm;
+
+ info("Test case: Obvious signup form is detected as sign up form");
+ const signUpForm = doc.getElementById("obvious-signup-form");
+ isSignUpForm = docState.isProbablyASignUpForm(signUpForm);
+ Assert.equal(isSignUpForm, true);
+
+ info(
+ "Test case: Obvious non signup form is detected as non sign up form"
+ );
+ const loginForm = doc.getElementById("obvious-login-form");
+ isSignUpForm = docState.isProbablyASignUpForm(loginForm);
+ Assert.equal(isSignUpForm, false);
+
+ info(
+ "Test case: An <input> HTML element is detected as non sign up form"
+ );
+ const inputField = doc.getElementById("obvious-signup-username");
+ isSignUpForm = docState.isProbablyASignUpForm(inputField);
+ Assert.equal(isSignUpForm, false);
+ });
+ }
+ );
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_localip_frame.js b/toolkit/components/passwordmgr/test/browser/browser_localip_frame.js
new file mode 100644
index 0000000000..33ae0e6c0b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_localip_frame.js
@@ -0,0 +1,86 @@
+"use strict";
+
+add_setup(async () => {
+ const login1 = LoginTestUtils.testData.formLogin({
+ origin: "http://10.0.0.0",
+ formActionOrigin: "https://example.org",
+ username: "username1",
+ password: "password1",
+ });
+ const login2 = LoginTestUtils.testData.formLogin({
+ origin: "https://example.org",
+ formActionOrigin: "https://example.org",
+ username: "username2",
+ password: "password2",
+ });
+ await Services.logins.addLogins([login1, login2]);
+});
+
+add_task(async function test_warningForLocalIP() {
+ let tests = [
+ /* when the url of top-level and iframe are both ip address, do not show insecure warning */
+ {
+ top: "http://192.168.0.0",
+ iframe: "http://10.0.0.0",
+ expected: `[originaltype="loginWithOrigin"]`,
+ },
+ {
+ top: "http://192.168.0.0",
+ iframe: "https://example.org",
+ expected: `[type="insecureWarning"]`,
+ },
+ {
+ top: "http://example.com",
+ iframe: "http://10.0.0.0",
+ expected: `[type="insecureWarning"]`,
+ },
+ {
+ top: "http://example.com",
+ iframe: "http://example.org",
+ expected: `[type="insecureWarning"]`,
+ },
+ ];
+
+ for (let test of tests) {
+ let urlTop = test.top + DIRECTORY_PATH + "empty.html";
+ let urlIframe =
+ test.iframe + DIRECTORY_PATH + "insecure_test_subframe.html";
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, urlTop);
+ let browser = tab.linkedBrowser;
+
+ await SpecialPowers.spawn(browser, [urlIframe], async url => {
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = resolve;
+ ifr.src = url;
+ content.document.body.appendChild(ifr);
+ });
+ });
+
+ let popup = document.getElementById("PopupAutoComplete");
+ Assert.ok(popup, "Got popup");
+
+ let ifr = browser.browsingContext.children[0];
+ Assert.ok(ifr, "Got iframe");
+
+ let popupShown = openACPopup(
+ popup,
+ tab.linkedBrowser,
+ "#form-basic-username",
+ ifr
+ );
+ await popupShown;
+
+ let item = popup.querySelector(test.expected);
+ Assert.ok(item, "Got expected richlistitem");
+
+ await BrowserTestUtils.waitForCondition(
+ () => !item.collapsed,
+ "Wait for autocomplete to show"
+ );
+
+ await closePopup(popup);
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_message_onFormSubmit.js b/toolkit/components/passwordmgr/test/browser/browser_message_onFormSubmit.js
new file mode 100644
index 0000000000..5e611a7384
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_message_onFormSubmit.js
@@ -0,0 +1,82 @@
+/**
+ * Test "passwordmgr-form-submission-detected" should be notified
+ * regardless of whehter the password saving is enabled.
+ */
+
+async function waitForFormSubmissionDetected() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(subject, topic) {
+ Services.obs.removeObserver(
+ observer,
+ "passwordmgr-form-submission-detected"
+ );
+ resolve();
+ }, "passwordmgr-form-submission-detected");
+ });
+}
+
+add_task(async function test_login_save_disable() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.rememberSignons", false]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+ await changeContentFormValues(browser, {
+ "#form-basic-username": "username",
+ "#form-basic-password": "password",
+ });
+
+ let promise = waitForFormSubmissionDetected();
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = this.content.document;
+ doc.getElementById("form-basic").submit();
+ });
+
+ await promise;
+ Assert.ok(true, "Test completed");
+ }
+ );
+});
+
+add_task(async function test_login_save_enable() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.rememberSignons", true]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url:
+ "https://example.com/browser/toolkit/components/" +
+ "passwordmgr/test/browser/form_basic.html",
+ },
+ async function (browser) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+
+ await changeContentFormValues(browser, {
+ "#form-basic-username": "username",
+ "#form-basic-password": "password",
+ });
+
+ // When login saving is enabled, we should receive both FormSubmit
+ // event and "passwordmgr-form-submission-detected" event
+ let p1 = waitForFormSubmissionDetected();
+ let p2 = listenForTestNotification("ShowDoorhanger");
+ await SpecialPowers.spawn(browser, [], async function () {
+ let doc = this.content.document;
+ doc.getElementById("form-basic").submit();
+ });
+
+ await Promise.all([p1, p2]);
+ Assert.ok(true, "Test completed");
+ }
+ );
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_openPasswordManager.js b/toolkit/components/passwordmgr/test/browser/browser_openPasswordManager.js
new file mode 100644
index 0000000000..0bb50f81c8
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_openPasswordManager.js
@@ -0,0 +1,161 @@
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+add_task(async function test_noFilter() {
+ let openingFunc = () =>
+ LoginHelper.openPasswordManager(window, { entryPoint: "mainmenu" });
+ let passwordManager = await openPasswordManager(openingFunc);
+
+ Assert.ok(passwordManager, "Login dialog was opened");
+ await passwordManager.close();
+ await TestUtils.waitForCondition(() => {
+ return Services.wm.getMostRecentWindow("Toolkit:PasswordManager") === null;
+ }, "Waiting for the password manager dialog to close");
+});
+
+add_task(async function test_filter() {
+ // Greek IDN for example.test
+ let domain = "παράδειγμα.δοκιμή";
+ let openingFunc = () =>
+ LoginHelper.openPasswordManager(window, {
+ filterString: domain,
+ entryPoint: "mainmenu",
+ });
+ let passwordManager = await openPasswordManager(openingFunc, true);
+ Assert.equal(
+ passwordManager.filterValue,
+ domain,
+ "search string to filter logins should match expectation"
+ );
+ await passwordManager.close();
+ await TestUtils.waitForCondition(() => {
+ return Services.wm.getMostRecentWindow("Toolkit:PasswordManager") === null;
+ }, "Waiting for the password manager dialog to close");
+});
+
+add_task(async function test_management_noFilter() {
+ let tabOpenPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:logins");
+ LoginHelper.openPasswordManager(window, { entryPoint: "mainmenu" });
+ let tab = await tabOpenPromise;
+ Assert.ok(tab, "Got the new tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_management_filter() {
+ let tabOpenPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:logins?filter=%CF%80%CE%B1%CF%81%CE%AC%CE%B4%CE%B5%CE%B9%CE%B3%CE%BC%CE%B1.%CE%B4%CE%BF%CE%BA%CE%B9%CE%BC%CE%AE"
+ );
+ // Greek IDN for example.test
+ LoginHelper.openPasswordManager(window, {
+ filterString: "παράδειγμα.δοκιμή",
+ entryPoint: "mainmenu",
+ });
+ let tab = await tabOpenPromise;
+ Assert.ok(tab, "Got the new tab with a domain filter");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(
+ async function test_url_when_opening_password_manager_without_a_filterString() {
+ sinon.spy(window, "openTrustedLinkIn");
+ const openingFunc = () =>
+ LoginHelper.openPasswordManager(window, {
+ filterString: "",
+ entryPoint: "mainmenu",
+ });
+ const passwordManager = await openPasswordManager(openingFunc);
+
+ const url = window.openTrustedLinkIn.lastCall.args[0];
+
+ Assert.ok(
+ !url.includes("filter"),
+ "LoginHelper.openPasswordManager call without a filterString navigated to a URL with a filter query param"
+ );
+ Assert.equal(
+ 0,
+ url.split("").filter(char => char === "&").length,
+ "LoginHelper.openPasswordManager call without a filterString navigated to a URL with an &"
+ );
+ Assert.equal(
+ url,
+ "about:logins?entryPoint=mainmenu",
+ "LoginHelper.openPasswordManager call without a filterString navigated to an unexpected URL"
+ );
+
+ Assert.ok(passwordManager, "Login dialog was opened");
+ await passwordManager.close();
+ window.openTrustedLinkIn.restore();
+ }
+);
+
+add_task(
+ async function test_url_when_opening_password_manager_with_a_filterString() {
+ sinon.spy(window, "openTrustedLinkIn");
+ const openingFunc = () =>
+ LoginHelper.openPasswordManager(window, {
+ filterString: "testFilter",
+ entryPoint: "mainmenu",
+ });
+ const passwordManager = await openPasswordManager(openingFunc);
+
+ const url = window.openTrustedLinkIn.lastCall.args[0];
+
+ Assert.ok(
+ url.includes("filter"),
+ "LoginHelper.openPasswordManager call with a filterString navigated to a URL without a filter query param"
+ );
+ Assert.equal(
+ 1,
+ url.split("").filter(char => char === "&").length,
+ "LoginHelper.openPasswordManager call with a filterString navigated to a URL without the correct number of '&'s"
+ );
+ Assert.equal(
+ url,
+ "about:logins?filter=testFilter&entryPoint=mainmenu",
+ "LoginHelper.openPasswordManager call with a filterString navigated to an unexpected URL"
+ );
+
+ Assert.ok(passwordManager, "Login dialog was opened");
+ await passwordManager.close();
+ window.openTrustedLinkIn.restore();
+ }
+);
+
+add_task(
+ async function test_url_when_opening_password_manager_without_filterString_or_entryPoint() {
+ sinon.spy(window, "openTrustedLinkIn");
+ const openingFunc = () =>
+ LoginHelper.openPasswordManager(window, {
+ filterString: "",
+ entryPoint: "",
+ });
+ const passwordManager = await openPasswordManager(openingFunc);
+
+ const url = window.openTrustedLinkIn.lastCall.args[0];
+
+ Assert.ok(
+ !url.includes("filter"),
+ "LoginHelper.openPasswordManager call without a filterString navigated to a URL with a filter query param"
+ );
+ Assert.ok(
+ !url.includes("entryPoint"),
+ "LoginHelper.openPasswordManager call without an entryPoint navigated to a URL with an entryPoint query param"
+ );
+ Assert.equal(
+ 0,
+ url.split("").filter(char => char === "&").length,
+ "LoginHelper.openPasswordManager call without query params navigated to a URL that included at least one '&'"
+ );
+ Assert.equal(
+ url,
+ "about:logins",
+ "LoginHelper.openPasswordManager call without a filterString or entryPoint navigated to an unexpected URL"
+ );
+
+ Assert.ok(passwordManager, "Login dialog was opened");
+ await passwordManager.close();
+ window.openTrustedLinkIn.restore();
+ }
+);
diff --git a/toolkit/components/passwordmgr/test/browser/browser_preselect_login.js b/toolkit/components/passwordmgr/test/browser/browser_preselect_login.js
new file mode 100644
index 0000000000..146b0ae92b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_preselect_login.js
@@ -0,0 +1,183 @@
+const TEST_ORIGIN = "https://example.org";
+const ABOUT_LOGINS_ORIGIN = "about:logins";
+const TEST_URL_PATH = `${TEST_ORIGIN}${DIRECTORY_PATH}form_basic_login.html`;
+
+const LOGINS_DATA = [
+ {
+ origin: "https://aurl.com",
+ username: "user1",
+ password: "pass1",
+ guid: Services.uuid.generateUUID().toString(),
+ },
+ {
+ origin: TEST_ORIGIN,
+ username: "user2",
+ password: "pass2",
+ guid: Services.uuid.generateUUID().toString(),
+ },
+ {
+ origin: TEST_ORIGIN,
+ username: "user3",
+ password: "pass3",
+ guid: Services.uuid.generateUUID().toString(),
+ },
+];
+
+const waitForAppMenu = async () => {
+ const appMenu = document.getElementById("appMenu-popup");
+ const appMenuButton = document.getElementById("PanelUI-menu-button");
+ await TestUtils.waitForCondition(
+ () => BrowserTestUtils.is_visible(appMenuButton),
+ "App menu button should be visible."
+ );
+
+ let popupshown = BrowserTestUtils.waitForEvent(appMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(appMenuButton, {});
+ await popupshown;
+ Assert.equal(
+ appMenu.state,
+ "open",
+ `Menu panel (${appMenu.id}) should be visible`
+ );
+};
+
+const isExpectedLoginItemSelected = async ({ expectedGuid }) => {
+ const loginList = content.document.querySelector("login-list").shadowRoot;
+
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ loginList.querySelector("li[aria-selected='true']")?.dataset?.guid ===
+ expectedGuid,
+ "Wait for login item to be selected"
+ );
+
+ Assert.equal(
+ loginList.querySelector("li[aria-selected='true']")?.dataset?.guid,
+ expectedGuid,
+ "Expected login is preselected"
+ );
+};
+
+add_setup(async () => {
+ await Services.logins.addLogins(
+ LOGINS_DATA.map(login => LoginTestUtils.testData.formLogin(login))
+ );
+});
+
+add_task(async function test_about_logins_defaults_to_first_item() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:logins#random-guid",
+ },
+ async function (gBrowser) {
+ await SpecialPowers.spawn(
+ gBrowser,
+ [{ expectedGuid: LOGINS_DATA[0].guid }],
+ isExpectedLoginItemSelected
+ );
+
+ Assert.ok(
+ true,
+ "First item of the list is selected when random hash is supplied."
+ );
+ }
+ );
+});
+
+add_task(
+ async function test_gear_icon_opens_about_logins_with_preselected_login() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ const popup = document.getElementById("PopupAutoComplete");
+
+ await openACPopup(popup, browser, "#form-basic-username");
+
+ const secondLoginItem = popup.firstChild.getItemAtIndex(1);
+ const secondLoginItemSettingsIcon = secondLoginItem.querySelector(
+ ".ac-settings-button"
+ );
+
+ Assert.ok(
+ !secondLoginItemSettingsIcon.checkVisibility({
+ checkVisibilityCSS: true,
+ }),
+ "Gear icon should not be visible initially"
+ );
+
+ await EventUtils.synthesizeKey("KEY_ArrowDown");
+ await EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ await BrowserTestUtils.waitForCondition(
+ () => secondLoginItem.attributes.selected,
+ "Wait for second login to become active"
+ );
+
+ Assert.ok(
+ secondLoginItemSettingsIcon.checkVisibility({
+ checkVisibilityCSS: true,
+ }),
+ "Gear icon should be visible when login item is active"
+ );
+
+ const aboutLoginsTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ url => url.includes(ABOUT_LOGINS_ORIGIN),
+ true
+ );
+
+ EventUtils.synthesizeMouseAtCenter(secondLoginItemSettingsIcon, {});
+ const aboutLoginsTab = await aboutLoginsTabPromise;
+
+ await SpecialPowers.spawn(
+ aboutLoginsTab.linkedBrowser,
+ [{ expectedGuid: LOGINS_DATA[2].guid }],
+ isExpectedLoginItemSelected
+ );
+
+ await closePopup(popup);
+ gBrowser.removeTab(aboutLoginsTab);
+ }
+ );
+ }
+);
+
+add_task(
+ async function test_password_menu_opens_about_logins_with_preselected_login() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ await waitForAppMenu();
+
+ const appMenuPasswordsButton = document.getElementById(
+ "appMenu-passwords-button"
+ );
+
+ const aboutLoginsTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ url => url.includes(ABOUT_LOGINS_ORIGIN),
+ true
+ );
+
+ EventUtils.synthesizeMouseAtCenter(appMenuPasswordsButton, {});
+
+ const aboutLoginsTab = await aboutLoginsTabPromise;
+
+ await SpecialPowers.spawn(
+ aboutLoginsTab.linkedBrowser,
+ [{ expectedGuid: LOGINS_DATA[1].guid }],
+ isExpectedLoginItemSelected
+ );
+
+ gBrowser.removeTab(aboutLoginsTab);
+ }
+ );
+ }
+);
diff --git a/toolkit/components/passwordmgr/test/browser/browser_private_window.js b/toolkit/components/passwordmgr/test/browser/browser_private_window.js
new file mode 100644
index 0000000000..513549fbbe
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_private_window.js
@@ -0,0 +1,954 @@
+"use strict";
+
+async function focusWindow(win) {
+ if (Services.focus.activeWindow == win) {
+ return;
+ }
+ let promise = new Promise(resolve => {
+ win.addEventListener(
+ "focus",
+ function () {
+ resolve();
+ },
+ { capture: true, once: true }
+ );
+ });
+ win.focus();
+ await promise;
+}
+
+function getDialogDoc() {
+ // Trudge through all the open windows, until we find the one
+ // that has either commonDialog.xhtml or selectDialog.xhtml loaded.
+ // var enumerator = Services.wm.getEnumerator("navigator:browser");
+ for (let { docShell } of Services.wm.getEnumerator(null)) {
+ var containedDocShells = docShell.getAllDocShellsInSubtree(
+ docShell.typeChrome,
+ docShell.ENUMERATE_FORWARDS
+ );
+ for (let childDocShell of containedDocShells) {
+ // Get the corresponding document for this docshell
+ // We don't want it if it's not done loading.
+ if (childDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE) {
+ continue;
+ }
+ var childDoc = childDocShell.contentViewer.DOMDocument;
+ if (
+ childDoc.location.href !=
+ "chrome://global/content/commonDialog.xhtml" &&
+ childDoc.location.href != "chrome://global/content/selectDialog.xhtml"
+ ) {
+ continue;
+ }
+
+ // We're expecting the dialog to be focused. If it's not yet, try later.
+ // (In particular, this is needed on Linux to reliably check focused elements.)
+ if (Services.focus.focusedWindow != childDoc.defaultView) {
+ continue;
+ }
+
+ return childDoc;
+ }
+ }
+
+ return null;
+}
+
+async function waitForAuthPrompt() {
+ let promptDoc = await TestUtils.waitForCondition(() => {
+ return getAuthPrompt();
+ });
+ info("Got prompt: " + promptDoc);
+ return promptDoc;
+}
+
+function getAuthPrompt() {
+ let doc = getDialogDoc();
+ if (!doc) {
+ return false; // try again in a bit
+ }
+ return doc;
+}
+
+async function loadAccessRestrictedURL(browser, url, username, password) {
+ let browserLoaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.loadURIString(browser, url);
+
+ // Wait for the auth prompt, enter the login details and close the prompt
+ await PromptTestUtils.handleNextPrompt(
+ browser,
+ { modalType: authPromptModalType, promptType: "promptUserAndPass" },
+ { buttonNumClick: 0, loginInput: username, passwordInput: password }
+ );
+
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+ await browserLoaded;
+}
+
+const PRIVATE_BROWSING_CAPTURE_PREF = "signon.privateBrowsingCapture.enabled";
+let nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo,
+ "init"
+);
+let login = new nsLoginInfo(
+ "https://example.com",
+ "https://example.com",
+ null,
+ "notifyu1",
+ "notifyp1",
+ "user",
+ "pass"
+);
+const form1Url = `https://example.com/${DIRECTORY_PATH}subtst_privbrowsing_1.html`;
+const form2Url = `https://example.com/${DIRECTORY_PATH}form_password_change.html`;
+const authUrl = `https://example.com/${DIRECTORY_PATH}authenticate.sjs`;
+
+let normalWin;
+let privateWin;
+let authPromptModalType;
+
+// XXX: Note that tasks are currently run in sequence. Some tests may assume the state
+// resulting from successful or unsuccessful logins in previous tasks
+
+add_task(async function test_setup() {
+ authPromptModalType = Services.prefs.getIntPref("prompts.modalType.httpAuth");
+ normalWin = await BrowserTestUtils.openNewBrowserWindow({ private: false });
+ privateWin = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ Services.logins.removeAllUserFacingLogins();
+});
+
+add_task(async function test_normal_popup_notification_1() {
+ info("test 1: run outside of private mode, popup notification should appear");
+ await focusWindow(normalWin);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: normalWin.gBrowser,
+ url: form1Url,
+ },
+ async function (browser) {
+ let fieldValues = await submitFormAndGetResults(
+ browser,
+ "formsubmit.sjs",
+ {
+ "#user": "notifyu1",
+ "#pass": "notifyp1",
+ }
+ );
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+
+ let notif = getCaptureDoorhanger(
+ "password-save",
+ PopupNotifications,
+ browser
+ );
+ Assert.ok(notif, "got notification popup");
+ if (notif) {
+ await TestUtils.waitForCondition(
+ () => !notif.dismissed,
+ "notification should not be dismissed"
+ );
+ await cleanupDoorhanger(notif);
+ }
+ }
+ );
+});
+
+add_task(async function test_private_popup_notification_2() {
+ info(
+ "test 2: run inside of private mode, dismissed popup notification should appear"
+ );
+
+ const capturePrefValue = Services.prefs.getBoolPref(
+ PRIVATE_BROWSING_CAPTURE_PREF
+ );
+ Assert.ok(
+ capturePrefValue,
+ `Expect ${PRIVATE_BROWSING_CAPTURE_PREF} to default to true`
+ );
+
+ // clear existing logins for parity with the previous test
+ Services.logins.removeAllUserFacingLogins();
+ await focusWindow(privateWin);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: privateWin.gBrowser,
+ url: form1Url,
+ },
+ async function (browser) {
+ let fieldValues = await submitFormAndGetResults(
+ browser,
+ "formsubmit.sjs",
+ {
+ "#user": "notifyu1",
+ "#pass": "notifyp1",
+ }
+ );
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+
+ let notif = getCaptureDoorhanger(
+ "password-save",
+ PopupNotifications,
+ browser
+ );
+ Assert.ok(notif, "Expected notification popup");
+ if (notif) {
+ await TestUtils.waitForCondition(
+ () => notif.dismissed,
+ "notification should be dismissed"
+ );
+
+ let { panel } = privateWin.PopupNotifications;
+ let promiseShown = BrowserTestUtils.waitForEvent(panel, "popupshown");
+ notif.anchorElement.click();
+ await promiseShown;
+
+ let notificationElement = panel.childNodes[0];
+ let toggleCheckbox = notificationElement.querySelector(
+ "#password-notification-visibilityToggle"
+ );
+
+ Assert.ok(
+ !toggleCheckbox.hidden,
+ "Toggle should be visible upon 1st opening"
+ );
+
+ info("Hiding popup.");
+ let promiseHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ panel.hidePopup();
+ await promiseHidden;
+
+ info("Clicking on anchor to reshow popup.");
+ promiseShown = BrowserTestUtils.waitForEvent(panel, "popupshown");
+ notif.anchorElement.click();
+ await promiseShown;
+
+ Assert.ok(
+ toggleCheckbox.hidden,
+ "Toggle should be hidden upon 2nd opening"
+ );
+
+ await cleanupDoorhanger(notif);
+ }
+ }
+ );
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 0,
+ "No logins were saved"
+ );
+});
+
+add_task(async function test_private_popup_notification_no_capture_pref_2b() {
+ info(
+ "test 2b: run inside of private mode, with capture pref off," +
+ "popup notification should not appear"
+ );
+
+ const capturePrefValue = Services.prefs.getBoolPref(
+ PRIVATE_BROWSING_CAPTURE_PREF
+ );
+ Services.prefs.setBoolPref(PRIVATE_BROWSING_CAPTURE_PREF, false);
+
+ // clear existing logins for parity with the previous test
+ Services.logins.removeAllUserFacingLogins();
+
+ await focusWindow(privateWin);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: privateWin.gBrowser,
+ url: form1Url,
+ },
+ async function (browser) {
+ let fieldValues = await submitFormAndGetResults(
+ browser,
+ "formsubmit.sjs",
+ {
+ "#user": "notifyu1",
+ "#pass": "notifyp1",
+ }
+ );
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+
+ let notif = getCaptureDoorhanger(
+ "password-save",
+ PopupNotifications,
+ browser
+ );
+ // restore the pref to its original value
+ Services.prefs.setBoolPref(
+ PRIVATE_BROWSING_CAPTURE_PREF,
+ capturePrefValue
+ );
+
+ Assert.ok(!notif, "Expected no notification popup");
+ if (notif) {
+ await cleanupDoorhanger(notif);
+ }
+ }
+ );
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 0,
+ "No logins were saved"
+ );
+});
+
+add_task(async function test_normal_popup_notification_3() {
+ info(
+ "test 3: run with a login, outside of private mode, " +
+ "match existing username/password: no popup notification should appear"
+ );
+
+ Services.logins.removeAllUserFacingLogins();
+ await Services.logins.addLoginAsync(login);
+ let allLogins = Services.logins.getAllLogins();
+ // Sanity check the HTTP login exists.
+ Assert.equal(allLogins.length, 1, "Should have the HTTP login");
+ let timeLastUsed = allLogins[0].timeLastUsed;
+ let loginGuid = allLogins[0].guid;
+
+ await focusWindow(normalWin);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: normalWin.gBrowser,
+ url: form1Url,
+ },
+ async function (browser) {
+ let fieldValues = await submitFormAndGetResults(
+ browser,
+ "formsubmit.sjs",
+ {
+ "#user": "notifyu1",
+ "#pass": "notifyp1",
+ }
+ );
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+
+ let notif = getCaptureDoorhanger("any", PopupNotifications, browser);
+ Assert.ok(!notif, "got no notification popup");
+ if (notif) {
+ await cleanupDoorhanger(notif);
+ }
+ }
+ );
+ allLogins = Services.logins.getAllLogins();
+ Assert.equal(
+ allLogins[0].guid,
+ loginGuid,
+ "Sanity-check we are comparing the same login record"
+ );
+ Assert.ok(
+ allLogins[0].timeLastUsed > timeLastUsed,
+ "The timeLastUsed timestamp has been updated"
+ );
+});
+
+add_task(async function test_private_popup_notification_3b() {
+ info(
+ "test 3b: run with a login, in private mode," +
+ " match existing username/password: no popup notification should appear"
+ );
+
+ Services.logins.removeAllUserFacingLogins();
+ await Services.logins.addLoginAsync(login);
+ let allLogins = Services.logins.getAllLogins();
+ // Sanity check the HTTP login exists.
+ Assert.equal(allLogins.length, 1, "Should have the HTTP login");
+ let timeLastUsed = allLogins[0].timeLastUsed;
+ let loginGuid = allLogins[0].guid;
+
+ await focusWindow(privateWin);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: privateWin.gBrowser,
+ url: form1Url,
+ },
+ async function (browser) {
+ let fieldValues = await submitFormAndGetResults(
+ browser,
+ "formsubmit.sjs",
+ {
+ "#user": "notifyu1",
+ "#pass": "notifyp1",
+ }
+ );
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+
+ let notif = getCaptureDoorhanger("any", PopupNotifications, browser);
+
+ Assert.ok(!notif, "got no notification popup");
+ if (notif) {
+ await cleanupDoorhanger(notif);
+ }
+ }
+ );
+ allLogins = Services.logins.getAllLogins();
+ Assert.equal(
+ allLogins[0].guid,
+ loginGuid,
+ "Sanity-check we are comparing the same login record"
+ );
+ Assert.equal(
+ allLogins[0].timeLastUsed,
+ timeLastUsed,
+ "The timeLastUsed timestamp has not been updated"
+ );
+});
+
+add_task(async function test_normal_new_password_4() {
+ info(
+ "test 4: run with a login, outside of private mode," +
+ " add a new password: popup notification should appear"
+ );
+ Services.logins.removeAllUserFacingLogins();
+ await Services.logins.addLoginAsync(login);
+ let allLogins = Services.logins.getAllLogins();
+ // Sanity check the HTTP login exists.
+ Assert.equal(allLogins.length, 1, "Should have the HTTP login");
+ let timeLastUsed = allLogins[0].timeLastUsed;
+ let loginGuid = allLogins[0].guid;
+
+ await focusWindow(normalWin);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: normalWin.gBrowser,
+ url: form2Url,
+ },
+ async function (browser) {
+ let fieldValues = await submitFormAndGetResults(
+ browser,
+ "formsubmit.sjs",
+ {
+ "#pass": "notifyp1",
+ "#newpass": "notifyp2",
+ }
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = getCaptureDoorhanger(
+ "password-change",
+ PopupNotifications,
+ browser
+ );
+ Assert.ok(notif, "got notification popup");
+ if (notif) {
+ await TestUtils.waitForCondition(
+ () => !notif.dismissed,
+ "notification should not be dismissed"
+ );
+ await cleanupDoorhanger(notif);
+ }
+ }
+ );
+ // We put up a doorhanger, but didn't interact with it, so we expect the login timestamps
+ // to be unchanged
+ allLogins = Services.logins.getAllLogins();
+ Assert.equal(
+ allLogins[0].guid,
+ loginGuid,
+ "Sanity-check we are comparing the same login record"
+ );
+ Assert.equal(
+ allLogins[0].timeLastUsed,
+ timeLastUsed,
+ "The timeLastUsed timestamp was not updated"
+ );
+});
+
+add_task(async function test_private_new_password_5() {
+ info(
+ "test 5: run with a login, in private mode," +
+ "add a new password: popup notification should appear"
+ );
+
+ const capturePrefValue = Services.prefs.getBoolPref(
+ PRIVATE_BROWSING_CAPTURE_PREF
+ );
+ Assert.ok(
+ capturePrefValue,
+ `Expect ${PRIVATE_BROWSING_CAPTURE_PREF} to default to true`
+ );
+
+ let allLogins = Services.logins.getAllLogins();
+ // Sanity check the HTTP login exists.
+ Assert.equal(allLogins.length, 1, "Should have the HTTP login");
+ let timeLastUsed = allLogins[0].timeLastUsed;
+ let loginGuid = allLogins[0].guid;
+
+ await focusWindow(privateWin);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: privateWin.gBrowser,
+ url: form2Url,
+ },
+ async function (browser) {
+ let fieldValues = await submitFormAndGetResults(
+ browser,
+ "formsubmit.sjs",
+ {
+ "#pass": "notifyp1",
+ "#newpass": "notifyp2",
+ }
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = getCaptureDoorhanger(
+ "password-change",
+ PopupNotifications,
+ browser
+ );
+ Assert.ok(notif, "Expected notification popup");
+ if (notif) {
+ await TestUtils.waitForCondition(
+ () => !notif.dismissed,
+ "notification should not be dismissed"
+ );
+ await cleanupDoorhanger(notif);
+ }
+ }
+ );
+ // We put up a doorhanger, but didn't interact with it, so we expect the login timestamps
+ // to be unchanged
+ allLogins = Services.logins.getAllLogins();
+ Assert.equal(
+ allLogins[0].guid,
+ loginGuid,
+ "Sanity-check we are comparing the same login record"
+ );
+ Assert.equal(
+ allLogins[0].timeLastUsed,
+ timeLastUsed,
+ "The timeLastUsed timestamp has not been updated"
+ );
+});
+
+add_task(async function test_normal_with_login_6() {
+ info(
+ "test 6: run with a login, outside of private mode, " +
+ "submit with an existing password (from test 4): popup notification should appear"
+ );
+
+ await focusWindow(normalWin);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: normalWin.gBrowser,
+ url: form2Url,
+ },
+ async function (browser) {
+ let fieldValues = await submitFormAndGetResults(
+ browser,
+ "formsubmit.sjs",
+ {
+ "#pass": "notifyp1",
+ "#newpass": "notifyp2",
+ }
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ let notif = getCaptureDoorhanger(
+ "password-change",
+ PopupNotifications,
+ browser
+ );
+ Assert.ok(notif, "got notification popup");
+ if (notif) {
+ await TestUtils.waitForCondition(
+ () => !notif.dismissed,
+ "notification should not be dismissed"
+ );
+ await cleanupDoorhanger(notif);
+ }
+ Services.logins.removeLogin(login);
+ }
+ );
+});
+
+add_task(async function test_normal_autofilled_7() {
+ info("test 7: verify that the user/pass pair was autofilled");
+ await Services.logins.addLoginAsync(login);
+
+ // Sanity check the HTTP login exists.
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 1,
+ "Should have the HTTP login"
+ );
+
+ await focusWindow(normalWin);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: normalWin.gBrowser,
+ url: "about:blank",
+ },
+ async function (browser) {
+ // Add the observer before loading the form page
+ let formFilled = listenForTestNotification("FormProcessed");
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+ BrowserTestUtils.loadURIString(browser, form1Url);
+ await formFilled;
+
+ // the form should have been autofilled, so submit without updating field values
+ let fieldValues = await submitFormAndGetResults(
+ browser,
+ "formsubmit.sjs",
+ {}
+ );
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ }
+ );
+});
+
+add_task(async function test_private_not_autofilled_8() {
+ info("test 8: verify that the user/pass pair was not autofilled");
+ // Sanity check the HTTP login exists.
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 1,
+ "Should have the HTTP login"
+ );
+
+ let formFilled = listenForTestNotification("FormProcessed");
+
+ await focusWindow(privateWin);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: privateWin.gBrowser,
+ url: form1Url,
+ },
+ async function (browser) {
+ await formFilled;
+ let fieldValues = await submitFormAndGetResults(
+ browser,
+ "formsubmit.sjs",
+ {}
+ );
+ Assert.ok(!fieldValues.username, "Checking submitted username");
+ Assert.ok(!fieldValues.password, "Checking submitted password");
+ }
+ );
+});
+
+// Disabled for Bug 1523777
+// add_task(async function test_private_autocomplete_9() {
+// info("test 9: verify that the user/pass pair was available for autocomplete");
+// // Sanity check the HTTP login exists.
+// Assert.equal(Services.logins.getAllLogins().length, 1, "Should have the HTTP login");
+
+// await focusWindow(privateWin);
+// await BrowserTestUtils.withNewTab({
+// gBrowser: privateWin.gBrowser,
+// url: form1Url,
+// }, async function(browser) {
+// let popup = document.getElementById("PopupAutoComplete");
+// Assert.ok(popup, "Got popup");
+
+// let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+
+// // focus the user field. This should trigger the autocomplete menu
+// await ContentTask.spawn(browser, null, async function() {
+// content.document.getElementById("user").focus();
+// });
+// await promiseShown;
+// Assert.ok(promiseShown, "autocomplete shown");
+
+// let promiseFormInput = ContentTask.spawn(browser, null, async function() {
+// let doc = content.document;
+// await new Promise(resolve => {
+// doc.getElementById("form").addEventListener("input", resolve, { once: true });
+// });
+// });
+// info("sending keys");
+// // select the item and hit enter to fill the form
+// await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+// await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
+// await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+// await promiseFormInput;
+
+// let fieldValues = await submitFormAndGetResults(browser, "formsubmit.sjs", {});
+// Assert.equal(fieldValues.username, "notifyu1", "Checking submitted username");
+// Assert.equal(fieldValues.password, "notifyp1", "Checking submitted password");
+// });
+// });
+
+add_task(async function test_normal_autofilled_10() {
+ info(
+ "test 10: verify that the user/pass pair does get autofilled in non-private window"
+ );
+ // Sanity check the HTTP login exists.
+ Assert.equal(
+ Services.logins.getAllLogins().length,
+ 1,
+ "Should have the HTTP login"
+ );
+
+ let formFilled = listenForTestNotification("FormProcessed");
+
+ await focusWindow(normalWin);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: normalWin.gBrowser,
+ url: form1Url,
+ },
+ async function (browser) {
+ await formFilled;
+ let fieldValues = await submitFormAndGetResults(
+ browser,
+ "formsubmit.sjs",
+ {}
+ );
+ Assert.equal(
+ fieldValues.username,
+ "notifyu1",
+ "Checking submitted username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "notifyp1",
+ "Checking submitted password"
+ );
+ }
+ );
+});
+
+add_task(async function test_normal_http_basic_auth() {
+ info(
+ "test normal/basic-auth: verify that we get a doorhanger after basic-auth login"
+ );
+ Services.logins.removeAllUserFacingLogins();
+ clearHttpAuths();
+
+ await focusWindow(normalWin);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: normalWin.gBrowser,
+ url: "https://example.com",
+ },
+ async function (browser) {
+ await loadAccessRestrictedURL(browser, authUrl, "test", "testpass");
+ Assert.ok(true, "Auth-required page loaded");
+
+ // verify result in the response document
+ let fieldValues = await SpecialPowers.spawn(
+ browser,
+ [[]],
+ async function () {
+ let username = content.document.getElementById("user").textContent;
+ let password = content.document.getElementById("pass").textContent;
+ let ok = content.document.getElementById("ok").textContent;
+ return {
+ username,
+ password,
+ ok,
+ };
+ }
+ );
+ Assert.equal(fieldValues.ok, "PASS", "Checking authorization passed");
+ Assert.equal(
+ fieldValues.username,
+ "test",
+ "Checking authorized username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "testpass",
+ "Checking authorized password"
+ );
+
+ let notif = getCaptureDoorhanger(
+ "password-save",
+ PopupNotifications,
+ browser
+ );
+ Assert.ok(notif, "got notification popup");
+ if (notif) {
+ await TestUtils.waitForCondition(
+ () => !notif.dismissed,
+ "notification should not be dismissed"
+ );
+ await cleanupDoorhanger(notif);
+ }
+ }
+ );
+});
+
+add_task(async function test_private_http_basic_auth() {
+ info(
+ "test private/basic-auth: verify that we don't get a doorhanger after basic-auth login"
+ );
+ Services.logins.removeAllUserFacingLogins();
+ clearHttpAuths();
+
+ const capturePrefValue = Services.prefs.getBoolPref(
+ PRIVATE_BROWSING_CAPTURE_PREF
+ );
+ Assert.ok(
+ capturePrefValue,
+ `Expect ${PRIVATE_BROWSING_CAPTURE_PREF} to default to true`
+ );
+
+ await focusWindow(privateWin);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: privateWin.gBrowser,
+ url: "https://example.com",
+ },
+ async function (browser) {
+ await loadAccessRestrictedURL(browser, authUrl, "test", "testpass");
+
+ let fieldValues = await getFormSubmitResponseResult(
+ browser,
+ "authenticate.sjs"
+ );
+ Assert.equal(
+ fieldValues.username,
+ "test",
+ "Checking authorized username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "testpass",
+ "Checking authorized password"
+ );
+
+ let notif = getCaptureDoorhanger(
+ "password-save",
+ PopupNotifications,
+ browser
+ );
+ Assert.ok(notif, "got notification popup");
+ if (notif) {
+ await TestUtils.waitForCondition(
+ () => notif.dismissed,
+ "notification should be dismissed"
+ );
+ await cleanupDoorhanger(notif);
+ }
+ }
+ );
+});
+
+add_task(async function test_private_http_basic_auth_no_capture_pref() {
+ info(
+ "test private/basic-auth: verify that we don't get a doorhanger after basic-auth login" +
+ "with capture pref off"
+ );
+
+ const capturePrefValue = Services.prefs.getBoolPref(
+ PRIVATE_BROWSING_CAPTURE_PREF
+ );
+ Services.prefs.setBoolPref(PRIVATE_BROWSING_CAPTURE_PREF, false);
+
+ Services.logins.removeAllUserFacingLogins();
+ clearHttpAuths();
+
+ await focusWindow(privateWin);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: privateWin.gBrowser,
+ url: "https://example.com",
+ },
+ async function (browser) {
+ await loadAccessRestrictedURL(browser, authUrl, "test", "testpass");
+
+ let fieldValues = await getFormSubmitResponseResult(
+ browser,
+ "authenticate.sjs"
+ );
+ Assert.equal(
+ fieldValues.username,
+ "test",
+ "Checking authorized username"
+ );
+ Assert.equal(
+ fieldValues.password,
+ "testpass",
+ "Checking authorized password"
+ );
+
+ let notif = getCaptureDoorhanger(
+ "password-save",
+ PopupNotifications,
+ browser
+ );
+ // restore the pref to its original value
+ Services.prefs.setBoolPref(
+ PRIVATE_BROWSING_CAPTURE_PREF,
+ capturePrefValue
+ );
+
+ Assert.ok(!notif, "got no notification popup");
+ if (notif) {
+ await cleanupDoorhanger(notif);
+ }
+ }
+ );
+});
+
+add_task(async function test_cleanup() {
+ await BrowserTestUtils.closeWindow(normalWin);
+ await BrowserTestUtils.closeWindow(privateWin);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_proxyAuth_prompt.js b/toolkit/components/passwordmgr/test/browser/browser_proxyAuth_prompt.js
new file mode 100644
index 0000000000..478f204581
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_proxyAuth_prompt.js
@@ -0,0 +1,182 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+let proxyChannel;
+
+function initProxy() {
+ return new Promise(resolve => {
+ let proxyChannel;
+
+ let proxyCallback = {
+ QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyCallback"]),
+
+ onProxyAvailable(req, uri, pi, status) {
+ class ProxyChannelListener {
+ onStartRequest(request) {
+ resolve(proxyChannel);
+ }
+ onStopRequest(request, status) {}
+ }
+ // 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
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null, // aTriggeringPrincipal
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ proxyChannel.asyncOpen(new ProxyChannelListener());
+ },
+ };
+
+ // 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(
+ "https://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
+ );
+ pps.asyncResolve(channel, 0, proxyCallback);
+ });
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ // This test relies on tab auth prompts.
+ set: [["prompts.modalType.httpAuth", Services.prompt.MODAL_TYPE_TAB]],
+ });
+ proxyChannel = await initProxy();
+});
+
+/**
+ * Create an object for consuming an nsIAuthPromptCallback.
+ * @returns result
+ * @returns {nsIAuthPromptCallback} result.callback - Callback to be passed into
+ * asyncPromptAuth.
+ * @returns {Promise} result.promise - Promise which resolves with authInfo once
+ * the callback has been called.
+ */
+function getAuthPromptCallback() {
+ let callbackResolver;
+ let promise = new Promise(resolve => {
+ callbackResolver = resolve;
+ });
+ let callback = {
+ onAuthAvailable(context, authInfo) {
+ callbackResolver(authInfo);
+ },
+ };
+ return { callback, promise };
+}
+
+/**
+ * Tests that if a window proxy auth prompt is open, subsequent auth calls with
+ * matching realm will be merged into the existing prompt. This should work even
+ * if the follwing auth call has browser parent.
+ */
+add_task(async function testProxyAuthPromptMerge() {
+ let tabA = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ let tabB = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.org"
+ );
+
+ const promptFac = Cc[
+ "@mozilla.org/passwordmanager/authpromptfactory;1"
+ ].getService(Ci.nsIPromptFactory);
+ let prompter = promptFac.getPrompt(window, Ci.nsIAuthPrompt2);
+
+ let level = Ci.nsIAuthPrompt2.LEVEL_NONE;
+ let proxyAuthinfo = {
+ username: "",
+ password: "",
+ domain: "",
+ flags: Ci.nsIAuthInformation.AUTH_PROXY,
+ authenticationScheme: "basic",
+ realm: "",
+ };
+
+ // The next prompt call will result in window prompt, because the prompt does
+ // not have a browser set yet. The browser is used as a parent for tab
+ // prompts.
+ let promptOpened = PromptTestUtils.waitForPrompt(null, {
+ modalType: Services.prompt.MODAL_TYPE_WINDOW,
+ });
+ let cbWinPrompt = getAuthPromptCallback();
+ info("asyncPromptAuth no parent");
+ prompter.asyncPromptAuth(
+ proxyChannel,
+ cbWinPrompt.callback,
+ null,
+ level,
+ proxyAuthinfo
+ );
+ let prompt = await promptOpened;
+
+ // Set a browser so the next prompt would open as tab prompt.
+ prompter.QueryInterface(Ci.nsILoginManagerAuthPrompter).browser =
+ tabA.linkedBrowser;
+
+ // Since we already have an open window prompts, subsequent calls with
+ // matching realm should be merged into this prompt and no additional prompts
+ // must be spawned.
+ let cbNoPrompt = getAuthPromptCallback();
+ info("asyncPromptAuth tabA parent");
+ prompter.asyncPromptAuth(
+ proxyChannel,
+ cbNoPrompt.callback,
+ null,
+ level,
+ proxyAuthinfo
+ );
+
+ // Switch to the next tabs browser.
+ prompter.QueryInterface(Ci.nsILoginManagerAuthPrompter).browser =
+ tabB.linkedBrowser;
+
+ let cbNoPrompt2 = getAuthPromptCallback();
+ info("asyncPromptAuth tabB parent");
+ prompter.asyncPromptAuth(
+ proxyChannel,
+ cbNoPrompt2.callback,
+ null,
+ level,
+ proxyAuthinfo
+ );
+
+ // Accept the prompt.
+ PromptTestUtils.handlePrompt(prompt, {});
+
+ // Accepting the window prompts should complete all auth requests.
+ let authInfo1 = await cbWinPrompt.promise;
+ Assert.ok(authInfo1, "Received callback from first proxy auth call.");
+ let authInfo2 = await cbNoPrompt.promise;
+ Assert.ok(authInfo2, "Received callback from second proxy auth call.");
+ let authInfo3 = await cbNoPrompt2.promise;
+ Assert.ok(authInfo3, "Received callback from third proxy auth call.");
+
+ BrowserTestUtils.removeTab(tabA);
+ BrowserTestUtils.removeTab(tabB);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_relay_telemetry.js b/toolkit/components/passwordmgr/test/browser/browser_relay_telemetry.js
new file mode 100644
index 0000000000..4a6cee3b43
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_relay_telemetry.js
@@ -0,0 +1,514 @@
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+const { getFxAccountsSingleton } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+);
+const { FirefoxRelayTelemetry } = ChromeUtils.importESModule(
+ "resource://gre/modules/FirefoxRelayTelemetry.mjs"
+);
+
+const gFxAccounts = getFxAccountsSingleton();
+let gRelayACOptionsTitles;
+let gHttpServer;
+
+const TEST_URL_PATH = `https://example.org${DIRECTORY_PATH}form_basic_signup.html`;
+
+const MOCK_MASKS = [
+ {
+ full_address: "email1@mozilla.com",
+ description: "Email 1 Description",
+ enabled: true,
+ },
+ {
+ full_address: "email2@mozilla.com",
+ description: "Email 2 Description",
+ enabled: false,
+ },
+ {
+ full_address: "email3@mozilla.com",
+ description: "Email 3 Description",
+ enabled: true,
+ },
+];
+
+const SERVER_SCENARIOS = {
+ free_tier_limit: {
+ "/relayaddresses/": {
+ POST: (request, response) => {
+ response.setStatusLine(request.httpVersion, 403);
+ response.write(JSON.stringify({ error_code: "free_tier_limit" }));
+ },
+ GET: (_, response) => {
+ response.write(JSON.stringify(MOCK_MASKS));
+ },
+ },
+ },
+ unknown_error: {
+ "/relayaddresses/": {
+ default: (request, response) => {
+ response.setStatusLine(request.httpVersion, 408);
+ },
+ },
+ },
+
+ default: {
+ default: (request, response) => {
+ response.setStatusLine(request.httpVersion, 200);
+ response.write(JSON.stringify({ foo: "bar" }));
+ },
+ },
+};
+
+const simpleRouter = scenarioName => (request, response) => {
+ const routeHandler =
+ SERVER_SCENARIOS[scenarioName][request._path] ?? SERVER_SCENARIOS.default;
+ const methodHandler =
+ routeHandler?.[request._method] ??
+ routeHandler.default ??
+ SERVER_SCENARIOS.default.default;
+ methodHandler(request, response);
+};
+const setupServerScenario = (scenarioName = "default") =>
+ gHttpServer.registerPrefixHandler("/", simpleRouter(scenarioName));
+
+const setupRelayScenario = async scenarioName => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.firefoxRelay.feature", scenarioName]],
+ });
+ Services.telemetry.clearEvents();
+};
+
+const waitForEvents = async expectedEvents =>
+ TestUtils.waitForCondition(
+ () => {
+ const snapshots = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+
+ return (snapshots.parent?.length ?? 0) >= (expectedEvents.length ?? 0);
+ },
+ "Wait for telemetry to be collected",
+ 100,
+ 100
+ );
+
+async function assertEvents(expectedEvents) {
+ // To avoid intermittent failures, we wait for telemetry to be collected
+ await waitForEvents(expectedEvents);
+ const events = TelemetryTestUtils.getEvents(
+ { category: "relay_integration" },
+ { process: "parent" }
+ );
+
+ for (let i = 0; i < expectedEvents.length; i++) {
+ const keysInExpectedEvent = Object.keys(expectedEvents[i]);
+ keysInExpectedEvent.forEach(key => {
+ const assertFn =
+ typeof events[i][key] === "object"
+ ? Assert.deepEqual.bind(Assert)
+ : Assert.equal.bind(Assert);
+ assertFn(
+ events[i][key],
+ expectedEvents[i][key],
+ `Key value for ${key} should match`
+ );
+ });
+ }
+}
+
+async function openRelayAC(browser) {
+ // In rare cases, especially in chaos mode in verify tests, some events creep in.
+ // Clear them out before we start.
+ Services.telemetry.clearEvents();
+ const popup = document.getElementById("PopupAutoComplete");
+ await openACPopup(popup, browser, "#form-basic-username");
+ const popupItem = document
+ .querySelector("richlistitem")
+ .getAttribute("ac-label");
+ const popupItemTitle = JSON.parse(popupItem).title;
+
+ Assert.ok(
+ gRelayACOptionsTitles.some(title => title.value === popupItemTitle),
+ "AC Popup has an item Relay option shown in popup"
+ );
+
+ const promiseHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ popup.firstChild.getItemAtIndex(0).click();
+ await promiseHidden;
+}
+
+add_setup(async function () {
+ gHttpServer = new HttpServer();
+ setupServerScenario();
+
+ gHttpServer.start(-1);
+
+ const API_ENDPOINT = `http://localhost:${gHttpServer.identity.primaryPort}/`;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.firefoxRelay.feature", "available"],
+ ["signon.firefoxRelay.base_url", API_ENDPOINT],
+ ],
+ });
+
+ sinon.stub(gFxAccounts, "hasLocalSession").returns(true);
+ sinon
+ .stub(gFxAccounts.constructor.config, "isProductionConfig")
+ .returns(true);
+ sinon.stub(gFxAccounts, "getOAuthToken").returns("MOCK_TOKEN");
+ sinon.stub(gFxAccounts, "getSignedInUser").returns({
+ email: "example@mozilla.com",
+ });
+
+ const canRecordExtendedOld = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ Services.telemetry.clearEvents();
+ Services.telemetry.setEventRecordingEnabled("relay_integration", true);
+
+ gRelayACOptionsTitles = await new Localization([
+ "browser/firefoxRelay.ftl",
+ "toolkit/branding/brandings.ftl",
+ ]).formatMessages([
+ "firefox-relay-opt-in-title-1",
+ "firefox-relay-use-mask-title",
+ ]);
+
+ registerCleanupFunction(async () => {
+ await new Promise(resolve => {
+ gHttpServer.stop(function () {
+ resolve();
+ });
+ });
+ Services.telemetry.setEventRecordingEnabled("relay_integration", false);
+ Services.telemetry.clearEvents();
+ Services.telemetry.canRecordExtended = canRecordExtendedOld;
+ sinon.restore();
+ });
+});
+
+add_task(async function test_pref_toggle() {
+ await setupRelayScenario("available");
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:preferences#privacy",
+ },
+ async function (browser) {
+ const relayIntegrationCheckbox = content.document.querySelector(
+ "checkbox#relayIntegration"
+ );
+ relayIntegrationCheckbox.click();
+ relayIntegrationCheckbox.click();
+ await assertEvents([
+ { object: "pref_change", method: "disabled" },
+ { object: "pref_change", method: "enabled" },
+ ]);
+ }
+ );
+});
+
+add_task(async function test_popup_option_optin_enabled() {
+ await setupRelayScenario("available");
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ await openRelayAC(browser);
+ const notificationPopup = document.getElementById("notification-popup");
+ const notificationShown = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "shown"
+ );
+ const notificationHidden = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "hidden"
+ );
+
+ await notificationShown;
+
+ notificationPopup
+ .querySelector("button.popup-notification-primary-button")
+ .click();
+
+ await notificationHidden;
+
+ await BrowserTestUtils.waitForEvent(
+ ConfirmationHint._panel,
+ "popuphidden"
+ );
+
+ await assertEvents([
+ {
+ object: "offer_relay",
+ method: "shown",
+ extra: { is_relay_user: "true", scenario: "SignUpFormScenario" },
+ },
+ {
+ object: "offer_relay",
+ method: "clicked",
+ extra: { is_relay_user: "true", scenario: "SignUpFormScenario" },
+ },
+ { object: "opt_in_panel", method: "shown" },
+ { object: "opt_in_panel", method: "enabled" },
+ {
+ object: "fill_username",
+ method: "shown",
+ extra: { error_code: "0" },
+ },
+ ]);
+ }
+ );
+});
+
+add_task(async function test_popup_option_optin_postponed() {
+ await setupRelayScenario("available");
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ await openRelayAC(browser);
+ const notificationPopup = document.getElementById("notification-popup");
+ const notificationShown = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "shown"
+ );
+ const notificationHidden = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "hidden"
+ );
+
+ await notificationShown;
+
+ notificationPopup
+ .querySelector("button.popup-notification-secondary-button")
+ .click();
+
+ await notificationHidden;
+
+ await assertEvents([
+ { object: "offer_relay", method: "shown" },
+ { object: "offer_relay", method: "clicked" },
+ { object: "opt_in_panel", method: "shown" },
+ { object: "opt_in_panel", method: "postponed" },
+ ]);
+ }
+ );
+});
+
+add_task(async function test_popup_option_optin_disabled() {
+ await setupRelayScenario("available");
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ await openRelayAC(browser);
+ const notificationPopup = document.getElementById("notification-popup");
+ const notificationShown = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "shown"
+ );
+ const notificationHidden = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "hidden"
+ );
+
+ await notificationShown;
+ const menupopup = notificationPopup.querySelector("menupopup");
+ const menuitem = menupopup.querySelector("menuitem");
+
+ menuitem.click();
+ await notificationHidden;
+
+ await assertEvents([
+ { object: "offer_relay", method: "shown" },
+ { object: "offer_relay", method: "clicked" },
+ { object: "opt_in_panel", method: "shown" },
+ { object: "opt_in_panel", method: "disabled" },
+ ]);
+ }
+ );
+});
+
+add_task(async function test_popup_option_fillusername() {
+ await setupRelayScenario("enabled");
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ await openRelayAC(browser);
+ await BrowserTestUtils.waitForEvent(
+ ConfirmationHint._panel,
+ "popuphidden"
+ );
+ await assertEvents([
+ { object: "fill_username", method: "shown" },
+ {
+ object: "fill_username",
+ method: "clicked",
+ },
+ ]);
+ }
+ );
+});
+
+add_task(async function test_fillusername_free_tier_limit() {
+ await setupRelayScenario("enabled");
+ setupServerScenario("free_tier_limit");
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ await openRelayAC(browser);
+
+ const notificationPopup = document.getElementById("notification-popup");
+ const notificationShown = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "shown"
+ );
+ const notificationHidden = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "hidden"
+ );
+
+ await notificationShown;
+ notificationPopup.querySelector(".reusable-relay-masks button").click();
+ await notificationHidden;
+
+ await assertEvents([
+ { object: "fill_username", method: "shown" },
+ {
+ object: "fill_username",
+ method: "clicked",
+ },
+ {
+ object: "fill_username",
+ method: "shown",
+ extra: { error_code: "free_tier_limit" },
+ },
+ {
+ object: "reuse_panel",
+ method: "shown",
+ },
+ {
+ object: "reuse_panel",
+ method: "reuse_mask",
+ },
+ ]);
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ const username = content.document.getElementById("form-basic-username");
+ Assert.equal(
+ username.value,
+ "email1@mozilla.com",
+ "Username field should be filled with the first mask"
+ );
+ });
+ }
+ );
+});
+
+add_task(async function test_fillusername_error() {
+ await setupRelayScenario("enabled");
+ setupServerScenario("unknown_error");
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ await openRelayAC(browser);
+
+ const notificationPopup = document.getElementById("notification-popup");
+ const notificationShown = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "shown"
+ );
+
+ await notificationShown;
+ Assert.equal(
+ notificationPopup.querySelector("popupnotification").id,
+ "relay-integration-error-notification",
+ "Error message should be displayed"
+ );
+
+ await assertEvents([
+ { object: "fill_username", method: "shown" },
+ {
+ object: "fill_username",
+ method: "clicked",
+ },
+ {
+ object: "reuse_panel",
+ method: "shown",
+ extra: { error_code: "408" },
+ },
+ ]);
+ }
+ );
+});
+
+add_task(async function test_auth_token_error() {
+ setupRelayScenario("enabled");
+ gFxAccounts.getOAuthToken.restore();
+ const oauthTokenStub = sinon.stub(gFxAccounts, "getOAuthToken").throws();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ await openRelayAC(browser);
+ const notificationPopup = document.getElementById("notification-popup");
+ const notificationShown = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "shown"
+ );
+ const notificationHidden = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "hidden"
+ );
+
+ await notificationShown;
+
+ notificationPopup
+ .querySelector("button.popup-notification-primary-button")
+ .click();
+
+ await notificationHidden;
+
+ await assertEvents([
+ {
+ object: "fill_username",
+ method: "shown",
+ extra: { error_code: "0" },
+ },
+ {
+ object: "fill_username",
+ method: "clicked",
+ extra: { error_code: "0" },
+ },
+ {
+ object: "fill_username",
+ method: "shown",
+ extra: { error_code: "418" },
+ },
+ ]);
+ }
+ );
+ oauthTokenStub.restore();
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_telemetry_SignUpFormRuleset.js b/toolkit/components/passwordmgr/test/browser/browser_telemetry_SignUpFormRuleset.js
new file mode 100644
index 0000000000..e1ea3af99a
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_telemetry_SignUpFormRuleset.js
@@ -0,0 +1,57 @@
+"use strict";
+
+const SIGNUP_DETECTION_HISTOGRAM = "PWMGR_SIGNUP_FORM_DETECTION_MS";
+const TEST_URL = `https://example.com${DIRECTORY_PATH}form_signup_detection.html`;
+
+/**
+ *
+ * @param {Object} histogramData The histogram data to examine
+ * @returns The amount of entries found in the histogram data
+ */
+function countEntries(histogramData) {
+ info(typeof histogramData);
+ return histogramData
+ ? Object.values(histogramData.values).reduce((a, b) => a + b, 0)
+ : null;
+}
+
+/**
+ * @param {String} id The histogram to examine
+ * @param {Number} expected The expected amount of entries for a histogram
+ */
+async function countEntriesOfChildHistogram(id, expected) {
+ let histogram;
+ await TestUtils.waitForCondition(() => {
+ let histograms = Services.telemetry.getSnapshotForHistograms(
+ "main",
+ false
+ ).content;
+
+ histogram = histograms[id];
+
+ return !!histogram && countEntries(histogram) == expected;
+ }, `The histogram ${id} was expected to have ${expected} entries.`);
+ Assert.equal(countEntries(histogram), expected);
+}
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.signupDetection.enabled", true]],
+ });
+ Services.telemetry.getHistogramById(SIGNUP_DETECTION_HISTOGRAM).clear();
+});
+
+add_task(async () => {
+ let formProcessed = listenForTestNotification("FormProcessed", 2);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ await formProcessed;
+
+ info(
+ "Test case: When loading the document the two <form> HTML elements are processed and each one is run against the SignUpFormRuleset. After the page load the histogram PWMGR_SIGNUP_FORM_DETECTION_MS should have two entries."
+ );
+ await countEntriesOfChildHistogram(SIGNUP_DETECTION_HISTOGRAM, 2);
+
+ gBrowser.removeTab(tab);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_test_changeContentInputValue.js b/toolkit/components/passwordmgr/test/browser/browser_test_changeContentInputValue.js
new file mode 100644
index 0000000000..d0016708b2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_test_changeContentInputValue.js
@@ -0,0 +1,129 @@
+/**
+ * Tests head.js#changeContentInputValue.
+ */
+
+"use strict";
+
+// The origin for the test URIs.
+const TEST_ORIGIN = "https://example.com";
+const BASIC_FORM_PAGE_PATH = DIRECTORY_PATH + "form_basic.html";
+const USERNAME_INPUT_SELECTOR = "#form-basic-username";
+
+let testCases = [
+ {
+ name: "blank string should clear input value",
+ originalValue: "start text",
+ inputEvent: "",
+ expectedKeypresses: ["Backspace"],
+ },
+ {
+ name: "input value that adds to original string should only add the difference",
+ originalValue: "start text",
+ inputEvent: "start text!!!",
+ expectedKeypresses: ["!", "!", "!"],
+ },
+ {
+ name: "input value that is a subset of original string should only delete the difference",
+ originalValue: "start text",
+ inputEvent: "start",
+ expectedKeypresses: ["Backspace"],
+ },
+ {
+ name: "input value that is unrelated to the original string should replace it",
+ originalValue: "start text",
+ inputEvent: "wut?",
+ expectedKeypresses: ["w", "u", "t", "?"],
+ },
+];
+
+for (let testData of testCases) {
+ let tmp = {
+ async [testData.name]() {
+ await testStringChange(testData);
+ },
+ };
+ add_task(tmp[testData.name]);
+}
+
+async function testStringChange({
+ name,
+ originalValue,
+ inputEvent,
+ expectedKeypresses,
+}) {
+ info("Starting test " + name);
+ await LoginTestUtils.clearData();
+
+ await LoginTestUtils.addLogin({
+ username: originalValue,
+ password: "password",
+ });
+
+ let formProcessedPromise = listenForTestNotification("FormProcessed");
+ let url = TEST_ORIGIN + BASIC_FORM_PAGE_PATH;
+ info("Opening tab with url: " + url);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function (browser) {
+ info(`Opened tab with url: ${url}, waiting for focus`);
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+ info("Waiting for form-processed message");
+ await formProcessedPromise;
+ await checkForm(browser, originalValue);
+ info("form checked");
+
+ await ContentTask.spawn(
+ browser,
+ { USERNAME_INPUT_SELECTOR, expectedKeypresses },
+ async function ({ USERNAME_INPUT_SELECTOR, expectedKeypresses }) {
+ let input = content.document.querySelector(USERNAME_INPUT_SELECTOR);
+
+ let verifyKeyListener = event => {
+ Assert.equal(
+ expectedKeypresses[0],
+ event.key,
+ "Key press matches expected value"
+ );
+ expectedKeypresses.shift();
+
+ if (!expectedKeypresses.length) {
+ input.removeEventListner("keydown", verifyKeyListener);
+ input.addEventListener("keydown", () => {
+ throw new Error("Unexpected keypress encountered");
+ });
+ }
+ };
+
+ input.addEventListener("keydown", verifyKeyListener);
+ }
+ );
+
+ changeContentInputValue(browser, USERNAME_INPUT_SELECTOR, inputEvent);
+ }
+ );
+}
+
+async function checkForm(browser, expectedUsername) {
+ await ContentTask.spawn(
+ browser,
+ {
+ expectedUsername,
+ USERNAME_INPUT_SELECTOR,
+ },
+ async function contentCheckForm({
+ expectedUsername,
+ USERNAME_INPUT_SELECTOR,
+ }) {
+ let field = content.document.querySelector(USERNAME_INPUT_SELECTOR);
+ Assert.equal(
+ field.value,
+ expectedUsername,
+ `Username field has teh expected initial value '${expectedUsername}'`
+ );
+ }
+ );
+}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_username_only_form_telemetry.js b/toolkit/components/passwordmgr/test/browser/browser_username_only_form_telemetry.js
new file mode 100644
index 0000000000..54304c24ac
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_username_only_form_telemetry.js
@@ -0,0 +1,198 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+"use strict";
+
+async function setupForms(numUsernameOnly, numBasic, numOther) {
+ const TEST_HOSTNAME = "https://example.com";
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_HOSTNAME + DIRECTORY_PATH + "empty.html"
+ );
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [
+ {
+ numUsernameOnly,
+ numBasic,
+ },
+ ],
+ async function (data) {
+ // type: 1: basic, 2:usernameOnly, 3:other
+ function addForm(type) {
+ const form = content.document.createElement("form");
+ content.document.body.appendChild(form);
+
+ const user = content.document.createElement("input");
+ if (type === 3) {
+ user.type = "url";
+ } else {
+ user.type = "text";
+ user.autocomplete = "username";
+ }
+ form.appendChild(user);
+
+ if (type === 1) {
+ const password = content.document.createElement("input");
+ password.type = "password";
+ form.appendChild(password);
+ }
+ }
+ for (let i = 0; i < data.numBasic; i++) {
+ addForm(1);
+ }
+ for (let i = 0; i < data.numUsernameOnly; i++) {
+ addForm(2);
+ }
+ for (let i = 0; i < data.numOther; i++) {
+ addForm(3);
+ }
+ }
+ );
+
+ return tab;
+}
+
+async function checkChildHistogram(id, index, expected) {
+ let histogram;
+ await TestUtils.waitForCondition(() => {
+ let histograms = Services.telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).content;
+
+ histogram = histograms[id];
+ return !!histogram && histogram.values[index] == expected;
+ });
+ Assert.equal(histogram.values[index], expected);
+}
+
+add_setup(async function () {
+ SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.usernameOnlyForm.enabled", true],
+ ["signon.usernameOnlyForm.lookupThreshold", 100], // ignore the threshold in test
+ ],
+ });
+
+ // Wait 1sec to make sure all the telemetry data recorded prior to the beginning of the
+ // test is cleared.
+ await new Promise(res => setTimeout(res, 1000));
+ Services.telemetry.getSnapshotForHistograms("main", true /* clear */);
+});
+
+add_task(async function test_oneUsernameOnlyForm() {
+ const numUsernameOnlyForms = 1;
+ const numBasicForms = 0;
+
+ // number of "other" forms doesn't change the outcome, set it to 2 here and
+ // in the following testcase just to ensure it doesn't affect the result.
+ let tab = await setupForms(numUsernameOnlyForms, numBasicForms, 2);
+
+ await checkChildHistogram(
+ "PWMGR_IS_USERNAME_ONLY_FORM",
+ 1,
+ numUsernameOnlyForms
+ );
+ await checkChildHistogram(
+ "PWMGR_NUM_FORM_HAS_POSSIBLE_USERNAME_EVENT_PER_DOC",
+ numUsernameOnlyForms,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ Services.telemetry.getSnapshotForHistograms("main", true /* clear */);
+});
+
+add_task(async function test_multipleUsernameOnlyForms() {
+ const numUsernameOnlyForms = 3;
+ const numBasicForms = 2;
+
+ let tab = await setupForms(numUsernameOnlyForms, numBasicForms, 2);
+
+ await checkChildHistogram(
+ "PWMGR_IS_USERNAME_ONLY_FORM",
+ 1,
+ numUsernameOnlyForms
+ );
+ await checkChildHistogram(
+ "PWMGR_NUM_FORM_HAS_POSSIBLE_USERNAME_EVENT_PER_DOC",
+ 5,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ Services.telemetry.getSnapshotForHistograms("main", true /* clear */);
+});
+
+add_task(async function test_multipleDocument() {
+ // The first document
+ let numUsernameOnlyForms1 = 2;
+ let numBasicForms1 = 2;
+
+ let tab1 = await setupForms(numUsernameOnlyForms1, numBasicForms1, 2);
+
+ await checkChildHistogram(
+ "PWMGR_IS_USERNAME_ONLY_FORM",
+ 1,
+ numUsernameOnlyForms1
+ );
+ await checkChildHistogram(
+ "PWMGR_NUM_FORM_HAS_POSSIBLE_USERNAME_EVENT_PER_DOC",
+ numUsernameOnlyForms1 + numBasicForms1,
+ 1
+ );
+
+ // The second document
+ let numUsernameOnlyForms2 = 15;
+ let numBasicForms2 = 3;
+
+ let tab2 = await setupForms(numUsernameOnlyForms2, numBasicForms2, 2);
+
+ await checkChildHistogram(
+ "PWMGR_IS_USERNAME_ONLY_FORM",
+ 1,
+ numUsernameOnlyForms1 + numUsernameOnlyForms2
+ );
+
+ // the result is stacked, so the new document add a counter to all
+ // buckets under "numUsernameOnlyForms2 + numBasicForms2"
+ await checkChildHistogram(
+ "PWMGR_NUM_FORM_HAS_POSSIBLE_USERNAME_EVENT_PER_DOC",
+ numUsernameOnlyForms1 + numBasicForms1,
+ 2
+ );
+ await checkChildHistogram(
+ "PWMGR_NUM_FORM_HAS_POSSIBLE_USERNAME_EVENT_PER_DOC",
+ numUsernameOnlyForms2 + numBasicForms2,
+ 1
+ );
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ Services.telemetry.getSnapshotForHistograms("main", true /* clear */);
+});
+
+add_task(async function test_tooManyUsernameOnlyForms() {
+ const numUsernameOnlyForms = 25;
+ const numBasicForms = 2;
+
+ let tab = await setupForms(numUsernameOnlyForms, numBasicForms, 2);
+
+ await checkChildHistogram(
+ "PWMGR_IS_USERNAME_ONLY_FORM",
+ 1,
+ numUsernameOnlyForms
+ );
+ await checkChildHistogram(
+ "PWMGR_NUM_FORM_HAS_POSSIBLE_USERNAME_EVENT_PER_DOC",
+ 21,
+ numUsernameOnlyForms + numBasicForms - 20 // maximum is 20
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ Services.telemetry.getSnapshotForHistograms("main", true /* clear */);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/browser_username_select_dialog.js b/toolkit/components/passwordmgr/test/browser/browser_username_select_dialog.js
new file mode 100644
index 0000000000..03ac74e9ef
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_username_select_dialog.js
@@ -0,0 +1,177 @@
+/*
+ * Test username selection dialog, on password update from a p-only form,
+ * when there are multiple saved logins on the domain.
+ */
+
+// Copied from prompt_common.js. TODO: share the code.
+function getSelectDialogDoc() {
+ // Trudge through all the open windows, until we find the one
+ // that has selectDialog.xhtml loaded.
+ // var enumerator = Services.wm.getEnumerator("navigator:browser");
+ for (let { docShell } of Services.wm.getEnumerator(null)) {
+ var containedDocShells = docShell.getAllDocShellsInSubtree(
+ docShell.typeChrome,
+ docShell.ENUMERATE_FORWARDS
+ );
+ for (let childDocShell of containedDocShells) {
+ // We don't want it if it's not done loading.
+ if (childDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE) {
+ continue;
+ }
+ var childDoc = childDocShell.contentViewer.DOMDocument;
+
+ if (
+ childDoc.location.href == "chrome://global/content/selectDialog.xhtml"
+ ) {
+ return childDoc;
+ }
+ }
+ }
+
+ return null;
+}
+
+let nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo,
+ "init"
+);
+let login1 = new nsLoginInfo(
+ "https://example.com",
+ "https://example.com",
+ null,
+ "notifyu1",
+ "notifyp1",
+ "user",
+ "pass"
+);
+let login1B = new nsLoginInfo(
+ "https://example.com",
+ "https://example.com",
+ null,
+ "notifyu1B",
+ "notifyp1B",
+ "user",
+ "pass"
+);
+
+add_task(async function test_changeUPLoginOnPUpdateForm_accept() {
+ info(
+ "Select an u+p login from multiple logins, on password update form, and accept."
+ );
+ await Services.logins.addLogins([login1, login1B]);
+
+ let selectDialogPromise = TestUtils.topicObserved("select-dialog-loaded");
+
+ await testSubmittingLoginForm(
+ "subtst_notifications_change_p.html",
+ async function (fieldValues) {
+ Assert.equal(fieldValues.username, "null", "Checking submitted username");
+ Assert.equal(
+ fieldValues.password,
+ "pass2",
+ "Checking submitted password"
+ );
+
+ info("Waiting for select dialog to appear.");
+ let doc = (await selectDialogPromise)[0].document;
+ let dialog = doc.getElementsByTagName("dialog")[0];
+ let listbox = doc.getElementById("list");
+
+ Assert.equal(listbox.selectedIndex, 0, "Checking selected index");
+ Assert.equal(listbox.itemCount, 2, "Checking selected length");
+ ["notifyu1", "notifyu1B"].forEach((username, i) => {
+ Assert.equal(
+ listbox.getItemAtIndex(i).label,
+ username,
+ "Check username selection on dialog"
+ );
+ });
+
+ dialog.acceptDialog();
+
+ await TestUtils.waitForCondition(() => {
+ return !getSelectDialogDoc();
+ }, "Wait for selection dialog to disappear.");
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 2, "Should have 2 logins");
+
+ let login = SpecialPowers.wrap(logins[0]).QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "notifyu1", "Check the username unchanged");
+ Assert.equal(login.password, "pass2", "Check the password changed");
+ Assert.equal(login.timesUsed, 2, "Check times used");
+
+ login = SpecialPowers.wrap(logins[1]).QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "notifyu1B", "Check the username unchanged");
+ Assert.equal(login.password, "notifyp1B", "Check the password unchanged");
+ Assert.equal(login.timesUsed, 1, "Check times used");
+
+ // cleanup
+ login1.password = "pass2";
+ Services.logins.removeLogin(login1);
+ login1.password = "notifyp1";
+
+ Services.logins.removeLogin(login1B);
+});
+
+add_task(async function test_changeUPLoginOnPUpdateForm_cancel() {
+ info(
+ "Select an u+p login from multiple logins, on password update form, and cancel."
+ );
+ await Services.logins.addLogins([login1, login1B]);
+
+ let selectDialogPromise = TestUtils.topicObserved("select-dialog-loaded");
+
+ await testSubmittingLoginForm(
+ "subtst_notifications_change_p.html",
+ async function (fieldValues) {
+ Assert.equal(fieldValues.username, "null", "Checking submitted username");
+ Assert.equal(
+ fieldValues.password,
+ "pass2",
+ "Checking submitted password"
+ );
+
+ info("Waiting for select dialog to appear.");
+ let doc = (await selectDialogPromise)[0].document;
+ let dialog = doc.getElementsByTagName("dialog")[0];
+ let listbox = doc.getElementById("list");
+
+ Assert.equal(listbox.selectedIndex, 0, "Checking selected index");
+ Assert.equal(listbox.itemCount, 2, "Checking selected length");
+ ["notifyu1", "notifyu1B"].forEach((username, i) => {
+ Assert.equal(
+ listbox.getItemAtIndex(i).label,
+ username,
+ "Check username selection on dialog"
+ );
+ });
+
+ dialog.cancelDialog();
+
+ await TestUtils.waitForCondition(() => {
+ return !getSelectDialogDoc();
+ }, "Wait for selection dialog to disappear.");
+ }
+ );
+
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 2, "Should have 2 logins");
+
+ let login = SpecialPowers.wrap(logins[0]).QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "notifyu1", "Check the username unchanged");
+ Assert.equal(login.password, "notifyp1", "Check the password unchanged");
+ Assert.equal(login.timesUsed, 1, "Check times used");
+
+ login = SpecialPowers.wrap(logins[1]).QueryInterface(Ci.nsILoginMetaInfo);
+ Assert.equal(login.username, "notifyu1B", "Check the username unchanged");
+ Assert.equal(login.password, "notifyp1B", "Check the password unchanged");
+ Assert.equal(login.timesUsed, 1, "Check times used");
+
+ // cleanup
+ Services.logins.removeLogin(login1);
+ Services.logins.removeLogin(login1B);
+});
diff --git a/toolkit/components/passwordmgr/test/browser/empty.html b/toolkit/components/passwordmgr/test/browser/empty.html
new file mode 100644
index 0000000000..1ad28bb1f7
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/empty.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Empty file</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/file_focus_before_DOMContentLoaded.sjs b/toolkit/components/passwordmgr/test/browser/file_focus_before_DOMContentLoaded.sjs
new file mode 100644
index 0000000000..b99246c166
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/file_focus_before_DOMContentLoaded.sjs
@@ -0,0 +1,35 @@
+/**
+ * Focus a username field before DOMContentLoaded.
+ */
+
+"use strict";
+
+const DELAY = 2 * 1000; // Delay two seconds before completing the request.
+
+// In an SJS file we need to get the setTimeout bits ourselves, despite
+// what eslint might think applies for browser tests.
+// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix
+let { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+function handleRequest(request, response) {
+ response.processAsync();
+
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.write(`
+ <!DOCTYPE html><html><body>
+ <form id="early_focus_form" action="https://autocomplete:8888/formtest.js">
+ <input type="text" id="uname" name="uname">
+ <input type="password" id="pword" name="pword">
+ <button type="submit">Submit</button>
+ </form>
+ <script>document.querySelector("#uname").focus();</script>
+ `);
+
+ setTimeout(function finishOutput() {
+ response.write(`</body></html>`);
+ response.finish();
+ }, DELAY);
+}
diff --git a/toolkit/components/passwordmgr/test/browser/form_autofocus_frame.html b/toolkit/components/passwordmgr/test/browser/form_autofocus_frame.html
new file mode 100644
index 0000000000..3a145a9ea8
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_autofocus_frame.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head>
+<body>
+<form id="form-basic">
+ <input id="form-basic-username" name="username" autofocus>
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-submit" type="submit">
+</form>
+<iframe src="/document-builder.sjs?html=<html><body>Hi</body></html>"></iframe>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_autofocus_js.html b/toolkit/components/passwordmgr/test/browser/form_autofocus_js.html
new file mode 100644
index 0000000000..76056e3751
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_autofocus_js.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head>
+<body onload="document.getElementById('form-basic-username').focus();">
+<!-- Username field is focused by js onload -->
+<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/browser/form_basic.html b/toolkit/components/passwordmgr/test/browser/form_basic.html
new file mode 100644
index 0000000000..df2083a93c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_basic.html
@@ -0,0 +1,12 @@
+<!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/ -->
+
+<!-- Simplest form with username and password fields. -->
+<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/browser/form_basic_iframe.html b/toolkit/components/passwordmgr/test/browser/form_basic_iframe.html
new file mode 100644
index 0000000000..dd34739e6d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_basic_iframe.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta charset="utf-8">
+</head>
+
+<body>
+ <!-- Form in an iframe -->
+ <iframe src="https://example.org/browser/toolkit/components/passwordmgr/test/browser/form_basic.html" id="test-iframe"></iframe>
+
+ <!-- Form in a fully sandboxed iframe -->
+ <iframe src="https://example.org/browser/toolkit/components/passwordmgr/test/browser/form_basic.html"
+ sandbox=""
+ id="test-iframe-sandbox"></iframe>
+
+ <!-- Form in an "allow-same-origin" sandboxed iframe -->
+ <iframe src="https://example.org/browser/toolkit/components/passwordmgr/test/browser/form_basic.html"
+ sandbox="allow-same-origin"
+ id="test-iframe-sandbox-same-origin"></iframe>
+</body>
+
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_basic_login.html b/toolkit/components/passwordmgr/test/browser/form_basic_login.html
new file mode 100644
index 0000000000..f63aca8b41
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_basic_login.html
@@ -0,0 +1,12 @@
+<!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/ -->
+
+<!-- Simplest login form with username and password fields. -->
+<form id="form-basic-login">
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-submit" type="submit" value="sign in">
+</form>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_basic_no_username.html b/toolkit/components/passwordmgr/test/browser/form_basic_no_username.html
new file mode 100644
index 0000000000..885fa9c11c
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_basic_no_username.html
@@ -0,0 +1,11 @@
+<!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/ -->
+
+<!-- Simplest form with just password field. -->
+<form id="form-basic">
+ <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/browser/form_basic_signup.html b/toolkit/components/passwordmgr/test/browser/form_basic_signup.html
new file mode 100644
index 0000000000..a42a8cc609
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_basic_signup.html
@@ -0,0 +1,10 @@
+<!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 id="form-basic-signup">
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password" autocomplete="new-password">
+ <input id="form-basic-submit" type="submit" value="sign up">
+</form>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_basic_with_confirm_field.html b/toolkit/components/passwordmgr/test/browser/form_basic_with_confirm_field.html
new file mode 100644
index 0000000000..c638be4af7
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_basic_with_confirm_field.html
@@ -0,0 +1,13 @@
+<!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/ -->
+
+<!-- Simplest form with username, password and confirm password fields. -->
+<form id="form-basic">
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-confirm-password" name="confirm-password" type="password">
+ <input id="form-basic-submit" type="submit">
+</form>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_cross_origin_insecure_action.html b/toolkit/components/passwordmgr/test/browser/form_cross_origin_insecure_action.html
new file mode 100644
index 0000000000..8dde7ceb63
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_cross_origin_insecure_action.html
@@ -0,0 +1,12 @@
+<!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/ -->
+
+<!-- Simplest form with username and password fields. -->
+<form id="form-basic" action="http://example.org/custom_action.html">
+ <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/browser/form_cross_origin_secure_action.html b/toolkit/components/passwordmgr/test/browser/form_cross_origin_secure_action.html
new file mode 100644
index 0000000000..2d95419549
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_cross_origin_secure_action.html
@@ -0,0 +1,12 @@
+<!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/ -->
+
+<!-- Simplest form with username and password fields. -->
+<form id="form-basic" action="https://example.org/custom_action.html">
+ <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/browser/form_crossframe.html b/toolkit/components/passwordmgr/test/browser/form_crossframe.html
new file mode 100644
index 0000000000..fbb759c632
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_crossframe.html
@@ -0,0 +1,13 @@
+<html><body>
+
+<button onclick="window.location = 'about:blank'">Next</button>
+<iframe src="https://test2.example.org:443/browser/toolkit/components/passwordmgr/test/browser/form_crossframe_inner.html"
+ width="300" height="300"></iframe>
+<form id="outer-form" action="formsubmit.sjs">
+ <input id="outer-username" name="outer-username">
+ <input id="outer-password" name="outer-password" type="password">
+ <input id="outer-submit" type="submit">
+ <button id="outer-gobutton" onclick="this.location = 'about:blank'">Go</button>
+</form>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_crossframe_inner.html b/toolkit/components/passwordmgr/test/browser/form_crossframe_inner.html
new file mode 100644
index 0000000000..9a919993bf
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_crossframe_inner.html
@@ -0,0 +1,13 @@
+<!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 for use inside a frame. -->
+<form id="inner-form" action="formsubmit.sjs">
+ <input id="inner-username" name="username">
+ <input id="inner-password" name="password" type="password">
+ <input id="inner-submit" type="submit">
+</form>
+<button id="inner-gobutton" onclick="document.location = 'about:blank'">Go</button>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_disabled_readonly_passwordField.html b/toolkit/components/passwordmgr/test/browser/form_disabled_readonly_passwordField.html
new file mode 100644
index 0000000000..4e47d9790d
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_disabled_readonly_passwordField.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html><body>
+ <form id="login_form_disabled_password">
+ <input type="text" name="username" autocomplete="username">
+ <input type="password" name="password" autocomplete="password" disabled>
+ <button type="submit">Log in</button>
+ </form>
+ <form id="login_form_readonly_password">
+ <input type="text" name="username" autocomplete="username">
+ <input type="password" name="password" autocomplete="password" readonly>
+ <button type="submit">Log in</button>
+ </form>
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_expanded.html b/toolkit/components/passwordmgr/test/browser/form_expanded.html
new file mode 100644
index 0000000000..567a373f5b
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_expanded.html
@@ -0,0 +1,16 @@
+<!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/ -->
+
+<!-- Expanded page containing a form + several unrelated elements -->
+
+<form id="form-basic">
+ <input id="form-expanded-search">
+ <input id="form-expanded-username" name="username">
+ <input id="form-expanded-password" name="password" type="password">
+ <input id="form-expanded-submit" type="submit">
+ <input id="form-expanded-captcha">
+</form>
+<input id="form-expanded-non-form-input">
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_multipage.html b/toolkit/components/passwordmgr/test/browser/form_multipage.html
new file mode 100644
index 0000000000..908457fd50
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_multipage.html
@@ -0,0 +1,32 @@
+<!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 a username-only field. -->
+<form id="form-basic">
+ <input id="form-basic-username" type="text" name="username" autocomplete="username">
+ <input id="form-basic-submit" type="submit">
+</form>
+
+<script>
+ const form = document.getElementById( "form-basic" );
+ form.addEventListener( "submit", function onFormSubmit(event) {
+ event.preventDefault()
+ document.getElementById("form-basic").remove();
+
+ // Create the password-only form after the username-only form is submitted.
+ var form = document.createElement("form");
+ form.id = "form-basic";
+ var password = document.createElement("input");
+ password.id = "form-basic-password";
+ password.type = "password";
+ form.appendChild(password);
+ var submit = document.createElement("input");
+ submit.id = "form-basic-submit";
+ submit.type = "submit";
+ form.appendChild(submit);
+ document.body.appendChild(form);
+ });
+</script>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_password_change.html b/toolkit/components/passwordmgr/test/browser/form_password_change.html
new file mode 100644
index 0000000000..279de622f2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_password_change.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Login Manager notifications w/ new password</title>
+</head>
+<body>
+<h2>Test for Login Manager notifications w/ new password</h2>
+
+<form id="form" action="formsubmit.sjs">
+ <input id="pass" name="pass" type="password">
+ <input id="newpass" name="newpass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/form_same_origin_action.html b/toolkit/components/passwordmgr/test/browser/form_same_origin_action.html
new file mode 100644
index 0000000000..8f0c9a14e2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_same_origin_action.html
@@ -0,0 +1,12 @@
+<!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/ -->
+
+<!-- Simplest form with username and password fields. -->
+<form id="form-basic" action="./custom_action.html">
+ <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/browser/form_signup_detection.html b/toolkit/components/passwordmgr/test/browser/form_signup_detection.html
new file mode 100644
index 0000000000..de01223239
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/form_signup_detection.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+</head>
+<body>
+ <h1>Sign up</h1>
+ <form id="obvious-signup-form">
+ <label for="obvious-signup-email">Email</label>
+ <input id="obvious-signup-email" type="email">
+ <label for="obvious-signup-username">Username</label>
+ <input id="obvious-signup-username">
+ <label for="obvious-signup-password">Password</label>
+ <input id="obvious-signup-password" type="password" autocomplete="new-password">
+ <input id= "obvious-signup-privacyPolicy" type="checkbox">
+ <label for="obvious-signup-privacyPolicy">I have read and agree to the privacy policy.</label>
+ <input id="obvious-signup-submit" type="submit" value="Sign up">
+ </form>
+ <h1>Login</h1>
+ <form id="obvious-login-form">
+ <label for="obvious-login-username">Username</label>
+ <input id="obvious-login-username">
+ <label for="obvious-login-password">Password</label>
+ <input id="obvious-login-password" type="password" autocomplete="current-password">
+ <a>Password forgotten?</a>
+ <input id= "obvious-login-rememberMe" type="checkbox">
+ <label for="obvious-login-rememberMe">Remember me</label>
+ <input id="obvious-login-submit" type="submit" value="Login">
+ </form>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/formless_basic.html b/toolkit/components/passwordmgr/test/browser/formless_basic.html
new file mode 100644
index 0000000000..17455df96e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/formless_basic.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
+
+<!-- Simplest form with username and password fields. -->
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-submit" type="submit">
+
+ <button id="add">Add input[type=password]</button>
+
+ <script>
+ document.getElementById("add").addEventListener("click", function() {
+ var node = document.createElement("input");
+ node.setAttribute("type", "password");
+ document.querySelector("body").appendChild(node);
+ });
+ </script>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/head.js b/toolkit/components/passwordmgr/test/browser/head.js
new file mode 100644
index 0000000000..70a1f685e2
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/head.js
@@ -0,0 +1,965 @@
+const DIRECTORY_PATH = "/browser/toolkit/components/passwordmgr/test/browser/";
+
+var { LoginTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/LoginTestUtils.sys.mjs"
+);
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+add_setup(async function common_initialize() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.rememberSignons", true],
+ ["signon.testOnlyUserHasInteractedByPrefValue", true],
+ ["signon.testOnlyUserHasInteractedWithDocument", true],
+ ["toolkit.telemetry.ipcBatchTimeout", 0],
+ ],
+ });
+ if (LoginHelper.relatedRealmsEnabled) {
+ await LoginTestUtils.remoteSettings.setupWebsitesWithSharedCredentials();
+ registerCleanupFunction(async function () {
+ await LoginTestUtils.remoteSettings.cleanWebsitesWithSharedCredentials();
+ });
+ }
+});
+
+registerCleanupFunction(
+ async function cleanup_removeAllLoginsAndResetRecipes() {
+ await SpecialPowers.popPrefEnv();
+
+ LoginTestUtils.clearData();
+ LoginTestUtils.resetGeneratedPasswordsCache();
+ clearHttpAuths();
+ Services.telemetry.clearEvents();
+
+ let recipeParent = LoginTestUtils.recipes.getRecipeParent();
+ if (!recipeParent) {
+ // No need to reset the recipes if the recipe module wasn't even loaded.
+ return;
+ }
+ await recipeParent.then(recipeParentResult => recipeParentResult.reset());
+
+ await cleanupDoorhanger();
+ await cleanupPasswordNotifications();
+ await closePopup(document.getElementById("contentAreaContextMenu"));
+ await closePopup(document.getElementById("PopupAutoComplete"));
+ }
+);
+
+/**
+ * Compared logins in storage to expected values
+ *
+ * @param {array} expectedLogins
+ * An array of expected login properties
+ * @return {nsILoginInfo[]} - All saved logins sorted by timeCreated
+ */
+function verifyLogins(expectedLogins = []) {
+ let allLogins = Services.logins.getAllLogins();
+ allLogins.sort((a, b) => a.timeCreated > b.timeCreated);
+ Assert.equal(
+ allLogins.length,
+ expectedLogins.length,
+ "Check actual number of logins matches the number of provided expected property-sets"
+ );
+ for (let i = 0; i < expectedLogins.length; i++) {
+ // if the test doesn't care about comparing properties for this login, just pass false/null.
+ let expected = expectedLogins[i];
+ if (expected) {
+ let login = allLogins[i];
+ if (typeof expected.timesUsed !== "undefined") {
+ Assert.equal(login.timesUsed, expected.timesUsed, "Check timesUsed");
+ }
+ if (typeof expected.passwordLength !== "undefined") {
+ Assert.equal(
+ login.password.length,
+ expected.passwordLength,
+ "Check passwordLength"
+ );
+ }
+ if (typeof expected.username !== "undefined") {
+ Assert.equal(login.username, expected.username, "Check username");
+ }
+ if (typeof expected.password !== "undefined") {
+ Assert.equal(login.password, expected.password, "Check password");
+ }
+ if (typeof expected.usedSince !== "undefined") {
+ Assert.ok(
+ login.timeLastUsed > expected.usedSince,
+ "Check timeLastUsed"
+ );
+ }
+ if (typeof expected.passwordChangedSince !== "undefined") {
+ Assert.ok(
+ login.timePasswordChanged > expected.passwordChangedSince,
+ "Check timePasswordChanged"
+ );
+ }
+ if (typeof expected.timeCreated !== "undefined") {
+ Assert.equal(
+ login.timeCreated,
+ expected.timeCreated,
+ "Check timeCreated"
+ );
+ }
+ }
+ }
+ return allLogins;
+}
+
+/**
+ * Submit the content form and return a promise resolving to the username and
+ * password values echoed out in the response
+ *
+ * @param {Object} [browser] - browser with the form
+ * @param {String = ""} formAction - Optional url to set the form's action to before submitting
+ * @param {Object = null} selectorValues - Optional object with field values to set before form submission
+ * @param {Object = null} responseSelectors - Optional object with selectors to find the username and password in the response
+ */
+async function submitFormAndGetResults(
+ browser,
+ formAction = "",
+ selectorValues,
+ responseSelectors
+) {
+ async function contentSubmitForm([contentFormAction, contentSelectorValues]) {
+ const { WrapPrivileged } = ChromeUtils.importESModule(
+ "resource://testing-common/WrapPrivileged.sys.mjs"
+ );
+ let doc = content.document;
+ let form = doc.querySelector("form");
+ if (contentFormAction) {
+ form.action = contentFormAction;
+ }
+ for (let [sel, value] of Object.entries(contentSelectorValues)) {
+ try {
+ let field = doc.querySelector(sel);
+ let gotInput = ContentTaskUtils.waitForEvent(
+ field,
+ "input",
+ "Got input event on " + sel
+ );
+ // we don't get an input event if the new value == the old
+ field.value = "###";
+ WrapPrivileged.wrap(field, this).setUserInput(value);
+ await gotInput;
+ } catch (ex) {
+ throw new Error(
+ `submitForm: Couldn't set value of field at: ${sel}: ${ex.message}`
+ );
+ }
+ }
+ form.submit();
+ }
+
+ let loadPromise = BrowserTestUtils.browserLoaded(browser);
+ await SpecialPowers.spawn(
+ browser,
+ [[formAction, selectorValues]],
+ contentSubmitForm
+ );
+ await loadPromise;
+
+ let result = await getFormSubmitResponseResult(
+ browser,
+ formAction,
+ responseSelectors
+ );
+ return result;
+}
+
+/**
+ * Wait for a given result page to load and return a promise resolving to an object with the parsed-out
+ * username/password values from the response
+ *
+ * @param {Object} [browser] - browser which is loading this page
+ * @param {String} resultURL - the path or filename to look for in the content.location
+ * @param {Object = null} - Optional object with selectors to find the username and password in the response
+ */
+async function getFormSubmitResponseResult(
+ browser,
+ resultURL = "/formsubmit.sjs",
+ { username = "#user", password = "#pass" } = {}
+) {
+ // default selectors are for the response page produced by formsubmit.sjs
+ let fieldValues = await ContentTask.spawn(
+ browser,
+ {
+ resultURL,
+ usernameSelector: username,
+ passwordSelector: password,
+ },
+ async function ({ resultURL, usernameSelector, passwordSelector }) {
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ content.location.pathname.endsWith(resultURL) &&
+ content.document.readyState == "complete"
+ );
+ }, `Wait for form submission load (${resultURL})`);
+ let username =
+ content.document.querySelector(usernameSelector).textContent;
+ // Bug 1686071: Since generated passwords can have special characters in them,
+ // we need to unescape the characters. These special characters are automatically escaped
+ // when we submit a form in `submitFormAndGetResults`.
+ // Otherwise certain tests will intermittently fail when these special characters are present in the passwords.
+ let password = unescape(
+ content.document.querySelector(passwordSelector).textContent
+ );
+ return {
+ username,
+ password,
+ };
+ }
+ );
+ return fieldValues;
+}
+
+/**
+ * Loads a test page in `DIRECTORY_URL` which automatically submits to formsubmit.sjs and returns a
+ * promise resolving with the field values when the optional `aTaskFn` is done.
+ *
+ * @param {String} aPageFile - test page file name which auto-submits to formsubmit.sjs
+ * @param {Function} aTaskFn - task which can be run before the tab closes.
+ * @param {String} [aOrigin="https://example.com"] - origin of the server to use
+ * to load `aPageFile`.
+ */
+function testSubmittingLoginForm(
+ aPageFile,
+ aTaskFn,
+ aOrigin = "https://example.com"
+) {
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: aOrigin + DIRECTORY_PATH + aPageFile,
+ },
+ async function (browser) {
+ Assert.ok(true, "loaded " + aPageFile);
+ let fieldValues = await getFormSubmitResponseResult(
+ browser,
+ "/formsubmit.sjs"
+ );
+ Assert.ok(true, "form submission loaded");
+ if (aTaskFn) {
+ await aTaskFn(fieldValues, browser);
+ }
+ return fieldValues;
+ }
+ );
+}
+/**
+ * Loads a test page in `DIRECTORY_URL` which automatically submits to formsubmit.sjs and returns a
+ * promise resolving with the field values when the optional `aTaskFn` is done.
+ *
+ * @param {String} aPageFile - test page file name which auto-submits to formsubmit.sjs
+ * @param {Function} aTaskFn - task which can be run before the tab closes.
+ * @param {String} [aOrigin="http://example.com"] - origin of the server to use
+ * to load `aPageFile`.
+ */
+function testSubmittingLoginFormHTTP(
+ aPageFile,
+ aTaskFn,
+ aOrigin = "http://example.com"
+) {
+ return testSubmittingLoginForm(aPageFile, aTaskFn, aOrigin);
+}
+
+function checkOnlyLoginWasUsedTwice({ justChanged }) {
+ // Check to make sure we updated the timestamps and use count on the
+ // existing login that was submitted for the test.
+ let logins = Services.logins.getAllLogins();
+ Assert.equal(logins.length, 1, "Should only have 1 login");
+ Assert.ok(logins[0] instanceof Ci.nsILoginMetaInfo, "metainfo QI");
+ Assert.equal(
+ logins[0].timesUsed,
+ 2,
+ "check .timesUsed for existing login submission"
+ );
+ Assert.ok(
+ logins[0].timeCreated < logins[0].timeLastUsed,
+ "timeLastUsed bumped"
+ );
+ if (justChanged) {
+ Assert.equal(
+ logins[0].timeLastUsed,
+ logins[0].timePasswordChanged,
+ "timeLastUsed == timePasswordChanged"
+ );
+ } else {
+ Assert.equal(
+ logins[0].timeCreated,
+ logins[0].timePasswordChanged,
+ "timeChanged not updated"
+ );
+ }
+}
+
+function clearHttpAuths() {
+ let authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].getService(
+ Ci.nsIHttpAuthManager
+ );
+ authMgr.clearAll();
+}
+
+// Begin popup notification (doorhanger) functions //
+
+const REMEMBER_BUTTON = "button";
+const NEVER_MENUITEM = 0;
+
+const CHANGE_BUTTON = "button";
+const DONT_CHANGE_BUTTON = "secondaryButton";
+const REMOVE_LOGIN_MENUITEM = 0;
+
+/**
+ * Checks if we have a password capture popup notification
+ * of the right type and with the right label.
+ *
+ * @param {String} aKind The desired `passwordNotificationType` ("any" for any type)
+ * @param {Object} [popupNotifications = PopupNotifications]
+ * @param {Object} [browser = null] Optional browser whose notifications should be searched.
+ * @return the found password popup notification.
+ */
+function getCaptureDoorhanger(
+ aKind,
+ popupNotifications = PopupNotifications,
+ browser = null
+) {
+ Assert.ok(true, "Looking for " + aKind + " popup notification");
+ let notification = popupNotifications.getNotification("password", browser);
+ if (!aKind) {
+ throw new Error(
+ "getCaptureDoorhanger needs aKind to be a non-empty string"
+ );
+ }
+ if (aKind !== "any" && notification) {
+ Assert.equal(
+ notification.options.passwordNotificationType,
+ aKind,
+ "Notification type matches."
+ );
+ if (aKind == "password-change") {
+ Assert.equal(
+ notification.mainAction.label,
+ "Update",
+ "Main action label matches update doorhanger."
+ );
+ } else if (aKind == "password-save") {
+ Assert.equal(
+ notification.mainAction.label,
+ "Save",
+ "Main action label matches save doorhanger."
+ );
+ }
+ }
+ return notification;
+}
+
+async function getCaptureDoorhangerThatMayOpen(
+ aKind,
+ popupNotifications = PopupNotifications,
+ browser = null
+) {
+ let notif = getCaptureDoorhanger(aKind, popupNotifications, browser);
+ if (notif && !notif.dismissed) {
+ if (popupNotifications.panel.state !== "open") {
+ await BrowserTestUtils.waitForEvent(
+ popupNotifications.panel,
+ "popupshown"
+ );
+ }
+ }
+ return notif;
+}
+
+async function waitForDoorhanger(browser, type) {
+ let notif;
+ await TestUtils.waitForCondition(() => {
+ notif = PopupNotifications.getNotification("password", browser);
+ if (notif && type !== "any") {
+ return (
+ notif.options.passwordNotificationType == type &&
+ notif.anchorElement &&
+ BrowserTestUtils.is_visible(notif.anchorElement)
+ );
+ }
+ return notif;
+ }, `Waiting for a ${type} notification`);
+ return notif;
+}
+
+async function hideDoorhangerPopup() {
+ info("hideDoorhangerPopup");
+ if (!PopupNotifications.isPanelOpen) {
+ return;
+ }
+ let { panel } = PopupNotifications;
+ let promiseHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ panel.hidePopup();
+ await promiseHidden;
+ info("got popuphidden from notification panel");
+}
+
+function getDoorhangerButton(aPopup, aButtonIndex) {
+ let notifications = aPopup.owner.panel.children;
+ Assert.ok(!!notifications.length, "at least one notification displayed");
+ Assert.ok(true, notifications.length + " notification(s)");
+ let notification = notifications[0];
+
+ if (aButtonIndex == "button") {
+ return notification.button;
+ } else if (aButtonIndex == "secondaryButton") {
+ return notification.secondaryButton;
+ }
+ return notification.menupopup.querySelectorAll("menuitem")[aButtonIndex];
+}
+
+/**
+ * Clicks the specified popup notification button.
+ *
+ * @param {Element} aPopup Popup Notification element
+ * @param {Number} aButtonIndex Number indicating which button to click.
+ * See the constants in this file.
+ */
+function clickDoorhangerButton(aPopup, aButtonIndex) {
+ Assert.ok(true, "Looking for action at index " + aButtonIndex);
+
+ let button = getDoorhangerButton(aPopup, aButtonIndex);
+ if (aButtonIndex == "button") {
+ Assert.ok(true, "Triggering main action");
+ } else if (aButtonIndex == "secondaryButton") {
+ Assert.ok(true, "Triggering secondary action");
+ } else {
+ Assert.ok(true, "Triggering menuitem # " + aButtonIndex);
+ }
+ button.doCommand();
+}
+
+async function cleanupDoorhanger(notif) {
+ let PN = notif ? notif.owner : PopupNotifications;
+ if (notif) {
+ notif.remove();
+ }
+ let promiseHidden = PN.isPanelOpen
+ ? BrowserTestUtils.waitForEvent(PN.panel, "popuphidden")
+ : Promise.resolve();
+ PN.panel.hidePopup();
+ await promiseHidden;
+}
+
+async function cleanupPasswordNotifications(
+ popupNotifications = PopupNotifications
+) {
+ let notif;
+ while ((notif = popupNotifications.getNotification("password"))) {
+ notif.remove();
+ }
+}
+
+async function clearMessageCache(browser) {
+ await SpecialPowers.spawn(browser, [], async () => {
+ const { LoginManagerChild } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginManagerChild.sys.mjs"
+ );
+ let docState = LoginManagerChild.forWindow(content).stateForDocument(
+ content.document
+ );
+ docState.lastSubmittedValuesByRootElement = new content.WeakMap();
+ });
+}
+
+/**
+ * Checks the doorhanger's username and password.
+ *
+ * @param {String} username The username.
+ * @param {String} password The password.
+ */
+async function checkDoorhangerUsernamePassword(username, password) {
+ await BrowserTestUtils.waitForCondition(() => {
+ return (
+ document.getElementById("password-notification-username").value ==
+ username &&
+ document.getElementById("password-notification-password").value ==
+ password
+ );
+ }, "Wait for nsLoginManagerPrompter writeDataToUI() to update to the correct username/password values");
+}
+
+/**
+ * Change the doorhanger's username and password input values.
+ *
+ * @param {object} newValues
+ * named values to update
+ * @param {string} [newValues.password = undefined]
+ * An optional string value to replace whatever is in the password field
+ * @param {string} [newValues.username = undefined]
+ * An optional string value to replace whatever is in the username field
+ * @param {Object} [popupNotifications = PopupNotifications]
+ */
+async function updateDoorhangerInputValues(
+ newValues,
+ popupNotifications = PopupNotifications
+) {
+ let { panel } = popupNotifications;
+ if (popupNotifications.panel.state !== "open") {
+ await BrowserTestUtils.waitForEvent(popupNotifications.panel, "popupshown");
+ }
+ Assert.equal(panel.state, "open", "Check the doorhanger is already open");
+
+ let notifElem = panel.childNodes[0];
+
+ // Note: setUserInput does not reliably dispatch input events from chrome elements?
+ async function setInputValue(target, value) {
+ info(`setInputValue: on target: ${target.id}, value: ${value}`);
+ target.focus();
+ target.select();
+ info(
+ `setInputValue: current value: '${target.value}', setting new value '${value}'`
+ );
+ await EventUtils.synthesizeKey("KEY_Backspace");
+ await EventUtils.sendString(value);
+ await EventUtils.synthesizeKey("KEY_Tab");
+ return Promise.resolve();
+ }
+
+ let passwordField = notifElem.querySelector(
+ "#password-notification-password"
+ );
+ let usernameField = notifElem.querySelector(
+ "#password-notification-username"
+ );
+
+ if (typeof newValues.password !== "undefined") {
+ if (passwordField.value !== newValues.password) {
+ await setInputValue(passwordField, newValues.password);
+ }
+ }
+ if (typeof newValues.username !== "undefined") {
+ if (usernameField.value !== newValues.username) {
+ await setInputValue(usernameField, newValues.username);
+ }
+ }
+}
+
+/**
+ * Open doorhanger autocomplete popup and select a username value.
+ *
+ * @param {string} text the text value of the username that should be selected.
+ * Noop if `text` is falsy.
+ */
+async function selectDoorhangerUsername(text) {
+ await _selectDoorhanger(
+ text,
+ "#password-notification-username",
+ "#password-notification-username-dropmarker"
+ );
+}
+
+/**
+ * Open doorhanger autocomplete popup and select a password value.
+ *
+ * @param {string} text the text value of the password that should be selected.
+ * Noop if `text` is falsy.
+ */
+async function selectDoorhangerPassword(text) {
+ await _selectDoorhanger(
+ text,
+ "#password-notification-password",
+ "#password-notification-password-dropmarker"
+ );
+}
+
+async function _selectDoorhanger(text, inputSelector, dropmarkerSelector) {
+ if (!text) {
+ return;
+ }
+
+ info("Opening doorhanger suggestion popup");
+
+ let doorhangerPopup = document.getElementById("password-notification");
+ let dropmarker = doorhangerPopup.querySelector(dropmarkerSelector);
+
+ let autocompletePopup = document.getElementById("PopupAutoComplete");
+ let popupShown = BrowserTestUtils.waitForEvent(
+ autocompletePopup,
+ "popupshown"
+ );
+ // the dropmarker gets un-hidden async when looking up username suggestions
+ await TestUtils.waitForCondition(() => !dropmarker.hidden);
+
+ EventUtils.synthesizeMouseAtCenter(dropmarker, {});
+
+ await popupShown;
+
+ let suggestions = [
+ ...document
+ .getElementById("PopupAutoComplete")
+ .getElementsByTagName("richlistitem"),
+ ].filter(richlistitem => !richlistitem.collapsed);
+
+ let suggestionText = suggestions.map(
+ richlistitem => richlistitem.querySelector(".ac-title-text").innerHTML
+ );
+
+ let targetIndex = suggestionText.indexOf(text);
+ Assert.ok(targetIndex != -1, "Suggestions include expected text");
+
+ let promiseHidden = BrowserTestUtils.waitForEvent(
+ autocompletePopup,
+ "popuphidden"
+ );
+
+ info("Selecting doorhanger suggestion");
+
+ EventUtils.synthesizeMouseAtCenter(suggestions[targetIndex], {});
+
+ await promiseHidden;
+}
+
+// End popup notification (doorhanger) functions //
+
+async function openPasswordManager(openingFunc, waitForFilter) {
+ info("waiting for new tab to open");
+ let tabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ url => url.includes("about:logins") && !url.includes("entryPoint="),
+ true
+ );
+ await openingFunc();
+ let tab = await tabPromise;
+ Assert.ok(tab, "got password management tab");
+ let filterValue;
+ if (waitForFilter) {
+ filterValue = await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ let loginFilter = Cu.waiveXrays(
+ content.document
+ .querySelector("login-list")
+ .shadowRoot.querySelector("login-filter")
+ );
+ await ContentTaskUtils.waitForCondition(
+ () => !!loginFilter.value,
+ "wait for login-filter to have a value"
+ );
+ return loginFilter.value;
+ });
+ }
+ return {
+ filterValue,
+ close() {
+ BrowserTestUtils.removeTab(tab);
+ },
+ };
+}
+
+// Autocomplete popup related functions //
+
+async function openACPopup(
+ popup,
+ browser,
+ inputSelector,
+ iframeBrowsingContext = null
+) {
+ let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+
+ await SimpleTest.promiseFocus(browser);
+ info("content window focused");
+
+ // Focus the username field to open the popup.
+ let target = iframeBrowsingContext || browser;
+ await SpecialPowers.spawn(
+ target,
+ [[inputSelector]],
+ function openAutocomplete(sel) {
+ content.document.querySelector(sel).focus();
+ }
+ );
+
+ let shown = await promiseShown;
+ Assert.ok(shown, "autocomplete popup shown");
+ return shown;
+}
+
+async function closePopup(popup) {
+ if (popup.state == "closed") {
+ await Promise.resolve();
+ } else {
+ let promiseHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ popup.hidePopup();
+ await promiseHidden;
+ }
+}
+
+async function fillGeneratedPasswordFromOpenACPopup(
+ browser,
+ passwordInputSelector
+) {
+ let popup = browser.ownerDocument.getElementById("PopupAutoComplete");
+ let item;
+
+ await new Promise(requestAnimationFrame);
+ await TestUtils.waitForCondition(() => {
+ item = popup.querySelector(`[originaltype="generatedPassword"]`);
+ return item && !EventUtils.isHidden(item);
+ }, "Waiting for item to become visible");
+
+ let inputEventPromise = ContentTask.spawn(
+ browser,
+ [passwordInputSelector],
+ async function waitForInput(inputSelector) {
+ let passwordInput = content.document.querySelector(inputSelector);
+ await ContentTaskUtils.waitForEvent(
+ passwordInput,
+ "input",
+ "Password input value changed"
+ );
+ }
+ );
+
+ let passwordGeneratedPromise = listenForTestNotification(
+ "PasswordEditedOrGenerated"
+ );
+
+ info("Clicking the generated password AC item");
+ EventUtils.synthesizeMouseAtCenter(item, {});
+ info("Waiting for the content input value to change");
+ await inputEventPromise;
+ info("Waiting for the passwordGeneratedPromise");
+ await passwordGeneratedPromise;
+}
+
+// Contextmenu functions //
+
+/**
+ * Synthesize mouse clicks to open the password manager context menu popup
+ * for a target password input element.
+ *
+ * assertCallback should return true if we should continue or else false.
+ */
+async function openPasswordContextMenu(
+ browser,
+ input,
+ assertCallback = null,
+ browsingContext = null,
+ openFillMenu = null
+) {
+ const doc = browser.ownerDocument;
+ const CONTEXT_MENU = doc.getElementById("contentAreaContextMenu");
+ const POPUP_HEADER = doc.getElementById("fill-login");
+ const LOGIN_POPUP = doc.getElementById("fill-login-popup");
+
+ if (!browsingContext) {
+ browsingContext = browser.browsingContext;
+ }
+
+ let contextMenuShownPromise = BrowserTestUtils.waitForEvent(
+ CONTEXT_MENU,
+ "popupshown"
+ );
+
+ // Synthesize a right mouse click over the password input element, we have to trigger
+ // both events because formfill code relies on this event happening before the contextmenu
+ // (which it does for real user input) in order to not show the password autocomplete.
+ let eventDetails = { type: "mousedown", button: 2 };
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ input,
+ eventDetails,
+ browsingContext
+ );
+ // Synthesize a contextmenu event to actually open the context menu.
+ eventDetails = { type: "contextmenu", button: 2 };
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ input,
+ eventDetails,
+ browsingContext
+ );
+
+ await contextMenuShownPromise;
+
+ if (assertCallback) {
+ let shouldContinue = await assertCallback();
+ if (!shouldContinue) {
+ return;
+ }
+ }
+
+ if (openFillMenu) {
+ // Open the fill login menu.
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ LOGIN_POPUP,
+ "popupshown"
+ );
+ POPUP_HEADER.openMenu(true);
+ await popupShownPromise;
+ }
+}
+
+/**
+ * Listen for the login manager test notification specified by
+ * expectedMessage. Possible messages:
+ * FormProcessed - a form was processed after page load.
+ * FormSubmit - a form was just submitted.
+ * PasswordEditedOrGenerated - a password was filled in or modified.
+ *
+ * The count is the number of that messages to wait for. This should
+ * typically be used when waiting for the FormProcessed message for a page
+ * that has subframes to ensure all have been handled.
+ *
+ * Returns a promise that will passed additional data specific to the message.
+ */
+function listenForTestNotification(expectedMessage, count = 1) {
+ return new Promise(resolve => {
+ LoginManagerParent.setListenerForTests((msg, data) => {
+ if (msg == expectedMessage && --count == 0) {
+ LoginManagerParent.setListenerForTests(null);
+ info("listenForTestNotification, resolving for message: " + msg);
+ resolve(data);
+ }
+ });
+ });
+}
+
+/**
+ * Use the contextmenu to fill a field with a generated password
+ */
+async function doFillGeneratedPasswordContextMenuItem(browser, passwordInput) {
+ await SimpleTest.promiseFocus(browser);
+ await openPasswordContextMenu(browser, passwordInput);
+
+ let generatedPasswordItem = document.getElementById(
+ "fill-login-generated-password"
+ );
+ let generatedPasswordSeparator = document.getElementById(
+ "passwordmgr-items-separator"
+ );
+
+ Assert.ok(
+ BrowserTestUtils.is_visible(generatedPasswordItem),
+ "generated password item is visible"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(generatedPasswordSeparator),
+ "separator is visible"
+ );
+
+ let popup = document.getElementById("PopupAutoComplete");
+ Assert.ok(popup, "Got popup");
+ let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+
+ await new Promise(resolve => {
+ SimpleTest.executeSoon(resolve);
+ });
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ contextMenu.activateItem(generatedPasswordItem);
+
+ await promiseShown;
+ await fillGeneratedPasswordFromOpenACPopup(browser, passwordInput);
+}
+
+// Content form helpers
+async function changeContentFormValues(
+ browser,
+ selectorValues,
+ shouldBlur = true
+) {
+ for (let [sel, value] of Object.entries(selectorValues)) {
+ info("changeContentFormValues, update: " + sel + ", to: " + value);
+ await changeContentInputValue(browser, sel, value, shouldBlur);
+ await TestUtils.waitForTick();
+ }
+}
+
+async function changeContentInputValue(
+ browser,
+ selector,
+ str,
+ shouldBlur = true
+) {
+ await SimpleTest.promiseFocus(browser.ownerGlobal);
+ let oldValue = await ContentTask.spawn(browser, [selector], function (sel) {
+ return content.document.querySelector(sel).value;
+ });
+
+ if (str === oldValue) {
+ info("no change needed to value of " + selector + ": " + oldValue);
+ return;
+ }
+ info(`changeContentInputValue: from "${oldValue}" to "${str}"`);
+ await ContentTask.spawn(
+ browser,
+ { selector, str, shouldBlur },
+ async function ({ selector, str, shouldBlur }) {
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ let input = content.document.querySelector(selector);
+
+ input.focus();
+ if (!str) {
+ input.select();
+ await EventUtils.synthesizeKey("KEY_Backspace", {}, content);
+ } else if (input.value.startsWith(str)) {
+ info(
+ `New string is substring of value: ${str.length}, ${input.value.length}`
+ );
+ input.setSelectionRange(str.length, input.value.length);
+ await EventUtils.synthesizeKey("KEY_Backspace", {}, content);
+ } else if (str.startsWith(input.value)) {
+ info(
+ `New string appends to value: ${input.value}, ${str.substring(
+ input.value.length
+ )}`
+ );
+ input.setSelectionRange(input.value.length, input.value.length);
+ await EventUtils.sendString(str.substring(input.value.length), content);
+ } else {
+ input.select();
+ await EventUtils.sendString(str, content);
+ }
+
+ if (shouldBlur) {
+ let changedPromise = ContentTaskUtils.waitForEvent(input, "change");
+ input.blur();
+ await changedPromise;
+ }
+
+ Assert.equal(str, input.value, `Expected value '${str}' is set on input`);
+ }
+ );
+ info("Input value changed");
+ await TestUtils.waitForTick();
+}
+
+async function verifyConfirmationHint(
+ browser,
+ forceClose,
+ anchorID = "password-notification-icon"
+) {
+ let hintElem = browser.ownerGlobal.ConfirmationHint._panel;
+ await BrowserTestUtils.waitForPopupEvent(hintElem, "shown");
+ try {
+ Assert.equal(hintElem.state, "open", "hint popup is open");
+ Assert.ok(
+ BrowserTestUtils.is_visible(hintElem.anchorNode),
+ "hint anchorNode is visible"
+ );
+ Assert.equal(
+ hintElem.anchorNode.id,
+ anchorID,
+ "Hint should be anchored on the expected notification icon"
+ );
+ info("verifyConfirmationHint, hint is shown and has its anchorNode");
+ if (forceClose) {
+ await closePopup(hintElem);
+ } else {
+ info("verifyConfirmationHint, assertion ok, wait for poopuphidden");
+ await BrowserTestUtils.waitForPopupEvent(hintElem, "hidden");
+ info("verifyConfirmationHint, hintElem popup is hidden");
+ }
+ } catch (ex) {
+ Assert.ok(false, "Confirmation hint not shown: " + ex.message);
+ } finally {
+ info("verifyConfirmationHint promise finalized");
+ }
+}
diff --git a/toolkit/components/passwordmgr/test/browser/insecure_test.html b/toolkit/components/passwordmgr/test/browser/insecure_test.html
new file mode 100644
index 0000000000..fedea1428e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/insecure_test.html
@@ -0,0 +1,9 @@
+<!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/ -->
+
+<!-- This frame is initially loaded over HTTP. -->
+<iframe id="test-iframe"
+ src="http://example.org/browser/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html"/>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html b/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html
new file mode 100644
index 0000000000..42411ff6bc
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html
@@ -0,0 +1,16 @@
+<!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/ -->
+
+<!-- Simplest form with username and password fields. -->
+<form id="form-basic" action="https://example.org/custom_action.html">
+ <input id="form-basic-username" name="username">
+ <input id="form-basic-password" name="password" type="password">
+ <input id="form-basic-submit" type="submit">
+</form>
+
+<!-- Link to reload this page over HTTPS. -->
+<a id="test-link"
+ href="https://example.org/browser/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.html">HTTPS</a>
+
+</body></html>
diff --git a/toolkit/components/passwordmgr/test/browser/multiple_forms.html b/toolkit/components/passwordmgr/test/browser/multiple_forms.html
new file mode 100644
index 0000000000..33c5de8a0e
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/multiple_forms.html
@@ -0,0 +1,145 @@
+<!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 class="test-form"
+ description="Password only form">
+ <input id='test-password-1' type='password' name='pname' value=''>
+ <input type='submit'>Submit</input>
+</form>
+
+<!-- This is a username-only form -->
+<form class="test-form"
+ description="Username only form">
+ <input id='test-username-1' type='text' name='uname' autocomplete='username' value=''>
+ <input type='submit'>Submit</input>
+</form>
+
+<!-- This is NOT a username-only form -->
+<form class="test-form"
+ description="text input only form">
+ <input id='test-input-2' type='text' name='uname' value=''>
+ <input type='submit'>Submit</input>
+</form>
+
+<form class="test-form"
+ description="Simple username and password blank form">
+ <input id='test-username-3' type='text' name='uname' value=''>
+ <input id='test-password-3' type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Simple username and password form, prefilled username">
+ <input id='test-username-4' type='text' name='uname' value='testuser'>
+ <input id='test-password-4' type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Simple username and password form, prefilled username and password">
+ <input id='test-username-5' type='text' name='uname' value='testuser'>
+ <input id='test-password-5' type='password' name='pname' value='testpass'>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="One username and two passwords empty form">
+ <input id='test-username-6' type='text' name='uname'>
+ <input id='test-password-6' type='password' name='pname'>
+ <input id='test-password2-6' type='password' name='pname2'>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="One username and two passwords form, fields prefiled">
+ <input id='test-username-7' type='text' name='uname' value="testuser">
+ <input id='test-password-7' type='password' name='pname' value="testpass">
+ <input id='test-password2-7' type='password' name='pname2' value="testpass">
+ <button type='submit'>Submit</button>
+</form>
+
+
+<div class="test-form"
+ description="Username and password fields with no form">
+ <input id='test-username-8' type='text' name='uname' value="testuser">
+ <input id='test-password-8' type='password' name='pname' value="testpass">
+</div>
+
+
+<form class="test-form"
+ description="Simple username and password blank form, with disabled password">
+ <input id='test-username-9' type='text' name='uname' value=''>
+ <input id='test-password-9' type='password' name='pname' value='' disabled>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Simple username and password blank form, with disabled username">
+ <input id='test-username-10' type='text' name='uname' value='' disabled>
+ <input id='test-password-10' type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Simple username and password blank form, with readonly password">
+ <input id='test-username-11' type='text' name='uname' value=''>
+ <input id='test-password-11' type='password' name='pname' value='' readonly>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Simple username and password blank form, with readonly username">
+ <input id='test-username-12' type='text' name='uname' value='' readonly>
+ <input id='test-password-12' type='password' name='pname' value=''>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Two username and one passwords form, fields prefiled">
+ <input id='test-username-13' type='text' name='uname' value="testuser">
+ <input id='test-username2-13' type='text' name='uname2' value="testuser">
+ <input id='test-password-13' type='password' name='pname' value="testpass">
+ <button type='submit'>Submit</button>
+</form>
+
+
+<form class="test-form"
+ description="Two username and one passwords form, one disabled username field">
+ <input id='test-username-14' type='text' name='uname'>
+ <input id='test-username2-14' type='text' name='uname2' disabled>
+ <input id='test-password-14' type='password' name='pname'>
+ <button type='submit'>Submit</button>
+</form>
+
+
+<div class="test-form"
+ description="Second username and password fields with no form">
+ <input id='test-username-15' type='text' name='uname'>
+ <input id='test-password-15' type='password' name='pname' expectedFail>
+</div>
+
+<form class="test-form"
+ description="Simple username and password blank form with the password field unmasked by JS">
+ <input id='test-username-16' type='text' name='uname' value=''>
+ <input id='test-password-16' type='password' name='pname' value='' data-type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<!-- Form in an iframe -->
+<iframe src="https://example.org/browser/toolkit/components/passwordmgr/test/browser/form_basic.html" id="test-iframe"></iframe>
+
+<script>
+ document.getElementById("test-password-16").type = "text";
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_1.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_1.html
new file mode 100644
index 0000000000..c90fb083ae
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_1.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications - Basic 1un 1pw</title>
+</head>
+<body>
+<h2>Subtest 1</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ SpecialPowers.wrap(userField).setUserInput("notifyu1");
+ SpecialPowers.wrap(passField).setUserInput("notifyp1");
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_10.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_10.html
new file mode 100644
index 0000000000..7eca613bc3
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_10.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 10</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ SpecialPowers.wrap(passField).setUserInput("notifyp1");
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html
new file mode 100644
index 0000000000..cd29d0536f
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications - Popup Windows</title>
+</head>
+<body>
+<h2>Subtest 11 (popup windows)</h2>
+<script>
+
+// Ignore the '?' and split on |
+let [username, password, features, autoClose] = window.location.search.substring(1).split("|");
+
+var url = "subtst_notifications_11_popup.html?" + username + "|" + password;
+var popupWin = window.open(url, "subtst_11", features);
+
+// Popup window will call this function on form submission.
+function formSubmitted() {
+ if (autoClose) {
+ popupWin.close();
+ }
+}
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html
new file mode 100644
index 0000000000..67aa3fafd8
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11_popup.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 11</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ // Get the password from the query string (exclude '?').
+ let [username, password] = window.location.search.substring(1).split("|");
+ SpecialPowers.wrap(userField).setUserInput(username);
+ SpecialPowers.wrap(passField).setUserInput(password);
+ form.submit();
+ window.opener.formSubmitted();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_12_target_blank.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_12_target_blank.html
new file mode 100644
index 0000000000..06c924c2ab
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_12_target_blank.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>target="_blank" subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 12 - target="_blank"</h2>
+<form id="form" action="formsubmit.sjs" target="_blank">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ // Get the password from the query string (exclude '?').
+ let [username, password] = window.location.search.substring(1).split("|");
+ SpecialPowers.wrap(userField).setUserInput(username);
+ SpecialPowers.wrap(passField).setUserInput(password);
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_2.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2.html
new file mode 100644
index 0000000000..e732252c16
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications - autocomplete=off on the username field</title>
+</head>
+<body>
+<h2>Subtest 2</h2>
+(username autocomplete=off)
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user" autocomplete="off">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ SpecialPowers.wrap(userField).setUserInput("notifyu1");
+ SpecialPowers.wrap(passField).setUserInput("notifyp1");
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_0un.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_0un.html
new file mode 100644
index 0000000000..f6a4b1bf78
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_0un.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications with 2 password fields and no username</title>
+</head>
+<body>
+<h2>Subtest 24</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="pass1" name="pass1" type="password" value="staticpw">
+ <input id="pass" name="pass" type="password">
+ <button type="submit">Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ SpecialPowers.wrap(pass).setUserInput("notifyp1");
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var pass = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_1un_1text.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_1un_1text.html
new file mode 100644
index 0000000000..d5ccbc8d18
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2pw_1un_1text.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications with 2 password fields and 1 username field and one other text field before the first password field</title>
+</head>
+<body>
+<h2>1 username field followed by a text field followed by 2 username fields</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user" value="staticpw">
+ <input id="city" name="city" value="city">
+ <input id="pass" name="pass" type="password">
+ <input id="pin" name="pin" type="password" value="static-pin">
+ <button type="submit">Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ SpecialPowers.wrap(userField).setUserInput("notifyu1");
+ SpecialPowers.wrap(passField).setUserInput("notifyp1");
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_3.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_3.html
new file mode 100644
index 0000000000..1a5e6c3417
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_3.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications - autocomplete=off on the password field</title>
+</head>
+<body>
+<h2>Subtest 3</h2>
+(password autocomplete=off)
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password" autocomplete="off">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ SpecialPowers.wrap(userField).setUserInput("notifyu1");
+ SpecialPowers.wrap(passField).setUserInput("notifyp1");
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_4.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_4.html
new file mode 100644
index 0000000000..2df721f995
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_4.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" >
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 4</h2>
+(form autocomplete=off)
+<form id="form" action="formsubmit.sjs" autocomplete="off">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ SpecialPowers.wrap(userField).setUserInput("notifyu1");
+ SpecialPowers.wrap(passField).setUserInput("notifyp1");
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_5.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_5.html
new file mode 100644
index 0000000000..b0c5034272
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_5.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications - Form with only a username field</title>
+</head>
+<body>
+<h2>Subtest 5</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ SpecialPowers.wrap(userField).setUserInput("notifyu1");
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_6.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_6.html
new file mode 100644
index 0000000000..a90930bb59
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_6.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 6</h2>
+(password-only form)
+<form id="form" action="formsubmit.sjs">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ SpecialPowers.wrap(passField).setUserInput("notifyp1");
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_8.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_8.html
new file mode 100644
index 0000000000..7455d15d90
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_8.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 8</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ SpecialPowers.wrap(userField).setUserInput("notifyu1");
+ SpecialPowers.wrap(passField).setUserInput("pass2");
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_9.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_9.html
new file mode 100644
index 0000000000..404b4f9a26
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_9.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Subtest 9</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ SpecialPowers.wrap(userField).setUserInput("");
+ SpecialPowers.wrap(passField).setUserInput("pass2");
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_notifications_change_p.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_change_p.html
new file mode 100644
index 0000000000..8f52fe9682
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_change_p.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Subtest for Login Manager notifications</title>
+</head>
+<body>
+<h2>Change password</h2>
+<form id="form" action="formsubmit.sjs">
+ <input id="pass_current" name="pass_current" type="password" value="notifyp1">
+ <input id="pass" name="pass" type="password">
+ <input id="pass_confirm" name="pass_confirm" type="password">
+ <button type='submit'>Submit</button>
+</form>
+
+<script>
+function submitForm() {
+ SpecialPowers.wrap(passField).setUserInput("pass2");
+ SpecialPowers.wrap(passConfirmField).setUserInput("pass2");
+
+ form.submit();
+}
+
+window.onload = submitForm;
+var form = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+var passConfirmField = document.getElementById("pass_confirm");
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/passwordmgr/test/browser/subtst_privbrowsing_1.html b/toolkit/components/passwordmgr/test/browser/subtst_privbrowsing_1.html
new file mode 100644
index 0000000000..2ef067ea43
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/subtst_privbrowsing_1.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test Login Manager notifications</title>
+</head>
+<body>
+<h2>Test Login Manager notifications</h2>
+
+<form id="form" action="formsubmit.sjs">
+ <input id="user" name="user" type="text">
+ <input id="pass" name="pass" type="password">
+ <button type='submit'>Submit</button>
+</form>
+</body>
+</html>