diff options
Diffstat (limited to 'toolkit/components/passwordmgr/test/browser')
57 files changed, 4836 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 000000000..7c8021192 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/mochitest/browser.eslintrc.js" + ] +}; diff --git a/toolkit/components/passwordmgr/test/browser/authenticate.sjs b/toolkit/components/passwordmgr/test/browser/authenticate.sjs new file mode 100644 index 000000000..fe2d2423c --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/authenticate.sjs @@ -0,0 +1,110 @@ +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"; + + // 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, authPresent = false; + if (request.hasHeader("Authorization")) { + authPresent = true; + authHeader = request.getHeader("Authorization"); + match = /Basic (.+)/.exec(authHeader); + if (match.length != 2) + throw new Error("Couldn't parse auth header: " + authHeader); + + var userpass = base64ToString(match[1]); // no atob() :-( + 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>"); +} + + +// base64 decoder +// +// Yoinked from extensions/xml-rpc/src/nsXmlRpcClient.js because btoa() +// doesn't seem to exist. :-( +/* Convert Base64 data to a string */ +const toBinaryTable = [ + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, + 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, + 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, + -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, + 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 +]; +const base64Pad = '='; + +function base64ToString(data) { + + var result = ''; + var leftbits = 0; // number of bits decoded, but yet to be appended + var leftdata = 0; // bits decoded, but yet to be appended + + // Convert one by one. + for (var i = 0; i < data.length; i++) { + var c = toBinaryTable[data.charCodeAt(i) & 0x7f]; + var padding = (data[i] == base64Pad); + // Skip illegal characters and whitespace + if (c == -1) continue; + + // Collect data into leftdata, update bitcount + leftdata = (leftdata << 6) | c; + leftbits += 6; + + // If we have 8 or more bits, append 8 bits to the result + if (leftbits >= 8) { + leftbits -= 8; + // Append if not padding. + if (!padding) + result += String.fromCharCode((leftdata >> leftbits) & 0xff); + leftdata &= (1 << leftbits) - 1; + } + } + + // If there are any bits left, the base64 string was corrupted + if (leftbits) + throw Components.Exception('Corrupted base64 string'); + + return result; +} diff --git a/toolkit/components/passwordmgr/test/browser/browser.ini b/toolkit/components/passwordmgr/test/browser/browser.ini new file mode 100644 index 000000000..b17591436 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser.ini @@ -0,0 +1,72 @@ +[DEFAULT] +support-files = + ../formsubmit.sjs + authenticate.sjs + form_basic.html + form_basic_iframe.html + formless_basic.html + form_same_origin_action.html + form_cross_origin_secure_action.html + head.js + insecure_test.html + insecure_test_subframe.html + multiple_forms.html + streamConverter_content.sjs + +[browser_autocomplete_insecure_warning.js] +support-files = + form_cross_origin_insecure_action.html +[browser_capture_doorhanger.js] +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_capture_doorhanger_httpsUpgrade.js] +support-files = + subtst_notifications_1.html + subtst_notifications_8.html +[browser_capture_doorhanger_window_open.js] +support-files = + subtst_notifications_11.html + subtst_notifications_11_popup.html +skip-if = os == "linux" # Bug 1312981, bug 1313136 +[browser_context_menu_autocomplete_interaction.js] +[browser_username_select_dialog.js] +support-files = + subtst_notifications_change_p.html +[browser_DOMFormHasPassword.js] +[browser_DOMInputPasswordAdded.js] +[browser_exceptions_dialog.js] +[browser_formless_submit_chrome.js] +[browser_hasInsecureLoginForms.js] +[browser_hasInsecureLoginForms_streamConverter.js] +[browser_http_autofill.js] +[browser_insecurePasswordConsoleWarning.js] +support-files = + form_cross_origin_insecure_action.html +[browser_master_password_autocomplete.js] +[browser_notifications.js] +[browser_notifications_username.js] +[browser_notifications_password.js] +[browser_notifications_2.js] +skip-if = os == "linux" # Bug 1272849 Main action button disabled state intermittent +[browser_passwordmgr_editing.js] +skip-if = os == "linux" +[browser_context_menu.js] +[browser_context_menu_iframe.js] +[browser_passwordmgr_contextmenu.js] +subsuite = clipboard +[browser_passwordmgr_fields.js] +[browser_passwordmgr_observers.js] +[browser_passwordmgr_sort.js] +[browser_passwordmgr_switchtab.js] +[browser_passwordmgrdlg.js] 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 000000000..80a0dd903 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_DOMFormHasPassword.js @@ -0,0 +1,94 @@ +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) { + 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, false); + gDoc.defaultView.setTimeout(test_inputAdd, 0); + } + + function test_inputAdd() { + addEventListener("DOMFormHasPassword", test_inputAddHandler, false); + 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) { + removeEventListener(evt.type, test_inputAddHandler, false); + is(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, false); + let input = gDoc.getElementById(contentIds.INPUT_ID); + input.setAttribute("form", contentIds.FORM2_ID); + } + + function test_inputChangeFormHandler(evt) { + removeEventListener(evt.type, test_inputChangeFormHandler, false); + is(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, false); + let input = gDoc.getElementById(contentIds.CHANGE_INPUT_ID); + input.setAttribute("type", "password"); + } + + function test_inputChangesTypeHandler(evt) { + removeEventListener(evt.type, test_inputChangesTypeHandler, false); + is(evt.target.id, contentIds.FORM1_ID, + evt.type + " event targets correct form element (changed type)"); + gDoc.defaultView.setTimeout(finish, 0); + } + + function finish() { + gDoc.removeEventListener("DOMFormHasPassword", unexpectedContentEvent, false); + resolve(); + } + + return promise; +} + +add_task(function* () { + let tab = gBrowser.selectedTab = gBrowser.addTab(); + + let promise = ContentTask.spawn(tab.linkedBrowser, ids, task); + tab.linkedBrowser.loadURI("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>"); + yield promise; + + ok(true, "Test completed"); + gBrowser.removeCurrentTab(); +}); 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 000000000..f54892e19 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_DOMInputPasswordAdded.js @@ -0,0 +1,99 @@ +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, false); + gDoc.defaultView.setTimeout(test_inputAdd, 0); + } + + function test_inputAdd() { + addEventListener("DOMInputPasswordAdded", test_inputAddHandler, false); + let input = gDoc.createElementNS(contentConsts.HTML_NS, "input"); + input.setAttribute("type", "password"); + input.setAttribute("id", contentConsts.INPUT_ID); + input.setAttribute("data-test", "unique-attribute"); + gDoc.getElementById(contentConsts.FORM1_ID).appendChild(input); + info("Done appending the input element"); + } + + function test_inputAddHandler(evt) { + removeEventListener(evt.type, test_inputAddHandler, false); + Assert.equal(evt.target.id, contentConsts.INPUT_ID, + evt.type + " event targets correct input element (added password element)"); + 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, false); + resolve(); + } + + return promise; +} + +add_task(function* () { + let tab = gBrowser.selectedTab = gBrowser.addTab(); + let promise = ContentTask.spawn(tab.linkedBrowser, consts, task); + tab.linkedBrowser.loadURI("data:text/html;charset=utf-8," + + "<html><body>" + + "<form id='" + consts.FORM1_ID + "'>" + + "<input id='" + consts.CHANGE_INPUT_ID + "'></form>" + + "<form id='" + consts.FORM2_ID + "'></form>" + + "</body></html>"); + yield promise; + gBrowser.removeCurrentTab(); +}); + 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 000000000..6aa8e5cf7 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_autocomplete_insecure_warning.js @@ -0,0 +1,41 @@ +"use strict"; + +const EXPECTED_SUPPORT_URL = Services.urlFormatter.formatURLPref("app.support.baseURL") + + "insecure-password"; + +add_task(function* test_clickInsecureFieldWarning() { + let url = "https://example.com" + DIRECTORY_PATH + "form_cross_origin_insecure_action.html"; + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url, + }, function*(browser) { + let popup = document.getElementById("PopupAutoComplete"); + ok(popup, "Got popup"); + + let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + + yield SimpleTest.promiseFocus(browser); + info("content window focused"); + + // Focus the username field to open the popup. + yield ContentTask.spawn(browser, null, function openAutocomplete() { + content.document.getElementById("form-basic-username").focus(); + }); + + yield promiseShown; + ok(promiseShown, "autocomplete shown"); + + let warningItem = document.getAnonymousElementByAttribute(popup, "type", "insecureWarning"); + ok(warningItem, "Got warning richlistitem"); + + yield 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 = yield supportTabPromise; + ok(supportTab, "Support tab opened"); + yield BrowserTestUtils.removeTab(supportTab); + }); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js new file mode 100644 index 000000000..b6bfdbf50 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js @@ -0,0 +1,600 @@ +/* + * 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_task(function* setup() { + // Load recipes for this test. + let recipeParent = yield LoginManagerParent.recipeParentPromise; + yield recipeParent.load({ + siteRecipes: [{ + hosts: ["example.org"], + usernameSelector: "#user", + passwordSelector: "#pass", + }], + }); +}); + +add_task(function* test_remember_opens() { + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + notif.remove(); + }); +}); + +add_task(function* test_clickNever() { + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + is(true, Services.logins.getLoginSavingEnabled("http://example.com"), + "Checking for login saving enabled"); + + yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1"); + clickDoorhangerButton(notif, NEVER_BUTTON); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); + + info("Make sure Never took effect"); + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(!notif, "checking for no notification popup"); + is(false, Services.logins.getLoginSavingEnabled("http://example.com"), + "Checking for login saving disabled"); + Services.logins.setLoginSavingEnabled("http://example.com", true); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +add_task(function* test_clickRemember() { + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); + + yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1"); + clickDoorhangerButton(notif, REMEMBER_BUTTON); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username used on the new entry"); + is(login.password, "notifyp1", "Check the password used on the new entry"); + is(login.timesUsed, 1, "Check times used on new entry"); + + info("Make sure Remember took effect and we don't prompt for an existing login"); + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(!notif, "checking for no notification popup"); + }); + + logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username used"); + is(login.password, "notifyp1", "Check the password used"); + is(login.timesUsed, 2, "Check times used incremented"); + + checkOnlyLoginWasUsedTwice({ justChanged: false }); + + // remove that login + Services.logins.removeLogin(login1); +}); + +/* signons.rememberSignons pref tests... */ + +add_task(function* test_rememberSignonsFalse() { + info("Make sure we don't prompt with rememberSignons=false"); + Services.prefs.setBoolPref("signon.rememberSignons", false); + + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(!notif, "checking for no notification popup"); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +add_task(function* test_rememberSignonsTrue() { + info("Make sure we prompt with rememberSignons=true"); + Services.prefs.setBoolPref("signon.rememberSignons", true); + + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + notif.remove(); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +/* autocomplete=off tests... */ + +add_task(function* test_autocompleteOffUsername() { + info("Check for notification popup when autocomplete=off present on username"); + + yield testSubmittingLoginForm("subtst_notifications_2.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "checking for notification popup"); + notif.remove(); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +add_task(function* test_autocompleteOffPassword() { + info("Check for notification popup when autocomplete=off present on password"); + + yield testSubmittingLoginForm("subtst_notifications_3.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "checking for notification popup"); + notif.remove(); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +add_task(function* test_autocompleteOffForm() { + info("Check for notification popup when autocomplete=off present on form"); + + yield testSubmittingLoginForm("subtst_notifications_4.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "checking for notification popup"); + notif.remove(); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + + +add_task(function* test_noPasswordField() { + info("Check for no notification popup when no password field present"); + + yield testSubmittingLoginForm("subtst_notifications_5.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "null", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(!notif, "checking for no notification popup"); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +add_task(function* test_pwOnlyLoginMatchesForm() { + info("Check for update popup when existing pw-only login matches form."); + Services.logins.addLogin(login2); + + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "checking for notification popup"); + notif.remove(); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "", "Check the username"); + is(login.password, "notifyp1", "Check the password"); + is(login.timesUsed, 1, "Check times used"); + + Services.logins.removeLogin(login2); +}); + +add_task(function* test_pwOnlyFormMatchesLogin() { + info("Check for no notification popup when pw-only form matches existing login."); + Services.logins.addLogin(login1); + + yield testSubmittingLoginForm("subtst_notifications_6.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(!notif, "checking for no notification popup"); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username"); + is(login.password, "notifyp1", "Check the password"); + is(login.timesUsed, 2, "Check times used"); + + Services.logins.removeLogin(login1); +}); + +add_task(function* test_pwOnlyFormDoesntMatchExisting() { + info("Check for notification popup when pw-only form doesn't match existing login."); + Services.logins.addLogin(login1B); + + yield testSubmittingLoginForm("subtst_notifications_6.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + notif.remove(); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1B", "Check the username unchanged"); + is(login.password, "notifyp1B", "Check the password unchanged"); + is(login.timesUsed, 1, "Check times used"); + + Services.logins.removeLogin(login1B); +}); + +add_task(function* test_changeUPLoginOnUPForm_dont() { + info("Check for change-password popup, u+p login on u+p form. (not changed)"); + Services.logins.addLogin(login1); + + yield testSubmittingLoginForm("subtst_notifications_8.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "pass2", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "got notification popup"); + + yield* checkDoorhangerUsernamePassword("notifyu1", "pass2"); + clickDoorhangerButton(notif, DONT_CHANGE_BUTTON); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username unchanged"); + is(login.password, "notifyp1", "Check the password unchanged"); + is(login.timesUsed, 1, "Check times used"); + + Services.logins.removeLogin(login1); +}); + +add_task(function* test_changeUPLoginOnUPForm_change() { + info("Check for change-password popup, u+p login on u+p form."); + Services.logins.addLogin(login1); + + yield testSubmittingLoginForm("subtst_notifications_8.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "pass2", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "got notification popup"); + + yield* checkDoorhangerUsernamePassword("notifyu1", "pass2"); + clickDoorhangerButton(notif, CHANGE_BUTTON); + + ok(!getCaptureDoorhanger("password-change"), "popup should be gone"); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username unchanged"); + is(login.password, "pass2", "Check the password changed"); + is(login.timesUsed, 2, "Check times used"); + + checkOnlyLoginWasUsedTwice({ justChanged: true }); + + // cleanup + login1.password = "pass2"; + Services.logins.removeLogin(login1); + login1.password = "notifyp1"; +}); + +add_task(function* test_changePLoginOnUPForm() { + info("Check for change-password popup, p-only login on u+p form."); + Services.logins.addLogin(login2); + + yield testSubmittingLoginForm("subtst_notifications_9.html", function*(fieldValues) { + is(fieldValues.username, "", "Checking submitted username"); + is(fieldValues.password, "pass2", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "got notification popup"); + + yield* checkDoorhangerUsernamePassword("", "pass2"); + clickDoorhangerButton(notif, CHANGE_BUTTON); + + ok(!getCaptureDoorhanger("password-change"), "popup should be gone"); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "", "Check the username unchanged"); + is(login.password, "pass2", "Check the password changed"); + is(login.timesUsed, 2, "Check times used"); + + // no cleanup -- saved password to be used in the next test. +}); + +add_task(function* test_changePLoginOnPForm() { + info("Check for change-password popup, p-only login on p-only form."); + + yield testSubmittingLoginForm("subtst_notifications_10.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "got notification popup"); + + yield* checkDoorhangerUsernamePassword("", "notifyp1"); + clickDoorhangerButton(notif, CHANGE_BUTTON); + + ok(!getCaptureDoorhanger("password-change"), "popup should be gone"); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "", "Check the username unchanged"); + is(login.password, "notifyp1", "Check the password changed"); + is(login.timesUsed, 3, "Check times used"); + + Services.logins.removeLogin(login2); +}); + +add_task(function* test_checkUPSaveText() { + info("Check text on a user+pass notification popup"); + + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + // Check the text, which comes from the localized saveLoginText string. + let notificationText = notif.message; + let expectedText = "Would you like " + BRAND_SHORT_NAME + " to remember this login?"; + is(expectedText, notificationText, "Checking text: " + notificationText); + notif.remove(); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +add_task(function* test_checkPSaveText() { + info("Check text on a pass-only notification popup"); + + yield testSubmittingLoginForm("subtst_notifications_6.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + // Check the text, which comes from the localized saveLoginTextNoUser string. + let notificationText = notif.message; + let expectedText = "Would you like " + BRAND_SHORT_NAME + " to remember this password?"; + is(expectedText, notificationText, "Checking text: " + notificationText); + notif.remove(); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +add_task(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."); + + yield testSubmittingLoginForm("subtst_notifications_2pw_0un.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + notif.remove(); + }); + + is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet"); +}); + +add_task(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."); + + Services.logins.addLogin(login1B); + + yield testSubmittingLoginForm("subtst_notifications_2pw_0un.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "got notification popup"); + notif.remove(); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1B", "Check the username unchanged"); + is(login.password, "notifyp1B", "Check the password unchanged"); + is(login.timesUsed, 1, "Check times used"); + + Services.logins.removeLogin(login1B); +}); + +add_task(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."); + + Services.logins.addLogin(login2B); + + yield testSubmittingLoginForm("subtst_notifications_2pw_0un.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "got notification popup"); + notif.remove(); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "", "Check the username unchanged"); + is(login.password, "notifyp1B", "Check the password unchanged"); + is(login.timesUsed, 1, "Check times used"); + + Services.logins.removeLogin(login2B); +}); + +add_task(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."); + + Services.logins.addLogin(login2); + + yield testSubmittingLoginForm("subtst_notifications_2pw_0un.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(!notif, "checking for no notification popup"); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "", "Check the username unchanged"); + is(login.password, "notifyp1", "Check the password unchanged"); + is(login.timesUsed, 2, "Check times used incremented"); + + checkOnlyLoginWasUsedTwice({ justChanged: false }); + + Services.logins.removeLogin(login2); +}); + +add_task(function* test_changeUPLoginOnPUpdateForm() { + info("Check for change-password popup, u+p login on password update form."); + Services.logins.addLogin(login1); + + yield testSubmittingLoginForm("subtst_notifications_change_p.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "pass2", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "got notification popup"); + + yield* checkDoorhangerUsernamePassword("notifyu1", "pass2"); + clickDoorhangerButton(notif, CHANGE_BUTTON); + + ok(!getCaptureDoorhanger("password-change"), "popup should be gone"); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username unchanged"); + is(login.password, "pass2", "Check the password changed"); + is(login.timesUsed, 2, "Check times used"); + + checkOnlyLoginWasUsedTwice({ justChanged: true }); + + // cleanup + login1.password = "pass2"; + Services.logins.removeLogin(login1); + login1.password = "notifyp1"; +}); + +add_task(function* test_recipeCaptureFields_NewLogin() { + info("Check that we capture the proper fields when a field recipe is in use."); + + yield testSubmittingLoginForm("subtst_notifications_2pw_1un_1text.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + + // Sanity check, no logins should exist yet. + let logins = Services.logins.getAllLogins(); + is(logins.length, 0, "Should not have any logins yet"); + + yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1"); + clickDoorhangerButton(notif, REMEMBER_BUTTON); + + }, "http://example.org"); // The recipe is for example.org + + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username unchanged"); + is(login.password, "notifyp1", "Check the password unchanged"); + is(login.timesUsed, 1, "Check times used"); +}); + +add_task(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"); + + yield testSubmittingLoginForm("subtst_notifications_2pw_1un_1text.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(!notif, "checking for no notification popup"); + }, "http://example.org"); + + checkOnlyLoginWasUsedTwice({ justChanged: false }); + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username unchanged"); + is(login.password, "notifyp1", "Check the password unchanged"); + is(login.timesUsed, 2, "Check times used incremented"); + + Services.logins.removeAllLogins(); +}); + +add_task(function* test_noShowPasswordOnDismissal() { + info("Check for no Show Password field when the doorhanger is dismissed"); + + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + info("Opening popup"); + let notif = getCaptureDoorhanger("password-save"); + let { panel } = PopupNotifications; + + info("Hiding popup."); + let promiseHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden"); + panel.hidePopup(); + yield promiseHidden; + + info("Clicking on anchor to reshow popup."); + let promiseShown = BrowserTestUtils.waitForEvent(panel, "popupshown"); + notif.anchorElement.click(); + yield promiseShown; + + let passwordVisiblityToggle = panel.querySelector("#password-notification-visibilityToggle"); + is(passwordVisiblityToggle.hidden, true, "Check that the Show Password field is Hidden"); + }); +}); + +// TODO: +// * existing login test, form has different password --> change password, no save prompt diff --git a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_httpsUpgrade.js b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_httpsUpgrade.js new file mode 100644 index 000000000..9be0aa631 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_httpsUpgrade.js @@ -0,0 +1,123 @@ +/* + * 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(function* test_httpsUpgradeCaptureFields_noChange() { + info("Check that we don't prompt to remember when capturing an upgraded login with no change"); + Services.logins.addLogin(login1); + // Sanity check the HTTP login exists. + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should have the HTTP login"); + + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(!notif, "checking for no notification popup"); + }, "https://example.com"); // This is HTTPS whereas the saved login is HTTP + + logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login still"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.hostname, "http://example.com", "Check the hostname is unchanged"); + is(login.username, "notifyu1", "Check the username is unchanged"); + is(login.password, "notifyp1", "Check the password is unchanged"); + is(login.timesUsed, 2, "Check times used increased"); + + Services.logins.removeLogin(login1); +}); + +add_task(function* test_httpsUpgradeCaptureFields_changePW() { + info("Check that we prompt to change when capturing an upgraded login with a new PW"); + Services.logins.addLogin(login1); + // Sanity check the HTTP login exists. + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should have the HTTP login"); + + yield testSubmittingLoginForm("subtst_notifications_8.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "pass2", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-change"); + ok(notif, "checking for a change popup"); + + yield* checkDoorhangerUsernamePassword("notifyu1", "pass2"); + clickDoorhangerButton(notif, CHANGE_BUTTON); + + 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(); + is(logins.length, 1, "Should only have 1 login still"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.hostname, "https://example.com", "Check the hostname is upgraded"); + is(login.formSubmitURL, "https://example.com", "Check the formSubmitURL is upgraded"); + is(login.username, "notifyu1", "Check the username is unchanged"); + is(login.password, "pass2", "Check the password changed"); + is(login.timesUsed, 2, "Check times used increased"); + + Services.logins.removeAllLogins(); +}); + +add_task(function* test_httpsUpgradeCaptureFields_captureMatchingHTTP() { + info("Capture a new HTTP login which matches a stored HTTPS one."); + Services.logins.addLogin(login1HTTPS); + + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(notif, "got notification popup"); + + is(Services.logins.getAllLogins().length, 1, "Should only have the HTTPS login"); + + yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1"); + clickDoorhangerButton(notif, REMEMBER_BUTTON); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 2, "Should have both HTTP and HTTPS logins"); + for (let login of logins) { + login = login.QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username used on the new entry"); + is(login.password, "notifyp1", "Check the password used on the new entry"); + is(login.timesUsed, 1, "Check times used on entry"); + } + + info("Make sure Remember took effect and we don't prompt for an existing HTTP login"); + yield testSubmittingLoginForm("subtst_notifications_1.html", function*(fieldValues) { + is(fieldValues.username, "notifyu1", "Checking submitted username"); + is(fieldValues.password, "notifyp1", "Checking submitted password"); + let notif = getCaptureDoorhanger("password-save"); + ok(!notif, "checking for no notification popup"); + }); + + logins = Services.logins.getAllLogins(); + is(logins.length, 2, "Should have both HTTP and HTTPS still"); + + let httpsLogins = LoginHelper.searchLoginsWithObject({ + hostname: "https://example.com", + }); + is(httpsLogins.length, 1, "Check https logins count"); + let httpsLogin = httpsLogins[0].QueryInterface(Ci.nsILoginMetaInfo); + ok(httpsLogin.equals(login1HTTPS), "Check HTTPS login didn't change"); + is(httpsLogin.timesUsed, 1, "Check times used"); + + let httpLogins = LoginHelper.searchLoginsWithObject({ + hostname: "http://example.com", + }); + is(httpLogins.length, 1, "Check http logins count"); + let httpLogin = httpLogins[0].QueryInterface(Ci.nsILoginMetaInfo); + ok(httpLogin.equals(login1), "Check HTTP login is as expected"); + is(httpLogin.timesUsed, 2, "Check times used increased"); + + Services.logins.removeLogin(login1); + Services.logins.removeLogin(login1HTTPS); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js new file mode 100644 index 000000000..1bcfec5eb --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js @@ -0,0 +1,144 @@ +/* + * 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, + }, function*(browser) { + ok(true, "loaded " + aPageFile); + info("running test case task"); + yield* aTaskFn(); + info("waiting for storage change"); + yield storageChangedPromised; + }); +} + +add_task(function* setup() { + yield SimpleTest.promiseFocus(window); +}); + +add_task(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"; + yield withTestTabUntilStorageChange(url, function*() { + info("waiting for popupshown"); + yield notifShownPromise; + // the popup closes and the doorhanger should appear in the opener + let popup = getCaptureDoorhanger("password-save"); + ok(popup, "got notification popup"); + yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1"); + // Sanity check, no logins should exist yet. + let logins = Services.logins.getAllLogins(); + is(logins.length, 0, "Should not have any logins yet"); + + clickDoorhangerButton(popup, REMEMBER_BUTTON); + }); + // Check result of clicking Remember + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.timesUsed, 1, "Check times used on new entry"); + is(login.username, "notifyu1", "Check the username used on the new entry"); + is(login.password, "notifyp1", "Check the password used on the new entry"); +}); + +add_task(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"; + yield withTestTabUntilStorageChange(url, function*() { + info("waiting for popupshown"); + yield notifShownPromise; + let popup = getCaptureDoorhanger("password-change"); + ok(popup, "got notification popup"); + yield* 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(); + is(logins.length, 1, "Should only have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username"); + is(login.password, "pass2", "Check password changed"); + is(login.timesUsed, 2, "check .timesUsed incremented on change"); + ok(login.timeCreated < login.timeLastUsed, "timeLastUsed bumped"); + ok(login.timeLastUsed == login.timePasswordChanged, "timeUsed == timeChanged"); + + login1.password = "pass2"; + Services.logins.removeLogin(login1); + login1.password = "notifyp1"; +}); + +add_task(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"); + yield withTestTabUntilStorageChange(url, function*() { + yield notifShownPromise; + let popup = getCaptureDoorhanger("password-save"); + ok(popup, "got notification popup"); + yield* checkDoorhangerUsernamePassword("notifyu2", "notifyp2"); + clickDoorhangerButton(popup, REMEMBER_BUTTON); + yield BrowserTestUtils.removeTab(gBrowser.selectedTab); + }); + + // Check result of clicking Remember + let logins = Services.logins.getAllLogins(); + is(logins.length, 1, "Should only have 1 login now"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu2", "Check the username used on the new entry"); + is(login.password, "notifyp2", "Check the password used on the new entry"); + is(login.timesUsed, 1, "Check times used on new entry"); +}); + +add_task(function* test_changeChromeVisibleSameWindow() { + let url = "subtst_notifications_11.html?notifyu2|pass2||"; + let notifShownPromise = BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown"); + yield withTestTabUntilStorageChange(url, function*() { + yield notifShownPromise; + let popup = getCaptureDoorhanger("password-change"); + ok(popup, "got notification popup"); + yield* checkDoorhangerUsernamePassword("notifyu2", "pass2"); + clickDoorhangerButton(popup, CHANGE_BUTTON); + yield 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(); + is(logins.length, 1, "Should have 1 login"); + let login = logins[0].QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu2", "Check the username"); + is(login.password, "pass2", "Check password changed"); + is(login.timesUsed, 2, "check .timesUsed incremented on change"); + ok(login.timeCreated < login.timeLastUsed, "timeLastUsed bumped"); + 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_context_menu.js b/toolkit/components/passwordmgr/test/browser/browser_context_menu.js new file mode 100644 index 000000000..6cfcaa7c2 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu.js @@ -0,0 +1,432 @@ +/* + * Test the password manager context menu. + */ + +/* eslint no-shadow:"off" */ + +"use strict"; + +// The hostname for the test URIs. +const TEST_HOSTNAME = "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(function* test_initialize() { + Services.prefs.setBoolPref("signon.autofillForms", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("signon.autofillForms"); + Services.prefs.clearUserPref("signon.schemeUpgrades"); + }); + for (let login of loginList()) { + Services.logins.addLogin(login); + } +}); + +/** + * Check if the context menu is populated with the right + * menuitems for the target password input field. + */ +add_task(function* test_context_menu_populate_password_noSchemeUpgrades() { + Services.prefs.setBoolPref("signon.schemeUpgrades", false); + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH, + }, function* (browser) { + yield openPasswordContextMenu(browser, "#test-password-1"); + + // Check the content of the password manager popup + let popupMenu = document.getElementById("fill-login-popup"); + checkMenu(popupMenu, 2); + + CONTEXT_MENU.hidePopup(); + }); +}); + +/** + * Check if the context menu is populated with the right + * menuitems for the target password input field. + */ +add_task(function* test_context_menu_populate_password_schemeUpgrades() { + Services.prefs.setBoolPref("signon.schemeUpgrades", true); + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH, + }, function* (browser) { + yield openPasswordContextMenu(browser, "#test-password-1"); + + // Check the content of the password manager popup + let popupMenu = document.getElementById("fill-login-popup"); + checkMenu(popupMenu, 3); + + CONTEXT_MENU.hidePopup(); + }); +}); + +/** + * Check if the context menu is populated with the right menuitems + * for the target username field with a password field present. + */ +add_task(function* test_context_menu_populate_username_with_password_noSchemeUpgrades() { + Services.prefs.setBoolPref("signon.schemeUpgrades", false); + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + "/browser/toolkit/components/" + + "passwordmgr/test/browser/multiple_forms.html", + }, function* (browser) { + yield openPasswordContextMenu(browser, "#test-username-2"); + + // Check the content of the password manager popup + let popupMenu = document.getElementById("fill-login-popup"); + checkMenu(popupMenu, 2); + + CONTEXT_MENU.hidePopup(); + }); +}); +/** + * Check if the context menu is populated with the right menuitems + * for the target username field with a password field present. + */ +add_task(function* test_context_menu_populate_username_with_password_schemeUpgrades() { + Services.prefs.setBoolPref("signon.schemeUpgrades", true); + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + "/browser/toolkit/components/" + + "passwordmgr/test/browser/multiple_forms.html", + }, function* (browser) { + yield openPasswordContextMenu(browser, "#test-username-2"); + + // Check the content of the password manager popup + let popupMenu = document.getElementById("fill-login-popup"); + checkMenu(popupMenu, 3); + + CONTEXT_MENU.hidePopup(); + }); +}); + +/** + * Check if the password field is correctly filled when one + * login menuitem is clicked. + */ +add_task(function* test_context_menu_password_fill() { + Services.prefs.setBoolPref("signon.schemeUpgrades", true); + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH, + }, function* (browser) { + let formDescriptions = yield ContentTask.spawn(browser, {}, 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 = yield ContentTask.spawn(browser, {description}, function*({description}) { + let formElement = content.document.querySelector(`[description="${description}"]`); + let passwords = Array.from(formElement.querySelectorAll("input[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 username input element. + yield openPasswordContextMenu(browser, "#" + inputId, function*() { + let inputDisabled = yield ContentTask + .spawn(browser, {inputId}, 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."); + CONTEXT_MENU.hidePopup(); + } + + return !inputDisabled; + }); + + if (CONTEXT_MENU.state != "open") { + continue; + } + + // The only field affected by the password fill + // should be the target password field itself. + yield assertContextMenuFill(browser, description, null, inputId, 1); + yield ContentTask.spawn(browser, {inputId}, function*({inputId}) { + let passwordField = content.document.getElementById(inputId); + Assert.equal(passwordField.value, "password1", "Check upgraded login was actually used"); + }); + + CONTEXT_MENU.hidePopup(); + } + } + }); +}); + +/** + * Check if the form is correctly filled when one + * username context menu login menuitem is clicked. + */ +add_task(function* test_context_menu_username_login_fill() { + Services.prefs.setBoolPref("signon.schemeUpgrades", true); + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + MULTIPLE_FORMS_PAGE_PATH, + }, function* (browser) { + + let formDescriptions = yield ContentTask.spawn(browser, {}, 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 = yield ContentTask + .spawn(browser, {description}, function*({description}) { + let formElement = content.document.querySelector(`[description="${description}"]`); + let inputs = Array.from(formElement.querySelectorAll("input[type='text']")); + 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. + yield openPasswordContextMenu(browser, "#" + inputId, function*() { + let headerHidden = POPUP_HEADER.hidden; + let headerDisabled = POPUP_HEADER.disabled; + + let data = {description, inputId, headerHidden, headerDisabled}; + let shouldContinue = yield ContentTask.spawn(browser, data, function*(data) { + let {description, inputId, headerHidden, headerDisabled} = 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']"); + + // 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) { + 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; + } + return true; + }); + + if (!shouldContinue) { + CONTEXT_MENU.hidePopup(); + } + + return shouldContinue; + }); + + if (CONTEXT_MENU.state != "open") { + continue; + } + + let passwordFieldId = yield ContentTask + .spawn(browser, {description}, function*({description}) { + let formElement = content.document.querySelector(`[description="${description}"]`); + return formElement.querySelector("input[type='password']").id; + }); + + // We shouldn't change any field that's not the target username field or the first password field + yield assertContextMenuFill(browser, description, inputId, passwordFieldId, 1); + + yield ContentTask.spawn(browser, {passwordFieldId}, function*({passwordFieldId}) { + let passwordField = content.document.getElementById(passwordFieldId); + if (!passwordField.hasAttribute("expectedFail")) { + Assert.equal(passwordField.value, "password1", "Check upgraded login was actually used"); + } + }); + + CONTEXT_MENU.hidePopup(); + } + } + }); +}); + +/** + * 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. + */ +function* openPasswordContextMenu(browser, passwordInput, assertCallback = null) { + // Synthesize a right mouse click over the password input element. + let contextMenuShownPromise = BrowserTestUtils.waitForEvent(CONTEXT_MENU, "popupshown"); + let eventDetails = {type: "contextmenu", button: 2}; + BrowserTestUtils.synthesizeMouseAtCenter(passwordInput, eventDetails, browser); + yield contextMenuShownPromise; + + if (assertCallback) { + let shouldContinue = yield assertCallback(); + if (!shouldContinue) { + return; + } + } + + // Synthesize a mouse click over the fill login menu header. + let popupShownPromise = BrowserTestUtils.waitForEvent(POPUP_HEADER, "popupshown"); + EventUtils.synthesizeMouseAtCenter(POPUP_HEADER, {}); + yield popupShownPromise; +} + +/** + * Verify that only the expected form fields are filled. + */ +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})`; + } + + yield ContentTask.spawn(browser, {unchangedSelector}, 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, function*(data) { + let {username, password, usernameFieldId, passwordFieldId, formId, unchangedSelector} = data; + let form = content.document.querySelector(`[description="${formId}"]`); + yield 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 hostname 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.hostname, TEST_HOSTNAME, { + 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/hostname 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({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: "username", + password: "password", + }), + // Same as above but HTTP in order to test de-duping. + LoginTestUtils.testData.formLogin({ + hostname: "http://example.com", + formSubmitURL: "http://example.com", + username: "username", + password: "password", + }), + LoginTestUtils.testData.formLogin({ + hostname: "http://example.com", + formSubmitURL: "http://example.com", + username: "username1", + password: "password1", + }), + LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: "username2", + password: "password2", + }), + LoginTestUtils.testData.formLogin({ + hostname: "http://example.org", + formSubmitURL: "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 000000000..1b37e3f79 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js @@ -0,0 +1,99 @@ +/* + * Test the password manager context menu interaction with autocomplete. + */ + +"use strict"; + +const TEST_HOSTNAME = "https://example.com"; +const BASIC_FORM_PAGE_PATH = DIRECTORY_PATH + "form_basic.html"; + +var gUnexpectedIsTODO = false; + +/** + * Initialize logins needed for the tests and disable autofill + * for login forms for easier testing of manual fill. + */ +add_task(function* test_initialize() { + let autocompletePopup = document.getElementById("PopupAutoComplete"); + Services.prefs.setBoolPref("signon.autofillForms", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("signon.autofillForms"); + autocompletePopup.removeEventListener("popupshowing", autocompleteUnexpectedPopupShowing); + }); + for (let login of loginList()) { + Services.logins.addLogin(login); + } + autocompletePopup.addEventListener("popupshowing", autocompleteUnexpectedPopupShowing); +}); + +add_task(function* test_context_menu_username() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + BASIC_FORM_PAGE_PATH, + }, function* (browser) { + yield openContextMenu(browser, "#form-basic-username"); + + let contextMenu = document.getElementById("contentAreaContextMenu"); + Assert.equal(contextMenu.state, "open", "Context menu opened"); + contextMenu.hidePopup(); + }); +}); + +add_task(function* test_context_menu_password() { + gUnexpectedIsTODO = true; + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + BASIC_FORM_PAGE_PATH, + }, function* (browser) { + yield openContextMenu(browser, "#form-basic-password"); + + let contextMenu = document.getElementById("contentAreaContextMenu"); + Assert.equal(contextMenu.state, "open", "Context menu opened"); + contextMenu.hidePopup(); + }); +}); + +function autocompleteUnexpectedPopupShowing(event) { + if (gUnexpectedIsTODO) { + todo(false, "Autocomplete shouldn't appear"); + } else { + 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. + */ +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}; + 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}; + BrowserTestUtils.synthesizeMouseAtCenter(loginInput, eventDetails, browser); + yield contextMenuShownPromise; + + // Wait to see which popups are shown. + yield new Promise(resolve => setTimeout(resolve, 1000)); +} + +function loginList() { + return [ + LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: "username", + password: "password", + }), + LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: "username2", + password: "password2", + }), + ]; +} 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 000000000..c5219789d --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu_iframe.js @@ -0,0 +1,144 @@ +/* + * Test the password manager context menu. + */ + +"use strict"; + +const TEST_HOSTNAME = "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(function* test_initialize() { + Services.prefs.setBoolPref("signon.autofillForms", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("signon.autofillForms"); + Services.prefs.clearUserPref("signon.schemeUpgrades"); + }); + for (let login of loginList()) { + Services.logins.addLogin(login); + } +}); + +/** + * Check if the password field is correctly filled when it's in an iframe. + */ +add_task(function* test_context_menu_iframe_fill() { + Services.prefs.setBoolPref("signon.schemeUpgrades", true); + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: TEST_HOSTNAME + IFRAME_PAGE_PATH + }, function* (browser) { + function getPasswordInput() { + let frame = content.document.getElementById("test-iframe"); + return frame.contentDocument.getElementById("form-basic-password"); + } + + let contextMenuShownPromise = BrowserTestUtils.waitForEvent(window, "popupshown"); + let eventDetails = {type: "contextmenu", button: 2}; + + // To click at the right point we have to take into account the iframe offset. + // Synthesize a right mouse click over the password input element. + BrowserTestUtils.synthesizeMouseAtCenter(getPasswordInput, eventDetails, browser); + yield contextMenuShownPromise; + + // Synthesize a mouse click over the fill login menu header. + let popupHeader = document.getElementById("fill-login"); + let popupShownPromise = BrowserTestUtils.waitForEvent(popupHeader, "popupshown"); + EventUtils.synthesizeMouseAtCenter(popupHeader, {}); + yield popupShownPromise; + + let popupMenu = document.getElementById("fill-login-popup"); + + // Stores the original value of username + function promiseFrameInputValue(name) { + return ContentTask.spawn(browser, name, function(inputname) { + let iframe = content.document.getElementById("test-iframe"); + let input = iframe.contentDocument.getElementById(inputname); + return input.value; + }); + } + let usernameOriginalValue = yield promiseFrameInputValue("form-basic-username"); + + // Execute the command of the first login menuitem found at the context menu. + let passwordChangedPromise = ContentTask.spawn(browser, null, function* () { + let frame = content.document.getElementById("test-iframe"); + let passwordInput = frame.contentDocument.getElementById("form-basic-password"); + yield ContentTaskUtils.waitForEvent(passwordInput, "input"); + }); + + let firstLoginItem = popupMenu.getElementsByClassName("context-login-item")[0]; + firstLoginItem.doCommand(); + + yield passwordChangedPromise; + + // Find the used login by it's username. + let login = getLoginFromUsername(firstLoginItem.label); + let passwordValue = yield promiseFrameInputValue("form-basic-password"); + is(login.password, passwordValue, "Password filled and correct."); + + let usernameNewValue = yield promiseFrameInputValue("form-basic-username"); + is(usernameOriginalValue, + usernameNewValue, + "Username value was not changed."); + + let contextMenu = document.getElementById("contentAreaContextMenu"); + contextMenu.hidePopup(); + }); +}); + +/** + * Search for a login by it's username. + * + * Only unique login/hostname 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({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: "username", + password: "password", + }), + // Same as above but HTTP in order to test de-duping. + LoginTestUtils.testData.formLogin({ + hostname: "http://example.com", + formSubmitURL: "http://example.com", + username: "username", + password: "password", + }), + LoginTestUtils.testData.formLogin({ + hostname: "http://example.com", + formSubmitURL: "http://example.com", + username: "username1", + password: "password1", + }), + LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: "username2", + password: "password2", + }), + LoginTestUtils.testData.formLogin({ + hostname: "http://example.org", + formSubmitURL: "http://example.org", + username: "username-cross-origin", + password: "password-cross-origin", + }), + ]; +} 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 000000000..09fbe0eea --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_exceptions_dialog.js @@ -0,0 +1,56 @@ + +"use strict"; + +const LOGIN_HOST = "http://example.com"; + +function openExceptionsDialog() { + return window.openDialog( + "chrome://browser/content/preferences/permissions.xul", + "Toolkit:PasswordManagerExceptions", "", + { + blockVisible: true, + sessionVisible: false, + allowVisible: false, + hideStatusColumn: true, + prefilledHost: "", + permissionType: "login-saving" + } + ); +} + +function countDisabledHosts(dialog) { + let doc = dialog.document; + let rejectsTree = doc.getElementById("permissionsTree"); + + return rejectsTree.view.rowCount; +} + +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(function* test_disable() { + let dialog = openExceptionsDialog(); + let promiseChanged = promiseStorageChanged("hostSavingDisabled"); + + yield BrowserTestUtils.waitForEvent(dialog, "load"); + Services.logins.setLoginSavingEnabled(LOGIN_HOST, false); + yield promiseChanged; + is(countDisabledHosts(dialog), 1, "Verify disabled host added"); + yield BrowserTestUtils.closeWindow(dialog); +}); + +add_task(function* test_enable() { + let dialog = openExceptionsDialog(); + let promiseChanged = promiseStorageChanged("hostSavingEnabled"); + + yield BrowserTestUtils.waitForEvent(dialog, "load"); + Services.logins.setLoginSavingEnabled(LOGIN_HOST, true); + yield promiseChanged; + is(countDisabledHosts(dialog), 0, "Verify disabled host removed"); + yield BrowserTestUtils.closeWindow(dialog); +}); 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 000000000..c6d9ce50a --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_formless_submit_chrome.js @@ -0,0 +1,126 @@ +/* + * Test that browser chrome UI interactions don't trigger a capture doorhanger. + */ + +"use strict"; + +function* fillTestPage(aBrowser) { + yield ContentTask.spawn(aBrowser, null, function*() { + content.document.getElementById("form-basic-username").value = "my_username"; + content.document.getElementById("form-basic-password").value = "my_password"; + }); + info("fields filled"); +} + +function* withTestPage(aTaskFn) { + return BrowserTestUtils.withNewTab({ + gBrowser, + url: "https://example.com" + DIRECTORY_PATH + "formless_basic.html", + }, function*(aBrowser) { + info("tab opened"); + yield fillTestPage(aBrowser); + yield* aTaskFn(aBrowser); + + // Give a chance for the doorhanger to appear + yield new Promise(resolve => SimpleTest.executeSoon(resolve)); + ok(!getCaptureDoorhanger("any"), "No doorhanger should be present"); + }); +} + +add_task(function* setup() { + yield SimpleTest.promiseFocus(window); +}); + +add_task(function* test_urlbar_new_URL() { + yield withTestPage(function*(aBrowser) { + gURLBar.value = ""; + let focusPromise = BrowserTestUtils.waitForEvent(gURLBar, "focus"); + gURLBar.focus(); + yield focusPromise; + info("focused"); + EventUtils.sendString("http://mochi.test:8888/"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield BrowserTestUtils.browserLoaded(aBrowser, false, "http://mochi.test:8888/"); + }); +}); + +add_task(function* test_urlbar_fragment_enter() { + yield withTestPage(function*(aBrowser) { + gURLBar.focus(); + EventUtils.synthesizeKey("VK_RIGHT", {}); + EventUtils.sendString("#fragment"); + EventUtils.synthesizeKey("VK_RETURN", {}); + }); +}); + +add_task(function* test_backButton_forwardButton() { + yield withTestPage(function*(aBrowser) { + // Load a new page in the tab so we can test going back + aBrowser.loadURI("https://example.com" + DIRECTORY_PATH + "formless_basic.html?second"); + yield BrowserTestUtils.browserLoaded(aBrowser, false, + "https://example.com" + DIRECTORY_PATH + + "formless_basic.html?second"); + yield fillTestPage(aBrowser); + + let forwardButton = document.getElementById("forward-button"); + // We need to wait for the forward button transition to complete before we + // can click it, so we hook up a listener to wait for it to be ready. + let forwardTransitionPromise = BrowserTestUtils.waitForEvent(forwardButton, "transitionend"); + + let backPromise = BrowserTestUtils.browserStopped(aBrowser); + EventUtils.synthesizeMouseAtCenter(document.getElementById("back-button"), {}); + yield backPromise; + + // Give a chance for the doorhanger to appear + yield new Promise(resolve => SimpleTest.executeSoon(resolve)); + ok(!getCaptureDoorhanger("any"), "No doorhanger should be present"); + + // Now go forward again after filling + yield fillTestPage(aBrowser); + + yield forwardTransitionPromise; + info("transition done"); + yield BrowserTestUtils.waitForCondition(() => { + return forwardButton.disabled == false; + }); + let forwardPromise = BrowserTestUtils.browserStopped(aBrowser); + info("click the forward button"); + EventUtils.synthesizeMouseAtCenter(forwardButton, {}); + yield forwardPromise; + }); +}); + + +add_task(function* test_reloadButton() { + yield withTestPage(function*(aBrowser) { + let reloadButton = document.getElementById("urlbar-reload-button"); + let loadPromise = BrowserTestUtils.browserLoaded(aBrowser, false, + "https://example.com" + DIRECTORY_PATH + + "formless_basic.html"); + + yield BrowserTestUtils.waitForCondition(() => { + return reloadButton.disabled == false; + }); + EventUtils.synthesizeMouseAtCenter(reloadButton, {}); + yield loadPromise; + }); +}); + +add_task(function* test_back_keyboard_shortcut() { + if (Services.prefs.getIntPref("browser.backspace_action") != 0) { + ok(true, "Skipped testing backspace to go back since it's disabled"); + return; + } + yield withTestPage(function*(aBrowser) { + // Load a new page in the tab so we can test going back + aBrowser.loadURI("https://example.com" + DIRECTORY_PATH + "formless_basic.html?second"); + yield BrowserTestUtils.browserLoaded(aBrowser, false, + "https://example.com" + DIRECTORY_PATH + + "formless_basic.html?second"); + yield fillTestPage(aBrowser); + + let backPromise = BrowserTestUtils.browserStopped(aBrowser); + EventUtils.synthesizeKey("VK_BACK_SPACE", {}); + yield backPromise; + }); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms.js b/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms.js new file mode 100644 index 000000000..039312b7d --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://gre/modules/LoginManagerParent.jsm", this); + +const testUrlPath = + "://example.com/browser/toolkit/components/passwordmgr/test/browser/"; + +/** + * Waits for the given number of occurrences of InsecureLoginFormsStateChange + * on the given browser element. + */ +function waitForInsecureLoginFormsStateChange(browser, count) { + return BrowserTestUtils.waitForEvent(browser, "InsecureLoginFormsStateChange", + false, () => --count == 0); +} + +/** + * Checks that hasInsecureLoginForms is true for a simple HTTP page and false + * for a simple HTTPS page. + */ +add_task(function* test_simple() { + for (let scheme of ["http", "https"]) { + let tab = gBrowser.addTab(scheme + testUrlPath + "form_basic.html"); + let browser = tab.linkedBrowser; + yield Promise.all([ + BrowserTestUtils.switchTab(gBrowser, tab), + BrowserTestUtils.browserLoaded(browser), + // One event is triggered by pageshow and one by DOMFormHasPassword. + waitForInsecureLoginFormsStateChange(browser, 2), + ]); + + Assert.equal(LoginManagerParent.hasInsecureLoginForms(browser), + scheme == "http"); + + gBrowser.removeTab(tab); + } +}); + +/** + * Checks that hasInsecureLoginForms is true if a password field is present in + * an HTTP page loaded as a subframe of a top-level HTTPS page, when mixed + * active content blocking is disabled. + * + * When the subframe is navigated to an HTTPS page, hasInsecureLoginForms should + * be set to false. + * + * Moving back in history should set hasInsecureLoginForms to true again. + */ +add_task(function* test_subframe_navigation() { + yield new Promise(resolve => SpecialPowers.pushPrefEnv({ + "set": [["security.mixed_content.block_active_content", false]], + }, resolve)); + + // Load the page with the subframe in a new tab. + let tab = gBrowser.addTab("https" + testUrlPath + "insecure_test.html"); + let browser = tab.linkedBrowser; + yield Promise.all([ + BrowserTestUtils.switchTab(gBrowser, tab), + BrowserTestUtils.browserLoaded(browser), + // Two events are triggered by pageshow and one by DOMFormHasPassword. + waitForInsecureLoginFormsStateChange(browser, 3), + ]); + + Assert.ok(LoginManagerParent.hasInsecureLoginForms(browser)); + + // Navigate the subframe to a secure page. + let promiseSubframeReady = Promise.all([ + BrowserTestUtils.browserLoaded(browser, true), + // One event is triggered by pageshow and one by DOMFormHasPassword. + waitForInsecureLoginFormsStateChange(browser, 2), + ]); + yield ContentTask.spawn(browser, null, function* () { + content.document.getElementById("test-iframe") + .contentDocument.getElementById("test-link").click(); + }); + yield promiseSubframeReady; + + Assert.ok(!LoginManagerParent.hasInsecureLoginForms(browser)); + + // Navigate back to the insecure page. We only have to wait for the + // InsecureLoginFormsStateChange event that is triggered by pageshow. + let promise = waitForInsecureLoginFormsStateChange(browser, 1); + yield ContentTask.spawn(browser, null, function* () { + content.document.getElementById("test-iframe") + .contentWindow.history.back(); + }); + yield promise; + + Assert.ok(LoginManagerParent.hasInsecureLoginForms(browser)); + + gBrowser.removeTab(tab); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms_streamConverter.js b/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms_streamConverter.js new file mode 100644 index 000000000..2dbffb9cc --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_hasInsecureLoginForms_streamConverter.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://gre/modules/LoginManagerParent.jsm", this); + +function* registerConverter() { + Cu.import("resource://gre/modules/Services.jsm", this); + Cu.import("resource://gre/modules/NetUtil.jsm", this); + + /** + * Converts the "test/content" MIME type, served by the test over HTTP, to an + * HTML viewer page containing the "form_basic.html" code. The viewer is + * served from a "resource:" URI while keeping the "resource:" principal. + */ + function TestStreamConverter() {} + + TestStreamConverter.prototype = { + classID: Components.ID("{5f01d6ef-c090-45a4-b3e5-940d64713eb7}"), + contractID: "@mozilla.org/streamconv;1?from=test/content&to=*/*", + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIRequestObserver, + Ci.nsIStreamListener, + Ci.nsIStreamConverter, + ]), + + // nsIStreamConverter + convert() {}, + + // nsIStreamConverter + asyncConvertData(aFromType, aToType, aListener, aCtxt) { + this.listener = aListener; + }, + + // nsIRequestObserver + onStartRequest(aRequest, aContext) { + let channel = NetUtil.newChannel({ + uri: "resource://testing-common/form_basic.html", + loadUsingSystemPrincipal: true, + }); + channel.originalURI = aRequest.QueryInterface(Ci.nsIChannel).URI; + channel.loadGroup = aRequest.loadGroup; + channel.owner = Services.scriptSecurityManager + .createCodebasePrincipal(channel.URI, {}); + // In this test, we pass the new channel to the listener but don't fire a + // redirect notification, even if it would be required. This keeps the + // test code simpler and doesn't impact the principal check we're testing. + channel.asyncOpen2(this.listener); + }, + + // nsIRequestObserver + onStopRequest() {}, + + // nsIStreamListener + onDataAvailable() {}, + }; + + let factory = XPCOMUtils._getFactory(TestStreamConverter); + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory(TestStreamConverter.prototype.classID, "", + TestStreamConverter.prototype.contractID, factory); + this.cleanupFunction = function () { + registrar.unregisterFactory(TestStreamConverter.prototype.classID, factory); + }; +} + +/** + * Waits for the given number of occurrences of InsecureLoginFormsStateChange + * on the given browser element. + */ +function waitForInsecureLoginFormsStateChange(browser, count) { + return BrowserTestUtils.waitForEvent(browser, "InsecureLoginFormsStateChange", + false, () => --count == 0); +} + +/** + * Checks that hasInsecureLoginForms is false for a viewer served internally + * using a "resource:" URI. + */ +add_task(function* test_streamConverter() { + let originalBrowser = gBrowser.selectedTab.linkedBrowser; + + yield ContentTask.spawn(originalBrowser, null, registerConverter); + + let tab = gBrowser.addTab("http://example.com/browser/toolkit/components/" + + "passwordmgr/test/browser/streamConverter_content.sjs", + { relatedBrowser: originalBrowser.linkedBrowser }); + let browser = tab.linkedBrowser; + yield Promise.all([ + BrowserTestUtils.switchTab(gBrowser, tab), + BrowserTestUtils.browserLoaded(browser), + // One event is triggered by pageshow and one by DOMFormHasPassword. + waitForInsecureLoginFormsStateChange(browser, 2), + ]); + + Assert.ok(!LoginManagerParent.hasInsecureLoginForms(browser)); + + yield BrowserTestUtils.removeTab(tab); + + yield ContentTask.spawn(originalBrowser, null, function* () { + this.cleanupFunction(); + }); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_http_autofill.js b/toolkit/components/passwordmgr/test/browser/browser_http_autofill.js new file mode 100644 index 000000000..beb928a34 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_http_autofill.js @@ -0,0 +1,78 @@ +const TEST_URL_PATH = "://example.org/browser/toolkit/components/passwordmgr/test/browser/"; + +add_task(function* setup() { + let login = LoginTestUtils.testData.formLogin({ + hostname: "http://example.org", + formSubmitURL: "http://example.org", + username: "username", + password: "password", + }); + Services.logins.addLogin(login); + login = LoginTestUtils.testData.formLogin({ + hostname: "http://example.org", + formSubmitURL: "http://another.domain", + username: "username", + password: "password", + }); + Services.logins.addLogin(login); + yield SpecialPowers.pushPrefEnv({ "set": [["signon.autofillForms.http", false]] }); +}); + +add_task(function* test_http_autofill() { + for (let scheme of ["http", "https"]) { + let tab = yield BrowserTestUtils + .openNewForegroundTab(gBrowser, `${scheme}${TEST_URL_PATH}form_basic.html`); + + let [username, password] = yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + let doc = content.document; + let contentUsername = doc.getElementById("form-basic-username").value; + let contentPassword = doc.getElementById("form-basic-password").value; + return [contentUsername, contentPassword]; + }); + + is(username, scheme == "http" ? "" : "username", "Username filled correctly"); + is(password, scheme == "http" ? "" : "password", "Password filled correctly"); + + gBrowser.removeTab(tab); + } +}); + +add_task(function* test_iframe_in_http_autofill() { + for (let scheme of ["http", "https"]) { + let tab = yield BrowserTestUtils + .openNewForegroundTab(gBrowser, `${scheme}${TEST_URL_PATH}form_basic_iframe.html`); + + let [username, password] = yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + let doc = content.document; + let iframe = doc.getElementById("test-iframe"); + let contentUsername = iframe.contentWindow.document.getElementById("form-basic-username").value; + let contentPassword = iframe.contentWindow.document.getElementById("form-basic-password").value; + return [contentUsername, contentPassword]; + }); + + is(username, scheme == "http" ? "" : "username", "Username filled correctly"); + is(password, scheme == "http" ? "" : "password", "Password filled correctly"); + + gBrowser.removeTab(tab); + } +}); + +add_task(function* test_http_action_autofill() { + for (let type of ["insecure", "secure"]) { + let tab = yield BrowserTestUtils + .openNewForegroundTab(gBrowser, `https${TEST_URL_PATH}form_cross_origin_${type}_action.html`); + + let [username, password] = yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + let doc = content.document; + let contentUsername = doc.getElementById("form-basic-username").value; + let contentPassword = doc.getElementById("form-basic-password").value; + return [contentUsername, contentPassword]; + }); + + is(username, type == "insecure" ? "" : "username", "Username filled correctly"); + is(password, type == "insecure" ? "" : "password", "Password filled correctly"); + + gBrowser.removeTab(tab); + } +}); + 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 000000000..f16ae1b98 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_insecurePasswordConsoleWarning.js @@ -0,0 +1,94 @@ +"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(function* testInsecurePasswordWarning() { + let warningPatternHandler; + + function messageHandler(msgObj) { + function findWarningPattern(msg) { + return WARNING_PATTERN.find(patternPair => { + return msg.indexOf(patternPair.msg) !== -1; + }); + } + + 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. + 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) { + ok(warning, "Handling a warning pattern"); + let fullMessage = `[${warning.msg} {file: "${testURL}" line: 0 column: 0 source: "0"}]`; + is(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(); + } + }; + }); + + yield 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_master_password_autocomplete.js b/toolkit/components/passwordmgr/test/browser/browser_master_password_autocomplete.js new file mode 100644 index 000000000..f3bc62b0a --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_master_password_autocomplete.js @@ -0,0 +1,59 @@ +const HOST = "https://example.com"; +const URL = HOST + "/browser/toolkit/components/passwordmgr/test/browser/form_basic.html"; +const TIMEOUT_PREF = "signon.masterPasswordReprompt.timeout_ms"; + +// Waits for the master password prompt and cancels it. +function waitForDialog() { + let dialogShown = TestUtils.topicObserved("common-dialog-loaded"); + return dialogShown.then(function([subject]) { + let dialog = subject.Dialog; + is(dialog.args.title, "Password Required"); + dialog.ui.button1.click(); + }); +} + +// Test that autocomplete does not trigger a master password prompt +// for a certain time after it was cancelled. +add_task(function* test_mpAutocompleteTimeout() { + let login = LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: "username", + password: "password", + }); + Services.logins.addLogin(login); + LoginTestUtils.masterPassword.enable(); + + registerCleanupFunction(function() { + LoginTestUtils.masterPassword.disable(); + Services.logins.removeAllLogins(); + }); + + // Set master password prompt timeout to 3s. + // If this test goes intermittent, you likely have to increase this value. + yield SpecialPowers.pushPrefEnv({set: [[TIMEOUT_PREF, 3000]]}); + + // Wait for initial master password dialog after opening the tab. + let dialogShown = waitForDialog(); + + yield BrowserTestUtils.withNewTab(URL, function*(browser) { + yield dialogShown; + + yield ContentTask.spawn(browser, null, 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). + yield new Promise((c) => setTimeout(c, 4000)); + + dialogShown = waitForDialog(); + yield ContentTask.spawn(browser, null, function*() { + // Re-focus the password field to trigger autocompletion. + content.document.getElementById("form-basic-username").focus(); + content.document.getElementById("form-basic-password").focus(); + }); + yield dialogShown; + }); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_notifications.js b/toolkit/components/passwordmgr/test/browser/browser_notifications.js new file mode 100644 index 000000000..4fb012f14 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_notifications.js @@ -0,0 +1,81 @@ +/** + * Test that the doorhanger notification for password saving is populated with + * the correct values in various password capture cases. + */ +add_task(function* test_save_change() { + let testCases = [{ + username: "username", + password: "password", + }, { + username: "", + password: "password", + }, { + username: "username", + oldPassword: "password", + password: "newPassword", + }, { + username: "", + oldPassword: "password", + password: "newPassword", + }]; + + for (let { username, oldPassword, password } of testCases) { + // Add a login for the origin of the form if testing a change notification. + if (oldPassword) { + Services.logins.addLogin(LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username, + password: oldPassword, + })); + } + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "https://example.com/browser/toolkit/components/" + + "passwordmgr/test/browser/form_basic.html", + }, 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. + let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel, + "popupshown", + (event) => event.target == PopupNotifications.panel); + yield ContentTask.spawn(browser, [username, password], + function* ([contentUsername, contentPassword]) { + let doc = content.document; + doc.getElementById("form-basic-username").value = contentUsername; + doc.getElementById("form-basic-password").value = contentPassword; + doc.getElementById("form-basic").submit(); + }); + yield promiseShown; + let notificationElement = PopupNotifications.panel.childNodes[0]; + // Style flush to make sure binding is attached + notificationElement.querySelector("#password-notification-password").clientTop; + + // Check the actual content of the popup notification. + Assert.equal(notificationElement.querySelector("#password-notification-username") + .value, username); + Assert.equal(notificationElement.querySelector("#password-notification-password") + .value, password); + + // 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 = oldPassword ? "modifyLogin" : "addLogin"; + let promiseLogin = TestUtils.topicObserved("passwordmgr-storage-changed", + (_, data) => data == expectedNotification); + notificationElement.button.doCommand(); + let [result] = yield promiseLogin; + + // Check that the values in the database match the expected values. + let login = oldPassword ? result.QueryInterface(Ci.nsIArray) + .queryElementAt(1, Ci.nsILoginInfo) + : result.QueryInterface(Ci.nsILoginInfo); + Assert.equal(login.username, username); + Assert.equal(login.password, password); + }); + + // Clean up the database before the next test case is executed. + Services.logins.removeAllLogins(); + } +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_notifications_2.js b/toolkit/components/passwordmgr/test/browser/browser_notifications_2.js new file mode 100644 index 000000000..48c73b0e6 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_notifications_2.js @@ -0,0 +1,125 @@ +add_task(function* setup() { + yield 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(function* test_empty_password() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "https://example.com/browser/toolkit/components/" + + "passwordmgr/test/browser/form_basic.html", + }, 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. + let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel, + "popupshown", + (event) => event.target == PopupNotifications.panel); + yield ContentTask.spawn(browser, null, + function* () { + let doc = content.document; + doc.getElementById("form-basic-username").value = "username"; + doc.getElementById("form-basic-password").value = "p"; + doc.getElementById("form-basic").submit(); + }); + yield promiseShown; + + let notificationElement = PopupNotifications.panel.childNodes[0]; + let passwordTextbox = notificationElement.querySelector("#password-notification-password"); + let toggleCheckbox = notificationElement.querySelector("#password-notification-visibilityToggle"); + + // Synthesize input to empty the field + passwordTextbox.focus(); + yield EventUtils.synthesizeKey("VK_RIGHT", {}); + yield EventUtils.synthesizeKey("VK_BACK_SPACE", {}); + + let mainActionButton = document.getAnonymousElementByAttribute(notificationElement.button, "anonid", "button"); + Assert.ok(mainActionButton.disabled, "Main action button is disabled"); + + // Makes sure submiting an empty password throws an error + Assert.throws(notificationElement.button.doCommand(), + "Can't add a login with a null or empty password.", + "Should fail for an empty password"); + }); +}); + +/** + * Test that the doorhanger password field shows plain or * text + * when the checkbox is checked. + */ +add_task(function* test_toggle_password() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "https://example.com/browser/toolkit/components/" + + "passwordmgr/test/browser/form_basic.html", + }, 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. + let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel, + "popupshown", + (event) => event.target == PopupNotifications.panel); + yield ContentTask.spawn(browser, null, + function* () { + let doc = content.document; + doc.getElementById("form-basic-username").value = "username"; + doc.getElementById("form-basic-password").value = "p"; + doc.getElementById("form-basic").submit(); + }); + yield promiseShown; + + let notificationElement = PopupNotifications.panel.childNodes[0]; + let passwordTextbox = notificationElement.querySelector("#password-notification-password"); + let toggleCheckbox = notificationElement.querySelector("#password-notification-visibilityToggle"); + + yield EventUtils.synthesizeMouseAtCenter(toggleCheckbox, {}); + Assert.ok(toggleCheckbox.checked); + Assert.equal(passwordTextbox.type, "", "Password textbox changed to plain text"); + + yield EventUtils.synthesizeMouseAtCenter(toggleCheckbox, {}); + Assert.ok(!toggleCheckbox.checked); + Assert.equal(passwordTextbox.type, "password", "Password textbox changed to * text"); + }); +}); + +/** + * Test that the doorhanger password toggle checkbox is disabled + * when the master password is set. + */ +add_task(function* test_checkbox_disabled_if_has_master_password() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "https://example.com/browser/toolkit/components/" + + "passwordmgr/test/browser/form_basic.html", + }, 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. + let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel, + "popupshown", + (event) => event.target == PopupNotifications.panel); + + LoginTestUtils.masterPassword.enable(); + + yield ContentTask.spawn(browser, null, function* () { + let doc = content.document; + doc.getElementById("form-basic-username").value = "username"; + doc.getElementById("form-basic-password").value = "p"; + doc.getElementById("form-basic").submit(); + }); + yield promiseShown; + + let notificationElement = PopupNotifications.panel.childNodes[0]; + let passwordTextbox = notificationElement.querySelector("#password-notification-password"); + let toggleCheckbox = notificationElement.querySelector("#password-notification-visibilityToggle"); + + Assert.equal(passwordTextbox.type, "password", "Password textbox should show * text"); + Assert.ok(toggleCheckbox.getAttribute("hidden"), "checkbox is hidden when master password is set"); + }); + + LoginTestUtils.masterPassword.disable(); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_notifications_password.js b/toolkit/components/passwordmgr/test/browser/browser_notifications_password.js new file mode 100644 index 000000000..8ac49dac5 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_notifications_password.js @@ -0,0 +1,145 @@ +/** + * 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(function* test_edit_password() { + let testCases = [{ + usernameInPage: "username", + passwordInPage: "password", + passwordChangedTo: "newPassword", + timesUsed: 1, + }, { + usernameInPage: "username", + usernameInPageExists: true, + passwordInPage: "password", + passwordInStorage: "oldPassword", + passwordChangedTo: "newPassword", + timesUsed: 2, + }, { + usernameInPage: "username", + usernameChangedTo: "newUsername", + usernameChangedToExists: true, + passwordInPage: "password", + passwordChangedTo: "newPassword", + timesUsed: 2, + }, { + usernameInPage: "username", + usernameChangedTo: "newUsername", + usernameChangedToExists: true, + passwordInPage: "password", + passwordChangedTo: "password", + timesUsed: 2, + checkPasswordNotUpdated: true, + }, { + usernameInPage: "newUsername", + usernameChangedTo: "", + usernameChangedToExists: true, + passwordInPage: "password", + passwordChangedTo: "newPassword", + timesUsed: 2, + }]; + + for (let testCase of testCases) { + info("Test case: " + JSON.stringify(testCase)); + + // Create the pre-existing logins when needed. + if (testCase.usernameInPageExists) { + Services.logins.addLogin(LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: testCase.usernameInPage, + password: testCase.passwordInStorage, + })); + } + + if (testCase.usernameChangedToExists) { + Services.logins.addLogin(LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: testCase.usernameChangedTo, + password: testCase.passwordChangedTo, + })); + } + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "https://example.com/browser/toolkit/components/" + + "passwordmgr/test/browser/form_basic.html", + }, 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. + let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel, + "popupshown", + (event) => event.target == PopupNotifications.panel); + yield ContentTask.spawn(browser, testCase, + function* (contentTestCase) { + let doc = content.document; + doc.getElementById("form-basic-username").value = contentTestCase.usernameInPage; + doc.getElementById("form-basic-password").value = contentTestCase.passwordInPage; + doc.getElementById("form-basic").submit(); + }); + yield promiseShown; + let notificationElement = PopupNotifications.panel.childNodes[0]; + // Style flush to make sure binding is attached + notificationElement.querySelector("#password-notification-password").clientTop; + + // Modify the username in the dialog if requested. + if (testCase.usernameChangedTo) { + notificationElement.querySelector("#password-notification-username") + .value = testCase.usernameChangedTo; + } + + // Modify the password in the dialog if requested. + if (testCase.passwordChangedTo) { + notificationElement.querySelector("#password-notification-password") + .value = 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); + notificationElement.button.doCommand(); + let [result] = yield 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 || + testCase.usernameInPage); + Assert.equal(login.password, testCase.passwordChangedTo || + testCase.passwordInPage); + + let meta = login.QueryInterface(Ci.nsILoginMetaInfo); + Assert.equal(meta.timesUsed, testCase.timesUsed); + + // Check that the password was not updated if the user is empty + if (testCase.checkPasswordNotUpdated) { + Assert.ok(meta.timeLastUsed > meta.timeCreated); + Assert.ok(meta.timeCreated == meta.timePasswordChanged); + } + }); + + // Clean up the database before the next test case is executed. + Services.logins.removeAllLogins(); + } +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_notifications_username.js b/toolkit/components/passwordmgr/test/browser/browser_notifications_username.js new file mode 100644 index 000000000..2c9ea2607 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_notifications_username.js @@ -0,0 +1,119 @@ +/** + * 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(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)); + + // Create the pre-existing logins when needed. + if (testCase.usernameInPageExists) { + Services.logins.addLogin(LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: testCase.usernameInPage, + password: "old password", + })); + } + + if (testCase.usernameChangedToExists) { + Services.logins.addLogin(LoginTestUtils.testData.formLogin({ + hostname: "https://example.com", + formSubmitURL: "https://example.com", + username: testCase.usernameChangedTo, + password: "old password", + })); + } + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "https://example.com/browser/toolkit/components/" + + "passwordmgr/test/browser/form_basic.html", + }, 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. + let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel, + "popupshown", + (event) => event.target == PopupNotifications.panel); + yield ContentTask.spawn(browser, testCase.usernameInPage, + function* (usernameInPage) { + let doc = content.document; + doc.getElementById("form-basic-username").value = usernameInPage; + doc.getElementById("form-basic-password").value = "password"; + doc.getElementById("form-basic").submit(); + }); + yield promiseShown; + let notificationElement = PopupNotifications.panel.childNodes[0]; + // Style flush to make sure binding is attached + notificationElement.querySelector("#password-notification-password").clientTop; + + // Modify the username in the dialog if requested. + if (testCase.usernameChangedTo) { + notificationElement.querySelector("#password-notification-username") + .value = 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 + ? 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); + notificationElement.button.doCommand(); + let [result] = yield 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 || + testCase.usernameInPage); + Assert.equal(login.password, "password"); + }); + + // Clean up the database before the next test case is executed. + Services.logins.removeAllLogins(); + } +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.js new file mode 100644 index 000000000..ece2b731f --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.js @@ -0,0 +1,100 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + Services.logins.removeAllLogins(); + + // Add some initial logins + let urls = [ + "http://example.com/", + "http://mozilla.org/", + "http://spreadfirefox.com/", + "https://support.mozilla.org/", + "http://hg.mozilla.org/" + ]; + let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); + let logins = [ + new nsLoginInfo(urls[0], urls[0], null, "", "o hai", "u1", "p1"), + new nsLoginInfo(urls[1], urls[1], null, "ehsan", "coded", "u2", "p2"), + new nsLoginInfo(urls[2], urls[2], null, "this", "awesome", "u3", "p3"), + new nsLoginInfo(urls[3], urls[3], null, "array of", "logins", "u4", "p4"), + new nsLoginInfo(urls[4], urls[4], null, "then", "i wrote the test", "u5", "p5") + ]; + logins.forEach(login => Services.logins.addLogin(login)); + + // Open the password manager dialog + const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul"; + let pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", ""); + SimpleTest.waitForFocus(doTest, pwmgrdlg); + + // Test if "Copy Username" and "Copy Password" works + function doTest() { + let doc = pwmgrdlg.document; + let selection = doc.getElementById("signonsTree").view.selection; + let menuitem = doc.getElementById("context-copyusername"); + + function copyField() { + info("Select all"); + selection.selectAll(); + assertMenuitemEnabled("copyusername", false); + assertMenuitemEnabled("editusername", false); + assertMenuitemEnabled("copypassword", false); + assertMenuitemEnabled("editpassword", false); + + info("Select the first row (with an empty username)"); + selection.select(0); + assertMenuitemEnabled("copyusername", false, "empty username"); + assertMenuitemEnabled("editusername", true); + assertMenuitemEnabled("copypassword", true); + assertMenuitemEnabled("editpassword", false, "password column hidden"); + + info("Clear the selection"); + selection.clearSelection(); + assertMenuitemEnabled("copyusername", false); + assertMenuitemEnabled("editusername", false); + assertMenuitemEnabled("copypassword", false); + assertMenuitemEnabled("editpassword", false); + + info("Select the third row and making the password column visible"); + selection.select(2); + doc.getElementById("passwordCol").hidden = false; + assertMenuitemEnabled("copyusername", true); + assertMenuitemEnabled("editusername", true); + assertMenuitemEnabled("copypassword", true); + assertMenuitemEnabled("editpassword", true, "password column visible"); + menuitem.doCommand(); + } + + function assertMenuitemEnabled(idSuffix, expected, reason = "") { + doc.defaultView.UpdateContextMenu(); + let actual = !doc.getElementById("context-" + idSuffix).getAttribute("disabled"); + is(actual, expected, idSuffix + " should be " + (expected ? "enabled" : "disabled") + + (reason ? ": " + reason : "")); + } + + function cleanUp() { + Services.ww.registerNotification(function (aSubject, aTopic, aData) { + Services.ww.unregisterNotification(arguments.callee); + Services.logins.removeAllLogins(); + doc.getElementById("passwordCol").hidden = true; + finish(); + }); + pwmgrdlg.close(); + } + + function testPassword() { + info("Testing Copy Password"); + waitForClipboard("coded", function copyPassword() { + menuitem = doc.getElementById("context-copypassword"); + menuitem.doCommand(); + }, cleanUp, cleanUp); + } + + info("Testing Copy Username"); + waitForClipboard("ehsan", copyField, testPassword, testPassword); + } +} diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js new file mode 100644 index 000000000..2b2e42273 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js @@ -0,0 +1,126 @@ +const { ContentTaskUtils } = Cu.import("resource://testing-common/ContentTaskUtils.jsm", {}); +const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul"; + +var doc; +var pwmgr; +var pwmgrdlg; +var signonsTree; + +function addLogin(site, username, password) { + let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); + let login = new nsLoginInfo(site, site, null, username, password, "u", "p"); + Services.logins.addLogin(login); +} + +function getUsername(row) { + return signonsTree.view.getCellText(row, signonsTree.columns.getNamedColumn("userCol")); +} + +function getPassword(row) { + return signonsTree.view.getCellText(row, signonsTree.columns.getNamedColumn("passwordCol")); +} + +function synthesizeDblClickOnCell(aTree, column, row) { + let tbo = aTree.treeBoxObject; + let rect = tbo.getCoordsForCellItem(row, aTree.columns[column], "text"); + let x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + // Simulate the double click. + EventUtils.synthesizeMouse(aTree.body, x, y, { clickCount: 2 }, + aTree.ownerDocument.defaultView); +} + +function* togglePasswords() { + pwmgrdlg.document.querySelector("#togglePasswords").doCommand(); + yield new Promise(resolve => waitForFocus(resolve, pwmgrdlg)); +} + +function* editUsernamePromises(site, oldUsername, newUsername) { + is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login found"); + let login = Services.logins.findLogins({}, site, "", "")[0]; + is(login.username, oldUsername, "Correct username saved"); + is(getUsername(0), oldUsername, "Correct username shown"); + synthesizeDblClickOnCell(signonsTree, 1, 0); + yield ContentTaskUtils.waitForCondition(() => signonsTree.getAttribute("editing"), + "Waiting for editing"); + + EventUtils.sendString(newUsername, pwmgrdlg); + let signonsIntro = doc.querySelector("#signonsIntro"); + EventUtils.sendMouseEvent({type: "click"}, signonsIntro, pwmgrdlg); + yield ContentTaskUtils.waitForCondition(() => !signonsTree.getAttribute("editing"), + "Waiting for editing to stop"); + + is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login replaced"); + login = Services.logins.findLogins({}, site, "", "")[0]; + is(login.username, newUsername, "Correct username updated"); + is(getUsername(0), newUsername, "Correct username shown after the update"); +} + +function* editPasswordPromises(site, oldPassword, newPassword) { + is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login found"); + let login = Services.logins.findLogins({}, site, "", "")[0]; + is(login.password, oldPassword, "Correct password saved"); + is(getPassword(0), oldPassword, "Correct password shown"); + + synthesizeDblClickOnCell(signonsTree, 2, 0); + yield ContentTaskUtils.waitForCondition(() => signonsTree.getAttribute("editing"), + "Waiting for editing"); + + EventUtils.sendString(newPassword, pwmgrdlg); + let signonsIntro = doc.querySelector("#signonsIntro"); + EventUtils.sendMouseEvent({type: "click"}, signonsIntro, pwmgrdlg); + yield ContentTaskUtils.waitForCondition(() => !signonsTree.getAttribute("editing"), + "Waiting for editing to stop"); + + is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login replaced"); + login = Services.logins.findLogins({}, site, "", "")[0]; + is(login.password, newPassword, "Correct password updated"); + is(getPassword(0), newPassword, "Correct password shown after the update"); +} + +add_task(function* test_setup() { + registerCleanupFunction(function() { + Services.logins.removeAllLogins(); + }); + + Services.logins.removeAllLogins(); + // Open the password manager dialog. + pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", ""); + + Services.ww.registerNotification(function (aSubject, aTopic, aData) { + if (aTopic == "domwindowopened") { + let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget); + SimpleTest.waitForFocus(function() { + EventUtils.sendKey("RETURN", win); + }, win); + } else if (aSubject.location == pwmgrdlg.location && aTopic == "domwindowclosed") { + // Unregister ourself. + Services.ww.unregisterNotification(arguments.callee); + } + }); + + yield new Promise((resolve) => { + SimpleTest.waitForFocus(() => { + doc = pwmgrdlg.document; + signonsTree = doc.querySelector("#signonsTree"); + resolve(); + }, pwmgrdlg); + }); +}); + +add_task(function* test_edit_multiple_logins() { + function* testLoginChange(site, oldUsername, oldPassword, newUsername, newPassword) { + addLogin(site, oldUsername, oldPassword); + yield* editUsernamePromises(site, oldUsername, newUsername); + yield* togglePasswords(); + yield* editPasswordPromises(site, oldPassword, newPassword); + yield* togglePasswords(); + } + + yield* testLoginChange("http://c.tn/", "userC", "passC", "usernameC", "passwordC"); + yield* testLoginChange("http://b.tn/", "userB", "passB", "usernameB", "passwordB"); + yield* testLoginChange("http://a.tn/", "userA", "passA", "usernameA", "passwordA"); + + pwmgrdlg.close(); +}); diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_fields.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_fields.js new file mode 100644 index 000000000..95bcee9ed --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_fields.js @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + let pwmgr = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + pwmgr.removeAllLogins(); + + // add login data + let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); + let login = new nsLoginInfo("http://example.com/", "http://example.com/", null, + "user", "password", "u1", "p1"); + pwmgr.addLogin(login); + + // Open the password manager dialog + const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul"; + let pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", ""); + SimpleTest.waitForFocus(doTest, pwmgrdlg); + + function doTest() { + let doc = pwmgrdlg.document; + + let signonsTree = doc.querySelector("#signonsTree"); + is(signonsTree.view.rowCount, 1, "One entry in the passwords list"); + + is(signonsTree.view.getCellText(0, signonsTree.columns.getNamedColumn("siteCol")), + "http://example.com/", + "Correct website saved"); + + is(signonsTree.view.getCellText(0, signonsTree.columns.getNamedColumn("userCol")), + "user", + "Correct user saved"); + + let timeCreatedCol = doc.getElementById("timeCreatedCol"); + is(timeCreatedCol.getAttribute("hidden"), "true", + "Time created column is not displayed"); + + + let timeLastUsedCol = doc.getElementById("timeLastUsedCol"); + is(timeLastUsedCol.getAttribute("hidden"), "true", + "Last Used column is not displayed"); + + let timePasswordChangedCol = doc.getElementById("timePasswordChangedCol"); + is(timePasswordChangedCol.getAttribute("hidden"), "", + "Last Changed column is displayed"); + + // cleanup + Services.ww.registerNotification(function (aSubject, aTopic, aData) { + if (aSubject.location == pwmgrdlg.location && aTopic == "domwindowclosed") { + // unregister ourself + Services.ww.unregisterNotification(arguments.callee); + + pwmgr.removeAllLogins(); + + finish(); + } + }); + + pwmgrdlg.close(); + } +} diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_observers.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_observers.js new file mode 100644 index 000000000..1dc7076aa --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_observers.js @@ -0,0 +1,129 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + + const LOGIN_HOST = "http://example.com"; + const LOGIN_COUNT = 5; + + let nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); + let pmDialog = window.openDialog( + "chrome://passwordmgr/content/passwordManager.xul", + "Toolkit:PasswordManager", ""); + + let logins = []; + let loginCounter = 0; + let loginOrder = null; + let modifiedLogin; + let testNumber = 0; + let testObserver = { + observe: function (subject, topic, data) { + if (topic == "passwordmgr-dialog-updated") { + switch (testNumber) { + case 1: + case 2: + case 3: + case 4: + case 5: + is(countLogins(), loginCounter, "Verify login added"); + ok(getLoginOrder().startsWith(loginOrder), "Verify login order"); + runNextTest(); + break; + case 6: + is(countLogins(), loginCounter, "Verify login count"); + is(getLoginOrder(), loginOrder, "Verify login order"); + is(getLoginPassword(), "newpassword0", "Verify login modified"); + runNextTest(); + break; + case 7: + is(countLogins(), loginCounter, "Verify login removed"); + ok(loginOrder.endsWith(getLoginOrder()), "Verify login order"); + runNextTest(); + break; + case 8: + is(countLogins(), 0, "Verify all logins removed"); + runNextTest(); + break; + } + } + } + }; + + SimpleTest.waitForFocus(startTest, pmDialog); + + function createLogins() { + let login; + for (let i = 0; i < LOGIN_COUNT; i++) { + login = new nsLoginInfo(LOGIN_HOST + "?n=" + i, LOGIN_HOST + "?n=" + i, + null, "user" + i, "password" + i, "u" + i, "p" + i); + logins.push(login); + } + modifiedLogin = new nsLoginInfo(LOGIN_HOST + "?n=0", LOGIN_HOST + "?n=0", + null, "user0", "newpassword0", "u0", "p0"); + is(logins.length, LOGIN_COUNT, "Verify logins created"); + } + + function countLogins() { + let doc = pmDialog.document; + let signonsTree = doc.getElementById("signonsTree"); + return signonsTree.view.rowCount; + } + + function getLoginOrder() { + let doc = pmDialog.document; + let signonsTree = doc.getElementById("signonsTree"); + let column = signonsTree.columns[0]; // host column + let order = []; + for (let i = 0; i < signonsTree.view.rowCount; i++) { + order.push(signonsTree.view.getCellText(i, column)); + } + return order.join(','); + } + + function getLoginPassword() { + let doc = pmDialog.document; + let loginsTree = doc.getElementById("signonsTree"); + let column = loginsTree.columns[2]; // password column + return loginsTree.view.getCellText(0, column); + } + + function startTest() { + Services.obs.addObserver( + testObserver, "passwordmgr-dialog-updated", false); + is(countLogins(), 0, "Verify starts with 0 logins"); + createLogins(); + runNextTest(); + } + + function runNextTest() { + switch (++testNumber) { + case 1: // add the logins + for (let i = 0; i < logins.length; i++) { + loginCounter++; + loginOrder = getLoginOrder(); + Services.logins.addLogin(logins[i]); + } + break; + case 6: // modify a login + loginOrder = getLoginOrder(); + Services.logins.modifyLogin(logins[0], modifiedLogin); + break; + case 7: // remove a login + loginCounter--; + loginOrder = getLoginOrder(); + Services.logins.removeLogin(modifiedLogin); + break; + case 8: // remove all logins + Services.logins.removeAllLogins(); + break; + case 9: // finish + Services.obs.removeObserver( + testObserver, "passwordmgr-dialog-updated", false); + pmDialog.close(); + finish(); + break; + } + } +} diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js new file mode 100644 index 000000000..83272a9c4 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js @@ -0,0 +1,208 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + let pwmgr = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + pwmgr.removeAllLogins(); + + // Add some initial logins + let urls = [ + "http://example.com/", + "http://example.org/", + "http://mozilla.com/", + "http://mozilla.org/", + "http://spreadfirefox.com/", + "http://planet.mozilla.org/", + "https://developer.mozilla.org/", + "http://hg.mozilla.org/", + "http://dxr.mozilla.org/", + "http://feeds.mozilla.org/", + ]; + let users = [ + "user", + "username", + "ehsan", + "ehsan", + "john", + "what?", + "really?", + "you sure?", + "my user name", + "my username", + ]; + let pwds = [ + "password", + "password", + "mypass", + "mypass", + "smith", + "very secret", + "super secret", + "absolutely", + "mozilla", + "mozilla.com", + ]; + let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); + for (let i = 0; i < 10; i++) + pwmgr.addLogin(new nsLoginInfo(urls[i], urls[i], null, users[i], pwds[i], + "u" + (i + 1), "p" + (i + 1))); + + // Open the password manager dialog + const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul"; + let pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", ""); + SimpleTest.waitForFocus(doTest, pwmgrdlg); + + // the meat of the test + function doTest() { + let doc = pwmgrdlg.document; + let win = doc.defaultView; + let sTree = doc.getElementById("signonsTree"); + let filter = doc.getElementById("filter"); + let siteCol = doc.getElementById("siteCol"); + let userCol = doc.getElementById("userCol"); + let passwordCol = doc.getElementById("passwordCol"); + + let toggleCalls = 0; + function toggleShowPasswords(func) { + let toggleButton = doc.getElementById("togglePasswords"); + let showMode = (toggleCalls++ % 2) == 0; + + // only watch for a confirmation dialog every other time being called + if (showMode) { + Services.ww.registerNotification(function (aSubject, aTopic, aData) { + if (aTopic == "domwindowclosed") + Services.ww.unregisterNotification(arguments.callee); + else if (aTopic == "domwindowopened") { + let targetWin = aSubject.QueryInterface(Ci.nsIDOMEventTarget); + SimpleTest.waitForFocus(function() { + EventUtils.sendKey("RETURN", targetWin); + }, targetWin); + } + }); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + if (aTopic == "passwordmgr-password-toggle-complete") { + Services.obs.removeObserver(arguments.callee, aTopic); + func(); + } + }, "passwordmgr-password-toggle-complete", false); + + EventUtils.synthesizeMouse(toggleButton, 1, 1, {}, win); + } + + function clickCol(col) { + EventUtils.synthesizeMouse(col, 20, 1, {}, win); + setTimeout(runNextTest, 0); + } + + function setFilter(string) { + filter.value = string; + filter.doCommand(); + setTimeout(runNextTest, 0); + } + + function checkSortMarkers(activeCol) { + let isOk = true; + let col = null; + let hasAttr = false; + let treecols = activeCol.parentNode; + for (let i = 0; i < treecols.childNodes.length; i++) { + col = treecols.childNodes[i]; + if (col.nodeName != "treecol") + continue; + hasAttr = col.hasAttribute("sortDirection"); + isOk &= col == activeCol ? hasAttr : !hasAttr; + } + ok(isOk, "Only " + activeCol.id + " has a sort marker"); + } + + function checkSortDirection(col, ascending) { + checkSortMarkers(col); + let direction = ascending ? "ascending" : "descending"; + is(col.getAttribute("sortDirection"), direction, + col.id + ": sort direction is " + direction); + } + + function checkColumnEntries(aCol, expectedValues) { + let actualValues = getColumnEntries(aCol); + is(actualValues.length, expectedValues.length, "Checking length of expected column"); + for (let i = 0; i < expectedValues.length; i++) + is(actualValues[i], expectedValues[i], "Checking column entry #" + i); + } + + function getColumnEntries(aCol) { + let entries = []; + let column = sTree.columns[aCol]; + let numRows = sTree.view.rowCount; + for (let i = 0; i < numRows; i++) + entries.push(sTree.view.getCellText(i, column)); + return entries; + } + + let testCounter = 0; + let expectedValues; + function runNextTest() { + switch (testCounter++) { + case 0: + expectedValues = urls.slice().sort(); + checkColumnEntries(0, expectedValues); + checkSortDirection(siteCol, true); + // Toggle sort direction on Host column + clickCol(siteCol); + break; + case 1: + expectedValues.reverse(); + checkColumnEntries(0, expectedValues); + checkSortDirection(siteCol, false); + // Sort by Username + clickCol(userCol); + break; + case 2: + expectedValues = users.slice().sort(); + checkColumnEntries(1, expectedValues); + checkSortDirection(userCol, true); + // Sort by Password + clickCol(passwordCol); + break; + case 3: + expectedValues = pwds.slice().sort(); + checkColumnEntries(2, expectedValues); + checkSortDirection(passwordCol, true); + // Set filter + setFilter("moz"); + break; + case 4: + expectedValues = [ "absolutely", "mozilla", "mozilla.com", + "mypass", "mypass", "super secret", + "very secret" ]; + checkColumnEntries(2, expectedValues); + checkSortDirection(passwordCol, true); + // Reset filter + setFilter(""); + break; + case 5: + expectedValues = pwds.slice().sort(); + checkColumnEntries(2, expectedValues); + checkSortDirection(passwordCol, true); + // cleanup + Services.ww.registerNotification(function (aSubject, aTopic, aData) { + // unregister ourself + Services.ww.unregisterNotification(arguments.callee); + + pwmgr.removeAllLogins(); + finish(); + }); + pwmgrdlg.close(); + } + } + + // Toggle Show Passwords to display Password column, then start tests + toggleShowPasswords(runNextTest); + } +} diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_switchtab.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_switchtab.js new file mode 100644 index 000000000..bd4f265b5 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_switchtab.js @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const PROMPT_URL = "chrome://global/content/commonDialog.xul"; +var { interfaces: Ci } = Components; + +function test() { + waitForExplicitFinish(); + + let tab = gBrowser.addTab(); + isnot(tab, gBrowser.selectedTab, "New tab shouldn't be selected"); + + let listener = { + onOpenWindow: function(window) { + var domwindow = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + waitForFocus(() => { + is(domwindow.document.location.href, PROMPT_URL, "Should have seen a prompt window"); + is(domwindow.args.promptType, "promptUserAndPass", "Should be an authenticate prompt"); + + is(gBrowser.selectedTab, tab, "Should have selected the new tab"); + + domwindow.document.documentElement.cancelDialog(); + }, domwindow); + }, + + onCloseWindow: function() { + } + }; + + Services.wm.addListener(listener); + registerCleanupFunction(() => { + Services.wm.removeListener(listener); + gBrowser.removeTab(tab); + }); + + tab.linkedBrowser.addEventListener("load", () => { + finish(); + }, true); + tab.linkedBrowser.loadURI("http://example.com/browser/toolkit/components/passwordmgr/test/browser/authenticate.sjs"); +} diff --git a/toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js b/toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js new file mode 100644 index 000000000..57cfa9f83 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js @@ -0,0 +1,192 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + let pwmgr = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + pwmgr.removeAllLogins(); + + // Add some initial logins + let urls = [ + "http://example.com/", + "http://example.org/", + "http://mozilla.com/", + "http://mozilla.org/", + "http://spreadfirefox.com/", + "http://planet.mozilla.org/", + "https://developer.mozilla.org/", + "http://hg.mozilla.org/", + "http://dxr.mozilla.org/", + "http://feeds.mozilla.org/", + ]; + let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, "init"); + let logins = [ + new nsLoginInfo(urls[0], urls[0], null, "user", "password", "u1", "p1"), + new nsLoginInfo(urls[1], urls[1], null, "username", "password", "u2", "p2"), + new nsLoginInfo(urls[2], urls[2], null, "ehsan", "mypass", "u3", "p3"), + new nsLoginInfo(urls[3], urls[3], null, "ehsan", "mypass", "u4", "p4"), + new nsLoginInfo(urls[4], urls[4], null, "john", "smith", "u5", "p5"), + new nsLoginInfo(urls[5], urls[5], null, "what?", "very secret", "u6", "p6"), + new nsLoginInfo(urls[6], urls[6], null, "really?", "super secret", "u7", "p7"), + new nsLoginInfo(urls[7], urls[7], null, "you sure?", "absolutely", "u8", "p8"), + new nsLoginInfo(urls[8], urls[8], null, "my user name", "mozilla", "u9", "p9"), + new nsLoginInfo(urls[9], urls[9], null, "my username", "mozilla.com", "u10", "p10"), + ]; + logins.forEach(login => pwmgr.addLogin(login)); + + // Open the password manager dialog + const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul"; + let pwmgrdlg = window.openDialog(PWMGR_DLG, "Toolkit:PasswordManager", ""); + SimpleTest.waitForFocus(doTest, pwmgrdlg); + + // the meat of the test + function doTest() { + let doc = pwmgrdlg.document; + let win = doc.defaultView; + let filter = doc.getElementById("filter"); + let tree = doc.getElementById("signonsTree"); + let view = tree.view; + + is(filter.value, "", "Filter box should initially be empty"); + is(view.rowCount, 10, "There should be 10 passwords initially"); + + // Prepare a set of tests + // filter: the text entered in the filter search box + // count: the number of logins which should match the respective filter + // count2: the number of logins which should match the respective filter + // if the passwords are being shown as well + // Note: if a test doesn't have count2 set, count is used instead. + let tests = [ + {filter: "pass", count: 0, count2: 4}, + {filter: "", count: 10}, // test clearing the filter + {filter: "moz", count: 7}, + {filter: "mozi", count: 7}, + {filter: "mozil", count: 7}, + {filter: "mozill", count: 7}, + {filter: "mozilla", count: 7}, + {filter: "mozilla.com", count: 1, count2: 2}, + {filter: "user", count: 4}, + {filter: "user ", count: 1}, + {filter: " user", count: 2}, + {filter: "http", count: 10}, + {filter: "https", count: 1}, + {filter: "secret", count: 0, count2: 2}, + {filter: "secret!", count: 0}, + ]; + + let toggleCalls = 0; + function toggleShowPasswords(func) { + let toggleButton = doc.getElementById("togglePasswords"); + let showMode = (toggleCalls++ % 2) == 0; + + // only watch for a confirmation dialog every other time being called + if (showMode) { + Services.ww.registerNotification(function (aSubject, aTopic, aData) { + if (aTopic == "domwindowclosed") + Services.ww.unregisterNotification(arguments.callee); + else if (aTopic == "domwindowopened") { + let targetWin = aSubject.QueryInterface(Ci.nsIDOMEventTarget); + SimpleTest.waitForFocus(function() { + EventUtils.sendKey("RETURN", targetWin); + }, targetWin); + } + }); + } + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + if (aTopic == "passwordmgr-password-toggle-complete") { + Services.obs.removeObserver(arguments.callee, aTopic); + func(); + } + }, "passwordmgr-password-toggle-complete", false); + + EventUtils.synthesizeMouse(toggleButton, 1, 1, {}, win); + } + + function runTests(mode, endFunction) { + let testCounter = 0; + + function setFilter(string) { + filter.value = string; + filter.doCommand(); + } + + function runOneTest(testCase) { + function tester() { + is(view.rowCount, expected, expected + " logins should match '" + testCase.filter + "'"); + } + + let expected; + switch (mode) { + case 1: // without showing passwords + expected = testCase.count; + break; + case 2: // showing passwords + expected = ("count2" in testCase) ? testCase.count2 : testCase.count; + break; + case 3: // toggle + expected = testCase.count; + tester(); + toggleShowPasswords(function () { + expected = ("count2" in testCase) ? testCase.count2 : testCase.count; + tester(); + toggleShowPasswords(proceed); + }); + return; + } + tester(); + proceed(); + } + + function proceed() { + // run the next test if necessary or proceed with the tests + if (testCounter != tests.length) + runNextTest(); + else + endFunction(); + } + + function runNextTest() { + let testCase = tests[testCounter++]; + setFilter(testCase.filter); + setTimeout(runOneTest, 0, testCase); + } + + runNextTest(); + } + + function step1() { + runTests(1, step2); + } + + function step2() { + toggleShowPasswords(function() { + runTests(2, step3); + }); + } + + function step3() { + toggleShowPasswords(function() { + runTests(3, lastStep); + }); + } + + function lastStep() { + // cleanup + Services.ww.registerNotification(function (aSubject, aTopic, aData) { + // unregister ourself + Services.ww.unregisterNotification(arguments.callee); + + pwmgr.removeAllLogins(); + finish(); + }); + pwmgrdlg.close(); + } + + step1(); + } +} 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 000000000..8df89b510 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/browser_username_select_dialog.js @@ -0,0 +1,144 @@ +/* + * 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.xul loaded. + var wm = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + // var enumerator = wm.getEnumerator("navigator:browser"); + var enumerator = wm.getXULWindowEnumerator(null); + + while (enumerator.hasMoreElements()) { + var win = enumerator.getNext(); + var windowDocShell = win.QueryInterface(Ci.nsIXULWindow).docShell; + + var containedDocShells = windowDocShell.getDocShellEnumerator( + Ci.nsIDocShellTreeItem.typeChrome, + Ci.nsIDocShell.ENUMERATE_FORWARDS); + while (containedDocShells.hasMoreElements()) { + // Get the corresponding document for this docshell + var childDocShell = containedDocShells.getNext(); + // We don't want it if it's not done loading. + if (childDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE) + continue; + var childDoc = childDocShell.QueryInterface(Ci.nsIDocShell) + .contentViewer + .DOMDocument; + + if (childDoc.location.href == "chrome://global/content/selectDialog.xul") + return childDoc; + } + } + + return null; +} + +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 login1B = new nsLoginInfo("http://example.com", "http://example.com", null, + "notifyu1B", "notifyp1B", "user", "pass"); + +add_task(function* test_changeUPLoginOnPUpdateForm_accept() { + info("Select an u+p login from multiple logins, on password update form, and accept."); + Services.logins.addLogin(login1); + Services.logins.addLogin(login1B); + + yield testSubmittingLoginForm("subtst_notifications_change_p.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "pass2", "Checking submitted password"); + + yield ContentTaskUtils.waitForCondition(() => { + return getSelectDialogDoc(); + }, "Wait for selection dialog to be accessible."); + + let doc = getSelectDialogDoc(); + let dialog = doc.getElementsByTagName("dialog")[0]; + let listbox = doc.getElementById("list"); + + is(listbox.selectedIndex, 0, "Checking selected index"); + is(listbox.itemCount, 2, "Checking selected length"); + ['notifyu1', 'notifyu1B'].forEach((username, i) => { + is(listbox.getItemAtIndex(i).label, username, "Check username selection on dialog"); + }); + + dialog.acceptDialog(); + + yield ContentTaskUtils.waitForCondition(() => { + return !getSelectDialogDoc(); + }, "Wait for selection dialog to disappear."); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 2, "Should have 2 logins"); + + let login = SpecialPowers.wrap(logins[0]).QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username unchanged"); + is(login.password, "pass2", "Check the password changed"); + is(login.timesUsed, 2, "Check times used"); + + login = SpecialPowers.wrap(logins[1]).QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1B", "Check the username unchanged"); + is(login.password, "notifyp1B", "Check the password unchanged"); + is(login.timesUsed, 1, "Check times used"); + + // cleanup + login1.password = "pass2"; + Services.logins.removeLogin(login1); + login1.password = "notifyp1"; + + Services.logins.removeLogin(login1B); +}); + +add_task(function* test_changeUPLoginOnPUpdateForm_cancel() { + info("Select an u+p login from multiple logins, on password update form, and cancel."); + Services.logins.addLogin(login1); + Services.logins.addLogin(login1B); + + yield testSubmittingLoginForm("subtst_notifications_change_p.html", function*(fieldValues) { + is(fieldValues.username, "null", "Checking submitted username"); + is(fieldValues.password, "pass2", "Checking submitted password"); + + yield ContentTaskUtils.waitForCondition(() => { + return getSelectDialogDoc(); + }, "Wait for selection dialog to be accessible."); + + let doc = getSelectDialogDoc(); + let dialog = doc.getElementsByTagName("dialog")[0]; + let listbox = doc.getElementById("list"); + + is(listbox.selectedIndex, 0, "Checking selected index"); + is(listbox.itemCount, 2, "Checking selected length"); + ['notifyu1', 'notifyu1B'].forEach((username, i) => { + is(listbox.getItemAtIndex(i).label, username, "Check username selection on dialog"); + }); + + dialog.cancelDialog(); + + yield ContentTaskUtils.waitForCondition(() => { + return !getSelectDialogDoc(); + }, "Wait for selection dialog to disappear."); + }); + + let logins = Services.logins.getAllLogins(); + is(logins.length, 2, "Should have 2 logins"); + + let login = SpecialPowers.wrap(logins[0]).QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1", "Check the username unchanged"); + is(login.password, "notifyp1", "Check the password unchanged"); + is(login.timesUsed, 1, "Check times used"); + + login = SpecialPowers.wrap(logins[1]).QueryInterface(Ci.nsILoginMetaInfo); + is(login.username, "notifyu1B", "Check the username unchanged"); + is(login.password, "notifyp1B", "Check the password unchanged"); + is(login.timesUsed, 1, "Check times used"); + + // cleanup + Services.logins.removeLogin(login1); + Services.logins.removeLogin(login1B); +}); 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 000000000..76056e375 --- /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 000000000..df2083a93 --- /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 000000000..616f56947 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/form_basic_iframe.html @@ -0,0 +1,13 @@ +<!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> +</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 000000000..e8aa8b215 --- /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://another.domain/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 000000000..892a9f6f6 --- /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://another.domain/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_same_origin_action.html b/toolkit/components/passwordmgr/test/browser/form_same_origin_action.html new file mode 100644 index 000000000..8f0c9a14e --- /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/formless_basic.html b/toolkit/components/passwordmgr/test/browser/formless_basic.html new file mode 100644 index 000000000..2f4c5de52 --- /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 000000000..926cb6616 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/head.js @@ -0,0 +1,137 @@ +const DIRECTORY_PATH = "/browser/toolkit/components/passwordmgr/test/browser/"; + +Cu.import("resource://testing-common/LoginTestUtils.jsm", this); +Cu.import("resource://testing-common/ContentTaskUtils.jsm", this); + +registerCleanupFunction(function* cleanup_removeAllLoginsAndResetRecipes() { + Services.logins.removeAllLogins(); + + let recipeParent = LoginTestUtils.recipes.getRecipeParent(); + if (!recipeParent) { + // No need to reset the recipes if the recipe module wasn't even loaded. + return; + } + yield recipeParent.then(recipeParentResult => recipeParentResult.reset()); +}); + +/** + * 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 testSubmittingLoginForm(aPageFile, aTaskFn, aOrigin = "http://example.com") { + return BrowserTestUtils.withNewTab({ + gBrowser, + url: aOrigin + DIRECTORY_PATH + aPageFile, + }, function*(browser) { + ok(true, "loaded " + aPageFile); + let fieldValues = yield ContentTask.spawn(browser, undefined, function*() { + yield ContentTaskUtils.waitForCondition(() => { + return content.location.pathname.endsWith("/formsubmit.sjs") && + content.document.readyState == "complete"; + }, "Wait for form submission load (formsubmit.sjs)"); + let username = content.document.getElementById("user").textContent; + let password = content.document.getElementById("pass").textContent; + return { + username, + password, + }; + }); + ok(true, "form submission loaded"); + if (aTaskFn) { + yield* aTaskFn(fieldValues); + } + return fieldValues; + }); +} + +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(); + is(logins.length, 1, "Should only have 1 login"); + ok(logins[0] instanceof Ci.nsILoginMetaInfo, "metainfo QI"); + is(logins[0].timesUsed, 2, "check .timesUsed for existing login submission"); + ok(logins[0].timeCreated < logins[0].timeLastUsed, "timeLastUsed bumped"); + if (justChanged) { + is(logins[0].timeLastUsed, logins[0].timePasswordChanged, "timeLastUsed == timePasswordChanged"); + } else { + is(logins[0].timeCreated, logins[0].timePasswordChanged, "timeChanged not updated"); + } +} + +// Begin popup notification (doorhanger) functions // + +const REMEMBER_BUTTON = 0; +const NEVER_BUTTON = 1; + +const CHANGE_BUTTON = 0; +const DONT_CHANGE_BUTTON = 1; + +/** + * Checks if we have a password capture popup notification + * of the right type and with the right label. + * + * @param {String} aKind The desired `passwordNotificationType` + * @param {Object} [popupNotifications = PopupNotifications] + * @return the found password popup notification. + */ +function getCaptureDoorhanger(aKind, popupNotifications = PopupNotifications) { + ok(true, "Looking for " + aKind + " popup notification"); + let notification = popupNotifications.getNotification("password"); + if (notification) { + is(notification.options.passwordNotificationType, aKind, "Notification type matches."); + if (aKind == "password-change") { + is(notification.mainAction.label, "Update", "Main action label matches update doorhanger."); + } else if (aKind == "password-save") { + is(notification.mainAction.label, "Remember", "Main action label matches save doorhanger."); + } + } + return notification; +} + +/** + * 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) { + ok(true, "Looking for action at index " + aButtonIndex); + + let notifications = aPopup.owner.panel.childNodes; + ok(notifications.length > 0, "at least one notification displayed"); + ok(true, notifications.length + " notification(s)"); + let notification = notifications[0]; + + if (aButtonIndex == 0) { + ok(true, "Triggering main action"); + notification.button.doCommand(); + } else if (aButtonIndex <= aPopup.secondaryActions.length) { + ok(true, "Triggering secondary action " + aButtonIndex); + notification.childNodes[aButtonIndex].doCommand(); + } +} + +/** + * Checks the doorhanger's username and password. + * + * @param {String} username The username. + * @param {String} password The password. + */ +function* checkDoorhangerUsernamePassword(username, password) { + yield BrowserTestUtils.waitForCondition(() => { + return document.getElementById("password-notification-username").value == username; + }, "Wait for nsLoginManagerPrompter writeDataToUI()"); + is(document.getElementById("password-notification-username").value, username, + "Check doorhanger username"); + is(document.getElementById("password-notification-password").value, password, + "Check doorhanger password"); +} + +// End popup notification (doorhanger) functions // 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 000000000..fedea1428 --- /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 000000000..3f01e36a6 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/insecure_test_subframe.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> + <input name="password" type="password"> +</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 000000000..3f64f8993 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/multiple_forms.html @@ -0,0 +1,129 @@ +<!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> + + +<form class="test-form" + description="Username only form"> + <input id='test-username-1' 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-2' type='text' name='uname' value=''> + <input id='test-password-2' 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-3' type='text' name='uname' value='testuser'> + <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 and password"> + <input id='test-username-4' type='text' name='uname' value='testuser'> + <input id='test-password-4' 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-5' type='text' name='uname'> + <input id='test-password-5' type='password' name='pname'> + <input id='test-password2-5' 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-6' type='text' name='uname' value="testuser"> + <input id='test-password-6' type='password' name='pname' value="testpass"> + <input id='test-password2-6' 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-7' type='text' name='uname' value="testuser"> + <input id='test-password-7' 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-8' type='text' name='uname' value=''> + <input id='test-password-8' 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-9' type='text' name='uname' value='' disabled> + <input id='test-password-9' 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-10' type='text' name='uname' value=''> + <input id='test-password-10' 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-11' type='text' name='uname' value='' readonly> + <input id='test-password-11' 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-12' type='text' name='uname' value="testuser"> + <input id='test-username2-12' type='text' name='uname2' value="testuser"> + <input id='test-password-12' 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-13' type='text' name='uname'> + <input id='test-username2-13' type='text' name='uname2' disabled> + <input id='test-password-13' 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-14' type='text' name='uname'> + <input id='test-password-14' type='password' name='pname' expectedFail> +</div> + +<!-- Form in an iframe --> +<iframe src="https://example.org/browser/toolkit/components/passwordmgr/test/browser/form_basic.html" id="test-iframe"></iframe> + +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/browser/streamConverter_content.sjs b/toolkit/components/passwordmgr/test/browser/streamConverter_content.sjs new file mode 100644 index 000000000..84c75437e --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/streamConverter_content.sjs @@ -0,0 +1,6 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) { + response.setHeader("Content-Type", "test/content", false); +} 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 000000000..b96faf2ee --- /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() { + userField.value = "notifyu1"; + passField.value = "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 000000000..2dc96b4fd --- /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() { + passField.value = "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 000000000..cf3df5275 --- /dev/null +++ b/toolkit/components/passwordmgr/test/browser/subtst_notifications_11.html @@ -0,0 +1,25 @@ +<!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 | +[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 000000000..2e8e4135c --- /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 '?'). + [username, password] = window.location.search.substring(1).split('|'); + userField.value = username; + passField.value = 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_2.html b/toolkit/components/passwordmgr/test/browser/subtst_notifications_2.html new file mode 100644 index 000000000..72651d6c1 --- /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() { + userField.value = "notifyu1"; + passField.value = "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 000000000..7ddbf0851 --- /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() { + pass.value = "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 000000000..893f18724 --- /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() { + userField.value = "notifyu1"; + passField.value = "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 000000000..291e735d0 --- /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() { + userField.value = "notifyu1"; + passField.value = "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 000000000..63df3a42d --- /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() { + userField.value = "notifyu1"; + passField.value = "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 000000000..72a3df95f --- /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() { + userField.value = "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 000000000..47e23e972 --- /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() { + passField.value = "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 000000000..abeea4262 --- /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() { + userField.value = "notifyu1"; + passField.value = "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 000000000..c6f741068 --- /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() { + userField.value = ""; + passField.value = "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 000000000..d74f3bcdf --- /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() { + passField.value = "pass2"; + passConfirmField.value = "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> |